1
13import { coercePropertyWriteValue } from "@factory/shared";
14import { asJson } from "../../../lib/prisma.js";
15import { getLogger } from "../../../logger.js";
16import { FatalError } from "./classify.js";
17import { emitDomainEvent, loadProperties, resolveRecordId } from "./helpers.js";
18import { assertAutomationCanEdit } from "./permission.js";
19import type { AutomationRunContext, ExecutorDeps } from "./types.js";
20
21const log = getLogger("automation-record-write");
22
23
24export async function loadRecord(deps: ExecutorDeps, ctx: AutomationRunContext, recordId: string) {
25 const record = await deps.prisma.databaseRecord.findUnique({
26 where: { id: recordId },
27 select: { id: true, workspaceId: true, databaseId: true, pageId: true }
28 });
29 if (!record) throw new FatalError("RECORD_NOT_FOUND", `record ${recordId} not found`);
30 if (record.workspaceId !== ctx.workspaceId) {
31 throw new FatalError("PERMISSION_DENIED", "record is in a different workspace");
32 }
33 return record;
34}
35
36
41export async function writeRecordProperties(
42 deps: ExecutorDeps,
43 ctx: AutomationRunContext,
44 recordId: string,
45 propertyValues: Record<string, unknown>
46): Promise<{ recordId: string; changed: string[]; eventEmitted: boolean }> {
47 const record = await loadRecord(deps, ctx, recordId);
48 await assertAutomationCanEdit(deps.prisma, ctx.workspaceId, ctx.actorUserId, {
49 recordId,
50 pageId: record.pageId,
51 databaseId: record.databaseId
52 });
53
54 const props = await loadProperties(deps.prisma, record.databaseId);
55 const byName = new Map(props.map((p) => [p.name, p]));
56 const changed: string[] = [];
57 for (const [name, raw] of Object.entries(propertyValues)) {
58 const prop = byName.get(name);
59 if (!prop) throw new FatalError("PROPERTY_UNKNOWN", `property "${name}" not found`);
60 const envelope = coercePropertyWriteValue(prop.type as string, raw);
61 if (envelope === null) continue;
62 await deps.prisma.recordProperty.upsert({
63 where: { recordId_propertyId: { recordId, propertyId: prop.id } },
64 create: { recordId, propertyId: prop.id, value: asJson(envelope) },
65 update: { value: asJson(envelope) }
66 });
67 changed.push(name);
68 }
69
70 const emitted = await emitDomainEvent(
71 deps.redis,
72 "record.updated",
73 { workspaceId: ctx.workspaceId, databaseId: record.databaseId, recordId, changedProperties: changed },
74 ctx
75 );
76 return { recordId, changed, eventEmitted: emitted };
77}
78
79
83export async function writeRecordPropertyById(
84 deps: ExecutorDeps,
85 ctx: AutomationRunContext,
86 recordId: string,
87 propertyId: string,
88 rawValue: unknown
89): Promise<{ recordId: string; changed: string[]; eventEmitted: boolean }> {
90 const record = await loadRecord(deps, ctx, recordId);
91 const prop = await deps.prisma.databaseProperty.findFirst({
92 where: { id: propertyId, databaseId: record.databaseId, deletedAt: null },
93 select: { id: true, name: true, type: true }
94 });
95 if (!prop) throw new FatalError("PROPERTY_UNKNOWN", `property ${propertyId} not found in the record's database`);
96 return writeRecordProperties(deps, ctx, recordId, { [prop.name]: rawValue });
97}
98
99export { resolveRecordId };
100