Files
ApiCore/ApiCore-Backend/src/automation/rule-config/rule-config.reader.ts
T
Nick SzaunigandClaude Opus 5.5 d99a697739 feat: T4 waves 1-2 - rule configs, schema, suggestions core, season, rule forms
- 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]>
2026-10-14 13:42:32 +02:00

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 };
}