@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/ir.js
CHANGED
|
@@ -202,10 +202,16 @@ function compileCheckExpression(source, columns) {
|
|
|
202
202
|
|
|
203
203
|
// src/constants.ts
|
|
204
204
|
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
205
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
205
206
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
206
207
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
208
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
207
209
|
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
210
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
208
211
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
212
|
+
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
213
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
214
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
209
215
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
210
216
|
var CELL_PART_SUFFIX = {
|
|
211
217
|
at: "__at",
|
|
@@ -221,6 +227,741 @@ function cellColumnName(column, part) {
|
|
|
221
227
|
}
|
|
222
228
|
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
223
229
|
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
230
|
+
|
|
231
|
+
// src/predicate/ast.ts
|
|
232
|
+
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
233
|
+
function walkPredicate(predicate, visit) {
|
|
234
|
+
visit(predicate);
|
|
235
|
+
if (predicate.kind === "and" || predicate.kind === "or") {
|
|
236
|
+
for (const operand of predicate.operands) walkPredicate(operand, visit);
|
|
237
|
+
}
|
|
238
|
+
if (predicate.kind === "not") walkPredicate(predicate.operand, visit);
|
|
239
|
+
}
|
|
240
|
+
function predicateOperands(predicate) {
|
|
241
|
+
const operands = [];
|
|
242
|
+
walkPredicate(predicate, (node) => {
|
|
243
|
+
if (node.kind === "compare") operands.push(node.left, node.right);
|
|
244
|
+
});
|
|
245
|
+
return operands;
|
|
246
|
+
}
|
|
247
|
+
function referencedColumns(predicate) {
|
|
248
|
+
const names = predicateOperands(predicate).flatMap(
|
|
249
|
+
(operand) => operand.kind === "column" ? [operand.name] : []
|
|
250
|
+
);
|
|
251
|
+
return [...new Set(names)];
|
|
252
|
+
}
|
|
253
|
+
var identifierSchema = z.string().max(MAX_IDENTIFIER_LENGTH).regex(IDENTIFIER_PATTERN, "must be a lowercase identifier (a-z, 0-9, _)");
|
|
254
|
+
var namespaceSchema = z.string().regex(NAMESPACE_PATTERN, "must be lowercase, start with a letter, and be at most 41 characters");
|
|
255
|
+
var tableReferenceSchema = z.string().refine(
|
|
256
|
+
(value) => IDENTIFIER_PATTERN.test(value) || QUALIFIED_TABLE_PATTERN.test(value),
|
|
257
|
+
"must be a table name or a schema-qualified table reference"
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// src/schema/columns.ts
|
|
261
|
+
var COLUMN_TYPES = [
|
|
262
|
+
"uuid",
|
|
263
|
+
"text",
|
|
264
|
+
"integer",
|
|
265
|
+
"bigint",
|
|
266
|
+
"numeric",
|
|
267
|
+
"double",
|
|
268
|
+
"boolean",
|
|
269
|
+
"timestamptz",
|
|
270
|
+
"date",
|
|
271
|
+
"jsonb"
|
|
272
|
+
];
|
|
273
|
+
var columnTypeSchema = z.enum(COLUMN_TYPES);
|
|
274
|
+
var FUNCTION_DEFAULTS = {
|
|
275
|
+
"gen_random_uuid()": ["uuid"],
|
|
276
|
+
"now()": ["timestamptz", "date"]
|
|
277
|
+
};
|
|
278
|
+
var CAST_PATTERN = /^([a-z][a-z0-9_]*)::([a-z]+)$/;
|
|
279
|
+
function defaultIssue(value, type) {
|
|
280
|
+
if (typeof value === "boolean") return type === "boolean" ? null : `boolean default is invalid for type '${type}'`;
|
|
281
|
+
if (typeof value === "number") {
|
|
282
|
+
if (!["integer", "bigint", "numeric", "double"].includes(type)) {
|
|
283
|
+
return `numeric default is invalid for type '${type}'`;
|
|
284
|
+
}
|
|
285
|
+
if (["integer", "bigint"].includes(type)) {
|
|
286
|
+
if (!Number.isInteger(value)) return `default for type '${type}' must be an integer`;
|
|
287
|
+
if (!Number.isSafeInteger(value)) {
|
|
288
|
+
return `default for type '${type}' exceeds the safe integer range \u2014 YAML numbers lose precision past 2^53`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (type === "numeric" && Math.abs(value) > Number.MAX_SAFE_INTEGER) {
|
|
292
|
+
return "numeric default exceeds the safe float range \u2014 write it as a string (e.g. '9007199254740993') to keep exact precision";
|
|
293
|
+
}
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const allowedTypes = FUNCTION_DEFAULTS[value];
|
|
297
|
+
if (allowedTypes) return allowedTypes.includes(type) ? null : `default '${value}' is invalid for type '${type}'`;
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
var columnSchema = z.strictObject({
|
|
301
|
+
type: columnTypeSchema,
|
|
302
|
+
primary_key: z.boolean().optional(),
|
|
303
|
+
nullable: z.boolean().optional(),
|
|
304
|
+
unique: z.boolean().optional(),
|
|
305
|
+
default: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
306
|
+
description: z.string().optional(),
|
|
307
|
+
renamed_from: identifierSchema.optional(),
|
|
308
|
+
cast: z.string().optional(),
|
|
309
|
+
deprecated: z.boolean().optional()
|
|
310
|
+
}).superRefine((column, ctx) => {
|
|
311
|
+
if (column.default !== void 0) {
|
|
312
|
+
const issue = defaultIssue(column.default, column.type);
|
|
313
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["default"] });
|
|
314
|
+
}
|
|
315
|
+
if (column.primary_key && column.nullable) {
|
|
316
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be nullable", path: ["nullable"] });
|
|
317
|
+
}
|
|
318
|
+
if (column.primary_key && column.deprecated) {
|
|
319
|
+
ctx.addIssue({ code: "custom", message: "a primary key column cannot be deprecated", path: ["deprecated"] });
|
|
320
|
+
}
|
|
321
|
+
if (column.cast && !CAST_PATTERN.test(column.cast)) {
|
|
322
|
+
ctx.addIssue({ code: "custom", message: "cast must have the shape '<column>::<type>'", path: ["cast"] });
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
function castParts(cast) {
|
|
326
|
+
const match = CAST_PATTERN.exec(cast);
|
|
327
|
+
if (!match) return null;
|
|
328
|
+
return { column: match[1], type: match[2] };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// src/errors.ts
|
|
332
|
+
var PredicateParseError = class extends Error {
|
|
333
|
+
constructor(message, position) {
|
|
334
|
+
super(`${message} (at position ${position})`);
|
|
335
|
+
this.position = position;
|
|
336
|
+
}
|
|
337
|
+
name = "PredicateParseError";
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// src/predicate/parse.ts
|
|
341
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["and", "or", "not", "true", "false", "null", "claim", "capability"]);
|
|
342
|
+
var OPERATOR_PATTERN = /^(!=|<=|>=|<>|=|<|>)/;
|
|
343
|
+
var IDENTIFIER_START = /[a-z_]/;
|
|
344
|
+
var IDENTIFIER_CHAR = /[a-z0-9_.-]/;
|
|
345
|
+
var NUMBER_PATTERN = /^-?\d+(\.\d+)?/;
|
|
346
|
+
function tokenize2(source) {
|
|
347
|
+
const tokens = [];
|
|
348
|
+
let position = 0;
|
|
349
|
+
while (position < source.length) {
|
|
350
|
+
const char = source[position];
|
|
351
|
+
if (/\s/.test(char)) {
|
|
352
|
+
position += 1;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (char === "(") {
|
|
356
|
+
tokens.push({ kind: "lparen", value: char, position });
|
|
357
|
+
position += 1;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (char === ")") {
|
|
361
|
+
tokens.push({ kind: "rparen", value: char, position });
|
|
362
|
+
position += 1;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
if (char === ",") {
|
|
366
|
+
tokens.push({ kind: "comma", value: char, position });
|
|
367
|
+
position += 1;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
if (char === "'") {
|
|
371
|
+
const { value, end } = readString(source, position);
|
|
372
|
+
tokens.push({ kind: "string", value, position });
|
|
373
|
+
position = end;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
const operator = OPERATOR_PATTERN.exec(source.slice(position));
|
|
377
|
+
if (operator) {
|
|
378
|
+
const value = operator[0] === "<>" ? "!=" : operator[0];
|
|
379
|
+
tokens.push({ kind: "operator", value, position });
|
|
380
|
+
position += operator[0].length;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
const number = NUMBER_PATTERN.exec(source.slice(position));
|
|
384
|
+
if (number && !IDENTIFIER_START.test(char)) {
|
|
385
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
386
|
+
position += number[0].length;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (IDENTIFIER_START.test(char)) {
|
|
390
|
+
let end = position;
|
|
391
|
+
while (end < source.length && IDENTIFIER_CHAR.test(source[end])) end += 1;
|
|
392
|
+
tokens.push({ kind: "identifier", value: source.slice(position, end), position });
|
|
393
|
+
position = end;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
throw new PredicateParseError(`unexpected character '${char}'`, position);
|
|
397
|
+
}
|
|
398
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
399
|
+
return tokens;
|
|
400
|
+
}
|
|
401
|
+
function readString(source, start) {
|
|
402
|
+
let value = "";
|
|
403
|
+
let position = start + 1;
|
|
404
|
+
while (position < source.length) {
|
|
405
|
+
const char = source[position];
|
|
406
|
+
if (char !== "'") {
|
|
407
|
+
value += char;
|
|
408
|
+
position += 1;
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
if (source[position + 1] === "'") {
|
|
412
|
+
value += "'";
|
|
413
|
+
position += 2;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
return { value, end: position + 1 };
|
|
417
|
+
}
|
|
418
|
+
throw new PredicateParseError("unterminated string literal", start);
|
|
419
|
+
}
|
|
420
|
+
var Parser2 = class {
|
|
421
|
+
constructor(tokens) {
|
|
422
|
+
this.tokens = tokens;
|
|
423
|
+
}
|
|
424
|
+
index = 0;
|
|
425
|
+
parse() {
|
|
426
|
+
const predicate = this.parseOr();
|
|
427
|
+
this.expect("end", "end of expression");
|
|
428
|
+
return predicate;
|
|
429
|
+
}
|
|
430
|
+
parseOr() {
|
|
431
|
+
const operands = [this.parseAnd()];
|
|
432
|
+
while (this.matchKeyword("or")) operands.push(this.parseAnd());
|
|
433
|
+
return operands.length === 1 ? operands[0] : { kind: "or", operands };
|
|
434
|
+
}
|
|
435
|
+
parseAnd() {
|
|
436
|
+
const operands = [this.parseNot()];
|
|
437
|
+
while (this.matchKeyword("and")) operands.push(this.parseNot());
|
|
438
|
+
return operands.length === 1 ? operands[0] : { kind: "and", operands };
|
|
439
|
+
}
|
|
440
|
+
parseNot() {
|
|
441
|
+
if (this.matchKeyword("not")) return { kind: "not", operand: this.parseNot() };
|
|
442
|
+
return this.parsePrimary();
|
|
443
|
+
}
|
|
444
|
+
parsePrimary() {
|
|
445
|
+
if (this.peek().kind === "lparen") {
|
|
446
|
+
this.advance();
|
|
447
|
+
const inner = this.parseOr();
|
|
448
|
+
this.expect("rparen", "')'");
|
|
449
|
+
return inner;
|
|
450
|
+
}
|
|
451
|
+
if (this.peekKeyword("capability")) {
|
|
452
|
+
const capability = this.parseCapability();
|
|
453
|
+
const next = this.peek();
|
|
454
|
+
if (next.kind === "operator") {
|
|
455
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", next.position);
|
|
456
|
+
}
|
|
457
|
+
return capability;
|
|
458
|
+
}
|
|
459
|
+
return this.parseComparison();
|
|
460
|
+
}
|
|
461
|
+
parseCapability() {
|
|
462
|
+
this.advance();
|
|
463
|
+
const key = this.parseCallArgument("capability");
|
|
464
|
+
if (!CAPABILITY_KEY_PATTERN.test(key)) {
|
|
465
|
+
throw new PredicateParseError(`invalid capability key '${key}'`, this.peek().position);
|
|
466
|
+
}
|
|
467
|
+
return { kind: "capability", key };
|
|
468
|
+
}
|
|
469
|
+
parseComparison() {
|
|
470
|
+
const left = this.parseOperand();
|
|
471
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
472
|
+
if (!COMPARE_OPS.includes(operator.value)) {
|
|
473
|
+
throw new PredicateParseError(`unsupported operator '${operator.value}'`, operator.position);
|
|
474
|
+
}
|
|
475
|
+
const right = this.parseOperand();
|
|
476
|
+
return { kind: "compare", op: operator.value, left, right };
|
|
477
|
+
}
|
|
478
|
+
parseOperand() {
|
|
479
|
+
const token = this.peek();
|
|
480
|
+
if (token.kind === "string") {
|
|
481
|
+
this.advance();
|
|
482
|
+
return { kind: "literal", value: token.value };
|
|
483
|
+
}
|
|
484
|
+
if (token.kind === "number") {
|
|
485
|
+
this.advance();
|
|
486
|
+
const value = Number(token.value);
|
|
487
|
+
if (!Number.isFinite(value) || Number.isInteger(value) && !Number.isSafeInteger(value)) {
|
|
488
|
+
throw new PredicateParseError(
|
|
489
|
+
`numeric literal '${token.value}' exceeds safe integer precision \u2014 quote it as a string`,
|
|
490
|
+
token.position
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
return { kind: "literal", value };
|
|
494
|
+
}
|
|
495
|
+
if (token.kind !== "identifier") {
|
|
496
|
+
throw new PredicateParseError(`expected an operand, got '${token.value || "end"}'`, token.position);
|
|
497
|
+
}
|
|
498
|
+
this.advance();
|
|
499
|
+
if (token.value === "true" || token.value === "false") {
|
|
500
|
+
return { kind: "literal", value: token.value === "true" };
|
|
501
|
+
}
|
|
502
|
+
if (token.value === "null") return { kind: "literal", value: null };
|
|
503
|
+
if (token.value === "claim") {
|
|
504
|
+
const key = this.parseCallArgument("claim");
|
|
505
|
+
if (!CLAIM_KEY_PATTERN.test(key)) {
|
|
506
|
+
throw new PredicateParseError(`invalid claim key '${key}'`, token.position);
|
|
507
|
+
}
|
|
508
|
+
return { kind: "claim", key };
|
|
509
|
+
}
|
|
510
|
+
if (token.value === "capability") {
|
|
511
|
+
throw new PredicateParseError("capability() is boolean-valued and cannot appear in a comparison", token.position);
|
|
512
|
+
}
|
|
513
|
+
if (KEYWORDS.has(token.value) || !IDENTIFIER_PATTERN.test(token.value)) {
|
|
514
|
+
throw new PredicateParseError(`invalid column reference '${token.value}'`, token.position);
|
|
515
|
+
}
|
|
516
|
+
return { kind: "column", name: token.value };
|
|
517
|
+
}
|
|
518
|
+
parseCallArgument(fn) {
|
|
519
|
+
this.expect("lparen", `'(' after ${fn}`);
|
|
520
|
+
const argument = this.expect("string", `a quoted key in ${fn}(...)`);
|
|
521
|
+
this.expect("rparen", `')' after ${fn} key`);
|
|
522
|
+
return argument.value;
|
|
523
|
+
}
|
|
524
|
+
peek() {
|
|
525
|
+
return this.tokens[this.index];
|
|
526
|
+
}
|
|
527
|
+
peekKeyword(keyword) {
|
|
528
|
+
const token = this.peek();
|
|
529
|
+
return token.kind === "identifier" && token.value === keyword;
|
|
530
|
+
}
|
|
531
|
+
matchKeyword(keyword) {
|
|
532
|
+
if (!this.peekKeyword(keyword)) return false;
|
|
533
|
+
this.advance();
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
advance() {
|
|
537
|
+
const token = this.peek();
|
|
538
|
+
if (token.kind !== "end") this.index += 1;
|
|
539
|
+
return token;
|
|
540
|
+
}
|
|
541
|
+
expect(kind, description) {
|
|
542
|
+
const token = this.peek();
|
|
543
|
+
if (token.kind !== kind) {
|
|
544
|
+
throw new PredicateParseError(`expected ${description}, got '${token.value || "end"}'`, token.position);
|
|
545
|
+
}
|
|
546
|
+
return this.advance();
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
function parsePredicate(source) {
|
|
550
|
+
const trimmed = source.trim();
|
|
551
|
+
if (trimmed.length === 0) throw new PredicateParseError("empty predicate", 0);
|
|
552
|
+
return { source: trimmed, ast: new Parser2(tokenize2(trimmed)).parse() };
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// src/schema/predicates.ts
|
|
556
|
+
var parsedPredicateSchema = z.string().transform((source, ctx) => {
|
|
557
|
+
try {
|
|
558
|
+
return parsePredicate(source);
|
|
559
|
+
} catch (error) {
|
|
560
|
+
ctx.addIssue({ code: "custom", message: error instanceof Error ? error.message : String(error) });
|
|
561
|
+
return z.NEVER;
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// src/schema/actions.ts
|
|
566
|
+
var EXECUTES_VERB_PATTERN = /^\s*(insert|update|delete)\b/i;
|
|
567
|
+
var DOLLAR_QUOTE_PATTERN = /\$[a-zA-Z0-9_]*\$/;
|
|
568
|
+
var RETURNING_PATTERN = /\breturning\b/i;
|
|
569
|
+
var ESCAPE_STRING_PATTERN = /(?<![a-zA-Z0-9_])[Ee]'/;
|
|
570
|
+
var MASKED_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\(\s*''\s*\)/g;
|
|
571
|
+
var BARE_CLAIM_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
572
|
+
var EXISTS_ENTRY_PATTERN = /^exists\(\s*'([^']+)'\s*,\s*([a-z][a-z0-9_]*)\s*\)$/;
|
|
573
|
+
var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
|
|
574
|
+
var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
|
|
575
|
+
var SQL_COMMENT_PATTERN = /--|\/\*/;
|
|
576
|
+
var actionParamSchema = z.strictObject({
|
|
577
|
+
type: columnTypeSchema,
|
|
578
|
+
optional: z.boolean().optional(),
|
|
579
|
+
description: z.string().optional()
|
|
580
|
+
});
|
|
581
|
+
var validatesEntrySchema = z.string().transform((source, ctx) => {
|
|
582
|
+
const match = EXISTS_ENTRY_PATTERN.exec(source.trim());
|
|
583
|
+
if (!match) {
|
|
584
|
+
ctx.addIssue({ code: "custom", message: "validates entries must have the shape exists('<table>', <param>)" });
|
|
585
|
+
return z.NEVER;
|
|
586
|
+
}
|
|
587
|
+
const table = tableReferenceSchema.safeParse(match[1]);
|
|
588
|
+
if (!table.success) {
|
|
589
|
+
ctx.addIssue({ code: "custom", message: `invalid table reference '${match[1]}'` });
|
|
590
|
+
return z.NEVER;
|
|
591
|
+
}
|
|
592
|
+
return { source: source.trim(), table: table.data, param: match[2] };
|
|
593
|
+
});
|
|
594
|
+
var AUDIT_ACTIONS = ["create", "update", "delete"];
|
|
595
|
+
var auditSchema = z.strictObject({
|
|
596
|
+
action: z.enum(AUDIT_ACTIONS),
|
|
597
|
+
resource: identifierSchema.optional()
|
|
598
|
+
});
|
|
599
|
+
var actionSchema = z.strictObject({
|
|
600
|
+
description: z.string().optional(),
|
|
601
|
+
requires: parsedPredicateSchema.optional(),
|
|
602
|
+
params: z.record(identifierSchema, actionParamSchema).optional(),
|
|
603
|
+
validates: z.array(validatesEntrySchema).optional(),
|
|
604
|
+
// One statement, or an ordered list run atomically in the action function.
|
|
605
|
+
executes: z.union([z.string().min(1), z.array(z.string().min(1)).min(1)]),
|
|
606
|
+
// Declares the columns the action's RETURNING yields. Present → the
|
|
607
|
+
// function returns those rows (typed in the client); absent → the action
|
|
608
|
+
// reports rows_affected only. The column names/types must match the
|
|
609
|
+
// RETURNING list, which Postgres enforces at CREATE FUNCTION.
|
|
610
|
+
returns: z.record(identifierSchema, columnTypeSchema).optional(),
|
|
611
|
+
audit: auditSchema.optional()
|
|
612
|
+
}).superRefine((action, ctx) => {
|
|
613
|
+
const params = Object.keys(action.params ?? {});
|
|
614
|
+
if (action.requires && referencedColumns(action.requires.ast).length > 0) {
|
|
615
|
+
ctx.addIssue({
|
|
616
|
+
code: "custom",
|
|
617
|
+
message: "requires may reference claims and capabilities only, not columns",
|
|
618
|
+
path: ["requires"]
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
for (const issue of executesIssues(action.executes, new Set(params), action.returns !== void 0)) {
|
|
622
|
+
ctx.addIssue({ code: "custom", message: issue, path: ["executes"] });
|
|
623
|
+
}
|
|
624
|
+
if (action.returns && Object.keys(action.returns).length === 0) {
|
|
625
|
+
ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
|
|
626
|
+
}
|
|
627
|
+
for (const [index, entry] of (action.validates ?? []).entries()) {
|
|
628
|
+
if (!params.includes(entry.param)) {
|
|
629
|
+
ctx.addIssue({
|
|
630
|
+
code: "custom",
|
|
631
|
+
message: `validates references undeclared param '${entry.param}'`,
|
|
632
|
+
path: ["validates", index]
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
function extractNamedParams(sql) {
|
|
638
|
+
const withoutStrings = sql.replace(QUOTED_STRING_PATTERN, "''");
|
|
639
|
+
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
640
|
+
return [...new Set(names)];
|
|
641
|
+
}
|
|
642
|
+
function actionStatementSources(executes) {
|
|
643
|
+
return (Array.isArray(executes) ? executes : [executes]).map((statement) => statement.trim());
|
|
644
|
+
}
|
|
645
|
+
function executesIssues(executes, declaredParams, hasReturns) {
|
|
646
|
+
const issues = [];
|
|
647
|
+
const statements = actionStatementSources(executes);
|
|
648
|
+
statements.forEach((statement, index) => {
|
|
649
|
+
const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
|
|
650
|
+
const at = statements.length > 1 ? ` (statement ${index + 1})` : "";
|
|
651
|
+
if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
|
|
652
|
+
if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
|
|
653
|
+
if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
|
|
654
|
+
if (DOLLAR_QUOTE_PATTERN.test(withoutStrings)) issues.push(`executes must not use dollar-quoted strings${at}`);
|
|
655
|
+
if (ESCAPE_STRING_PATTERN.test(withoutStrings)) issues.push(`executes must not use escape strings (E'\u2026')${at}`);
|
|
656
|
+
if (withoutStrings.includes('"')) issues.push(`executes must not use double-quoted identifiers${at}`);
|
|
657
|
+
if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
|
|
658
|
+
issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
|
|
659
|
+
}
|
|
660
|
+
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index === statements.length - 1)) {
|
|
661
|
+
issues.push(
|
|
662
|
+
hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
const last = statements.at(-1);
|
|
667
|
+
if (hasReturns && last && !RETURNING_PATTERN.test(last.replace(QUOTED_STRING_PATTERN, "''"))) {
|
|
668
|
+
issues.push("the last executes statement must use RETURNING to populate the declared returns columns");
|
|
669
|
+
}
|
|
670
|
+
const undeclared = extractNamedParams(statements.join("\n")).filter((name) => !declaredParams.has(name));
|
|
671
|
+
if (undeclared.length > 0) issues.push(`executes references undeclared params: ${undeclared.join(", ")}`);
|
|
672
|
+
return issues;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// src/schema/table.ts
|
|
676
|
+
var linkSchema = z.strictObject({
|
|
677
|
+
to: tableReferenceSchema,
|
|
678
|
+
on: identifierSchema,
|
|
679
|
+
enforce: z.boolean().optional(),
|
|
680
|
+
description: z.string().optional()
|
|
681
|
+
});
|
|
682
|
+
var indexSchema = z.union([
|
|
683
|
+
z.array(identifierSchema).min(1),
|
|
684
|
+
z.strictObject({
|
|
685
|
+
columns: z.array(identifierSchema).min(1),
|
|
686
|
+
unique: z.boolean().optional(),
|
|
687
|
+
name: identifierSchema.optional()
|
|
688
|
+
})
|
|
689
|
+
]).transform((index) => Array.isArray(index) ? { columns: index } : index);
|
|
690
|
+
var policiesSchema = z.strictObject({
|
|
691
|
+
read: parsedPredicateSchema.optional(),
|
|
692
|
+
write: parsedPredicateSchema.optional(),
|
|
693
|
+
insert: parsedPredicateSchema.optional(),
|
|
694
|
+
update: parsedPredicateSchema.optional()
|
|
695
|
+
});
|
|
696
|
+
z.strictObject({
|
|
697
|
+
namespace: namespaceSchema,
|
|
698
|
+
table: identifierSchema,
|
|
699
|
+
description: z.string().optional(),
|
|
700
|
+
columns: z.record(identifierSchema, columnSchema).optional(),
|
|
701
|
+
source: z.string().regex(SYNCED_SOURCE_PATTERN, "source must be a qualified lowercase schema.table identifier").optional(),
|
|
702
|
+
links: z.record(identifierSchema, linkSchema).optional(),
|
|
703
|
+
indexes: z.array(indexSchema).optional(),
|
|
704
|
+
policies: policiesSchema.optional(),
|
|
705
|
+
actions: z.record(identifierSchema, actionSchema).optional()
|
|
706
|
+
}).superRefine((table, ctx) => {
|
|
707
|
+
if (RESERVED_TABLE_NAMES.has(table.table)) {
|
|
708
|
+
ctx.addIssue({ code: "custom", message: `'${table.table}' is a reserved table name`, path: ["table"] });
|
|
709
|
+
}
|
|
710
|
+
for (const name of Object.keys(table.columns ?? {})) {
|
|
711
|
+
if (RESERVED_COLUMN_NAMES.has(name)) {
|
|
712
|
+
ctx.addIssue({
|
|
713
|
+
code: "custom",
|
|
714
|
+
message: `column '${name}' collides with the Data API's reserved query parameters (select, order, limit, offset) \u2014 it could never be filtered`,
|
|
715
|
+
path: ["columns", name]
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (table.source) validateSyncedTable(table, ctx);
|
|
720
|
+
else validateColumnTable(table, ctx);
|
|
721
|
+
validateLinks(table, ctx);
|
|
722
|
+
validatePolicyColumns(table, ctx);
|
|
723
|
+
validateDerivedNameLengths(table, ctx);
|
|
724
|
+
});
|
|
725
|
+
var indexName = (table, definition) => definition.name ?? `ix_${table}_${definition.columns.join("_")}`;
|
|
726
|
+
var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
727
|
+
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
728
|
+
function validateSyncedTable(table, ctx) {
|
|
729
|
+
const forbidden = [
|
|
730
|
+
["actions", "synced tables are read-only; actions are not allowed"],
|
|
731
|
+
["indexes", "v1 synced tables cannot declare indexes; declare them with defineSyncedTable({ indexes }) in a v2 definition"]
|
|
732
|
+
];
|
|
733
|
+
for (const [field, message] of forbidden) {
|
|
734
|
+
if (table[field] !== void 0) ctx.addIssue({ code: "custom", message, path: [field] });
|
|
735
|
+
}
|
|
736
|
+
if (table.policies?.write) {
|
|
737
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
738
|
+
}
|
|
739
|
+
if (table.policies?.insert) {
|
|
740
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
741
|
+
}
|
|
742
|
+
if (table.policies?.update) {
|
|
743
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
744
|
+
}
|
|
745
|
+
const columns = table.columns ?? {};
|
|
746
|
+
const names = Object.keys(columns);
|
|
747
|
+
if (names.length === 0) return;
|
|
748
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
749
|
+
if (primaryKeys.length !== 1) {
|
|
750
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
751
|
+
}
|
|
752
|
+
validateRenames(columns, names, ctx);
|
|
753
|
+
for (const name of names) {
|
|
754
|
+
const column = columns[name];
|
|
755
|
+
if (!column) continue;
|
|
756
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
757
|
+
if (column.cast && cast?.column !== name) {
|
|
758
|
+
ctx.addIssue({
|
|
759
|
+
code: "custom",
|
|
760
|
+
message: "cast must reference the column it is declared on",
|
|
761
|
+
path: ["columns", name, "cast"]
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
if (cast && cast.type !== column.type) {
|
|
765
|
+
ctx.addIssue({
|
|
766
|
+
code: "custom",
|
|
767
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
768
|
+
path: ["columns", name, "cast"]
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
function validateRenames(columns, names, ctx) {
|
|
774
|
+
const renameSources = /* @__PURE__ */ new Map();
|
|
775
|
+
for (const name of names) {
|
|
776
|
+
const renamedFrom = columns[name]?.renamed_from;
|
|
777
|
+
if (!renamedFrom) continue;
|
|
778
|
+
if (renamedFrom === name || names.includes(renamedFrom)) {
|
|
779
|
+
ctx.addIssue({
|
|
780
|
+
code: "custom",
|
|
781
|
+
message: "renamed_from must name a column that no longer exists",
|
|
782
|
+
path: ["columns", name, "renamed_from"]
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
const claimed = renameSources.get(renamedFrom);
|
|
786
|
+
if (claimed) {
|
|
787
|
+
ctx.addIssue({
|
|
788
|
+
code: "custom",
|
|
789
|
+
message: `renamed_from '${renamedFrom}' is already claimed by '${claimed}'`,
|
|
790
|
+
path: ["columns", name, "renamed_from"]
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
renameSources.set(renamedFrom, name);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
function validateColumnTable(table, ctx) {
|
|
797
|
+
const columns = table.columns ?? {};
|
|
798
|
+
const names = Object.keys(columns);
|
|
799
|
+
if (names.length === 0) {
|
|
800
|
+
ctx.addIssue({ code: "custom", message: "a table must declare columns or a source", path: ["columns"] });
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
804
|
+
if (primaryKeys.length !== 1) {
|
|
805
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
806
|
+
}
|
|
807
|
+
validateRenames(columns, names, ctx);
|
|
808
|
+
for (const name of names) {
|
|
809
|
+
const column = columns[name];
|
|
810
|
+
if (!column) continue;
|
|
811
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
812
|
+
if (column.cast && cast?.column !== name) {
|
|
813
|
+
ctx.addIssue({
|
|
814
|
+
code: "custom",
|
|
815
|
+
message: "cast must reference the column it is declared on",
|
|
816
|
+
path: ["columns", name, "cast"]
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
if (cast && cast.type !== column.type) {
|
|
820
|
+
ctx.addIssue({
|
|
821
|
+
code: "custom",
|
|
822
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
823
|
+
path: ["columns", name, "cast"]
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
828
|
+
if (new Set(index.columns).size !== index.columns.length) {
|
|
829
|
+
ctx.addIssue({
|
|
830
|
+
code: "custom",
|
|
831
|
+
message: "index names a column more than once",
|
|
832
|
+
path: ["indexes", position]
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
for (const column of index.columns) {
|
|
836
|
+
if (!names.includes(column)) {
|
|
837
|
+
ctx.addIssue({
|
|
838
|
+
code: "custom",
|
|
839
|
+
message: `index references unknown column '${column}'`,
|
|
840
|
+
path: ["indexes", position]
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
function validateLinks(table, ctx) {
|
|
847
|
+
const columnNames = Object.keys(table.columns ?? {});
|
|
848
|
+
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
849
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
850
|
+
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
|
|
851
|
+
}
|
|
852
|
+
if (link.enforce && link.to.includes(".")) {
|
|
853
|
+
ctx.addIssue({
|
|
854
|
+
code: "custom",
|
|
855
|
+
message: "enforce is only valid for links inside the namespace; cross-boundary links stay soft",
|
|
856
|
+
path: ["links", name, "enforce"]
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
if (link.enforce && table.source) {
|
|
860
|
+
ctx.addIssue({
|
|
861
|
+
code: "custom",
|
|
862
|
+
message: "a synced table cannot enforce foreign keys \u2014 its DDL is owned by the sync",
|
|
863
|
+
path: ["links", name, "enforce"]
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
function validateDerivedNameLengths(table, ctx) {
|
|
869
|
+
const worstPolicy = policyName(table.table, "insert");
|
|
870
|
+
if (!table.source && worstPolicy.length > MAX_IDENTIFIER_LENGTH) {
|
|
871
|
+
ctx.addIssue({
|
|
872
|
+
code: "custom",
|
|
873
|
+
message: `table name is too long \u2014 the derived policy name '${worstPolicy}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit`,
|
|
874
|
+
path: ["table"]
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
for (const [position, index] of (table.indexes ?? []).entries()) {
|
|
878
|
+
const name = indexName(table.table, index);
|
|
879
|
+
if (name.length > MAX_IDENTIFIER_LENGTH) {
|
|
880
|
+
ctx.addIssue({
|
|
881
|
+
code: "custom",
|
|
882
|
+
message: `derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`,
|
|
883
|
+
path: ["indexes", position]
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
888
|
+
const constraint = foreignKeyName(table.table, name);
|
|
889
|
+
if (link.enforce && constraint.length > MAX_IDENTIFIER_LENGTH) {
|
|
890
|
+
ctx.addIssue({
|
|
891
|
+
code: "custom",
|
|
892
|
+
message: `derived foreign-key name '${constraint}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the link or table name`,
|
|
893
|
+
path: ["links", name]
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
for (const name of Object.keys(table.actions ?? {})) {
|
|
898
|
+
const fnName = `action_${name}`;
|
|
899
|
+
if (fnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
900
|
+
ctx.addIssue({
|
|
901
|
+
code: "custom",
|
|
902
|
+
message: `derived action function name '${fnName}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 shorten the action name`,
|
|
903
|
+
path: ["actions", name]
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
var TYPE_GROUPS = {
|
|
909
|
+
integer: "numeric",
|
|
910
|
+
bigint: "numeric",
|
|
911
|
+
numeric: "numeric",
|
|
912
|
+
double: "numeric",
|
|
913
|
+
text: "text",
|
|
914
|
+
uuid: "uuid",
|
|
915
|
+
date: "temporal",
|
|
916
|
+
timestamptz: "temporal",
|
|
917
|
+
boolean: "boolean",
|
|
918
|
+
jsonb: "jsonb"
|
|
919
|
+
};
|
|
920
|
+
function validatePolicyColumns(table, ctx) {
|
|
921
|
+
const columns = table.columns ?? {};
|
|
922
|
+
const columnNames = Object.keys(columns);
|
|
923
|
+
if (columnNames.length === 0) return;
|
|
924
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
925
|
+
const predicate = table.policies?.[kind];
|
|
926
|
+
if (!predicate) continue;
|
|
927
|
+
for (const column of referencedColumns(predicate.ast)) {
|
|
928
|
+
if (!columnNames.includes(column)) {
|
|
929
|
+
ctx.addIssue({
|
|
930
|
+
code: "custom",
|
|
931
|
+
message: `policy references unknown column '${column}'`,
|
|
932
|
+
path: ["policies", kind]
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
walkPredicate(predicate.ast, (node) => {
|
|
937
|
+
if (node.kind !== "compare") return;
|
|
938
|
+
const issue = comparisonTypeIssue(node.left, node.right, columns);
|
|
939
|
+
if (issue) ctx.addIssue({ code: "custom", message: issue, path: ["policies", kind] });
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
function comparisonTypeIssue(left, right, columns) {
|
|
944
|
+
const groupOf = (operand) => operand.kind === "column" ? columns[operand.name] ? TYPE_GROUPS[columns[operand.name].type] : null : null;
|
|
945
|
+
if (left.kind === "column" && right.kind === "column") {
|
|
946
|
+
const leftGroup = groupOf(left);
|
|
947
|
+
const rightGroup = groupOf(right);
|
|
948
|
+
if (leftGroup && rightGroup && leftGroup !== rightGroup) {
|
|
949
|
+
return `policy compares '${left.name}' (${columns[left.name].type}) with '${right.name}' (${columns[right.name].type}) \u2014 incomparable types`;
|
|
950
|
+
}
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
const column = left.kind === "column" ? left : right.kind === "column" ? right : null;
|
|
954
|
+
const literal = left.kind === "literal" ? left : right.kind === "literal" ? right : null;
|
|
955
|
+
if (!column || !literal || literal.value === null) return null;
|
|
956
|
+
const columnGroup = groupOf(column);
|
|
957
|
+
if (!columnGroup) return null;
|
|
958
|
+
if (typeof literal.value === "string") return null;
|
|
959
|
+
const literalGroup = typeof literal.value === "number" ? "numeric" : "boolean";
|
|
960
|
+
if (literalGroup !== columnGroup) {
|
|
961
|
+
return `policy compares '${column.name}' (${columns[column.name].type}) with a ${typeof literal.value} literal \u2014 incomparable types`;
|
|
962
|
+
}
|
|
963
|
+
return null;
|
|
964
|
+
}
|
|
224
965
|
var policyVariantSchema = z.object({
|
|
225
966
|
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
226
967
|
when: z.string().min(1).max(1e3),
|
|
@@ -529,7 +1270,7 @@ var CANONICAL_FORMAT = 2;
|
|
|
529
1270
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
530
1271
|
var identifier2 = z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
531
1272
|
var columnIdentifier = z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
532
|
-
var
|
|
1273
|
+
var COLUMN_TYPES2 = [
|
|
533
1274
|
"uuid",
|
|
534
1275
|
"text",
|
|
535
1276
|
"integer",
|
|
@@ -541,7 +1282,7 @@ var COLUMN_TYPES = [
|
|
|
541
1282
|
"date",
|
|
542
1283
|
"jsonb"
|
|
543
1284
|
];
|
|
544
|
-
var
|
|
1285
|
+
var columnTypeSchema2 = z.enum(COLUMN_TYPES2);
|
|
545
1286
|
var canonicalEnumSchema = z.object({
|
|
546
1287
|
name: identifier2,
|
|
547
1288
|
values: z.array(z.string().min(1).max(63)).min(1).max(100)
|
|
@@ -550,7 +1291,7 @@ var canonicalColumnSchema = z.object({
|
|
|
550
1291
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
551
1292
|
name: z.string().min(1).max(63),
|
|
552
1293
|
columnName: columnIdentifier,
|
|
553
|
-
type: z.union([
|
|
1294
|
+
type: z.union([columnTypeSchema2, z.object({ enum: identifier2 })]),
|
|
554
1295
|
nullable: z.boolean().default(true),
|
|
555
1296
|
unique: z.boolean().default(false),
|
|
556
1297
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -643,7 +1384,7 @@ var canonicalActionSchema = z.object({
|
|
|
643
1384
|
name: identifier2,
|
|
644
1385
|
requires: z.string().max(1e3).optional(),
|
|
645
1386
|
params: z.record(z.string(), z.object({
|
|
646
|
-
type:
|
|
1387
|
+
type: columnTypeSchema2,
|
|
647
1388
|
nullable: z.boolean().default(false)
|
|
648
1389
|
})).default({}),
|
|
649
1390
|
executes: z.string().min(1).max(1e4),
|
|
@@ -744,7 +1485,8 @@ var canonicalTableSchema = z.object({
|
|
|
744
1485
|
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
745
1486
|
fail("is read-only and cannot declare write policies", ["policies"]);
|
|
746
1487
|
}
|
|
747
|
-
const
|
|
1488
|
+
const ownedIndexes = table.source === void 0 ? table.indexes : table.indexes.filter((index) => index.unique);
|
|
1489
|
+
const ownedCollections = [ownedIndexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
748
1490
|
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
749
1491
|
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
750
1492
|
fail("cannot declare owned storage, constraints or identity operations");
|
|
@@ -846,6 +1588,23 @@ function trackedCellColumns(table) {
|
|
|
846
1588
|
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
847
1589
|
}
|
|
848
1590
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1591
|
+
function validateSyncedIndexes(table, tableIndex, ctx) {
|
|
1592
|
+
if (table.source === void 0) return;
|
|
1593
|
+
const columns = new Set(table.columns.map((column) => column.columnName));
|
|
1594
|
+
for (const [position, index] of table.indexes.entries()) {
|
|
1595
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "indexes", position], message: `synced table '${table.table}' ${message}` });
|
|
1596
|
+
if (columns.size === 0) fail("must declare columns to declare indexes");
|
|
1597
|
+
if (index.unique) fail("cannot declare a unique index");
|
|
1598
|
+
if (new Set(index.columns).size !== index.columns.length) fail("index names a column more than once");
|
|
1599
|
+
for (const column of index.columns) {
|
|
1600
|
+
if (columns.size > 0 && !columns.has(column)) fail(`index references unknown column '${column}'`);
|
|
1601
|
+
}
|
|
1602
|
+
const name = indexName(table.table, index);
|
|
1603
|
+
if (name.length > MAX_IDENTIFIER_LENGTH) {
|
|
1604
|
+
fail(`derived index name '${name}' exceeds Postgres's ${MAX_IDENTIFIER_LENGTH}-byte identifier limit \u2014 set a shorter explicit name`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
849
1608
|
function validateCellMetadata(table, index, ctx) {
|
|
850
1609
|
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
851
1610
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
@@ -912,12 +1671,24 @@ var localDefinitionSchema = z.object({
|
|
|
912
1671
|
}).default({ uses: [] })
|
|
913
1672
|
}).superRefine((definition, ctx) => {
|
|
914
1673
|
const seen = /* @__PURE__ */ new Set();
|
|
1674
|
+
const syncedIndexOwners = /* @__PURE__ */ new Map();
|
|
915
1675
|
for (const [index, table] of definition.tables.entries()) {
|
|
916
1676
|
if (seen.has(table.table)) {
|
|
917
1677
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
918
1678
|
}
|
|
919
1679
|
seen.add(table.table);
|
|
920
1680
|
validateCellMetadata(table, index, ctx);
|
|
1681
|
+
validateSyncedIndexes(table, index, ctx);
|
|
1682
|
+
if (table.source !== void 0) {
|
|
1683
|
+
for (const [position, entry] of table.indexes.entries()) {
|
|
1684
|
+
const name = indexName(table.table, entry);
|
|
1685
|
+
const owner = syncedIndexOwners.get(name);
|
|
1686
|
+
if (owner !== void 0) {
|
|
1687
|
+
ctx.addIssue({ code: "custom", path: ["tables", index, "indexes", position], message: `synced index '${name}' is also declared on '${owner}'` });
|
|
1688
|
+
}
|
|
1689
|
+
syncedIndexOwners.set(name, table.table);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
921
1692
|
if (table.source !== void 0) {
|
|
922
1693
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
923
1694
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1116,9 +1887,6 @@ localDefinitionSchema.safeExtend({
|
|
|
1116
1887
|
imports: z.array(localDefinitionSchema).max(32).optional()
|
|
1117
1888
|
});
|
|
1118
1889
|
|
|
1119
|
-
// src/client/sql.ts
|
|
1120
|
-
var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
|
|
1121
|
-
|
|
1122
1890
|
// src/ir/index.ts
|
|
1123
1891
|
var IR_VERSION = 6;
|
|
1124
1892
|
var LEGACY_IR_VERSION = 1;
|
|
@@ -1409,7 +2177,7 @@ var variableDefaultIrSchema = z.object({
|
|
|
1409
2177
|
var variableIrSchema = z.object({
|
|
1410
2178
|
name: z.string(),
|
|
1411
2179
|
type: z.union([
|
|
1412
|
-
z.object({ kind: z.literal("scalar"), scalar: z.enum(
|
|
2180
|
+
z.object({ kind: z.literal("scalar"), scalar: z.enum(COLUMN_TYPES2) }),
|
|
1413
2181
|
/** `values` is the declared membership whitelist — the executor validates against it. */
|
|
1414
2182
|
z.object({ kind: z.literal("enum"), name: z.string(), values: z.array(z.string()).min(1) }),
|
|
1415
2183
|
/** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
|
|
@@ -1447,8 +2215,7 @@ var compiledOperationSchema = z.object({
|
|
|
1447
2215
|
maxRows: z.number().int().min(1).max(1e4)
|
|
1448
2216
|
}).strict().optional(),
|
|
1449
2217
|
irVersion: irVersionSchema,
|
|
1450
|
-
name: z.string(),
|
|
1451
|
-
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
2218
|
+
name: z.string().regex(/^[_A-Za-z][_0-9A-Za-z]{0,254}$/),
|
|
1452
2219
|
kind: z.enum(["query", "mutation"]),
|
|
1453
2220
|
requires: z.object({
|
|
1454
2221
|
capability: z.string().optional(),
|
|
@@ -1464,7 +2231,7 @@ var compiledOperationSchema = z.object({
|
|
|
1464
2231
|
}
|
|
1465
2232
|
if (operation.language === "sql") {
|
|
1466
2233
|
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1467
|
-
const builtin = operation.name === "BuiltinSqlRead" && operation.
|
|
2234
|
+
const builtin = operation.name === "BuiltinSqlRead" && operation.requires === null;
|
|
1468
2235
|
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !builtin && !operation.requires?.capability) {
|
|
1469
2236
|
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1470
2237
|
}
|
|
@@ -1479,7 +2246,7 @@ var compiledOperationSchema = z.object({
|
|
|
1479
2246
|
}
|
|
1480
2247
|
});
|
|
1481
2248
|
var resolvedSourceIrSchema = z.object({
|
|
1482
|
-
name: z.string(),
|
|
2249
|
+
name: z.string().regex(/^[_A-Za-z][_0-9A-Za-z]{0,254}$/),
|
|
1483
2250
|
kind: z.enum(["manual", "etl", "prefetch"]),
|
|
1484
2251
|
/** Logical table key of the backing relation. */
|
|
1485
2252
|
table: z.string(),
|
|
@@ -1542,7 +2309,7 @@ var tableMetaIrSchema = z.object({
|
|
|
1542
2309
|
columns: z.array(z.object({
|
|
1543
2310
|
name: z.string(),
|
|
1544
2311
|
columnName: z.string(),
|
|
1545
|
-
type: z.union([z.enum(
|
|
2312
|
+
type: z.union([z.enum(COLUMN_TYPES2), z.object({ enum: z.string(), values: z.array(z.string()) })]),
|
|
1546
2313
|
nullable: z.boolean()
|
|
1547
2314
|
})),
|
|
1548
2315
|
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
@@ -1593,7 +2360,6 @@ var compiledArtifactSchema = z.object({
|
|
|
1593
2360
|
ownSchemaHash: z.string().optional(),
|
|
1594
2361
|
dependencies: z.record(z.string(), z.string()).optional(),
|
|
1595
2362
|
readPolicies: z.record(z.string(), predIrSchema).optional(),
|
|
1596
|
-
opsHash: z.string(),
|
|
1597
2363
|
/** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
|
|
1598
2364
|
unbackedTables: z.array(z.string()).optional(),
|
|
1599
2365
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
@@ -1623,12 +2389,12 @@ function normalizeIr(raw) {
|
|
|
1623
2389
|
if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
|
|
1624
2390
|
return compiledArtifactSchema.parse(raw);
|
|
1625
2391
|
}
|
|
1626
|
-
function
|
|
2392
|
+
function indexByName(artifact) {
|
|
1627
2393
|
const map = /* @__PURE__ */ new Map();
|
|
1628
|
-
for (const op of Object.values(artifact.operations)) map.set(op.
|
|
2394
|
+
for (const op of Object.values(artifact.operations)) map.set(op.name, op);
|
|
1629
2395
|
return map;
|
|
1630
2396
|
}
|
|
1631
2397
|
|
|
1632
|
-
export { CELLS_FIELD, CELL_PARTS, CLAIMS_CONTRACT, CMP_OPS, COMPOSED_IR_VERSION, DATE_BUCKETS, EXTERNAL_POLICY_CLAIM_KEYS, GUARDED_IR_VERSION, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, RUNTIME_SQL_IR_VERSION, SERVICE_PRINCIPAL_IR_VERSION, SQL_IR_VERSION, cellColumnName, compiledArtifactSchema, compiledOperationSchema,
|
|
2398
|
+
export { CELLS_FIELD, CELL_PARTS, CLAIMS_CONTRACT, CMP_OPS, COMPOSED_IR_VERSION, DATE_BUCKETS, EXTERNAL_POLICY_CLAIM_KEYS, GUARDED_IR_VERSION, INPUT_REF_KINDS, IR_VERSION, IrVersionError, LEGACY_IR_VERSION, RUNTIME_SQL_IR_VERSION, SERVICE_PRINCIPAL_IR_VERSION, SQL_IR_VERSION, cellColumnName, compiledArtifactSchema, compiledOperationSchema, indexByName, isExternalPolicyClaimKey, limitIrSchema, normalizeIr, orderIrSchema, predIrSchema, prefetchPlanIrSchema, requiredArtifactIrVersion, resolvedMetaIrSchema, resolvedSourceIrSchema, rootIrSchema, selectionIrSchema, sqlObjectIrSchema, tableMetaIrSchema, usesServicePrincipal, valueIrSchema, variableDefaultIrSchema, variableIrSchema };
|
|
1633
2399
|
//# sourceMappingURL=ir.js.map
|
|
1634
2400
|
//# sourceMappingURL=ir.js.map
|