@sequenceholdings/orm 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/dist/authoring.cjs +796 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +11 -2
- package/dist/authoring.d.ts +11 -2
- package/dist/authoring.js +796 -7
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +777 -6
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +43 -43
- package/dist/canonical.d.ts +43 -43
- package/dist/canonical.js +777 -6
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7561 -7502
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7561 -7502
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +801 -33
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +1 -1
- package/dist/compiler.d.ts +1 -1
- package/dist/compiler.js +801 -33
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +3716 -3445
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -30
- package/dist/index.d.ts +95 -30
- package/dist/index.js +3711 -3444
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +778 -7
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +58 -58
- package/dist/ir.d.ts +58 -58
- package/dist/ir.js +778 -7
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +793 -23
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +793 -23
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +786 -17
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.js +786 -17
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/dist/{definition-B99BsOAO.d.cts → definition-CevES7lM.d.cts} +2 -2
- package/dist/{definition-B99BsOAO.d.ts → definition-CevES7lM.d.ts} +2 -2
package/dist/compiler.js
CHANGED
|
@@ -7,16 +7,21 @@ import { PgParser } from '@supabase/pg-parser';
|
|
|
7
7
|
|
|
8
8
|
// src/constants.ts
|
|
9
9
|
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
10
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
10
11
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
11
12
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
12
13
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
13
14
|
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
15
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
14
16
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
17
|
+
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
15
18
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
16
19
|
var ormSchemaName = (namespace) => {
|
|
17
20
|
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
18
21
|
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
19
22
|
};
|
|
23
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
24
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
20
25
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
21
26
|
var CELL_PART_SUFFIX = {
|
|
22
27
|
at: "__at",
|
|
@@ -248,6 +253,748 @@ var Parser = class {
|
|
|
248
253
|
function compileCheckExpression(source, columns) {
|
|
249
254
|
return new Parser(tokenize(source), columns).parse();
|
|
250
255
|
}
|
|
256
|
+
|
|
257
|
+
// src/predicate/ast.ts
|
|
258
|
+
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
259
|
+
function walkPredicate(predicate, visit2) {
|
|
260
|
+
visit2(predicate);
|
|
261
|
+
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
262
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit2);
|
|
263
|
+
}
|
|
264
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
|
|
265
|
+
}
|
|
266
|
+
function predicateOperands(predicate) {
|
|
267
|
+
const operands = [];
|
|
268
|
+
walkPredicate(predicate, (node) => {
|
|
269
|
+
if (node.kind === "compare") operands.push(node.left, node.right);
|
|
270
|
+
});
|
|
271
|
+
return operands;
|
|
272
|
+
}
|
|
273
|
+
function referencedColumns(predicate) {
|
|
274
|
+
const names = predicateOperands(predicate).flatMap(
|
|
275
|
+
(operand) => operand.kind === "column" ? [operand.name] : []
|
|
276
|
+
);
|
|
277
|
+
return [...new Set(names)];
|
|
278
|
+
}
|
|
279
|
+
var identifierSchema = z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
280
|
+
var namespaceSchema = z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
281
|
+
var tableReferenceSchema = z.string().refine(
|
|
282
|
+
(value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
|
|
283
|
+
"must be a table name or a schema-qualified table reference"
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
// src/schema/columns.ts
|
|
287
|
+
var COLUMN_TYPES = [
|
|
288
|
+
"uuid",
|
|
289
|
+
"text",
|
|
290
|
+
"integer",
|
|
291
|
+
"bigint",
|
|
292
|
+
"numeric",
|
|
293
|
+
"double",
|
|
294
|
+
"boolean",
|
|
295
|
+
"timestamptz",
|
|
296
|
+
"date",
|
|
297
|
+
"jsonb"
|
|
298
|
+
];
|
|
299
|
+
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
300
|
+
var FUNCTION_DEFAULTS = {
|
|
301
|
+
"gen_random_uuid()": ["uuid"],
|
|
302
|
+
"now()": ["timestamptz", "date"]
|
|
303
|
+
};
|
|
304
|
+
var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
|
|
305
|
+
function defaultIssue(value, type) {
|
|
306
|
+
if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
|
|
307
|
+
if (typeof value === "number") {
|
|
308
|
+
if (!["integer", "bigint", "numeric", "double"].includes(type)) {
|
|
309
|
+
return `numeric default is invalid for type '${type}'`;
|
|
310
|
+
}
|
|
311
|
+
if (["integer", "bigint"].includes(type)) {
|
|
312
|
+
if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
|
|
313
|
+
if (!Number.isSafeInteger(value)) {
|
|
314
|
+
return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
318
|
+
return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const allowedTypes = FUNCTION_DEFAULTS[value];
|
|
323
|
+
if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
var columnSchema = z.strictObject({
|
|
327
|
+
type: columnTypeSchema,
|
|
328
|
+
primary_key: z.boolean().optional(),
|
|
329
|
+
nullable: z.boolean().optional(),
|
|
330
|
+
unique: z.boolean().optional(),
|
|
331
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
332
|
+
description: z.string().optional(),
|
|
333
|
+
renamed_from: identifierSchema.optional(),
|
|
334
|
+
cast: z.string().optional(),
|
|
335
|
+
deprecated: z.boolean().optional()
|
|
336
|
+
}).superRefine((column, ctx) => {
|
|
337
|
+
if (column.default !== void 0) {
|
|
338
|
+
const issue = defaultIssue(column.default, column.type);
|
|
339
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
|
|
340
|
+
}
|
|
341
|
+
if (column.primary_key && column.nullable) {
|
|
342
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
|
|
343
|
+
}
|
|
344
|
+
if (column.primary_key && column.deprecated) {
|
|
345
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
|
|
346
|
+
}
|
|
347
|
+
if (column.cast && !CAST_PATTERN.test(column.cast)) {
|
|
348
|
+
ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
function castParts(cast) {
|
|
352
|
+
const match = CAST_PATTERN.exec(cast);
|
|
353
|
+
if (!match) return null;
|
|
354
|
+
return { column: match[1], type: match[2] };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/errors.ts
|
|
358
|
+
var OrmDefinitionError = class extends Error {
|
|
359
|
+
constructor(message, file) {
|
|
360
|
+
super(file ? `${file}: ${message}` : message);
|
|
361
|
+
this.file = file;
|
|
362
|
+
}
|
|
363
|
+
name = "OrmDefinitionError";
|
|
364
|
+
};
|
|
365
|
+
var PredicateParseError = class extends Error {
|
|
366
|
+
constructor(message, position) {
|
|
367
|
+
super(`${message} (at position ${position})`);
|
|
368
|
+
this.position = position;
|
|
369
|
+
}
|
|
370
|
+
name = "PredicateParseError";
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// src/predicate/parse.ts
|
|
374
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
|
|
375
|
+
var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
376
|
+
var IDENTIFIER_START = /[a-z_]/;
|
|
377
|
+
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
378
|
+
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
379
|
+
function tokenize2(source) {
|
|
380
|
+
const tokens = [];
|
|
381
|
+
let position = 0;
|
|
382
|
+
while (position < source.length) {
|
|
383
|
+
const char = source[position];
|
|
384
|
+
if (/\s/.test(char)) {
|
|
385
|
+
position += 1;
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
if (char === "(") {
|
|
389
|
+
tokens.push({ kind: "lparen", value: char, position });
|
|
390
|
+
position += 1;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (char === ")") {
|
|
394
|
+
tokens.push({ kind: "rparen", value: char, position });
|
|
395
|
+
position += 1;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (char === ",") {
|
|
399
|
+
tokens.push({ kind: "comma", value: char, position });
|
|
400
|
+
position += 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (char === "'") {
|
|
404
|
+
const { value, end } = readString(source, position);
|
|
405
|
+
tokens.push({ kind: "string", value, position });
|
|
406
|
+
position = end;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
const operator = OPERATOR_PATTERN.exec(source.slice(position));
|
|
410
|
+
if (operator) {
|
|
411
|
+
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
412
|
+
tokens.push({ kind: "operator", value, position });
|
|
413
|
+
position += operator[0].length;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const number = NUMBER_PATTERN.exec(source.slice(position));
|
|
417
|
+
if (number && !IDENTIFIER_START.test(char)) {
|
|
418
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
419
|
+
position += number[0].length;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
if (IDENTIFIER_START.test(char)) {
|
|
423
|
+
let end = position;
|
|
424
|
+
while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
|
|
425
|
+
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
426
|
+
position = end;
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
throw new PredicateParseError(`unexpected character '${char}'`, position);
|
|
430
|
+
}
|
|
431
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
432
|
+
return tokens;
|
|
433
|
+
}
|
|
434
|
+
function readString(source, start) {
|
|
435
|
+
let value = "";
|
|
436
|
+
let position = start + 1;
|
|
437
|
+
while (position < source.length) {
|
|
438
|
+
const char = source[position];
|
|
439
|
+
if (char !== "'") {
|
|
440
|
+
value += char;
|
|
441
|
+
position += 1;
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
if (source[position + 1] === "'") {
|
|
445
|
+
value += "'";
|
|
446
|
+
position += 2;
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
return { value, end: position + 1 };
|
|
450
|
+
}
|
|
451
|
+
throw new PredicateParseError("unterminated string literal", start);
|
|
452
|
+
}
|
|
453
|
+
var Parser2 = class {
|
|
454
|
+
constructor(tokens) {
|
|
455
|
+
this.tokens = tokens;
|
|
456
|
+
}
|
|
457
|
+
index = 0;
|
|
458
|
+
parse() {
|
|
459
|
+
const predicate = this.parseOr();
|
|
460
|
+
this.expect("end", "end of expression");
|
|
461
|
+
return predicate;
|
|
462
|
+
}
|
|
463
|
+
parseOr() {
|
|
464
|
+
const operands = [this.parseAnd()];
|
|
465
|
+
while (this.matchKeyword("or")) operands.push(this.parseAnd());
|
|
466
|
+
return operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
467
|
+
}
|
|
468
|
+
parseAnd() {
|
|
469
|
+
const operands = [this.parseNot()];
|
|
470
|
+
while (this.matchKeyword("and")) operands.push(this.parseNot());
|
|
471
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
472
|
+
}
|
|
473
|
+
parseNot() {
|
|
474
|
+
if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
|
|
475
|
+
return this.parsePrimary();
|
|
476
|
+
}
|
|
477
|
+
parsePrimary() {
|
|
478
|
+
if (this.peek().kind === "lparen") {
|
|
479
|
+
this.advance();
|
|
480
|
+
const inner = this.parseOr();
|
|
481
|
+
this.expect("rparen", "')'");
|
|
482
|
+
return inner;
|
|
483
|
+
}
|
|
484
|
+
if (this.peekKeyword("capability")) {
|
|
485
|
+
const capability = this.parseCapability();
|
|
486
|
+
const next = this.peek();
|
|
487
|
+
if (next.kind === "operator") {
|
|
488
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
|
|
489
|
+
}
|
|
490
|
+
return capability;
|
|
491
|
+
}
|
|
492
|
+
return this.parseComparison();
|
|
493
|
+
}
|
|
494
|
+
parseCapability() {
|
|
495
|
+
this.advance();
|
|
496
|
+
const key = this.parseCallArgument("capability");
|
|
497
|
+
if (!CAPABILITY_KEY_PATTERN.test(key)) {
|
|
498
|
+
throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
|
|
499
|
+
}
|
|
500
|
+
return { kind: "capability", key };
|
|
501
|
+
}
|
|
502
|
+
parseComparison() {
|
|
503
|
+
const left = this.parseOperand();
|
|
504
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
505
|
+
if (!COMPARE_OPS.includes(operator.value)) {
|
|
506
|
+
throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
|
|
507
|
+
}
|
|
508
|
+
const right = this.parseOperand();
|
|
509
|
+
return { kind: "compare", op: operator.value, left, right };
|
|
510
|
+
}
|
|
511
|
+
parseOperand() {
|
|
512
|
+
const token = this.peek();
|
|
513
|
+
if (token.kind === "string") {
|
|
514
|
+
this.advance();
|
|
515
|
+
return { kind: "literal", value: token.value };
|
|
516
|
+
}
|
|
517
|
+
if (token.kind === "number") {
|
|
518
|
+
this.advance();
|
|
519
|
+
const value = Number(token.value);
|
|
520
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
521
|
+
throw new PredicateParseError(
|
|
522
|
+
`numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
|
|
523
|
+
token.position
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
return { kind: "literal", value };
|
|
527
|
+
}
|
|
528
|
+
if (token.kind !== "identifier") {
|
|
529
|
+
throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
|
|
530
|
+
}
|
|
531
|
+
this.advance();
|
|
532
|
+
if (token.value === "true" || token.value === "false") {
|
|
533
|
+
return { kind: "literal", value: token.value === "true" };
|
|
534
|
+
}
|
|
535
|
+
if (token.value === "null") return { kind: "literal", value: null };
|
|
536
|
+
if (token.value === "claim") {
|
|
537
|
+
const key = this.parseCallArgument("claim");
|
|
538
|
+
if (!CLAIM_KEY_PATTERN.test(key)) {
|
|
539
|
+
throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
|
|
540
|
+
}
|
|
541
|
+
return { kind: "claim", key };
|
|
542
|
+
}
|
|
543
|
+
if (token.value === "capability") {
|
|
544
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
|
|
545
|
+
}
|
|
546
|
+
if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
|
|
547
|
+
throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
|
|
548
|
+
}
|
|
549
|
+
return { kind: "column", name: token.value };
|
|
550
|
+
}
|
|
551
|
+
parseCallArgument(fn) {
|
|
552
|
+
this.expect("lparen", `'(' after ${fn}`);
|
|
553
|
+
const argument = this.expect("string", `a quoted key in ${fn}(...)`);
|
|
554
|
+
this.expect("rparen", `')' after ${fn} key`);
|
|
555
|
+
return argument.value;
|
|
556
|
+
}
|
|
557
|
+
peek() {
|
|
558
|
+
return this.tokens[this.index];
|
|
559
|
+
}
|
|
560
|
+
peekKeyword(keyword) {
|
|
561
|
+
const token = this.peek();
|
|
562
|
+
return token.kind === "identifier" && token.value === keyword;
|
|
563
|
+
}
|
|
564
|
+
matchKeyword(keyword) {
|
|
565
|
+
if (!this.peekKeyword(keyword)) return false;
|
|
566
|
+
this.advance();
|
|
567
|
+
return true;
|
|
568
|
+
}
|
|
569
|
+
advance() {
|
|
570
|
+
const token = this.peek();
|
|
571
|
+
if (token.kind !== "end") this.index += 1;
|
|
572
|
+
return token;
|
|
573
|
+
}
|
|
574
|
+
expect(kind, description) {
|
|
575
|
+
const token = this.peek();
|
|
576
|
+
if (token.kind !== kind) {
|
|
577
|
+
throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
|
|
578
|
+
}
|
|
579
|
+
return this.advance();
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
function parsePredicate(source) {
|
|
583
|
+
const trimmed = source.trim();
|
|
584
|
+
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
585
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// src/schema/predicates.ts
|
|
589
|
+
var parsedPredicateSchema = z.string().transform((source, ctx) => {
|
|
590
|
+
try {
|
|
591
|
+
return parsePredicate(source);
|
|
592
|
+
} catch (error) {
|
|
593
|
+
ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
|
|
594
|
+
return z.NEVER;
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// src/schema/actions.ts
|
|
599
|
+
var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
|
|
600
|
+
var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
|
|
601
|
+
var RETURNING_PATTERN = /\breturning\b/i;
|
|
602
|
+
var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
|
|
603
|
+
var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
|
|
604
|
+
var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
605
|
+
var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
|
|
606
|
+
var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
|
|
607
|
+
var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
|
|
608
|
+
var SQL_COMMENT_PATTERN = /--|\/\*/;
|
|
609
|
+
var actionParamSchema = z.strictObject({
|
|
610
|
+
type: columnTypeSchema,
|
|
611
|
+
optional: z.boolean().optional(),
|
|
612
|
+
description: z.string().optional()
|
|
613
|
+
});
|
|
614
|
+
var validatesEntrySchema = z.string().transform((source, ctx) => {
|
|
615
|
+
const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
|
|
616
|
+
if (!match) {
|
|
617
|
+
ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
|
|
618
|
+
return z.NEVER;
|
|
619
|
+
}
|
|
620
|
+
const table = tableReferenceSchema.safeParse(match[1]);
|
|
621
|
+
if (!table.success) {
|
|
622
|
+
ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
|
|
623
|
+
return z.NEVER;
|
|
624
|
+
}
|
|
625
|
+
return { source: source.trim(), table: table.data, param: match[2] };
|
|
626
|
+
});
|
|
627
|
+
var AUDIT_ACTIONS = ["create", "update", "delete"];
|
|
628
|
+
var auditSchema = z.strictObject({
|
|
629
|
+
action: z.enum(AUDIT_ACTIONS),
|
|
630
|
+
resource: identifierSchema.optional()
|
|
631
|
+
});
|
|
632
|
+
var actionSchema = z.strictObject({
|
|
633
|
+
description: z.string().optional(),
|
|
634
|
+
requires: parsedPredicateSchema.optional(),
|
|
635
|
+
params: z.record(identifierSchema, actionParamSchema).optional(),
|
|
636
|
+
validates: z.array(validatesEntrySchema).optional(),
|
|
637
|
+
// One statement, or an ordered list run atomically in the action function.
|
|
638
|
+
executes: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),
|
|
639
|
+
// Declares the columns the action's RETURNING yields. Present → the
|
|
640
|
+
// function returns those rows (typed in the client); absent → the action
|
|
641
|
+
// reports rows_affected only. The column names/types must match the
|
|
642
|
+
// RETURNING list, which Postgres enforces at CREATE FUNCTION.
|
|
643
|
+
returns: z.record(identifierSchema, columnTypeSchema).optional(),
|
|
644
|
+
audit: auditSchema.optional()
|
|
645
|
+
}).superRefine((action, ctx) => {
|
|
646
|
+
const params = Object.keys(action.params ?? {});
|
|
647
|
+
if (action.requires && referencedColumns(action.requires.ast).length > 0) {
|
|
648
|
+
ctx.addIssue({
|
|
649
|
+
code: "custom",
|
|
650
|
+
message: "requires may reference claims and capabilities only, not columns",
|
|
651
|
+
path: ["requires"]
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
|
|
655
|
+
ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
|
|
656
|
+
}
|
|
657
|
+
if (action.returns && Object.keys(action.returns).length === 0) {
|
|
658
|
+
ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
|
|
659
|
+
}
|
|
660
|
+
for (const [index, entry] of (action.validates ?? []).entries()) {
|
|
661
|
+
if (!params.includes(entry.param)) {
|
|
662
|
+
ctx.addIssue({
|
|
663
|
+
code: "custom",
|
|
664
|
+
message: `validates references undeclared param '${entry.param}'`,
|
|
665
|
+
path: ["validates", index]
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
function extractNamedParams(sql) {
|
|
671
|
+
const withoutStrings = sql.replace(QUOTED_STRING_PATTERN, "''");
|
|
672
|
+
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
673
|
+
return [...new Set(names)];
|
|
674
|
+
}
|
|
675
|
+
function actionStatementSources(executes) {
|
|
676
|
+
return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
|
|
677
|
+
}
|
|
678
|
+
function executesIssues(executes, declaredParams, hasReturns) {
|
|
679
|
+
const issues = [];
|
|
680
|
+
const statements = actionStatementSources(executes);
|
|
681
|
+
statements.forEach((statement, index) => {
|
|
682
|
+
const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
|
|
683
|
+
const at = statements.length > 1 ? ` (statement ${index + 1})` : "";
|
|
684
|
+
if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
|
|
685
|
+
if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
|
|
686
|
+
if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
|
|
687
|
+
if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
|
|
688
|
+
if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
|
|
689
|
+
if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
|
|
690
|
+
if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
|
|
691
|
+
issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
|
|
692
|
+
}
|
|
693
|
+
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index === statements.length - 1)) {
|
|
694
|
+
issues.push(
|
|
695
|
+
hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
const last = statements.at(-1);
|
|
700
|
+
if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
|
|
701
|
+
issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
|
|
702
|
+
}
|
|
703
|
+
const undeclared = extractNamedParams(statements.join("\n")).filter((name2) => !declaredParams.has(name2));
|
|
704
|
+
if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
|
|
705
|
+
return issues;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/schema/table.ts
|
|
709
|
+
var linkSchema = z.strictObject({
|
|
710
|
+
to: tableReferenceSchema,
|
|
711
|
+
on: identifierSchema,
|
|
712
|
+
enforce: z.boolean().optional(),
|
|
713
|
+
description: z.string().optional()
|
|
714
|
+
});
|
|
715
|
+
var indexSchema = z.union([
|
|
716
|
+
z.array(identifierSchema).min(1),
|
|
717
|
+
z.strictObject({
|
|
718
|
+
columns: z.array(identifierSchema).min(1),
|
|
719
|
+
unique: z.boolean().optional(),
|
|
720
|
+
name: identifierSchema.optional()
|
|
721
|
+
})
|
|
722
|
+
]).transform((index) => Array.isArray(index) ? { columns: index } : index);
|
|
723
|
+
var policiesSchema = z.strictObject({
|
|
724
|
+
read: parsedPredicateSchema.optional(),
|
|
725
|
+
write: parsedPredicateSchema.optional(),
|
|
726
|
+
insert: parsedPredicateSchema.optional(),
|
|
727
|
+
update: parsedPredicateSchema.optional()
|
|
728
|
+
});
|
|
729
|
+
z.strictObject({
|
|
730
|
+
namespace: namespaceSchema,
|
|
731
|
+
table: identifierSchema,
|
|
732
|
+
description: z.string().optional(),
|
|
733
|
+
columns: z.record(identifierSchema, columnSchema).optional(),
|
|
734
|
+
source: z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
|
|
735
|
+
links: z.record(identifierSchema, linkSchema).optional(),
|
|
736
|
+
indexes: z.array(indexSchema).optional(),
|
|
737
|
+
policies: policiesSchema.optional(),
|
|
738
|
+
actions: z.record(identifierSchema, actionSchema).optional()
|
|
739
|
+
}).superRefine((table, ctx) => {
|
|
740
|
+
if (RESERVED_TABLE_NAMES.has(table.table)) {
|
|
741
|
+
ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
|
|
742
|
+
}
|
|
743
|
+
for (const name2 of Object.keys(table.columns ?? {})) {
|
|
744
|
+
if (RESERVED_COLUMN_NAMES.has(name2)) {
|
|
745
|
+
ctx.addIssue({
|
|
746
|
+
code: "custom",
|
|
747
|
+
message: `column '${name2}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
|
|
748
|
+
path: ["columns", name2]
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (table.source) validateSyncedTable(table, ctx);
|
|
753
|
+
else validateColumnTable(table, ctx);
|
|
754
|
+
validateLinks(table, ctx);
|
|
755
|
+
validatePolicyColumns(table, ctx);
|
|
756
|
+
validateDerivedNameLengths(table, ctx);
|
|
757
|
+
});
|
|
758
|
+
var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
|
|
759
|
+
var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
760
|
+
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
761
|
+
function validateSyncedTable(table, ctx) {
|
|
762
|
+
const forbidden = [
|
|
763
|
+
["actions", "synced tables are read-only; actions are not allowed"],
|
|
764
|
+
["indexes", "v1 synced tables cannot declare indexes; declare them with defineSyncedTable({ indexes }) in a v2 definition"]
|
|
765
|
+
];
|
|
766
|
+
for (const [field, message] of forbidden) {
|
|
767
|
+
if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
|
|
768
|
+
}
|
|
769
|
+
if (table.policies?.write) {
|
|
770
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
771
|
+
}
|
|
772
|
+
if (table.policies?.insert) {
|
|
773
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
774
|
+
}
|
|
775
|
+
if (table.policies?.update) {
|
|
776
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
777
|
+
}
|
|
778
|
+
const columns = table.columns ?? {};
|
|
779
|
+
const names = Object.keys(columns);
|
|
780
|
+
if (names.length === 0) return;
|
|
781
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
782
|
+
if (primaryKeys.length !== 1) {
|
|
783
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
784
|
+
}
|
|
785
|
+
validateRenames(columns, names, ctx);
|
|
786
|
+
for (const name2 of names) {
|
|
787
|
+
const column = columns[name2];
|
|
788
|
+
if (!column) continue;
|
|
789
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
790
|
+
if (column.cast && cast?.column !== name2) {
|
|
791
|
+
ctx.addIssue({
|
|
792
|
+
code: "custom",
|
|
793
|
+
message: "cast must reference the column it is declared on",
|
|
794
|
+
path: ["columns", name2, "cast"]
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
if (cast && cast.type !== column.type) {
|
|
798
|
+
ctx.addIssue({
|
|
799
|
+
code: "custom",
|
|
800
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
801
|
+
path: ["columns", name2, "cast"]
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
function validateRenames(columns, names, ctx) {
|
|
807
|
+
const renameSources = /* @__PURE__ */ new Map();
|
|
808
|
+
for (const name2 of names) {
|
|
809
|
+
const renamedFrom = columns[name2]?.renamed_from;
|
|
810
|
+
if (!renamedFrom) continue;
|
|
811
|
+
if (renamedFrom === name2 || names.includes(renamedFrom)) {
|
|
812
|
+
ctx.addIssue({
|
|
813
|
+
code: "custom",
|
|
814
|
+
message: "renamed_from must name a column that no longer exists",
|
|
815
|
+
path: ["columns", name2, "renamed_from"]
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
const claimed = renameSources.get(renamedFrom);
|
|
819
|
+
if (claimed) {
|
|
820
|
+
ctx.addIssue({
|
|
821
|
+
code: "custom",
|
|
822
|
+
message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
|
|
823
|
+
path: ["columns", name2, "renamed_from"]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
renameSources.set(renamedFrom, name2);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function validateColumnTable(table, ctx) {
|
|
830
|
+
const columns = table.columns ?? {};
|
|
831
|
+
const names = Object.keys(columns);
|
|
832
|
+
if (names.length === 0) {
|
|
833
|
+
ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
837
|
+
if (primaryKeys.length !== 1) {
|
|
838
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
839
|
+
}
|
|
840
|
+
validateRenames(columns, names, ctx);
|
|
841
|
+
for (const name2 of names) {
|
|
842
|
+
const column = columns[name2];
|
|
843
|
+
if (!column) continue;
|
|
844
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
845
|
+
if (column.cast && cast?.column !== name2) {
|
|
846
|
+
ctx.addIssue({
|
|
847
|
+
code: "custom",
|
|
848
|
+
message: "cast must reference the column it is declared on",
|
|
849
|
+
path: ["columns", name2, "cast"]
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
if (cast && cast.type !== column.type) {
|
|
853
|
+
ctx.addIssue({
|
|
854
|
+
code: "custom",
|
|
855
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
856
|
+
path: ["columns", name2, "cast"]
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
861
|
+
if (new Set(index.columns).size !== index.columns.length) {
|
|
862
|
+
ctx.addIssue({
|
|
863
|
+
code: "custom",
|
|
864
|
+
message: "index names a column more than once",
|
|
865
|
+
path: ["indexes", position]
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
for (const column of index.columns) {
|
|
869
|
+
if (!names.includes(column)) {
|
|
870
|
+
ctx.addIssue({
|
|
871
|
+
code: "custom",
|
|
872
|
+
message: `index references unknown column '${column}'`,
|
|
873
|
+
path: ["indexes", position]
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
function validateLinks(table, ctx) {
|
|
880
|
+
const columnNames = Object.keys(table.columns ?? {});
|
|
881
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
882
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
883
|
+
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name2, "on"] });
|
|
884
|
+
}
|
|
885
|
+
if (link.enforce && link.to.includes(".")) {
|
|
886
|
+
ctx.addIssue({
|
|
887
|
+
code: "custom",
|
|
888
|
+
message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
|
|
889
|
+
path: ["links", name2, "enforce"]
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
if (link.enforce && table.source) {
|
|
893
|
+
ctx.addIssue({
|
|
894
|
+
code: "custom",
|
|
895
|
+
message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
|
|
896
|
+
path: ["links", name2, "enforce"]
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function validateDerivedNameLengths(table, ctx) {
|
|
902
|
+
const worstPolicy = policyName(table.table, "insert");
|
|
903
|
+
if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
|
|
904
|
+
ctx.addIssue({
|
|
905
|
+
code: "custom",
|
|
906
|
+
message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
|
|
907
|
+
path: ["table"]
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
911
|
+
const name2 = indexName(table.table, index);
|
|
912
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
913
|
+
ctx.addIssue({
|
|
914
|
+
code: "custom",
|
|
915
|
+
message: `derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
|
|
916
|
+
path: ["indexes", position]
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
921
|
+
const constraint = foreignKeyName(table.table, name2);
|
|
922
|
+
if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
|
|
923
|
+
ctx.addIssue({
|
|
924
|
+
code: "custom",
|
|
925
|
+
message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
|
|
926
|
+
path: ["links", name2]
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
for (const name2 of Object.keys(table.actions ?? {})) {
|
|
931
|
+
const fnName = `action_${name2}`;
|
|
932
|
+
if (fnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
933
|
+
ctx.addIssue({
|
|
934
|
+
code: "custom",
|
|
935
|
+
message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
|
|
936
|
+
path: ["actions", name2]
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
var TYPE_GROUPS = {
|
|
942
|
+
integer: "numeric",
|
|
943
|
+
bigint: "numeric",
|
|
944
|
+
numeric: "numeric",
|
|
945
|
+
double: "numeric",
|
|
946
|
+
text: "text",
|
|
947
|
+
uuid: "uuid",
|
|
948
|
+
date: "temporal",
|
|
949
|
+
timestamptz: "temporal",
|
|
950
|
+
boolean: "boolean",
|
|
951
|
+
jsonb: "jsonb"
|
|
952
|
+
};
|
|
953
|
+
function validatePolicyColumns(table, ctx) {
|
|
954
|
+
const columns = table.columns ?? {};
|
|
955
|
+
const columnNames = Object.keys(columns);
|
|
956
|
+
if (columnNames.length === 0) return;
|
|
957
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
958
|
+
const predicate = table.policies?.[kind];
|
|
959
|
+
if (!predicate) continue;
|
|
960
|
+
for (const column of referencedColumns(predicate.ast)) {
|
|
961
|
+
if (!columnNames.includes(column)) {
|
|
962
|
+
ctx.addIssue({
|
|
963
|
+
code: "custom",
|
|
964
|
+
message: `policy references unknown column '${column}'`,
|
|
965
|
+
path: ["policies", kind]
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
walkPredicate(predicate.ast, (node) => {
|
|
970
|
+
if (node.kind !== "compare") return;
|
|
971
|
+
const issue = comparisonTypeIssue(node.left, node.right, columns);
|
|
972
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
function comparisonTypeIssue(left, right, columns) {
|
|
977
|
+
const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
|
|
978
|
+
if (left.kind === "column" && right.kind === "column") {
|
|
979
|
+
const leftGroup = groupOf(left);
|
|
980
|
+
const rightGroup = groupOf(right);
|
|
981
|
+
if (leftGroup && rightGroup && leftGroup !== rightGroup) {
|
|
982
|
+
return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
|
|
983
|
+
}
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
987
|
+
const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
988
|
+
if (!column || !literal || literal.value === null) return null;
|
|
989
|
+
const columnGroup = groupOf(column);
|
|
990
|
+
if (!columnGroup) return null;
|
|
991
|
+
if (typeof literal.value === "string") return null;
|
|
992
|
+
const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
|
|
993
|
+
if (literalGroup !== columnGroup) {
|
|
994
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
|
|
995
|
+
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
251
998
|
var policyVariantSchema = z.object({
|
|
252
999
|
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
253
1000
|
when: z.string().min(1).max(1e3),
|
|
@@ -564,7 +1311,7 @@ var CANONICAL_FORMAT = 2;
|
|
|
564
1311
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
565
1312
|
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
566
1313
|
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
567
|
-
var
|
|
1314
|
+
var COLUMN_TYPES2 = [
|
|
568
1315
|
"uuid",
|
|
569
1316
|
"text",
|
|
570
1317
|
"integer",
|
|
@@ -576,7 +1323,7 @@ var COLUMN_TYPES = [
|
|
|
576
1323
|
"date",
|
|
577
1324
|
"jsonb"
|
|
578
1325
|
];
|
|
579
|
-
var
|
|
1326
|
+
var columnTypeSchema2 = z.enum(COLUMN_TYPES2);
|
|
580
1327
|
var canonicalEnumSchema = z.object({
|
|
581
1328
|
name: identifier2,
|
|
582
1329
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
@@ -585,7 +1332,7 @@ var canonicalColumnSchema = z.object({
|
|
|
585
1332
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
586
1333
|
name: z.string().min(1).max(63),
|
|
587
1334
|
columnName: columnIdentifier,
|
|
588
|
-
type: z.union([
|
|
1335
|
+
type: z.union([columnTypeSchema2, z.object({ enum: identifier2 })]),
|
|
589
1336
|
nullable: z.boolean().default(true),
|
|
590
1337
|
unique: z.boolean().default(false),
|
|
591
1338
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -835,7 +1582,7 @@ var canonicalActionSchema = z.object({
|
|
|
835
1582
|
name: identifier2,
|
|
836
1583
|
requires: z.string().max(1e3).optional(),
|
|
837
1584
|
params: z.record(z.string(), z.object({
|
|
838
|
-
type:
|
|
1585
|
+
type: columnTypeSchema2,
|
|
839
1586
|
nullable: z.boolean().default(false)
|
|
840
1587
|
})).default({}),
|
|
841
1588
|
executes: z.string().min(1).max(1e4),
|
|
@@ -936,7 +1683,8 @@ var canonicalTableSchema = z.object({
|
|
|
936
1683
|
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
937
1684
|
fail4("is read-only and cannot declare write policies", ["policies"]);
|
|
938
1685
|
}
|
|
939
|
-
const
|
|
1686
|
+
const ownedIndexes = table.source === void 0 ? table.indexes : table.indexes.filter((index) => index.unique);
|
|
1687
|
+
const ownedCollections = [ownedIndexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
940
1688
|
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
941
1689
|
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
942
1690
|
fail4("cannot declare owned storage, constraints or identity operations");
|
|
@@ -1038,6 +1786,23 @@ function trackedCellColumns(table) {
|
|
|
1038
1786
|
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1039
1787
|
}
|
|
1040
1788
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1789
|
+
function validateSyncedIndexes(table, tableIndex, ctx) {
|
|
1790
|
+
if (table.source === void 0) return;
|
|
1791
|
+
const columns = new Set(table.columns.map((column) => column.columnName));
|
|
1792
|
+
for (const [position, index] of table.indexes.entries()) {
|
|
1793
|
+
const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "indexes", position], message: `synced table '${table.table}' ${message}` });
|
|
1794
|
+
if (columns.size === 0) fail4("must declare columns to declare indexes");
|
|
1795
|
+
if (index.unique) fail4("cannot declare a unique index");
|
|
1796
|
+
if (new Set(index.columns).size !== index.columns.length) fail4("index names a column more than once");
|
|
1797
|
+
for (const column of index.columns) {
|
|
1798
|
+
if (columns.size > 0 && !columns.has(column)) fail4(`index references unknown column '${column}'`);
|
|
1799
|
+
}
|
|
1800
|
+
const name2 = indexName(table.table, index);
|
|
1801
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
1802
|
+
fail4(`derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1041
1806
|
function validateCellMetadata(table, index, ctx) {
|
|
1042
1807
|
const fail4 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
1043
1808
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
@@ -1104,12 +1869,24 @@ var localDefinitionSchema = z.object({
|
|
|
1104
1869
|
}).default({ uses: [] })
|
|
1105
1870
|
}).superRefine((definition, ctx) => {
|
|
1106
1871
|
const seen = /* @__PURE__ */ new Set();
|
|
1872
|
+
const syncedIndexOwners = /* @__PURE__ */ new Map();
|
|
1107
1873
|
for (const [index, table] of definition.tables.entries()) {
|
|
1108
1874
|
if (seen.has(table.table)) {
|
|
1109
1875
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
1110
1876
|
}
|
|
1111
1877
|
seen.add(table.table);
|
|
1112
1878
|
validateCellMetadata(table, index, ctx);
|
|
1879
|
+
validateSyncedIndexes(table, index, ctx);
|
|
1880
|
+
if (table.source !== void 0) {
|
|
1881
|
+
for (const [position, entry] of table.indexes.entries()) {
|
|
1882
|
+
const name2 = indexName(table.table, entry);
|
|
1883
|
+
const owner = syncedIndexOwners.get(name2);
|
|
1884
|
+
if (owner !== void 0) {
|
|
1885
|
+
ctx.addIssue({ code: "custom", path: ["tables", index, "indexes", position], message: `synced index '${name2}' is also declared on '${owner}'` });
|
|
1886
|
+
}
|
|
1887
|
+
syncedIndexOwners.set(name2, table.table);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1113
1890
|
if (table.source !== void 0) {
|
|
1114
1891
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1115
1892
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1651,7 +2428,7 @@ var variableDefaultIrSchema = z.object({
|
|
|
1651
2428
|
var variableIrSchema = z.object({
|
|
1652
2429
|
name: z.string(),
|
|
1653
2430
|
type: z.union([
|
|
1654
|
-
z.object({ kind: z.literal("scalar"), scalar: z.enum(
|
|
2431
|
+
z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES2) }),
|
|
1655
2432
|
/** `values` is the declared membership whitelist — the executor validates against it. */
|
|
1656
2433
|
z.object({ kind: z.literal("enum"), name: z.string(), values: z.array(z.string()).min(1) }),
|
|
1657
2434
|
/** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
|
|
@@ -1784,7 +2561,7 @@ var tableMetaIrSchema = z.object({
|
|
|
1784
2561
|
columns: z.array(z.object({
|
|
1785
2562
|
name: z.string(),
|
|
1786
2563
|
columnName: z.string(),
|
|
1787
|
-
type: z.union([z.enum(
|
|
2564
|
+
type: z.union([z.enum(COLUMN_TYPES2), z.object({ enum: z.string(), values: z.array(z.string()) })]),
|
|
1788
2565
|
nullable: z.boolean()
|
|
1789
2566
|
})),
|
|
1790
2567
|
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
@@ -1921,7 +2698,7 @@ function graphqlScalarFor(type) {
|
|
|
1921
2698
|
}
|
|
1922
2699
|
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1923
2700
|
["ID", "text"],
|
|
1924
|
-
...
|
|
2701
|
+
...COLUMN_TYPES2.map((type) => [graphqlScalarFor(type), type])
|
|
1925
2702
|
]);
|
|
1926
2703
|
function columnTypeForScalar(scalarName) {
|
|
1927
2704
|
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
@@ -2022,14 +2799,14 @@ var PolicyCompileError = class extends Error {
|
|
|
2022
2799
|
};
|
|
2023
2800
|
|
|
2024
2801
|
// src/predicate-v2/parse.ts
|
|
2025
|
-
var
|
|
2026
|
-
var
|
|
2027
|
-
var
|
|
2028
|
-
var
|
|
2029
|
-
var
|
|
2802
|
+
var KEYWORDS2 = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
|
|
2803
|
+
var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
|
|
2804
|
+
var IDENTIFIER_START2 = /[a-zA-Z_]/;
|
|
2805
|
+
var IDENTIFIER_CHAR2 = /[a-zA-Z0-9_]/;
|
|
2806
|
+
var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
|
|
2030
2807
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
2031
2808
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
2032
|
-
function
|
|
2809
|
+
function tokenize3(source) {
|
|
2033
2810
|
const tokens = [];
|
|
2034
2811
|
let position = 0;
|
|
2035
2812
|
while (position < source.length) {
|
|
@@ -2059,27 +2836,27 @@ function tokenize2(source) {
|
|
|
2059
2836
|
continue;
|
|
2060
2837
|
}
|
|
2061
2838
|
if (char === "'") {
|
|
2062
|
-
const { value, end } =
|
|
2839
|
+
const { value, end } = readString2(source, position);
|
|
2063
2840
|
tokens.push({ kind: "string", value, position });
|
|
2064
2841
|
position = end;
|
|
2065
2842
|
continue;
|
|
2066
2843
|
}
|
|
2067
|
-
const operator =
|
|
2844
|
+
const operator = OPERATOR_PATTERN2.exec(source.slice(position));
|
|
2068
2845
|
if (operator) {
|
|
2069
2846
|
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
2070
2847
|
tokens.push({ kind: "operator", value, position });
|
|
2071
2848
|
position += operator[0].length;
|
|
2072
2849
|
continue;
|
|
2073
2850
|
}
|
|
2074
|
-
const number =
|
|
2075
|
-
if (number && !
|
|
2851
|
+
const number = NUMBER_PATTERN2.exec(source.slice(position));
|
|
2852
|
+
if (number && !IDENTIFIER_START2.test(char)) {
|
|
2076
2853
|
tokens.push({ kind: "number", value: number[0], position });
|
|
2077
2854
|
position += number[0].length;
|
|
2078
2855
|
continue;
|
|
2079
2856
|
}
|
|
2080
|
-
if (
|
|
2857
|
+
if (IDENTIFIER_START2.test(char)) {
|
|
2081
2858
|
let end = position;
|
|
2082
|
-
while (end < source.length &&
|
|
2859
|
+
while (end < source.length && IDENTIFIER_CHAR2.test(source[end])) end += 1;
|
|
2083
2860
|
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
2084
2861
|
position = end;
|
|
2085
2862
|
continue;
|
|
@@ -2089,7 +2866,7 @@ function tokenize2(source) {
|
|
|
2089
2866
|
tokens.push({ kind: "end", value: "", position: source.length });
|
|
2090
2867
|
return tokens;
|
|
2091
2868
|
}
|
|
2092
|
-
function
|
|
2869
|
+
function readString2(source, start) {
|
|
2093
2870
|
let value = "";
|
|
2094
2871
|
let position = start + 1;
|
|
2095
2872
|
while (position < source.length) {
|
|
@@ -2108,7 +2885,7 @@ function readString(source, start) {
|
|
|
2108
2885
|
}
|
|
2109
2886
|
throw new PolicyParseError("unterminated string literal", start);
|
|
2110
2887
|
}
|
|
2111
|
-
var
|
|
2888
|
+
var Parser3 = class {
|
|
2112
2889
|
constructor(tokens) {
|
|
2113
2890
|
this.tokens = tokens;
|
|
2114
2891
|
}
|
|
@@ -2250,7 +3027,7 @@ var Parser2 = class {
|
|
|
2250
3027
|
/** `ident(.ident)*` — first segment must not be a keyword; later segments may be (e.g. `author.role`). */
|
|
2251
3028
|
parsePath(description) {
|
|
2252
3029
|
const first = this.peek();
|
|
2253
|
-
if (first.kind !== "identifier" ||
|
|
3030
|
+
if (first.kind !== "identifier" || KEYWORDS2.has(first.value)) {
|
|
2254
3031
|
throw new PolicyParseError(`expected ${description}, got '${first.value || "end"}'`, first.position);
|
|
2255
3032
|
}
|
|
2256
3033
|
this.advance();
|
|
@@ -2296,7 +3073,7 @@ var Parser2 = class {
|
|
|
2296
3073
|
function parsePolicy(source) {
|
|
2297
3074
|
const trimmed = source.trim();
|
|
2298
3075
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
2299
|
-
return { source: trimmed, ast: new
|
|
3076
|
+
return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
|
|
2300
3077
|
}
|
|
2301
3078
|
|
|
2302
3079
|
// src/predicate-v2/compile.ts
|
|
@@ -2981,15 +3758,6 @@ function classifyInputType(name2, tableByTypename) {
|
|
|
2981
3758
|
return void 0;
|
|
2982
3759
|
}
|
|
2983
3760
|
|
|
2984
|
-
// src/errors.ts
|
|
2985
|
-
var OrmDefinitionError = class extends Error {
|
|
2986
|
-
constructor(message, file) {
|
|
2987
|
-
super(file ? `${file}: ${message}` : message);
|
|
2988
|
-
this.file = file;
|
|
2989
|
-
}
|
|
2990
|
-
name = "OrmDefinitionError";
|
|
2991
|
-
};
|
|
2992
|
-
|
|
2993
3761
|
// src/canonical/composition.ts
|
|
2994
3762
|
function localContract(definition) {
|
|
2995
3763
|
return normalizeCanonical(localDefinitionSchema.parse(definition));
|