- Rule configs with typed parsers and registry; Berlin day helpers - Schema: T4 enums, Suggestion table, feed vocabulary (feedStatus as ColonyCondition with legacy text mapping), split queen source, varroa sample size; migration harness for data migrations - SuggestionsService: propose/refresh/resolve/obsolete/expire, accept/dismiss/reopen incl. bulk, lock order, concurrency e2e - SeasonService: phenology-based segments, inspection/varroa due dates, queen age; TimeEventsStep with JobPartialFailure - Frontend: per-rule settings forms with visible value sources Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
101 lines
2.9 KiB
TypeScript
101 lines
2.9 KiB
TypeScript
import { Prisma } from '../../../generated/prisma';
|
|
import type { AutomationTriggerType } from '../../../generated/prisma';
|
|
import { assertTransactionClient, type DbClient } from '../../prisma/db-client';
|
|
import { lockRowForUpdate } from '../../prisma/row-lock';
|
|
import { DEFAULT_RULES } from './default-rules';
|
|
import { ruleConfigOf } from './rule-config.registry';
|
|
import type { RuleConfigMap } from './rule-config.types';
|
|
|
|
export interface ActiveRule<K extends AutomationTriggerType> {
|
|
/** null while no row exists yet: the built-in setting applies. */
|
|
id: string | null;
|
|
active: boolean;
|
|
config: RuleConfigMap[K];
|
|
actionTemplate: string | null;
|
|
}
|
|
|
|
interface RuleRow {
|
|
id: string;
|
|
active: boolean;
|
|
config: Prisma.JsonValue;
|
|
actionTemplate: string | null;
|
|
}
|
|
|
|
const RULE_COLUMNS = {
|
|
id: true,
|
|
active: true,
|
|
config: true,
|
|
actionTemplate: true,
|
|
} as const;
|
|
|
|
function toActiveRule<K extends AutomationTriggerType>(
|
|
trigger: K,
|
|
row: RuleRow | null,
|
|
): ActiveRule<K> {
|
|
if (row === null) {
|
|
const defaults = DEFAULT_RULES[trigger];
|
|
return {
|
|
id: null,
|
|
active: defaults.active ?? true,
|
|
config: ruleConfigOf(trigger, defaults.config),
|
|
actionTemplate: defaults.actionTemplate,
|
|
};
|
|
}
|
|
return {
|
|
id: row.id,
|
|
active: row.active,
|
|
config: ruleConfigOf(trigger, row.config),
|
|
actionTemplate: row.actionTemplate,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The rule as it is in force: its row, or the built-in setting while there is
|
|
* none. The config is parsed, so every key is valid.
|
|
*/
|
|
export async function readRule<K extends AutomationTriggerType>(
|
|
db: DbClient,
|
|
trigger: K,
|
|
): Promise<ActiveRule<K>> {
|
|
const row = await db.automationRule.findUnique({
|
|
where: { triggerType: trigger },
|
|
select: RULE_COLUMNS,
|
|
});
|
|
return toActiveRule(trigger, row);
|
|
}
|
|
|
|
/**
|
|
* Creates the rule row if it is missing (without touching an existing one),
|
|
* locks it until the transaction ends and reads it. Serialises everything
|
|
* that decides "is there already an open task/suggestion" under the rule.
|
|
*/
|
|
export async function lockRuleIn<K extends AutomationTriggerType>(
|
|
tx: Prisma.TransactionClient,
|
|
trigger: K,
|
|
): Promise<ActiveRule<K> & { id: string }> {
|
|
assertTransactionClient(tx, 'lockRuleIn');
|
|
const defaults = DEFAULT_RULES[trigger];
|
|
await tx.automationRule.createMany({
|
|
data: [
|
|
{
|
|
triggerType: trigger,
|
|
active: defaults.active ?? true,
|
|
config: defaults.config as Prisma.InputJsonValue,
|
|
actionTemplate: defaults.actionTemplate,
|
|
},
|
|
],
|
|
skipDuplicates: true,
|
|
});
|
|
const { id } = await tx.automationRule.findUniqueOrThrow({
|
|
where: { triggerType: trigger },
|
|
select: { id: true },
|
|
});
|
|
await lockRowForUpdate(tx, 'AutomationRule', id);
|
|
// Read after the lock: a concurrent edit of the rule is visible now.
|
|
const row = await tx.automationRule.findUniqueOrThrow({
|
|
where: { id },
|
|
select: RULE_COLUMNS,
|
|
});
|
|
return { ...toActiveRule(trigger, row), id };
|
|
}
|