@adminiumjs/manifest 0.3.27 → 0.3.28
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/app-grants.d.ts +134 -0
- package/dist/app-grants.d.ts.map +1 -0
- package/dist/app-grants.js +263 -0
- package/dist/app-grants.js.map +1 -0
- package/dist/automations.d.ts +2172 -56
- package/dist/automations.d.ts.map +1 -1
- package/dist/automations.js +438 -25
- package/dist/automations.js.map +1 -1
- package/dist/compose.d.ts.map +1 -1
- package/dist/compose.js +3 -0
- package/dist/compose.js.map +1 -1
- package/dist/documents.d.ts +1 -0
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +2 -0
- package/dist/documents.js.map +1 -1
- package/dist/index.d.ts +11 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/ledgers.d.ts.map +1 -1
- package/dist/ledgers.js +2 -0
- package/dist/ledgers.js.map +1 -1
- package/dist/modules.d.ts +93 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +489 -0
- package/dist/modules.js.map +1 -0
- package/dist/occupies.d.ts +50 -0
- package/dist/occupies.d.ts.map +1 -0
- package/dist/occupies.js +123 -0
- package/dist/occupies.js.map +1 -0
- package/dist/outbox.d.ts +223 -17
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +125 -13
- package/dist/outbox.js.map +1 -1
- package/dist/page-board.d.ts +144 -0
- package/dist/page-board.d.ts.map +1 -0
- package/dist/page-board.js +280 -0
- package/dist/page-board.js.map +1 -0
- package/dist/public-access.d.ts +6 -0
- package/dist/public-access.d.ts.map +1 -1
- package/dist/public-access.js +74 -0
- package/dist/public-access.js.map +1 -1
- package/dist/rail.d.ts +50 -0
- package/dist/rail.d.ts.map +1 -0
- package/dist/rail.js +122 -0
- package/dist/rail.js.map +1 -0
- package/dist/roles.d.ts +647 -1
- package/dist/roles.d.ts.map +1 -1
- package/dist/roles.js +459 -3
- package/dist/roles.js.map +1 -1
- package/dist/sample.d.ts +62 -0
- package/dist/sample.d.ts.map +1 -1
- package/dist/sample.js +203 -4
- package/dist/sample.js.map +1 -1
- package/dist/schema.d.ts +4277 -169
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +539 -40
- package/dist/schema.js.map +1 -1
- package/dist/shapes.d.ts +3 -2
- package/dist/shapes.d.ts.map +1 -1
- package/dist/shapes.js +76 -7
- package/dist/shapes.js.map +1 -1
- package/dist/state-actions.d.ts +4 -0
- package/dist/state-actions.d.ts.map +1 -1
- package/dist/state-actions.js +7 -0
- package/dist/state-actions.js.map +1 -1
- package/dist/states.d.ts +20 -1
- package/dist/states.d.ts.map +1 -1
- package/dist/states.js +47 -5
- package/dist/states.js.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +41 -0
- package/dist/validate.js.map +1 -1
- package/dist/words.d.ts +1 -1
- package/dist/words.d.ts.map +1 -1
- package/dist/words.js +328 -1
- package/dist/words.js.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"automations.d.ts","sourceRoot":"","sources":["../src/automations.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAA6B,KAAK,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3E,4EAA4E;AAC5E,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAC3C,eAAO,MAAM,oBAAoB,KAAK,CAAC;AAUvC,eAAO,MAAM,wBAAwB,8IAA+I,CAAC;
|
|
1
|
+
{"version":3,"file":"automations.d.ts","sourceRoot":"","sources":["../src/automations.ts"],"names":[],"mappings":"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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAW5B,CAAC;AAEL,eAAO,MAAM,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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"}
|
package/dist/automations.js
CHANGED
|
@@ -24,7 +24,9 @@ import { z } from 'zod';
|
|
|
24
24
|
import { placeholderNames, requiredNamesOfEmail, showWhenNames } from './placeholders.js';
|
|
25
25
|
import { bcp47TagSchema, refSchema } from './refs.js';
|
|
26
26
|
/** The most rules one manifest ships, and the most steps one rule holds. */
|
|
27
|
-
export const MANIFEST_AUTOMATIONS_MAX =
|
|
27
|
+
export const MANIFEST_AUTOMATIONS_MAX = 24;
|
|
28
|
+
/** How many a manifest could ship before: one with more says so, so a server that reads only these refuses it by its version. */
|
|
29
|
+
export const MANIFEST_AUTOMATIONS_FIRST_MAX = 12;
|
|
28
30
|
export const AUTOMATION_NODES_MAX = 40;
|
|
29
31
|
/** One text, or the same text in several languages (US English always among them). */
|
|
30
32
|
const localized = (max) => z.union([z.string().max(max), z.record(bcp47TagSchema, z.string().min(1).max(max)).refine((texts) => texts['en-US'] !== undefined, { message: 'a text in several languages includes en-US' })]);
|
|
@@ -35,7 +37,15 @@ export const AUTOMATION_CONDITION_OPS = ['is', 'is_not', 'contains', 'gt', 'lt',
|
|
|
35
37
|
const RELATIVE_OPS = ['within_next', 'within_last', 'more_than_ago', 'more_than_ahead'];
|
|
36
38
|
const NO_OPERAND_OPS = ['is_empty', 'not_empty'];
|
|
37
39
|
const COUNT_OPS = ['is', 'is_not', 'gt', 'lt'];
|
|
38
|
-
|
|
40
|
+
/**
|
|
41
|
+
* An amount of time; `setting` reads it from a whole-number column of the app's settings row when
|
|
42
|
+
* the conditions are read, and `amount` is used while that holds none.
|
|
43
|
+
*/
|
|
44
|
+
const operand = z.union([
|
|
45
|
+
z.string().max(2000),
|
|
46
|
+
z.number(),
|
|
47
|
+
z.object({ amount: z.number().int().min(1).max(3650), unit: durationUnit, setting: z.object({ table: refSchema, column: refSchema }).strict().optional() }).strict(),
|
|
48
|
+
]);
|
|
39
49
|
const fieldLeft = z.object({ field: refSchema }).strict();
|
|
40
50
|
const leaf = z.object({ left: fieldLeft, op: z.enum(AUTOMATION_CONDITION_OPS), right: operand.optional() }).strict();
|
|
41
51
|
const countLeft = z.object({ count: z.object({ table: refSchema, matchColumn: refSchema, equalsField: refSchema, where: leaf.optional() }).strict() }).strict();
|
|
@@ -99,22 +109,76 @@ export const manifestTriggerSchema = z.discriminatedUnion('kind', [
|
|
|
99
109
|
.object({
|
|
100
110
|
kind: z.literal('schedule'),
|
|
101
111
|
schedule: scheduleSchema,
|
|
102
|
-
|
|
112
|
+
/**
|
|
113
|
+
* The rows a run visits. `once: true` visits a row one time ever;
|
|
114
|
+
* `once: "per-spell"` one time for each spell the row matches: a row that
|
|
115
|
+
* stops matching and matches again later is visited again (a deal gone
|
|
116
|
+
* quiet, worked on, and quiet once more).
|
|
117
|
+
*/
|
|
118
|
+
forEach: z.object({ table: refSchema, where: z.array(manifestConditionSchema).max(8), once: z.union([z.boolean(), z.literal('per-spell')]) }).strict().optional(),
|
|
119
|
+
})
|
|
120
|
+
.strict(),
|
|
121
|
+
]);
|
|
122
|
+
/** A column of the app's settings row: the row of that table with the lowest key. */
|
|
123
|
+
const settingRef = z.object({ table: refSchema, column: refSchema }).strict();
|
|
124
|
+
/**
|
|
125
|
+
* A value a step writes: text, which may carry `{{record.<column>}}`; the
|
|
126
|
+
* moment it runs; what a column of the app's settings row holds when it runs
|
|
127
|
+
* (`setting`); or a day so many days after a date of the record, or after
|
|
128
|
+
* now, on the business's calendar (`daysAfter`), the days read from a
|
|
129
|
+
* whole-number setting when it holds some.
|
|
130
|
+
*/
|
|
131
|
+
const writeValue = z.union([
|
|
132
|
+
z.string().max(4000),
|
|
133
|
+
z.object({ now: z.literal(true) }).strict(),
|
|
134
|
+
z.object({ setting: settingRef }).strict(),
|
|
135
|
+
z
|
|
136
|
+
.object({
|
|
137
|
+
daysAfter: z
|
|
138
|
+
.object({
|
|
139
|
+
column: refSchema.optional(),
|
|
140
|
+
now: z.literal(true).optional(),
|
|
141
|
+
days: z.number().int().min(0).max(3650),
|
|
142
|
+
setting: settingRef.optional(),
|
|
143
|
+
/** The wall time on that day, for a column that holds a moment. */
|
|
144
|
+
at: z.string().regex(HHMM, 'a time such as 09:00').optional(),
|
|
145
|
+
})
|
|
146
|
+
.strict()
|
|
147
|
+
.refine((after) => (after.column === undefined) !== (after.now === undefined), { message: 'days after a column of the record ("column") or after now ("now": true): one of the two' }),
|
|
103
148
|
})
|
|
104
149
|
.strict(),
|
|
105
150
|
]);
|
|
106
|
-
/** A value a step writes: text, which may carry `{{record.<column>}}`, or the moment it runs. */
|
|
107
|
-
const writeValue = z.union([z.string().max(4000), z.object({ now: z.literal(true) }).strict()]);
|
|
108
151
|
const values = z.record(refSchema, writeValue);
|
|
109
152
|
const roleKey = z.string().regex(/^[a-z][a-z0-9-]*$/, 'a role key');
|
|
153
|
+
/** A staff column of the record, or one of the row a link of it names: `owner_id`, `deal_id.owner_id`. */
|
|
154
|
+
const staffPath = z.string().regex(/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)?$/, 'a column, or <link column>.<column>');
|
|
155
|
+
/** What later steps call a row a step made. */
|
|
156
|
+
const stepName = z.string().regex(/^[a-z][a-z0-9_]{0,39}$/, 'a step name is snake_case, at most 40 characters');
|
|
110
157
|
export const manifestActionSchema = z.discriminatedUnion('kind', [
|
|
111
158
|
z
|
|
112
159
|
.object({
|
|
113
160
|
kind: z.literal('email'),
|
|
114
161
|
/** One of the manifest's own templates. */
|
|
115
162
|
templateKey: z.string().regex(/^[a-z][a-z0-9-]{1,79}$/, 'a template key'),
|
|
116
|
-
/**
|
|
117
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Who it goes to: `field`, the record's column that holds the address;
|
|
165
|
+
* or `staff`, a staff column of the record (or `<link>.<staff column>`
|
|
166
|
+
* of the row a link of it names), whose person's address Adminium looks up;
|
|
167
|
+
* or `listed`, the people a table that lists people against the record
|
|
168
|
+
* (one of its `belongs` ways `through`) names for it: one mail each, to
|
|
169
|
+
* those of them who can open the record when it is sent.
|
|
170
|
+
*/
|
|
171
|
+
to: z.union([
|
|
172
|
+
z.object({ kind: z.literal('field'), column: refSchema }).strict(),
|
|
173
|
+
z.object({ kind: z.literal('staff'), column: staffPath }).strict(),
|
|
174
|
+
z.object({ kind: z.literal('listed'), table: refSchema }).strict(),
|
|
175
|
+
]),
|
|
176
|
+
/**
|
|
177
|
+
* Where a reply goes: to the person a staff column of the record names
|
|
178
|
+
* (or `<link>.<staff column>` of the row a link of it names), by the
|
|
179
|
+
* address they sign in with. Nobody there: no Reply-To.
|
|
180
|
+
*/
|
|
181
|
+
replyTo: z.object({ staff: staffPath }).strict().optional(),
|
|
118
182
|
/** What fills a placeholder of the template that the record does not. */
|
|
119
183
|
vars: z
|
|
120
184
|
.record(z.string().regex(/^[A-Za-z0-9_.-]{1,120}$/), z.string().max(2000))
|
|
@@ -122,8 +186,75 @@ export const manifestActionSchema = z.discriminatedUnion('kind', [
|
|
|
122
186
|
.optional(),
|
|
123
187
|
})
|
|
124
188
|
.strict(),
|
|
125
|
-
z
|
|
126
|
-
|
|
189
|
+
z
|
|
190
|
+
.object({
|
|
191
|
+
kind: z.literal('notification'),
|
|
192
|
+
/**
|
|
193
|
+
* Everybody who holds one of the roles; or the person a staff column of
|
|
194
|
+
* the record names (or `<link>.<staff column>`). With `lead: true`, not
|
|
195
|
+
* that person but whoever leads a team they are in (never the person
|
|
196
|
+
* themselves), and of those only who can open the record. `else`: the
|
|
197
|
+
* roles told when the column, or its lead, names nobody; a lead always
|
|
198
|
+
* has one. Or `listed`: the people a table that lists people against the
|
|
199
|
+
* record (one of its `belongs` ways `through`) names for it, of those
|
|
200
|
+
* only who can open the record when the step runs.
|
|
201
|
+
*/
|
|
202
|
+
to: z.union([
|
|
203
|
+
z.object({ roles: z.array(roleKey).min(1).max(20) }).strict(),
|
|
204
|
+
z.object({ listed: refSchema }).strict(),
|
|
205
|
+
z
|
|
206
|
+
.object({ column: staffPath, lead: z.literal(true).optional(), else: z.object({ roles: z.array(roleKey).min(1).max(20) }).strict().optional() })
|
|
207
|
+
.strict()
|
|
208
|
+
.refine((to) => to.lead !== true || to.else !== undefined, { message: 'a notice to a lead says who is told when there is none ("else": { "roles": [...] })', path: ['else'] }),
|
|
209
|
+
]),
|
|
210
|
+
title: localized(200),
|
|
211
|
+
body: localized(2000).optional(),
|
|
212
|
+
/** Told at once, also in the person's quiet hours (a change to today's day sheet). */
|
|
213
|
+
urgent: z.literal(true).optional(),
|
|
214
|
+
})
|
|
215
|
+
.strict(),
|
|
216
|
+
/** `name`: what later steps call the row this one makes, in `{{steps.<name>.<column>}}`. */
|
|
217
|
+
z.object({ kind: z.literal('record.create'), table: refSchema, values, name: stepName.optional() }).strict(),
|
|
218
|
+
/**
|
|
219
|
+
* A summary for each person: one email each, of the rows that are theirs.
|
|
220
|
+
* Each section lists the rows of a table whose `owner` (a staff column) is
|
|
221
|
+
* the person, that meet `where` and that the person may read; a block
|
|
222
|
+
* `email.rows` of the template with `"section": "<key>"` draws it.
|
|
223
|
+
* `"reach": "team"` adds the rows of the people the person leads when it is
|
|
224
|
+
* sent; `sum` adds columns up over every such row; `"by": "owner"` lists a
|
|
225
|
+
* line a person; `target` measures a sum against those people's targets.
|
|
226
|
+
* Nothing in it is read further than the person's own roles reach.
|
|
227
|
+
*/
|
|
228
|
+
z
|
|
229
|
+
.object({
|
|
230
|
+
kind: z.literal('digest'),
|
|
231
|
+
templateKey: z.string().regex(/^[a-z][a-z0-9-]{1,79}$/, 'a template key'),
|
|
232
|
+
to: z.object({ roles: z.array(roleKey).min(1).max(20) }).strict(),
|
|
233
|
+
sections: z
|
|
234
|
+
.array(z
|
|
235
|
+
.object({
|
|
236
|
+
key: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, 'a section key is kebab-case'),
|
|
237
|
+
table: refSchema,
|
|
238
|
+
owner: refSchema,
|
|
239
|
+
where: z.array(manifestConditionSchema).max(8).optional(),
|
|
240
|
+
orderBy: z.object({ column: refSchema, dir: z.enum(['asc', 'desc']).optional() }).strict().optional(),
|
|
241
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
242
|
+
/** `team`: the person's rows and those of the people they lead when it is sent. Never "all". */
|
|
243
|
+
reach: z.enum(['own', 'team']).optional(),
|
|
244
|
+
/** Number columns added up over every row of the section: `{{sum.<section>.<column>}}`. */
|
|
245
|
+
sum: z.array(refSchema).min(1).max(4).optional(),
|
|
246
|
+
/** One line a person (their name, how many, each sum) instead of one a row. */
|
|
247
|
+
by: z.literal('owner').optional(),
|
|
248
|
+
/** What the sum `of` is measured against: `column` of the rows of `table` whose `owner` names the same people. */
|
|
249
|
+
target: z.object({ table: refSchema, owner: refSchema, column: refSchema, year: refSchema.optional(), of: refSchema }).strict().optional(),
|
|
250
|
+
})
|
|
251
|
+
.strict())
|
|
252
|
+
.min(1)
|
|
253
|
+
.max(6),
|
|
254
|
+
/** Sent also to a person none of whose sections lists a row. Default: such a person gets no email. */
|
|
255
|
+
sendEmpty: z.literal(true).optional(),
|
|
256
|
+
})
|
|
257
|
+
.strict(),
|
|
127
258
|
z.object({ kind: z.literal('record.update'), values }).strict(),
|
|
128
259
|
/**
|
|
129
260
|
* A step an add-on gives (its `addOn.steps`): which add-on, which step, and
|
|
@@ -191,6 +322,8 @@ export const manifestAutomationSchema = z
|
|
|
191
322
|
enabled: z.boolean(),
|
|
192
323
|
trigger: manifestTriggerSchema,
|
|
193
324
|
graph: manifestGraphSchema,
|
|
325
|
+
/** The part of the app the rule belongs to: not run while it is off (see `modules.ts`). */
|
|
326
|
+
module: z.string().regex(/^[a-z][a-z0-9-]{0,39}$/, 'a module id is kebab-case').optional(),
|
|
194
327
|
})
|
|
195
328
|
.strict();
|
|
196
329
|
export const manifestAutomationsSchema = z
|
|
@@ -199,6 +332,8 @@ export const manifestAutomationsSchema = z
|
|
|
199
332
|
.refine((rules) => new Set(rules.map((rule) => rule.key)).size === rules.length, { message: 'two rules share a key' });
|
|
200
333
|
/** What every rule's email can read whatever its record is. */
|
|
201
334
|
export const RULE_EMAIL_VARS = ['now', 'ruleName', 'recordLabel', 'appName'];
|
|
335
|
+
/** What a summary's email can read beside its sections' figures: the person's name, and when the figures were read (`readAt`). */
|
|
336
|
+
export const DIGEST_EMAIL_VARS = ['now', 'ruleName', 'appName', 'name', 'readAt'];
|
|
202
337
|
/**
|
|
203
338
|
* Every placeholder a template reads in one language, in the order it reads
|
|
204
339
|
* them. A `{{row.*}}` is left out: it belongs to the rows a list block draws.
|
|
@@ -256,12 +391,30 @@ export function automationIssues(m) {
|
|
|
256
391
|
out.push({ path: [...path, 'right'], message: `"${of?.ref ?? ''}.${found.ref}" is a number: compare it with a number (${JSON.stringify(right)} is not one)` });
|
|
257
392
|
}
|
|
258
393
|
};
|
|
259
|
-
|
|
394
|
+
/** An amount read from a setting: a whole-number column of one of the manifest's tables, where the run can read it. */
|
|
395
|
+
const settingIssues = (condition, path, settingsOk) => {
|
|
396
|
+
const right = condition.right;
|
|
397
|
+
if (typeof right !== 'object' || right === null || right.setting === undefined)
|
|
398
|
+
return;
|
|
399
|
+
const at = [...path, 'right', 'setting'];
|
|
400
|
+
if (!settingsOk) {
|
|
401
|
+
out.push({ path: at, message: 'an amount read from a setting is read where a rule visits rows by the clock (forEach.where) and in a condition step, not here' });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const held = tableAt(right.setting.table, [...at, 'table']);
|
|
405
|
+
const found = column(held, right.setting.column, [...at, 'column']);
|
|
406
|
+
if (found !== undefined && !['int', 'bigint'].includes(found.type))
|
|
407
|
+
out.push({ path: [...at, 'column'], message: `"${right.setting.table}.${right.setting.column}" is not a whole-number column: it holds an amount of ${right.unit}` });
|
|
408
|
+
};
|
|
409
|
+
const conditionIssues = (of, condition, path, inScan, settingsOk = false) => {
|
|
410
|
+
settingIssues(condition, path, settingsOk);
|
|
260
411
|
if ('count' in condition.left) {
|
|
261
412
|
// A scan's filter is one query over the scanned table: it cannot count another table's rows.
|
|
262
413
|
if (inScan)
|
|
263
414
|
out.push({ path: [...path, 'left'], message: 'a rule that runs by the clock cannot filter the rows it visits by a count: keep the count in a column of the row (a rollup), and compare that' });
|
|
264
415
|
const count = condition.left.count;
|
|
416
|
+
if (typeof count.where?.right === 'object' && count.where.right.setting !== undefined)
|
|
417
|
+
out.push({ path: [...path, 'left', 'count', 'where', 'right', 'setting'], message: 'a count reads no setting: write the amount' });
|
|
265
418
|
const counted = tableAt(count.table, [...path, 'left', 'count', 'table']);
|
|
266
419
|
column(counted, count.matchColumn, [...path, 'left', 'count', 'matchColumn']);
|
|
267
420
|
column(of, count.equalsField, [...path, 'left', 'count', 'equalsField']);
|
|
@@ -296,8 +449,8 @@ export function automationIssues(m) {
|
|
|
296
449
|
else if (trigger.forEach !== undefined) {
|
|
297
450
|
const forEach = trigger.forEach;
|
|
298
451
|
record = tableAt(forEach.table, at('trigger', 'forEach', 'table'));
|
|
299
|
-
forEach.where.forEach((condition, c) => conditionIssues(record, condition, at('trigger', 'forEach', 'where', c), true));
|
|
300
|
-
if (forEach.once && forEach.where.length === 0)
|
|
452
|
+
forEach.where.forEach((condition, c) => conditionIssues(record, condition, at('trigger', 'forEach', 'where', c), true, true));
|
|
453
|
+
if (forEach.once !== false && forEach.where.length === 0)
|
|
301
454
|
out.push({ path: at('trigger', 'forEach', 'once'), message: 'a row visited once needs a condition that picks it: without one every row is visited on the first run' });
|
|
302
455
|
}
|
|
303
456
|
if (trigger.kind === 'schedule' && trigger.schedule.kind !== 'interval') {
|
|
@@ -307,10 +460,71 @@ export function automationIssues(m) {
|
|
|
307
460
|
if (schedule.kind === 'monthly' && schedule.dayOfMonth === undefined)
|
|
308
461
|
out.push({ path: at('trigger', 'schedule', 'dayOfMonth'), message: 'a monthly rule names its day (1 to 28)' });
|
|
309
462
|
}
|
|
310
|
-
|
|
463
|
+
/** A staff column of the rule's record, or of the row one of its links names: who a step tells. */
|
|
464
|
+
const staffAt = (path, where) => {
|
|
465
|
+
if (record === undefined) {
|
|
466
|
+
out.push({ path: where, message: 'this rule runs for no record, so there is no person on it to tell' });
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const [first, second] = path.split('.');
|
|
470
|
+
let of = record;
|
|
471
|
+
let ref = first;
|
|
472
|
+
if (second !== undefined) {
|
|
473
|
+
const link = column(record, first, where);
|
|
474
|
+
if (link === undefined)
|
|
475
|
+
return;
|
|
476
|
+
if (link.references === undefined) {
|
|
477
|
+
out.push({ path: where, message: `"${record.ref}.${first}" is not a link to another table` });
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
of = tables.get(link.references);
|
|
481
|
+
// A link into an add-on's table, or one this manifest cannot see: checked where the rule is installed.
|
|
482
|
+
if (of === undefined)
|
|
483
|
+
return;
|
|
484
|
+
ref = second;
|
|
485
|
+
}
|
|
486
|
+
const found = column(of, ref, where);
|
|
487
|
+
if (found !== undefined && found.rules?.['staff'] !== true) {
|
|
488
|
+
out.push({ path: where, message: `"${of.ref}.${ref}" is not a staff column ("rules": { "staff": true }): only one names a person to tell` });
|
|
489
|
+
}
|
|
490
|
+
};
|
|
491
|
+
/** A table that lists people against the rule's record (a `belongs` way `through` it): who a step to "the people listed" tells. */
|
|
492
|
+
const listedAt = (ref, where) => {
|
|
493
|
+
if (record === undefined) {
|
|
494
|
+
out.push({ path: where, message: 'this rule runs for no record, so nobody is listed on it to tell' });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const lists = (record.belongs ?? []).some((way) => typeof way === 'object' && way !== null && 'through' in way && way.through?.table === ref);
|
|
498
|
+
if (!lists)
|
|
499
|
+
out.push({ path: where, message: `"${ref}" lists nobody against "${record.ref}": name a table one of its "belongs" ways reads "through"` });
|
|
500
|
+
};
|
|
501
|
+
/** `{{was.<column>}}`: a column of the record, in a rule run by its change. */
|
|
502
|
+
const changing = trigger.kind === 'record' && trigger.event === 'updated';
|
|
503
|
+
const wasIssue = (ref, path) => {
|
|
504
|
+
if (!changing)
|
|
505
|
+
out.push({ path, message: `{{was.${ref}}}: only a rule run when its record is updated reads the record as it was` });
|
|
506
|
+
else if (record !== undefined && !record.columns.some((candidate) => candidate.ref === ref))
|
|
507
|
+
out.push({ path, message: `{{was.${ref}}}: "${record.ref}" has no column "${ref}"` });
|
|
508
|
+
};
|
|
509
|
+
// The rows earlier steps made, by the name each step gave its row.
|
|
510
|
+
const made = new Map();
|
|
511
|
+
// Every `{{record.<column>}}` a text carries is a column of the rule's record; every `{{steps.<name>.<column>}}` a column of a row an earlier step made.
|
|
311
512
|
const tokens = (text, path) => {
|
|
312
513
|
const texts = typeof text === 'string' ? [text] : typeof text === 'object' && text !== null ? Object.values(text).filter((value) => typeof value === 'string') : [];
|
|
313
514
|
for (const each of texts) {
|
|
515
|
+
for (const match of each.matchAll(/\{\{\s*steps\.([A-Za-z0-9_]+)\.([A-Za-z0-9_]+)\s*(\|[^{}]*)?\}\}/g)) {
|
|
516
|
+
const name = match[1];
|
|
517
|
+
if (!made.has(name))
|
|
518
|
+
out.push({ path, message: `{{steps.${name}.${match[2]}}}: no earlier step makes a row called "${name}" (give a "record.create" step "name": "${name}")` });
|
|
519
|
+
else if (made.get(name) !== undefined && !made.get(name).columns.some((candidate) => candidate.ref === match[2]))
|
|
520
|
+
out.push({ path, message: `{{steps.${name}.${match[2]}}}: "${made.get(name).ref}" has no column "${match[2]}"` });
|
|
521
|
+
// A row that was not made has no value to stand in for: the run stops instead.
|
|
522
|
+
if (match[3] !== undefined)
|
|
523
|
+
out.push({ path, message: `{{steps.${name}.${match[2]}}} takes no backup: a row that was not made stops the run` });
|
|
524
|
+
}
|
|
525
|
+
// The record as it was before the change: only a rule about a change has one.
|
|
526
|
+
for (const match of each.matchAll(/\{\{\s*was\.([A-Za-z0-9_]+)\s*(?:\|[^{}]*)?\}\}/g))
|
|
527
|
+
wasIssue(match[1], path);
|
|
314
528
|
for (const match of each.matchAll(/\{\{\s*record\.([A-Za-z0-9_]+)\s*(?:\|[^{}]*)?\}\}/g)) {
|
|
315
529
|
if (record === undefined)
|
|
316
530
|
out.push({ path, message: `{{record.${match[1]}}}: this rule runs for no record (a clock with no rows to visit)` });
|
|
@@ -319,21 +533,85 @@ export function automationIssues(m) {
|
|
|
319
533
|
}
|
|
320
534
|
}
|
|
321
535
|
};
|
|
536
|
+
/** A setting a value reads: a column of one of the manifest's tables that is neither secret nor personal, of a table that is not private. */
|
|
537
|
+
const settingAt = (setting, path) => {
|
|
538
|
+
const held = tableAt(setting.table, [...path, 'table']);
|
|
539
|
+
if (held?.private === true)
|
|
540
|
+
out.push({ path: [...path, 'table'], message: `"${setting.table}" is private: no rule reads a value of it` });
|
|
541
|
+
const found = column(held, setting.column, [...path, 'column']);
|
|
542
|
+
if (found?.rules?.['secret'] === true || found?.rules?.['personal'] === true) {
|
|
543
|
+
out.push({ path: [...path, 'column'], message: `"${setting.table}.${setting.column}" is ${found.rules['secret'] === true ? 'a secret' : 'a personal'} column: a rule never writes it into another row` });
|
|
544
|
+
}
|
|
545
|
+
return found;
|
|
546
|
+
};
|
|
547
|
+
/** One value a step writes into `ref` of `into`: its placeholders, the setting it reads, the date it counts from. */
|
|
548
|
+
const valueIssues = (into, ref, value, path) => {
|
|
549
|
+
if (typeof value === 'string') {
|
|
550
|
+
tokens(value, path);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const target = into?.columns.find((candidate) => candidate.ref === ref);
|
|
554
|
+
if ('setting' in value) {
|
|
555
|
+
const found = settingAt(value.setting, [...path, 'setting']);
|
|
556
|
+
if (found === undefined || target === undefined)
|
|
557
|
+
return;
|
|
558
|
+
// A person is filled from a column that holds one; any other value from a column of its own kind.
|
|
559
|
+
if (target.rules?.['staff'] === true && found.rules?.['staff'] !== true)
|
|
560
|
+
out.push({ path: [...path, 'setting', 'column'], message: `"${into?.ref ?? ''}.${ref}" holds a staff member, and "${value.setting.table}.${value.setting.column}" is not a staff column` });
|
|
561
|
+
else if (found.type !== target.type || found.references !== target.references)
|
|
562
|
+
out.push({ path: [...path, 'setting', 'column'], message: `"${value.setting.table}.${value.setting.column}" is ${found.type} and "${into?.ref ?? ''}.${ref}" is ${target.type}: a setting fills a column of its own kind` });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if ('daysAfter' in value) {
|
|
566
|
+
const after = value.daysAfter;
|
|
567
|
+
const at = [...path, 'daysAfter'];
|
|
568
|
+
if (target !== undefined && !DATES.includes(target.type))
|
|
569
|
+
out.push({ path, message: `"${into?.ref ?? ''}.${ref}" is ${target.type}: a day so many days on is written into a date column` });
|
|
570
|
+
if (after.at !== undefined && target?.type === 'date')
|
|
571
|
+
out.push({ path: [...at, 'at'], message: `"${into?.ref ?? ''}.${ref}" holds a day, which has no time of day` });
|
|
572
|
+
if (after.column !== undefined) {
|
|
573
|
+
if (record === undefined)
|
|
574
|
+
out.push({ path: [...at, 'column'], message: 'this rule runs for no record, so there is no date of it to count from' });
|
|
575
|
+
const from = column(record, after.column, [...at, 'column']);
|
|
576
|
+
if (from !== undefined && !DATES.includes(from.type))
|
|
577
|
+
out.push({ path: [...at, 'column'], message: `"${record?.ref ?? ''}.${after.column}" is ${from.type}: days are counted from a date` });
|
|
578
|
+
if (from?.rules?.['secret'] === true || from?.rules?.['personal'] === true)
|
|
579
|
+
out.push({ path: [...at, 'column'], message: `"${record?.ref ?? ''}.${after.column}" is a protected column: a day counted from it would tell it` });
|
|
580
|
+
}
|
|
581
|
+
if (after.setting !== undefined) {
|
|
582
|
+
const found = settingAt(after.setting, [...at, 'setting']);
|
|
583
|
+
if (found !== undefined && !['int', 'bigint'].includes(found.type))
|
|
584
|
+
out.push({ path: [...at, 'setting', 'column'], message: `"${after.setting.table}.${after.setting.column}" is not a whole-number column: it holds a number of days` });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
322
588
|
const ids = [];
|
|
323
589
|
walk(rule.graph.nodes, at('graph', 'nodes'), (node, path) => {
|
|
324
590
|
tokens(node.title, [...path, 'title']);
|
|
325
591
|
tokens(node.sub, [...path, 'sub']);
|
|
326
592
|
if (node.kind === 'condition' || node.kind === 'branch')
|
|
327
|
-
conditionIssues(record, node.condition, [...path, 'condition'], false);
|
|
593
|
+
conditionIssues(record, node.condition, [...path, 'condition'], false, true);
|
|
328
594
|
if (node.kind !== 'action')
|
|
329
595
|
return;
|
|
330
596
|
const action = node.action;
|
|
331
597
|
const here = (...rest) => [...path, 'action', ...rest];
|
|
332
598
|
if (action.kind === 'notification') {
|
|
333
|
-
action.to
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
599
|
+
if ('roles' in action.to) {
|
|
600
|
+
action.to.roles.forEach((role, i) => {
|
|
601
|
+
if (!m.roles.includes(role))
|
|
602
|
+
out.push({ path: here('to', 'roles', i), message: `"${role}" is not one of this manifest's roles` });
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
else if ('listed' in action.to) {
|
|
606
|
+
listedAt(action.to.listed, here('to', 'listed'));
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
staffAt(action.to.column, here('to', 'column'));
|
|
610
|
+
(action.to.else?.roles ?? []).forEach((role, i) => {
|
|
611
|
+
if (!m.roles.includes(role))
|
|
612
|
+
out.push({ path: here('to', 'else', 'roles', i), message: `"${role}" is not one of this manifest's roles` });
|
|
613
|
+
});
|
|
614
|
+
}
|
|
337
615
|
tokens(action.title, here('title'));
|
|
338
616
|
tokens(action.body, here('body'));
|
|
339
617
|
return;
|
|
@@ -342,7 +620,121 @@ export function automationIssues(m) {
|
|
|
342
620
|
const into = tableAt(action.table, here('table'));
|
|
343
621
|
for (const [ref, value] of Object.entries(action.values)) {
|
|
344
622
|
column(into, ref, here('values', ref));
|
|
345
|
-
|
|
623
|
+
valueIssues(into, ref, value, here('values', ref));
|
|
624
|
+
}
|
|
625
|
+
// Named after its own values are read: a step does not read the row it is about to make.
|
|
626
|
+
if (action.name !== undefined) {
|
|
627
|
+
if (made.has(action.name))
|
|
628
|
+
out.push({ path: here('name'), message: `"${action.name}" already names a row an earlier step makes` });
|
|
629
|
+
if (['record', 'steps', 'now'].includes(action.name))
|
|
630
|
+
out.push({ path: here('name'), message: `"${action.name}" is a word a rule already reads: choose another name` });
|
|
631
|
+
made.set(action.name, into);
|
|
632
|
+
}
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (action.kind === 'digest') {
|
|
636
|
+
action.to.roles.forEach((role, i) => {
|
|
637
|
+
if (!m.roles.includes(role))
|
|
638
|
+
out.push({ path: here('to', 'roles', i), message: `"${role}" is not one of this manifest's roles` });
|
|
639
|
+
});
|
|
640
|
+
const keys = new Set();
|
|
641
|
+
action.sections.forEach((section, s) => {
|
|
642
|
+
if (keys.has(section.key))
|
|
643
|
+
out.push({ path: here('sections', s, 'key'), message: `"${section.key}" is the key of an earlier section` });
|
|
644
|
+
keys.add(section.key);
|
|
645
|
+
const of = tableAt(section.table, here('sections', s, 'table'));
|
|
646
|
+
const owner = column(of, section.owner, here('sections', s, 'owner'));
|
|
647
|
+
if (owner !== undefined && owner.rules?.['staff'] !== true) {
|
|
648
|
+
out.push({ path: here('sections', s, 'owner'), message: `"${section.table}.${section.owner}" is not a staff column ("rules": { "staff": true }): a section lists the rows whose person is the one written to` });
|
|
649
|
+
}
|
|
650
|
+
(section.where ?? []).forEach((condition, c) => conditionIssues(of, condition, here('sections', s, 'where', c), true));
|
|
651
|
+
if (section.orderBy !== undefined)
|
|
652
|
+
column(of, section.orderBy.column, here('sections', s, 'orderBy', 'column'));
|
|
653
|
+
// A sum adds up numbers; the same column twice is one figure with two names.
|
|
654
|
+
(section.sum ?? []).forEach((ref, n) => {
|
|
655
|
+
const added = column(of, ref, here('sections', s, 'sum', n));
|
|
656
|
+
if (added !== undefined && !NUMBERS.includes(added.type))
|
|
657
|
+
out.push({ path: here('sections', s, 'sum', n), message: `"${section.table}.${ref}" is not a number column: a sum adds numbers up` });
|
|
658
|
+
if ((section.sum ?? []).indexOf(ref) !== n)
|
|
659
|
+
out.push({ path: here('sections', s, 'sum', n), message: `"${ref}" is summed twice` });
|
|
660
|
+
});
|
|
661
|
+
// A line a person has no row to sort by.
|
|
662
|
+
if (section.by !== undefined && section.orderBy !== undefined)
|
|
663
|
+
out.push({ path: here('sections', s, 'orderBy'), message: 'a section with one line a person ("by": "owner") is listed by name: it has no orderBy' });
|
|
664
|
+
if (section.target !== undefined) {
|
|
665
|
+
const target = section.target;
|
|
666
|
+
if (!(section.sum ?? []).includes(target.of))
|
|
667
|
+
out.push({ path: here('sections', s, 'target', 'of'), message: `"${target.of}" is not one of this section's sums ("sum")` });
|
|
668
|
+
const goals = tableAt(target.table, here('sections', s, 'target', 'table'));
|
|
669
|
+
const whose = column(goals, target.owner, here('sections', s, 'target', 'owner'));
|
|
670
|
+
if (whose !== undefined && whose.rules?.['staff'] !== true)
|
|
671
|
+
out.push({ path: here('sections', s, 'target', 'owner'), message: `"${target.table}.${target.owner}" is not a staff column ("rules": { "staff": true }): a target is somebody's` });
|
|
672
|
+
const goal = column(goals, target.column, here('sections', s, 'target', 'column'));
|
|
673
|
+
if (goal !== undefined && !NUMBERS.includes(goal.type))
|
|
674
|
+
out.push({ path: here('sections', s, 'target', 'column'), message: `"${target.table}.${target.column}" is not a number column` });
|
|
675
|
+
if (target.year !== undefined) {
|
|
676
|
+
const year = column(goals, target.year, here('sections', s, 'target', 'year'));
|
|
677
|
+
if (year !== undefined && !['int', 'bigint'].includes(year.type))
|
|
678
|
+
out.push({ path: here('sections', s, 'target', 'year'), message: `"${target.table}.${target.year}" is not a whole-number column: it holds a calendar year` });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
const template = templates.get(action.templateKey);
|
|
683
|
+
if (template === undefined) {
|
|
684
|
+
out.push({ path: here('templateKey'), message: `"${action.templateKey}" is not one of this manifest's emailTemplates: a shipped rule sends only its own` });
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (template.attach !== undefined)
|
|
688
|
+
out.push({ path: here('templateKey'), message: `"${action.templateKey}" carries a document, which a summary does not` });
|
|
689
|
+
for (const [locale, content] of Object.entries(template.locales)) {
|
|
690
|
+
const drawn = new Set();
|
|
691
|
+
content.blocks.forEach((block) => {
|
|
692
|
+
if (block.block !== 'email.rows')
|
|
693
|
+
return;
|
|
694
|
+
const section = block.data?.['section'];
|
|
695
|
+
if (typeof section !== 'string' || !keys.has(section)) {
|
|
696
|
+
out.push({ path: here('templateKey'), message: `"${action.templateKey}" (${locale}) has a rows block that names no section of this step ("section": one of ${[...keys].join(', ')})` });
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
drawn.add(section);
|
|
700
|
+
const drawing = action.sections.find((one) => one.key === section);
|
|
701
|
+
// A line a person reads the person's name, how many rows, each sum, and the target with its cover.
|
|
702
|
+
if (drawing.by !== undefined) {
|
|
703
|
+
const lines = new Set(['row.owner', 'row.count', ...(drawing.sum ?? []).map((ref) => `row.sum.${ref}`), ...(drawing.target === undefined ? [] : ['row.target', 'row.cover'])]);
|
|
704
|
+
for (const name of placeholderNames([block.data?.['row']])) {
|
|
705
|
+
if (name.startsWith('row.') && !lines.has(name))
|
|
706
|
+
out.push({ path: here('templateKey'), message: `"${action.templateKey}" (${locale}) reads {{${name}}} in the section "${section}", which lists one line a person: a line reads ${[...lines].map((one) => `{{${one}}}`).join(', ')}` });
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
// Each line reads the columns of its section's table: {{row.<column>}}, or a form of one ({{row.<column>.money}}).
|
|
711
|
+
const of = tables.get(drawing.table);
|
|
712
|
+
for (const name of placeholderNames([block.data?.['row']])) {
|
|
713
|
+
if (!name.startsWith('row.'))
|
|
714
|
+
continue;
|
|
715
|
+
const ref = name.slice('row.'.length).split('.')[0];
|
|
716
|
+
if (of !== undefined && !of.columns.some((candidate) => candidate.ref === ref)) {
|
|
717
|
+
out.push({ path: here('templateKey'), message: `"${action.templateKey}" (${locale}) reads {{${name}}}, and "${of.ref}" has no column "${ref}"` });
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
for (const key of keys) {
|
|
722
|
+
if (!drawn.has(key))
|
|
723
|
+
out.push({ path: here('sections'), message: `"${action.templateKey}" (${locale}) draws no rows block for the section "${key}"` });
|
|
724
|
+
}
|
|
725
|
+
// A section's own figures: how many rows, each sum. They are there for everybody who reads the section.
|
|
726
|
+
const figures = new Set(action.sections.flatMap((one) => [`count.${one.key}`, ...(one.sum ?? []).map((ref) => `sum.${one.key}.${ref}`)]));
|
|
727
|
+
// A target and its cover are there only for a person who has one: a template that reads either says what it writes without.
|
|
728
|
+
const sometimes = new Set(action.sections.flatMap((one) => (one.target === undefined ? [] : [`target.${one.key}`, `cover.${one.key}`])));
|
|
729
|
+
const required = templateRequiredPlaceholdersOf(content);
|
|
730
|
+
const unfilled = required.filter((name) => !DIGEST_EMAIL_VARS.includes(name) && !figures.has(name) && !sometimes.has(name));
|
|
731
|
+
if (unfilled.length > 0) {
|
|
732
|
+
out.push({ path: here(), message: `"${action.templateKey}" (${locale}) reads ${unfilled.map((name) => `{{${name}}}`).join(', ')}, which a summary cannot fill: it fills {{name}}, {{appName}}, {{ruleName}}, {{now}}, {{readAt}}, {{count.<section>}}, and {{sum.<section>.<column>}}, {{target.<section>}} and {{cover.<section>}} of a section that asks for them` });
|
|
733
|
+
}
|
|
734
|
+
const bare = required.filter((name) => sometimes.has(name));
|
|
735
|
+
if (bare.length > 0) {
|
|
736
|
+
out.push({ path: here(), message: `"${action.templateKey}" (${locale}) reads ${bare.map((name) => `{{${name}}}`).join(', ')} with nothing to write for a person who has no target: give it a backup ({{${bare[0]}|none set}}), or tie its block to it with "showWhen"` });
|
|
737
|
+
}
|
|
346
738
|
}
|
|
347
739
|
return;
|
|
348
740
|
}
|
|
@@ -352,7 +744,7 @@ export function automationIssues(m) {
|
|
|
352
744
|
const decided = record === undefined ? new Set() : (m.decided?.(record.ref) ?? new Set());
|
|
353
745
|
for (const [ref, value] of Object.entries(action.values)) {
|
|
354
746
|
const found = column(record, ref, here('values', ref));
|
|
355
|
-
|
|
747
|
+
valueIssues(record, ref, value, here('values', ref));
|
|
356
748
|
const rule2 = DECIDING.find((name) => found?.rules?.[name] !== undefined);
|
|
357
749
|
if (found !== undefined && (rule2 !== undefined || decided.has(ref))) {
|
|
358
750
|
out.push({ path: here('values', ref), message: `"${record?.ref ?? ''}.${ref}" is decided by Adminium${rule2 === undefined ? '' : ` (its ${rule2} rule)`}: a rule cannot write it` });
|
|
@@ -384,7 +776,14 @@ export function automationIssues(m) {
|
|
|
384
776
|
// An email: one of the manifest's own templates, to a column of the record, and only one a rule can fill.
|
|
385
777
|
if (record === undefined)
|
|
386
778
|
out.push({ path: here(), message: 'this rule runs for no record, so there is no address to send to' });
|
|
387
|
-
|
|
779
|
+
if (action.to.kind === 'staff')
|
|
780
|
+
staffAt(action.to.column, here('to', 'column'));
|
|
781
|
+
else if (action.to.kind === 'listed')
|
|
782
|
+
listedAt(action.to.table, here('to', 'table'));
|
|
783
|
+
else
|
|
784
|
+
column(record, action.to.column, here('to', 'column'));
|
|
785
|
+
if (action.replyTo !== undefined)
|
|
786
|
+
staffAt(action.replyTo.staff, here('replyTo', 'staff'));
|
|
388
787
|
const template = templates.get(action.templateKey);
|
|
389
788
|
if (template === undefined) {
|
|
390
789
|
out.push({ path: here('templateKey'), message: `"${action.templateKey}" is not one of this manifest's emailTemplates: a shipped rule sends only its own` });
|
|
@@ -394,7 +793,17 @@ export function automationIssues(m) {
|
|
|
394
793
|
for (const [name, text] of Object.entries(vars))
|
|
395
794
|
tokens(text, here('vars', name));
|
|
396
795
|
const columns = new Set((record?.columns ?? []).map((candidate) => candidate.ref));
|
|
397
|
-
|
|
796
|
+
// The name of the person a staff column of the record names; the record's address, in a mail to a person.
|
|
797
|
+
const people = new Set((record?.columns ?? []).filter((candidate) => candidate.rules?.['staff'] === true).flatMap((candidate) => [`${candidate.ref}.name`, `record.${candidate.ref}.name`]));
|
|
798
|
+
// A mail to a person, or to each of the people listed on the record: each is somebody the address is worked out for.
|
|
799
|
+
const toPerson = action.to.kind === 'staff' || action.to.kind === 'listed';
|
|
800
|
+
const filled = (name) => RULE_EMAIL_VARS.includes(name) ||
|
|
801
|
+
Object.hasOwn(vars, name) ||
|
|
802
|
+
columns.has(name) ||
|
|
803
|
+
(name.startsWith('record.') && columns.has(name.slice('record.'.length))) ||
|
|
804
|
+
people.has(name) ||
|
|
805
|
+
(toPerson && name === 'recordUrl') ||
|
|
806
|
+
(changing && name.startsWith('was.') && columns.has(name.slice('was.'.length)));
|
|
398
807
|
const everywhere = new Set();
|
|
399
808
|
if (template.attach !== undefined)
|
|
400
809
|
out.push({ path: here('templateKey'), message: `"${action.templateKey}" carries a document, which only the outbox draws: send it from the outbox, or give the rule a template with no "attach"` });
|
|
@@ -407,11 +816,15 @@ export function automationIssues(m) {
|
|
|
407
816
|
for (const name of names)
|
|
408
817
|
everywhere.add(name);
|
|
409
818
|
const unfilled = templateRequiredPlaceholdersOf(content).filter((name) => !filled(name));
|
|
410
|
-
if (unfilled.
|
|
411
|
-
|
|
819
|
+
if (unfilled.includes('recordUrl') && !toPerson) {
|
|
820
|
+
out.push({ path: here(), message: `"${action.templateKey}" (${locale}) reads {{recordUrl}}, which only a mail to a person carries ("to": { "kind": "staff", … }): an address of the record is no link for anybody else` });
|
|
821
|
+
}
|
|
822
|
+
const others = unfilled.filter((name) => name !== 'recordUrl' || toPerson);
|
|
823
|
+
if (others.length > 0) {
|
|
824
|
+
const first = others[0];
|
|
412
825
|
out.push({
|
|
413
826
|
path: here(),
|
|
414
|
-
message: `"${action.templateKey}" (${locale}) reads ${
|
|
827
|
+
message: `"${action.templateKey}" (${locale}) reads ${others.map((name) => `{{${name}}}`).join(', ')}, which a rule cannot fill. Add "vars": { "${first}": "{{record.<column>}}" } to this step, or send this email from the outbox.`,
|
|
415
828
|
});
|
|
416
829
|
}
|
|
417
830
|
}
|