1
14import { z } from "zod";
15import {
16 AUTOMATION_ACTION_TIMEOUT_MS,
17 AUTOMATION_DESCRIPTION_MAX,
18 AUTOMATION_MAX_ACTIONS,
19 AUTOMATION_MAX_TRIGGERS,
20 AUTOMATION_NAME_MAX,
21 validateActionDto,
22 validateConditionTree,
23 validateTriggerConfig,
24 type AutomationActionDtoInput,
25 type AutomationActionType,
26 type AutomationStatus,
27 type AutomationTriggerType
28} from "@factory/shared";
29import { ApiError } from "../../common/errors.js";
30
31
32
33
34
35const ACTION_TYPE_ENUM = z.enum([
36 "UPDATE_RECORD",
37 "CREATE_PAGE",
38 "SEND_EMAIL",
39 "SEND_WEBHOOK",
40 "CREATE_NOTIFICATION",
41 "ASSIGN_USER",
42 "CHANGE_STATUS",
43 "CALL_INTEGRATION",
44 "RUN_AI_ACTION"
45]);
46
47const ACTION_ENTRY = z
48 .object({
49 type: ACTION_TYPE_ENUM,
50 config: z.record(z.string(), z.unknown()),
51
52
53
54
55 retries: z.number().int().optional(),
56 timeoutMs: z.number().int().optional()
57 })
58 .strict();
59
60const TRIGGER_ENTRY = z
61 .object({
62 type: z.enum(["RECORD_CREATED", "RECORD_UPDATED", "PROPERTY_CHANGED", "PAGE_CREATED", "FORM_SUBMITTED", "SCHEDULE", "WEBHOOK", "MANUAL"]),
63 config: z.record(z.string(), z.unknown())
64 })
65 .strict();
66
67export const CreateAutomationDto = z
68 .object({
69 name: z.string().min(1).max(AUTOMATION_NAME_MAX),
70 description: z.string().max(AUTOMATION_DESCRIPTION_MAX).optional(),
71 databaseId: z.string().min(1).optional(),
72
73 triggers: z.array(TRIGGER_ENTRY).optional(),
74 condition: z.unknown().optional(),
75 actions: z.array(ACTION_ENTRY).optional()
76 })
77 .strict();
78export type CreateAutomationInput = z.infer<typeof CreateAutomationDto>;
79
80export const UpdateAutomationDto = z
81 .object({
82 name: z.string().min(1).max(AUTOMATION_NAME_MAX).optional(),
83 description: z.string().max(AUTOMATION_DESCRIPTION_MAX).nullable().optional(),
84 databaseId: z.string().min(1).nullable().optional()
85 })
86 .strict();
87export type UpdateAutomationInput = z.infer<typeof UpdateAutomationDto>;
88
89export const SetStatusDto = z.object({
90 status: z.enum(["DRAFT", "ACTIVE", "PAUSED", "ARCHIVED"])
91});
92export type SetStatusInput = z.infer<typeof SetStatusDto>;
93
94
95
96
97
98export const PutTriggersDto = z
99 .object({
100 triggers: z
101 .array(
102 z
103 .object({
104 type: z.enum(["RECORD_CREATED", "RECORD_UPDATED", "PROPERTY_CHANGED", "PAGE_CREATED", "FORM_SUBMITTED", "SCHEDULE", "WEBHOOK", "MANUAL"]),
105 config: z.record(z.string(), z.unknown())
106 })
107 .strict()
108 )
109 .min(1, "at least 1 trigger")
110 .max(AUTOMATION_MAX_TRIGGERS, `at most ${AUTOMATION_MAX_TRIGGERS} triggers`)
111 })
112 .strict();
113export type PutTriggersInput = z.infer<typeof PutTriggersDto>;
114
115export const PutConditionsDto = z
116 .object({
117
118 condition: z.unknown().nullable()
119 })
120 .strict();
121export type PutConditionsInput = z.infer<typeof PutConditionsDto>;
122
123export const PutActionsDto = z
124 .object({
125 actions: z
126 .array(
127 z
128 .object({
129 type: z.enum([
130 "UPDATE_RECORD",
131 "CREATE_PAGE",
132 "SEND_EMAIL",
133 "SEND_WEBHOOK",
134 "CREATE_NOTIFICATION",
135 "ASSIGN_USER",
136 "CHANGE_STATUS",
137 "CALL_INTEGRATION",
138 "RUN_AI_ACTION"
139 ]),
140 config: z.record(z.string(), z.unknown()),
141
142
143
144 retries: z.number().int().optional(),
145 timeoutMs: z.number().int().optional()
146 })
147 .strict()
148 )
149 .min(1, "at least 1 action")
150 .max(AUTOMATION_MAX_ACTIONS, `at most ${AUTOMATION_MAX_ACTIONS} actions`)
151 })
152 .strict();
153export type PutActionsInput = z.infer<typeof PutActionsDto>;
154
155
156export function assertValidTriggers(triggers: PutTriggersInput["triggers"]): void {
157 for (let i = 0; i < triggers.length; i += 1) {
158 const t = triggers[i] as { type: string; config: Record<string, unknown> };
159 const r = validateTriggerConfig(t.type as AutomationTriggerType, t.config ?? {});
160 if (!r.ok) {
161 const errs = r.errors.slice(0, 5).map((e) => ({
162 field: `triggers[${i}].config.${e.path}`,
163 issue: e.message
164 }));
165 throw ApiError.unprocessable(`triggers[${i}]: invalid trigger config.`, errs);
166 }
167 }
168}
169
170
171export function assertValidCondition(tree: unknown): void {
172 const r = validateConditionTree(tree);
173 if (!r.ok) {
174 throw ApiError.unprocessable(r.message ?? "invalid condition.", [
175 { field: "condition", issue: r.code ?? "invalid" }
176 ]);
177 }
178}
179
180
181export function assertValidActions(actions: PutActionsInput["actions"]): void {
182 for (let i = 0; i < actions.length; i += 1) {
183 const a = actions[i] as AutomationActionDtoInput;
184 const r = validateActionDto({
185 type: a.type as AutomationActionType,
186 config: a.config,
187 ...(a.retries !== undefined ? { retries: a.retries } : {}),
188 ...(a.timeoutMs !== undefined ? { timeoutMs: a.timeoutMs } : {})
189 });
190 if (!r.ok) {
191 const errs = r.errors.slice(0, 5).map((e) => ({
192 field: `actions[${i}].${e.path}`,
193 issue: e.message
194 }));
195 throw ApiError.unprocessable(`actions[${i}]: invalid action config.`, errs);
196 }
197 }
198}
199
200
201
202
203
204export const ListRunsQuery = z.object({
205 status: z.enum(["PENDING", "RUNNING", "SUCCEEDED", "FAILED", "DEAD_LETTER"]).optional(),
206 cursor: z.string().optional(),
207 limit: z.coerce.number().int().min(1).max(200).default(50)
208});
209export type ListRunsQueryInput = z.infer<typeof ListRunsQuery>;
210
211export const RunInputDto = z
212 .object({
213
214 input: z.unknown().optional()
215 })
216 .strict();
217export type RunInputInput = z.infer<typeof RunInputDto>;
218
219export { AUTOMATION_ACTION_TIMEOUT_MS };
220export type { AutomationStatus };
221