@stndrds/schema 1.0.0-alpha.199 → 1.0.0-alpha.201

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.
@@ -0,0 +1,509 @@
1
+ import { z } from 'zod';
2
+
3
+ // src/validation/config/schemas.ts
4
+
5
+ // src/computed/parser.ts
6
+ var ComputedFormulaParseError = class extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ComputedFormulaParseError";
10
+ }
11
+ };
12
+ function parseComputedFormula(expression) {
13
+ const parser = new Parser(tokenize(expression));
14
+ const ast = parser.parseExpression();
15
+ parser.expectEnd();
16
+ return ast;
17
+ }
18
+ function tokenize(expression) {
19
+ const tokens = [];
20
+ let index = 0;
21
+ while (index < expression.length) {
22
+ const char = expression[index];
23
+ if (char === " " || char === " " || char === "\n" || char === "\r") {
24
+ index += 1;
25
+ continue;
26
+ }
27
+ if (char === '"' || char === "'") {
28
+ const result = scanString(expression, index, char);
29
+ tokens.push({ kind: "string", value: result.value, position: index });
30
+ index = result.nextIndex;
31
+ continue;
32
+ }
33
+ if (isDigit(char)) {
34
+ const result = scanNumber(expression, index);
35
+ tokens.push({ kind: "number", value: result.value, position: index });
36
+ index = result.nextIndex;
37
+ continue;
38
+ }
39
+ if (isIdentifierStart(char)) {
40
+ const result = scanIdentifier(expression, index);
41
+ const canonical = result.value.toLowerCase();
42
+ if (canonical === "true") {
43
+ tokens.push({ kind: "boolean", value: true, position: index });
44
+ } else if (canonical === "false") {
45
+ tokens.push({ kind: "boolean", value: false, position: index });
46
+ } else if (canonical === "null") {
47
+ tokens.push({ kind: "null", position: index });
48
+ } else {
49
+ tokens.push({ kind: "identifier", value: result.value, position: index });
50
+ }
51
+ index = result.nextIndex;
52
+ continue;
53
+ }
54
+ const twoChar = expression.slice(index, index + 2);
55
+ if (twoChar === ">=" || twoChar === "<=" || twoChar === "==" || twoChar === "!=") {
56
+ tokens.push({ kind: "operator", value: twoChar, position: index });
57
+ index += 2;
58
+ continue;
59
+ }
60
+ if (char === ">" || char === "<" || char === "+" || char === "-" || char === "*" || char === "/") {
61
+ tokens.push({ kind: "operator", value: char, position: index });
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (char === "(" || char === ")" || char === "," || char === ".") {
66
+ tokens.push({ kind: "punctuation", value: char, position: index });
67
+ index += 1;
68
+ continue;
69
+ }
70
+ throw new ComputedFormulaParseError(`Unexpected character "${char}" at position ${index}`);
71
+ }
72
+ return tokens;
73
+ }
74
+ function scanString(expression, startIndex, delimiter = '"') {
75
+ let value = "";
76
+ let index = startIndex + 1;
77
+ while (index < expression.length) {
78
+ const char = expression[index];
79
+ if (char === delimiter) {
80
+ return { value, nextIndex: index + 1 };
81
+ }
82
+ if (char === "\\") {
83
+ const escaped = expression[index + 1];
84
+ if (escaped === void 0) {
85
+ throw new ComputedFormulaParseError(`Unterminated string literal at position ${index}`);
86
+ }
87
+ value += decodeEscape(escaped, index);
88
+ index += 2;
89
+ continue;
90
+ }
91
+ value += char;
92
+ index += 1;
93
+ }
94
+ throw new ComputedFormulaParseError(`Unterminated string literal at position ${startIndex}`);
95
+ }
96
+ function decodeEscape(char, position) {
97
+ switch (char) {
98
+ case '"':
99
+ case "\\":
100
+ return char;
101
+ case "n":
102
+ return "\n";
103
+ case "r":
104
+ return "\r";
105
+ case "t":
106
+ return " ";
107
+ default:
108
+ throw new ComputedFormulaParseError(
109
+ `Unknown escape sequence "\\${char}" at position ${position}`
110
+ );
111
+ }
112
+ }
113
+ function scanNumber(expression, startIndex) {
114
+ let index = startIndex;
115
+ while (index < expression.length && isDigit(expression[index])) {
116
+ index += 1;
117
+ }
118
+ if (expression[index] === ".") {
119
+ const decimalStart = index;
120
+ index += 1;
121
+ if (!isDigit(expression[index])) {
122
+ throw new ComputedFormulaParseError(`Invalid number literal at position ${decimalStart}`);
123
+ }
124
+ while (index < expression.length && isDigit(expression[index])) {
125
+ index += 1;
126
+ }
127
+ }
128
+ const raw = expression.slice(startIndex, index);
129
+ const value = Number(raw);
130
+ if (!Number.isFinite(value)) {
131
+ throw new ComputedFormulaParseError(`Invalid number literal "${raw}"`);
132
+ }
133
+ return { value, nextIndex: index };
134
+ }
135
+ function scanIdentifier(expression, startIndex) {
136
+ let index = startIndex + 1;
137
+ while (index < expression.length && isIdentifierPart(expression[index])) {
138
+ index += 1;
139
+ }
140
+ return { value: expression.slice(startIndex, index), nextIndex: index };
141
+ }
142
+ function isDigit(char) {
143
+ return char !== void 0 && char >= "0" && char <= "9";
144
+ }
145
+ function isIdentifierStart(char) {
146
+ return char !== void 0 && /[A-Za-z_]/.test(char);
147
+ }
148
+ function isIdentifierPart(char) {
149
+ return char !== void 0 && /[A-Za-z0-9_]/.test(char);
150
+ }
151
+ var Parser = class {
152
+ constructor(tokens) {
153
+ this.tokens = tokens;
154
+ this.position = 0;
155
+ }
156
+ parseExpression() {
157
+ return this.parseBinaryExpression(0);
158
+ }
159
+ expectEnd() {
160
+ const token = this.peek();
161
+ if (token !== void 0) {
162
+ throw new ComputedFormulaParseError(
163
+ `Unexpected token ${describeToken(token)} at position ${token.position}`
164
+ );
165
+ }
166
+ }
167
+ parseBinaryExpression(minPrecedence) {
168
+ let left = this.parsePrimary();
169
+ while (true) {
170
+ const token = this.peek();
171
+ if (token?.kind !== "operator") {
172
+ return left;
173
+ }
174
+ const precedence = getOperatorPrecedence(token.value);
175
+ if (precedence < minPrecedence) {
176
+ return left;
177
+ }
178
+ const operator = token.value;
179
+ this.consume();
180
+ const right = this.parseBinaryExpression(precedence + 1);
181
+ left = { kind: "binary", operator, left, right };
182
+ }
183
+ }
184
+ parsePrimary() {
185
+ const token = this.consume();
186
+ if (token === void 0) {
187
+ throw new ComputedFormulaParseError("Unexpected end of expression");
188
+ }
189
+ switch (token.kind) {
190
+ case "string":
191
+ case "number":
192
+ case "boolean":
193
+ return { kind: "literal", value: token.value };
194
+ case "null":
195
+ return { kind: "literal", value: null };
196
+ case "identifier":
197
+ return this.parseIdentifier(token.value);
198
+ case "punctuation":
199
+ if (token.value === "(") {
200
+ const expression = this.parseExpression();
201
+ this.expectPunctuation(")");
202
+ return expression;
203
+ }
204
+ break;
205
+ case "operator":
206
+ if (token.value === "-") {
207
+ const next = this.consume();
208
+ if (next?.kind === "number") {
209
+ return { kind: "literal", value: -next.value };
210
+ }
211
+ }
212
+ break;
213
+ }
214
+ throw new ComputedFormulaParseError(
215
+ `Expected literal, path, call, or parenthesized expression at position ${token.position}`
216
+ );
217
+ }
218
+ parseIdentifier(identifier) {
219
+ if (this.matchPunctuation("(")) {
220
+ const args = [];
221
+ if (!this.matchPunctuation(")")) {
222
+ do {
223
+ args.push(this.parseExpression());
224
+ } while (this.matchPunctuation(","));
225
+ this.expectPunctuation(")");
226
+ }
227
+ return { kind: "call", functionName: identifier.toLowerCase(), args };
228
+ }
229
+ const parts = [identifier];
230
+ while (this.matchPunctuation(".")) {
231
+ const next = this.consume();
232
+ if (next?.kind !== "identifier") {
233
+ const position = next?.position ?? this.previousPosition();
234
+ throw new ComputedFormulaParseError(
235
+ `Expected attribute name after path separator at position ${position}`
236
+ );
237
+ }
238
+ parts.push(next.value);
239
+ }
240
+ return { kind: "path", parts };
241
+ }
242
+ expectPunctuation(value) {
243
+ if (!this.matchPunctuation(value)) {
244
+ const token = this.peek();
245
+ const position = token?.position ?? this.previousPosition();
246
+ throw new ComputedFormulaParseError(`Expected "${value}" at position ${position}`);
247
+ }
248
+ }
249
+ matchPunctuation(value) {
250
+ const token = this.peek();
251
+ if (token?.kind === "punctuation" && token.value === value) {
252
+ this.consume();
253
+ return true;
254
+ }
255
+ return false;
256
+ }
257
+ peek() {
258
+ return this.tokens[this.position];
259
+ }
260
+ consume() {
261
+ const token = this.tokens[this.position];
262
+ this.position += 1;
263
+ return token;
264
+ }
265
+ previousPosition() {
266
+ const previous = this.tokens[this.position - 1];
267
+ return previous?.position ?? 0;
268
+ }
269
+ };
270
+ function describeToken(token) {
271
+ switch (token.kind) {
272
+ case "identifier":
273
+ case "number":
274
+ case "string":
275
+ case "boolean":
276
+ case "operator":
277
+ case "punctuation":
278
+ return `"${String(token.value)}"`;
279
+ case "null":
280
+ return '"null"';
281
+ default: {
282
+ const exhaustive = token;
283
+ return exhaustive;
284
+ }
285
+ }
286
+ }
287
+ function getOperatorPrecedence(operator) {
288
+ switch (operator) {
289
+ case "*":
290
+ case "/":
291
+ return 3;
292
+ case "+":
293
+ case "-":
294
+ return 2;
295
+ case ">":
296
+ case "<":
297
+ case ">=":
298
+ case "<=":
299
+ case "==":
300
+ case "!=":
301
+ return 1;
302
+ default: {
303
+ const exhaustive = operator;
304
+ return exhaustive;
305
+ }
306
+ }
307
+ }
308
+
309
+ // src/validation/config/schemas.ts
310
+ var baseConfigSchema = z.object({
311
+ placeholder: z.string().optional(),
312
+ description: z.string().optional(),
313
+ defaultValue: z.unknown().optional(),
314
+ icon: z.string().optional(),
315
+ order: z.number().int().optional(),
316
+ hidden: z.boolean().optional(),
317
+ archived: z.boolean().optional(),
318
+ deprecated: z.boolean().optional(),
319
+ metadata: z.record(z.string(), z.unknown()).optional()
320
+ });
321
+ var optionSchema = z.object({
322
+ value: z.string().min(1),
323
+ label: z.string().min(1),
324
+ color: z.string().optional(),
325
+ description: z.string().optional(),
326
+ group: z.enum(["idle", "in_progress", "finished"]).optional(),
327
+ inverse: z.string().optional(),
328
+ archived: z.boolean().optional()
329
+ }).strict();
330
+ var optionsArraySchema = z.array(optionSchema).min(1).refine(
331
+ (options) => {
332
+ const values = options.map((o) => o.value);
333
+ return new Set(values).size === values.length;
334
+ },
335
+ { message: "Duplicate option values are not allowed" }
336
+ );
337
+ var relationTargetSchema = z.object({
338
+ object: z.string().min(1),
339
+ displayTemplate: z.string().optional(),
340
+ filter: z.record(z.string(), z.unknown()).optional()
341
+ });
342
+ var bilateralConfigSchema = z.object({
343
+ object: z.string().min(1),
344
+ attribute: z.string().min(1),
345
+ cardinality: z.enum(["one", "many"]).optional(),
346
+ storageOwner: z.boolean().optional()
347
+ });
348
+ var computedOptionsSourceSchema = z.object({
349
+ objectName: z.string().min(1),
350
+ attributeName: z.string().min(1),
351
+ attributeId: z.string().optional()
352
+ });
353
+ var textConfigSchema = baseConfigSchema.extend({
354
+ multiline: z.boolean().optional(),
355
+ minLength: z.number().int().min(0).optional(),
356
+ maxLength: z.number().int().min(1).optional(),
357
+ pattern: z.string().optional(),
358
+ format: z.enum(["email", "url", "slug"]).optional()
359
+ });
360
+ var richtextConfigSchema = baseConfigSchema;
361
+ var numberConfigSchema = baseConfigSchema.extend({
362
+ renderAs: z.enum(["number", "rating"]).optional(),
363
+ min: z.number().optional(),
364
+ max: z.number().optional(),
365
+ unit: z.enum(["integer", "decimal", "percentage"]).optional(),
366
+ decimals: z.number().int().min(0).max(10).optional()
367
+ });
368
+ var checkboxConfigSchema = baseConfigSchema;
369
+ var dateConfigSchema = baseConfigSchema.extend({
370
+ dateFormat: z.enum(["short", "long", "full", "relative"]).optional(),
371
+ minDate: z.string().optional(),
372
+ maxDate: z.string().optional()
373
+ });
374
+ var phoneConfigSchema = baseConfigSchema.extend({
375
+ defaultCountryCode: z.string().length(3).optional()
376
+ });
377
+ var currencyConfigSchema = baseConfigSchema.extend({
378
+ defaultCurrency: z.string().length(3).optional(),
379
+ allowedCurrencies: z.array(z.string().length(3)).optional(),
380
+ allowNegative: z.boolean().optional()
381
+ });
382
+ var statusConfigSchema = baseConfigSchema.extend({
383
+ options: optionsArraySchema
384
+ });
385
+ var locationConfigSchema = baseConfigSchema.extend({
386
+ granularity: z.enum(["full", "address", "city", "state", "country", "coordinates"]).optional(),
387
+ defaultCountry: z.string().length(3).optional(),
388
+ allowedCountries: z.array(z.string().length(3)).optional()
389
+ });
390
+ var selectConfigSchema = baseConfigSchema.extend({
391
+ options: optionsArraySchema
392
+ });
393
+ var multiselectConfigSchema = baseConfigSchema.extend({
394
+ options: optionsArraySchema
395
+ });
396
+ var fileConfigSchema = baseConfigSchema.extend({
397
+ maxFiles: z.number().int().min(1).optional(),
398
+ maxSize: z.number().int().min(1).optional(),
399
+ allowedTypes: z.array(z.string()).optional(),
400
+ multiple: z.boolean().optional()
401
+ });
402
+ var userConfigSchema = baseConfigSchema.extend({
403
+ types: z.array(z.enum(["user", "agent"])).min(1).optional(),
404
+ multiple: z.boolean().optional()
405
+ });
406
+ var relationConfigSchema = baseConfigSchema.extend({
407
+ targets: z.array(relationTargetSchema).min(1),
408
+ cardinality: z.enum(["one", "many"]),
409
+ maxItems: z.number().int().min(1).optional(),
410
+ bilateral: bilateralConfigSchema.optional(),
411
+ /**
412
+ * PropertySchema declared via `.qualifyWith()`. Pass-through; structural
413
+ * validation happens at the builder level — see `propertySchemaPassthrough`
414
+ * comment further below for the rationale.
415
+ */
416
+ properties: z.object({ definitions: z.array(z.record(z.string(), z.unknown())) }).passthrough().optional()
417
+ });
418
+ var formulaConfigSchema = baseConfigSchema.extend({
419
+ // Validate the expression grammar up-front so an assignment-style "=" (or any
420
+ // other malformed syntax) is rejected at save time instead of crashing the
421
+ // computed write-path later. Type/attribute coherence is still checked
422
+ // downstream by compileComputedFormula, which needs the full schema.
423
+ expression: z.string().min(1).superRefine((value, ctx) => {
424
+ try {
425
+ parseComputedFormula(value);
426
+ } catch (error) {
427
+ ctx.addIssue({
428
+ code: z.ZodIssueCode.custom,
429
+ message: error instanceof Error ? error.message : "Invalid formula expression"
430
+ });
431
+ }
432
+ }),
433
+ returnType: z.enum(["text", "number", "boolean", "date", "select", "multiselect"]),
434
+ decimals: z.number().int().min(0).max(10).optional(),
435
+ allowRelations: z.boolean().optional(),
436
+ optionsSource: computedOptionsSourceSchema.optional()
437
+ });
438
+ var rollupConfigSchema = baseConfigSchema.extend({
439
+ relationAttribute: z.string().min(1).optional(),
440
+ relationPath: z.string().optional(),
441
+ targetAttribute: z.string().min(1),
442
+ function: z.enum([
443
+ "sum",
444
+ "avg",
445
+ "earliest",
446
+ "latest",
447
+ "count",
448
+ "countValues",
449
+ "countUniqueValues",
450
+ "countEmpty",
451
+ "percentEmpty",
452
+ "percentNotEmpty",
453
+ "original"
454
+ ]),
455
+ decimals: z.number().int().min(0).max(10).optional(),
456
+ targetAttributeType: z.string().optional(),
457
+ targetAttributeOptions: z.array(optionSchema).optional(),
458
+ optionsSource: computedOptionsSourceSchema.optional()
459
+ });
460
+ var propertySchemaPassthrough = z.object({ definitions: z.array(z.record(z.string(), z.unknown())) }).passthrough();
461
+ var documentSlotConfigPassthrough = z.object({
462
+ name: z.string(),
463
+ label: z.string().optional(),
464
+ description: z.string().optional(),
465
+ required: z.boolean().optional(),
466
+ acceptedMimeTypes: z.array(z.string()).optional(),
467
+ maxSizeBytes: z.number().optional()
468
+ }).passthrough();
469
+ var documentConfigSchema = baseConfigSchema.extend({
470
+ /**
471
+ * PropertySchema declared via `.qualifyWith()` on a document attribute.
472
+ * Drives hydration into `[{id, props}]` shape and write-time validation.
473
+ */
474
+ properties: propertySchemaPassthrough.optional(),
475
+ /** File slots declared via `.slots(...)`. */
476
+ slots: z.array(documentSlotConfigPassthrough).optional()
477
+ });
478
+ var attributeConfigSchemas = {
479
+ text: textConfigSchema,
480
+ richtext: richtextConfigSchema,
481
+ number: numberConfigSchema,
482
+ checkbox: checkboxConfigSchema,
483
+ date: dateConfigSchema,
484
+ phone: phoneConfigSchema,
485
+ currency: currencyConfigSchema,
486
+ status: statusConfigSchema,
487
+ location: locationConfigSchema,
488
+ select: selectConfigSchema,
489
+ multiselect: multiselectConfigSchema,
490
+ file: fileConfigSchema,
491
+ user: userConfigSchema,
492
+ relation: relationConfigSchema,
493
+ formula: formulaConfigSchema,
494
+ rollup: rollupConfigSchema,
495
+ document: documentConfigSchema
496
+ };
497
+ function getAttributeConfigSchema(type) {
498
+ const schema = attributeConfigSchemas[type];
499
+ if (!schema) {
500
+ throw new Error(`Unsupported attribute type: ${type}`);
501
+ }
502
+ return schema;
503
+ }
504
+ function parseAttributeConfig(type, config) {
505
+ const schema = getAttributeConfigSchema(type);
506
+ return schema.strip().parse(config);
507
+ }
508
+
509
+ export { ComputedFormulaParseError, attributeConfigSchemas, getAttributeConfigSchema, parseAttributeConfig, parseComputedFormula };