@dev.smartpricing/message-composer-utils 4.5.1-next.2 → 5.0.0-automations.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/schemas/automation-catalog.js +47 -0
- package/dist/schemas/automation-catalog.js.map +1 -0
- package/dist/schemas/automation.js +191 -0
- package/dist/schemas/automation.js.map +1 -0
- package/dist/schemas/condition.js +17 -0
- package/dist/schemas/condition.js.map +1 -0
- package/dist/schemas/condition.test.js +73 -0
- package/dist/schemas/condition.test.js.map +1 -0
- package/dist/schemas/index.js +3 -0
- package/dist/schemas/index.js.map +1 -1
- package/dist/schemas/message-group.js +10 -8
- package/dist/schemas/message-group.js.map +1 -1
- package/dist/types/app.js +3 -0
- package/dist/types/app.js.map +1 -1
- package/dist/types/automation-catalog.js +16 -0
- package/dist/types/automation-catalog.js.map +1 -0
- package/dist/types/automation.js +9 -0
- package/dist/types/automation.js.map +1 -0
- package/dist/types/condition.js +51 -0
- package/dist/types/condition.js.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -1
- package/package.json +1 -1
- package/src/schemas/automation-catalog.ts +62 -0
- package/src/schemas/automation.ts +230 -0
- package/src/schemas/condition.test.ts +92 -0
- package/src/schemas/condition.ts +27 -0
- package/src/schemas/index.ts +3 -0
- package/src/schemas/message-group.ts +15 -11
- package/src/types/app.ts +3 -0
- package/src/types/automation-catalog.ts +79 -0
- package/src/types/automation.ts +216 -0
- package/src/types/condition.ts +114 -0
- package/src/types/index.ts +3 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
AUTOMATION_ACTION_STATUSES,
|
|
4
|
+
AUTOMATION_CHANNELS,
|
|
5
|
+
AUTOMATION_MESSAGE_GROUP_KEPT_REASONS,
|
|
6
|
+
Automation,
|
|
7
|
+
AutomationAction,
|
|
8
|
+
AutomationActionInput,
|
|
9
|
+
AutomationActionsInvalidDetails,
|
|
10
|
+
AutomationDeleteResult,
|
|
11
|
+
AutomationPartlySavedDetails,
|
|
12
|
+
CloneAutomationBody,
|
|
13
|
+
CreateAutomationBody,
|
|
14
|
+
MESSAGE_GROUP_PRODUCTS,
|
|
15
|
+
UpdateAutomationBody,
|
|
16
|
+
} from "../types/index.js";
|
|
17
|
+
import { conditionOperatorSchema, conditionSchema } from "./condition.js";
|
|
18
|
+
import { messageGroupValidationErrorsSchema } from "./message-group.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* GET /automations: the Automations of one Organization for one Product.
|
|
22
|
+
* Both are required: the Automation Engine lists one Organization at a time
|
|
23
|
+
* (ADR 0016, a deviation from ADR 0014) and the Product is never read from
|
|
24
|
+
* the `sm-product` header (ADR 0011).
|
|
25
|
+
*/
|
|
26
|
+
export const automationListQuerySchema = z.object({
|
|
27
|
+
organization_id: z.uuid(),
|
|
28
|
+
product: z.enum(MESSAGE_GROUP_PRODUCTS),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/** GET /automations/:id: an Automation of another Product is not found. */
|
|
32
|
+
export const automationQuerySchema = z.object({
|
|
33
|
+
product: z.enum(MESSAGE_GROUP_PRODUCTS),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* DELETE /automations/:id: the Message Groups to delete with the Automation,
|
|
38
|
+
* one repeated `message_group_ids` each (ADR 0016). A single one reaches the
|
|
39
|
+
* schema as a string; the route always reads a list. The Product is in the
|
|
40
|
+
* query too: a DELETE has no body to name it in.
|
|
41
|
+
*/
|
|
42
|
+
export const deleteAutomationQuerySchema = automationQuerySchema.extend({
|
|
43
|
+
message_group_ids: z
|
|
44
|
+
.union([z.uuid(), z.array(z.uuid())])
|
|
45
|
+
.optional()
|
|
46
|
+
.transform((ids) => (ids === undefined ? [] : [ids].flat())),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const messageGroupRef = { id: z.uuid(), name: z.string() };
|
|
50
|
+
|
|
51
|
+
/** 200 of a DELETE /automations/:id that selected Message Groups. */
|
|
52
|
+
export const automationDeleteResponseSchema = z.object({
|
|
53
|
+
deleted: z.array(z.object(messageGroupRef)),
|
|
54
|
+
kept: z.array(
|
|
55
|
+
z.object({
|
|
56
|
+
...messageGroupRef,
|
|
57
|
+
reason: z.enum(AUTOMATION_MESSAGE_GROUP_KEPT_REASONS),
|
|
58
|
+
}),
|
|
59
|
+
),
|
|
60
|
+
}) satisfies z.ZodType<AutomationDeleteResult>;
|
|
61
|
+
|
|
62
|
+
/** Automation ids are the engine's (`aut_<uuid>`), not UUIDs. */
|
|
63
|
+
export const automationIdParamsSchema = z.object({
|
|
64
|
+
id: z.string().min(1),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export const automationActionStatusSchema = z.enum(AUTOMATION_ACTION_STATUSES);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An Action as served: the engine's `config` as stored, loose because the
|
|
71
|
+
* engine owns its shape (ADR 0016).
|
|
72
|
+
*/
|
|
73
|
+
export const automationActionSchema = z.object({
|
|
74
|
+
id: z.string(),
|
|
75
|
+
type: z.string(),
|
|
76
|
+
status: automationActionStatusSchema,
|
|
77
|
+
created_at: z.string(),
|
|
78
|
+
updated_at: z.string(),
|
|
79
|
+
config: z.looseObject({
|
|
80
|
+
template: z.string(),
|
|
81
|
+
channel: z.string(),
|
|
82
|
+
recipient: z.string(),
|
|
83
|
+
delay: z.number(),
|
|
84
|
+
}),
|
|
85
|
+
}) satisfies z.ZodType<AutomationAction>;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* An Automation with its Actions embedded (ADR 0016). `settings` and
|
|
89
|
+
* `metadata` keep the engine's shapes, loose so a key the engine adds is
|
|
90
|
+
* served, not stripped; `active` is derived by the backend.
|
|
91
|
+
*/
|
|
92
|
+
export const automationResponseSchema = z.object({
|
|
93
|
+
id: z.string(),
|
|
94
|
+
organization_id: z.uuid(),
|
|
95
|
+
product: z.enum(MESSAGE_GROUP_PRODUCTS),
|
|
96
|
+
name: z.string(),
|
|
97
|
+
created_at: z.string(),
|
|
98
|
+
updated_at: z.string(),
|
|
99
|
+
active: z.boolean(),
|
|
100
|
+
settings: z.looseObject({
|
|
101
|
+
trigger: z.looseObject({
|
|
102
|
+
source: z.union([z.string(), z.array(z.string())]).optional(),
|
|
103
|
+
type: z.union([z.string(), z.array(z.string())]),
|
|
104
|
+
}),
|
|
105
|
+
conditions: z.array(
|
|
106
|
+
z.looseObject({
|
|
107
|
+
field: z.string(),
|
|
108
|
+
operator: z.string(),
|
|
109
|
+
value: z.unknown().optional(),
|
|
110
|
+
}),
|
|
111
|
+
),
|
|
112
|
+
}),
|
|
113
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
114
|
+
actions: z.array(automationActionSchema),
|
|
115
|
+
}) satisfies z.ZodType<Automation>;
|
|
116
|
+
|
|
117
|
+
export const automationListResponseSchema = z.array(automationResponseSchema);
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* An Action as submitted: `config` keeps the engine's shape, loose for the
|
|
121
|
+
* rest of the Schedule. The recipient and the sender are not the client's
|
|
122
|
+
* to choose (ADR 0017).
|
|
123
|
+
*/
|
|
124
|
+
export const automationActionInputSchema = z.object({
|
|
125
|
+
status: automationActionStatusSchema,
|
|
126
|
+
config: z.looseObject({
|
|
127
|
+
template: z.string().min(1),
|
|
128
|
+
channel: z.enum(AUTOMATION_CHANNELS),
|
|
129
|
+
delay: z.number(),
|
|
130
|
+
dateTimeReference: z.string().min(1),
|
|
131
|
+
}),
|
|
132
|
+
}) satisfies z.ZodType<AutomationActionInput>;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* POST /automations: an Automation and its Actions in one request
|
|
136
|
+
* (ADR 0016). The Organization and the Product are explicit in the body
|
|
137
|
+
* (ADR 0014, ADR 0011).
|
|
138
|
+
*/
|
|
139
|
+
export const createAutomationBodySchema = z.object({
|
|
140
|
+
organization_id: z.uuid(),
|
|
141
|
+
product: z.enum(MESSAGE_GROUP_PRODUCTS),
|
|
142
|
+
name: z.string().trim().min(1),
|
|
143
|
+
settings: z.looseObject({
|
|
144
|
+
trigger: z.looseObject({ type: z.string().min(1) }),
|
|
145
|
+
conditions: z.array(conditionSchema),
|
|
146
|
+
}),
|
|
147
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
148
|
+
actions: z.array(automationActionInputSchema).min(1),
|
|
149
|
+
}) satisfies z.ZodType<CreateAutomationBody>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* PUT /automations/:id: the full list of Actions, each stored one named by
|
|
153
|
+
* its `id` (ADR 0016). The Product is explicit (ADR 0011); the Organization
|
|
154
|
+
* is the stored one and is not sent.
|
|
155
|
+
*/
|
|
156
|
+
export const updateAutomationBodySchema = createAutomationBodySchema
|
|
157
|
+
.omit({ organization_id: true })
|
|
158
|
+
.extend({
|
|
159
|
+
actions: z
|
|
160
|
+
.array(
|
|
161
|
+
automationActionInputSchema.extend({
|
|
162
|
+
id: z.string().min(1).optional(),
|
|
163
|
+
}),
|
|
164
|
+
)
|
|
165
|
+
.min(1),
|
|
166
|
+
}) satisfies z.ZodType<UpdateAutomationBody>;
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* POST /automations/:id/clone: the name of the copy, and the Product as on
|
|
170
|
+
* every write (ADR 0011). Everything else is read from the source.
|
|
171
|
+
*/
|
|
172
|
+
export const cloneAutomationBodySchema = createAutomationBodySchema.pick({
|
|
173
|
+
product: true,
|
|
174
|
+
name: true,
|
|
175
|
+
}) satisfies z.ZodType<CloneAutomationBody>;
|
|
176
|
+
|
|
177
|
+
/** `details` of the 502 of an update that was saved only in part. */
|
|
178
|
+
export const automationPartlySavedDetailsSchema = z.object({
|
|
179
|
+
automation: automationResponseSchema,
|
|
180
|
+
action_ids: z.array(z.string().nullable()),
|
|
181
|
+
}) satisfies z.ZodType<AutomationPartlySavedDetails>;
|
|
182
|
+
|
|
183
|
+
const position = z.number().int().nonnegative();
|
|
184
|
+
|
|
185
|
+
const actionProblem = { target: z.literal("action"), position };
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* `details` of the 422 `AUTOMATION_ACTIONS_INVALID`: a flat list, each
|
|
189
|
+
* entry naming its `target`. A new kind of problem is a new entry shape,
|
|
190
|
+
* never a change to an existing one.
|
|
191
|
+
*/
|
|
192
|
+
export const automationActionsInvalidDetailsSchema = z.object({
|
|
193
|
+
problems: z.array(
|
|
194
|
+
z.union([
|
|
195
|
+
z.object({
|
|
196
|
+
target: z.literal("trigger"),
|
|
197
|
+
reason: z.literal("not_in_catalog"),
|
|
198
|
+
type: z.string(),
|
|
199
|
+
}),
|
|
200
|
+
z.object({
|
|
201
|
+
target: z.literal("condition"),
|
|
202
|
+
position,
|
|
203
|
+
reason: z.enum(["field_not_in_catalog", "operator_not_allowed"]),
|
|
204
|
+
field: z.string(),
|
|
205
|
+
operator: conditionOperatorSchema,
|
|
206
|
+
}),
|
|
207
|
+
z.object({
|
|
208
|
+
...actionProblem,
|
|
209
|
+
reason: z.enum(["not_found", "other_organization"]),
|
|
210
|
+
message_group_id: z.string(),
|
|
211
|
+
}),
|
|
212
|
+
z.object({
|
|
213
|
+
...actionProblem,
|
|
214
|
+
reason: z.literal("invalid"),
|
|
215
|
+
message_group_id: z.string(),
|
|
216
|
+
errors: messageGroupValidationErrorsSchema,
|
|
217
|
+
}),
|
|
218
|
+
z.object({
|
|
219
|
+
...actionProblem,
|
|
220
|
+
reason: z.literal("date_reference_not_in_catalog"),
|
|
221
|
+
date_reference: z.string(),
|
|
222
|
+
}),
|
|
223
|
+
z.object({
|
|
224
|
+
...actionProblem,
|
|
225
|
+
reason: z.literal("channel_not_in_catalog"),
|
|
226
|
+
channel: z.enum(AUTOMATION_CHANNELS),
|
|
227
|
+
}),
|
|
228
|
+
]),
|
|
229
|
+
),
|
|
230
|
+
}) satisfies z.ZodType<AutomationActionsInvalidDetails>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
CONDITION_OPERATORS_BY_FIELD_TYPE,
|
|
4
|
+
conditionFieldTypeSchema,
|
|
5
|
+
conditionOperatorSchema,
|
|
6
|
+
conditionSchema,
|
|
7
|
+
operatorFitsFieldType,
|
|
8
|
+
} from "./condition.js";
|
|
9
|
+
|
|
10
|
+
describe("conditionSchema", () => {
|
|
11
|
+
it.each([
|
|
12
|
+
"eq",
|
|
13
|
+
"neq",
|
|
14
|
+
"gt",
|
|
15
|
+
"gte",
|
|
16
|
+
"lt",
|
|
17
|
+
"lte",
|
|
18
|
+
"in",
|
|
19
|
+
"not_in",
|
|
20
|
+
"contains",
|
|
21
|
+
"exclude",
|
|
22
|
+
"exists",
|
|
23
|
+
])("accepts the engine operator %s", (operator) => {
|
|
24
|
+
const condition = { field: "guest.language", operator, value: "it" };
|
|
25
|
+
expect(conditionSchema.parse(condition)).toEqual(condition);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("knows eleven operators and no other", () => {
|
|
29
|
+
expect(conditionOperatorSchema.options).toHaveLength(11);
|
|
30
|
+
expect(
|
|
31
|
+
conditionSchema.safeParse({ field: "status", operator: "is", value: 1 })
|
|
32
|
+
.success,
|
|
33
|
+
).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("accepts a Condition with no value", () => {
|
|
37
|
+
const condition = { field: "totalPrice", operator: "exists" };
|
|
38
|
+
expect(conditionSchema.parse(condition)).toEqual(condition);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("keeps the value as it is, whatever its type", () => {
|
|
42
|
+
const condition = {
|
|
43
|
+
field: "guest.marketingConsent",
|
|
44
|
+
operator: "in",
|
|
45
|
+
value: [1, 2],
|
|
46
|
+
};
|
|
47
|
+
expect(conditionSchema.parse(condition)).toEqual(condition);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("rejects a Condition with no field", () => {
|
|
51
|
+
expect(
|
|
52
|
+
conditionSchema.safeParse({ field: "", operator: "eq", value: 1 })
|
|
53
|
+
.success,
|
|
54
|
+
).toBe(false);
|
|
55
|
+
expect(
|
|
56
|
+
conditionSchema.safeParse({ operator: "eq", value: 1 }).success,
|
|
57
|
+
).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("the field types and their operators (ADR 0018)", () => {
|
|
62
|
+
it("has six field types", () => {
|
|
63
|
+
expect([...conditionFieldTypeSchema.options].sort()).toEqual([
|
|
64
|
+
"date",
|
|
65
|
+
"multiselect",
|
|
66
|
+
"number",
|
|
67
|
+
"price",
|
|
68
|
+
"select",
|
|
69
|
+
"text",
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("gives each field type the operators of the ADR's table", () => {
|
|
74
|
+
const comparison = ["eq", "neq", "gt", "gte", "lt", "lte", "exists"];
|
|
75
|
+
expect(CONDITION_OPERATORS_BY_FIELD_TYPE).toEqual({
|
|
76
|
+
select: ["eq", "neq", "exists"],
|
|
77
|
+
multiselect: ["in", "not_in", "exists"],
|
|
78
|
+
number: comparison,
|
|
79
|
+
price: comparison,
|
|
80
|
+
date: comparison,
|
|
81
|
+
text: ["eq", "neq", "contains", "exclude", "exists"],
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("tells whether an operator fits a field type", () => {
|
|
86
|
+
expect(operatorFitsFieldType("gte", "price")).toBe(true);
|
|
87
|
+
expect(operatorFitsFieldType("in", "multiselect")).toBe(true);
|
|
88
|
+
expect(operatorFitsFieldType("in", "select")).toBe(false);
|
|
89
|
+
expect(operatorFitsFieldType("contains", "number")).toBe(false);
|
|
90
|
+
expect(operatorFitsFieldType("exists", "text")).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
CONDITION_FIELD_TYPES,
|
|
4
|
+
CONDITION_OPERATORS,
|
|
5
|
+
Condition,
|
|
6
|
+
} from "../types/index.js";
|
|
7
|
+
|
|
8
|
+
export const conditionOperatorSchema = z.enum(CONDITION_OPERATORS);
|
|
9
|
+
|
|
10
|
+
export const conditionFieldTypeSchema = z.enum(CONDITION_FIELD_TYPES);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A Condition in the Automation Engine's shape (ADR 0018). The value is not
|
|
14
|
+
* checked: what fits depends on the field, which only a catalog knows.
|
|
15
|
+
*/
|
|
16
|
+
export const conditionSchema = z.object({
|
|
17
|
+
field: z.string().min(1),
|
|
18
|
+
operator: conditionOperatorSchema,
|
|
19
|
+
value: z.unknown().optional(),
|
|
20
|
+
}) satisfies z.ZodType<Condition>;
|
|
21
|
+
|
|
22
|
+
// The type-to-operator table sits with the vocabulary it is made of, so the
|
|
23
|
+
// layer can read it without the schemas.
|
|
24
|
+
export {
|
|
25
|
+
CONDITION_OPERATORS_BY_FIELD_TYPE,
|
|
26
|
+
operatorFitsFieldType,
|
|
27
|
+
} from "../types/index.js";
|
package/src/schemas/index.ts
CHANGED
|
@@ -13,3 +13,6 @@ export * from "./message-compilation.js";
|
|
|
13
13
|
export * from "./meta-integration.js";
|
|
14
14
|
export * from "./onboarding.js";
|
|
15
15
|
export * from "./tracking.js";
|
|
16
|
+
export * from "./automation.js";
|
|
17
|
+
export * from "./condition.js";
|
|
18
|
+
export * from "./automation-catalog.js";
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
DuplicateMessageGroupBody,
|
|
4
4
|
ListMessageGroupsQuery,
|
|
5
5
|
MessageGroupFromDb,
|
|
6
|
+
MessageGroupValidationErrors,
|
|
6
7
|
MessageGroupWithMessageSummary,
|
|
7
8
|
MESSAGE_GROUP_CATEGORIES,
|
|
8
9
|
MESSAGE_GROUP_LIST_SORT,
|
|
@@ -123,17 +124,20 @@ export const messageGroupSummaryListResponseSchema = paginatedResponseSchema(
|
|
|
123
124
|
messageGroupWithMessageSummaryResponseSchema,
|
|
124
125
|
);
|
|
125
126
|
|
|
127
|
+
/** The errors of the Message Group validation, by what they are about. */
|
|
128
|
+
export const messageGroupValidationErrorsSchema = z.object({
|
|
129
|
+
status: z.array(z.string()),
|
|
130
|
+
languages: z.array(z.string()),
|
|
131
|
+
content: z.record(
|
|
132
|
+
z.string(),
|
|
133
|
+
z.object({
|
|
134
|
+
type: z.enum(MESSAGE_RENDER_TYPES),
|
|
135
|
+
errors: z.array(z.string()),
|
|
136
|
+
}),
|
|
137
|
+
),
|
|
138
|
+
}) satisfies z.ZodType<MessageGroupValidationErrors>;
|
|
139
|
+
|
|
126
140
|
export const messageGroupValidationResponseSchema = z.object({
|
|
127
141
|
valid: z.boolean(),
|
|
128
|
-
errors:
|
|
129
|
-
status: z.array(z.string()),
|
|
130
|
-
languages: z.array(z.string()),
|
|
131
|
-
content: z.record(
|
|
132
|
-
z.string(),
|
|
133
|
-
z.object({
|
|
134
|
-
type: z.string(),
|
|
135
|
-
errors: z.array(z.string()),
|
|
136
|
-
}),
|
|
137
|
-
),
|
|
138
|
-
}),
|
|
142
|
+
errors: messageGroupValidationErrorsSchema,
|
|
139
143
|
});
|
package/src/types/app.ts
CHANGED
|
@@ -21,6 +21,9 @@ export const errorCodes = {
|
|
|
21
21
|
UNSUPPORTED_RENDER_TYPE: "UNSUPPORTED_RENDER_TYPE",
|
|
22
22
|
NOT_IMPLEMENTED: "NOT_IMPLEMENTED",
|
|
23
23
|
TRANSLATION_TOKEN_MISMATCH: "TRANSLATION_TOKEN_MISMATCH",
|
|
24
|
+
UPSTREAM_ERROR: "UPSTREAM_ERROR",
|
|
25
|
+
AUTOMATION_ACTIONS_INVALID: "AUTOMATION_ACTIONS_INVALID",
|
|
26
|
+
SYSTEM_AUTOMATION_NOT_DELETABLE: "SYSTEM_AUTOMATION_NOT_DELETABLE",
|
|
24
27
|
} as const;
|
|
25
28
|
|
|
26
29
|
type ErrorCode = keyof typeof errorCodes;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { Condition, ConditionFieldType } from "./condition.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The Automation Catalog (ADR 0017): what an Automation of a Product can be
|
|
5
|
+
* built from. The backend keeps one config per Product with a label per
|
|
6
|
+
* language and serves it resolved to the requested one.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** The languages a catalog holds a label in. */
|
|
10
|
+
export const AUTOMATION_CATALOG_LANGUAGES = [
|
|
11
|
+
"en",
|
|
12
|
+
"it",
|
|
13
|
+
"de",
|
|
14
|
+
"es",
|
|
15
|
+
"fr",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export type AutomationCatalogLanguage =
|
|
19
|
+
(typeof AUTOMATION_CATALOG_LANGUAGES)[number];
|
|
20
|
+
|
|
21
|
+
export const AUTOMATION_CHANNELS = ["email", "whatsapp"] as const;
|
|
22
|
+
|
|
23
|
+
export type AutomationChannel = (typeof AUTOMATION_CHANNELS)[number];
|
|
24
|
+
|
|
25
|
+
export const DATE_REFERENCE_DIRECTIONS = ["after", "before"] as const;
|
|
26
|
+
|
|
27
|
+
export type DateReferenceDirection = (typeof DATE_REFERENCE_DIRECTIONS)[number];
|
|
28
|
+
|
|
29
|
+
/** A label in every language of the catalog, as the backend keeps it. */
|
|
30
|
+
export type AutomationCatalogLabels = Record<AutomationCatalogLanguage, string>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The shapes below take the type of a label: a string as the route serves
|
|
34
|
+
* them, in one language, or `AutomationCatalogLabels` as the backend keeps
|
|
35
|
+
* them.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
export interface ConditionField<Label = string> {
|
|
39
|
+
/** Dot path into the Trigger Event's data. */
|
|
40
|
+
key: string;
|
|
41
|
+
label: Label;
|
|
42
|
+
type: ConditionFieldType;
|
|
43
|
+
/**
|
|
44
|
+
* The name of the option list the Host supplies for this field. The
|
|
45
|
+
* catalog holds no option of its own.
|
|
46
|
+
*/
|
|
47
|
+
option_list?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface DateReference<Label = string> {
|
|
51
|
+
key: string;
|
|
52
|
+
/** The directions a delay can count in, in display order. */
|
|
53
|
+
directions: { direction: DateReferenceDirection; label: Label }[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A Channel a Trigger can send over, with whom its Actions send to and from. */
|
|
57
|
+
export interface AutomationCatalogChannel {
|
|
58
|
+
channel: AutomationChannel;
|
|
59
|
+
recipient: string;
|
|
60
|
+
sender: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface AutomationTrigger<Label = string> {
|
|
64
|
+
type: string;
|
|
65
|
+
label: Label;
|
|
66
|
+
date_references: DateReference<Label>[];
|
|
67
|
+
condition_fields: ConditionField<Label>[];
|
|
68
|
+
default_conditions: Condition[];
|
|
69
|
+
channels: AutomationCatalogChannel[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface AutomationCatalog<Label = string> {
|
|
73
|
+
triggers: AutomationTrigger<Label>[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `catalog` is null for a Product that has no Automation Catalog. */
|
|
77
|
+
export interface AutomationCatalogResponse {
|
|
78
|
+
catalog: AutomationCatalog | null;
|
|
79
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import type { AutomationChannel } from "./automation-catalog.js";
|
|
2
|
+
import type { Condition, ConditionOperator } from "./condition.js";
|
|
3
|
+
import type {
|
|
4
|
+
MessageGroupProduct,
|
|
5
|
+
MessageGroupValidationErrors,
|
|
6
|
+
} from "./message.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Automations as the composer's API serves them (ADR 0016). The envelope
|
|
10
|
+
* speaks the composer's names; `settings`, `metadata` and an Action's
|
|
11
|
+
* `config` keep the Automation Engine's shapes, open to keys it adds.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const AUTOMATION_ACTION_STATUSES = ["active", "inactive"] as const;
|
|
15
|
+
|
|
16
|
+
export type AutomationActionStatus =
|
|
17
|
+
(typeof AUTOMATION_ACTION_STATUSES)[number];
|
|
18
|
+
|
|
19
|
+
export interface AutomationCondition {
|
|
20
|
+
field: string;
|
|
21
|
+
operator: string;
|
|
22
|
+
value?: unknown;
|
|
23
|
+
[key: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AutomationSettings {
|
|
27
|
+
trigger: {
|
|
28
|
+
source?: string | string[];
|
|
29
|
+
type: string | string[];
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
conditions: AutomationCondition[];
|
|
33
|
+
[key: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The engine's Action config: `template` is the id of a Message Group. */
|
|
37
|
+
export interface AutomationActionConfig {
|
|
38
|
+
template: string;
|
|
39
|
+
channel: string;
|
|
40
|
+
recipient: string;
|
|
41
|
+
delay: number;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AutomationAction {
|
|
46
|
+
id: string;
|
|
47
|
+
type: string;
|
|
48
|
+
status: AutomationActionStatus;
|
|
49
|
+
created_at: string;
|
|
50
|
+
updated_at: string;
|
|
51
|
+
config: AutomationActionConfig;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface Automation {
|
|
55
|
+
id: string;
|
|
56
|
+
organization_id: string;
|
|
57
|
+
product: MessageGroupProduct;
|
|
58
|
+
name: string;
|
|
59
|
+
created_at: string;
|
|
60
|
+
updated_at: string;
|
|
61
|
+
/** Derived: at least one Active Action. */
|
|
62
|
+
active: boolean;
|
|
63
|
+
settings: AutomationSettings;
|
|
64
|
+
metadata?: Record<string, unknown>;
|
|
65
|
+
actions: AutomationAction[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* An Action as a client submits it (ADR 0016). It has no `type`, recipient
|
|
70
|
+
* or sender to choose: the backend writes them at save (ADR 0017), and
|
|
71
|
+
* ignores a `recipient` or `sender` sent in `config`.
|
|
72
|
+
*/
|
|
73
|
+
export interface AutomationActionInput {
|
|
74
|
+
status: AutomationActionStatus;
|
|
75
|
+
config: {
|
|
76
|
+
/** The id of the Message Group the Action sends. */
|
|
77
|
+
template: string;
|
|
78
|
+
channel: AutomationChannel;
|
|
79
|
+
delay: number;
|
|
80
|
+
/** The key of the Action's Date Reference. */
|
|
81
|
+
dateTimeReference: string;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One Trigger, with no `source`: the backend sets it from the Product. */
|
|
87
|
+
export interface AutomationSettingsInput {
|
|
88
|
+
trigger: { type: string; [key: string]: unknown };
|
|
89
|
+
conditions: Condition[];
|
|
90
|
+
[key: string]: unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Body of `POST /automations`: an Automation with its Actions. */
|
|
94
|
+
export interface CreateAutomationBody {
|
|
95
|
+
organization_id: string;
|
|
96
|
+
product: MessageGroupProduct;
|
|
97
|
+
name: string;
|
|
98
|
+
settings: AutomationSettingsInput;
|
|
99
|
+
metadata?: Record<string, unknown>;
|
|
100
|
+
actions: AutomationActionInput[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* An Action in the full list an update submits (ADR 0016): without an `id`
|
|
105
|
+
* it is new, with one it is the stored Action of that id.
|
|
106
|
+
*/
|
|
107
|
+
export interface AutomationActionUpdateInput extends AutomationActionInput {
|
|
108
|
+
id?: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Body of `PUT /automations/:id`. It names no Organization: an Automation
|
|
113
|
+
* never changes it. `metadata` is merged into what is stored.
|
|
114
|
+
*/
|
|
115
|
+
export interface UpdateAutomationBody {
|
|
116
|
+
product: MessageGroupProduct;
|
|
117
|
+
name: string;
|
|
118
|
+
settings: AutomationSettingsInput;
|
|
119
|
+
metadata?: Record<string, unknown>;
|
|
120
|
+
/** Every Action the Automation keeps: a stored one left out is deleted. */
|
|
121
|
+
actions: AutomationActionUpdateInput[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Body of `POST /automations/:id/clone` (ADR 0016). The copy is made from
|
|
126
|
+
* the Automation as stored, never from what a client holds: the request
|
|
127
|
+
* only names it.
|
|
128
|
+
*/
|
|
129
|
+
export interface CloneAutomationBody {
|
|
130
|
+
product: MessageGroupProduct;
|
|
131
|
+
name: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* `details` of the 502 of an update that failed after its first write
|
|
136
|
+
* (ADR 0016): nothing is rolled back, so this is what a retry starts from.
|
|
137
|
+
*/
|
|
138
|
+
export interface AutomationPartlySavedDetails {
|
|
139
|
+
/** The Automation as it now stands in the Automation Engine. */
|
|
140
|
+
automation: Automation;
|
|
141
|
+
/**
|
|
142
|
+
* The id of each submitted Action, by position: `null` for a new Action
|
|
143
|
+
* that was not created. A retry that sends these ids creates nothing twice.
|
|
144
|
+
*/
|
|
145
|
+
action_ids: Array<string | null>;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* One thing that keeps an Automation from being saved. `target` names what
|
|
150
|
+
* it is about; an Action and a Condition are pointed at by `position`, their
|
|
151
|
+
* index in the submitted list, since a new Action has no id to be named by.
|
|
152
|
+
* A client skips a `target` or a `reason` it does not know.
|
|
153
|
+
*/
|
|
154
|
+
export type AutomationSaveProblem =
|
|
155
|
+
| { target: "trigger"; reason: "not_in_catalog"; type: string }
|
|
156
|
+
| {
|
|
157
|
+
target: "condition";
|
|
158
|
+
/** Index in `settings.conditions`. */
|
|
159
|
+
position: number;
|
|
160
|
+
reason: "field_not_in_catalog" | "operator_not_allowed";
|
|
161
|
+
field: string;
|
|
162
|
+
operator: ConditionOperator;
|
|
163
|
+
}
|
|
164
|
+
| ({ target: "action"; position: number } & (
|
|
165
|
+
| {
|
|
166
|
+
reason: "not_found" | "other_organization";
|
|
167
|
+
message_group_id: string;
|
|
168
|
+
}
|
|
169
|
+
| {
|
|
170
|
+
reason: "invalid";
|
|
171
|
+
message_group_id: string;
|
|
172
|
+
/** The errors of the Message Group validation. */
|
|
173
|
+
errors: MessageGroupValidationErrors;
|
|
174
|
+
}
|
|
175
|
+
| { reason: "date_reference_not_in_catalog"; date_reference: string }
|
|
176
|
+
| { reason: "channel_not_in_catalog"; channel: AutomationChannel }
|
|
177
|
+
));
|
|
178
|
+
|
|
179
|
+
export type AutomationActionProblem = Extract<
|
|
180
|
+
AutomationSaveProblem,
|
|
181
|
+
{ target: "action" }
|
|
182
|
+
>;
|
|
183
|
+
|
|
184
|
+
export type AutomationConditionProblem = Extract<
|
|
185
|
+
AutomationSaveProblem,
|
|
186
|
+
{ target: "condition" }
|
|
187
|
+
>;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `details` of the 422 `AUTOMATION_ACTIONS_INVALID`: everything that keeps
|
|
191
|
+
* an Automation from being saved, found before anything is written. One
|
|
192
|
+
* flat list: the Trigger first, then Conditions and Actions by position.
|
|
193
|
+
*/
|
|
194
|
+
export interface AutomationActionsInvalidDetails {
|
|
195
|
+
problems: AutomationSaveProblem[];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** Why a selected Message Group is still there after its Automation went. */
|
|
199
|
+
export const AUTOMATION_MESSAGE_GROUP_KEPT_REASONS = ["delete_failed"] as const;
|
|
200
|
+
|
|
201
|
+
export type AutomationMessageGroupKeptReason =
|
|
202
|
+
(typeof AUTOMATION_MESSAGE_GROUP_KEPT_REASONS)[number];
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Answer of a `DELETE /automations/:id` that selected Message Groups
|
|
206
|
+
* (ADR 0016): the Automation is gone, and each selected Message Group is
|
|
207
|
+
* either `deleted` or `kept`. A client skips a `reason` it does not know.
|
|
208
|
+
*/
|
|
209
|
+
export interface AutomationDeleteResult {
|
|
210
|
+
deleted: Array<{ id: string; name: string }>;
|
|
211
|
+
kept: Array<{
|
|
212
|
+
id: string;
|
|
213
|
+
name: string;
|
|
214
|
+
reason: AutomationMessageGroupKeptReason;
|
|
215
|
+
}>;
|
|
216
|
+
}
|