@adminiumjs/manifest 0.3.5 → 0.3.7

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.
Files changed (66) hide show
  1. package/dist/capacity.d.ts +786 -0
  2. package/dist/capacity.d.ts.map +1 -0
  3. package/dist/capacity.js +717 -0
  4. package/dist/capacity.js.map +1 -0
  5. package/dist/documents.d.ts +91 -5
  6. package/dist/documents.d.ts.map +1 -1
  7. package/dist/documents.js +129 -32
  8. package/dist/documents.js.map +1 -1
  9. package/dist/formula.d.ts +114 -2
  10. package/dist/formula.d.ts.map +1 -1
  11. package/dist/formula.js +291 -5
  12. package/dist/formula.js.map +1 -1
  13. package/dist/index.d.ts +12 -6
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +12 -6
  16. package/dist/index.js.map +1 -1
  17. package/dist/marketplace-wire.d.ts +230 -0
  18. package/dist/marketplace-wire.d.ts.map +1 -0
  19. package/dist/marketplace-wire.js +138 -0
  20. package/dist/marketplace-wire.js.map +1 -0
  21. package/dist/outbox.d.ts +242 -27
  22. package/dist/outbox.d.ts.map +1 -1
  23. package/dist/outbox.js +553 -27
  24. package/dist/outbox.js.map +1 -1
  25. package/dist/plan-context.d.ts +28 -3
  26. package/dist/plan-context.d.ts.map +1 -1
  27. package/dist/plan-context.js +291 -14
  28. package/dist/plan-context.js.map +1 -1
  29. package/dist/plan-model.d.ts +58 -3
  30. package/dist/plan-model.d.ts.map +1 -1
  31. package/dist/public-access.d.ts +637 -0
  32. package/dist/public-access.d.ts.map +1 -1
  33. package/dist/public-access.js +1593 -12
  34. package/dist/public-access.js.map +1 -1
  35. package/dist/refs.d.ts +86 -0
  36. package/dist/refs.d.ts.map +1 -1
  37. package/dist/refs.js +186 -0
  38. package/dist/refs.js.map +1 -1
  39. package/dist/roles.d.ts +10 -2
  40. package/dist/roles.d.ts.map +1 -1
  41. package/dist/roles.js +27 -5
  42. package/dist/roles.js.map +1 -1
  43. package/dist/sample.d.ts +218 -2
  44. package/dist/sample.d.ts.map +1 -1
  45. package/dist/sample.js +145 -7
  46. package/dist/sample.js.map +1 -1
  47. package/dist/schema.d.ts +2860 -360
  48. package/dist/schema.d.ts.map +1 -1
  49. package/dist/schema.js +1002 -54
  50. package/dist/schema.js.map +1 -1
  51. package/dist/shapes.d.ts +5 -1
  52. package/dist/shapes.d.ts.map +1 -1
  53. package/dist/shapes.js +13 -3
  54. package/dist/shapes.js.map +1 -1
  55. package/dist/states.d.ts +342 -2
  56. package/dist/states.d.ts.map +1 -1
  57. package/dist/states.js +566 -7
  58. package/dist/states.js.map +1 -1
  59. package/dist/table-shapes.d.ts +47 -0
  60. package/dist/table-shapes.d.ts.map +1 -0
  61. package/dist/table-shapes.js +159 -0
  62. package/dist/table-shapes.js.map +1 -0
  63. package/dist/validate.d.ts.map +1 -1
  64. package/dist/validate.js +80 -2
  65. package/dist/validate.js.map +1 -1
  66. package/package.json +2 -2
@@ -0,0 +1,717 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ /**
3
+ * `capacity` — how much of a pool a table's rows may take. One rule, three
4
+ * ways of naming the pool:
5
+ *
6
+ * - `slot` (the default, and the only kind before): rows add up per start
7
+ * time, a table in a restaurant or an order in a pickup slot. A rule with
8
+ * no `kind` is a slot rule, and its parse gives back exactly what was
9
+ * written — no `kind` is added — so a released app's stored rule reads the
10
+ * same as it always has.
11
+ * - `parent`: rows take from a limit held on the row they point at (tickets
12
+ * of a type, portions of a dish, uses of a code), within its sales window,
13
+ * up to a number per order, and within wider pools (`also`).
14
+ * - `night`: a stay takes one unit on every night from its arrival to the
15
+ * day before it leaves, from a pool counted in another table (the rooms of
16
+ * a type), less the ones out of service.
17
+ *
18
+ * A table may carry up to three rules as a list (a pool per room type and a
19
+ * pool per room on the same stays).
20
+ *
21
+ * `hold` makes a row count only while its hold has not ended, and only while
22
+ * its state is one of `hold.states` (a paid order counts whatever its old
23
+ * hold says). `reserved` keeps places that came back (a refunded ticket held
24
+ * for the waitlist) counted against the public while staff decide.
25
+ *
26
+ * Every table a rule names sits under a key called `table`, so the installer
27
+ * maps each of them to the real table. Columns of the rows a rule reaches
28
+ * through a foreign key (`via`) are named plainly and found at run time.
29
+ */
30
+ import { z } from 'zod';
31
+ import { BOOKING_WEEKDAYS } from './booking.js';
32
+ import { NUMERIC_TYPES, numberOrSetting, refSchema, settingRefSchema, valueFits, } from './refs.js';
33
+ /** One hop to the row this row belongs to (an order's tickets read the order's status). */
34
+ const ownerVia = { via: refSchema.optional() };
35
+ /** Only rows whose column holds one of `values` count (a cancelled order holds nothing). */
36
+ const countConditionSchema = z
37
+ .object({ column: refSchema, values: z.array(z.string().min(1)).min(1), ...ownerVia })
38
+ .strict();
39
+ /** One condition, or one on the row and one on its owner (a ticket's status and its order's). */
40
+ const countWhereSchema = z.union([countConditionSchema, z.array(countConditionSchema).min(1).max(2)]);
41
+ /** Where a hold's end is read when it is not a column of the row itself. */
42
+ const holdEndSchema = z.object({ column: refSchema, via: refSchema.optional() }).strict();
43
+ /**
44
+ * A hold that ends at a moment of a linked row: `via` is a foreign key of the
45
+ * row the hold reads (the owner's, when the hold has a `via`), and the first
46
+ * of `or` that is filled answers when the link is empty (a waitlist offer's
47
+ * end, else the order's own).
48
+ */
49
+ const holdMomentSchema = z
50
+ .object({ column: refSchema, via: refSchema.optional(), or: z.array(holdEndSchema).min(1).max(2).optional() })
51
+ .strict();
52
+ const holdSchema = z
53
+ .object({
54
+ column: z.union([refSchema, holdMomentSchema]),
55
+ states: z.array(z.string().min(1)).min(1).max(8),
56
+ ...ownerVia,
57
+ })
58
+ .strict();
59
+ /** States whose places still count against the public, and are left for staff to hand on. */
60
+ const reservedSchema = z.object({ states: z.array(z.string().min(1)).min(1).max(8), ...ownerVia }).strict();
61
+ /**
62
+ * A number, a setting, or a column of the row the rule points at (empty
63
+ * there: no limit). With `onDay`, a date column of that row, the number holds
64
+ * only on that venue day, and any other day has no limit (today's portions).
65
+ */
66
+ const sizeSchema = z.union([numberOrSetting, z.object({ column: refSchema, onDay: refSchema.optional() }).strict()]);
67
+ const timeSchema = z.union([z.string().regex(/^\d{2}:\d{2}$/), settingRefSchema]);
68
+ const amountSchema = z.union([refSchema, z.number().int().min(1).max(1000)]);
69
+ const slotRuleSchema = z
70
+ .object({
71
+ kind: z.literal('slot').optional(),
72
+ slot: refSchema,
73
+ amount: amountSchema,
74
+ perSlot: numberOrSetting,
75
+ countWhere: countWhereSchema.optional(),
76
+ slotMinutes: numberOrSetting,
77
+ windowDays: numberOrSetting.optional(),
78
+ opens: timeSchema.optional(),
79
+ closes: timeSchema.optional(),
80
+ /** A table, a room: the limit applies per value of this column too. */
81
+ resource: refSchema.optional(),
82
+ /**
83
+ * How many hours before its time a guest may still cancel through the
84
+ * public API; later, only the venue can. Staff are never held to it.
85
+ */
86
+ cancelHours: numberOrSetting.optional(),
87
+ /** Opening hours per weekday, in place of `opens` and `closes`. */
88
+ hours: z
89
+ .object({ table: refSchema, weekday: refSchema, open: refSchema.optional(), opens: refSchema, closes: refSchema })
90
+ .strict()
91
+ .optional(),
92
+ /** Days the venue is closed, `from` to `to` inclusive. */
93
+ closures: z.object({ table: refSchema, from: refSchema, to: refSchema, active: refSchema.optional() }).strict().optional(),
94
+ /** Slots the venue has paused (a kitchen that is full). */
95
+ pauses: z.object({ table: refSchema, slot: refSchema, active: refSchema.optional() }).strict().optional(),
96
+ /** How many minutes ahead a guest's slot must be. */
97
+ noticeMinutes: numberOrSetting.optional(),
98
+ hold: holdSchema.optional(),
99
+ })
100
+ .strict();
101
+ const parentRuleSchema = z
102
+ .object({
103
+ kind: z.literal('parent'),
104
+ /** The foreign key to the row holding the limit; an empty one leaves the row out of this rule. */
105
+ via: refSchema,
106
+ size: sizeSchema,
107
+ /** How much a row takes: a column of the row, or a number (absent: one). */
108
+ amount: amountSchema.optional(),
109
+ countWhere: countWhereSchema.optional(),
110
+ /** The parent's columns a sale must fall between (empty: no bound). */
111
+ window: z.object({ opens: refSchema.optional(), closes: refSchema.optional() }).strict().optional(),
112
+ /** At most `max` per row of `within` (up to six tickets an order). */
113
+ perWrite: z.object({ max: sizeSchema, within: refSchema }).strict().optional(),
114
+ /** Wider pools the same rows also take from (a room's cap across its ticket types). */
115
+ also: z
116
+ .array(z
117
+ .object({
118
+ via: refSchema,
119
+ size: z.union([sizeSchema, z.object({ via: refSchema, column: refSchema }).strict()]),
120
+ })
121
+ .strict())
122
+ .min(1)
123
+ .max(2)
124
+ .optional(),
125
+ /** Count only the rows of the same venue day as this time (a dish's portions today). */
126
+ day: z.union([refSchema, z.object({ column: refSchema, ...ownerVia }).strict()]).optional(),
127
+ /** The column whose value names the lock (absent: the whole table). */
128
+ lockBy: refSchema.optional(),
129
+ hold: holdSchema.optional(),
130
+ reserved: reservedSchema.optional(),
131
+ })
132
+ .strict();
133
+ const outOfServiceSchema = z
134
+ .object({ table: refSchema, room: refSchema, from: refSchema, to: refSchema, active: refSchema.optional() })
135
+ .strict();
136
+ const nightPoolSchema = z.union([
137
+ /** The rows of `count.table` pointing at the same row as `via` (the rooms of a type). */
138
+ z
139
+ .object({
140
+ via: refSchema,
141
+ count: z.object({ table: refSchema, column: refSchema, outOfService: outOfServiceSchema.optional() }).strict(),
142
+ /** A column of the pool's row a guest count must fit (how many it sleeps). */
143
+ fits: z.object({ column: refSchema }).strict().optional(),
144
+ /**
145
+ * When the row's `via` link is set, it counts against that row's
146
+ * `column` instead (the type of the room given, not the one booked).
147
+ */
148
+ given: z.object({ via: refSchema, column: refSchema }).strict().optional(),
149
+ })
150
+ .strict(),
151
+ /**
152
+ * One unit per row pointed at (a room holds one stay a night), or the
153
+ * number in a column of that row (parking spaces of an extra).
154
+ */
155
+ z
156
+ .object({
157
+ via: refSchema,
158
+ size: z.union([z.literal(1), z.object({ column: refSchema }).strict()]),
159
+ outOfService: outOfServiceSchema.optional(),
160
+ })
161
+ .strict(),
162
+ ]);
163
+ /** A date of the row, or of the row it belongs to (an extra's nights are its stay's). */
164
+ const nightDateSchema = z.union([refSchema, z.object({ via: refSchema, column: refSchema }).strict()]);
165
+ const nightRuleSchema = z
166
+ .object({
167
+ kind: z.literal('night'),
168
+ /** The arrival and the departure: two dates. */
169
+ from: nightDateSchema,
170
+ to: nightDateSchema,
171
+ countWhere: countWhereSchema.optional(),
172
+ pool: nightPoolSchema,
173
+ nights: z
174
+ .object({
175
+ min: numberOrSetting.optional(),
176
+ max: numberOrSetting.optional(),
177
+ minByArrival: z.partialRecord(z.enum(BOOKING_WEEKDAYS), z.number().int().min(1).max(60)).optional(),
178
+ /** How many days ahead a stay may start. */
179
+ aheadDays: numberOrSetting.optional(),
180
+ })
181
+ .strict()
182
+ .optional(),
183
+ hold: holdSchema.optional(),
184
+ /**
185
+ * The states of a stay whose guest has arrived (its own, or its owner's
186
+ * with `via`): a change of such a stay — another room, another type, other
187
+ * dates — is judged from the venue's today on. The nights already slept
188
+ * are never judged again (a room closed last night takes nothing from a
189
+ * guest moved today).
190
+ */
191
+ arrived: reservedSchema.optional(),
192
+ })
193
+ .strict();
194
+ /** One limit on a table: a slot rule (with or without `kind`), a parent rule or a night rule. */
195
+ export const capacityRuleSchema = z.discriminatedUnion('kind', [slotRuleSchema, parentRuleSchema, nightRuleSchema]);
196
+ /** Up to three limits on one table. */
197
+ export const capacityListSchema = z.array(capacityRuleSchema).min(1).max(3);
198
+ /**
199
+ * `capacity` on a table: one rule, or a list of up to three. Parsed by the
200
+ * schema of the form written, so a mistake is named where it is (a union of
201
+ * the two would only say the input is invalid). What comes back is what was
202
+ * written: nothing is added.
203
+ */
204
+ export const capacitySchema = z.custom().superRefine((value, ctx) => {
205
+ const parsed = (Array.isArray(value) ? capacityListSchema : capacityRuleSchema).safeParse(value);
206
+ if (parsed.success)
207
+ return;
208
+ for (const issue of parsed.error.issues)
209
+ ctx.addIssue({ ...issue, path: [...issue.path], continue: false });
210
+ });
211
+ /** A table's limits as a list, whichever form it wrote. */
212
+ export function rulesOf(capacity) {
213
+ if (capacity === undefined)
214
+ return [];
215
+ return Array.isArray(capacity) ? capacity : [capacity];
216
+ }
217
+ /** The kind of a rule: a rule that names none is a slot rule. */
218
+ export function kindOf(rule) {
219
+ return rule.kind ?? 'slot';
220
+ }
221
+ /** The keys a slot rule had before it had kinds, and one with no `kind` using only these reads as before. */
222
+ const LEGACY_KEYS = new Set([
223
+ 'slot',
224
+ 'amount',
225
+ 'perSlot',
226
+ 'countWhere',
227
+ 'slotMinutes',
228
+ 'windowDays',
229
+ 'opens',
230
+ 'closes',
231
+ 'resource',
232
+ 'cancelHours',
233
+ ]);
234
+ /**
235
+ * Whether a table's capacity is the one rule a released app writes: a single
236
+ * slot rule with only the keys it had before kinds, its `amount` a column and
237
+ * its `countWhere` one condition on the row itself.
238
+ */
239
+ export function isLegacyCapacity(capacity) {
240
+ if (capacity === undefined || Array.isArray(capacity))
241
+ return false;
242
+ if (capacity.kind !== undefined)
243
+ return false;
244
+ if (Object.keys(capacity).some((key) => !LEGACY_KEYS.has(key)))
245
+ return false;
246
+ if (typeof capacity.amount !== 'string')
247
+ return false;
248
+ const count = capacity.countWhere;
249
+ return count === undefined || (!Array.isArray(count) && count.via === undefined);
250
+ }
251
+ /** The columns a trigger (a stamp's, a code renewal's) watches: `{column}` or `{columns}`, one or a list. */
252
+ function watchedBy(on) {
253
+ const triggers = Array.isArray(on) ? on : [on];
254
+ return triggers.flatMap((trigger) => {
255
+ if (typeof trigger !== 'object' || trigger === null)
256
+ return [];
257
+ const t = trigger;
258
+ return [...(typeof t.column === 'string' ? [t.column] : []), ...(Array.isArray(t.columns) ? t.columns.filter((c) => typeof c === 'string') : [])];
259
+ });
260
+ }
261
+ /**
262
+ * Everything in a table's capacity that names something the manifest does
263
+ * not declare, or a column of the wrong kind. A released app's rule (see
264
+ * {@link isLegacyCapacity}) gets exactly the checks it always had, so no app
265
+ * that validated before is refused now.
266
+ */
267
+ export function capacityIssues(table, capacity, index, at) {
268
+ if (isLegacyCapacity(capacity))
269
+ return legacyIssues(table, capacity, index, at);
270
+ const out = [];
271
+ const rules = rulesOf(capacity);
272
+ const list = Array.isArray(capacity);
273
+ if (rules.filter((rule) => kindOf(rule) === 'slot').length > 1) {
274
+ out.push({ path: at('capacity'), message: 'a table has one slot limit' });
275
+ }
276
+ rules.forEach((rule, r) => {
277
+ const here = (...rest) => (list ? at('capacity', r, ...rest) : at('capacity', ...rest));
278
+ out.push(...ruleIssues(table, rule, index, here));
279
+ });
280
+ return out;
281
+ }
282
+ /** The checks a slot rule has always had: its own columns, and each setting it reads. */
283
+ function legacyIssues(table, cap, index, at) {
284
+ const out = [];
285
+ const countWhere = Array.isArray(cap.countWhere) ? undefined : cap.countWhere;
286
+ for (const [name, value] of [['slot', cap.slot], ['amount', cap.amount], ['resource', cap.resource], ['countWhere', countWhere?.column]]) {
287
+ if (typeof value === 'string' && !index.has(table.ref, value)) {
288
+ out.push({ path: at('capacity', name), message: `"${table.ref}" has no column "${value}"` });
289
+ }
290
+ }
291
+ for (const [name, value] of Object.entries(cap)) {
292
+ if (typeof value === 'object' && value !== null && 'table' in value && 'column' in value) {
293
+ const setting = value;
294
+ if (!index.has(setting.table, setting.column)) {
295
+ out.push({ path: at('capacity', name), message: `"${setting.table}" has no column "${setting.column}"` });
296
+ }
297
+ }
298
+ }
299
+ return out;
300
+ }
301
+ function ruleIssues(table, rule, index, here) {
302
+ const out = [];
303
+ const column = (tableRef, ref) => index.column(tableRef, ref);
304
+ /** The column, when its table and it exist and it is one of `types`; an issue otherwise. */
305
+ const want = (tableRef, ref, types, path, what = '') => {
306
+ if (index.table(tableRef) === undefined) {
307
+ out.push({ path, message: `"${tableRef}" is not a table of this app` });
308
+ return undefined;
309
+ }
310
+ const found = column(tableRef, ref);
311
+ if (found === undefined) {
312
+ out.push({ path, message: `"${tableRef}" has no column "${ref}"` });
313
+ return undefined;
314
+ }
315
+ if (types !== null && !types.includes(found.type)) {
316
+ out.push({ path, message: `"${tableRef}.${ref}" must be ${what}` });
317
+ return undefined;
318
+ }
319
+ return found;
320
+ };
321
+ /** A foreign key of `tableRef`, and the table it points at. */
322
+ const wantFk = (tableRef, ref, path) => {
323
+ const found = want(tableRef, ref, null, path);
324
+ if (found === undefined)
325
+ return undefined;
326
+ if (found.type !== 'fk' || found.references === undefined) {
327
+ out.push({ path, message: `"${tableRef}.${ref}" must be a foreign key` });
328
+ return undefined;
329
+ }
330
+ return found.references;
331
+ };
332
+ /** A setting a rule reads when it runs: a column of the app's settings row, of the kind asked. */
333
+ const setting = (value, path, types, what) => {
334
+ const parsed = settingRefSchema.safeParse(value);
335
+ if (parsed.success)
336
+ want(parsed.data.table, parsed.data.column, types, path, what);
337
+ };
338
+ const number = (value, path) => setting(value, path, NUMERIC_TYPES, 'a number');
339
+ const time = (value, path) => setting(value, path, ['text'], 'a text column holding HH:MM');
340
+ /*
341
+ * The owner: the row one hop up that a condition, a hold or a day reads
342
+ * through. A rule reads one owner, through one foreign key.
343
+ */
344
+ const owners = new Map();
345
+ const ownerOf = (via, path) => {
346
+ if (via === undefined)
347
+ return table.ref;
348
+ if (!owners.has(via))
349
+ owners.set(via, path);
350
+ return wantFk(table.ref, via, path);
351
+ };
352
+ // What counts, per level: the row's own states and its owner's.
353
+ const conditions = rule.countWhere === undefined ? [] : Array.isArray(rule.countWhere) ? rule.countWhere : [rule.countWhere];
354
+ const counted = new Map();
355
+ const levels = new Set();
356
+ conditions.forEach((condition, k) => {
357
+ const path = Array.isArray(rule.countWhere) ? here('countWhere', k) : here('countWhere');
358
+ const level = condition.via ?? '';
359
+ if (levels.has(level)) {
360
+ out.push({ path, message: level === '' ? 'one condition on the row itself' : 'one condition on the row it belongs to' });
361
+ return;
362
+ }
363
+ levels.add(level);
364
+ const owner = ownerOf(condition.via, [...path, 'via']);
365
+ if (owner === undefined)
366
+ return;
367
+ const found = want(owner, condition.column, null, [...path, 'column']);
368
+ if (found === undefined)
369
+ return;
370
+ for (const value of condition.values) {
371
+ if (!valueFits(found, value))
372
+ out.push({ path: [...path, 'values'], message: `"${value}" is not a value of "${owner}.${found.ref}"` });
373
+ }
374
+ counted.set(level, new Set(condition.values));
375
+ });
376
+ /** States a hold or a reservation names must be states that count at the same level. */
377
+ const statesCount = (states, via, path, what) => {
378
+ const values = counted.get(via ?? '');
379
+ if (values === undefined) {
380
+ out.push({ path, message: `nothing says which ${via === undefined ? 'rows' : `rows of "${via}"`} count, so ${what}` });
381
+ return;
382
+ }
383
+ for (const state of states) {
384
+ if (!values.has(state))
385
+ out.push({ path, message: `"${state}" is not counted, so ${what}` });
386
+ }
387
+ };
388
+ /** A time column a hold ends at: a timestamptz Adminium stamps, never one a guest writes. */
389
+ const holdEnd = (tableRef, ref, path) => {
390
+ if (tableRef === undefined)
391
+ return;
392
+ const found = want(tableRef, ref, ['timestamptz'], path, 'a timestamptz');
393
+ if (found !== undefined && found.rules?.stamp === undefined) {
394
+ out.push({ path, message: `a hold ends when Adminium says: "${tableRef}.${ref}" needs a stamp` });
395
+ }
396
+ };
397
+ /**
398
+ * A hold's end on the row itself is written directly when a buyer's new
399
+ * hold lets the old one go — no rule runs on that write. So nothing may
400
+ * watch it: a stamp or a code renewal set off by it, or rows that follow it.
401
+ */
402
+ const unwatchedEnd = (tableRef, ref, path) => {
403
+ const shape = tableRef === undefined ? undefined : index.table(tableRef);
404
+ if (tableRef === undefined || shape === undefined)
405
+ return;
406
+ const said = (what) => out.push({ path, message: `"${tableRef}.${ref}" is a hold's end, which a new hold writes directly as it lets the old one go, so ${what} may not watch it` });
407
+ for (const other of shape.columns) {
408
+ const stamp = other.rules?.stamp;
409
+ if (stamp !== undefined && watchedBy(stamp.on).includes(ref))
410
+ said(`the stamp of "${other.ref}"`);
411
+ if (watchedBy(other.rules?.code?.renew?.on).includes(ref))
412
+ said(`the code renewal of "${other.ref}"`);
413
+ }
414
+ for (const child of index.tables()) {
415
+ for (const other of child.columns) {
416
+ const copy = other.rules?.copy;
417
+ if (copy?.follow !== true || copy.from !== ref)
418
+ continue;
419
+ const via = column(child.ref, copy.via);
420
+ if (via?.references === tableRef)
421
+ said(`"${child.ref}.${other.ref}", which follows it,`);
422
+ }
423
+ }
424
+ };
425
+ if (rule.hold !== undefined) {
426
+ const hold = rule.hold;
427
+ const level = ownerOf(hold.via, here('hold', 'via'));
428
+ statesCount(hold.states, hold.via, here('hold', 'states'), 'it holds nothing');
429
+ if (typeof hold.column === 'string') {
430
+ holdEnd(level, hold.column, here('hold', 'column'));
431
+ unwatchedEnd(level, hold.column, here('hold', 'column'));
432
+ }
433
+ else if (level !== undefined) {
434
+ const moment = hold.column;
435
+ const ends = [
436
+ { column: moment.column, via: moment.via, path: here('hold', 'column') },
437
+ ...(moment.or ?? []).map((end, k) => ({ column: end.column, via: end.via, path: here('hold', 'column', 'or', k) })),
438
+ ];
439
+ for (const end of ends) {
440
+ const target = end.via === undefined ? level : wantFk(level, end.via, [...end.path, 'via']);
441
+ holdEnd(target, end.column, [...end.path, 'column']);
442
+ }
443
+ if (moment.via === undefined)
444
+ out.push({ path: here('hold', 'column', 'via'), message: 'a hold read through a link names the link: otherwise it is a column' });
445
+ }
446
+ }
447
+ if (rule.kind === 'parent') {
448
+ const target = wantFk(table.ref, rule.via, here('via'));
449
+ const onTarget = (ref, types, path, what) => {
450
+ if (target !== undefined)
451
+ want(target, ref, types, path, what);
452
+ };
453
+ const size = (value, path, on) => {
454
+ if (typeof value === 'object' && 'column' in value && !('table' in value)) {
455
+ if (on !== undefined)
456
+ want(on, value.column, NUMERIC_TYPES, [...path, 'column'], 'a number');
457
+ if (value.onDay !== undefined) {
458
+ if (on !== undefined)
459
+ want(on, value.onDay, ['date'], [...path, 'onDay'], 'a date');
460
+ if (rule.day === undefined)
461
+ out.push({ path: [...path, 'onDay'], message: 'a size for one day needs a rule that counts by day (day)' });
462
+ }
463
+ }
464
+ else {
465
+ number(value, path);
466
+ }
467
+ };
468
+ size(rule.size, here('size'), target);
469
+ if (typeof rule.amount === 'string')
470
+ want(table.ref, rule.amount, ['int', 'bigint'], here('amount'), 'an int');
471
+ if (rule.window !== undefined) {
472
+ for (const name of ['opens', 'closes']) {
473
+ const ref = rule.window[name];
474
+ if (ref !== undefined)
475
+ onTarget(ref, ['timestamptz'], here('window', name), 'a timestamptz');
476
+ }
477
+ }
478
+ if (rule.perWrite !== undefined) {
479
+ wantFk(table.ref, rule.perWrite.within, here('perWrite', 'within'));
480
+ size(rule.perWrite.max, here('perWrite', 'max'), target);
481
+ }
482
+ const alsoVias = [];
483
+ (rule.also ?? []).forEach((wider, k) => {
484
+ const path = here('also', k);
485
+ alsoVias.push(wider.via);
486
+ const pool = wantFk(table.ref, wider.via, [...path, 'via']);
487
+ const s = wider.size;
488
+ if (typeof s === 'object' && 'via' in s) {
489
+ const hop = pool === undefined ? undefined : wantFk(pool, s.via, [...path, 'size', 'via']);
490
+ if (hop !== undefined)
491
+ want(hop, s.column, NUMERIC_TYPES, [...path, 'size', 'column'], 'a number');
492
+ }
493
+ else {
494
+ size(s, [...path, 'size'], pool);
495
+ }
496
+ });
497
+ if (rule.day !== undefined) {
498
+ const day = typeof rule.day === 'string' ? { column: rule.day, via: undefined } : rule.day;
499
+ const path = typeof rule.day === 'string' ? here('day') : here('day', 'column');
500
+ const on = ownerOf(day.via, here('day', 'via'));
501
+ if (on !== undefined)
502
+ want(on, day.column, ['timestamptz'], path, 'a timestamptz');
503
+ }
504
+ if (rule.lockBy !== undefined) {
505
+ if (rule.lockBy !== rule.via && !alsoVias.includes(rule.lockBy)) {
506
+ out.push({ path: here('lockBy'), message: `the lock follows a pool: "${rule.via}"${alsoVias.length > 0 ? ` or ${alsoVias.map((v) => `"${v}"`).join(', ')}` : ''}` });
507
+ }
508
+ else if (rule.lockBy !== rule.via) {
509
+ // A wider pool's key names the lock only when the pool's own key decides it.
510
+ const copy = column(table.ref, rule.lockBy)?.rules?.copy;
511
+ if (copy?.via !== rule.via) {
512
+ out.push({ path: here('lockBy'), message: `the lock must follow the pool: copy "${rule.lockBy}" from "${rule.via}"` });
513
+ }
514
+ }
515
+ }
516
+ if (rule.reserved !== undefined) {
517
+ const reserved = rule.reserved;
518
+ ownerOf(reserved.via, here('reserved', 'via'));
519
+ statesCount(reserved.states, reserved.via, here('reserved', 'states'), 'it keeps no place');
520
+ if (rule.hold !== undefined && (rule.hold.via ?? '') === (reserved.via ?? '')) {
521
+ for (const state of reserved.states) {
522
+ if (rule.hold.states.includes(state))
523
+ out.push({ path: here('reserved', 'states'), message: `"${state}" is held, so it is not also kept back` });
524
+ }
525
+ }
526
+ }
527
+ }
528
+ else if (rule.kind === 'night') {
529
+ for (const name of ['from', 'to']) {
530
+ const date = rule[name];
531
+ if (typeof date === 'string') {
532
+ want(table.ref, date, ['date'], here(name), 'a date');
533
+ }
534
+ else {
535
+ const on = ownerOf(date.via, here(name, 'via'));
536
+ if (on !== undefined)
537
+ want(on, date.column, ['date'], here(name, 'column'), 'a date');
538
+ }
539
+ }
540
+ const pool = rule.pool;
541
+ const target = wantFk(table.ref, pool.via, here('pool', 'via'));
542
+ const outOfService = (oos, rooms, path) => {
543
+ if (index.table(oos.table) === undefined) {
544
+ out.push({ path: [...path, 'table'], message: `"${oos.table}" is not a table of this app` });
545
+ return;
546
+ }
547
+ const room = wantFk(oos.table, oos.room, [...path, 'room']);
548
+ if (room !== undefined && rooms !== undefined && room !== rooms) {
549
+ out.push({ path: [...path, 'room'], message: `"${oos.table}.${oos.room}" does not point at "${rooms}"` });
550
+ }
551
+ want(oos.table, oos.from, ['date'], [...path, 'from'], 'a date');
552
+ want(oos.table, oos.to, ['date'], [...path, 'to'], 'a date');
553
+ if (oos.active !== undefined)
554
+ want(oos.table, oos.active, ['bool'], [...path, 'active'], 'a bool');
555
+ };
556
+ if ('count' in pool) {
557
+ const count = pool.count;
558
+ if (index.table(count.table) === undefined) {
559
+ out.push({ path: here('pool', 'count', 'table'), message: `"${count.table}" is not a table of this app` });
560
+ }
561
+ else {
562
+ const counts = wantFk(count.table, count.column, here('pool', 'count', 'column'));
563
+ if (counts !== undefined && target !== undefined && counts !== target) {
564
+ out.push({ path: here('pool', 'count', 'column'), message: `"${count.table}.${count.column}" does not point at "${target}"` });
565
+ }
566
+ if (count.outOfService !== undefined)
567
+ outOfService(count.outOfService, count.table, here('pool', 'count', 'outOfService'));
568
+ }
569
+ if (pool.fits !== undefined && target !== undefined)
570
+ want(target, pool.fits.column, NUMERIC_TYPES, here('pool', 'fits', 'column'), 'a number');
571
+ if (pool.given !== undefined) {
572
+ const given = wantFk(table.ref, pool.given.via, here('pool', 'given', 'via'));
573
+ const typed = given === undefined ? undefined : wantFk(given, pool.given.column, here('pool', 'given', 'column'));
574
+ if (typed !== undefined && target !== undefined && typed !== target) {
575
+ out.push({ path: here('pool', 'given', 'column'), message: `"${given}.${pool.given.column}" does not point at "${target}"` });
576
+ }
577
+ }
578
+ }
579
+ else {
580
+ if (typeof pool.size === 'object' && target !== undefined)
581
+ want(target, pool.size.column, NUMERIC_TYPES, here('pool', 'size', 'column'), 'a number');
582
+ if (pool.outOfService !== undefined)
583
+ outOfService(pool.outOfService, target, here('pool', 'outOfService'));
584
+ }
585
+ if (rule.arrived !== undefined) {
586
+ ownerOf(rule.arrived.via, here('arrived', 'via'));
587
+ statesCount(rule.arrived.states, rule.arrived.via, here('arrived', 'states'), 'no stay counted in it has begun');
588
+ }
589
+ if (rule.nights !== undefined) {
590
+ number(rule.nights.min, here('nights', 'min'));
591
+ number(rule.nights.max, here('nights', 'max'));
592
+ number(rule.nights.aheadDays, here('nights', 'aheadDays'));
593
+ const { min, max } = rule.nights;
594
+ if (typeof min === 'number' && typeof max === 'number' && min > max) {
595
+ out.push({ path: here('nights'), message: 'the shortest stay is not longer than the longest' });
596
+ }
597
+ if (typeof min === 'number' && min === 0)
598
+ out.push({ path: here('nights', 'min'), message: 'a stay is at least one night' });
599
+ }
600
+ }
601
+ else {
602
+ want(table.ref, rule.slot, ['timestamptz'], here('slot'), 'a timestamptz');
603
+ if (typeof rule.amount === 'string')
604
+ want(table.ref, rule.amount, ['int', 'bigint'], here('amount'), 'an int');
605
+ if (rule.resource !== undefined)
606
+ want(table.ref, rule.resource, null, here('resource'));
607
+ number(rule.perSlot, here('perSlot'));
608
+ number(rule.slotMinutes, here('slotMinutes'));
609
+ if (typeof rule.slotMinutes === 'number' && rule.slotMinutes === 0)
610
+ out.push({ path: here('slotMinutes'), message: 'the grid is at least one minute' });
611
+ number(rule.windowDays, here('windowDays'));
612
+ number(rule.cancelHours, here('cancelHours'));
613
+ number(rule.noticeMinutes, here('noticeMinutes'));
614
+ time(rule.opens, here('opens'));
615
+ time(rule.closes, here('closes'));
616
+ if (rule.hours !== undefined) {
617
+ const hours = rule.hours;
618
+ if (rule.opens !== undefined || rule.closes !== undefined) {
619
+ out.push({ path: here('hours'), message: 'the hours come from the hours table, not from opens and closes' });
620
+ }
621
+ const weekday = want(hours.table, hours.weekday, ['enum'], here('hours', 'weekday'), `an enum of ${BOOKING_WEEKDAYS.join(', ')}`);
622
+ if (weekday !== undefined && (weekday.enum ?? []).join(',') !== BOOKING_WEEKDAYS.join(',')) {
623
+ out.push({ path: here('hours', 'weekday'), message: `"${hours.table}.${hours.weekday}" must be an enum of ${BOOKING_WEEKDAYS.join(', ')}` });
624
+ }
625
+ for (const name of ['opens', 'closes'])
626
+ want(hours.table, hours[name], ['text'], here('hours', name), 'a text column holding HH:MM');
627
+ if (hours.open !== undefined)
628
+ want(hours.table, hours.open, ['bool'], here('hours', 'open'), 'a bool');
629
+ }
630
+ if (rule.closures !== undefined) {
631
+ const closures = rule.closures;
632
+ want(closures.table, closures.from, ['date'], here('closures', 'from'), 'a date');
633
+ want(closures.table, closures.to, ['date'], here('closures', 'to'), 'a date');
634
+ if (closures.active !== undefined)
635
+ want(closures.table, closures.active, ['bool'], here('closures', 'active'), 'a bool');
636
+ }
637
+ if (rule.pauses !== undefined) {
638
+ const pauses = rule.pauses;
639
+ want(pauses.table, pauses.slot, ['timestamptz'], here('pauses', 'slot'), 'a timestamptz');
640
+ if (pauses.active !== undefined)
641
+ want(pauses.table, pauses.active, ['bool'], here('pauses', 'active'), 'a bool');
642
+ }
643
+ }
644
+ if (owners.size > 1) {
645
+ const [first, second] = [...owners.keys()];
646
+ out.push({ path: [...owners.values()][1], message: `a rule reads one owner: "${first}" and "${second}" differ` });
647
+ }
648
+ return out;
649
+ }
650
+ /**
651
+ * The foreign keys a limit counts by, on `tableRef`: the pool's, a wider
652
+ * pool's, the per-order key and the owner's. A plain index on one of them
653
+ * keeps the count from reading the whole table.
654
+ */
655
+ export function capacityViaColumns(capacity) {
656
+ const out = new Set();
657
+ for (const rule of rulesOf(capacity)) {
658
+ const conditions = rule.countWhere === undefined ? [] : Array.isArray(rule.countWhere) ? rule.countWhere : [rule.countWhere];
659
+ for (const condition of conditions)
660
+ if (condition.via !== undefined)
661
+ out.add(condition.via);
662
+ if (rule.hold?.via !== undefined)
663
+ out.add(rule.hold.via);
664
+ if (rule.kind === 'parent') {
665
+ out.add(rule.via);
666
+ if (rule.perWrite !== undefined)
667
+ out.add(rule.perWrite.within);
668
+ for (const wider of rule.also ?? [])
669
+ out.add(wider.via);
670
+ if (typeof rule.day === 'object' && rule.day.via !== undefined)
671
+ out.add(rule.day.via);
672
+ if (rule.reserved?.via !== undefined)
673
+ out.add(rule.reserved.via);
674
+ }
675
+ else if (rule.kind === 'night') {
676
+ out.add(rule.pool.via);
677
+ for (const date of [rule.from, rule.to])
678
+ if (typeof date === 'object')
679
+ out.add(date.via);
680
+ if ('given' in rule.pool && rule.pool.given !== undefined)
681
+ out.add(rule.pool.given.via);
682
+ }
683
+ }
684
+ return [...out];
685
+ }
686
+ /**
687
+ * Columns marked `index: true`: each must be a foreign key a limit or a total
688
+ * counts by (a capacity's `via`, or the `via` of a rollup that adds up this
689
+ * table's rows), and not already unique.
690
+ */
691
+ export function viaIndexIssues(tables) {
692
+ const out = [];
693
+ const rolledUpBy = new Set();
694
+ for (const table of tables) {
695
+ for (const column of table.columns) {
696
+ const rollup = column.rules?.rollup;
697
+ if (rollup !== undefined)
698
+ rolledUpBy.add(`${rollup.from}\u0000${rollup.via}`);
699
+ }
700
+ }
701
+ tables.forEach((table, t) => {
702
+ const vias = new Set(capacityViaColumns(table.capacity));
703
+ table.columns.forEach((column, c) => {
704
+ if (column.index !== true)
705
+ return;
706
+ const path = ['requiredSchema', 'tables', t, 'columns', c, 'index'];
707
+ if (column.unique === true || column.role === 'pk') {
708
+ out.push({ path, message: `"${table.ref}.${column.ref}" is unique, so it is indexed already` });
709
+ }
710
+ else if (column.type !== 'fk' || (!vias.has(column.ref) && !rolledUpBy.has(`${table.ref}\u0000${column.ref}`))) {
711
+ out.push({ path, message: `"${table.ref}.${column.ref}" is indexed only as a foreign key a limit or a total counts by` });
712
+ }
713
+ });
714
+ });
715
+ return out;
716
+ }
717
+ //# sourceMappingURL=capacity.js.map