@weave-kit/engine 0.2.0 → 0.2.2

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 (56) hide show
  1. package/README.md +1 -0
  2. package/dist/adapters/auth/source.d.ts.map +1 -1
  3. package/dist/adapters/auth/source.js +4 -1
  4. package/dist/adapters/auth/source.js.map +1 -1
  5. package/dist/cli/index.js +1 -0
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/core/object/validate/field.d.ts +5 -0
  8. package/dist/core/object/validate/field.d.ts.map +1 -0
  9. package/dist/core/object/validate/field.js +432 -0
  10. package/dist/core/object/validate/field.js.map +1 -0
  11. package/dist/core/object/validate/formulas.d.ts +8 -0
  12. package/dist/core/object/validate/formulas.d.ts.map +1 -0
  13. package/dist/core/object/validate/formulas.js +112 -0
  14. package/dist/core/object/validate/formulas.js.map +1 -0
  15. package/dist/core/object/validate/indexes.d.ts +5 -0
  16. package/dist/core/object/validate/indexes.d.ts.map +1 -0
  17. package/dist/core/object/validate/indexes.js +21 -0
  18. package/dist/core/object/validate/indexes.js.map +1 -0
  19. package/dist/core/object/validate/labels.d.ts +4 -0
  20. package/dist/core/object/validate/labels.d.ts.map +1 -0
  21. package/dist/core/object/validate/labels.js +21 -0
  22. package/dist/core/object/validate/labels.js.map +1 -0
  23. package/dist/core/object/validate/object.d.ts +21 -0
  24. package/dist/core/object/validate/object.d.ts.map +1 -0
  25. package/dist/core/object/validate/object.js +129 -0
  26. package/dist/core/object/validate/object.js.map +1 -0
  27. package/dist/core/object/validate/permissions.d.ts +4 -0
  28. package/dist/core/object/validate/permissions.d.ts.map +1 -0
  29. package/dist/core/object/validate/permissions.js +69 -0
  30. package/dist/core/object/validate/permissions.js.map +1 -0
  31. package/dist/core/object/validate/primitives.d.ts +34 -0
  32. package/dist/core/object/validate/primitives.d.ts.map +1 -0
  33. package/dist/core/object/validate/primitives.js +57 -0
  34. package/dist/core/object/validate/primitives.js.map +1 -0
  35. package/dist/core/object/validate.d.ts +5 -18
  36. package/dist/core/object/validate.d.ts.map +1 -1
  37. package/dist/core/object/validate.js +4 -816
  38. package/dist/core/object/validate.js.map +1 -1
  39. package/dist/core/script/index.d.ts +1 -1
  40. package/dist/core/script/index.d.ts.map +1 -1
  41. package/dist/core/script/index.js +1 -1
  42. package/dist/core/script/index.js.map +1 -1
  43. package/dist/core/script/source.d.ts +9 -0
  44. package/dist/core/script/source.d.ts.map +1 -1
  45. package/dist/core/script/source.js +46 -4
  46. package/dist/core/script/source.js.map +1 -1
  47. package/dist/runtime/sql-analyzer/index.d.ts +7 -0
  48. package/dist/runtime/sql-analyzer/index.d.ts.map +1 -1
  49. package/dist/runtime/sql-analyzer/index.js +29 -0
  50. package/dist/runtime/sql-analyzer/index.js.map +1 -1
  51. package/dist/subsystems/script/backend/isolated-vm.d.ts.map +1 -1
  52. package/dist/subsystems/script/backend/isolated-vm.js +3 -10
  53. package/dist/subsystems/script/backend/isolated-vm.js.map +1 -1
  54. package/dist/version.d.ts +1 -1
  55. package/dist/version.js +1 -1
  56. package/package.json +5 -2
@@ -1,819 +1,7 @@
1
- import { DEFAULT_LOCALE } from '../i18n/index.js';
2
- import { FormulaOperandError } from '../formula/index.js';
3
- import { FORMULA_EXCLUDED_ATTRS, FORMULA_TYPES, checkOperands, detectCycle, extractRefs, inferType, parseFormula, typeCompatible, } from '../formula/index.js';
4
- import { SchemaError } from '../types/errors.js';
5
- import { SCHEMA_FORMAT_VERSION } from './schema-version.js';
6
- import { FIELD_TYPES, INDEX_TYPES, ON_DELETE_ACTIONS, READ_SCOPES, ROW_SCOPE_MARKERS, SCALAR_FIELD_TYPES, SEQUENCE_CYCLES, SEQUENCE_TOKENS, } from '../types/values.js';
7
- const SNAKE_CASE = /^[a-z][a-z0-9_]*$/;
8
- const LOCALE_TAG = /^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$/;
9
- const FIELD_TYPE_VALUES = Object.values(FIELD_TYPES);
10
- const ON_DELETE_ACTION_VALUES = Object.values(ON_DELETE_ACTIONS);
11
- const INDEX_TYPE_VALUES = Object.values(INDEX_TYPES);
12
- const SCALAR_TYPE_VALUES = Object.values(SCALAR_FIELD_TYPES);
13
- const SEQUENCE_TOKEN_VALUES = Object.values(SEQUENCE_TOKENS);
14
- const SEQUENCE_CYCLE_VALUES = Object.values(SEQUENCE_CYCLES);
15
- const SEQUENCE_PLACEHOLDER = /\{seq(?::\d+)?\}/;
16
- /** placeholders allowed in titleTemplate */
17
- const TITLE_PLACEHOLDER_RE = /\{(\w+)\}/g;
18
- /** keys always allowed on any field */
19
- const BASE_KEYS = ['name', 'type', 'labels', 'description', 'primary', 'system', 'sensitive'];
20
- /** extra keys allowed per field type */
21
- const EXTRA_KEYS = {
22
- string: ['minLength', 'maxLength', 'regex', 'required', 'unique', 'default', 'formula', ROW_SCOPE_MARKERS.OWNERSHIP, ROW_SCOPE_MARKERS.TEAM],
23
- text: ['maxLength', 'required', 'unique', 'default', 'formula'],
24
- integer: ['min', 'max', 'required', 'unique', 'default', 'formula'],
25
- number: ['min', 'max', 'precision', 'required', 'unique', 'default', 'formula'],
26
- currency: ['min', 'max', 'required', 'unique', 'default', 'formula'],
27
- boolean: ['required', 'unique', 'default', 'formula'],
28
- datetime: ['required', 'unique', 'default'],
29
- date: ['required', 'unique', 'default'],
30
- json: ['required', 'default'],
31
- enum: ['options', 'multiple', 'required', 'unique', 'default'],
32
- relation: ['target', 'required', 'unique', 'onDelete'],
33
- details: ['target'],
34
- multiRelation: ['target', 'required'],
35
- seq_no: ['format', 'cycle'],
36
- firstName: ['minLength', 'maxLength', 'regex', 'required', 'unique', 'default', 'formula'],
37
- lastName: ['minLength', 'maxLength', 'regex', 'required', 'unique', 'default', 'formula'],
38
- email: ['minLength', 'maxLength', 'regex', 'required', 'unique', 'default', 'formula'],
39
- phone: ['minLength', 'maxLength', 'regex', 'required', 'unique', 'default', 'formula'],
40
- image: ['required', 'unique', 'multiple', 'default'],
41
- person: ['target', 'required', 'unique', 'onDelete', 'department'],
42
- department: ['target', 'required', 'unique', 'onDelete'],
43
- };
44
- function fail(vc, code, params = {}) {
45
- throw new SchemaError(code, { object: vc.object, ...params }, vc.locale);
46
- }
47
- /** type guard: plain object (not array/null) */
48
- function isRecord(value) {
49
- return typeof value === 'object' && value !== null && !Array.isArray(value);
50
- }
51
- /** read an optional string attribute; fails with a localized error if present but not a string */
52
- function expectString(record, key, vc) {
53
- const value = record[key];
54
- if (value === undefined)
55
- return undefined;
56
- if (typeof value !== 'string')
57
- fail(vc, 'field.attr.string', { attr: key });
58
- return value;
59
- }
60
- /** read an optional boolean attribute; fails if present but not boolean */
61
- function expectBoolean(record, key, vc) {
62
- const value = record[key];
63
- if (value === undefined)
64
- return undefined;
65
- if (typeof value !== 'boolean')
66
- fail(vc, 'field.attr.boolean', { attr: key });
67
- return value;
68
- }
69
- /** read an optional numeric attribute; fails if present but not a finite number */
70
- function expectNumber(record, key, vc) {
71
- const value = record[key];
72
- if (value === undefined)
73
- return undefined;
74
- if (typeof value !== 'number' || Number.isNaN(value))
75
- fail(vc, 'field.attr.number', { attr: key });
76
- return value;
77
- }
78
- /** read an optional positive integer attribute (used for lengths/precision/padding) */
79
- function expectPositiveInt(record, key, vc) {
80
- const value = expectNumber(record, key, vc);
81
- if (value === undefined)
82
- return undefined;
83
- if (!Number.isInteger(value) || value <= 0)
84
- fail(vc, 'field.attr.positiveInt', { attr: key });
85
- return value;
86
- }
87
- /** validate a labels map: keys must be valid BCP-47 locale tags, values non-empty strings */
88
- function validateLabels(raw, vc) {
89
- if (raw === undefined)
90
- return undefined;
91
- if (!isRecord(raw))
92
- fail(vc, 'field.labels.notObject');
93
- const result = {};
94
- for (const [locale, value] of Object.entries(raw)) {
95
- if (!LOCALE_TAG.test(locale))
96
- fail(vc, 'field.labels.locale.invalid', { locale });
97
- if (typeof value !== 'string' || value.length === 0) {
98
- fail(vc, 'field.labels.value.string', { locale });
99
- }
100
- result[locale] = value;
101
- }
102
- if (Object.keys(result).length === 0)
103
- fail(vc, 'field.labels.empty');
104
- return result;
105
- }
106
- /** reject any attribute not allowed for the given field type (typo protection) */
107
- function checkAllowedKeys(record, type, vc) {
108
- const allowed = new Set([...BASE_KEYS, ...(EXTRA_KEYS[type] ?? [])]);
109
- for (const key of Object.keys(record)) {
110
- if (!allowed.has(key))
111
- fail(vc, 'field.attr.notAllowed', { type, attr: key });
112
- }
113
- }
114
- /** type-check a default value against the field type; options = enum option list */
115
- function validateDefault(value, type, vc, options) {
116
- if (value === undefined)
117
- return;
118
- switch (type) {
119
- case FIELD_TYPES.STRING:
120
- case FIELD_TYPES.TEXT:
121
- case FIELD_TYPES.FIRST_NAME:
122
- case FIELD_TYPES.LAST_NAME:
123
- case FIELD_TYPES.EMAIL:
124
- case FIELD_TYPES.PHONE:
125
- if (typeof value !== 'string')
126
- fail(vc, 'field.default.string');
127
- break;
128
- case FIELD_TYPES.IMAGE:
129
- if (typeof value !== 'string' && !Array.isArray(value))
130
- fail(vc, 'field.default.string');
131
- break;
132
- case FIELD_TYPES.INTEGER:
133
- if (typeof value !== 'number' || !Number.isInteger(value))
134
- fail(vc, 'field.default.integer');
135
- break;
136
- case FIELD_TYPES.NUMBER:
137
- case FIELD_TYPES.CURRENCY:
138
- if (typeof value !== 'number')
139
- fail(vc, 'field.default.number');
140
- break;
141
- case FIELD_TYPES.BOOLEAN:
142
- if (typeof value !== 'boolean')
143
- fail(vc, 'field.default.boolean');
144
- break;
145
- case FIELD_TYPES.DATETIME:
146
- if (typeof value !== 'string')
147
- fail(vc, 'field.default.iso');
148
- break;
149
- case FIELD_TYPES.DATE:
150
- if (typeof value !== 'string')
151
- fail(vc, 'field.default.string');
152
- break;
153
- case FIELD_TYPES.JSON:
154
- break;
155
- case FIELD_TYPES.ENUM:
156
- if (typeof value !== 'string')
157
- fail(vc, 'field.default.string');
158
- if (options !== undefined && !options.includes(value))
159
- fail(vc, 'field.default.inOptions', { value });
160
- break;
161
- case FIELD_TYPES.RELATION:
162
- case FIELD_TYPES.PERSON:
163
- case FIELD_TYPES.DEPARTMENT:
164
- case FIELD_TYPES.DETAILS:
165
- case FIELD_TYPES.MULTI_RELATION:
166
- case FIELD_TYPES.SEQ_NO:
167
- fail(vc, 'field.default.notSupported', { type });
168
- }
169
- }
170
- /** parse a formula expression; fails on syntax errors */
171
- function parseFormulaAttr(raw, vc) {
172
- if (raw.formula === undefined)
173
- return undefined;
174
- if (typeof raw.formula !== 'string')
175
- fail(vc, 'field.attr.string', { attr: 'formula' });
176
- try {
177
- parseFormula(raw.formula);
178
- }
179
- catch (err) {
180
- fail(vc, 'formula.syntax', { detail: err.message });
181
- }
182
- return raw.formula;
183
- }
184
- /** validate a single field definition into its typed discriminated-union member */
185
- function validateField(raw, vc, allowedFieldTypes) {
186
- if (!isRecord(raw))
187
- fail(vc, 'field.notObject');
188
- const name = expectString(raw, 'name', vc);
189
- if (name === undefined)
190
- fail(vc, 'field.name.required');
191
- if (!SNAKE_CASE.test(name))
192
- fail(vc, 'field.name.snake', { name });
193
- const rawType = raw.type;
194
- if (typeof rawType !== 'string' || !FIELD_TYPE_VALUES.includes(rawType)) {
195
- fail(vc, 'field.type.invalid', { type: String(rawType) });
196
- }
197
- const type = rawType;
198
- if (allowedFieldTypes !== undefined && !allowedFieldTypes.includes(type)) {
199
- fail(vc, 'field.type.disabled', { type });
200
- }
201
- checkAllowedKeys(raw, type, vc);
202
- const labels = validateLabels(raw.labels, vc);
203
- const description = expectString(raw, 'description', vc);
204
- const primary = expectBoolean(raw, 'primary', vc);
205
- const system = expectBoolean(raw, 'system', vc);
206
- const sensitive = expectBoolean(raw, 'sensitive', vc);
207
- const required = expectBoolean(raw, 'required', vc);
208
- const unique = expectBoolean(raw, 'unique', vc);
209
- const base = { name, labels, description, primary, system, sensitive };
210
- const formula = parseFormulaAttr(raw, vc);
211
- switch (type) {
212
- case FIELD_TYPES.STRING: {
213
- const minLength = expectPositiveInt(raw, 'minLength', vc);
214
- const maxLength = expectPositiveInt(raw, 'maxLength', vc);
215
- const regex = expectString(raw, 'regex', vc);
216
- const ownership = expectBoolean(raw, ROW_SCOPE_MARKERS.OWNERSHIP, vc);
217
- const team = expectBoolean(raw, ROW_SCOPE_MARKERS.TEAM, vc);
218
- if (regex !== undefined) {
219
- try {
220
- new RegExp(regex);
221
- }
222
- catch {
223
- fail(vc, 'field.regex.invalid');
224
- }
225
- }
226
- if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
227
- fail(vc, 'field.minLengthGtMaxLength');
228
- }
229
- validateDefault(raw.default, type, vc);
230
- return {
231
- ...base,
232
- type: FIELD_TYPES.STRING,
233
- formula,
234
- minLength,
235
- maxLength,
236
- regex,
237
- required,
238
- unique,
239
- default: raw.default,
240
- ownership,
241
- team,
242
- };
243
- }
244
- case FIELD_TYPES.TEXT: {
245
- const maxLength = expectPositiveInt(raw, 'maxLength', vc);
246
- validateDefault(raw.default, type, vc);
247
- return {
248
- ...base,
249
- type: FIELD_TYPES.TEXT,
250
- formula,
251
- maxLength,
252
- required,
253
- unique,
254
- default: raw.default,
255
- };
256
- }
257
- case FIELD_TYPES.INTEGER: {
258
- const min = expectNumber(raw, 'min', vc);
259
- const max = expectNumber(raw, 'max', vc);
260
- if (min !== undefined && !Number.isInteger(min))
261
- fail(vc, 'field.min.integer');
262
- if (max !== undefined && !Number.isInteger(max))
263
- fail(vc, 'field.max.integer');
264
- if (min !== undefined && max !== undefined && min > max)
265
- fail(vc, 'field.minGtMax');
266
- validateDefault(raw.default, type, vc);
267
- return {
268
- ...base,
269
- type: FIELD_TYPES.INTEGER,
270
- formula,
271
- min,
272
- max,
273
- required,
274
- unique,
275
- default: raw.default,
276
- };
277
- }
278
- case FIELD_TYPES.NUMBER: {
279
- const min = expectNumber(raw, 'min', vc);
280
- const max = expectNumber(raw, 'max', vc);
281
- const precision = expectPositiveInt(raw, 'precision', vc);
282
- if (min !== undefined && max !== undefined && min > max)
283
- fail(vc, 'field.minGtMax');
284
- validateDefault(raw.default, type, vc);
285
- return {
286
- ...base,
287
- type: FIELD_TYPES.NUMBER,
288
- formula,
289
- min,
290
- max,
291
- precision,
292
- required,
293
- unique,
294
- default: raw.default,
295
- };
296
- }
297
- case FIELD_TYPES.CURRENCY: {
298
- const min = expectNumber(raw, 'min', vc);
299
- const max = expectNumber(raw, 'max', vc);
300
- if (min !== undefined && max !== undefined && min > max)
301
- fail(vc, 'field.minGtMax');
302
- validateDefault(raw.default, type, vc);
303
- return {
304
- ...base,
305
- type: FIELD_TYPES.CURRENCY,
306
- formula,
307
- min,
308
- max,
309
- required,
310
- unique,
311
- default: raw.default,
312
- };
313
- }
314
- case FIELD_TYPES.BOOLEAN:
315
- validateDefault(raw.default, type, vc);
316
- return {
317
- ...base,
318
- type: FIELD_TYPES.BOOLEAN,
319
- formula,
320
- required,
321
- unique,
322
- default: raw.default,
323
- };
324
- case FIELD_TYPES.DATETIME:
325
- validateDefault(raw.default, type, vc);
326
- return {
327
- ...base,
328
- type: FIELD_TYPES.DATETIME,
329
- required,
330
- unique,
331
- default: raw.default,
332
- };
333
- case FIELD_TYPES.DATE:
334
- validateDefault(raw.default, type, vc);
335
- return {
336
- ...base,
337
- type: FIELD_TYPES.DATE,
338
- required,
339
- unique,
340
- default: raw.default,
341
- };
342
- case FIELD_TYPES.JSON:
343
- validateDefault(raw.default, type, vc);
344
- return { ...base, type: FIELD_TYPES.JSON, required, default: raw.default };
345
- case FIELD_TYPES.ENUM: {
346
- const options = raw.options;
347
- if (!Array.isArray(options) || options.length === 0)
348
- fail(vc, 'field.enum.options.required');
349
- if (!options.every((o) => typeof o === 'string'))
350
- fail(vc, 'field.enum.options.strings');
351
- const seen = new Set();
352
- for (const o of options) {
353
- if (seen.has(o))
354
- fail(vc, 'field.enum.options.duplicate', { value: o });
355
- seen.add(o);
356
- }
357
- const multiple = expectBoolean(raw, 'multiple', vc);
358
- if (multiple === true && primary === true)
359
- fail(vc, 'field.enum.multiple.primary');
360
- if (multiple === true && unique === true)
361
- fail(vc, 'field.enum.multiple.unique');
362
- if (multiple === true) {
363
- if (raw.default !== undefined) {
364
- if (!Array.isArray(raw.default) || !raw.default.every((v) => typeof v === 'string')) {
365
- fail(vc, 'field.enum.multiple.default.array');
366
- }
367
- for (const v of raw.default) {
368
- if (!options.includes(v))
369
- fail(vc, 'field.enum.multiple.default.inOptions', { value: v });
370
- }
371
- }
372
- }
373
- else {
374
- validateDefault(raw.default, type, vc, options);
375
- }
376
- return {
377
- ...base,
378
- type: FIELD_TYPES.ENUM,
379
- options: options,
380
- multiple,
381
- required,
382
- unique,
383
- default: multiple === true ? raw.default : raw.default,
384
- };
385
- }
386
- case FIELD_TYPES.RELATION: {
387
- const target = expectString(raw, 'target', vc);
388
- if (target === undefined)
389
- fail(vc, 'field.relation.target.required');
390
- if (!SNAKE_CASE.test(target))
391
- fail(vc, 'field.relation.target.snake', { target });
392
- const onDelete = expectString(raw, 'onDelete', vc);
393
- if (onDelete !== undefined && !ON_DELETE_ACTION_VALUES.includes(onDelete)) {
394
- fail(vc, 'field.relation.onDelete.invalid', { actions: ON_DELETE_ACTION_VALUES.join('/') });
395
- }
396
- return { ...base, type: FIELD_TYPES.RELATION, target, required, unique, onDelete };
397
- }
398
- case FIELD_TYPES.DETAILS: {
399
- const target = expectString(raw, 'target', vc);
400
- if (target === undefined)
401
- fail(vc, 'field.details.target.required');
402
- if (!SNAKE_CASE.test(target))
403
- fail(vc, 'field.details.target.snake', { target });
404
- return { ...base, type: FIELD_TYPES.DETAILS, target };
405
- }
406
- case FIELD_TYPES.MULTI_RELATION: {
407
- const target = expectString(raw, 'target', vc);
408
- if (target === undefined)
409
- fail(vc, 'field.multiRelation.target.required');
410
- if (!SNAKE_CASE.test(target))
411
- fail(vc, 'field.multiRelation.target.snake', { target });
412
- return { ...base, type: FIELD_TYPES.MULTI_RELATION, target, required };
413
- }
414
- case FIELD_TYPES.FIRST_NAME:
415
- case FIELD_TYPES.LAST_NAME:
416
- case FIELD_TYPES.EMAIL:
417
- case FIELD_TYPES.PHONE: {
418
- const minLength = expectPositiveInt(raw, 'minLength', vc);
419
- const maxLength = expectPositiveInt(raw, 'maxLength', vc);
420
- const regex = expectString(raw, 'regex', vc);
421
- if (regex !== undefined) {
422
- try {
423
- new RegExp(regex);
424
- }
425
- catch {
426
- fail(vc, 'field.regex.invalid');
427
- }
428
- }
429
- if (minLength !== undefined && maxLength !== undefined && minLength > maxLength) {
430
- fail(vc, 'field.minLengthGtMaxLength');
431
- }
432
- validateDefault(raw.default, type, vc);
433
- return {
434
- ...base,
435
- type,
436
- formula,
437
- minLength,
438
- maxLength,
439
- regex,
440
- required,
441
- unique,
442
- default: raw.default,
443
- };
444
- }
445
- case FIELD_TYPES.IMAGE: {
446
- const multiple = expectBoolean(raw, 'multiple', vc);
447
- if (multiple === true && primary === true)
448
- fail(vc, 'field.enum.multiple.primary');
449
- if (multiple === true && unique === true)
450
- fail(vc, 'field.enum.multiple.unique');
451
- validateDefault(raw.default, type, vc);
452
- return {
453
- ...base,
454
- type: FIELD_TYPES.IMAGE,
455
- required,
456
- unique,
457
- multiple,
458
- default: multiple === true ? raw.default : raw.default,
459
- };
460
- }
461
- case FIELD_TYPES.PERSON: {
462
- const target = expectString(raw, 'target', vc);
463
- if (target === undefined)
464
- fail(vc, 'field.relation.target.required');
465
- if (!SNAKE_CASE.test(target))
466
- fail(vc, 'field.relation.target.snake', { target });
467
- const onDelete = expectString(raw, 'onDelete', vc);
468
- if (onDelete !== undefined && !ON_DELETE_ACTION_VALUES.includes(onDelete)) {
469
- fail(vc, 'field.relation.onDelete.invalid', { actions: ON_DELETE_ACTION_VALUES.join('/') });
470
- }
471
- const department = expectString(raw, 'department', vc);
472
- return { ...base, type: FIELD_TYPES.PERSON, target, required, unique, onDelete, department };
473
- }
474
- case FIELD_TYPES.DEPARTMENT: {
475
- const target = expectString(raw, 'target', vc);
476
- if (target === undefined)
477
- fail(vc, 'field.relation.target.required');
478
- if (!SNAKE_CASE.test(target))
479
- fail(vc, 'field.relation.target.snake', { target });
480
- const onDelete = expectString(raw, 'onDelete', vc);
481
- if (onDelete !== undefined && !ON_DELETE_ACTION_VALUES.includes(onDelete)) {
482
- fail(vc, 'field.relation.onDelete.invalid', { actions: ON_DELETE_ACTION_VALUES.join('/') });
483
- }
484
- return { ...base, type: FIELD_TYPES.DEPARTMENT, target, required, unique, onDelete };
485
- }
486
- case FIELD_TYPES.SEQ_NO: {
487
- const format = expectString(raw, 'format', vc);
488
- if (format !== undefined) {
489
- if (!SEQUENCE_PLACEHOLDER.test(format)) {
490
- fail(vc, 'field.seqNo.format.seq');
491
- }
492
- for (const m of format.matchAll(/\{(\w+)(?::\d+)?\}/g)) {
493
- const placeholder = m[1] ?? '';
494
- if (!SEQUENCE_TOKEN_VALUES.includes(placeholder)) {
495
- fail(vc, 'field.seqNo.format.token', { token: placeholder });
496
- }
497
- }
498
- }
499
- const cycle = expectString(raw, 'cycle', vc);
500
- if (cycle !== undefined && !SEQUENCE_CYCLE_VALUES.includes(cycle)) {
501
- fail(vc, 'field.seqNo.cycle.invalid');
502
- }
503
- return { ...base, type: FIELD_TYPES.SEQ_NO, format, cycle: cycle };
504
- }
505
- }
506
- }
507
- /** validate the permissions map: role -> read/create/update/delete/fields.exclude */
508
- const READ_SCOPE_VALUES = Object.values(READ_SCOPES);
509
- function validatePermissions(raw, vc, fields) {
510
- if (raw === undefined)
511
- return undefined;
512
- if (!isRecord(raw))
513
- fail(vc, 'permission.notObject');
514
- const result = {};
515
- for (const [role, value] of Object.entries(raw)) {
516
- if (!isRecord(value))
517
- fail(vc, 'permission.role.notObject', { role });
518
- if (value.read !== undefined && !READ_SCOPE_VALUES.includes(value.read)) {
519
- fail(vc, 'permission.read.invalid', { role });
520
- }
521
- if (value.create !== undefined && typeof value.create !== 'boolean')
522
- fail(vc, 'permission.create.boolean', { role });
523
- if (value.delete !== undefined && typeof value.delete !== 'boolean')
524
- fail(vc, 'permission.delete.boolean', { role });
525
- if (value.update !== undefined) {
526
- if (typeof value.update !== 'boolean' && (!Array.isArray(value.update) || !value.update.every((f) => typeof f === 'string'))) {
527
- fail(vc, 'permission.update.invalid', { role });
528
- }
529
- }
530
- if (value.fields !== undefined) {
531
- if (!isRecord(value.fields))
532
- fail(vc, 'permission.fields.notObject', { role });
533
- if (value.fields.exclude !== undefined && (!Array.isArray(value.fields.exclude) || !value.fields.exclude.every((f) => typeof f === 'string'))) {
534
- fail(vc, 'permission.exclude.strings', { role });
535
- }
536
- if (value.fields.create !== undefined) {
537
- if (!Array.isArray(value.fields.create) || !value.fields.create.every((f) => typeof f === 'string')) {
538
- fail(vc, 'permission.createFields.strings', { role });
539
- }
540
- validateCreateFields(role, value.fields.create, fields, vc);
541
- }
542
- }
543
- result[role] = {
544
- read: value.read,
545
- create: value.create,
546
- update: value.update,
547
- delete: value.delete,
548
- fields: value.fields,
549
- };
550
- }
551
- return result;
552
- }
553
- /** a create whitelist must name real fields and cover every required writable field */
554
- function validateCreateFields(role, create, fields, vc) {
555
- const byName = new Map(fields.map((f) => [f.name, f]));
556
- const pk = fields.find((f) => f.primary)?.name;
557
- for (const name of create) {
558
- if (!byName.has(name))
559
- fail(vc, 'permission.createFields.unknown', { role, field: name });
560
- }
561
- for (const field of fields) {
562
- if (field.primary === true || field.name === pk)
563
- continue;
564
- const required = 'required' in field && field.required === true;
565
- const readonly = field.system === true || field.type === FIELD_TYPES.SEQ_NO || field.formula !== undefined;
566
- if (!required || readonly)
567
- continue;
568
- if (!create.includes(field.name)) {
569
- fail(vc, 'permission.createFields.missingRequired', { role, field: field.name });
570
- }
571
- }
572
- }
573
- /** validate the optional indexes array (btree/gin/gist + non-empty string fields) */
574
- function validateIndexes(raw, vc) {
575
- if (raw === undefined)
576
- return undefined;
577
- if (!Array.isArray(raw))
578
- fail(vc, 'index.notArray');
579
- return raw.map((entry, i) => {
580
- if (!isRecord(entry))
581
- fail(vc, 'index.notObject', { i });
582
- const type = entry.type;
583
- if (typeof type !== 'string' || !INDEX_TYPE_VALUES.includes(type)) {
584
- fail(vc, 'index.type.invalid', { i, types: INDEX_TYPE_VALUES.join('/') });
585
- }
586
- if (!Array.isArray(entry.fields) || entry.fields.length === 0 || !entry.fields.every((f) => typeof f === 'string')) {
587
- fail(vc, 'index.fields.invalid', { i });
588
- }
589
- return { type: type, fields: entry.fields };
590
- });
591
- }
592
1
  /**
593
- * Validate a raw object definition (single object, self-contained checks only).
594
- * Includes field validation, primary-key rules, titleTemplate placeholder checks
595
- * and same-object formula validation. Cross-object checks live in buildGraph.
2
+ * Object validation entry point. The implementation is split by concern under
3
+ * `./validate/` (primitives, labels, field, permissions, indexes, formulas,
4
+ * object); this barrel keeps the stable `./validate.js` import path.
596
5
  */
597
- export function validateObject(raw, options) {
598
- const vc = { object: '(unknown)', locale: options?.locale ?? DEFAULT_LOCALE };
599
- if (!isRecord(raw))
600
- fail(vc, 'object.notObject');
601
- const name = expectString(raw, 'name', vc);
602
- if (name === undefined)
603
- fail(vc, 'object.name.required');
604
- vc.object = name;
605
- if (!SNAKE_CASE.test(name))
606
- fail(vc, 'object.name.snake', { name });
607
- if (options?.nameHint !== undefined && options.nameHint !== name) {
608
- fail(vc, 'object.nameHint.mismatch', { dir: options.nameHint, name });
609
- }
610
- // on-disk format version gate (fail-closed on unknown/future versions)
611
- let schemaVersion;
612
- const rawVersion = raw.schemaVersion;
613
- if (rawVersion !== undefined) {
614
- if (typeof rawVersion !== 'number' ||
615
- !Number.isInteger(rawVersion) ||
616
- rawVersion < 1 ||
617
- rawVersion > SCHEMA_FORMAT_VERSION) {
618
- fail(vc, 'schema.version.unsupported', {
619
- version: String(rawVersion),
620
- supported: SCHEMA_FORMAT_VERSION,
621
- });
622
- }
623
- schemaVersion = rawVersion;
624
- }
625
- if (raw.label !== undefined)
626
- fail(vc, 'object.label.removed');
627
- const labels = validateLabels(raw.labels, vc);
628
- const description = expectString(raw, 'description', vc);
629
- const rawFields = raw.fields;
630
- if (!Array.isArray(rawFields) || rawFields.length === 0)
631
- fail(vc, 'object.fields.required');
632
- const fields = rawFields.map((f) => validateField(f, vc, options?.allowedFieldTypes));
633
- const seen = new Set();
634
- for (const field of fields) {
635
- if (seen.has(field.name))
636
- fail(vc, 'object.field.duplicate', { field: field.name });
637
- seen.add(field.name);
638
- }
639
- validateFormulas(fields, vc);
640
- const primaries = fields.filter((f) => f.primary);
641
- if (primaries.length === 0)
642
- fail(vc, 'object.primary.none');
643
- if (primaries.length > 1)
644
- fail(vc, 'object.primary.many');
645
- const primaryField = primaries[0];
646
- if (primaryField !== undefined && !SCALAR_TYPE_VALUES.includes(primaryField.type)) {
647
- fail(vc, 'object.primary.scalarOnly', { type: primaryField.type });
648
- }
649
- if (primaryField !== undefined && primaryField.formula !== undefined) {
650
- fail(vc, 'formula.excludedAttr', { attr: 'primary' });
651
- }
652
- const permissions = validatePermissions(raw.permissions, vc, fields);
653
- const indexes = validateIndexes(raw.indexes, vc);
654
- // alter: opt-in additive auto-DDL for an existing table (default false)
655
- if (raw.alter !== undefined && typeof raw.alter !== 'boolean')
656
- fail(vc, 'object.alter.boolean');
657
- const alter = raw.alter === true;
658
- const titleTemplate = validateTitleTemplate(raw.titleTemplate, fields, vc);
659
- const ownershipFields = fields.filter((f) => f.ownership === true);
660
- if (ownershipFields.length > 1)
661
- fail(vc, 'field.ownership.many');
662
- const teamFields = fields.filter((f) => f.team === true);
663
- if (teamFields.length > 1)
664
- fail(vc, 'field.team.many');
665
- if (permissions !== undefined) {
666
- const hasOwnership = ownershipFields.length > 0;
667
- const hasTeam = teamFields.length > 0;
668
- for (const [role, p] of Object.entries(permissions)) {
669
- if (p.read === READ_SCOPES.OWN && !hasOwnership)
670
- fail(vc, 'permission.own.ownershipField', { role });
671
- if (p.read === READ_SCOPES.TEAM && !hasTeam)
672
- fail(vc, 'permission.team.teamField', { role });
673
- }
674
- }
675
- return {
676
- schemaVersion,
677
- name,
678
- labels,
679
- description,
680
- fields,
681
- permissions,
682
- workflow: raw.workflow,
683
- indexes,
684
- titleTemplate,
685
- alter,
686
- };
687
- }
688
- /**
689
- * Same-object formula validation: exclusions, reference existence, operand/type
690
- * checks and local formula-formula cycle detection.
691
- */
692
- function validateFormulas(fields, vc) {
693
- const formulaFields = fields.filter((f) => f.formula !== undefined);
694
- if (formulaFields.length === 0)
695
- return;
696
- const fieldByName = new Map(fields.map((f) => [f.name, f]));
697
- const fieldTypeOf = (name) => {
698
- const f = fieldByName.get(name);
699
- if (f === undefined)
700
- return undefined;
701
- switch (f.type) {
702
- case FIELD_TYPES.STRING:
703
- case FIELD_TYPES.TEXT:
704
- return FORMULA_TYPES.STRING;
705
- case FIELD_TYPES.INTEGER:
706
- case FIELD_TYPES.NUMBER:
707
- case FIELD_TYPES.CURRENCY:
708
- return FORMULA_TYPES.NUMBER;
709
- case FIELD_TYPES.BOOLEAN:
710
- return FORMULA_TYPES.BOOLEAN;
711
- default:
712
- return undefined;
713
- }
714
- };
715
- for (const field of formulaFields) {
716
- const formula = field.formula;
717
- const fieldType = field.type;
718
- for (const attr of Object.values(FORMULA_EXCLUDED_ATTRS)) {
719
- if (field[attr] !== undefined) {
720
- fail(vc, 'formula.excludedAttr', { attr });
721
- }
722
- }
723
- let ast;
724
- try {
725
- ast = parseFormula(formula);
726
- }
727
- catch (err) {
728
- fail(vc, 'formula.syntax', { detail: err.message });
729
- }
730
- const { fields: refs, aggregates } = extractRefs(ast);
731
- for (const ref of refs) {
732
- const target = fieldByName.get(ref.parent ?? ref.name);
733
- if (target === undefined) {
734
- fail(vc, 'formula.refMissing', { field: ref.parent ?? ref.name });
735
- }
736
- const referenced = ref.parent === null ? target : fieldByName.get(ref.parent);
737
- if (referenced !== undefined && ref.parent === null) {
738
- if (referenced.type === FIELD_TYPES.RELATION ||
739
- referenced.type === FIELD_TYPES.DETAILS ||
740
- referenced.type === FIELD_TYPES.MULTI_RELATION ||
741
- referenced.type === FIELD_TYPES.SEQ_NO) {
742
- fail(vc, 'formula.refType', { type: referenced.type, field: ref.name });
743
- }
744
- }
745
- else if (referenced !== undefined && ref.parent !== null) {
746
- if (referenced.type !== FIELD_TYPES.RELATION && referenced.type !== FIELD_TYPES.DETAILS) {
747
- fail(vc, 'formula.refType', { type: referenced.type, field: ref.parent });
748
- }
749
- }
750
- }
751
- for (const agg of aggregates) {
752
- const parent = fieldByName.get(agg.parent);
753
- if (parent === undefined) {
754
- fail(vc, 'formula.refMissing', { field: agg.parent });
755
- }
756
- else if (parent.type !== FIELD_TYPES.DETAILS) {
757
- fail(vc, 'formula.refType', { type: parent.type, field: agg.parent });
758
- }
759
- }
760
- try {
761
- checkOperands(ast, fieldTypeOf);
762
- }
763
- catch (err) {
764
- if (err instanceof FormulaOperandError) {
765
- fail(vc, 'formula.operandType', { op: err.op, expected: err.expected, got: err.got });
766
- }
767
- fail(vc, 'formula.syntax', { detail: err.message });
768
- }
769
- const exprType = inferType(ast, fieldTypeOf);
770
- if (exprType !== 'any' && !typeCompatible(fieldType, exprType)) {
771
- fail(vc, 'formula.typeMismatch', { got: exprType, expected: fieldType });
772
- }
773
- }
774
- const nodes = formulaFields.map((f) => f.name);
775
- const edges = [];
776
- for (const field of formulaFields) {
777
- const formula = field.formula;
778
- let ast;
779
- try {
780
- ast = parseFormula(formula);
781
- }
782
- catch {
783
- continue;
784
- }
785
- const { fields: refs } = extractRefs(ast);
786
- for (const ref of refs) {
787
- if (ref.parent === null && nodes.includes(ref.name)) {
788
- edges.push([field.name, ref.name]);
789
- }
790
- }
791
- }
792
- const cycle = detectCycle(nodes, edges);
793
- if (cycle)
794
- fail(vc, 'formula.cycle', { chain: cycle.join(' -> ') });
795
- }
796
- /** validate titleTemplate placeholders reference existing scalar/seq_no fields */
797
- function validateTitleTemplate(raw, fields, vc) {
798
- if (raw === undefined)
799
- return undefined;
800
- if (typeof raw !== 'string')
801
- fail(vc, 'title.notString');
802
- const fieldMap = new Map(fields.map((f) => [f.name, f]));
803
- for (const m of raw.matchAll(TITLE_PLACEHOLDER_RE)) {
804
- const fieldName = m[1] ?? '';
805
- const field = fieldMap.get(fieldName);
806
- if (field === undefined) {
807
- fail(vc, 'title.placeholder.missing', { field: fieldName });
808
- }
809
- else if (field.type === FIELD_TYPES.DETAILS ||
810
- field.type === FIELD_TYPES.MULTI_RELATION) {
811
- fail(vc, 'title.placeholder.relation', { field: fieldName, type: field.type });
812
- }
813
- else if (field.type === FIELD_TYPES.ENUM && field.multiple === true) {
814
- fail(vc, 'title.placeholder.multiEnum', { field: fieldName });
815
- }
816
- }
817
- return raw;
818
- }
6
+ export { validateObject } from './validate/object.js';
819
7
  //# sourceMappingURL=validate.js.map