@sequenceholdings/orm 2.10.0 → 2.11.1
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 +74 -4
- package/dist/authoring.cjs +796 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +10 -1
- package/dist/authoring.d.ts +10 -1
- 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.js +777 -6
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +7697 -7686
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +7698 -7687
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +5 -10
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +2 -13
- package/dist/client.d.ts +2 -13
- package/dist/client.js +6 -10
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +815 -100
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +5 -16
- package/dist/compiler.d.ts +5 -16
- package/dist/compiler.js +817 -99
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +3611 -3287
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +577 -7
- package/dist/index.d.ts +577 -7
- package/dist/index.js +3600 -3285
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +784 -18
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +3 -6
- package/dist/ir.d.ts +3 -6
- package/dist/ir.js +784 -18
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +796 -31
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +796 -31
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +790 -28
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +1 -5
- package/dist/sdl.d.ts +1 -5
- package/dist/sdl.js +790 -28
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen-gql/server-fns.cjs +1 -1
package/dist/predicate-v2.cjs
CHANGED
|
@@ -32,11 +32,16 @@ var PolicyCompileError = class extends Error {
|
|
|
32
32
|
|
|
33
33
|
// src/constants.ts
|
|
34
34
|
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
35
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
35
36
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
36
37
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
37
38
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
38
39
|
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
40
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
39
41
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
42
|
+
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
43
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
44
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
40
45
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
41
46
|
var CELL_PART_SUFFIX = {
|
|
42
47
|
at: "__at",
|
|
@@ -249,6 +254,741 @@ var Parser = class {
|
|
|
249
254
|
function compileCheckExpression(source, columns) {
|
|
250
255
|
return new Parser(tokenize(source), columns).parse();
|
|
251
256
|
}
|
|
257
|
+
|
|
258
|
+
// src/predicate/ast.ts
|
|
259
|
+
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
260
|
+
function walkPredicate(predicate, visit) {
|
|
261
|
+
visit(predicate);
|
|
262
|
+
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
263
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit);
|
|
264
|
+
}
|
|
265
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
|
|
266
|
+
}
|
|
267
|
+
function predicateOperands(predicate) {
|
|
268
|
+
const operands = [];
|
|
269
|
+
walkPredicate(predicate, (node) => {
|
|
270
|
+
if (node.kind === "compare") operands.push(node.left, node.right);
|
|
271
|
+
});
|
|
272
|
+
return operands;
|
|
273
|
+
}
|
|
274
|
+
function referencedColumns(predicate) {
|
|
275
|
+
const names = predicateOperands(predicate).flatMap(
|
|
276
|
+
(operand) => operand.kind === "column" ? [operand.name] : []
|
|
277
|
+
);
|
|
278
|
+
return [...new Set(names)];
|
|
279
|
+
}
|
|
280
|
+
var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
281
|
+
var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
282
|
+
var tableReferenceSchema = zod.z.string().refine(
|
|
283
|
+
(value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
|
|
284
|
+
"must be a table name or a schema-qualified table reference"
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// src/schema/columns.ts
|
|
288
|
+
var COLUMN_TYPES = [
|
|
289
|
+
"uuid",
|
|
290
|
+
"text",
|
|
291
|
+
"integer",
|
|
292
|
+
"bigint",
|
|
293
|
+
"numeric",
|
|
294
|
+
"double",
|
|
295
|
+
"boolean",
|
|
296
|
+
"timestamptz",
|
|
297
|
+
"date",
|
|
298
|
+
"jsonb"
|
|
299
|
+
];
|
|
300
|
+
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
301
|
+
var FUNCTION_DEFAULTS = {
|
|
302
|
+
"gen_random_uuid()": ["uuid"],
|
|
303
|
+
"now()": ["timestamptz", "date"]
|
|
304
|
+
};
|
|
305
|
+
var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
|
|
306
|
+
function defaultIssue(value, type) {
|
|
307
|
+
if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
|
|
308
|
+
if (typeof value === "number") {
|
|
309
|
+
if (!["integer", "bigint", "numeric", "double"].includes(type)) {
|
|
310
|
+
return `numeric default is invalid for type '${type}'`;
|
|
311
|
+
}
|
|
312
|
+
if (["integer", "bigint"].includes(type)) {
|
|
313
|
+
if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
|
|
314
|
+
if (!Number.isSafeInteger(value)) {
|
|
315
|
+
return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
319
|
+
return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
const allowedTypes = FUNCTION_DEFAULTS[value];
|
|
324
|
+
if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
var columnSchema = zod.z.strictObject({
|
|
328
|
+
type: columnTypeSchema,
|
|
329
|
+
primary_key: zod.z.boolean().optional(),
|
|
330
|
+
nullable: zod.z.boolean().optional(),
|
|
331
|
+
unique: zod.z.boolean().optional(),
|
|
332
|
+
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()]).optional(),
|
|
333
|
+
description: zod.z.string().optional(),
|
|
334
|
+
renamed_from: identifierSchema.optional(),
|
|
335
|
+
cast: zod.z.string().optional(),
|
|
336
|
+
deprecated: zod.z.boolean().optional()
|
|
337
|
+
}).superRefine((column, ctx) => {
|
|
338
|
+
if (column.default !== void 0) {
|
|
339
|
+
const issue = defaultIssue(column.default, column.type);
|
|
340
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
|
|
341
|
+
}
|
|
342
|
+
if (column.primary_key && column.nullable) {
|
|
343
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
|
|
344
|
+
}
|
|
345
|
+
if (column.primary_key && column.deprecated) {
|
|
346
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
|
|
347
|
+
}
|
|
348
|
+
if (column.cast && !CAST_PATTERN.test(column.cast)) {
|
|
349
|
+
ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
function castParts(cast) {
|
|
353
|
+
const match = CAST_PATTERN.exec(cast);
|
|
354
|
+
if (!match) return null;
|
|
355
|
+
return { column: match[1], type: match[2] };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/errors.ts
|
|
359
|
+
var PredicateParseError = class extends Error {
|
|
360
|
+
constructor(message, position) {
|
|
361
|
+
super(`${message} (at position ${position})`);
|
|
362
|
+
this.position = position;
|
|
363
|
+
}
|
|
364
|
+
name = "PredicateParseError";
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/predicate/parse.ts
|
|
368
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
|
|
369
|
+
var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
370
|
+
var IDENTIFIER_START = /[a-z_]/;
|
|
371
|
+
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
372
|
+
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
373
|
+
function tokenize2(source) {
|
|
374
|
+
const tokens = [];
|
|
375
|
+
let position = 0;
|
|
376
|
+
while (position < source.length) {
|
|
377
|
+
const char = source[position];
|
|
378
|
+
if (/\s/.test(char)) {
|
|
379
|
+
position += 1;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
if (char === "(") {
|
|
383
|
+
tokens.push({ kind: "lparen", value: char, position });
|
|
384
|
+
position += 1;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
if (char === ")") {
|
|
388
|
+
tokens.push({ kind: "rparen", value: char, position });
|
|
389
|
+
position += 1;
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (char === ",") {
|
|
393
|
+
tokens.push({ kind: "comma", value: char, position });
|
|
394
|
+
position += 1;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (char === "'") {
|
|
398
|
+
const { value, end } = readString(source, position);
|
|
399
|
+
tokens.push({ kind: "string", value, position });
|
|
400
|
+
position = end;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
const operator = OPERATOR_PATTERN.exec(source.slice(position));
|
|
404
|
+
if (operator) {
|
|
405
|
+
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
406
|
+
tokens.push({ kind: "operator", value, position });
|
|
407
|
+
position += operator[0].length;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const number = NUMBER_PATTERN.exec(source.slice(position));
|
|
411
|
+
if (number && !IDENTIFIER_START.test(char)) {
|
|
412
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
413
|
+
position += number[0].length;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (IDENTIFIER_START.test(char)) {
|
|
417
|
+
let end = position;
|
|
418
|
+
while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
|
|
419
|
+
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
420
|
+
position = end;
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
throw new PredicateParseError(`unexpected character '${char}'`, position);
|
|
424
|
+
}
|
|
425
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
426
|
+
return tokens;
|
|
427
|
+
}
|
|
428
|
+
function readString(source, start) {
|
|
429
|
+
let value = "";
|
|
430
|
+
let position = start + 1;
|
|
431
|
+
while (position < source.length) {
|
|
432
|
+
const char = source[position];
|
|
433
|
+
if (char !== "'") {
|
|
434
|
+
value += char;
|
|
435
|
+
position += 1;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (source[position + 1] === "'") {
|
|
439
|
+
value += "'";
|
|
440
|
+
position += 2;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
return { value, end: position + 1 };
|
|
444
|
+
}
|
|
445
|
+
throw new PredicateParseError("unterminated string literal", start);
|
|
446
|
+
}
|
|
447
|
+
var Parser2 = class {
|
|
448
|
+
constructor(tokens) {
|
|
449
|
+
this.tokens = tokens;
|
|
450
|
+
}
|
|
451
|
+
index = 0;
|
|
452
|
+
parse() {
|
|
453
|
+
const predicate = this.parseOr();
|
|
454
|
+
this.expect("end", "end of expression");
|
|
455
|
+
return predicate;
|
|
456
|
+
}
|
|
457
|
+
parseOr() {
|
|
458
|
+
const operands = [this.parseAnd()];
|
|
459
|
+
while (this.matchKeyword("or")) operands.push(this.parseAnd());
|
|
460
|
+
return operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
461
|
+
}
|
|
462
|
+
parseAnd() {
|
|
463
|
+
const operands = [this.parseNot()];
|
|
464
|
+
while (this.matchKeyword("and")) operands.push(this.parseNot());
|
|
465
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
466
|
+
}
|
|
467
|
+
parseNot() {
|
|
468
|
+
if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
|
|
469
|
+
return this.parsePrimary();
|
|
470
|
+
}
|
|
471
|
+
parsePrimary() {
|
|
472
|
+
if (this.peek().kind === "lparen") {
|
|
473
|
+
this.advance();
|
|
474
|
+
const inner = this.parseOr();
|
|
475
|
+
this.expect("rparen", "')'");
|
|
476
|
+
return inner;
|
|
477
|
+
}
|
|
478
|
+
if (this.peekKeyword("capability")) {
|
|
479
|
+
const capability = this.parseCapability();
|
|
480
|
+
const next = this.peek();
|
|
481
|
+
if (next.kind === "operator") {
|
|
482
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
|
|
483
|
+
}
|
|
484
|
+
return capability;
|
|
485
|
+
}
|
|
486
|
+
return this.parseComparison();
|
|
487
|
+
}
|
|
488
|
+
parseCapability() {
|
|
489
|
+
this.advance();
|
|
490
|
+
const key = this.parseCallArgument("capability");
|
|
491
|
+
if (!CAPABILITY_KEY_PATTERN.test(key)) {
|
|
492
|
+
throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
|
|
493
|
+
}
|
|
494
|
+
return { kind: "capability", key };
|
|
495
|
+
}
|
|
496
|
+
parseComparison() {
|
|
497
|
+
const left = this.parseOperand();
|
|
498
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
499
|
+
if (!COMPARE_OPS.includes(operator.value)) {
|
|
500
|
+
throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
|
|
501
|
+
}
|
|
502
|
+
const right = this.parseOperand();
|
|
503
|
+
return { kind: "compare", op: operator.value, left, right };
|
|
504
|
+
}
|
|
505
|
+
parseOperand() {
|
|
506
|
+
const token = this.peek();
|
|
507
|
+
if (token.kind === "string") {
|
|
508
|
+
this.advance();
|
|
509
|
+
return { kind: "literal", value: token.value };
|
|
510
|
+
}
|
|
511
|
+
if (token.kind === "number") {
|
|
512
|
+
this.advance();
|
|
513
|
+
const value = Number(token.value);
|
|
514
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
515
|
+
throw new PredicateParseError(
|
|
516
|
+
`numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
|
|
517
|
+
token.position
|
|
518
|
+
);
|
|
519
|
+
}
|
|
520
|
+
return { kind: "literal", value };
|
|
521
|
+
}
|
|
522
|
+
if (token.kind !== "identifier") {
|
|
523
|
+
throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
|
|
524
|
+
}
|
|
525
|
+
this.advance();
|
|
526
|
+
if (token.value === "true" || token.value === "false") {
|
|
527
|
+
return { kind: "literal", value: token.value === "true" };
|
|
528
|
+
}
|
|
529
|
+
if (token.value === "null") return { kind: "literal", value: null };
|
|
530
|
+
if (token.value === "claim") {
|
|
531
|
+
const key = this.parseCallArgument("claim");
|
|
532
|
+
if (!CLAIM_KEY_PATTERN.test(key)) {
|
|
533
|
+
throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
|
|
534
|
+
}
|
|
535
|
+
return { kind: "claim", key };
|
|
536
|
+
}
|
|
537
|
+
if (token.value === "capability") {
|
|
538
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
|
|
539
|
+
}
|
|
540
|
+
if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
|
|
541
|
+
throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
|
|
542
|
+
}
|
|
543
|
+
return { kind: "column", name: token.value };
|
|
544
|
+
}
|
|
545
|
+
parseCallArgument(fn) {
|
|
546
|
+
this.expect("lparen", `'(' after ${fn}`);
|
|
547
|
+
const argument = this.expect("string", `a quoted key in ${fn}(...)`);
|
|
548
|
+
this.expect("rparen", `')' after ${fn} key`);
|
|
549
|
+
return argument.value;
|
|
550
|
+
}
|
|
551
|
+
peek() {
|
|
552
|
+
return this.tokens[this.index];
|
|
553
|
+
}
|
|
554
|
+
peekKeyword(keyword) {
|
|
555
|
+
const token = this.peek();
|
|
556
|
+
return token.kind === "identifier" && token.value === keyword;
|
|
557
|
+
}
|
|
558
|
+
matchKeyword(keyword) {
|
|
559
|
+
if (!this.peekKeyword(keyword)) return false;
|
|
560
|
+
this.advance();
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
advance() {
|
|
564
|
+
const token = this.peek();
|
|
565
|
+
if (token.kind !== "end") this.index += 1;
|
|
566
|
+
return token;
|
|
567
|
+
}
|
|
568
|
+
expect(kind, description) {
|
|
569
|
+
const token = this.peek();
|
|
570
|
+
if (token.kind !== kind) {
|
|
571
|
+
throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
|
|
572
|
+
}
|
|
573
|
+
return this.advance();
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
function parsePredicate(source) {
|
|
577
|
+
const trimmed = source.trim();
|
|
578
|
+
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
579
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/schema/predicates.ts
|
|
583
|
+
var parsedPredicateSchema = zod.z.string().transform((source, ctx) => {
|
|
584
|
+
try {
|
|
585
|
+
return parsePredicate(source);
|
|
586
|
+
} catch (error) {
|
|
587
|
+
ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
|
|
588
|
+
return zod.z.NEVER;
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
// src/schema/actions.ts
|
|
593
|
+
var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
|
|
594
|
+
var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
|
|
595
|
+
var RETURNING_PATTERN = /\breturning\b/i;
|
|
596
|
+
var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
|
|
597
|
+
var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
|
|
598
|
+
var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
599
|
+
var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
|
|
600
|
+
var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
|
|
601
|
+
var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
|
|
602
|
+
var SQL_COMMENT_PATTERN = /--|\/\*/;
|
|
603
|
+
var actionParamSchema = zod.z.strictObject({
|
|
604
|
+
type: columnTypeSchema,
|
|
605
|
+
optional: zod.z.boolean().optional(),
|
|
606
|
+
description: zod.z.string().optional()
|
|
607
|
+
});
|
|
608
|
+
var validatesEntrySchema = zod.z.string().transform((source, ctx) => {
|
|
609
|
+
const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
|
|
610
|
+
if (!match) {
|
|
611
|
+
ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
|
|
612
|
+
return zod.z.NEVER;
|
|
613
|
+
}
|
|
614
|
+
const table = tableReferenceSchema.safeParse(match[1]);
|
|
615
|
+
if (!table.success) {
|
|
616
|
+
ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
|
|
617
|
+
return zod.z.NEVER;
|
|
618
|
+
}
|
|
619
|
+
return { source: source.trim(), table: table.data, param: match[2] };
|
|
620
|
+
});
|
|
621
|
+
var AUDIT_ACTIONS = ["create", "update", "delete"];
|
|
622
|
+
var auditSchema = zod.z.strictObject({
|
|
623
|
+
action: zod.z.enum(AUDIT_ACTIONS),
|
|
624
|
+
resource: identifierSchema.optional()
|
|
625
|
+
});
|
|
626
|
+
var actionSchema = zod.z.strictObject({
|
|
627
|
+
description: zod.z.string().optional(),
|
|
628
|
+
requires: parsedPredicateSchema.optional(),
|
|
629
|
+
params: zod.z.record(identifierSchema, actionParamSchema).optional(),
|
|
630
|
+
validates: zod.z.array(validatesEntrySchema).optional(),
|
|
631
|
+
// One statement, or an ordered list run atomically in the action function.
|
|
632
|
+
executes: zod.z.union([zod.z.string().min(1), zod.z.array(zod.z.string().min(1)).min(1)]),
|
|
633
|
+
// Declares the columns the action's RETURNING yields. Present → the
|
|
634
|
+
// function returns those rows (typed in the client); absent → the action
|
|
635
|
+
// reports rows_affected only. The column names/types must match the
|
|
636
|
+
// RETURNING list, which Postgres enforces at CREATE FUNCTION.
|
|
637
|
+
returns: zod.z.record(identifierSchema, columnTypeSchema).optional(),
|
|
638
|
+
audit: auditSchema.optional()
|
|
639
|
+
}).superRefine((action, ctx) => {
|
|
640
|
+
const params = Object.keys(action.params ?? {});
|
|
641
|
+
if (action.requires && referencedColumns(action.requires.ast).length > 0) {
|
|
642
|
+
ctx.addIssue({
|
|
643
|
+
code: "custom",
|
|
644
|
+
message: "requires may reference claims and capabilities only, not columns",
|
|
645
|
+
path: ["requires"]
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
|
|
649
|
+
ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
|
|
650
|
+
}
|
|
651
|
+
if (action.returns && Object.keys(action.returns).length === 0) {
|
|
652
|
+
ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
|
|
653
|
+
}
|
|
654
|
+
for (const [index, entry] of (action.validates ?? []).entries()) {
|
|
655
|
+
if (!params.includes(entry.param)) {
|
|
656
|
+
ctx.addIssue({
|
|
657
|
+
code: "custom",
|
|
658
|
+
message: `validates references undeclared param '${entry.param}'`,
|
|
659
|
+
path: ["validates", index]
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
function extractNamedParams(sql) {
|
|
665
|
+
const withoutStrings = sql.replace(QUOTED_STRING_PATTERN, "''");
|
|
666
|
+
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
667
|
+
return [...new Set(names)];
|
|
668
|
+
}
|
|
669
|
+
function actionStatementSources(executes) {
|
|
670
|
+
return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
|
|
671
|
+
}
|
|
672
|
+
function executesIssues(executes, declaredParams, hasReturns) {
|
|
673
|
+
const issues = [];
|
|
674
|
+
const statements = actionStatementSources(executes);
|
|
675
|
+
statements.forEach((statement, index) => {
|
|
676
|
+
const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
|
|
677
|
+
const at = statements.length > 1 ? ` (statement ${index + 1})` : "";
|
|
678
|
+
if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
|
|
679
|
+
if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
|
|
680
|
+
if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
|
|
681
|
+
if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
|
|
682
|
+
if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
|
|
683
|
+
if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
|
|
684
|
+
if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
|
|
685
|
+
issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
|
|
686
|
+
}
|
|
687
|
+
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index === statements.length - 1)) {
|
|
688
|
+
issues.push(
|
|
689
|
+
hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
const last = statements.at(-1);
|
|
694
|
+
if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
|
|
695
|
+
issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
|
|
696
|
+
}
|
|
697
|
+
const undeclared = extractNamedParams(statements.join("\n")).filter((name) => !declaredParams.has(name));
|
|
698
|
+
if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
|
|
699
|
+
return issues;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// src/schema/table.ts
|
|
703
|
+
var linkSchema = zod.z.strictObject({
|
|
704
|
+
to: tableReferenceSchema,
|
|
705
|
+
on: identifierSchema,
|
|
706
|
+
enforce: zod.z.boolean().optional(),
|
|
707
|
+
description: zod.z.string().optional()
|
|
708
|
+
});
|
|
709
|
+
var indexSchema = zod.z.union([
|
|
710
|
+
zod.z.array(identifierSchema).min(1),
|
|
711
|
+
zod.z.strictObject({
|
|
712
|
+
columns: zod.z.array(identifierSchema).min(1),
|
|
713
|
+
unique: zod.z.boolean().optional(),
|
|
714
|
+
name: identifierSchema.optional()
|
|
715
|
+
})
|
|
716
|
+
]).transform((index) => Array.isArray(index) ? { columns: index } : index);
|
|
717
|
+
var policiesSchema = zod.z.strictObject({
|
|
718
|
+
read: parsedPredicateSchema.optional(),
|
|
719
|
+
write: parsedPredicateSchema.optional(),
|
|
720
|
+
insert: parsedPredicateSchema.optional(),
|
|
721
|
+
update: parsedPredicateSchema.optional()
|
|
722
|
+
});
|
|
723
|
+
zod.z.strictObject({
|
|
724
|
+
namespace: namespaceSchema,
|
|
725
|
+
table: identifierSchema,
|
|
726
|
+
description: zod.z.string().optional(),
|
|
727
|
+
columns: zod.z.record(identifierSchema, columnSchema).optional(),
|
|
728
|
+
source: zod.z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
|
|
729
|
+
links: zod.z.record(identifierSchema, linkSchema).optional(),
|
|
730
|
+
indexes: zod.z.array(indexSchema).optional(),
|
|
731
|
+
policies: policiesSchema.optional(),
|
|
732
|
+
actions: zod.z.record(identifierSchema, actionSchema).optional()
|
|
733
|
+
}).superRefine((table, ctx) => {
|
|
734
|
+
if (RESERVED_TABLE_NAMES.has(table.table)) {
|
|
735
|
+
ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
|
|
736
|
+
}
|
|
737
|
+
for (const name of Object.keys(table.columns ?? {})) {
|
|
738
|
+
if (RESERVED_COLUMN_NAMES.has(name)) {
|
|
739
|
+
ctx.addIssue({
|
|
740
|
+
code: "custom",
|
|
741
|
+
message: `column '${name}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
|
|
742
|
+
path: ["columns", name]
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
if (table.source) validateSyncedTable(table, ctx);
|
|
747
|
+
else validateColumnTable(table, ctx);
|
|
748
|
+
validateLinks(table, ctx);
|
|
749
|
+
validatePolicyColumns(table, ctx);
|
|
750
|
+
validateDerivedNameLengths(table, ctx);
|
|
751
|
+
});
|
|
752
|
+
var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
|
|
753
|
+
var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
754
|
+
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
755
|
+
function validateSyncedTable(table, ctx) {
|
|
756
|
+
const forbidden = [
|
|
757
|
+
["actions", "synced tables are read-only; actions are not allowed"],
|
|
758
|
+
["indexes", "v1 synced tables cannot declare indexes; declare them with defineSyncedTable({ indexes }) in a v2 definition"]
|
|
759
|
+
];
|
|
760
|
+
for (const [field, message] of forbidden) {
|
|
761
|
+
if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
|
|
762
|
+
}
|
|
763
|
+
if (table.policies?.write) {
|
|
764
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
765
|
+
}
|
|
766
|
+
if (table.policies?.insert) {
|
|
767
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
768
|
+
}
|
|
769
|
+
if (table.policies?.update) {
|
|
770
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
771
|
+
}
|
|
772
|
+
const columns = table.columns ?? {};
|
|
773
|
+
const names = Object.keys(columns);
|
|
774
|
+
if (names.length === 0) return;
|
|
775
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
776
|
+
if (primaryKeys.length !== 1) {
|
|
777
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
778
|
+
}
|
|
779
|
+
validateRenames(columns, names, ctx);
|
|
780
|
+
for (const name of names) {
|
|
781
|
+
const column = columns[name];
|
|
782
|
+
if (!column) continue;
|
|
783
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
784
|
+
if (column.cast && cast?.column !== name) {
|
|
785
|
+
ctx.addIssue({
|
|
786
|
+
code: "custom",
|
|
787
|
+
message: "cast must reference the column it is declared on",
|
|
788
|
+
path: ["columns", name, "cast"]
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
if (cast && cast.type !== column.type) {
|
|
792
|
+
ctx.addIssue({
|
|
793
|
+
code: "custom",
|
|
794
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
795
|
+
path: ["columns", name, "cast"]
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
function validateRenames(columns, names, ctx) {
|
|
801
|
+
const renameSources = /* @__PURE__ */ new Map();
|
|
802
|
+
for (const name of names) {
|
|
803
|
+
const renamedFrom = columns[name]?.renamed_from;
|
|
804
|
+
if (!renamedFrom) continue;
|
|
805
|
+
if (renamedFrom === name || names.includes(renamedFrom)) {
|
|
806
|
+
ctx.addIssue({
|
|
807
|
+
code: "custom",
|
|
808
|
+
message: "renamed_from must name a column that no longer exists",
|
|
809
|
+
path: ["columns", name, "renamed_from"]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
const claimed = renameSources.get(renamedFrom);
|
|
813
|
+
if (claimed) {
|
|
814
|
+
ctx.addIssue({
|
|
815
|
+
code: "custom",
|
|
816
|
+
message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
|
|
817
|
+
path: ["columns", name, "renamed_from"]
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
renameSources.set(renamedFrom, name);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function validateColumnTable(table, ctx) {
|
|
824
|
+
const columns = table.columns ?? {};
|
|
825
|
+
const names = Object.keys(columns);
|
|
826
|
+
if (names.length === 0) {
|
|
827
|
+
ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
831
|
+
if (primaryKeys.length !== 1) {
|
|
832
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
833
|
+
}
|
|
834
|
+
validateRenames(columns, names, ctx);
|
|
835
|
+
for (const name of names) {
|
|
836
|
+
const column = columns[name];
|
|
837
|
+
if (!column) continue;
|
|
838
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
839
|
+
if (column.cast && cast?.column !== name) {
|
|
840
|
+
ctx.addIssue({
|
|
841
|
+
code: "custom",
|
|
842
|
+
message: "cast must reference the column it is declared on",
|
|
843
|
+
path: ["columns", name, "cast"]
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
if (cast && cast.type !== column.type) {
|
|
847
|
+
ctx.addIssue({
|
|
848
|
+
code: "custom",
|
|
849
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
850
|
+
path: ["columns", name, "cast"]
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
855
|
+
if (new Set(index.columns).size !== index.columns.length) {
|
|
856
|
+
ctx.addIssue({
|
|
857
|
+
code: "custom",
|
|
858
|
+
message: "index names a column more than once",
|
|
859
|
+
path: ["indexes", position]
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
for (const column of index.columns) {
|
|
863
|
+
if (!names.includes(column)) {
|
|
864
|
+
ctx.addIssue({
|
|
865
|
+
code: "custom",
|
|
866
|
+
message: `index references unknown column '${column}'`,
|
|
867
|
+
path: ["indexes", position]
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
function validateLinks(table, ctx) {
|
|
874
|
+
const columnNames = Object.keys(table.columns ?? {});
|
|
875
|
+
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
876
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
877
|
+
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
|
|
878
|
+
}
|
|
879
|
+
if (link.enforce && link.to.includes(".")) {
|
|
880
|
+
ctx.addIssue({
|
|
881
|
+
code: "custom",
|
|
882
|
+
message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
|
|
883
|
+
path: ["links", name, "enforce"]
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
if (link.enforce && table.source) {
|
|
887
|
+
ctx.addIssue({
|
|
888
|
+
code: "custom",
|
|
889
|
+
message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
|
|
890
|
+
path: ["links", name, "enforce"]
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
function validateDerivedNameLengths(table, ctx) {
|
|
896
|
+
const worstPolicy = policyName(table.table, "insert");
|
|
897
|
+
if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
|
|
898
|
+
ctx.addIssue({
|
|
899
|
+
code: "custom",
|
|
900
|
+
message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
|
|
901
|
+
path: ["table"]
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
905
|
+
const name = indexName(table.table, index);
|
|
906
|
+
if (name.length > MAX_IDENTIFIER_LENGTH) {
|
|
907
|
+
ctx.addIssue({
|
|
908
|
+
code: "custom",
|
|
909
|
+
message: `derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
|
|
910
|
+
path: ["indexes", position]
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
915
|
+
const constraint = foreignKeyName(table.table, name);
|
|
916
|
+
if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
|
|
917
|
+
ctx.addIssue({
|
|
918
|
+
code: "custom",
|
|
919
|
+
message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
|
|
920
|
+
path: ["links", name]
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
for (const name of Object.keys(table.actions ?? {})) {
|
|
925
|
+
const fnName = `action_${name}`;
|
|
926
|
+
if (fnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
927
|
+
ctx.addIssue({
|
|
928
|
+
code: "custom",
|
|
929
|
+
message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
|
|
930
|
+
path: ["actions", name]
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
var TYPE_GROUPS = {
|
|
936
|
+
integer: "numeric",
|
|
937
|
+
bigint: "numeric",
|
|
938
|
+
numeric: "numeric",
|
|
939
|
+
double: "numeric",
|
|
940
|
+
text: "text",
|
|
941
|
+
uuid: "uuid",
|
|
942
|
+
date: "temporal",
|
|
943
|
+
timestamptz: "temporal",
|
|
944
|
+
boolean: "boolean",
|
|
945
|
+
jsonb: "jsonb"
|
|
946
|
+
};
|
|
947
|
+
function validatePolicyColumns(table, ctx) {
|
|
948
|
+
const columns = table.columns ?? {};
|
|
949
|
+
const columnNames = Object.keys(columns);
|
|
950
|
+
if (columnNames.length === 0) return;
|
|
951
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
952
|
+
const predicate = table.policies?.[kind];
|
|
953
|
+
if (!predicate) continue;
|
|
954
|
+
for (const column of referencedColumns(predicate.ast)) {
|
|
955
|
+
if (!columnNames.includes(column)) {
|
|
956
|
+
ctx.addIssue({
|
|
957
|
+
code: "custom",
|
|
958
|
+
message: `policy references unknown column '${column}'`,
|
|
959
|
+
path: ["policies", kind]
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
walkPredicate(predicate.ast, (node) => {
|
|
964
|
+
if (node.kind !== "compare") return;
|
|
965
|
+
const issue = comparisonTypeIssue(node.left, node.right, columns);
|
|
966
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
function comparisonTypeIssue(left, right, columns) {
|
|
971
|
+
const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
|
|
972
|
+
if (left.kind === "column" && right.kind === "column") {
|
|
973
|
+
const leftGroup = groupOf(left);
|
|
974
|
+
const rightGroup = groupOf(right);
|
|
975
|
+
if (leftGroup && rightGroup && leftGroup !== rightGroup) {
|
|
976
|
+
return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
|
|
977
|
+
}
|
|
978
|
+
return null;
|
|
979
|
+
}
|
|
980
|
+
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
981
|
+
const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
982
|
+
if (!column || !literal || literal.value === null) return null;
|
|
983
|
+
const columnGroup = groupOf(column);
|
|
984
|
+
if (!columnGroup) return null;
|
|
985
|
+
if (typeof literal.value === "string") return null;
|
|
986
|
+
const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
|
|
987
|
+
if (literalGroup !== columnGroup) {
|
|
988
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
|
|
989
|
+
}
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
252
992
|
var policyVariantSchema = zod.z.object({
|
|
253
993
|
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
254
994
|
when: zod.z.string().min(1).max(1e3),
|
|
@@ -557,7 +1297,7 @@ var CANONICAL_FORMAT = 2;
|
|
|
557
1297
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
558
1298
|
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
559
1299
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
560
|
-
var
|
|
1300
|
+
var COLUMN_TYPES2 = [
|
|
561
1301
|
"uuid",
|
|
562
1302
|
"text",
|
|
563
1303
|
"integer",
|
|
@@ -569,7 +1309,7 @@ var COLUMN_TYPES = [
|
|
|
569
1309
|
"date",
|
|
570
1310
|
"jsonb"
|
|
571
1311
|
];
|
|
572
|
-
var
|
|
1312
|
+
var columnTypeSchema2 = zod.z.enum(COLUMN_TYPES2);
|
|
573
1313
|
var canonicalEnumSchema = zod.z.object({
|
|
574
1314
|
name: identifier2,
|
|
575
1315
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
@@ -578,7 +1318,7 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
578
1318
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
579
1319
|
name: zod.z.string().min(1).max(63),
|
|
580
1320
|
columnName: columnIdentifier,
|
|
581
|
-
type: zod.z.union([
|
|
1321
|
+
type: zod.z.union([columnTypeSchema2, zod.z.object({ enum: identifier2 })]),
|
|
582
1322
|
nullable: zod.z.boolean().default(true),
|
|
583
1323
|
unique: zod.z.boolean().default(false),
|
|
584
1324
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -671,7 +1411,7 @@ var canonicalActionSchema = zod.z.object({
|
|
|
671
1411
|
name: identifier2,
|
|
672
1412
|
requires: zod.z.string().max(1e3).optional(),
|
|
673
1413
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
674
|
-
type:
|
|
1414
|
+
type: columnTypeSchema2,
|
|
675
1415
|
nullable: zod.z.boolean().default(false)
|
|
676
1416
|
})).default({}),
|
|
677
1417
|
executes: zod.z.string().min(1).max(1e4),
|
|
@@ -772,7 +1512,8 @@ var canonicalTableSchema = zod.z.object({
|
|
|
772
1512
|
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
773
1513
|
fail("is read-only and cannot declare write policies", ["policies"]);
|
|
774
1514
|
}
|
|
775
|
-
const
|
|
1515
|
+
const ownedIndexes = table.source === void 0 ? table.indexes : table.indexes.filter((index) => index.unique);
|
|
1516
|
+
const ownedCollections = [ownedIndexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
776
1517
|
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
777
1518
|
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
778
1519
|
fail("cannot declare owned storage, constraints or identity operations");
|
|
@@ -874,6 +1615,23 @@ function trackedCellColumns(table) {
|
|
|
874
1615
|
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
875
1616
|
}
|
|
876
1617
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1618
|
+
function validateSyncedIndexes(table, tableIndex, ctx) {
|
|
1619
|
+
if (table.source === void 0) return;
|
|
1620
|
+
const columns = new Set(table.columns.map((column) => column.columnName));
|
|
1621
|
+
for (const [position, index] of table.indexes.entries()) {
|
|
1622
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "indexes", position], message: `synced table '${table.table}' ${message}` });
|
|
1623
|
+
if (columns.size === 0) fail("must declare columns to declare indexes");
|
|
1624
|
+
if (index.unique) fail("cannot declare a unique index");
|
|
1625
|
+
if (new Set(index.columns).size !== index.columns.length) fail("index names a column more than once");
|
|
1626
|
+
for (const column of index.columns) {
|
|
1627
|
+
if (columns.size > 0 && !columns.has(column)) fail(`index references unknown column '${column}'`);
|
|
1628
|
+
}
|
|
1629
|
+
const name = indexName(table.table, index);
|
|
1630
|
+
if (name.length > MAX_IDENTIFIER_LENGTH) {
|
|
1631
|
+
fail(`derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`);
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
877
1635
|
function validateCellMetadata(table, index, ctx) {
|
|
878
1636
|
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
879
1637
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
@@ -940,12 +1698,24 @@ var localDefinitionSchema = zod.z.object({
|
|
|
940
1698
|
}).default({ uses: [] })
|
|
941
1699
|
}).superRefine((definition, ctx) => {
|
|
942
1700
|
const seen = /* @__PURE__ */ new Set();
|
|
1701
|
+
const syncedIndexOwners = /* @__PURE__ */ new Map();
|
|
943
1702
|
for (const [index, table] of definition.tables.entries()) {
|
|
944
1703
|
if (seen.has(table.table)) {
|
|
945
1704
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
946
1705
|
}
|
|
947
1706
|
seen.add(table.table);
|
|
948
1707
|
validateCellMetadata(table, index, ctx);
|
|
1708
|
+
validateSyncedIndexes(table, index, ctx);
|
|
1709
|
+
if (table.source !== void 0) {
|
|
1710
|
+
for (const [position, entry] of table.indexes.entries()) {
|
|
1711
|
+
const name = indexName(table.table, entry);
|
|
1712
|
+
const owner = syncedIndexOwners.get(name);
|
|
1713
|
+
if (owner !== void 0) {
|
|
1714
|
+
ctx.addIssue({ code: "custom", path: ["tables", index, "indexes", position], message: `synced index '${name}' is also declared on '${owner}'` });
|
|
1715
|
+
}
|
|
1716
|
+
syncedIndexOwners.set(name, table.table);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
949
1719
|
if (table.source !== void 0) {
|
|
950
1720
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
951
1721
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1144,9 +1914,6 @@ localDefinitionSchema.safeExtend({
|
|
|
1144
1914
|
imports: zod.z.array(localDefinitionSchema).max(32).optional()
|
|
1145
1915
|
});
|
|
1146
1916
|
|
|
1147
|
-
// src/client/sql.ts
|
|
1148
|
-
var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
|
|
1149
|
-
|
|
1150
1917
|
// src/ir/index.ts
|
|
1151
1918
|
var LEGACY_IR_VERSION = 1;
|
|
1152
1919
|
var GUARDED_IR_VERSION = 2;
|
|
@@ -1428,7 +2195,7 @@ var variableDefaultIrSchema = zod.z.object({
|
|
|
1428
2195
|
var variableIrSchema = zod.z.object({
|
|
1429
2196
|
name: zod.z.string(),
|
|
1430
2197
|
type: zod.z.union([
|
|
1431
|
-
zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(
|
|
2198
|
+
zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(COLUMN_TYPES2) }),
|
|
1432
2199
|
/** `values` is the declared membership whitelist — the executor validates against it. */
|
|
1433
2200
|
zod.z.object({ kind: zod.z.literal("enum"), name: zod.z.string(), values: zod.z.array(zod.z.string()).min(1) }),
|
|
1434
2201
|
/** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
|
|
@@ -1466,8 +2233,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1466
2233
|
maxRows: zod.z.number().int().min(1).max(1e4)
|
|
1467
2234
|
}).strict().optional(),
|
|
1468
2235
|
irVersion: irVersionSchema,
|
|
1469
|
-
name: zod.z.string(),
|
|
1470
|
-
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
2236
|
+
name: zod.z.string().regex(/^[_A-Za-z][_0-9A-Za-z]{0,254}$/),
|
|
1471
2237
|
kind: zod.z.enum(["query", "mutation"]),
|
|
1472
2238
|
requires: zod.z.object({
|
|
1473
2239
|
capability: zod.z.string().optional(),
|
|
@@ -1483,7 +2249,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1483
2249
|
}
|
|
1484
2250
|
if (operation.language === "sql") {
|
|
1485
2251
|
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1486
|
-
const builtin = operation.name === "BuiltinSqlRead" && operation.
|
|
2252
|
+
const builtin = operation.name === "BuiltinSqlRead" && operation.requires === null;
|
|
1487
2253
|
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !builtin && !operation.requires?.capability) {
|
|
1488
2254
|
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1489
2255
|
}
|
|
@@ -1498,7 +2264,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1498
2264
|
}
|
|
1499
2265
|
});
|
|
1500
2266
|
var resolvedSourceIrSchema = zod.z.object({
|
|
1501
|
-
name: zod.z.string(),
|
|
2267
|
+
name: zod.z.string().regex(/^[_A-Za-z][_0-9A-Za-z]{0,254}$/),
|
|
1502
2268
|
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1503
2269
|
/** Logical table key of the backing relation. */
|
|
1504
2270
|
table: zod.z.string(),
|
|
@@ -1561,7 +2327,7 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1561
2327
|
columns: zod.z.array(zod.z.object({
|
|
1562
2328
|
name: zod.z.string(),
|
|
1563
2329
|
columnName: zod.z.string(),
|
|
1564
|
-
type: zod.z.union([zod.z.enum(
|
|
2330
|
+
type: zod.z.union([zod.z.enum(COLUMN_TYPES2), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
1565
2331
|
nullable: zod.z.boolean()
|
|
1566
2332
|
})),
|
|
1567
2333
|
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
@@ -1612,7 +2378,6 @@ zod.z.object({
|
|
|
1612
2378
|
ownSchemaHash: zod.z.string().optional(),
|
|
1613
2379
|
dependencies: zod.z.record(zod.z.string(), zod.z.string()).optional(),
|
|
1614
2380
|
readPolicies: zod.z.record(zod.z.string(), predIrSchema).optional(),
|
|
1615
|
-
opsHash: zod.z.string(),
|
|
1616
2381
|
/** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
|
|
1617
2382
|
unbackedTables: zod.z.array(zod.z.string()).optional(),
|
|
1618
2383
|
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
@@ -1632,14 +2397,14 @@ zod.z.object({
|
|
|
1632
2397
|
});
|
|
1633
2398
|
|
|
1634
2399
|
// src/predicate-v2/parse.ts
|
|
1635
|
-
var
|
|
1636
|
-
var
|
|
1637
|
-
var
|
|
1638
|
-
var
|
|
1639
|
-
var
|
|
2400
|
+
var KEYWORDS2 = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
|
|
2401
|
+
var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
|
|
2402
|
+
var IDENTIFIER_START2 = /[a-zA-Z_]/;
|
|
2403
|
+
var IDENTIFIER_CHAR2 = /[a-zA-Z0-9_]/;
|
|
2404
|
+
var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
|
|
1640
2405
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
1641
2406
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
1642
|
-
function
|
|
2407
|
+
function tokenize3(source) {
|
|
1643
2408
|
const tokens = [];
|
|
1644
2409
|
let position = 0;
|
|
1645
2410
|
while (position < source.length) {
|
|
@@ -1669,27 +2434,27 @@ function tokenize2(source) {
|
|
|
1669
2434
|
continue;
|
|
1670
2435
|
}
|
|
1671
2436
|
if (char === "'") {
|
|
1672
|
-
const { value, end } =
|
|
2437
|
+
const { value, end } = readString2(source, position);
|
|
1673
2438
|
tokens.push({ kind: "string", value, position });
|
|
1674
2439
|
position = end;
|
|
1675
2440
|
continue;
|
|
1676
2441
|
}
|
|
1677
|
-
const operator =
|
|
2442
|
+
const operator = OPERATOR_PATTERN2.exec(source.slice(position));
|
|
1678
2443
|
if (operator) {
|
|
1679
2444
|
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
1680
2445
|
tokens.push({ kind: "operator", value, position });
|
|
1681
2446
|
position += operator[0].length;
|
|
1682
2447
|
continue;
|
|
1683
2448
|
}
|
|
1684
|
-
const number =
|
|
1685
|
-
if (number && !
|
|
2449
|
+
const number = NUMBER_PATTERN2.exec(source.slice(position));
|
|
2450
|
+
if (number && !IDENTIFIER_START2.test(char)) {
|
|
1686
2451
|
tokens.push({ kind: "number", value: number[0], position });
|
|
1687
2452
|
position += number[0].length;
|
|
1688
2453
|
continue;
|
|
1689
2454
|
}
|
|
1690
|
-
if (
|
|
2455
|
+
if (IDENTIFIER_START2.test(char)) {
|
|
1691
2456
|
let end = position;
|
|
1692
|
-
while (end < source.length &&
|
|
2457
|
+
while (end < source.length && IDENTIFIER_CHAR2.test(source[end])) end += 1;
|
|
1693
2458
|
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
1694
2459
|
position = end;
|
|
1695
2460
|
continue;
|
|
@@ -1699,7 +2464,7 @@ function tokenize2(source) {
|
|
|
1699
2464
|
tokens.push({ kind: "end", value: "", position: source.length });
|
|
1700
2465
|
return tokens;
|
|
1701
2466
|
}
|
|
1702
|
-
function
|
|
2467
|
+
function readString2(source, start) {
|
|
1703
2468
|
let value = "";
|
|
1704
2469
|
let position = start + 1;
|
|
1705
2470
|
while (position < source.length) {
|
|
@@ -1718,7 +2483,7 @@ function readString(source, start) {
|
|
|
1718
2483
|
}
|
|
1719
2484
|
throw new PolicyParseError("unterminated string literal", start);
|
|
1720
2485
|
}
|
|
1721
|
-
var
|
|
2486
|
+
var Parser3 = class {
|
|
1722
2487
|
constructor(tokens) {
|
|
1723
2488
|
this.tokens = tokens;
|
|
1724
2489
|
}
|
|
@@ -1860,7 +2625,7 @@ var Parser2 = class {
|
|
|
1860
2625
|
/** `ident(.ident)*` — first segment must not be a keyword; later segments may be (e.g. `author.role`). */
|
|
1861
2626
|
parsePath(description) {
|
|
1862
2627
|
const first = this.peek();
|
|
1863
|
-
if (first.kind !== "identifier" ||
|
|
2628
|
+
if (first.kind !== "identifier" || KEYWORDS2.has(first.value)) {
|
|
1864
2629
|
throw new PolicyParseError(`expected ${description}, got '${first.value || "end"}'`, first.position);
|
|
1865
2630
|
}
|
|
1866
2631
|
this.advance();
|
|
@@ -1906,7 +2671,7 @@ var Parser2 = class {
|
|
|
1906
2671
|
function parsePolicy(source) {
|
|
1907
2672
|
const trimmed = source.trim();
|
|
1908
2673
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
1909
|
-
return { source: trimmed, ast: new
|
|
2674
|
+
return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
|
|
1910
2675
|
}
|
|
1911
2676
|
|
|
1912
2677
|
// src/predicate-v2/compile.ts
|