@sequenceholdings/orm 2.9.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 +7585 -7512
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +10 -1
- package/dist/cli.d.ts +10 -1
- package/dist/cli.js +7584 -7513
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +1059 -50
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +182 -2
- package/dist/compiler.d.ts +182 -2
- package/dist/compiler.js +1054 -51
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +3720 -3447
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +96 -31
- package/dist/index.d.ts +96 -31
- package/dist/index.js +3714 -3446
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +787 -14
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +70 -59
- package/dist/ir.d.ts +70 -59
- package/dist/ir.js +787 -15
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +802 -30
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -0
- package/dist/predicate-v2.d.ts +2 -0
- package/dist/predicate-v2.js +802 -30
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +794 -24
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.js +794 -24
- 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.cjs
CHANGED
|
@@ -9,16 +9,21 @@ var pgParser = require('@supabase/pg-parser');
|
|
|
9
9
|
|
|
10
10
|
// src/constants.ts
|
|
11
11
|
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
12
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
12
13
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
13
14
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
14
15
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
15
16
|
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
17
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
16
18
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
19
|
+
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
17
20
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
18
21
|
var ormSchemaName = (namespace) => {
|
|
19
22
|
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
20
23
|
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
21
24
|
};
|
|
25
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
26
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
22
27
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
23
28
|
var CELL_PART_SUFFIX = {
|
|
24
29
|
at: "__at",
|
|
@@ -250,6 +255,748 @@ var Parser = class {
|
|
|
250
255
|
function compileCheckExpression(source, columns) {
|
|
251
256
|
return new Parser(tokenize(source), columns).parse();
|
|
252
257
|
}
|
|
258
|
+
|
|
259
|
+
// src/predicate/ast.ts
|
|
260
|
+
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
261
|
+
function walkPredicate(predicate, visit2) {
|
|
262
|
+
visit2(predicate);
|
|
263
|
+
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
264
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit2);
|
|
265
|
+
}
|
|
266
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit2);
|
|
267
|
+
}
|
|
268
|
+
function predicateOperands(predicate) {
|
|
269
|
+
const operands = [];
|
|
270
|
+
walkPredicate(predicate, (node) => {
|
|
271
|
+
if (node.kind === "compare") operands.push(node.left, node.right);
|
|
272
|
+
});
|
|
273
|
+
return operands;
|
|
274
|
+
}
|
|
275
|
+
function referencedColumns(predicate) {
|
|
276
|
+
const names = predicateOperands(predicate).flatMap(
|
|
277
|
+
(operand) => operand.kind === "column" ? [operand.name] : []
|
|
278
|
+
);
|
|
279
|
+
return [...new Set(names)];
|
|
280
|
+
}
|
|
281
|
+
var identifierSchema = zod.z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
282
|
+
var namespaceSchema = zod.z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
283
|
+
var tableReferenceSchema = zod.z.string().refine(
|
|
284
|
+
(value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
|
|
285
|
+
"must be a table name or a schema-qualified table reference"
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
// src/schema/columns.ts
|
|
289
|
+
var COLUMN_TYPES = [
|
|
290
|
+
"uuid",
|
|
291
|
+
"text",
|
|
292
|
+
"integer",
|
|
293
|
+
"bigint",
|
|
294
|
+
"numeric",
|
|
295
|
+
"double",
|
|
296
|
+
"boolean",
|
|
297
|
+
"timestamptz",
|
|
298
|
+
"date",
|
|
299
|
+
"jsonb"
|
|
300
|
+
];
|
|
301
|
+
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
302
|
+
var FUNCTION_DEFAULTS = {
|
|
303
|
+
"gen_random_uuid()": ["uuid"],
|
|
304
|
+
"now()": ["timestamptz", "date"]
|
|
305
|
+
};
|
|
306
|
+
var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
|
|
307
|
+
function defaultIssue(value, type) {
|
|
308
|
+
if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
|
|
309
|
+
if (typeof value === "number") {
|
|
310
|
+
if (!["integer", "bigint", "numeric", "double"].includes(type)) {
|
|
311
|
+
return `numeric default is invalid for type '${type}'`;
|
|
312
|
+
}
|
|
313
|
+
if (["integer", "bigint"].includes(type)) {
|
|
314
|
+
if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
|
|
315
|
+
if (!Number.isSafeInteger(value)) {
|
|
316
|
+
return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
320
|
+
return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
const allowedTypes = FUNCTION_DEFAULTS[value];
|
|
325
|
+
if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
var columnSchema = zod.z.strictObject({
|
|
329
|
+
type: columnTypeSchema,
|
|
330
|
+
primary_key: zod.z.boolean().optional(),
|
|
331
|
+
nullable: zod.z.boolean().optional(),
|
|
332
|
+
unique: zod.z.boolean().optional(),
|
|
333
|
+
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean()]).optional(),
|
|
334
|
+
description: zod.z.string().optional(),
|
|
335
|
+
renamed_from: identifierSchema.optional(),
|
|
336
|
+
cast: zod.z.string().optional(),
|
|
337
|
+
deprecated: zod.z.boolean().optional()
|
|
338
|
+
}).superRefine((column, ctx) => {
|
|
339
|
+
if (column.default !== void 0) {
|
|
340
|
+
const issue = defaultIssue(column.default, column.type);
|
|
341
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
|
|
342
|
+
}
|
|
343
|
+
if (column.primary_key && column.nullable) {
|
|
344
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
|
|
345
|
+
}
|
|
346
|
+
if (column.primary_key && column.deprecated) {
|
|
347
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
|
|
348
|
+
}
|
|
349
|
+
if (column.cast && !CAST_PATTERN.test(column.cast)) {
|
|
350
|
+
ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
function castParts(cast) {
|
|
354
|
+
const match = CAST_PATTERN.exec(cast);
|
|
355
|
+
if (!match) return null;
|
|
356
|
+
return { column: match[1], type: match[2] };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/errors.ts
|
|
360
|
+
var OrmDefinitionError = class extends Error {
|
|
361
|
+
constructor(message, file) {
|
|
362
|
+
super(file ? `${file}: ${message}` : message);
|
|
363
|
+
this.file = file;
|
|
364
|
+
}
|
|
365
|
+
name = "OrmDefinitionError";
|
|
366
|
+
};
|
|
367
|
+
var PredicateParseError = class extends Error {
|
|
368
|
+
constructor(message, position) {
|
|
369
|
+
super(`${message} (at position ${position})`);
|
|
370
|
+
this.position = position;
|
|
371
|
+
}
|
|
372
|
+
name = "PredicateParseError";
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/predicate/parse.ts
|
|
376
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
|
|
377
|
+
var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
378
|
+
var IDENTIFIER_START = /[a-z_]/;
|
|
379
|
+
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
380
|
+
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
381
|
+
function tokenize2(source) {
|
|
382
|
+
const tokens = [];
|
|
383
|
+
let position = 0;
|
|
384
|
+
while (position < source.length) {
|
|
385
|
+
const char = source[position];
|
|
386
|
+
if (/\s/.test(char)) {
|
|
387
|
+
position += 1;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (char === "(") {
|
|
391
|
+
tokens.push({ kind: "lparen", value: char, position });
|
|
392
|
+
position += 1;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (char === ")") {
|
|
396
|
+
tokens.push({ kind: "rparen", value: char, position });
|
|
397
|
+
position += 1;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (char === ",") {
|
|
401
|
+
tokens.push({ kind: "comma", value: char, position });
|
|
402
|
+
position += 1;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (char === "'") {
|
|
406
|
+
const { value, end } = readString(source, position);
|
|
407
|
+
tokens.push({ kind: "string", value, position });
|
|
408
|
+
position = end;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const operator = OPERATOR_PATTERN.exec(source.slice(position));
|
|
412
|
+
if (operator) {
|
|
413
|
+
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
414
|
+
tokens.push({ kind: "operator", value, position });
|
|
415
|
+
position += operator[0].length;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const number = NUMBER_PATTERN.exec(source.slice(position));
|
|
419
|
+
if (number && !IDENTIFIER_START.test(char)) {
|
|
420
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
421
|
+
position += number[0].length;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (IDENTIFIER_START.test(char)) {
|
|
425
|
+
let end = position;
|
|
426
|
+
while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
|
|
427
|
+
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
428
|
+
position = end;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
throw new PredicateParseError(`unexpected character '${char}'`, position);
|
|
432
|
+
}
|
|
433
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
434
|
+
return tokens;
|
|
435
|
+
}
|
|
436
|
+
function readString(source, start) {
|
|
437
|
+
let value = "";
|
|
438
|
+
let position = start + 1;
|
|
439
|
+
while (position < source.length) {
|
|
440
|
+
const char = source[position];
|
|
441
|
+
if (char !== "'") {
|
|
442
|
+
value += char;
|
|
443
|
+
position += 1;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
if (source[position + 1] === "'") {
|
|
447
|
+
value += "'";
|
|
448
|
+
position += 2;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
return { value, end: position + 1 };
|
|
452
|
+
}
|
|
453
|
+
throw new PredicateParseError("unterminated string literal", start);
|
|
454
|
+
}
|
|
455
|
+
var Parser2 = class {
|
|
456
|
+
constructor(tokens) {
|
|
457
|
+
this.tokens = tokens;
|
|
458
|
+
}
|
|
459
|
+
index = 0;
|
|
460
|
+
parse() {
|
|
461
|
+
const predicate = this.parseOr();
|
|
462
|
+
this.expect("end", "end of expression");
|
|
463
|
+
return predicate;
|
|
464
|
+
}
|
|
465
|
+
parseOr() {
|
|
466
|
+
const operands = [this.parseAnd()];
|
|
467
|
+
while (this.matchKeyword("or")) operands.push(this.parseAnd());
|
|
468
|
+
return operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
469
|
+
}
|
|
470
|
+
parseAnd() {
|
|
471
|
+
const operands = [this.parseNot()];
|
|
472
|
+
while (this.matchKeyword("and")) operands.push(this.parseNot());
|
|
473
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
474
|
+
}
|
|
475
|
+
parseNot() {
|
|
476
|
+
if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
|
|
477
|
+
return this.parsePrimary();
|
|
478
|
+
}
|
|
479
|
+
parsePrimary() {
|
|
480
|
+
if (this.peek().kind === "lparen") {
|
|
481
|
+
this.advance();
|
|
482
|
+
const inner = this.parseOr();
|
|
483
|
+
this.expect("rparen", "')'");
|
|
484
|
+
return inner;
|
|
485
|
+
}
|
|
486
|
+
if (this.peekKeyword("capability")) {
|
|
487
|
+
const capability = this.parseCapability();
|
|
488
|
+
const next = this.peek();
|
|
489
|
+
if (next.kind === "operator") {
|
|
490
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
|
|
491
|
+
}
|
|
492
|
+
return capability;
|
|
493
|
+
}
|
|
494
|
+
return this.parseComparison();
|
|
495
|
+
}
|
|
496
|
+
parseCapability() {
|
|
497
|
+
this.advance();
|
|
498
|
+
const key = this.parseCallArgument("capability");
|
|
499
|
+
if (!CAPABILITY_KEY_PATTERN.test(key)) {
|
|
500
|
+
throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
|
|
501
|
+
}
|
|
502
|
+
return { kind: "capability", key };
|
|
503
|
+
}
|
|
504
|
+
parseComparison() {
|
|
505
|
+
const left = this.parseOperand();
|
|
506
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
507
|
+
if (!COMPARE_OPS.includes(operator.value)) {
|
|
508
|
+
throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
|
|
509
|
+
}
|
|
510
|
+
const right = this.parseOperand();
|
|
511
|
+
return { kind: "compare", op: operator.value, left, right };
|
|
512
|
+
}
|
|
513
|
+
parseOperand() {
|
|
514
|
+
const token = this.peek();
|
|
515
|
+
if (token.kind === "string") {
|
|
516
|
+
this.advance();
|
|
517
|
+
return { kind: "literal", value: token.value };
|
|
518
|
+
}
|
|
519
|
+
if (token.kind === "number") {
|
|
520
|
+
this.advance();
|
|
521
|
+
const value = Number(token.value);
|
|
522
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
523
|
+
throw new PredicateParseError(
|
|
524
|
+
`numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
|
|
525
|
+
token.position
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
return { kind: "literal", value };
|
|
529
|
+
}
|
|
530
|
+
if (token.kind !== "identifier") {
|
|
531
|
+
throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
|
|
532
|
+
}
|
|
533
|
+
this.advance();
|
|
534
|
+
if (token.value === "true" || token.value === "false") {
|
|
535
|
+
return { kind: "literal", value: token.value === "true" };
|
|
536
|
+
}
|
|
537
|
+
if (token.value === "null") return { kind: "literal", value: null };
|
|
538
|
+
if (token.value === "claim") {
|
|
539
|
+
const key = this.parseCallArgument("claim");
|
|
540
|
+
if (!CLAIM_KEY_PATTERN.test(key)) {
|
|
541
|
+
throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
|
|
542
|
+
}
|
|
543
|
+
return { kind: "claim", key };
|
|
544
|
+
}
|
|
545
|
+
if (token.value === "capability") {
|
|
546
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
|
|
547
|
+
}
|
|
548
|
+
if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
|
|
549
|
+
throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
|
|
550
|
+
}
|
|
551
|
+
return { kind: "column", name: token.value };
|
|
552
|
+
}
|
|
553
|
+
parseCallArgument(fn) {
|
|
554
|
+
this.expect("lparen", `'(' after ${fn}`);
|
|
555
|
+
const argument = this.expect("string", `a quoted key in ${fn}(...)`);
|
|
556
|
+
this.expect("rparen", `')' after ${fn} key`);
|
|
557
|
+
return argument.value;
|
|
558
|
+
}
|
|
559
|
+
peek() {
|
|
560
|
+
return this.tokens[this.index];
|
|
561
|
+
}
|
|
562
|
+
peekKeyword(keyword) {
|
|
563
|
+
const token = this.peek();
|
|
564
|
+
return token.kind === "identifier" && token.value === keyword;
|
|
565
|
+
}
|
|
566
|
+
matchKeyword(keyword) {
|
|
567
|
+
if (!this.peekKeyword(keyword)) return false;
|
|
568
|
+
this.advance();
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
advance() {
|
|
572
|
+
const token = this.peek();
|
|
573
|
+
if (token.kind !== "end") this.index += 1;
|
|
574
|
+
return token;
|
|
575
|
+
}
|
|
576
|
+
expect(kind, description) {
|
|
577
|
+
const token = this.peek();
|
|
578
|
+
if (token.kind !== kind) {
|
|
579
|
+
throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
|
|
580
|
+
}
|
|
581
|
+
return this.advance();
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
function parsePredicate(source) {
|
|
585
|
+
const trimmed = source.trim();
|
|
586
|
+
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
587
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/schema/predicates.ts
|
|
591
|
+
var parsedPredicateSchema = zod.z.string().transform((source, ctx) => {
|
|
592
|
+
try {
|
|
593
|
+
return parsePredicate(source);
|
|
594
|
+
} catch (error) {
|
|
595
|
+
ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
|
|
596
|
+
return zod.z.NEVER;
|
|
597
|
+
}
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// src/schema/actions.ts
|
|
601
|
+
var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
|
|
602
|
+
var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
|
|
603
|
+
var RETURNING_PATTERN = /\breturning\b/i;
|
|
604
|
+
var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
|
|
605
|
+
var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
|
|
606
|
+
var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
607
|
+
var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
|
|
608
|
+
var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
|
|
609
|
+
var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
|
|
610
|
+
var SQL_COMMENT_PATTERN = /--|\/\*/;
|
|
611
|
+
var actionParamSchema = zod.z.strictObject({
|
|
612
|
+
type: columnTypeSchema,
|
|
613
|
+
optional: zod.z.boolean().optional(),
|
|
614
|
+
description: zod.z.string().optional()
|
|
615
|
+
});
|
|
616
|
+
var validatesEntrySchema = zod.z.string().transform((source, ctx) => {
|
|
617
|
+
const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
|
|
618
|
+
if (!match) {
|
|
619
|
+
ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
|
|
620
|
+
return zod.z.NEVER;
|
|
621
|
+
}
|
|
622
|
+
const table = tableReferenceSchema.safeParse(match[1]);
|
|
623
|
+
if (!table.success) {
|
|
624
|
+
ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
|
|
625
|
+
return zod.z.NEVER;
|
|
626
|
+
}
|
|
627
|
+
return { source: source.trim(), table: table.data, param: match[2] };
|
|
628
|
+
});
|
|
629
|
+
var AUDIT_ACTIONS = ["create", "update", "delete"];
|
|
630
|
+
var auditSchema = zod.z.strictObject({
|
|
631
|
+
action: zod.z.enum(AUDIT_ACTIONS),
|
|
632
|
+
resource: identifierSchema.optional()
|
|
633
|
+
});
|
|
634
|
+
var actionSchema = zod.z.strictObject({
|
|
635
|
+
description: zod.z.string().optional(),
|
|
636
|
+
requires: parsedPredicateSchema.optional(),
|
|
637
|
+
params: zod.z.record(identifierSchema, actionParamSchema).optional(),
|
|
638
|
+
validates: zod.z.array(validatesEntrySchema).optional(),
|
|
639
|
+
// One statement, or an ordered list run atomically in the action function.
|
|
640
|
+
executes: zod.z.union([zod.z.string().min(1), zod.z.array(zod.z.string().min(1)).min(1)]),
|
|
641
|
+
// Declares the columns the action's RETURNING yields. Present → the
|
|
642
|
+
// function returns those rows (typed in the client); absent → the action
|
|
643
|
+
// reports rows_affected only. The column names/types must match the
|
|
644
|
+
// RETURNING list, which Postgres enforces at CREATE FUNCTION.
|
|
645
|
+
returns: zod.z.record(identifierSchema, columnTypeSchema).optional(),
|
|
646
|
+
audit: auditSchema.optional()
|
|
647
|
+
}).superRefine((action, ctx) => {
|
|
648
|
+
const params = Object.keys(action.params ?? {});
|
|
649
|
+
if (action.requires && referencedColumns(action.requires.ast).length > 0) {
|
|
650
|
+
ctx.addIssue({
|
|
651
|
+
code: "custom",
|
|
652
|
+
message: "requires may reference claims and capabilities only, not columns",
|
|
653
|
+
path: ["requires"]
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
|
|
657
|
+
ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
|
|
658
|
+
}
|
|
659
|
+
if (action.returns && Object.keys(action.returns).length === 0) {
|
|
660
|
+
ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
|
|
661
|
+
}
|
|
662
|
+
for (const [index, entry] of (action.validates ?? []).entries()) {
|
|
663
|
+
if (!params.includes(entry.param)) {
|
|
664
|
+
ctx.addIssue({
|
|
665
|
+
code: "custom",
|
|
666
|
+
message: `validates references undeclared param '${entry.param}'`,
|
|
667
|
+
path: ["validates", index]
|
|
668
|
+
});
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
function extractNamedParams(sql) {
|
|
673
|
+
const withoutStrings = sql.replace(QUOTED_STRING_PATTERN, "''");
|
|
674
|
+
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
675
|
+
return [...new Set(names)];
|
|
676
|
+
}
|
|
677
|
+
function actionStatementSources(executes) {
|
|
678
|
+
return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
|
|
679
|
+
}
|
|
680
|
+
function executesIssues(executes, declaredParams, hasReturns) {
|
|
681
|
+
const issues = [];
|
|
682
|
+
const statements = actionStatementSources(executes);
|
|
683
|
+
statements.forEach((statement, index) => {
|
|
684
|
+
const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
|
|
685
|
+
const at = statements.length > 1 ? ` (statement ${index + 1})` : "";
|
|
686
|
+
if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
|
|
687
|
+
if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
|
|
688
|
+
if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
|
|
689
|
+
if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
|
|
690
|
+
if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
|
|
691
|
+
if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
|
|
692
|
+
if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
|
|
693
|
+
issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
|
|
694
|
+
}
|
|
695
|
+
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index === statements.length - 1)) {
|
|
696
|
+
issues.push(
|
|
697
|
+
hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
const last = statements.at(-1);
|
|
702
|
+
if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
|
|
703
|
+
issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
|
|
704
|
+
}
|
|
705
|
+
const undeclared = extractNamedParams(statements.join("\n")).filter((name2) => !declaredParams.has(name2));
|
|
706
|
+
if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
|
|
707
|
+
return issues;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/schema/table.ts
|
|
711
|
+
var linkSchema = zod.z.strictObject({
|
|
712
|
+
to: tableReferenceSchema,
|
|
713
|
+
on: identifierSchema,
|
|
714
|
+
enforce: zod.z.boolean().optional(),
|
|
715
|
+
description: zod.z.string().optional()
|
|
716
|
+
});
|
|
717
|
+
var indexSchema = zod.z.union([
|
|
718
|
+
zod.z.array(identifierSchema).min(1),
|
|
719
|
+
zod.z.strictObject({
|
|
720
|
+
columns: zod.z.array(identifierSchema).min(1),
|
|
721
|
+
unique: zod.z.boolean().optional(),
|
|
722
|
+
name: identifierSchema.optional()
|
|
723
|
+
})
|
|
724
|
+
]).transform((index) => Array.isArray(index) ? { columns: index } : index);
|
|
725
|
+
var policiesSchema = zod.z.strictObject({
|
|
726
|
+
read: parsedPredicateSchema.optional(),
|
|
727
|
+
write: parsedPredicateSchema.optional(),
|
|
728
|
+
insert: parsedPredicateSchema.optional(),
|
|
729
|
+
update: parsedPredicateSchema.optional()
|
|
730
|
+
});
|
|
731
|
+
zod.z.strictObject({
|
|
732
|
+
namespace: namespaceSchema,
|
|
733
|
+
table: identifierSchema,
|
|
734
|
+
description: zod.z.string().optional(),
|
|
735
|
+
columns: zod.z.record(identifierSchema, columnSchema).optional(),
|
|
736
|
+
source: zod.z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
|
|
737
|
+
links: zod.z.record(identifierSchema, linkSchema).optional(),
|
|
738
|
+
indexes: zod.z.array(indexSchema).optional(),
|
|
739
|
+
policies: policiesSchema.optional(),
|
|
740
|
+
actions: zod.z.record(identifierSchema, actionSchema).optional()
|
|
741
|
+
}).superRefine((table, ctx) => {
|
|
742
|
+
if (RESERVED_TABLE_NAMES.has(table.table)) {
|
|
743
|
+
ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
|
|
744
|
+
}
|
|
745
|
+
for (const name2 of Object.keys(table.columns ?? {})) {
|
|
746
|
+
if (RESERVED_COLUMN_NAMES.has(name2)) {
|
|
747
|
+
ctx.addIssue({
|
|
748
|
+
code: "custom",
|
|
749
|
+
message: `column '${name2}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
|
|
750
|
+
path: ["columns", name2]
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (table.source) validateSyncedTable(table, ctx);
|
|
755
|
+
else validateColumnTable(table, ctx);
|
|
756
|
+
validateLinks(table, ctx);
|
|
757
|
+
validatePolicyColumns(table, ctx);
|
|
758
|
+
validateDerivedNameLengths(table, ctx);
|
|
759
|
+
});
|
|
760
|
+
var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
|
|
761
|
+
var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
762
|
+
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
763
|
+
function validateSyncedTable(table, ctx) {
|
|
764
|
+
const forbidden = [
|
|
765
|
+
["actions", "synced tables are read-only; actions are not allowed"],
|
|
766
|
+
["indexes", "v1 synced tables cannot declare indexes; declare them with defineSyncedTable({ indexes }) in a v2 definition"]
|
|
767
|
+
];
|
|
768
|
+
for (const [field, message] of forbidden) {
|
|
769
|
+
if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
|
|
770
|
+
}
|
|
771
|
+
if (table.policies?.write) {
|
|
772
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
773
|
+
}
|
|
774
|
+
if (table.policies?.insert) {
|
|
775
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
776
|
+
}
|
|
777
|
+
if (table.policies?.update) {
|
|
778
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
779
|
+
}
|
|
780
|
+
const columns = table.columns ?? {};
|
|
781
|
+
const names = Object.keys(columns);
|
|
782
|
+
if (names.length === 0) return;
|
|
783
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
784
|
+
if (primaryKeys.length !== 1) {
|
|
785
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
786
|
+
}
|
|
787
|
+
validateRenames(columns, names, ctx);
|
|
788
|
+
for (const name2 of names) {
|
|
789
|
+
const column = columns[name2];
|
|
790
|
+
if (!column) continue;
|
|
791
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
792
|
+
if (column.cast && cast?.column !== name2) {
|
|
793
|
+
ctx.addIssue({
|
|
794
|
+
code: "custom",
|
|
795
|
+
message: "cast must reference the column it is declared on",
|
|
796
|
+
path: ["columns", name2, "cast"]
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
if (cast && cast.type !== column.type) {
|
|
800
|
+
ctx.addIssue({
|
|
801
|
+
code: "custom",
|
|
802
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
803
|
+
path: ["columns", name2, "cast"]
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function validateRenames(columns, names, ctx) {
|
|
809
|
+
const renameSources = /* @__PURE__ */ new Map();
|
|
810
|
+
for (const name2 of names) {
|
|
811
|
+
const renamedFrom = columns[name2]?.renamed_from;
|
|
812
|
+
if (!renamedFrom) continue;
|
|
813
|
+
if (renamedFrom === name2 || names.includes(renamedFrom)) {
|
|
814
|
+
ctx.addIssue({
|
|
815
|
+
code: "custom",
|
|
816
|
+
message: "renamed_from must name a column that no longer exists",
|
|
817
|
+
path: ["columns", name2, "renamed_from"]
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
const claimed = renameSources.get(renamedFrom);
|
|
821
|
+
if (claimed) {
|
|
822
|
+
ctx.addIssue({
|
|
823
|
+
code: "custom",
|
|
824
|
+
message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
|
|
825
|
+
path: ["columns", name2, "renamed_from"]
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
renameSources.set(renamedFrom, name2);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
function validateColumnTable(table, ctx) {
|
|
832
|
+
const columns = table.columns ?? {};
|
|
833
|
+
const names = Object.keys(columns);
|
|
834
|
+
if (names.length === 0) {
|
|
835
|
+
ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const primaryKeys = names.filter((name2) => columns[name2]?.primary_key);
|
|
839
|
+
if (primaryKeys.length !== 1) {
|
|
840
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
841
|
+
}
|
|
842
|
+
validateRenames(columns, names, ctx);
|
|
843
|
+
for (const name2 of names) {
|
|
844
|
+
const column = columns[name2];
|
|
845
|
+
if (!column) continue;
|
|
846
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
847
|
+
if (column.cast && cast?.column !== name2) {
|
|
848
|
+
ctx.addIssue({
|
|
849
|
+
code: "custom",
|
|
850
|
+
message: "cast must reference the column it is declared on",
|
|
851
|
+
path: ["columns", name2, "cast"]
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
if (cast && cast.type !== column.type) {
|
|
855
|
+
ctx.addIssue({
|
|
856
|
+
code: "custom",
|
|
857
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
858
|
+
path: ["columns", name2, "cast"]
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
863
|
+
if (new Set(index.columns).size !== index.columns.length) {
|
|
864
|
+
ctx.addIssue({
|
|
865
|
+
code: "custom",
|
|
866
|
+
message: "index names a column more than once",
|
|
867
|
+
path: ["indexes", position]
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
for (const column of index.columns) {
|
|
871
|
+
if (!names.includes(column)) {
|
|
872
|
+
ctx.addIssue({
|
|
873
|
+
code: "custom",
|
|
874
|
+
message: `index references unknown column '${column}'`,
|
|
875
|
+
path: ["indexes", position]
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
function validateLinks(table, ctx) {
|
|
882
|
+
const columnNames = Object.keys(table.columns ?? {});
|
|
883
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
884
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
885
|
+
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name2, "on"] });
|
|
886
|
+
}
|
|
887
|
+
if (link.enforce && link.to.includes(".")) {
|
|
888
|
+
ctx.addIssue({
|
|
889
|
+
code: "custom",
|
|
890
|
+
message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
|
|
891
|
+
path: ["links", name2, "enforce"]
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
if (link.enforce && table.source) {
|
|
895
|
+
ctx.addIssue({
|
|
896
|
+
code: "custom",
|
|
897
|
+
message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
|
|
898
|
+
path: ["links", name2, "enforce"]
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
function validateDerivedNameLengths(table, ctx) {
|
|
904
|
+
const worstPolicy = policyName(table.table, "insert");
|
|
905
|
+
if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
|
|
906
|
+
ctx.addIssue({
|
|
907
|
+
code: "custom",
|
|
908
|
+
message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
|
|
909
|
+
path: ["table"]
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
913
|
+
const name2 = indexName(table.table, index);
|
|
914
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
915
|
+
ctx.addIssue({
|
|
916
|
+
code: "custom",
|
|
917
|
+
message: `derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
|
|
918
|
+
path: ["indexes", position]
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
for (const [name2, link] of Object.entries(table.links ?? {})) {
|
|
923
|
+
const constraint = foreignKeyName(table.table, name2);
|
|
924
|
+
if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
|
|
925
|
+
ctx.addIssue({
|
|
926
|
+
code: "custom",
|
|
927
|
+
message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
|
|
928
|
+
path: ["links", name2]
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
for (const name2 of Object.keys(table.actions ?? {})) {
|
|
933
|
+
const fnName = `action_${name2}`;
|
|
934
|
+
if (fnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
935
|
+
ctx.addIssue({
|
|
936
|
+
code: "custom",
|
|
937
|
+
message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
|
|
938
|
+
path: ["actions", name2]
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
var TYPE_GROUPS = {
|
|
944
|
+
integer: "numeric",
|
|
945
|
+
bigint: "numeric",
|
|
946
|
+
numeric: "numeric",
|
|
947
|
+
double: "numeric",
|
|
948
|
+
text: "text",
|
|
949
|
+
uuid: "uuid",
|
|
950
|
+
date: "temporal",
|
|
951
|
+
timestamptz: "temporal",
|
|
952
|
+
boolean: "boolean",
|
|
953
|
+
jsonb: "jsonb"
|
|
954
|
+
};
|
|
955
|
+
function validatePolicyColumns(table, ctx) {
|
|
956
|
+
const columns = table.columns ?? {};
|
|
957
|
+
const columnNames = Object.keys(columns);
|
|
958
|
+
if (columnNames.length === 0) return;
|
|
959
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
960
|
+
const predicate = table.policies?.[kind];
|
|
961
|
+
if (!predicate) continue;
|
|
962
|
+
for (const column of referencedColumns(predicate.ast)) {
|
|
963
|
+
if (!columnNames.includes(column)) {
|
|
964
|
+
ctx.addIssue({
|
|
965
|
+
code: "custom",
|
|
966
|
+
message: `policy references unknown column '${column}'`,
|
|
967
|
+
path: ["policies", kind]
|
|
968
|
+
});
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
walkPredicate(predicate.ast, (node) => {
|
|
972
|
+
if (node.kind !== "compare") return;
|
|
973
|
+
const issue = comparisonTypeIssue(node.left, node.right, columns);
|
|
974
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function comparisonTypeIssue(left, right, columns) {
|
|
979
|
+
const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
|
|
980
|
+
if (left.kind === "column" && right.kind === "column") {
|
|
981
|
+
const leftGroup = groupOf(left);
|
|
982
|
+
const rightGroup = groupOf(right);
|
|
983
|
+
if (leftGroup && rightGroup && leftGroup !== rightGroup) {
|
|
984
|
+
return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
989
|
+
const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
990
|
+
if (!column || !literal || literal.value === null) return null;
|
|
991
|
+
const columnGroup = groupOf(column);
|
|
992
|
+
if (!columnGroup) return null;
|
|
993
|
+
if (typeof literal.value === "string") return null;
|
|
994
|
+
const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
|
|
995
|
+
if (literalGroup !== columnGroup) {
|
|
996
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
|
|
997
|
+
}
|
|
998
|
+
return null;
|
|
999
|
+
}
|
|
253
1000
|
var policyVariantSchema = zod.z.object({
|
|
254
1001
|
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
255
1002
|
when: zod.z.string().min(1).max(1e3),
|
|
@@ -566,7 +1313,7 @@ var CANONICAL_FORMAT = 2;
|
|
|
566
1313
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
567
1314
|
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
568
1315
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
569
|
-
var
|
|
1316
|
+
var COLUMN_TYPES2 = [
|
|
570
1317
|
"uuid",
|
|
571
1318
|
"text",
|
|
572
1319
|
"integer",
|
|
@@ -578,7 +1325,7 @@ var COLUMN_TYPES = [
|
|
|
578
1325
|
"date",
|
|
579
1326
|
"jsonb"
|
|
580
1327
|
];
|
|
581
|
-
var
|
|
1328
|
+
var columnTypeSchema2 = zod.z.enum(COLUMN_TYPES2);
|
|
582
1329
|
var canonicalEnumSchema = zod.z.object({
|
|
583
1330
|
name: identifier2,
|
|
584
1331
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
@@ -587,7 +1334,7 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
587
1334
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
588
1335
|
name: zod.z.string().min(1).max(63),
|
|
589
1336
|
columnName: columnIdentifier,
|
|
590
|
-
type: zod.z.union([
|
|
1337
|
+
type: zod.z.union([columnTypeSchema2, zod.z.object({ enum: identifier2 })]),
|
|
591
1338
|
nullable: zod.z.boolean().default(true),
|
|
592
1339
|
unique: zod.z.boolean().default(false),
|
|
593
1340
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -837,7 +1584,7 @@ var canonicalActionSchema = zod.z.object({
|
|
|
837
1584
|
name: identifier2,
|
|
838
1585
|
requires: zod.z.string().max(1e3).optional(),
|
|
839
1586
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
840
|
-
type:
|
|
1587
|
+
type: columnTypeSchema2,
|
|
841
1588
|
nullable: zod.z.boolean().default(false)
|
|
842
1589
|
})).default({}),
|
|
843
1590
|
executes: zod.z.string().min(1).max(1e4),
|
|
@@ -938,7 +1685,8 @@ var canonicalTableSchema = zod.z.object({
|
|
|
938
1685
|
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
939
1686
|
fail4("is read-only and cannot declare write policies", ["policies"]);
|
|
940
1687
|
}
|
|
941
|
-
const
|
|
1688
|
+
const ownedIndexes = table.source === void 0 ? table.indexes : table.indexes.filter((index) => index.unique);
|
|
1689
|
+
const ownedCollections = [ownedIndexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
942
1690
|
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
943
1691
|
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
944
1692
|
fail4("cannot declare owned storage, constraints or identity operations");
|
|
@@ -1040,6 +1788,23 @@ function trackedCellColumns(table) {
|
|
|
1040
1788
|
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1041
1789
|
}
|
|
1042
1790
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1791
|
+
function validateSyncedIndexes(table, tableIndex, ctx) {
|
|
1792
|
+
if (table.source === void 0) return;
|
|
1793
|
+
const columns = new Set(table.columns.map((column) => column.columnName));
|
|
1794
|
+
for (const [position, index] of table.indexes.entries()) {
|
|
1795
|
+
const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "indexes", position], message: `synced table '${table.table}' ${message}` });
|
|
1796
|
+
if (columns.size === 0) fail4("must declare columns to declare indexes");
|
|
1797
|
+
if (index.unique) fail4("cannot declare a unique index");
|
|
1798
|
+
if (new Set(index.columns).size !== index.columns.length) fail4("index names a column more than once");
|
|
1799
|
+
for (const column of index.columns) {
|
|
1800
|
+
if (columns.size > 0 && !columns.has(column)) fail4(`index references unknown column '${column}'`);
|
|
1801
|
+
}
|
|
1802
|
+
const name2 = indexName(table.table, index);
|
|
1803
|
+
if (name2.length > MAX_IDENTIFIER_LENGTH) {
|
|
1804
|
+
fail4(`derived index name '${name2}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1043
1808
|
function validateCellMetadata(table, index, ctx) {
|
|
1044
1809
|
const fail4 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
1045
1810
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
@@ -1106,12 +1871,24 @@ var localDefinitionSchema = zod.z.object({
|
|
|
1106
1871
|
}).default({ uses: [] })
|
|
1107
1872
|
}).superRefine((definition, ctx) => {
|
|
1108
1873
|
const seen = /* @__PURE__ */ new Set();
|
|
1874
|
+
const syncedIndexOwners = /* @__PURE__ */ new Map();
|
|
1109
1875
|
for (const [index, table] of definition.tables.entries()) {
|
|
1110
1876
|
if (seen.has(table.table)) {
|
|
1111
1877
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
1112
1878
|
}
|
|
1113
1879
|
seen.add(table.table);
|
|
1114
1880
|
validateCellMetadata(table, index, ctx);
|
|
1881
|
+
validateSyncedIndexes(table, index, ctx);
|
|
1882
|
+
if (table.source !== void 0) {
|
|
1883
|
+
for (const [position, entry] of table.indexes.entries()) {
|
|
1884
|
+
const name2 = indexName(table.table, entry);
|
|
1885
|
+
const owner = syncedIndexOwners.get(name2);
|
|
1886
|
+
if (owner !== void 0) {
|
|
1887
|
+
ctx.addIssue({ code: "custom", path: ["tables", index, "indexes", position], message: `synced index '${name2}' is also declared on '${owner}'` });
|
|
1888
|
+
}
|
|
1889
|
+
syncedIndexOwners.set(name2, table.table);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1115
1892
|
if (table.source !== void 0) {
|
|
1116
1893
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1117
1894
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1653,7 +2430,7 @@ var variableDefaultIrSchema = zod.z.object({
|
|
|
1653
2430
|
var variableIrSchema = zod.z.object({
|
|
1654
2431
|
name: zod.z.string(),
|
|
1655
2432
|
type: zod.z.union([
|
|
1656
|
-
zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(
|
|
2433
|
+
zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(COLUMN_TYPES2) }),
|
|
1657
2434
|
/** `values` is the declared membership whitelist — the executor validates against it. */
|
|
1658
2435
|
zod.z.object({ kind: zod.z.literal("enum"), name: zod.z.string(), values: zod.z.array(zod.z.string()).min(1) }),
|
|
1659
2436
|
/** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
|
|
@@ -1665,19 +2442,20 @@ var variableIrSchema = zod.z.object({
|
|
|
1665
2442
|
defaultValue: variableDefaultIrSchema.optional()
|
|
1666
2443
|
});
|
|
1667
2444
|
var sqlColumnSchema = zod.z.object({ name: zod.z.string(), type: zod.z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz", "jsonb"]), nullable: zod.z.boolean() });
|
|
2445
|
+
var sqlObjectIrSchema = zod.z.object({
|
|
2446
|
+
ref: zod.z.string(),
|
|
2447
|
+
table: zod.z.string(),
|
|
2448
|
+
columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
|
|
2449
|
+
scan: zod.z.string().min(1),
|
|
2450
|
+
read: zod.z.array(zod.z.string()).min(1)
|
|
2451
|
+
}).strict();
|
|
1668
2452
|
var compiledOperationSchema = zod.z.object({
|
|
1669
2453
|
language: zod.z.literal("sql").optional(),
|
|
1670
2454
|
/** Server-compiled object catalog and permission ceiling, never caller SQL. */
|
|
1671
2455
|
sqlQuery: zod.z.object({
|
|
1672
2456
|
catalog: zod.z.object({
|
|
1673
2457
|
identity: zod.z.string(),
|
|
1674
|
-
objects: zod.z.array(
|
|
1675
|
-
ref: zod.z.string(),
|
|
1676
|
-
table: zod.z.string(),
|
|
1677
|
-
columns: zod.z.array(sqlColumnSchema.extend({ type: zod.z.string() })).min(1),
|
|
1678
|
-
scan: zod.z.string().min(1),
|
|
1679
|
-
read: zod.z.array(zod.z.string()).min(1)
|
|
1680
|
-
}).strict()).min(1).max(256)
|
|
2458
|
+
objects: zod.z.array(sqlObjectIrSchema).min(1).max(256)
|
|
1681
2459
|
}).strict(),
|
|
1682
2460
|
read: zod.z.array(zod.z.string()).min(1).max(256),
|
|
1683
2461
|
maxRows: zod.z.number().int().min(1).max(1e4)
|
|
@@ -1785,7 +2563,7 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1785
2563
|
columns: zod.z.array(zod.z.object({
|
|
1786
2564
|
name: zod.z.string(),
|
|
1787
2565
|
columnName: zod.z.string(),
|
|
1788
|
-
type: zod.z.union([zod.z.enum(
|
|
2566
|
+
type: zod.z.union([zod.z.enum(COLUMN_TYPES2), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
1789
2567
|
nullable: zod.z.boolean()
|
|
1790
2568
|
})),
|
|
1791
2569
|
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
@@ -1922,7 +2700,7 @@ function graphqlScalarFor(type) {
|
|
|
1922
2700
|
}
|
|
1923
2701
|
var COLUMN_TYPE_BY_SCALAR = new Map([
|
|
1924
2702
|
["ID", "text"],
|
|
1925
|
-
...
|
|
2703
|
+
...COLUMN_TYPES2.map((type) => [graphqlScalarFor(type), type])
|
|
1926
2704
|
]);
|
|
1927
2705
|
function columnTypeForScalar(scalarName) {
|
|
1928
2706
|
return COLUMN_TYPE_BY_SCALAR.get(scalarName);
|
|
@@ -2023,14 +2801,14 @@ var PolicyCompileError = class extends Error {
|
|
|
2023
2801
|
};
|
|
2024
2802
|
|
|
2025
2803
|
// src/predicate-v2/parse.ts
|
|
2026
|
-
var
|
|
2027
|
-
var
|
|
2028
|
-
var
|
|
2029
|
-
var
|
|
2030
|
-
var
|
|
2804
|
+
var KEYWORDS2 = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability", "role", "exists"]);
|
|
2805
|
+
var OPERATOR_PATTERN2 = /^(!=|<=|>=|<>|=|<|>)/;
|
|
2806
|
+
var IDENTIFIER_START2 = /[a-zA-Z_]/;
|
|
2807
|
+
var IDENTIFIER_CHAR2 = /[a-zA-Z0-9_]/;
|
|
2808
|
+
var NUMBER_PATTERN2 = /^-?\d+(\.\d+)?/;
|
|
2031
2809
|
var SCALAR_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => !type.endsWith("[]")).map(([key]) => key);
|
|
2032
2810
|
var ARRAY_CLAIM_KEYS = Object.entries(CLAIMS_CONTRACT).filter(([, type]) => type.endsWith("[]")).map(([key]) => key);
|
|
2033
|
-
function
|
|
2811
|
+
function tokenize3(source) {
|
|
2034
2812
|
const tokens = [];
|
|
2035
2813
|
let position = 0;
|
|
2036
2814
|
while (position < source.length) {
|
|
@@ -2060,27 +2838,27 @@ function tokenize2(source) {
|
|
|
2060
2838
|
continue;
|
|
2061
2839
|
}
|
|
2062
2840
|
if (char === "'") {
|
|
2063
|
-
const { value, end } =
|
|
2841
|
+
const { value, end } = readString2(source, position);
|
|
2064
2842
|
tokens.push({ kind: "string", value, position });
|
|
2065
2843
|
position = end;
|
|
2066
2844
|
continue;
|
|
2067
2845
|
}
|
|
2068
|
-
const operator =
|
|
2846
|
+
const operator = OPERATOR_PATTERN2.exec(source.slice(position));
|
|
2069
2847
|
if (operator) {
|
|
2070
2848
|
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
2071
2849
|
tokens.push({ kind: "operator", value, position });
|
|
2072
2850
|
position += operator[0].length;
|
|
2073
2851
|
continue;
|
|
2074
2852
|
}
|
|
2075
|
-
const number =
|
|
2076
|
-
if (number && !
|
|
2853
|
+
const number = NUMBER_PATTERN2.exec(source.slice(position));
|
|
2854
|
+
if (number && !IDENTIFIER_START2.test(char)) {
|
|
2077
2855
|
tokens.push({ kind: "number", value: number[0], position });
|
|
2078
2856
|
position += number[0].length;
|
|
2079
2857
|
continue;
|
|
2080
2858
|
}
|
|
2081
|
-
if (
|
|
2859
|
+
if (IDENTIFIER_START2.test(char)) {
|
|
2082
2860
|
let end = position;
|
|
2083
|
-
while (end < source.length &&
|
|
2861
|
+
while (end < source.length && IDENTIFIER_CHAR2.test(source[end])) end += 1;
|
|
2084
2862
|
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
2085
2863
|
position = end;
|
|
2086
2864
|
continue;
|
|
@@ -2090,7 +2868,7 @@ function tokenize2(source) {
|
|
|
2090
2868
|
tokens.push({ kind: "end", value: "", position: source.length });
|
|
2091
2869
|
return tokens;
|
|
2092
2870
|
}
|
|
2093
|
-
function
|
|
2871
|
+
function readString2(source, start) {
|
|
2094
2872
|
let value = "";
|
|
2095
2873
|
let position = start + 1;
|
|
2096
2874
|
while (position < source.length) {
|
|
@@ -2109,7 +2887,7 @@ function readString(source, start) {
|
|
|
2109
2887
|
}
|
|
2110
2888
|
throw new PolicyParseError("unterminated string literal", start);
|
|
2111
2889
|
}
|
|
2112
|
-
var
|
|
2890
|
+
var Parser3 = class {
|
|
2113
2891
|
constructor(tokens) {
|
|
2114
2892
|
this.tokens = tokens;
|
|
2115
2893
|
}
|
|
@@ -2251,7 +3029,7 @@ var Parser2 = class {
|
|
|
2251
3029
|
/** `ident(.ident)*` — first segment must not be a keyword; later segments may be (e.g. `author.role`). */
|
|
2252
3030
|
parsePath(description) {
|
|
2253
3031
|
const first = this.peek();
|
|
2254
|
-
if (first.kind !== "identifier" ||
|
|
3032
|
+
if (first.kind !== "identifier" || KEYWORDS2.has(first.value)) {
|
|
2255
3033
|
throw new PolicyParseError(`expected ${description}, got '${first.value || "end"}'`, first.position);
|
|
2256
3034
|
}
|
|
2257
3035
|
this.advance();
|
|
@@ -2297,7 +3075,7 @@ var Parser2 = class {
|
|
|
2297
3075
|
function parsePolicy(source) {
|
|
2298
3076
|
const trimmed = source.trim();
|
|
2299
3077
|
if (trimmed.length === 0) throw new PolicyParseError("empty policy expression", 0);
|
|
2300
|
-
return { source: trimmed, ast: new
|
|
3078
|
+
return { source: trimmed, ast: new Parser3(tokenize3(trimmed)).parse() };
|
|
2301
3079
|
}
|
|
2302
3080
|
|
|
2303
3081
|
// src/predicate-v2/compile.ts
|
|
@@ -2337,6 +3115,7 @@ var FLIPPED = {
|
|
|
2337
3115
|
">=": "<="
|
|
2338
3116
|
};
|
|
2339
3117
|
function resolveTable(resolver, table) {
|
|
3118
|
+
resolver.onTable?.(table);
|
|
2340
3119
|
const info = resolver.tables[table];
|
|
2341
3120
|
if (!info) {
|
|
2342
3121
|
throw new PolicyCompileError(`unknown table '${table}'`, "unknown-table", Object.keys(resolver.tables));
|
|
@@ -2981,15 +3760,6 @@ function classifyInputType(name2, tableByTypename) {
|
|
|
2981
3760
|
return void 0;
|
|
2982
3761
|
}
|
|
2983
3762
|
|
|
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
3763
|
// src/canonical/composition.ts
|
|
2994
3764
|
function localContract(definition) {
|
|
2995
3765
|
return normalizeCanonical(localDefinitionSchema.parse(definition));
|
|
@@ -3560,12 +4330,12 @@ function computeOpHash({
|
|
|
3560
4330
|
definition,
|
|
3561
4331
|
schemaHash
|
|
3562
4332
|
}) {
|
|
3563
|
-
const
|
|
3564
|
-
return `sha256:${
|
|
4333
|
+
const digest2 = crypto.createHash("sha256").update(OP_HASH_DOMAIN).update(`${COMPILER_MAJOR}\0${schemaHash}\0`).update(canonicalOperationText(definition)).digest("hex");
|
|
4334
|
+
return `sha256:${digest2}`;
|
|
3565
4335
|
}
|
|
3566
4336
|
function computeOpsHash(opHashes) {
|
|
3567
|
-
const
|
|
3568
|
-
return `sha256:${
|
|
4337
|
+
const digest2 = crypto.createHash("sha256").update(OPS_HASH_DOMAIN).update([...opHashes].sort().join("\n")).digest("hex");
|
|
4338
|
+
return `sha256:${digest2}`;
|
|
3569
4339
|
}
|
|
3570
4340
|
var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "localOnly"]);
|
|
3571
4341
|
var fail = (message, code) => {
|
|
@@ -5020,8 +5790,8 @@ function restrictOperation({
|
|
|
5020
5790
|
// src/compiler-gql/artifact.ts
|
|
5021
5791
|
var SCHEMA_HASH_DOMAIN = "sequence-orm-canonical-v2\0";
|
|
5022
5792
|
function computeSchemaHash(canonical) {
|
|
5023
|
-
const
|
|
5024
|
-
return `sha256:${
|
|
5793
|
+
const digest2 = crypto.createHash("sha256").update(SCHEMA_HASH_DOMAIN).update(stableJson(canonical)).digest("hex");
|
|
5794
|
+
return `sha256:${digest2}`;
|
|
5025
5795
|
}
|
|
5026
5796
|
function compileNamespace({ canonical, sdl, documents }) {
|
|
5027
5797
|
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
@@ -6475,10 +7245,13 @@ function sqlHash(value) {
|
|
|
6475
7245
|
return `sha256:${crypto.createHash("sha256").update(value).digest("hex")}`;
|
|
6476
7246
|
}
|
|
6477
7247
|
function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
|
|
6478
|
-
if (!publishedObjects.length || new Set(publishedObjects).size !== publishedObjects.length) throw new Error("SQL objects must be explicitly declared and unique");
|
|
6479
7248
|
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
6480
7249
|
if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
|
|
6481
|
-
|
|
7250
|
+
return createSqlCatalogFromContext({ canonical, publishedObjects, executionTarget, ctx });
|
|
7251
|
+
}
|
|
7252
|
+
function createSqlCatalogFromContext({ canonical, publishedObjects, executionTarget, ctx, identity: suppliedIdentity }) {
|
|
7253
|
+
if (!publishedObjects.length || new Set(publishedObjects).size !== publishedObjects.length) throw new Error("SQL objects must be explicitly declared and unique");
|
|
7254
|
+
const identity = suppliedIdentity ?? sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
|
|
6482
7255
|
const logicalNames = new Map(Object.entries(ctx.tables).map(([key, meta]) => [meta, key]));
|
|
6483
7256
|
const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${logicalNames.get(meta)}`;
|
|
6484
7257
|
const definitions = [canonical, ...canonical.imports ?? []];
|
|
@@ -6615,9 +7388,10 @@ var SqlEmitter = class {
|
|
|
6615
7388
|
only(cte, ["ctename", "ctequery", "ctematerialized", "location"]);
|
|
6616
7389
|
const name2 = str(cte.ctename);
|
|
6617
7390
|
if (scope.ctes.has(name2)) fail3("duplicate CTE");
|
|
7391
|
+
const materialization = cte.ctematerialized === "CTEMaterializeAlways" ? "MATERIALIZED " : cte.ctematerialized === "CTEMaterializeNever" ? "NOT MATERIALIZED " : cte.ctematerialized === "CTEMaterializeDefault" ? "" : fail3("unsupported CTE materialization");
|
|
6618
7392
|
const result2 = this.select(obj(cte.ctequery), { ...parent, ctes: scope.ctes });
|
|
6619
7393
|
scope.ctes.set(name2, result2);
|
|
6620
|
-
ctes.push(`${quoteIdentifier(name2)} AS (${result2.text})`);
|
|
7394
|
+
ctes.push(`${quoteIdentifier(name2)} AS ${materialization}(${result2.text})`);
|
|
6621
7395
|
}
|
|
6622
7396
|
}
|
|
6623
7397
|
const result = node.op === "SETOP_UNION" ? this.union(node, scope) : this.projection(node, scope);
|
|
@@ -7223,7 +7997,7 @@ function compileBuiltinSqlArtifact({ namespace, definitions, read, unbackedObjec
|
|
|
7223
7997
|
if (owner !== namespace && selected.some((identity) => !definition.exports?.[identity.slice(owner.length + 1)]?.read)) {
|
|
7224
7998
|
throw new Error("SQL object is not exported");
|
|
7225
7999
|
}
|
|
7226
|
-
objects.push(...
|
|
8000
|
+
objects.push(...createSqlCatalogFromContext({ canonical: definition, publishedObjects: selected, executionTarget, ctx }).objects);
|
|
7227
8001
|
}
|
|
7228
8002
|
if (objects.length !== read.length) throw new Error("SQL object is unavailable");
|
|
7229
8003
|
const actualRead = [...new Set(objects.flatMap((object) => object.read))].sort();
|
|
@@ -7261,6 +8035,177 @@ function compileBuiltinSqlArtifact({ namespace, definitions, read, unbackedObjec
|
|
|
7261
8035
|
} }
|
|
7262
8036
|
});
|
|
7263
8037
|
}
|
|
8038
|
+
var OBJECT_METADATA_FORMAT = 1;
|
|
8039
|
+
var digest = zod.z.string().regex(/^sha256:[a-f0-9]{64}$/u);
|
|
8040
|
+
var objectSqlBundleSchema = zod.z.object({
|
|
8041
|
+
format: zod.z.literal(OBJECT_METADATA_FORMAT),
|
|
8042
|
+
dependencies: zod.z.record(zod.z.string(), digest),
|
|
8043
|
+
object: sqlObjectIrSchema,
|
|
8044
|
+
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
8045
|
+
readPolicies: zod.z.record(zod.z.string(), predIrSchema)
|
|
8046
|
+
}).strict();
|
|
8047
|
+
function contractHashes(canonical) {
|
|
8048
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
8049
|
+
for (const owner of [canonical, ...canonical.imports ?? []]) {
|
|
8050
|
+
for (const relation of [...owner.tables, ...owner.views ?? []]) {
|
|
8051
|
+
const name2 = "table" in relation ? relation.table : relation.view;
|
|
8052
|
+
hashes.set(`${owner.namespace}/${name2}`, sqlHash(stableJson([
|
|
8053
|
+
"orm-object-contract-v1",
|
|
8054
|
+
owner.namespace,
|
|
8055
|
+
relation,
|
|
8056
|
+
owner.enums,
|
|
8057
|
+
owner.exports?.[name2]
|
|
8058
|
+
])));
|
|
8059
|
+
}
|
|
8060
|
+
}
|
|
8061
|
+
return hashes;
|
|
8062
|
+
}
|
|
8063
|
+
function objectClosure({ roots, tables, policies, sourceDependencies }) {
|
|
8064
|
+
const required = /* @__PURE__ */ new Set(), links = /* @__PURE__ */ new Map();
|
|
8065
|
+
const walk = (pred, table) => {
|
|
8066
|
+
if (!pred) return;
|
|
8067
|
+
if (pred.kind === "and" || pred.kind === "or") for (const operand of pred.operands) walk(operand, table);
|
|
8068
|
+
else if (pred.kind === "not") walk(pred.operand, table);
|
|
8069
|
+
else if (pred.kind === "exists" || pred.kind === "aggPredicate") {
|
|
8070
|
+
const link = tables[table]?.links.find((candidate) => candidate.name === pred.link);
|
|
8071
|
+
if (!link) throw new Error("object policy dependency is unavailable");
|
|
8072
|
+
const used = links.get(table) ?? /* @__PURE__ */ new Set();
|
|
8073
|
+
used.add(link.name);
|
|
8074
|
+
links.set(table, used);
|
|
8075
|
+
visit2(link.target);
|
|
8076
|
+
walk(pred.where, link.target);
|
|
8077
|
+
}
|
|
8078
|
+
};
|
|
8079
|
+
const visit2 = (ref) => {
|
|
8080
|
+
if (required.has(ref)) return;
|
|
8081
|
+
const meta = tables[ref];
|
|
8082
|
+
if (!meta) throw new Error("object dependency is unavailable");
|
|
8083
|
+
required.add(ref);
|
|
8084
|
+
if (required.size > 1024) throw new Error("object dependency budget exceeded");
|
|
8085
|
+
walk(policies[ref], ref);
|
|
8086
|
+
for (const dependency of sourceDependencies.get(ref) ?? []) visit2(dependency);
|
|
8087
|
+
for (const backing of meta.dependencies ?? []) visit2(backing);
|
|
8088
|
+
};
|
|
8089
|
+
for (const root of roots) visit2(root);
|
|
8090
|
+
return { required: [...required].sort(), links };
|
|
8091
|
+
}
|
|
8092
|
+
async function compileObjectMetadata({ canonical: raw }) {
|
|
8093
|
+
const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(raw));
|
|
8094
|
+
await validateManagedViewQueries({ canonical });
|
|
8095
|
+
const context = buildCompileContext(canonical);
|
|
8096
|
+
if (context.diagnostics.some((item) => item.severity === "error")) throw new Error("invalid object metadata definition");
|
|
8097
|
+
const { ctx } = context;
|
|
8098
|
+
const hashes = contractHashes(canonical);
|
|
8099
|
+
const ref = (key) => {
|
|
8100
|
+
const meta = ctx.tables[key];
|
|
8101
|
+
if (!meta) throw new Error("object dependency is unavailable");
|
|
8102
|
+
return `${meta.namespace ?? canonical.namespace}/${key}`;
|
|
8103
|
+
};
|
|
8104
|
+
const tables = {}, policies = {};
|
|
8105
|
+
const sourceDependencies = /* @__PURE__ */ new Map();
|
|
8106
|
+
const relations = [canonical, ...canonical.imports ?? []].flatMap(canonicalRelations);
|
|
8107
|
+
const expressions = new Map(relations.map((relation) => [relation.table, relation.policies.read]));
|
|
8108
|
+
for (const [key, meta] of Object.entries(ctx.tables)) {
|
|
8109
|
+
tables[ref(key)] = {
|
|
8110
|
+
...meta,
|
|
8111
|
+
namespace: meta.namespace ?? canonical.namespace,
|
|
8112
|
+
links: meta.links.map((link) => ({ ...link, target: ref(link.target) })),
|
|
8113
|
+
...meta.dependencies ? { dependencies: meta.dependencies.map(ref) } : {},
|
|
8114
|
+
...meta.foreignReferences ? { foreignReferences: meta.foreignReferences.map((link) => ({ ...link, target: ref(link.target) })) } : {}
|
|
8115
|
+
};
|
|
8116
|
+
const policy = ctx.readPolicies.get(key);
|
|
8117
|
+
if (policy) policies[ref(key)] = policy;
|
|
8118
|
+
const expression = expressions.get(key);
|
|
8119
|
+
if (expression !== void 0) {
|
|
8120
|
+
const consulted = /* @__PURE__ */ new Set();
|
|
8121
|
+
compilePolicy({ expression, table: key, access: "read", resolver: {
|
|
8122
|
+
tables: ctx.tables,
|
|
8123
|
+
onTable: (table) => {
|
|
8124
|
+
consulted.add(ref(table));
|
|
8125
|
+
},
|
|
8126
|
+
readPolicy: (table) => expressions.get(table),
|
|
8127
|
+
policyNamespace: (table) => ctx.tables[table]?.namespace
|
|
8128
|
+
} });
|
|
8129
|
+
sourceDependencies.set(ref(key), [...consulted]);
|
|
8130
|
+
}
|
|
8131
|
+
}
|
|
8132
|
+
const objects = [];
|
|
8133
|
+
for (const relation of [...canonical.tables, ...canonical.views ?? []]) {
|
|
8134
|
+
const name2 = "table" in relation ? relation.table : relation.view;
|
|
8135
|
+
const identity = `${canonical.namespace}/${name2}`, contractHash = hashes.get(identity);
|
|
8136
|
+
let bundle = null;
|
|
8137
|
+
if (!relation.internal) {
|
|
8138
|
+
const object = createSqlCatalogFromContext({
|
|
8139
|
+
canonical,
|
|
8140
|
+
ctx,
|
|
8141
|
+
identity: contractHash,
|
|
8142
|
+
publishedObjects: [identity],
|
|
8143
|
+
executionTarget: "default"
|
|
8144
|
+
}).objects[0];
|
|
8145
|
+
const closure = objectClosure({ roots: object.read, tables, policies, sourceDependencies });
|
|
8146
|
+
bundle = objectSqlBundleSchema.parse({
|
|
8147
|
+
format: OBJECT_METADATA_FORMAT,
|
|
8148
|
+
object,
|
|
8149
|
+
// platformUsers is compiler-owned metadata, pinned by the format above.
|
|
8150
|
+
dependencies: Object.fromEntries(closure.required.filter((key) => hashes.has(key)).map((key) => [key, hashes.get(key)])),
|
|
8151
|
+
tables: Object.fromEntries(closure.required.map((key) => {
|
|
8152
|
+
const { foreignReferences: _writeReferences, ...meta } = tables[key];
|
|
8153
|
+
return [key, { ...meta, links: meta.links.filter((link) => closure.links.get(key)?.has(link.name)) }];
|
|
8154
|
+
})),
|
|
8155
|
+
readPolicies: Object.fromEntries(closure.required.filter((key) => policies[key]).map((key) => [key, policies[key]]))
|
|
8156
|
+
});
|
|
8157
|
+
}
|
|
8158
|
+
objects.push({ name: name2, contractHash, bundle });
|
|
8159
|
+
}
|
|
8160
|
+
return {
|
|
8161
|
+
format: OBJECT_METADATA_FORMAT,
|
|
8162
|
+
objects,
|
|
8163
|
+
relations: Object.values(ctx.tables).map(({ schema, table }) => ({ schema, table }))
|
|
8164
|
+
};
|
|
8165
|
+
}
|
|
8166
|
+
function composeObjectSqlArtifact({ namespace, bundles }) {
|
|
8167
|
+
const tables = {}, readPolicies = {};
|
|
8168
|
+
const pins = /* @__PURE__ */ new Map();
|
|
8169
|
+
const objects = bundles.map((bundle) => {
|
|
8170
|
+
if (!bundle.object.ref.startsWith(`${namespace}/`) && !bundle.tables[bundle.object.ref]?.exportRead) throw new Error("SQL object is not exported");
|
|
8171
|
+
for (const [ref, hash] of Object.entries(bundle.dependencies)) {
|
|
8172
|
+
if (pins.has(ref) && pins.get(ref) !== hash) throw new Error("inconsistent object contracts");
|
|
8173
|
+
pins.set(ref, hash);
|
|
8174
|
+
}
|
|
8175
|
+
for (const [ref, meta] of Object.entries(bundle.tables)) {
|
|
8176
|
+
const previous = tables[ref];
|
|
8177
|
+
const links = new Map([...previous?.links ?? [], ...meta.links].map((link) => [link.name, link]));
|
|
8178
|
+
tables[ref] = { ...meta, links: [...links.values()] };
|
|
8179
|
+
}
|
|
8180
|
+
Object.assign(readPolicies, bundle.readPolicies);
|
|
8181
|
+
return bundle.object;
|
|
8182
|
+
});
|
|
8183
|
+
const read = [...new Set(objects.flatMap((object) => object.read))].sort();
|
|
8184
|
+
const identity = sqlHash(stableJson(["orm-object-sql-v1", namespace, bundles]));
|
|
8185
|
+
return normalizeIr({
|
|
8186
|
+
irVersion: usesServicePrincipal(readPolicies) ? SERVICE_PRINCIPAL_IR_VERSION : RUNTIME_SQL_IR_VERSION,
|
|
8187
|
+
namespace,
|
|
8188
|
+
schemaHash: identity,
|
|
8189
|
+
opsHash: BUILTIN_SQL_READ_HASH,
|
|
8190
|
+
tables,
|
|
8191
|
+
readPolicies,
|
|
8192
|
+
operations: { BuiltinSqlRead: {
|
|
8193
|
+
irVersion: RUNTIME_SQL_IR_VERSION,
|
|
8194
|
+
name: "BuiltinSqlRead",
|
|
8195
|
+
opHash: BUILTIN_SQL_READ_HASH,
|
|
8196
|
+
language: "sql",
|
|
8197
|
+
kind: "query",
|
|
8198
|
+
requires: null,
|
|
8199
|
+
roots: [],
|
|
8200
|
+
tableSet: read,
|
|
8201
|
+
variables: [
|
|
8202
|
+
{ name: "sql", type: { kind: "scalar", scalar: "text" }, nullable: false, list: false },
|
|
8203
|
+
{ name: "parameters", type: { kind: "scalar", scalar: "jsonb" }, nullable: true, list: false }
|
|
8204
|
+
],
|
|
8205
|
+
sqlQuery: { catalog: { identity, objects }, read, maxRows: 1e3 }
|
|
8206
|
+
} }
|
|
8207
|
+
});
|
|
8208
|
+
}
|
|
7264
8209
|
var DATASET_PREVIEW_LIMIT = 100;
|
|
7265
8210
|
function compileDatasetPreviews(input) {
|
|
7266
8211
|
const canonical = normalizeCanonical(input);
|
|
@@ -7306,6 +8251,64 @@ function compileDatasetPreviews(input) {
|
|
|
7306
8251
|
return operations;
|
|
7307
8252
|
}
|
|
7308
8253
|
|
|
8254
|
+
// src/compiler-gql/operation-metadata.ts
|
|
8255
|
+
var OPERATION_METADATA_FORMAT = 1;
|
|
8256
|
+
function projectOperationMetadata({ artifact, operation }) {
|
|
8257
|
+
const required = /* @__PURE__ */ new Set(), dynamic = /* @__PURE__ */ new Set();
|
|
8258
|
+
const visit2 = (table) => {
|
|
8259
|
+
if (required.has(table)) return;
|
|
8260
|
+
const meta = artifact.tables[table];
|
|
8261
|
+
if (!meta) throw new Error(`operation metadata references missing table '${table}'`);
|
|
8262
|
+
required.add(table);
|
|
8263
|
+
walk(artifact.readPolicies?.[table], table);
|
|
8264
|
+
for (const dependency of meta.dependencies ?? []) visit2(dependency);
|
|
8265
|
+
};
|
|
8266
|
+
const walk = (value, table) => {
|
|
8267
|
+
if (Array.isArray(value)) {
|
|
8268
|
+
for (const child of value) walk(child, table);
|
|
8269
|
+
return;
|
|
8270
|
+
}
|
|
8271
|
+
if (!value || typeof value !== "object") return;
|
|
8272
|
+
const node = value;
|
|
8273
|
+
let scope = typeof node.table === "string" && artifact.tables[node.table] ? node.table : table;
|
|
8274
|
+
if (scope !== table) visit2(scope);
|
|
8275
|
+
if (typeof node.link === "string") {
|
|
8276
|
+
const link = artifact.tables[scope]?.links.find((candidate) => candidate.name === node.link);
|
|
8277
|
+
if (!link) throw new Error("operation metadata has an unresolved relationship");
|
|
8278
|
+
scope = link.target;
|
|
8279
|
+
visit2(scope);
|
|
8280
|
+
}
|
|
8281
|
+
for (const [key, child] of Object.entries(node)) {
|
|
8282
|
+
if (["value", "objects", "set", "inc", "append", "pk", "onConflict"].includes(key)) continue;
|
|
8283
|
+
walk(child, scope);
|
|
8284
|
+
}
|
|
8285
|
+
};
|
|
8286
|
+
const visitDynamic = (table) => {
|
|
8287
|
+
if (dynamic.has(table)) return;
|
|
8288
|
+
dynamic.add(table);
|
|
8289
|
+
visit2(table);
|
|
8290
|
+
for (const link of artifact.tables[table].links) visitDynamic(link.target);
|
|
8291
|
+
};
|
|
8292
|
+
for (const table of operation.tableSet) visit2(table);
|
|
8293
|
+
for (const root of operation.roots) {
|
|
8294
|
+
visit2(root.table);
|
|
8295
|
+
walk(root, root.table);
|
|
8296
|
+
if (root.kind === "mutation" && !["delete", "deleteByPk", "cowRevert"].includes(root.op)) {
|
|
8297
|
+
for (const reference of artifact.tables[root.table].foreignReferences ?? []) visit2(reference.target);
|
|
8298
|
+
}
|
|
8299
|
+
}
|
|
8300
|
+
for (const variable of operation.variables) if (variable.type.kind === "inputRef") {
|
|
8301
|
+
visit2(variable.type.table);
|
|
8302
|
+
if (["boolExp", "orderBy"].includes(variable.type.input)) visitDynamic(variable.type.table);
|
|
8303
|
+
}
|
|
8304
|
+
return normalizeIr({
|
|
8305
|
+
...artifact,
|
|
8306
|
+
tables: Object.fromEntries([...required].sort().map((table) => [table, artifact.tables[table]])),
|
|
8307
|
+
readPolicies: Object.fromEntries([...required].filter((table) => artifact.readPolicies?.[table]).map((table) => [table, artifact.readPolicies[table]])),
|
|
8308
|
+
operations: { [operation.name]: operation }
|
|
8309
|
+
});
|
|
8310
|
+
}
|
|
8311
|
+
|
|
7309
8312
|
exports.COMPILER_MAJOR = COMPILER_MAJOR;
|
|
7310
8313
|
exports.CONST_TRUE = CONST_TRUE;
|
|
7311
8314
|
exports.DATASET_PREVIEW_LIMIT = DATASET_PREVIEW_LIMIT;
|
|
@@ -7313,6 +8316,8 @@ exports.DEFAULT_LIMIT = DEFAULT_LIMIT;
|
|
|
7313
8316
|
exports.DocumentCompileError = DocumentCompileError;
|
|
7314
8317
|
exports.MAX_LIMIT = MAX_LIMIT;
|
|
7315
8318
|
exports.NamespaceCompileError = NamespaceCompileError;
|
|
8319
|
+
exports.OBJECT_METADATA_FORMAT = OBJECT_METADATA_FORMAT;
|
|
8320
|
+
exports.OPERATION_METADATA_FORMAT = OPERATION_METADATA_FORMAT;
|
|
7316
8321
|
exports.ORDER_BY_DIRECTIONS = ORDER_BY_DIRECTIONS;
|
|
7317
8322
|
exports.OperationLowerer = OperationLowerer;
|
|
7318
8323
|
exports.PLATFORM_USER_TABLE = PLATFORM_USER_TABLE;
|
|
@@ -7336,10 +8341,12 @@ exports.compileDatasetPreviews = compileDatasetPreviews;
|
|
|
7336
8341
|
exports.compileDefaultOperations = compileDefaultOperations;
|
|
7337
8342
|
exports.compileNamespace = compileNamespace;
|
|
7338
8343
|
exports.compileNamespaceOrThrow = compileNamespaceOrThrow;
|
|
8344
|
+
exports.compileObjectMetadata = compileObjectMetadata;
|
|
7339
8345
|
exports.compileOperationDocuments = compileOperationDocuments;
|
|
7340
8346
|
exports.compilePolicy = compilePolicy;
|
|
7341
8347
|
exports.compilePredicateV2 = compilePredicateV2;
|
|
7342
8348
|
exports.compileRuntimeSql = compileRuntimeSql;
|
|
8349
|
+
exports.composeObjectSqlArtifact = composeObjectSqlArtifact;
|
|
7343
8350
|
exports.computeOpHash = computeOpHash;
|
|
7344
8351
|
exports.computeOpsHash = computeOpsHash;
|
|
7345
8352
|
exports.computePinnedColumnsByTable = computePinnedColumnsByTable;
|
|
@@ -7353,6 +8360,7 @@ exports.inputTypeName = inputTypeName;
|
|
|
7353
8360
|
exports.isSqlOperationDocument = isSqlOperationDocument;
|
|
7354
8361
|
exports.lowerOperation = lowerOperation;
|
|
7355
8362
|
exports.mutationRootNames = mutationRootNames;
|
|
8363
|
+
exports.objectSqlBundleSchema = objectSqlBundleSchema;
|
|
7356
8364
|
exports.parseDocuments = parseDocuments;
|
|
7357
8365
|
exports.parsePolicy = parsePolicy;
|
|
7358
8366
|
exports.parseSdl = parseSdl;
|
|
@@ -7360,6 +8368,7 @@ exports.pascalCase = pascalCase;
|
|
|
7360
8368
|
exports.platformUserMeta = platformUserMeta;
|
|
7361
8369
|
exports.predContains = predContains;
|
|
7362
8370
|
exports.predContainsExists = predContainsExists;
|
|
8371
|
+
exports.projectOperationMetadata = projectOperationMetadata;
|
|
7363
8372
|
exports.queryRootNames = queryRootNames;
|
|
7364
8373
|
exports.referencedSqlObjects = referencedSqlObjects;
|
|
7365
8374
|
exports.restrictOperation = restrictOperation;
|