1
11import { randomUUID } from "node:crypto";
12import { coercePropertyWriteValue } from "@factory/shared";
13import { asJson } from "../../../lib/prisma.js";
14import { getLogger } from "../../../logger.js";
15import { FatalError } from "./classify.js";
16import { emitDomainEvent, interpolateConfig, loadProperties, runWithTimeout, type PropertyMetaLite } from "./helpers.js";
17import { assertAutomationCanEdit } from "./permission.js";
18import type { ActionExecutor, ActionResult } from "./types.js";
19
20const log = getLogger("automation-create-page");
21
22const executor: ActionExecutor = {
23 type: "CREATE_PAGE",
24 async execute(cfg, ctx, deps): Promise<ActionResult> {
25 const warnings: string[] = [];
26 const title = interpolateConfig(cfg.title as string, ctx.vars, 200, "title", warnings);
27
28
29
30 let parentId: string | null = null;
31 if (cfg.parentId === "from_trigger_page") {
32 if (!ctx.recordId) throw new FatalError("NO_TRIGGER_RECORD", "CREATE_PAGE parent `from_trigger_page` but the event has no record");
33 const rec = await deps.prisma.databaseRecord.findUnique({ where: { id: ctx.recordId }, select: { pageId: true } });
34 parentId = rec?.pageId ?? null;
35 } else if (typeof cfg.parentId === "string" && cfg.parentId.length > 0) {
36 parentId = cfg.parentId;
37 }
38 if (parentId) {
39 const parent = await deps.prisma.page.findUnique({ where: { id: parentId }, select: { id: true, workspaceId: true } });
40 if (!parent || parent.workspaceId !== ctx.workspaceId) {
41 throw new FatalError("PERMISSION_DENIED", "parent page not found in the workspace");
42 }
43 }
44
45
46 const databaseId = typeof cfg.databaseId === "string" ? cfg.databaseId : null;
47 if (databaseId) {
48 const db = await deps.prisma.database.findUnique({ where: { id: databaseId }, select: { id: true, workspaceId: true } });
49 if (!db || db.workspaceId !== ctx.workspaceId) {
50 throw new FatalError("PERMISSION_DENIED", "database not found in the workspace");
51 }
52 await assertAutomationCanEdit(deps.prisma, ctx.workspaceId, ctx.actorUserId, { databaseId });
53 }
54
55 const propertyValues = (cfg.propertyValues ?? {}) as Record<string, unknown>;
56 const props = databaseId ? await loadProperties(deps.prisma, databaseId) : [];
57 const envs = coerceAll(props, propertyValues);
58
59 const created = await runWithTimeout(
60 30_000,
61 async () => {
62 const id = await deps.prisma.$transaction(async (tx: any) => {
63 const page = await tx.page.create({
64 data: {
65 workspaceId: ctx.workspaceId,
66 parentId,
67 title,
68 position: "a0" + randomUUID().slice(0, 8),
69 createdBy: ctx.actorUserId
70 },
71 select: { id: true }
72 });
73 let recordId: string | null = null;
74 if (databaseId) {
75 const rec = await tx.databaseRecord.create({
76 data: {
77 workspaceId: ctx.workspaceId,
78 databaseId,
79 pageId: page.id,
80 position: "a0" + randomUUID().slice(0, 8),
81 createdBy: ctx.actorUserId
82 },
83 select: { id: true }
84 });
85 recordId = rec.id;
86 for (const [propId, env] of Object.entries(envs)) {
87 await tx.recordProperty.create({ data: { recordId: rec.id, propertyId: propId, value: asJson(env) } });
88 }
89 }
90 return { pageId: page.id, recordId };
91 });
92 return id;
93 }
94 );
95
96 const emitted = await emitDomainEvent(
97 deps.redis,
98 "page.created",
99 { pageId: created.pageId, workspaceId: ctx.workspaceId, databaseId: databaseId ?? undefined, recordId: created.recordId ?? undefined },
100 ctx
101 );
102
103 return {
104 output: { pageId: created.pageId, recordId: created.recordId, title, eventEmitted: emitted },
105 emittedEvents: emitted ? ["page.created"] : [],
106 audit: { action: "automation.page_created", resourceType: "page", resourceId: created.pageId }
107 };
108 }
109};
110
111
112function coerceAll(props: PropertyMetaLite[], raw: Record<string, unknown>): Record<string, unknown> {
113 const out: Record<string, unknown> = {};
114 for (const [name, value] of Object.entries(raw)) {
115 const prop = props.find((p) => p.name === name);
116 if (!prop) throw new FatalError("PROPERTY_UNKNOWN", `property "${name}" not found`);
117 out[prop.id] = coercePropertyWriteValue(prop.type as string, value);
118 }
119 return out;
120}
121
122export default executor;
123