@sequenceholdings/orm 2.1.1 → 2.3.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 +3 -1
- package/dist/authoring.cjs +284 -99
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +31 -8
- package/dist/authoring.d.ts +31 -8
- package/dist/authoring.js +282 -100
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +51 -3
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +838 -12
- package/dist/canonical.d.ts +838 -12
- package/dist/canonical.js +47 -4
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +5876 -4198
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +5876 -4198
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +49 -32
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +49 -6
- package/dist/client.d.ts +49 -6
- package/dist/client.js +49 -32
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1275 -264
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +57 -3
- package/dist/compiler.d.ts +57 -3
- package/dist/compiler.js +1268 -265
- package/dist/compiler.js.map +1 -1
- package/dist/{constants-YoFGjIBM.d.cts → constants-C6EG6Wq7.d.cts} +1 -1
- package/dist/{constants-YoFGjIBM.d.ts → constants-C6EG6Wq7.d.ts} +1 -1
- package/dist/definition-DjfC7Lll.d.cts +91 -0
- package/dist/definition-DjfC7Lll.d.ts +91 -0
- package/dist/dynamic-input.cjs +2 -1
- package/dist/dynamic-input.cjs.map +1 -1
- package/dist/dynamic-input.d.cts +9 -2
- package/dist/dynamic-input.d.ts +9 -2
- package/dist/dynamic-input.js +2 -1
- package/dist/dynamic-input.js.map +1 -1
- package/dist/index.cjs +1103 -322
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +479 -19
- package/dist/index.d.ts +479 -19
- package/dist/index.js +1090 -323
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +93 -11
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +226 -15
- package/dist/ir.d.ts +226 -15
- package/dist/ir.js +88 -12
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +116 -10
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +11 -3
- package/dist/predicate-v2.d.ts +11 -3
- package/dist/predicate-v2.js +116 -10
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +158 -22
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +8 -1
- package/dist/sdl.d.ts +8 -1
- package/dist/sdl.js +158 -22
- package/dist/sdl.js.map +1 -1
- package/package.json +5 -4
- package/src/codegen-gql/server-fns.cjs +2 -2
- package/templates/orm-namespace/AGENTS.md +10 -0
- package/templates/orm-namespace/CLAUDE.md +1 -84
- package/templates/orm-namespace/package.json +1 -1
package/dist/compiler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { Kind, print, visit, buildSchema, parse, validate, valueFromASTUntyped } from 'graphql';
|
|
4
|
+
import { PgParser } from '@supabase/pg-parser';
|
|
4
5
|
|
|
5
6
|
// src/compiler-gql/artifact.ts
|
|
6
7
|
|
|
@@ -8,6 +9,7 @@ import { Kind, print, visit, buildSchema, parse, validate, valueFromASTUntyped }
|
|
|
8
9
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
9
10
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
10
11
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
12
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
11
13
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
12
14
|
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
13
15
|
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
@@ -33,8 +35,18 @@ function stableJson(value) {
|
|
|
33
35
|
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
|
|
34
36
|
return `{${entries.join(",")}}`;
|
|
35
37
|
}
|
|
36
|
-
|
|
38
|
+
|
|
39
|
+
// src/compiler/sql.ts
|
|
40
|
+
var CLAIMS_SETTING = "request.jwt.claims";
|
|
41
|
+
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
42
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
43
|
+
var quoteIdentifier = (name2) => `"${name2.replaceAll('"', '""')}"`;
|
|
37
44
|
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
45
|
+
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
46
|
+
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
47
|
+
var scopedClaimsSource = (namespace) => namespace ? `(CASE WHEN ${CLAIMS_SOURCE} ? 'namespaces' THEN ${CLAIMS_SOURCE} -> 'namespaces' -> ${quoteLiteral(namespace)} ELSE ${CLAIMS_SOURCE} END)` : CLAIMS_SOURCE;
|
|
48
|
+
var capabilityExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
49
|
+
var roleExpression = (key, namespace) => `coalesce(${scopedClaimsSource(namespace)} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
38
50
|
|
|
39
51
|
// src/checks/compile.ts
|
|
40
52
|
function tokenize(source) {
|
|
@@ -340,10 +352,10 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
340
352
|
const resolved = view.resolved;
|
|
341
353
|
if (!resolved) return [];
|
|
342
354
|
const errors = [];
|
|
343
|
-
const
|
|
355
|
+
const fail4 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
344
356
|
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
345
357
|
if (!pkColumn) {
|
|
346
|
-
|
|
358
|
+
fail4("requires exactly one primary-key column");
|
|
347
359
|
return errors;
|
|
348
360
|
}
|
|
349
361
|
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
@@ -353,92 +365,92 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
353
365
|
pkColumn,
|
|
354
366
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
355
367
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
356
|
-
fail:
|
|
368
|
+
fail: fail4
|
|
357
369
|
};
|
|
358
|
-
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1)
|
|
370
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail4("requires exactly one manual source");
|
|
359
371
|
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
360
372
|
for (const source of resolved.sources) {
|
|
361
|
-
if (scope.sourceByName.has(source.name))
|
|
373
|
+
if (scope.sourceByName.has(source.name)) fail4(`duplicate source '${source.name}'`);
|
|
362
374
|
scope.sourceByName.set(source.name, source);
|
|
363
375
|
const table = tableByName.get(source.table);
|
|
364
|
-
if (!table)
|
|
376
|
+
if (!table) fail4(`source '${source.name}' references unknown table '${source.table}'`);
|
|
365
377
|
else validateSource({ scope, source, table });
|
|
366
378
|
}
|
|
367
379
|
validateRules(scope);
|
|
368
380
|
validateViewCells(scope);
|
|
369
381
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
370
|
-
for (const
|
|
371
|
-
if (scope.sourceByName.get(
|
|
382
|
+
for (const name2 of resolved.deletedBy) {
|
|
383
|
+
if (scope.sourceByName.get(name2)?.kind !== "manual") fail4(`deletedBy '${name2}' must be the manual source`);
|
|
372
384
|
}
|
|
373
385
|
return errors;
|
|
374
386
|
}
|
|
375
387
|
function validateSource({ scope, source, table }) {
|
|
376
|
-
const { fail:
|
|
377
|
-
if (source.kind === "etl" && table.source === void 0)
|
|
388
|
+
const { fail: fail4 } = scope;
|
|
389
|
+
if (source.kind === "etl" && table.source === void 0) fail4(`etl source '${source.name}' must reference a synced table`);
|
|
378
390
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
379
|
-
|
|
391
|
+
fail4(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
380
392
|
}
|
|
381
393
|
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
382
|
-
if (source.kind === "manual" && !hasRev)
|
|
383
|
-
if (source.kind === "prefetch" && hasRev)
|
|
394
|
+
if (source.kind === "manual" && !hasRev) fail4(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
395
|
+
if (source.kind === "prefetch" && hasRev) fail4(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
384
396
|
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
385
397
|
validateSourceColumns({ scope, source, byName });
|
|
386
398
|
if (source.kind === "etl") {
|
|
387
399
|
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
388
400
|
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
389
|
-
|
|
401
|
+
fail4(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
390
402
|
}
|
|
391
403
|
}
|
|
392
404
|
validatePrefetchFields({ scope, source });
|
|
393
405
|
}
|
|
394
406
|
function validatePrefetchFields({ scope, source }) {
|
|
395
|
-
const { fail:
|
|
407
|
+
const { fail: fail4, pkColumn } = scope;
|
|
396
408
|
if (source.kind !== "prefetch") {
|
|
397
409
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
398
|
-
|
|
410
|
+
fail4(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
399
411
|
}
|
|
400
412
|
return;
|
|
401
413
|
}
|
|
402
414
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
403
|
-
|
|
415
|
+
fail4(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
404
416
|
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
405
|
-
|
|
417
|
+
fail4(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
406
418
|
}
|
|
407
419
|
for (const column of source.writeback?.columns ?? []) {
|
|
408
|
-
if (!source.columns.includes(column))
|
|
420
|
+
if (!source.columns.includes(column)) fail4(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
409
421
|
}
|
|
410
422
|
}
|
|
411
423
|
function validateSourceColumns({ scope, source, byName }) {
|
|
412
|
-
const { fail:
|
|
424
|
+
const { fail: fail4, pkColumn, dataColumns } = scope;
|
|
413
425
|
for (const column of [pkColumn.name, ...source.columns]) {
|
|
414
426
|
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
415
427
|
const theirs = byName.get(column);
|
|
416
|
-
if (!own)
|
|
417
|
-
else if (!theirs)
|
|
418
|
-
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type))
|
|
428
|
+
if (!own) fail4(`source '${source.name}' supplies unknown column '${column}'`);
|
|
429
|
+
else if (!theirs) fail4(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
430
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail4(`column '${column}' has a different type on source '${source.name}'`);
|
|
419
431
|
}
|
|
420
432
|
}
|
|
421
|
-
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail:
|
|
433
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail4 }) {
|
|
422
434
|
const ruled = /* @__PURE__ */ new Set();
|
|
423
435
|
for (const rule of resolved.resolve) {
|
|
424
436
|
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
425
|
-
|
|
437
|
+
fail4(`resolve rule targets unknown column '${rule.column}'`);
|
|
426
438
|
continue;
|
|
427
439
|
}
|
|
428
|
-
if (ruled.has(rule.column))
|
|
440
|
+
if (ruled.has(rule.column)) fail4(`column '${rule.column}' has more than one resolve rule`);
|
|
429
441
|
ruled.add(rule.column);
|
|
430
442
|
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
431
|
-
|
|
443
|
+
fail4(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
432
444
|
}
|
|
433
|
-
if (new Set(rule.candidates).size !== rule.candidates.length)
|
|
445
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail4(`column '${rule.column}' repeats a candidate`);
|
|
434
446
|
for (const candidate of rule.candidates) {
|
|
435
447
|
const source = sourceByName.get(candidate);
|
|
436
|
-
if (!source)
|
|
437
|
-
else if (!source.columns.includes(rule.column))
|
|
448
|
+
if (!source) fail4(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
449
|
+
else if (!source.columns.includes(rule.column)) fail4(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
438
450
|
}
|
|
439
451
|
}
|
|
440
452
|
for (const column of dataColumns.keys()) {
|
|
441
|
-
if (column !== pkColumn.name && !ruled.has(column))
|
|
453
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail4(`column '${column}' has no resolve rule`);
|
|
442
454
|
}
|
|
443
455
|
}
|
|
444
456
|
var VIEW_CELL_TYPES = {
|
|
@@ -447,7 +459,7 @@ var VIEW_CELL_TYPES = {
|
|
|
447
459
|
by: "text",
|
|
448
460
|
conflicts: "jsonb"
|
|
449
461
|
};
|
|
450
|
-
function validateViewCells({ view, dataColumns, pkColumn, fail:
|
|
462
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail4 }) {
|
|
451
463
|
const expected = /* @__PURE__ */ new Map();
|
|
452
464
|
for (const column of dataColumns.values()) {
|
|
453
465
|
if (column.name === pkColumn.name) continue;
|
|
@@ -459,34 +471,34 @@ function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
|
|
|
459
471
|
if (!column.cell) continue;
|
|
460
472
|
const want = expected.get(column.name);
|
|
461
473
|
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
462
|
-
|
|
474
|
+
fail4(`unexpected cell column '${column.name}'`);
|
|
463
475
|
continue;
|
|
464
476
|
}
|
|
465
|
-
if (column.type !== VIEW_CELL_TYPES[column.cell.part])
|
|
477
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail4(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
466
478
|
expected.delete(column.name);
|
|
467
479
|
}
|
|
468
|
-
for (const
|
|
480
|
+
for (const name2 of expected.keys()) fail4(`missing cell column '${name2}'`);
|
|
469
481
|
}
|
|
470
482
|
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
471
483
|
function validateMaterialized({ scope, tableByName, views }) {
|
|
472
|
-
const { view, resolved, fail:
|
|
484
|
+
const { view, resolved, fail: fail4 } = scope;
|
|
473
485
|
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
474
486
|
const table = tableByName.get(tableName);
|
|
475
487
|
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
476
|
-
|
|
488
|
+
fail4(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
477
489
|
} else {
|
|
478
|
-
if (columnSignature(table.columns) !== columnSignature(view.columns))
|
|
479
|
-
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? []))
|
|
490
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail4(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
491
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail4(`materialized table '${tableName}' must share the view's primary key`);
|
|
480
492
|
}
|
|
481
493
|
const compute = views.find((candidate) => candidate.view === computeName);
|
|
482
|
-
if (!compute || compute.internal !== true)
|
|
483
|
-
else if (columnSignature(compute.columns) !== columnSignature(view.columns))
|
|
494
|
+
if (!compute || compute.internal !== true) fail4(`compute view '${computeName}' must be an internal view`);
|
|
495
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail4(`compute view '${computeName}' must have exactly the view's columns`);
|
|
484
496
|
for (const source of resolved.sources) {
|
|
485
497
|
if (source.kind !== "etl") continue;
|
|
486
498
|
const read = tableByName.get(source.table)?.policies?.read;
|
|
487
499
|
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
488
500
|
if (!unconditional) {
|
|
489
|
-
|
|
501
|
+
fail4(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
490
502
|
}
|
|
491
503
|
}
|
|
492
504
|
}
|
|
@@ -556,6 +568,8 @@ var canonicalLinkSchema = z.object({
|
|
|
556
568
|
kind: z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
557
569
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
558
570
|
target: identifier2.optional(),
|
|
571
|
+
/** Owning namespace of an imported target; omitted for local links. */
|
|
572
|
+
namespace: identifier2.max(41).optional(),
|
|
559
573
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
560
574
|
on: columnIdentifier,
|
|
561
575
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
@@ -657,11 +671,12 @@ var canonicalForeignKeySchema = z.object({
|
|
|
657
671
|
}).strict();
|
|
658
672
|
var canonicalTableSchema = z.object({
|
|
659
673
|
table: identifier2,
|
|
674
|
+
displayName: z.string().trim().min(1).max(120).optional(),
|
|
660
675
|
description: z.string().max(2e3).optional(),
|
|
661
676
|
/** Table-level rename hint (new in v2). */
|
|
662
677
|
renamedFrom: identifier2.optional(),
|
|
663
|
-
/** Synced/reflected read-only tables (`source:
|
|
664
|
-
source: z.string().regex(
|
|
678
|
+
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
679
|
+
source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
665
680
|
columns: z.array(canonicalColumnSchema).default([]),
|
|
666
681
|
primaryKey: z.array(columnIdentifier).min(1).max(8).optional(),
|
|
667
682
|
links: z.array(canonicalLinkSchema).default([]),
|
|
@@ -768,53 +783,55 @@ function trackedCellColumns(table) {
|
|
|
768
783
|
}
|
|
769
784
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
770
785
|
function validateCellMetadata(table, index, ctx) {
|
|
771
|
-
const
|
|
786
|
+
const fail4 = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
772
787
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
773
788
|
if (table.materializes !== void 0) return;
|
|
774
789
|
if (table.cellMetadata !== true) {
|
|
775
|
-
for (const { position } of cells)
|
|
790
|
+
for (const { position } of cells) fail4(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
776
791
|
return;
|
|
777
792
|
}
|
|
778
793
|
if (table.source !== void 0) {
|
|
779
|
-
|
|
794
|
+
fail4(`synced table '${table.table}' cannot track cell metadata`);
|
|
780
795
|
return;
|
|
781
796
|
}
|
|
782
797
|
const tracked = trackedCellColumns(table);
|
|
783
798
|
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
784
799
|
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
785
|
-
if (withRev && table.internal !== true)
|
|
800
|
+
if (withRev && table.internal !== true) fail4(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
786
801
|
const seen = /* @__PURE__ */ new Set();
|
|
787
802
|
for (const { column, position, cell } of cells) {
|
|
788
803
|
const path = ["tables", index, "columns", position];
|
|
789
804
|
const owner = byName.get(cell.of);
|
|
790
805
|
if (!owner) {
|
|
791
|
-
|
|
806
|
+
fail4(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
792
807
|
continue;
|
|
793
808
|
}
|
|
794
|
-
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev")
|
|
809
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail4(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
795
810
|
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
796
|
-
|
|
811
|
+
fail4(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
797
812
|
}
|
|
798
813
|
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
799
|
-
|
|
814
|
+
fail4(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
800
815
|
}
|
|
801
816
|
const key = `${cell.of}:${cell.part}`;
|
|
802
|
-
if (seen.has(key))
|
|
817
|
+
if (seen.has(key)) fail4(`duplicate cell column '${column.name}'`, path);
|
|
803
818
|
seen.add(key);
|
|
804
819
|
}
|
|
805
820
|
for (const column of tracked) {
|
|
806
821
|
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
807
|
-
|
|
822
|
+
fail4(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
808
823
|
}
|
|
809
824
|
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
810
|
-
|
|
825
|
+
fail4(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
811
826
|
}
|
|
812
827
|
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
813
|
-
if (!seen.has(`${column.name}:${part}`))
|
|
828
|
+
if (!seen.has(`${column.name}:${part}`)) fail4(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
814
829
|
}
|
|
815
830
|
}
|
|
816
831
|
}
|
|
817
|
-
var
|
|
832
|
+
var exportGateSchema = z.union([z.literal(true), z.object({ capability: z.string().min(1) }).strict(), z.object({ role: z.string().min(1) }).strict()]);
|
|
833
|
+
var relationExportSchema = z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
834
|
+
var localDefinitionSchema = z.object({
|
|
818
835
|
definition_format: z.literal(CANONICAL_FORMAT),
|
|
819
836
|
namespace: identifier2.max(41),
|
|
820
837
|
description: z.string().max(2e3).optional(),
|
|
@@ -822,6 +839,8 @@ var canonicalDefinitionSchema = z.object({
|
|
|
822
839
|
tables: z.array(canonicalTableSchema).min(0).max(500),
|
|
823
840
|
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
824
841
|
views: z.array(canonicalViewSchema).max(500).optional(),
|
|
842
|
+
/** Target-owned cross-namespace access contract, using existing namespace grants. */
|
|
843
|
+
exports: z.record(identifier2, relationExportSchema).optional(),
|
|
825
844
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
826
845
|
capabilities: z.object({
|
|
827
846
|
roles: z.unknown().optional(),
|
|
@@ -848,7 +867,16 @@ var canonicalDefinitionSchema = z.object({
|
|
|
848
867
|
}
|
|
849
868
|
}
|
|
850
869
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
870
|
+
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
871
|
+
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
872
|
+
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
873
|
+
}
|
|
851
874
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
875
|
+
for (const [linkIndex, link] of table.links.entries()) {
|
|
876
|
+
if (link.enforce && link.namespace !== void 0 && link.namespace !== definition.namespace) {
|
|
877
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", linkIndex], message: "physical foreign keys require a local target; use an ORM link without enforce" });
|
|
878
|
+
}
|
|
879
|
+
}
|
|
852
880
|
const names = /* @__PURE__ */ new Set([
|
|
853
881
|
...table.primaryKey?.length ? [`${table.table}_pkey`] : [],
|
|
854
882
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
@@ -864,38 +892,38 @@ var canonicalDefinitionSchema = z.object({
|
|
|
864
892
|
}
|
|
865
893
|
}
|
|
866
894
|
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
867
|
-
const
|
|
868
|
-
if (table.source)
|
|
869
|
-
if (names.has(check.name))
|
|
895
|
+
const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
896
|
+
if (table.source) fail4("synced tables cannot declare checks");
|
|
897
|
+
if (names.has(check.name)) fail4(`duplicate constraint '${check.name}'`);
|
|
870
898
|
names.add(check.name);
|
|
871
899
|
try {
|
|
872
900
|
compileCheckExpression(check.expression, table.columns);
|
|
873
901
|
} catch (error) {
|
|
874
|
-
|
|
902
|
+
fail4(error instanceof Error ? error.message : String(error));
|
|
875
903
|
}
|
|
876
904
|
}
|
|
877
905
|
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
878
|
-
const
|
|
879
|
-
if (names.has(fk.name))
|
|
906
|
+
const fail4 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
907
|
+
if (names.has(fk.name)) fail4(`duplicate foreign key '${fk.name}'`);
|
|
880
908
|
names.add(fk.name);
|
|
881
909
|
const target = fkTables.get(fk.target);
|
|
882
910
|
if (table.source || !target || target.source) {
|
|
883
|
-
|
|
911
|
+
fail4("foreign keys must connect owned tables in the same namespace");
|
|
884
912
|
continue;
|
|
885
913
|
}
|
|
886
|
-
if (fk.columns.length !== fk.references.length)
|
|
887
|
-
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length)
|
|
888
|
-
for (const [position,
|
|
889
|
-
const sourceColumn = table.columns.find((column) => column.columnName ===
|
|
914
|
+
if (fk.columns.length !== fk.references.length) fail4("foreign key columns and references must have the same number of columns");
|
|
915
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail4("foreign key columns must not repeat");
|
|
916
|
+
for (const [position, name2] of fk.columns.entries()) {
|
|
917
|
+
const sourceColumn = table.columns.find((column) => column.columnName === name2);
|
|
890
918
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
891
|
-
if (!sourceColumn || !targetColumn)
|
|
892
|
-
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type))
|
|
919
|
+
if (!sourceColumn || !targetColumn) fail4("foreign key references an unknown column");
|
|
920
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail4("foreign key column types must match");
|
|
893
921
|
}
|
|
894
922
|
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
895
|
-
if (fk.columns.some((
|
|
923
|
+
if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail4("SET NULL requires nullable source columns");
|
|
896
924
|
}
|
|
897
925
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
898
|
-
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column))))
|
|
926
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail4("foreign key target columns must form a unique key");
|
|
899
927
|
}
|
|
900
928
|
}
|
|
901
929
|
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
@@ -997,27 +1025,31 @@ var canonicalDefinitionSchema = z.object({
|
|
|
997
1025
|
const visiting = /* @__PURE__ */ new Set();
|
|
998
1026
|
const visited = /* @__PURE__ */ new Set();
|
|
999
1027
|
let cycleReported = false;
|
|
1000
|
-
const visit2 = (
|
|
1001
|
-
if (visited.has(
|
|
1002
|
-
if (visiting.has(
|
|
1028
|
+
const visit2 = (name2, path) => {
|
|
1029
|
+
if (visited.has(name2) || cycleReported) return;
|
|
1030
|
+
if (visiting.has(name2)) {
|
|
1003
1031
|
cycleReported = true;
|
|
1004
1032
|
ctx.addIssue({
|
|
1005
1033
|
code: "custom",
|
|
1006
1034
|
path: ["views"],
|
|
1007
|
-
message: `view dependencies form a cycle: ${[...path,
|
|
1035
|
+
message: `view dependencies form a cycle: ${[...path, name2].join(" -> ")}`
|
|
1008
1036
|
});
|
|
1009
1037
|
return;
|
|
1010
1038
|
}
|
|
1011
|
-
visiting.add(
|
|
1012
|
-
const view = viewByName.get(
|
|
1039
|
+
visiting.add(name2);
|
|
1040
|
+
const view = viewByName.get(name2);
|
|
1013
1041
|
for (const dependency of view?.dependencies ?? []) {
|
|
1014
|
-
if (viewByName.has(dependency)) visit2(dependency, [...path,
|
|
1042
|
+
if (viewByName.has(dependency)) visit2(dependency, [...path, name2]);
|
|
1015
1043
|
}
|
|
1016
|
-
visiting.delete(
|
|
1017
|
-
visited.add(
|
|
1044
|
+
visiting.delete(name2);
|
|
1045
|
+
visited.add(name2);
|
|
1018
1046
|
};
|
|
1019
1047
|
for (const view of views) visit2(view.view, []);
|
|
1020
1048
|
});
|
|
1049
|
+
var canonicalDefinitionSchema = localDefinitionSchema.safeExtend({
|
|
1050
|
+
/** Flattened, pinned snapshots. Imported relations never participate in this namespace's DDL. */
|
|
1051
|
+
imports: z.array(localDefinitionSchema).max(32).optional()
|
|
1052
|
+
});
|
|
1021
1053
|
function canonicalRelations(definition) {
|
|
1022
1054
|
const views = (definition.views ?? []).map((view) => ({
|
|
1023
1055
|
table: view.view,
|
|
@@ -1061,13 +1093,22 @@ function normalizeCanonical(definition) {
|
|
|
1061
1093
|
dependencies: [...view.dependencies].sort()
|
|
1062
1094
|
}));
|
|
1063
1095
|
}
|
|
1096
|
+
if (definition.imports?.length) {
|
|
1097
|
+
normalized.imports = definition.imports.map(normalizeCanonical).sort((a, b) => a.namespace.localeCompare(b.namespace));
|
|
1098
|
+
} else {
|
|
1099
|
+
delete normalized.imports;
|
|
1100
|
+
}
|
|
1064
1101
|
return normalized;
|
|
1065
1102
|
}
|
|
1066
1103
|
|
|
1067
1104
|
// src/ir/index.ts
|
|
1068
|
-
var IR_VERSION =
|
|
1105
|
+
var IR_VERSION = 5;
|
|
1069
1106
|
var LEGACY_IR_VERSION = 1;
|
|
1070
|
-
var
|
|
1107
|
+
var GUARDED_IR_VERSION = 2;
|
|
1108
|
+
var COMPOSED_IR_VERSION = 3;
|
|
1109
|
+
var SQL_IR_VERSION = 4;
|
|
1110
|
+
var RUNTIME_SQL_IR_VERSION = 5;
|
|
1111
|
+
var irVersionSchema = z.union([z.literal(LEGACY_IR_VERSION), z.literal(GUARDED_IR_VERSION), z.literal(COMPOSED_IR_VERSION), z.literal(SQL_IR_VERSION), z.literal(RUNTIME_SQL_IR_VERSION)]);
|
|
1071
1112
|
var operationVersionSchema = z.object({ irVersion: irVersionSchema });
|
|
1072
1113
|
function requiredArtifactIrVersion(operations) {
|
|
1073
1114
|
let required = LEGACY_IR_VERSION;
|
|
@@ -1080,14 +1121,20 @@ function requiredArtifactIrVersion(operations) {
|
|
|
1080
1121
|
var CLAIMS_CONTRACT = {
|
|
1081
1122
|
sub: "string",
|
|
1082
1123
|
org: "string?",
|
|
1124
|
+
ext_subject: "string?",
|
|
1125
|
+
ext_email: "string?",
|
|
1083
1126
|
roles: "string[]",
|
|
1084
1127
|
capabilities: "string[]"
|
|
1085
1128
|
};
|
|
1129
|
+
var EXTERNAL_POLICY_CLAIM_KEYS = ["ext_subject", "ext_email"];
|
|
1130
|
+
function isExternalPolicyClaimKey(key) {
|
|
1131
|
+
return EXTERNAL_POLICY_CLAIM_KEYS.includes(key);
|
|
1132
|
+
}
|
|
1086
1133
|
var scalarLiteral = z.union([z.string(), z.number(), z.boolean(), z.null()]);
|
|
1087
1134
|
var valueIrSchema = z.discriminatedUnion("kind", [
|
|
1088
1135
|
z.object({ kind: z.literal("literal"), value: z.union([scalarLiteral, z.array(scalarLiteral)]) }),
|
|
1089
1136
|
z.object({ kind: z.literal("variable"), name: z.string() }),
|
|
1090
|
-
z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org"]) })
|
|
1137
|
+
z.object({ kind: z.literal("claim"), key: z.enum(["sub", "org", ...EXTERNAL_POLICY_CLAIM_KEYS]) })
|
|
1091
1138
|
]);
|
|
1092
1139
|
var CMP_OPS = [
|
|
1093
1140
|
"eq",
|
|
@@ -1119,8 +1166,8 @@ var predIrSchema = z.lazy(
|
|
|
1119
1166
|
value: z.number(),
|
|
1120
1167
|
where: predIrSchema.optional()
|
|
1121
1168
|
}),
|
|
1122
|
-
z.object({ kind: z.literal("capability"), key: z.string() }),
|
|
1123
|
-
z.object({ kind: z.literal("role"), key: z.string() }),
|
|
1169
|
+
z.object({ kind: z.literal("capability"), key: z.string(), namespace: z.string().optional() }),
|
|
1170
|
+
z.object({ kind: z.literal("role"), key: z.string(), namespace: z.string().optional() }),
|
|
1124
1171
|
z.object({ kind: z.literal("dynamicFilter"), slot: z.string() }),
|
|
1125
1172
|
z.object({ kind: z.literal("const"), value: z.boolean() })
|
|
1126
1173
|
])
|
|
@@ -1347,7 +1394,31 @@ var variableIrSchema = z.object({
|
|
|
1347
1394
|
/** Absent = no default declared; see {@link variableDefaultIrSchema}. */
|
|
1348
1395
|
defaultValue: variableDefaultIrSchema.optional()
|
|
1349
1396
|
});
|
|
1397
|
+
var sqlColumnSchema = z.object({ name: z.string(), type: z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]), nullable: z.boolean() });
|
|
1350
1398
|
var compiledOperationSchema = z.object({
|
|
1399
|
+
language: z.literal("sql").optional(),
|
|
1400
|
+
/** Server-compiled object catalog and permission ceiling, never caller SQL. */
|
|
1401
|
+
sqlQuery: z.object({
|
|
1402
|
+
catalog: z.object({
|
|
1403
|
+
identity: z.string(),
|
|
1404
|
+
objects: z.array(z.object({
|
|
1405
|
+
ref: z.string(),
|
|
1406
|
+
table: z.string(),
|
|
1407
|
+
columns: z.array(sqlColumnSchema.extend({ type: z.string() })).min(1),
|
|
1408
|
+
scan: z.string().min(1),
|
|
1409
|
+
read: z.array(z.string()).min(1)
|
|
1410
|
+
}).strict()).min(1).max(256)
|
|
1411
|
+
}).strict(),
|
|
1412
|
+
read: z.array(z.string()).min(1).max(256),
|
|
1413
|
+
maxRows: z.number().int().min(1).max(1e4)
|
|
1414
|
+
}).strict().optional(),
|
|
1415
|
+
sql: z.object({
|
|
1416
|
+
text: z.string().min(1).max(1048576),
|
|
1417
|
+
columns: z.array(sqlColumnSchema).min(1).max(256),
|
|
1418
|
+
parameters: z.array(z.string()).max(128),
|
|
1419
|
+
read: z.array(z.string()).min(1).max(256),
|
|
1420
|
+
maxRows: z.number().int().min(1).max(1e4)
|
|
1421
|
+
}).strict().optional(),
|
|
1351
1422
|
irVersion: irVersionSchema,
|
|
1352
1423
|
name: z.string(),
|
|
1353
1424
|
opHash: z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
@@ -1359,8 +1430,16 @@ var compiledOperationSchema = z.object({
|
|
|
1359
1430
|
variables: z.array(variableIrSchema),
|
|
1360
1431
|
/** Every table the op touches — data-token scoping + audit. */
|
|
1361
1432
|
tableSet: z.array(z.string()).min(1),
|
|
1362
|
-
roots: z.array(rootIrSchema)
|
|
1433
|
+
roots: z.array(rootIrSchema)
|
|
1363
1434
|
}).superRefine((operation, ctx) => {
|
|
1435
|
+
if (operation.language === "sql") {
|
|
1436
|
+
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
1437
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !operation.requires?.capability) {
|
|
1438
|
+
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
1439
|
+
}
|
|
1440
|
+
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
1441
|
+
ctx.addIssue({ code: "custom", message: "GraphQL operations require roots and cannot carry SQL" });
|
|
1442
|
+
}
|
|
1364
1443
|
for (const [index, root] of operation.roots.entries()) {
|
|
1365
1444
|
if (root.kind !== "mutation" || root.assertAffectedRows === void 0) continue;
|
|
1366
1445
|
if (operation.irVersion < 2 || operation.kind !== "mutation" || root.includeIf !== void 0) {
|
|
@@ -1407,11 +1486,16 @@ var resolvedMetaIrSchema = z.object({
|
|
|
1407
1486
|
});
|
|
1408
1487
|
var tableMetaIrSchema = z.object({
|
|
1409
1488
|
table: z.string(),
|
|
1489
|
+
namespace: z.string().optional(),
|
|
1490
|
+
dependencies: z.array(z.string()).optional(),
|
|
1491
|
+
foreignReferences: z.array(z.object({ column: z.string(), target: z.string(), targetColumn: z.string() })).optional(),
|
|
1492
|
+
exportRead: z.union([z.literal(true), z.object({ capability: z.string() }), z.object({ role: z.string() })]).optional(),
|
|
1493
|
+
exportWrite: z.union([z.literal(true), z.object({ capability: z.string() }), z.object({ role: z.string() })]).optional(),
|
|
1410
1494
|
schema: z.string(),
|
|
1411
1495
|
typename: z.string(),
|
|
1412
1496
|
/**
|
|
1413
1497
|
* True when statements over this relation evaluate a claims-gated synced
|
|
1414
|
-
* view: synced twins (`source:
|
|
1498
|
+
* view: synced twins (`source: schema.table`) materialize as views whose baked
|
|
1415
1499
|
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
1416
1500
|
* inherit the gate transitively through their declared dependencies. The
|
|
1417
1501
|
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
@@ -1470,13 +1554,21 @@ var compiledArtifactSchema = z.object({
|
|
|
1470
1554
|
irVersion: irVersionSchema,
|
|
1471
1555
|
namespace: z.string(),
|
|
1472
1556
|
schemaHash: z.string(),
|
|
1557
|
+
ownSchemaHash: z.string().optional(),
|
|
1558
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
1559
|
+
readPolicies: z.record(z.string(), predIrSchema).optional(),
|
|
1473
1560
|
opsHash: z.string(),
|
|
1561
|
+
/** Apply-observed synced objects without a serving backing; not part of the authored schema hash. */
|
|
1562
|
+
unbackedTables: z.array(z.string()).optional(),
|
|
1474
1563
|
tables: z.record(z.string(), tableMetaIrSchema),
|
|
1475
1564
|
operations: z.record(z.string(), compiledOperationSchema)
|
|
1476
1565
|
}).superRefine((artifact, ctx) => {
|
|
1477
|
-
|
|
1566
|
+
if (Object.keys(artifact.dependencies ?? {}).length > 0 && artifact.irVersion < COMPOSED_IR_VERSION) {
|
|
1567
|
+
ctx.addIssue({ code: "custom", path: ["irVersion"], message: "cross-namespace dependencies require IR v3" });
|
|
1568
|
+
}
|
|
1569
|
+
for (const [name2, operation] of Object.entries(artifact.operations)) {
|
|
1478
1570
|
if (operation.irVersion > artifact.irVersion) {
|
|
1479
|
-
ctx.addIssue({ code: "custom", path: ["operations",
|
|
1571
|
+
ctx.addIssue({ code: "custom", path: ["operations", name2, "irVersion"], message: "operation requires a newer IR version than its artifact" });
|
|
1480
1572
|
}
|
|
1481
1573
|
}
|
|
1482
1574
|
});
|
|
@@ -1489,20 +1581,20 @@ var IrVersionError = class extends Error {
|
|
|
1489
1581
|
function normalizeIr(raw) {
|
|
1490
1582
|
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
1491
1583
|
const version = raw.irVersion;
|
|
1492
|
-
if (version
|
|
1584
|
+
if (!irVersionSchema.safeParse(version).success) throw new IrVersionError(version);
|
|
1493
1585
|
return compiledArtifactSchema.parse(raw);
|
|
1494
1586
|
}
|
|
1495
1587
|
|
|
1496
1588
|
// src/sdl/naming.ts
|
|
1497
|
-
function camelCase(
|
|
1498
|
-
const segments =
|
|
1589
|
+
function camelCase(name2) {
|
|
1590
|
+
const segments = name2.split("_").filter((segment) => segment.length > 0);
|
|
1499
1591
|
const first = segments[0];
|
|
1500
|
-
if (first === void 0) return
|
|
1592
|
+
if (first === void 0) return name2;
|
|
1501
1593
|
const head = first.charAt(0).toLowerCase() + first.slice(1);
|
|
1502
1594
|
return head + segments.slice(1).map(capitalizeFirst).join("");
|
|
1503
1595
|
}
|
|
1504
|
-
function pascalCase(
|
|
1505
|
-
return capitalizeFirst(camelCase(
|
|
1596
|
+
function pascalCase(name2) {
|
|
1597
|
+
return capitalizeFirst(camelCase(name2));
|
|
1506
1598
|
}
|
|
1507
1599
|
function capitalizeFirst(segment) {
|
|
1508
1600
|
return segment.charAt(0).toUpperCase() + segment.slice(1);
|
|
@@ -1522,8 +1614,8 @@ function typeName(table) {
|
|
|
1522
1614
|
if (last === void 0) return pascalCase(table);
|
|
1523
1615
|
return [...segments.slice(0, -1), singularizeWord(last)].map(capitalizeFirst).join("");
|
|
1524
1616
|
}
|
|
1525
|
-
function enumTypeName(
|
|
1526
|
-
return pascalCase(
|
|
1617
|
+
function enumTypeName(name2) {
|
|
1618
|
+
return pascalCase(name2);
|
|
1527
1619
|
}
|
|
1528
1620
|
var SCALAR_FOR_COLUMN_TYPE = {
|
|
1529
1621
|
uuid: "uuid",
|
|
@@ -1941,6 +2033,7 @@ var PolicyCompiler = class {
|
|
|
1941
2033
|
case "or":
|
|
1942
2034
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
1943
2035
|
case "not":
|
|
2036
|
+
assertNoNegatedExternalClaim(node.operand);
|
|
1944
2037
|
return { kind: "not", operand: this.compile(node.operand, table, depth) };
|
|
1945
2038
|
case "capability":
|
|
1946
2039
|
return { kind: "capability", key: node.key };
|
|
@@ -2020,6 +2113,9 @@ var PolicyCompiler = class {
|
|
|
2020
2113
|
columnCandidates(info)
|
|
2021
2114
|
);
|
|
2022
2115
|
}
|
|
2116
|
+
if (valueSide.kind === "claim" && isExternalPolicyClaimKey(valueSide.key)) {
|
|
2117
|
+
assertExternalClaimComparison({ op, key: valueSide.key, column: info.columns.find((c) => c.name === column) });
|
|
2118
|
+
}
|
|
2023
2119
|
let cmp = this.buildCmp(op, column, valueSide);
|
|
2024
2120
|
for (let i = links.length - 1; i >= 0; i -= 1) {
|
|
2025
2121
|
cmp = { kind: "exists", link: links[i].name, where: cmp };
|
|
@@ -2040,6 +2136,33 @@ var PolicyCompiler = class {
|
|
|
2040
2136
|
return { kind: "cmp", op: CMP_BY_OP[op], column, value };
|
|
2041
2137
|
}
|
|
2042
2138
|
};
|
|
2139
|
+
function assertExternalClaimComparison({ op, key, column }) {
|
|
2140
|
+
if (op !== "=") {
|
|
2141
|
+
throw new PolicyCompileError(
|
|
2142
|
+
`claim('${key}') supports = comparisons only, got '${op}'`,
|
|
2143
|
+
"unsupported-comparison"
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
if (column?.type !== "text") {
|
|
2147
|
+
const actual = column?.type === void 0 ? "of unknown type" : typeof column.type === "string" ? column.type : "an enum";
|
|
2148
|
+
throw new PolicyCompileError(
|
|
2149
|
+
`claim('${key}') is text and compares to a text column only \u2014 '${column?.name ?? "?"}' is ${actual}`,
|
|
2150
|
+
"claim-type-mismatch"
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
function assertNoNegatedExternalClaim(operand) {
|
|
2155
|
+
walkPolicy(operand, (node) => {
|
|
2156
|
+
if (node.kind !== "compare") return;
|
|
2157
|
+
const claim = [node.left, node.right].find((side) => side.kind === "claim" && isExternalPolicyClaimKey(side.key));
|
|
2158
|
+
if (claim?.kind === "claim") {
|
|
2159
|
+
throw new PolicyCompileError(
|
|
2160
|
+
`claim('${claim.key}') cannot appear under not(): a negated borrower comparison admits other borrowers' rows`,
|
|
2161
|
+
"unsupported-comparison"
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2043
2166
|
function compilePredicateV2({ expression, table, resolver, maxLinkDepth }) {
|
|
2044
2167
|
const parsed = typeof expression === "string" ? parsePolicy(expression) : expression;
|
|
2045
2168
|
const compiler = new PolicyCompiler({ resolver, maxLinkDepth: maxLinkDepth ?? POLICY_MAX_LINK_DEPTH });
|
|
@@ -2314,7 +2437,7 @@ var PolicyInjector = class {
|
|
|
2314
2437
|
const updatePolicy = this.update(table);
|
|
2315
2438
|
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
2316
2439
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
2317
|
-
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
2440
|
+
const check = predContainsExists(updatePolicy) && !this.ctx.composed ? {} : { insertCheck: updatePolicy };
|
|
2318
2441
|
return { ...root, where, ...check, returning };
|
|
2319
2442
|
}
|
|
2320
2443
|
if (root.op === "cowOverride") {
|
|
@@ -2328,7 +2451,7 @@ var PolicyInjector = class {
|
|
|
2328
2451
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2329
2452
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2330
2453
|
);
|
|
2331
|
-
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
2454
|
+
const check = predContainsExists(updatePolicy) && !this.ctx.composed ? {} : { insertCheck: updatePolicy };
|
|
2332
2455
|
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
2333
2456
|
}
|
|
2334
2457
|
return { ...root, where, returning };
|
|
@@ -2479,16 +2602,92 @@ var NAME_KEY_BY_KIND = {
|
|
|
2479
2602
|
function inputTypeName(table, kind) {
|
|
2480
2603
|
return tableNames(table)[NAME_KEY_BY_KIND[kind]];
|
|
2481
2604
|
}
|
|
2482
|
-
function classifyInputType(
|
|
2605
|
+
function classifyInputType(name2, tableByTypename) {
|
|
2483
2606
|
for (const [suffix, input] of INPUT_TYPE_SUFFIXES) {
|
|
2484
|
-
if (!
|
|
2485
|
-
const table = tableByTypename.get(
|
|
2607
|
+
if (!name2.endsWith(suffix) || name2.length <= suffix.length) continue;
|
|
2608
|
+
const table = tableByTypename.get(name2.slice(0, -suffix.length));
|
|
2486
2609
|
if (table !== void 0) return { input, table };
|
|
2487
2610
|
}
|
|
2488
2611
|
return void 0;
|
|
2489
2612
|
}
|
|
2490
2613
|
|
|
2614
|
+
// src/errors.ts
|
|
2615
|
+
var OrmDefinitionError = class extends Error {
|
|
2616
|
+
constructor(message, file) {
|
|
2617
|
+
super(file ? `${file}: ${message}` : message);
|
|
2618
|
+
this.file = file;
|
|
2619
|
+
}
|
|
2620
|
+
name = "OrmDefinitionError";
|
|
2621
|
+
};
|
|
2622
|
+
|
|
2623
|
+
// src/canonical/composition.ts
|
|
2624
|
+
function localContract(definition) {
|
|
2625
|
+
return normalizeCanonical(localDefinitionSchema.parse(definition));
|
|
2626
|
+
}
|
|
2627
|
+
function composeNamespace(definition) {
|
|
2628
|
+
const all = [localContract(definition), ...definition.imports ?? []];
|
|
2629
|
+
const owners = /* @__PURE__ */ new Map();
|
|
2630
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
2631
|
+
const enums = /* @__PURE__ */ new Map();
|
|
2632
|
+
for (const owner of all) {
|
|
2633
|
+
if (namespaces.has(owner.namespace)) throw new OrmDefinitionError(`duplicate imported namespace '${owner.namespace}'`);
|
|
2634
|
+
namespaces.add(owner.namespace);
|
|
2635
|
+
for (const value of owner.enums) {
|
|
2636
|
+
const previous = enums.get(value.name);
|
|
2637
|
+
if (previous && stableJson(previous) !== stableJson(value)) {
|
|
2638
|
+
throw new OrmDefinitionError(`conflicting imported enum '${value.name}'`);
|
|
2639
|
+
}
|
|
2640
|
+
enums.set(value.name, value);
|
|
2641
|
+
}
|
|
2642
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2643
|
+
if (owners.has(relation.table)) throw new OrmDefinitionError(`ambiguous imported relation '${relation.table}'`);
|
|
2644
|
+
owners.set(relation.table, owner);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
for (const owner of definition.imports?.length ? all : []) {
|
|
2648
|
+
for (const relation of canonicalRelations(owner)) {
|
|
2649
|
+
for (const link of relation.links) {
|
|
2650
|
+
if (link.kind === "toPlatformUser") continue;
|
|
2651
|
+
const target = owners.get(link.target ?? "");
|
|
2652
|
+
if (!target || target.namespace !== (link.namespace ?? owner.namespace)) {
|
|
2653
|
+
throw new OrmDefinitionError(`link '${owner.namespace}/${relation.table}.${link.name}' has no matching imported target`);
|
|
2654
|
+
}
|
|
2655
|
+
if (target.namespace !== owner.namespace && !target.exports?.[link.target ?? ""]) {
|
|
2656
|
+
throw new OrmDefinitionError(`target '${target.namespace}/${link.target}' is not exported`);
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
}
|
|
2661
|
+
return {
|
|
2662
|
+
owners,
|
|
2663
|
+
definition: {
|
|
2664
|
+
...localContract(definition),
|
|
2665
|
+
tables: all.flatMap((item) => item.tables),
|
|
2666
|
+
views: all.flatMap((item) => item.views ?? []),
|
|
2667
|
+
enums: [...enums.values()]
|
|
2668
|
+
}
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2491
2672
|
// src/compiler-gql/context.ts
|
|
2673
|
+
function scopePolicy(pred, namespace) {
|
|
2674
|
+
switch (pred.kind) {
|
|
2675
|
+
case "capability":
|
|
2676
|
+
case "role":
|
|
2677
|
+
return { ...pred, namespace };
|
|
2678
|
+
case "and":
|
|
2679
|
+
case "or":
|
|
2680
|
+
return { ...pred, operands: pred.operands.map((p) => scopePolicy(p, namespace)) };
|
|
2681
|
+
case "not":
|
|
2682
|
+
return { ...pred, operand: scopePolicy(pred.operand, namespace) };
|
|
2683
|
+
case "exists":
|
|
2684
|
+
return { ...pred, where: scopePolicy(pred.where, namespace) };
|
|
2685
|
+
case "aggPredicate":
|
|
2686
|
+
return pred.where ? { ...pred, where: scopePolicy(pred.where, namespace) } : pred;
|
|
2687
|
+
default:
|
|
2688
|
+
return pred;
|
|
2689
|
+
}
|
|
2690
|
+
}
|
|
2492
2691
|
function platformUserMeta() {
|
|
2493
2692
|
const columns = [
|
|
2494
2693
|
// `sub` is the view's key (auth0_id, filtered NOT NULL in the view).
|
|
@@ -2553,7 +2752,8 @@ function logicalColumn(entries, ref) {
|
|
|
2553
2752
|
return entries.find((c) => c.name === ref || c.columnName === ref)?.name;
|
|
2554
2753
|
}
|
|
2555
2754
|
function buildCompileContext(rawCanonical) {
|
|
2556
|
-
const
|
|
2755
|
+
const normalized = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
|
|
2756
|
+
const { definition: canonical, owners } = composeNamespace(normalized);
|
|
2557
2757
|
const diagnostics = [];
|
|
2558
2758
|
const enums = new Map(canonical.enums.map((e) => [e.name, e]));
|
|
2559
2759
|
const relations = canonicalRelations(canonical);
|
|
@@ -2566,17 +2766,18 @@ function buildCompileContext(rawCanonical) {
|
|
|
2566
2766
|
const syncedTableNames = new Set(canonical.tables.filter((t) => t.source !== void 0).map((t) => t.table));
|
|
2567
2767
|
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
2568
2768
|
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
2569
|
-
const relationIsClaimsGated = (
|
|
2570
|
-
if (syncedTableNames.has(
|
|
2571
|
-
const memo = claimsGatedMemo.get(
|
|
2769
|
+
const relationIsClaimsGated = (name2) => {
|
|
2770
|
+
if (syncedTableNames.has(name2)) return true;
|
|
2771
|
+
const memo = claimsGatedMemo.get(name2);
|
|
2572
2772
|
if (memo !== void 0) return memo;
|
|
2573
|
-
claimsGatedMemo.set(
|
|
2574
|
-
const gated = (viewDependencies.get(
|
|
2575
|
-
claimsGatedMemo.set(
|
|
2773
|
+
claimsGatedMemo.set(name2, false);
|
|
2774
|
+
const gated = (viewDependencies.get(name2) ?? []).some(relationIsClaimsGated);
|
|
2775
|
+
claimsGatedMemo.set(name2, gated);
|
|
2576
2776
|
return gated;
|
|
2577
2777
|
};
|
|
2578
2778
|
for (const table of relations) {
|
|
2579
2779
|
const isView = viewNames.has(table.table);
|
|
2780
|
+
const owner = owners.get(table.table);
|
|
2580
2781
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
2581
2782
|
columnEntries.set(table.table, entries);
|
|
2582
2783
|
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
@@ -2614,7 +2815,10 @@ function buildCompileContext(rawCanonical) {
|
|
|
2614
2815
|
}
|
|
2615
2816
|
tables[table.table] = {
|
|
2616
2817
|
table: table.table,
|
|
2617
|
-
schema: ormSchemaName(
|
|
2818
|
+
schema: ormSchemaName(owner.namespace),
|
|
2819
|
+
namespace: owner.namespace,
|
|
2820
|
+
...viewDependencies.has(table.table) ? { dependencies: viewDependencies.get(table.table) } : {},
|
|
2821
|
+
...owner.exports?.[table.table] ? { exportRead: owner.exports[table.table].read, exportWrite: owner.exports[table.table].write } : {},
|
|
2618
2822
|
typename: typeNameForTable(table.table),
|
|
2619
2823
|
// Synced twins (and views reading them, resolved transitively below)
|
|
2620
2824
|
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
@@ -2625,7 +2829,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2625
2829
|
filterable: data.map((c) => c.name),
|
|
2626
2830
|
orderable: data.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
2627
2831
|
insertable: resolved ? resolvedWritable(data) : isView ? [] : active.map((c) => c.name),
|
|
2628
|
-
settable: resolved ? resolvedWritable(data).filter((
|
|
2832
|
+
settable: resolved ? resolvedWritable(data).filter((name2) => !primaryKey.includes(name2)) : isView ? [] : active.filter((c) => !c.onUpdateFn).map((c) => c.name),
|
|
2629
2833
|
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2630
2834
|
...touchOnUpdate.length > 0 ? { touchOnUpdate } : {},
|
|
2631
2835
|
...cells ? { cells } : {},
|
|
@@ -2641,7 +2845,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2641
2845
|
for (const link of table.links) {
|
|
2642
2846
|
const targetName = link.kind === "toPlatformUser" ? PLATFORM_USER_TABLE : link.target;
|
|
2643
2847
|
const target = targetName === void 0 ? void 0 : tables[targetName];
|
|
2644
|
-
if (!targetName || !target) {
|
|
2848
|
+
if (!targetName || !target || link.namespace !== void 0 && link.namespace !== target.namespace) {
|
|
2645
2849
|
diagnostics.push({
|
|
2646
2850
|
severity: "error",
|
|
2647
2851
|
code: "unknown-link-target",
|
|
@@ -2666,6 +2870,11 @@ function buildCompileContext(rawCanonical) {
|
|
|
2666
2870
|
continue;
|
|
2667
2871
|
}
|
|
2668
2872
|
meta.links.push({ name: link.name, kind: link.kind, target: targetName, on, references });
|
|
2873
|
+
if (link.kind !== "toPlatformUser" && meta.namespace !== target.namespace) {
|
|
2874
|
+
const fkOwner = link.kind === "toMany" ? target : meta;
|
|
2875
|
+
const referenced = link.kind === "toMany" ? table.table : targetName;
|
|
2876
|
+
(fkOwner.foreignReferences ??= []).push({ column: on, target: referenced, targetColumn: references });
|
|
2877
|
+
}
|
|
2669
2878
|
}
|
|
2670
2879
|
}
|
|
2671
2880
|
const readPolicies = /* @__PURE__ */ new Map();
|
|
@@ -2683,7 +2892,8 @@ function buildCompileContext(rawCanonical) {
|
|
|
2683
2892
|
return;
|
|
2684
2893
|
}
|
|
2685
2894
|
try {
|
|
2686
|
-
|
|
2895
|
+
const pred = compilePolicy({ expression, table: table.table, resolver });
|
|
2896
|
+
store.set(table.table, normalized.imports?.length ? scopePolicy(pred, owners.get(table.table).namespace) : pred);
|
|
2687
2897
|
} catch (error) {
|
|
2688
2898
|
if (error instanceof PolicyCompileError || error instanceof PolicyParseError) {
|
|
2689
2899
|
diagnostics.push({
|
|
@@ -2737,6 +2947,42 @@ function buildCompileContext(rawCanonical) {
|
|
|
2737
2947
|
updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2738
2948
|
pins.set(PLATFORM_USER_TABLE, []);
|
|
2739
2949
|
}
|
|
2950
|
+
if (normalized.imports?.length) {
|
|
2951
|
+
const originalReads = new Map(readPolicies);
|
|
2952
|
+
const protect = (pred, table, path) => {
|
|
2953
|
+
switch (pred.kind) {
|
|
2954
|
+
case "and":
|
|
2955
|
+
case "or":
|
|
2956
|
+
return { ...pred, operands: pred.operands.map((p) => protect(p, table, path)) };
|
|
2957
|
+
case "not":
|
|
2958
|
+
return { ...pred, operand: protect(pred.operand, table, path) };
|
|
2959
|
+
case "exists":
|
|
2960
|
+
case "aggPredicate": {
|
|
2961
|
+
const target = tables[table]?.links.find((link) => link.name === pred.link)?.target;
|
|
2962
|
+
if (!target) return pred;
|
|
2963
|
+
let where = pred.where ? protect(pred.where, target, path) : CONST_TRUE;
|
|
2964
|
+
if (tables[table]?.namespace !== tables[target]?.namespace) {
|
|
2965
|
+
if (path.includes(target)) {
|
|
2966
|
+
diagnostics.push({
|
|
2967
|
+
severity: "error",
|
|
2968
|
+
code: "cyclic-cross-namespace-policy",
|
|
2969
|
+
table,
|
|
2970
|
+
message: `cross-namespace read policies form a cycle through '${target}'`
|
|
2971
|
+
});
|
|
2972
|
+
return CONST_FALSE;
|
|
2973
|
+
}
|
|
2974
|
+
where = andPred(where, protect(originalReads.get(target) ?? CONST_TRUE, target, [...path, target]));
|
|
2975
|
+
}
|
|
2976
|
+
return { ...pred, where };
|
|
2977
|
+
}
|
|
2978
|
+
default:
|
|
2979
|
+
return pred;
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2982
|
+
for (const policies of [readPolicies, writePolicies, insertPolicies, updatePolicies]) {
|
|
2983
|
+
for (const [table, policy] of policies) policies.set(table, protect(policy, table, [table]));
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2740
2986
|
const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
|
|
2741
2987
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2742
2988
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
@@ -2813,6 +3059,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
2813
3059
|
return {
|
|
2814
3060
|
ctx: {
|
|
2815
3061
|
namespace: canonical.namespace,
|
|
3062
|
+
composed: !!normalized.imports?.length,
|
|
2816
3063
|
tables,
|
|
2817
3064
|
tableByTypename,
|
|
2818
3065
|
queryRoots,
|
|
@@ -2881,18 +3128,18 @@ function flattenSelectionSet(selectionSet, fragments, stack) {
|
|
|
2881
3128
|
continue;
|
|
2882
3129
|
}
|
|
2883
3130
|
if (selection.kind === Kind.FRAGMENT_SPREAD) {
|
|
2884
|
-
const
|
|
3131
|
+
const name2 = selection.name.value;
|
|
2885
3132
|
if (selection.directives && selection.directives.length > 0) {
|
|
2886
|
-
throw new DocumentCompileError(`directives on fragment spread '...${
|
|
3133
|
+
throw new DocumentCompileError(`directives on fragment spread '...${name2}' are not supported`, "fragment-directive");
|
|
2887
3134
|
}
|
|
2888
|
-
if (stack.includes(
|
|
2889
|
-
throw new DocumentCompileError(`fragment cycle detected through '${
|
|
3135
|
+
if (stack.includes(name2)) {
|
|
3136
|
+
throw new DocumentCompileError(`fragment cycle detected through '${name2}'`, "fragment-cycle");
|
|
2890
3137
|
}
|
|
2891
|
-
const fragment = fragments.get(
|
|
3138
|
+
const fragment = fragments.get(name2);
|
|
2892
3139
|
if (!fragment) {
|
|
2893
|
-
throw new DocumentCompileError(`unknown fragment '${
|
|
3140
|
+
throw new DocumentCompileError(`unknown fragment '${name2}'`, "unknown-fragment");
|
|
2894
3141
|
}
|
|
2895
|
-
const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack,
|
|
3142
|
+
const inlined2 = flattenSelectionSet(fragment.selectionSet, fragments, [...stack, name2]);
|
|
2896
3143
|
selections.push(...inlined2.selections);
|
|
2897
3144
|
continue;
|
|
2898
3145
|
}
|
|
@@ -2932,40 +3179,40 @@ var SOURCE_DIRECTIVES = /* @__PURE__ */ new Set(["prefetch", "writeback", "local
|
|
|
2932
3179
|
var fail = (message, code) => {
|
|
2933
3180
|
throw new DocumentCompileError(message, code);
|
|
2934
3181
|
};
|
|
2935
|
-
function readArgs(node,
|
|
2936
|
-
return (node.directives ?? []).filter((directive) => directive.name.value ===
|
|
3182
|
+
function readArgs(node, name2) {
|
|
3183
|
+
return (node.directives ?? []).filter((directive) => directive.name.value === name2).map((directive) => {
|
|
2937
3184
|
const args = {};
|
|
2938
3185
|
for (const arg of directive.arguments ?? []) {
|
|
2939
3186
|
const key = arg.name.value;
|
|
2940
3187
|
if (key === "source" || key === "maxStaleness") {
|
|
2941
|
-
if (arg.value.kind !== Kind.STRING) fail(`@${
|
|
3188
|
+
if (arg.value.kind !== Kind.STRING) fail(`@${name2}(${key}:) must be a string literal`, "invalid-source-directive");
|
|
2942
3189
|
args[key] = arg.value.value;
|
|
2943
|
-
} else if (key === "mode" &&
|
|
3190
|
+
} else if (key === "mode" && name2 === "writeback") {
|
|
2944
3191
|
if (arg.value.kind !== Kind.ENUM || !["SYNC", "ASYNC"].includes(arg.value.value)) {
|
|
2945
3192
|
fail("@writeback(mode:) must be SYNC or ASYNC", "invalid-source-directive");
|
|
2946
3193
|
}
|
|
2947
3194
|
args.mode = arg.value.value === "SYNC" ? "sync" : "async";
|
|
2948
3195
|
} else {
|
|
2949
|
-
fail(`@${
|
|
3196
|
+
fail(`@${name2} does not take argument '${key}'`, "invalid-source-directive");
|
|
2950
3197
|
}
|
|
2951
3198
|
}
|
|
2952
|
-
if (!args.source) fail(`@${
|
|
3199
|
+
if (!args.source) fail(`@${name2} needs a source: argument`, "invalid-source-directive");
|
|
2953
3200
|
return args;
|
|
2954
3201
|
});
|
|
2955
3202
|
}
|
|
2956
|
-
function prefetchSourceFor({ resolved, name, directive, table }) {
|
|
3203
|
+
function prefetchSourceFor({ resolved, name: name2, directive, table }) {
|
|
2957
3204
|
if (!resolved) return fail(`@${directive} is only valid on resolved tables \u2014 '${table}' is not one`, "invalid-source-directive");
|
|
2958
|
-
const source = resolved.sources.find((entry) => entry.name ===
|
|
3205
|
+
const source = resolved.sources.find((entry) => entry.name === name2);
|
|
2959
3206
|
if (!source || source.kind !== "prefetch") {
|
|
2960
3207
|
const options = resolved.sources.filter((entry) => entry.kind === "prefetch").map((entry) => entry.name);
|
|
2961
|
-
return fail(`@${directive}(source: "${
|
|
3208
|
+
return fail(`@${directive}(source: "${name2}") on '${table}' must name a prefetch source (${options.join(", ") || "none declared"})`, "invalid-source-directive");
|
|
2962
3209
|
}
|
|
2963
3210
|
return source;
|
|
2964
3211
|
}
|
|
2965
3212
|
function applyPrefetchDirectives({ node, root, meta }) {
|
|
2966
3213
|
const directives = readArgs(node, "prefetch");
|
|
2967
|
-
for (const
|
|
2968
|
-
if (readArgs(node,
|
|
3214
|
+
for (const name2 of ["writeback", "localOnly"]) {
|
|
3215
|
+
if (readArgs(node, name2).length > 0) fail(`@${name2} is only valid on resolved-table mutation roots`, "invalid-source-directive");
|
|
2969
3216
|
}
|
|
2970
3217
|
if (directives.length === 0) return root;
|
|
2971
3218
|
if (root.kind !== "select" && root.kind !== "byPk") {
|
|
@@ -3126,13 +3373,13 @@ var OperationLowerer = class {
|
|
|
3126
3373
|
return this.ctx.tables[table];
|
|
3127
3374
|
}
|
|
3128
3375
|
/** A data column — cell-metadata columns are reachable only through `cells { … }`. */
|
|
3129
|
-
column(table,
|
|
3376
|
+
column(table, name2) {
|
|
3130
3377
|
const meta = this.meta(table);
|
|
3131
|
-
const column = meta.columns.find((c) => c.name ===
|
|
3132
|
-
return column && !isCellColumn(meta,
|
|
3378
|
+
const column = meta.columns.find((c) => c.name === name2);
|
|
3379
|
+
return column && !isCellColumn(meta, name2) ? column : void 0;
|
|
3133
3380
|
}
|
|
3134
|
-
link(table,
|
|
3135
|
-
return this.meta(table).links.find((l) => l.name ===
|
|
3381
|
+
link(table, name2) {
|
|
3382
|
+
return this.meta(table).links.find((l) => l.name === name2);
|
|
3136
3383
|
}
|
|
3137
3384
|
fieldCandidates(table) {
|
|
3138
3385
|
const meta = this.meta(table);
|
|
@@ -3144,12 +3391,12 @@ var OperationLowerer = class {
|
|
|
3144
3391
|
];
|
|
3145
3392
|
}
|
|
3146
3393
|
/** Validate that a whole-variable argument is the declared §4f slot for this table. */
|
|
3147
|
-
slot(
|
|
3148
|
-
const variable = this.variables.get(
|
|
3394
|
+
slot(name2, input, table) {
|
|
3395
|
+
const variable = this.variables.get(name2);
|
|
3149
3396
|
if (!variable || variable.type.kind !== "inputRef" || variable.type.input !== input || variable.type.table !== table) {
|
|
3150
|
-
fail2(`variable $${
|
|
3397
|
+
fail2(`variable $${name2} must be declared as the ${input} input type for table '${table}'`, "slot-mismatch");
|
|
3151
3398
|
}
|
|
3152
|
-
return
|
|
3399
|
+
return name2;
|
|
3153
3400
|
}
|
|
3154
3401
|
// -------------------------------------------------------------------------
|
|
3155
3402
|
// Values
|
|
@@ -3325,13 +3572,13 @@ var OperationLowerer = class {
|
|
|
3325
3572
|
});
|
|
3326
3573
|
}
|
|
3327
3574
|
lowerLimit(node) {
|
|
3328
|
-
const
|
|
3329
|
-
if (!node) return
|
|
3330
|
-
if (node.kind === Kind.VARIABLE) return { ...
|
|
3575
|
+
const base2 = { default: DEFAULT_LIMIT, max: MAX_LIMIT };
|
|
3576
|
+
if (!node) return base2;
|
|
3577
|
+
if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3331
3578
|
if (node.kind === Kind.INT) {
|
|
3332
3579
|
const value = Number(node.value);
|
|
3333
3580
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3334
|
-
return { ...
|
|
3581
|
+
return { ...base2, value: Math.min(value, MAX_LIMIT) };
|
|
3335
3582
|
}
|
|
3336
3583
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3337
3584
|
}
|
|
@@ -3352,28 +3599,28 @@ var OperationLowerer = class {
|
|
|
3352
3599
|
if (node.directives && node.directives.length > 0) {
|
|
3353
3600
|
fail2(`@include/@skip are supported on root fields only ('${node.name.value}' is nested)`, "nested-directive");
|
|
3354
3601
|
}
|
|
3355
|
-
const
|
|
3356
|
-
if (
|
|
3357
|
-
const alias = node.alias?.value ??
|
|
3358
|
-
const column = this.column(table,
|
|
3602
|
+
const name2 = node.name.value;
|
|
3603
|
+
if (name2 === "__typename") continue;
|
|
3604
|
+
const alias = node.alias?.value ?? name2;
|
|
3605
|
+
const column = this.column(table, name2);
|
|
3359
3606
|
if (column) {
|
|
3360
3607
|
if (node.arguments !== void 0 && node.arguments.length > 0 || node.selectionSet !== void 0) {
|
|
3361
|
-
fail2(`column '${table}.${
|
|
3608
|
+
fail2(`column '${table}.${name2}' takes no arguments or sub-selection`, "invalid-selection");
|
|
3362
3609
|
}
|
|
3363
|
-
selection.columns.push({ alias, column:
|
|
3610
|
+
selection.columns.push({ alias, column: name2 });
|
|
3364
3611
|
continue;
|
|
3365
3612
|
}
|
|
3366
|
-
if (
|
|
3613
|
+
if (name2 === CELLS_FIELD && meta.cells) {
|
|
3367
3614
|
if (selection.cells) fail2(`'${CELLS_FIELD}' may be selected once per object`, "invalid-selection");
|
|
3368
3615
|
selection.cells = this.lowerCells(node, table, alias);
|
|
3369
3616
|
continue;
|
|
3370
3617
|
}
|
|
3371
|
-
const link = this.link(table,
|
|
3618
|
+
const link = this.link(table, name2);
|
|
3372
3619
|
if (link) {
|
|
3373
|
-
if (!node.selectionSet) fail2(`link '${table}.${
|
|
3620
|
+
if (!node.selectionSet) fail2(`link '${table}.${name2}' needs a selection set`, "invalid-selection");
|
|
3374
3621
|
const relation = {
|
|
3375
3622
|
alias,
|
|
3376
|
-
link:
|
|
3623
|
+
link: name2,
|
|
3377
3624
|
selection: this.lowerSelection(node.selectionSet, link.target)
|
|
3378
3625
|
};
|
|
3379
3626
|
if (link.kind === "toMany") {
|
|
@@ -3389,15 +3636,15 @@ var OperationLowerer = class {
|
|
|
3389
3636
|
if (offset) relation.offset = offset;
|
|
3390
3637
|
}
|
|
3391
3638
|
} else if (node.arguments && node.arguments.length > 0) {
|
|
3392
|
-
fail2(`to-one link '${table}.${
|
|
3639
|
+
fail2(`to-one link '${table}.${name2}' takes no arguments`, "invalid-selection");
|
|
3393
3640
|
}
|
|
3394
3641
|
selection.relations.push(relation);
|
|
3395
3642
|
continue;
|
|
3396
3643
|
}
|
|
3397
|
-
if (
|
|
3398
|
-
const aggLink = this.link(table,
|
|
3644
|
+
if (name2.endsWith("Aggregate")) {
|
|
3645
|
+
const aggLink = this.link(table, name2.slice(0, -"Aggregate".length));
|
|
3399
3646
|
if (aggLink && aggLink.kind === "toMany") {
|
|
3400
|
-
if (!node.selectionSet) fail2(`'${
|
|
3647
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
3401
3648
|
assertKnownArgs(node, ["where"]);
|
|
3402
3649
|
const whereArg = argsByName(node).get("where");
|
|
3403
3650
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, aggLink.target);
|
|
@@ -3410,7 +3657,7 @@ var OperationLowerer = class {
|
|
|
3410
3657
|
continue;
|
|
3411
3658
|
}
|
|
3412
3659
|
}
|
|
3413
|
-
fail2(`unknown field '${
|
|
3660
|
+
fail2(`unknown field '${name2}' on '${table}'`, "unknown-field", this.fieldCandidates(table));
|
|
3414
3661
|
}
|
|
3415
3662
|
return selection;
|
|
3416
3663
|
}
|
|
@@ -3466,13 +3713,13 @@ var OperationLowerer = class {
|
|
|
3466
3713
|
const result = {};
|
|
3467
3714
|
let sawAggregate = false;
|
|
3468
3715
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3469
|
-
const
|
|
3470
|
-
if (
|
|
3716
|
+
const name2 = node.name.value;
|
|
3717
|
+
if (name2 === "nodes") {
|
|
3471
3718
|
if (!node.selectionSet) fail2("'nodes' needs a selection set", "invalid-aggregate");
|
|
3472
3719
|
result.nodes = this.lowerSelection(node.selectionSet, table);
|
|
3473
3720
|
continue;
|
|
3474
3721
|
}
|
|
3475
|
-
if (
|
|
3722
|
+
if (name2 !== "aggregate") fail2(`unknown aggregate field '${name2}'`, "invalid-aggregate", ["aggregate", "nodes"]);
|
|
3476
3723
|
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3477
3724
|
sawAggregate = true;
|
|
3478
3725
|
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
@@ -3493,18 +3740,18 @@ var OperationLowerer = class {
|
|
|
3493
3740
|
let sawAggregate = false;
|
|
3494
3741
|
let sawKey = false;
|
|
3495
3742
|
for (const node of aggregateBlockFields(selectionSet)) {
|
|
3496
|
-
const
|
|
3497
|
-
if (
|
|
3743
|
+
const name2 = node.name.value;
|
|
3744
|
+
if (name2 === "key") {
|
|
3498
3745
|
if (sawKey) fail2("one key block per grouped aggregate", "invalid-aggregate");
|
|
3499
3746
|
sawKey = true;
|
|
3500
3747
|
if (!node.selectionSet) fail2("'key' needs a selection set", "invalid-aggregate");
|
|
3501
3748
|
result.key = this.lowerGroupKeySelection(node.selectionSet, table, groupColumns);
|
|
3502
3749
|
continue;
|
|
3503
3750
|
}
|
|
3504
|
-
if (
|
|
3751
|
+
if (name2 === "nodes") {
|
|
3505
3752
|
fail2("'nodes' is not available on a grouped aggregate \u2014 query the rows separately", "invalid-aggregate", ["key", "aggregate"]);
|
|
3506
3753
|
}
|
|
3507
|
-
if (
|
|
3754
|
+
if (name2 !== "aggregate") fail2(`unknown grouped-aggregate field '${name2}'`, "invalid-aggregate", ["key", "aggregate"]);
|
|
3508
3755
|
if (sawAggregate) fail2("one aggregate block per field \u2014 alias the root instead", "invalid-aggregate");
|
|
3509
3756
|
sawAggregate = true;
|
|
3510
3757
|
if (!node.selectionSet) fail2("'aggregate' needs a selection set", "invalid-aggregate");
|
|
@@ -3530,19 +3777,19 @@ var OperationLowerer = class {
|
|
|
3530
3777
|
const key = [];
|
|
3531
3778
|
for (const node of selectionSet.selections) {
|
|
3532
3779
|
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
3533
|
-
const
|
|
3534
|
-
if (
|
|
3535
|
-
if (!this.column(table,
|
|
3536
|
-
fail2(`unknown key column '${
|
|
3780
|
+
const name2 = node.name.value;
|
|
3781
|
+
if (name2 === "__typename") continue;
|
|
3782
|
+
if (!this.column(table, name2)) {
|
|
3783
|
+
fail2(`unknown key column '${name2}' on '${table}'`, "unknown-column", [...groupColumns]);
|
|
3537
3784
|
}
|
|
3538
|
-
if (!groupColumns.has(
|
|
3785
|
+
if (!groupColumns.has(name2)) {
|
|
3539
3786
|
fail2(
|
|
3540
|
-
`key column '${
|
|
3787
|
+
`key column '${name2}' is not one of this query's groupBy keys \u2014 it has no single value within a group`,
|
|
3541
3788
|
"invalid-group-key",
|
|
3542
3789
|
[...groupColumns]
|
|
3543
3790
|
);
|
|
3544
3791
|
}
|
|
3545
|
-
key.push({ alias: node.alias?.value ??
|
|
3792
|
+
key.push({ alias: node.alias?.value ?? name2, column: name2 });
|
|
3546
3793
|
}
|
|
3547
3794
|
return key;
|
|
3548
3795
|
}
|
|
@@ -3638,35 +3885,35 @@ var OperationLowerer = class {
|
|
|
3638
3885
|
let includeIf;
|
|
3639
3886
|
let omit = false;
|
|
3640
3887
|
for (const directive of node.directives ?? []) {
|
|
3641
|
-
const
|
|
3642
|
-
if (
|
|
3643
|
-
if (SOURCE_DIRECTIVES.has(
|
|
3644
|
-
if (
|
|
3645
|
-
fail2(`directive @${
|
|
3888
|
+
const name2 = directive.name.value;
|
|
3889
|
+
if (name2 === "assertAffectedRows") continue;
|
|
3890
|
+
if (SOURCE_DIRECTIVES.has(name2)) continue;
|
|
3891
|
+
if (name2 !== "include" && name2 !== "skip") {
|
|
3892
|
+
fail2(`directive @${name2} is not supported on fields`, "unknown-directive", ["include", "skip"]);
|
|
3646
3893
|
}
|
|
3647
3894
|
const ifArg = directive.arguments?.find((a) => a.name.value === "if");
|
|
3648
|
-
if (!ifArg) fail2(`@${
|
|
3895
|
+
if (!ifArg) fail2(`@${name2} needs an if: argument`, "invalid-directive");
|
|
3649
3896
|
if (ifArg.value.kind === Kind.BOOLEAN) {
|
|
3650
|
-
const keep =
|
|
3897
|
+
const keep = name2 === "include" ? ifArg.value.value : !ifArg.value.value;
|
|
3651
3898
|
if (!keep) omit = true;
|
|
3652
3899
|
continue;
|
|
3653
3900
|
}
|
|
3654
|
-
if (ifArg.value.kind !== Kind.VARIABLE) fail2(`@${
|
|
3901
|
+
if (ifArg.value.kind !== Kind.VARIABLE) fail2(`@${name2}(if:) must be a boolean literal or variable`, "invalid-directive");
|
|
3655
3902
|
if (includeIf) fail2("at most one of @include/@skip per root field", "invalid-directive");
|
|
3656
|
-
includeIf = { variable: ifArg.value.name.value, negated:
|
|
3903
|
+
includeIf = { variable: ifArg.value.name.value, negated: name2 === "skip" };
|
|
3657
3904
|
}
|
|
3658
3905
|
return omit ? { omit: true } : { includeIf, omit: false };
|
|
3659
3906
|
}
|
|
3660
3907
|
lowerQueryRoot(node) {
|
|
3661
|
-
const
|
|
3662
|
-
const alias = node.alias?.value ??
|
|
3663
|
-
const rootInfo = this.ctx.queryRoots.get(
|
|
3664
|
-
if (!rootInfo) fail2(`unknown query root '${
|
|
3908
|
+
const name2 = node.name.value;
|
|
3909
|
+
const alias = node.alias?.value ?? name2;
|
|
3910
|
+
const rootInfo = this.ctx.queryRoots.get(name2);
|
|
3911
|
+
if (!rootInfo) fail2(`unknown query root '${name2}'`, "unknown-root", [...this.ctx.queryRoots.keys()]);
|
|
3665
3912
|
const { table } = rootInfo;
|
|
3666
3913
|
const args = argsByName(node);
|
|
3667
3914
|
if (rootInfo.kind === "select") {
|
|
3668
3915
|
assertKnownArgs(node, ["where", "orderBy", "distinctOn", "limit", "offset"]);
|
|
3669
|
-
if (!node.selectionSet) fail2(`'${
|
|
3916
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
3670
3917
|
const where = args.get("where");
|
|
3671
3918
|
const orderBy = args.get("orderBy");
|
|
3672
3919
|
const distinctOn = args.get("distinctOn");
|
|
@@ -3685,7 +3932,7 @@ var OperationLowerer = class {
|
|
|
3685
3932
|
};
|
|
3686
3933
|
}
|
|
3687
3934
|
if (rootInfo.kind === "byPk") {
|
|
3688
|
-
if (!node.selectionSet) fail2(`'${
|
|
3935
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
3689
3936
|
const pk = this.lowerPkArgs(node, table);
|
|
3690
3937
|
return { kind: "byPk", alias, table, selection: this.lowerSelection(node.selectionSet, table), pk, where: CONST_TRUE };
|
|
3691
3938
|
}
|
|
@@ -3693,7 +3940,7 @@ var OperationLowerer = class {
|
|
|
3693
3940
|
return this.lowerAggregateGroupsRoot(node, { alias, table });
|
|
3694
3941
|
}
|
|
3695
3942
|
assertKnownArgs(node, ["where"]);
|
|
3696
|
-
if (!node.selectionSet) fail2(`'${
|
|
3943
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
3697
3944
|
const whereArg = args.get("where");
|
|
3698
3945
|
const aggregate = this.lowerAggregateSelection(node.selectionSet, table);
|
|
3699
3946
|
return {
|
|
@@ -3796,13 +4043,13 @@ var OperationLowerer = class {
|
|
|
3796
4043
|
}
|
|
3797
4044
|
/** Like lowerLimit, but bounded by MAX_GROUPS and defaulting to it. */
|
|
3798
4045
|
lowerGroupLimit(node) {
|
|
3799
|
-
const
|
|
3800
|
-
if (!node) return
|
|
3801
|
-
if (node.kind === Kind.VARIABLE) return { ...
|
|
4046
|
+
const base2 = { default: MAX_GROUPS, max: MAX_GROUPS };
|
|
4047
|
+
if (!node) return base2;
|
|
4048
|
+
if (node.kind === Kind.VARIABLE) return { ...base2, variable: node.name.value };
|
|
3802
4049
|
if (node.kind === Kind.INT) {
|
|
3803
4050
|
const value = Number(node.value);
|
|
3804
4051
|
if (value < 1) fail2("limit must be a positive integer", "invalid-limit");
|
|
3805
|
-
return { ...
|
|
4052
|
+
return { ...base2, value: Math.min(value, MAX_GROUPS) };
|
|
3806
4053
|
}
|
|
3807
4054
|
fail2("limit must be an integer literal or a variable", "invalid-limit");
|
|
3808
4055
|
}
|
|
@@ -3821,10 +4068,10 @@ var OperationLowerer = class {
|
|
|
3821
4068
|
// Mutations
|
|
3822
4069
|
// -------------------------------------------------------------------------
|
|
3823
4070
|
lowerMutationRoot(node) {
|
|
3824
|
-
const
|
|
3825
|
-
const alias = node.alias?.value ??
|
|
3826
|
-
const rootInfo = this.ctx.mutationRoots.get(
|
|
3827
|
-
if (!rootInfo) fail2(`unknown mutation root '${
|
|
4071
|
+
const name2 = node.name.value;
|
|
4072
|
+
const alias = node.alias?.value ?? name2;
|
|
4073
|
+
const rootInfo = this.ctx.mutationRoots.get(name2);
|
|
4074
|
+
if (!rootInfo) fail2(`unknown mutation root '${name2}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
3828
4075
|
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
3829
4076
|
return this.lowerCowRoot(node, rootInfo);
|
|
3830
4077
|
}
|
|
@@ -3833,16 +4080,16 @@ var OperationLowerer = class {
|
|
|
3833
4080
|
}
|
|
3834
4081
|
const { op, table } = rootInfo;
|
|
3835
4082
|
const args = argsByName(node);
|
|
3836
|
-
if (!node.selectionSet) fail2(`'${
|
|
3837
|
-
const
|
|
4083
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
4084
|
+
const base2 = { kind: "mutation", op, alias, table, pinnedColumns: [], affectedRows: false };
|
|
3838
4085
|
switch (op) {
|
|
3839
4086
|
case "insert": {
|
|
3840
4087
|
assertKnownArgs(node, ["objects", "onConflict"]);
|
|
3841
4088
|
const objects = args.get("objects");
|
|
3842
|
-
if (!objects) fail2(`'${
|
|
4089
|
+
if (!objects) fail2(`'${name2}' needs objects:`, "missing-argument");
|
|
3843
4090
|
const response = this.lowerMutationResponse(node.selectionSet, table);
|
|
3844
4091
|
return {
|
|
3845
|
-
...
|
|
4092
|
+
...base2,
|
|
3846
4093
|
...response,
|
|
3847
4094
|
objects: this.lowerInsertObjects(objects.value, table, { list: true }),
|
|
3848
4095
|
...this.lowerOnConflictArg(args.get("onConflict"), table)
|
|
@@ -3851,9 +4098,9 @@ var OperationLowerer = class {
|
|
|
3851
4098
|
case "insertOne": {
|
|
3852
4099
|
assertKnownArgs(node, ["object", "onConflict"]);
|
|
3853
4100
|
const object = args.get("object");
|
|
3854
|
-
if (!object) fail2(`'${
|
|
4101
|
+
if (!object) fail2(`'${name2}' needs object:`, "missing-argument");
|
|
3855
4102
|
return {
|
|
3856
|
-
...
|
|
4103
|
+
...base2,
|
|
3857
4104
|
objects: this.lowerInsertObjects(object.value, table, { list: false }),
|
|
3858
4105
|
...this.lowerOnConflictArg(args.get("onConflict"), table),
|
|
3859
4106
|
returning: this.lowerSelection(node.selectionSet, table)
|
|
@@ -3862,9 +4109,9 @@ var OperationLowerer = class {
|
|
|
3862
4109
|
case "update": {
|
|
3863
4110
|
assertKnownArgs(node, ["where", "_set", "_append", "_inc"]);
|
|
3864
4111
|
const where = args.get("where");
|
|
3865
|
-
if (!where) fail2(`'${
|
|
4112
|
+
if (!where) fail2(`'${name2}' needs where: (bulk updates without a filter are not allowed)`, "missing-argument");
|
|
3866
4113
|
return {
|
|
3867
|
-
...
|
|
4114
|
+
...base2,
|
|
3868
4115
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
3869
4116
|
where: this.lowerBoolExp(where.value, table),
|
|
3870
4117
|
...this.lowerWriteValues(args, table)
|
|
@@ -3873,9 +4120,9 @@ var OperationLowerer = class {
|
|
|
3873
4120
|
case "updateByPk": {
|
|
3874
4121
|
assertKnownArgs(node, ["pkColumns", "_set", "_append", "_inc"]);
|
|
3875
4122
|
const pkColumns = args.get("pkColumns");
|
|
3876
|
-
if (!pkColumns) fail2(`'${
|
|
4123
|
+
if (!pkColumns) fail2(`'${name2}' needs pkColumns:`, "missing-argument");
|
|
3877
4124
|
return {
|
|
3878
|
-
...
|
|
4125
|
+
...base2,
|
|
3879
4126
|
pk: this.lowerPkColumnsArg(pkColumns.value, table),
|
|
3880
4127
|
...this.lowerWriteValues(args, table),
|
|
3881
4128
|
returning: this.lowerSelection(node.selectionSet, table)
|
|
@@ -3884,16 +4131,16 @@ var OperationLowerer = class {
|
|
|
3884
4131
|
case "delete": {
|
|
3885
4132
|
assertKnownArgs(node, ["where"]);
|
|
3886
4133
|
const where = args.get("where");
|
|
3887
|
-
if (!where) fail2(`'${
|
|
4134
|
+
if (!where) fail2(`'${name2}' needs where: (bulk deletes without a filter are not allowed)`, "missing-argument");
|
|
3888
4135
|
return {
|
|
3889
|
-
...
|
|
4136
|
+
...base2,
|
|
3890
4137
|
...this.lowerMutationResponse(node.selectionSet, table),
|
|
3891
4138
|
where: this.lowerBoolExp(where.value, table)
|
|
3892
4139
|
};
|
|
3893
4140
|
}
|
|
3894
4141
|
case "deleteByPk": {
|
|
3895
4142
|
return {
|
|
3896
|
-
...
|
|
4143
|
+
...base2,
|
|
3897
4144
|
pk: this.lowerPkArgs(node, table),
|
|
3898
4145
|
returning: this.lowerSelection(node.selectionSet, table)
|
|
3899
4146
|
};
|
|
@@ -3908,16 +4155,16 @@ var OperationLowerer = class {
|
|
|
3908
4155
|
* the structured fields, never from authored statements.
|
|
3909
4156
|
*/
|
|
3910
4157
|
lowerCowRoot(node, rootInfo) {
|
|
3911
|
-
const
|
|
3912
|
-
const alias = node.alias?.value ??
|
|
4158
|
+
const name2 = node.name.value;
|
|
4159
|
+
const alias = node.alias?.value ?? name2;
|
|
3913
4160
|
const { table } = rootInfo;
|
|
3914
|
-
if (!node.selectionSet) fail2(`'${
|
|
4161
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
3915
4162
|
const meta = this.meta(table);
|
|
3916
4163
|
const args = argsByName(node);
|
|
3917
4164
|
const pkArg = args.get(rootInfo.pk);
|
|
3918
|
-
if (!pkArg) fail2(`'${
|
|
4165
|
+
if (!pkArg) fail2(`'${name2}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
3919
4166
|
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
3920
|
-
const
|
|
4167
|
+
const base2 = {
|
|
3921
4168
|
kind: "mutation",
|
|
3922
4169
|
op: rootInfo.op,
|
|
3923
4170
|
alias,
|
|
@@ -3930,14 +4177,14 @@ var OperationLowerer = class {
|
|
|
3930
4177
|
};
|
|
3931
4178
|
if (rootInfo.op === "cowRevert") {
|
|
3932
4179
|
assertKnownArgs(node, [rootInfo.pk]);
|
|
3933
|
-
return
|
|
4180
|
+
return base2;
|
|
3934
4181
|
}
|
|
3935
4182
|
assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
|
|
3936
4183
|
const entries = rootInfo.set.map((column) => {
|
|
3937
4184
|
const arg = args.get(column);
|
|
3938
4185
|
if (!arg) {
|
|
3939
4186
|
fail2(
|
|
3940
|
-
`'${
|
|
4187
|
+
`'${name2}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
3941
4188
|
"missing-argument",
|
|
3942
4189
|
[...rootInfo.set]
|
|
3943
4190
|
);
|
|
@@ -3947,7 +4194,7 @@ var OperationLowerer = class {
|
|
|
3947
4194
|
}
|
|
3948
4195
|
return { column, value: this.lowerValue(arg.value) };
|
|
3949
4196
|
});
|
|
3950
|
-
return { ...
|
|
4197
|
+
return { ...base2, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
3951
4198
|
}
|
|
3952
4199
|
/**
|
|
3953
4200
|
* Resolved-table roots: insertOne(object), updateByPk(pkColumns, _set),
|
|
@@ -3955,12 +4202,12 @@ var OperationLowerer = class {
|
|
|
3955
4202
|
* the touched cells into the manual source; `returning` reads the view.
|
|
3956
4203
|
*/
|
|
3957
4204
|
lowerResolvedRoot(node, rootInfo) {
|
|
3958
|
-
const
|
|
3959
|
-
const alias = node.alias?.value ??
|
|
4205
|
+
const name2 = node.name.value;
|
|
4206
|
+
const alias = node.alias?.value ?? name2;
|
|
3960
4207
|
const { table, action } = rootInfo;
|
|
3961
|
-
if (!node.selectionSet) fail2(`'${
|
|
4208
|
+
if (!node.selectionSet) fail2(`'${name2}' needs a selection set`, "invalid-selection");
|
|
3962
4209
|
const args = argsByName(node);
|
|
3963
|
-
const
|
|
4210
|
+
const base2 = {
|
|
3964
4211
|
kind: "mutation",
|
|
3965
4212
|
op: "resolvedWrite",
|
|
3966
4213
|
resolvedAction: action,
|
|
@@ -3974,32 +4221,32 @@ var OperationLowerer = class {
|
|
|
3974
4221
|
case "insert": {
|
|
3975
4222
|
assertKnownArgs(node, ["object"]);
|
|
3976
4223
|
const object = args.get("object");
|
|
3977
|
-
if (!object) fail2(`'${
|
|
3978
|
-
return { ...
|
|
4224
|
+
if (!object) fail2(`'${name2}' needs object:`, "missing-argument");
|
|
4225
|
+
return { ...base2, objects: this.lowerInsertObjects(object.value, table, { list: false }) };
|
|
3979
4226
|
}
|
|
3980
4227
|
case "update": {
|
|
3981
4228
|
assertKnownArgs(node, ["pkColumns", "_set"]);
|
|
3982
4229
|
const pkColumns = args.get("pkColumns");
|
|
3983
|
-
if (!pkColumns || !args.get("_set")) fail2(`'${
|
|
3984
|
-
return { ...
|
|
4230
|
+
if (!pkColumns || !args.get("_set")) fail2(`'${name2}' needs pkColumns: and _set:`, "missing-argument");
|
|
4231
|
+
return { ...base2, pk: this.lowerPkColumnsArg(pkColumns.value, table), ...this.lowerWriteValues(args, table) };
|
|
3985
4232
|
}
|
|
3986
4233
|
case "delete":
|
|
3987
|
-
return { ...
|
|
4234
|
+
return { ...base2, pk: this.lowerPkArgs(node, table) };
|
|
3988
4235
|
case "revert": {
|
|
3989
4236
|
assertKnownArgs(node, ["pkColumns", "columns"]);
|
|
3990
4237
|
const pkColumns = args.get("pkColumns");
|
|
3991
4238
|
const columns = args.get("columns");
|
|
3992
|
-
if (!pkColumns || !columns) fail2(`'${
|
|
4239
|
+
if (!pkColumns || !columns) fail2(`'${name2}' needs pkColumns: and columns:`, "missing-argument");
|
|
3993
4240
|
const items = columns.value.kind === Kind.LIST ? columns.value.values : [columns.value];
|
|
3994
4241
|
const meta = this.meta(table);
|
|
3995
4242
|
const revertColumns = items.map((item) => {
|
|
3996
4243
|
if (item.kind !== Kind.ENUM || !meta.settable.includes(item.value)) {
|
|
3997
|
-
fail2(`'${
|
|
4244
|
+
fail2(`'${name2}' columns: must list update-column enum values written in the document`, "invalid-revert-columns", meta.settable);
|
|
3998
4245
|
}
|
|
3999
4246
|
return item.value;
|
|
4000
4247
|
});
|
|
4001
|
-
if (revertColumns.length === 0) fail2(`'${
|
|
4002
|
-
return { ...
|
|
4248
|
+
if (revertColumns.length === 0) fail2(`'${name2}' columns: must name at least one column`, "invalid-revert-columns");
|
|
4249
|
+
return { ...base2, pk: this.lowerPkColumnsArg(pkColumns.value, table), revertColumns: [...new Set(revertColumns)] };
|
|
4003
4250
|
}
|
|
4004
4251
|
}
|
|
4005
4252
|
}
|
|
@@ -4008,24 +4255,24 @@ var OperationLowerer = class {
|
|
|
4008
4255
|
let returning = null;
|
|
4009
4256
|
for (const node of selectionSet.selections) {
|
|
4010
4257
|
if (node.kind !== Kind.FIELD) fail2("fragments must be flattened before lowering", "internal");
|
|
4011
|
-
const
|
|
4012
|
-
if (
|
|
4013
|
-
if (
|
|
4258
|
+
const name2 = node.name.value;
|
|
4259
|
+
if (name2 === "__typename") continue;
|
|
4260
|
+
if (name2 === "affectedRows") {
|
|
4014
4261
|
affectedRows = true;
|
|
4015
4262
|
continue;
|
|
4016
4263
|
}
|
|
4017
|
-
if (
|
|
4264
|
+
if (name2 === "returning") {
|
|
4018
4265
|
if (!node.selectionSet) fail2("'returning' needs a selection set", "invalid-selection");
|
|
4019
4266
|
returning = this.lowerSelection(node.selectionSet, table);
|
|
4020
4267
|
continue;
|
|
4021
4268
|
}
|
|
4022
|
-
fail2(`unknown mutation-response field '${
|
|
4269
|
+
fail2(`unknown mutation-response field '${name2}'`, "invalid-selection", ["affectedRows", "returning"]);
|
|
4023
4270
|
}
|
|
4024
4271
|
return { affectedRows, returning };
|
|
4025
4272
|
}
|
|
4026
|
-
lowerInsertObjects(node, table, { list }) {
|
|
4273
|
+
lowerInsertObjects(node, table, { list: list2 }) {
|
|
4027
4274
|
if (node.kind === Kind.VARIABLE) return { slot: this.slot(node.name.value, "insertInput", table) };
|
|
4028
|
-
if (
|
|
4275
|
+
if (list2) {
|
|
4029
4276
|
if (node.kind !== Kind.LIST) fail2(`objects: must be a list literal or a variable`, "invalid-insert");
|
|
4030
4277
|
return { rows: node.values.map((item) => this.lowerInsertRow(item, table)) };
|
|
4031
4278
|
}
|
|
@@ -4132,9 +4379,9 @@ function lowerOperation({ restricted, ctx }) {
|
|
|
4132
4379
|
const lowerer = new OperationLowerer(ctx, restricted);
|
|
4133
4380
|
return lowerer.lowerRoots(restricted.definition, restricted.kind);
|
|
4134
4381
|
}
|
|
4135
|
-
function isCellColumn(meta,
|
|
4382
|
+
function isCellColumn(meta, name2) {
|
|
4136
4383
|
if (!meta.cells) return false;
|
|
4137
|
-
return Object.values(meta.cells).some((parts) => Object.values(parts).includes(
|
|
4384
|
+
return Object.values(meta.cells).some((parts) => Object.values(parts).includes(name2));
|
|
4138
4385
|
}
|
|
4139
4386
|
function fieldNodes(selectionSet) {
|
|
4140
4387
|
const out = [];
|
|
@@ -4191,7 +4438,7 @@ function parseDocuments({
|
|
|
4191
4438
|
}
|
|
4192
4439
|
function unwrapType(node, variable) {
|
|
4193
4440
|
let nullable = true;
|
|
4194
|
-
let
|
|
4441
|
+
let list2 = false;
|
|
4195
4442
|
let current = node;
|
|
4196
4443
|
for (; ; ) {
|
|
4197
4444
|
if (current.kind === Kind.NON_NULL_TYPE) {
|
|
@@ -4200,17 +4447,17 @@ function unwrapType(node, variable) {
|
|
|
4200
4447
|
continue;
|
|
4201
4448
|
}
|
|
4202
4449
|
if (current.kind === Kind.LIST_TYPE) {
|
|
4203
|
-
if (
|
|
4450
|
+
if (list2) {
|
|
4204
4451
|
throw new DocumentCompileError(`variable $${variable}: nested list types are not supported`, "unsupported-variable-type");
|
|
4205
4452
|
}
|
|
4206
|
-
|
|
4453
|
+
list2 = true;
|
|
4207
4454
|
current = current.type;
|
|
4208
4455
|
continue;
|
|
4209
4456
|
}
|
|
4210
|
-
return { named: current.name.value, list, nullable };
|
|
4457
|
+
return { named: current.name.value, list: list2, nullable };
|
|
4211
4458
|
}
|
|
4212
4459
|
}
|
|
4213
|
-
function classifyVariableType({ named, list }, variable, ctx) {
|
|
4460
|
+
function classifyVariableType({ named, list: list2 }, variable, ctx) {
|
|
4214
4461
|
const inputRef = classifyInputType(named, ctx.tableByTypename);
|
|
4215
4462
|
if (inputRef) {
|
|
4216
4463
|
const listable = {
|
|
@@ -4223,13 +4470,13 @@ function classifyVariableType({ named, list }, variable, ctx) {
|
|
|
4223
4470
|
insertInput: "either"
|
|
4224
4471
|
};
|
|
4225
4472
|
const rule = listable[inputRef.input];
|
|
4226
|
-
if (rule === "must" && !
|
|
4473
|
+
if (rule === "must" && !list2) {
|
|
4227
4474
|
throw new DocumentCompileError(
|
|
4228
4475
|
`variable $${variable}: ${named} must be used as a list (e.g. [${named}!])`,
|
|
4229
4476
|
"unsupported-variable-type"
|
|
4230
4477
|
);
|
|
4231
4478
|
}
|
|
4232
|
-
if (rule === "never" &&
|
|
4479
|
+
if (rule === "never" && list2) {
|
|
4233
4480
|
throw new DocumentCompileError(
|
|
4234
4481
|
`variable $${variable}: ${named} cannot be used as a list`,
|
|
4235
4482
|
"unsupported-variable-type"
|
|
@@ -4266,7 +4513,7 @@ function restrictDefault({
|
|
|
4266
4513
|
node,
|
|
4267
4514
|
variable,
|
|
4268
4515
|
type,
|
|
4269
|
-
list,
|
|
4516
|
+
list: list2,
|
|
4270
4517
|
nullable
|
|
4271
4518
|
}) {
|
|
4272
4519
|
if (type.kind === "inputRef") {
|
|
@@ -4286,7 +4533,7 @@ function restrictDefault({
|
|
|
4286
4533
|
return { value: null };
|
|
4287
4534
|
}
|
|
4288
4535
|
if (Array.isArray(value)) {
|
|
4289
|
-
if (!
|
|
4536
|
+
if (!list2) {
|
|
4290
4537
|
throw new DocumentCompileError(
|
|
4291
4538
|
`variable $${variable}: default for a non-list variable must not be a list`,
|
|
4292
4539
|
"unsupported-variable-default"
|
|
@@ -4294,7 +4541,7 @@ function restrictDefault({
|
|
|
4294
4541
|
}
|
|
4295
4542
|
return { value: value.map((item) => restrictDefaultLiteral(item, variable, type)) };
|
|
4296
4543
|
}
|
|
4297
|
-
if (
|
|
4544
|
+
if (list2) {
|
|
4298
4545
|
throw new DocumentCompileError(
|
|
4299
4546
|
`variable $${variable}: default for a list variable must be a list`,
|
|
4300
4547
|
"unsupported-variable-default"
|
|
@@ -4329,8 +4576,8 @@ function restrictOperation({
|
|
|
4329
4576
|
if (operation.operation === "subscription") {
|
|
4330
4577
|
throw new DocumentCompileError("subscriptions are not supported in v2.0 \u2014 poll a query instead", "subscription-unsupported");
|
|
4331
4578
|
}
|
|
4332
|
-
const
|
|
4333
|
-
if (!
|
|
4579
|
+
const name2 = operation.name?.value;
|
|
4580
|
+
if (!name2) {
|
|
4334
4581
|
throw new DocumentCompileError(
|
|
4335
4582
|
"anonymous operations are not supported \u2014 every operation must be named (the name keys the registry)",
|
|
4336
4583
|
"anonymous-operation"
|
|
@@ -4339,14 +4586,14 @@ function restrictOperation({
|
|
|
4339
4586
|
const variables = (operation.variableDefinitions ?? []).map((definition) => {
|
|
4340
4587
|
const variable = definition.variable.name.value;
|
|
4341
4588
|
const unwrapped = unwrapType(definition.type, variable);
|
|
4342
|
-
const { list, nullable } = unwrapped;
|
|
4589
|
+
const { list: list2, nullable } = unwrapped;
|
|
4343
4590
|
const type = classifyVariableType(unwrapped, variable, ctx);
|
|
4344
|
-
const ir = { name: variable, type, nullable, list };
|
|
4591
|
+
const ir = { name: variable, type, nullable, list: list2 };
|
|
4345
4592
|
if (definition.defaultValue === void 0) return ir;
|
|
4346
|
-
return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list, nullable }) };
|
|
4593
|
+
return { ...ir, defaultValue: restrictDefault({ node: definition.defaultValue, variable, type, list: list2, nullable }) };
|
|
4347
4594
|
});
|
|
4348
4595
|
return {
|
|
4349
|
-
name,
|
|
4596
|
+
name: name2,
|
|
4350
4597
|
kind: operation.operation === "mutation" ? "mutation" : "query",
|
|
4351
4598
|
requires: parseRequires(operation),
|
|
4352
4599
|
variables,
|
|
@@ -4384,8 +4631,20 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
4384
4631
|
}
|
|
4385
4632
|
const loweredRoots = lowerOperation({ restricted: { ...restricted, definition: flattened }, ctx });
|
|
4386
4633
|
const { roots, tableSet } = injectPolicies({ roots: loweredRoots, ctx });
|
|
4634
|
+
for (const table of tableSet) {
|
|
4635
|
+
const meta = ctx.tables[table];
|
|
4636
|
+
if (meta.namespace && meta.namespace !== ctx.namespace && !meta.exportRead) {
|
|
4637
|
+
throw new DocumentCompileError(`object '${meta.namespace}/${table}' is not exported for reads`, "unexported-object");
|
|
4638
|
+
}
|
|
4639
|
+
}
|
|
4640
|
+
for (const root of roots) {
|
|
4641
|
+
const meta = ctx.tables[root.table];
|
|
4642
|
+
if (root.kind === "mutation" && meta.namespace && meta.namespace !== ctx.namespace && !meta.exportWrite) {
|
|
4643
|
+
throw new DocumentCompileError(`object '${meta.namespace}/${root.table}' is not exported for writes`, "unexported-object");
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4387
4646
|
operations[restricted.name] = {
|
|
4388
|
-
irVersion: roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ?
|
|
4647
|
+
irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : roots.some((root) => root.kind === "mutation" && root.assertAffectedRows !== void 0) ? GUARDED_IR_VERSION : LEGACY_IR_VERSION,
|
|
4389
4648
|
name: restricted.name,
|
|
4390
4649
|
opHash: computeOpHash({ definition: flattened, schemaHash }),
|
|
4391
4650
|
kind: restricted.kind,
|
|
@@ -4412,9 +4671,14 @@ function compileNamespace({ canonical, sdl, documents }) {
|
|
|
4412
4671
|
}
|
|
4413
4672
|
if (diagnostics.some((d) => d.severity === "error")) return { ok: false, diagnostics };
|
|
4414
4673
|
const artifact = {
|
|
4415
|
-
irVersion: requiredArtifactIrVersion(Object.values(operations)),
|
|
4674
|
+
irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : requiredArtifactIrVersion(Object.values(operations)),
|
|
4416
4675
|
namespace: ctx.namespace,
|
|
4417
4676
|
schemaHash,
|
|
4677
|
+
ownSchemaHash: computeSchemaHash(localContract(canonical)),
|
|
4678
|
+
...canonical.imports?.length ? {
|
|
4679
|
+
dependencies: Object.fromEntries(canonical.imports.map((owner) => [owner.namespace, computeSchemaHash(localContract(owner))]))
|
|
4680
|
+
} : {},
|
|
4681
|
+
readPolicies: Object.fromEntries(ctx.readPolicies),
|
|
4418
4682
|
opsHash: computeOpsHash(Object.values(operations).map((op) => op.opHash)),
|
|
4419
4683
|
tables: ctx.tables,
|
|
4420
4684
|
operations
|
|
@@ -4477,9 +4741,10 @@ function allowedOps(type) {
|
|
|
4477
4741
|
case "text":
|
|
4478
4742
|
return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
|
|
4479
4743
|
case "uuid":
|
|
4480
|
-
return ["eq", "neq", "in", "nin", "isNull"];
|
|
4744
|
+
return ["eq", "neq", "in", "nin", "like", "ilike", "isNull"];
|
|
4481
4745
|
case "integer":
|
|
4482
4746
|
case "bigint":
|
|
4747
|
+
return ["eq", "neq", "gt", "gte", "lt", "lte", "in", "nin", "like", "ilike", "isNull"];
|
|
4483
4748
|
case "numeric":
|
|
4484
4749
|
case "double":
|
|
4485
4750
|
case "timestamptz":
|
|
@@ -4697,12 +4962,12 @@ function validateSelectColumns({ table, value }) {
|
|
|
4697
4962
|
function validateInsertInput({
|
|
4698
4963
|
table,
|
|
4699
4964
|
value,
|
|
4700
|
-
list,
|
|
4965
|
+
list: list2,
|
|
4701
4966
|
maxRows
|
|
4702
4967
|
}) {
|
|
4703
4968
|
const collector = new IssueCollector();
|
|
4704
4969
|
const rowCap = maxRows ?? 1e3;
|
|
4705
|
-
const rows =
|
|
4970
|
+
const rows = list2 ? value : [value];
|
|
4706
4971
|
if (!Array.isArray(rows)) {
|
|
4707
4972
|
collector.add("invalid-shape", "$", "objects must be an array of rows");
|
|
4708
4973
|
return { ok: false, issues: collector.issues };
|
|
@@ -4713,7 +4978,7 @@ function validateInsertInput({
|
|
|
4713
4978
|
}
|
|
4714
4979
|
const normalized = [];
|
|
4715
4980
|
rows.forEach((row, index) => {
|
|
4716
|
-
const path =
|
|
4981
|
+
const path = list2 ? `$[${index}]` : "$";
|
|
4717
4982
|
if (!isPlainObject(row)) {
|
|
4718
4983
|
collector.add("invalid-shape", path, "insert rows must be objects");
|
|
4719
4984
|
return;
|
|
@@ -4823,6 +5088,744 @@ function validateOnConflictInput({
|
|
|
4823
5088
|
return finish(collector, { constraint, updateColumns });
|
|
4824
5089
|
}
|
|
4825
5090
|
|
|
4826
|
-
|
|
5091
|
+
// src/engine/types.ts
|
|
5092
|
+
var SQL_TYPE_NAMES = {
|
|
5093
|
+
double: "double precision",
|
|
5094
|
+
timestamptz: "timestamp with time zone"
|
|
5095
|
+
};
|
|
5096
|
+
var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
5097
|
+
|
|
5098
|
+
// src/compiler/pred-ir-sql.ts
|
|
5099
|
+
function compilePredIrToSql(pred, options = {}) {
|
|
5100
|
+
switch (pred.kind) {
|
|
5101
|
+
case "const":
|
|
5102
|
+
return pred.value ? "TRUE" : "FALSE";
|
|
5103
|
+
case "and":
|
|
5104
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
|
|
5105
|
+
case "or":
|
|
5106
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
|
|
5107
|
+
case "not":
|
|
5108
|
+
return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
|
|
5109
|
+
case "cmp":
|
|
5110
|
+
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
5111
|
+
case "capability":
|
|
5112
|
+
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5113
|
+
case "role":
|
|
5114
|
+
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
5115
|
+
case "exists":
|
|
5116
|
+
if (options.exists) return options.exists(pred);
|
|
5117
|
+
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
5118
|
+
case "aggPredicate":
|
|
5119
|
+
case "dynamicFilter":
|
|
5120
|
+
throw new OrmDefinitionError(
|
|
5121
|
+
`synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
|
|
5122
|
+
);
|
|
5123
|
+
default: {
|
|
5124
|
+
const exhausted = pred;
|
|
5125
|
+
throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
5129
|
+
function physicalColumn(logical, options) {
|
|
5130
|
+
return options.columnNames?.[logical] ?? logical;
|
|
5131
|
+
}
|
|
5132
|
+
function compileCmp(op, column, value, options) {
|
|
5133
|
+
const physical = physicalColumn(column, options);
|
|
5134
|
+
const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
|
|
5135
|
+
const isEnum = typeof declared === "object" && declared !== null;
|
|
5136
|
+
const qualified = options.alias ? `${quoteIdentifier(options.alias)}.${quoteIdentifier(physical)}` : quoteIdentifier(physical);
|
|
5137
|
+
const col = isEnum ? `${qualified}::text` : qualified;
|
|
5138
|
+
const columnType = isEnum ? "text" : declared;
|
|
5139
|
+
switch (op) {
|
|
5140
|
+
case "eq":
|
|
5141
|
+
return compileEquality(col, value, columnType, false);
|
|
5142
|
+
case "neq":
|
|
5143
|
+
return compileEquality(col, value, columnType, true);
|
|
5144
|
+
case "gt":
|
|
5145
|
+
return `${col} > ${compileValue(value, columnType)}`;
|
|
5146
|
+
case "gte":
|
|
5147
|
+
return `${col} >= ${compileValue(value, columnType)}`;
|
|
5148
|
+
case "lt":
|
|
5149
|
+
return `${col} < ${compileValue(value, columnType)}`;
|
|
5150
|
+
case "lte":
|
|
5151
|
+
return `${col} <= ${compileValue(value, columnType)}`;
|
|
5152
|
+
case "in":
|
|
5153
|
+
return `${col} = ANY(${compileArrayValue(value, columnType)})`;
|
|
5154
|
+
case "nin":
|
|
5155
|
+
return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
|
|
5156
|
+
case "like":
|
|
5157
|
+
return `${col} LIKE ${compileValue(value, columnType)}`;
|
|
5158
|
+
case "ilike":
|
|
5159
|
+
return `${col} ILIKE ${compileValue(value, columnType)}`;
|
|
5160
|
+
case "isNull":
|
|
5161
|
+
return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
5162
|
+
case "contains":
|
|
5163
|
+
case "hasKey":
|
|
5164
|
+
throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
|
|
5165
|
+
default: {
|
|
5166
|
+
const exhausted = op;
|
|
5167
|
+
throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
function compileEquality(col, value, columnType, negated, options) {
|
|
5172
|
+
if (value.kind === "literal" && value.value === null) {
|
|
5173
|
+
return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
|
|
5174
|
+
}
|
|
5175
|
+
const rhs = compileValue(value, columnType);
|
|
5176
|
+
return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
|
|
5177
|
+
}
|
|
5178
|
+
function compileValue(value, columnType, _options) {
|
|
5179
|
+
switch (value.kind) {
|
|
5180
|
+
case "literal": {
|
|
5181
|
+
const literal = value.value;
|
|
5182
|
+
if (literal === null) return "NULL";
|
|
5183
|
+
if (typeof literal === "string") return quoteLiteral(literal);
|
|
5184
|
+
if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
|
|
5185
|
+
return String(literal);
|
|
5186
|
+
}
|
|
5187
|
+
case "claim": {
|
|
5188
|
+
const expression = claimExpression(value.key);
|
|
5189
|
+
return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
|
|
5190
|
+
}
|
|
5191
|
+
case "variable":
|
|
5192
|
+
throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
|
|
5193
|
+
default: {
|
|
5194
|
+
const exhausted = value;
|
|
5195
|
+
throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
|
|
5196
|
+
}
|
|
5197
|
+
}
|
|
5198
|
+
}
|
|
5199
|
+
function compileArrayValue(value, columnType, _options) {
|
|
5200
|
+
if (value.kind !== "literal" || !Array.isArray(value.value)) {
|
|
5201
|
+
throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
|
|
5202
|
+
}
|
|
5203
|
+
const entries = value.value.map((entry) => {
|
|
5204
|
+
if (entry === null) return "NULL";
|
|
5205
|
+
if (typeof entry === "string") return quoteLiteral(entry);
|
|
5206
|
+
if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
|
|
5207
|
+
return String(entry);
|
|
5208
|
+
});
|
|
5209
|
+
const array = `ARRAY[${entries.join(", ")}]`;
|
|
5210
|
+
return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
|
|
5211
|
+
}
|
|
5212
|
+
|
|
5213
|
+
// src/sql/catalog.ts
|
|
5214
|
+
function sqlHash(value) {
|
|
5215
|
+
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
5216
|
+
}
|
|
5217
|
+
function createSqlCatalog({ canonical, publishedObjects, executionTarget }) {
|
|
5218
|
+
if (!publishedObjects.length || new Set(publishedObjects).size !== publishedObjects.length) throw new Error("SQL objects must be explicitly declared and unique");
|
|
5219
|
+
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
5220
|
+
if (diagnostics.some((d) => d.severity === "error")) throw new Error(diagnostics.map((d) => d.message).join("; "));
|
|
5221
|
+
const identity = sqlHash(`ontology-sql-v1\0${computeSchemaHash(canonical)}\0${executionTarget}`);
|
|
5222
|
+
const refOf = (meta) => `${meta.namespace ?? canonical.namespace}/${meta.table}`;
|
|
5223
|
+
const definitions = [canonical, ...canonical.imports ?? []];
|
|
5224
|
+
const internal = new Set(definitions.flatMap((owner) => [
|
|
5225
|
+
...owner.tables.filter((t) => t.internal).map((t) => `${owner.namespace}/${t.table}`),
|
|
5226
|
+
...(owner.views ?? []).filter((v) => v.internal).map((v) => `${owner.namespace}/${v.view}`)
|
|
5227
|
+
]));
|
|
5228
|
+
const objects = publishedObjects.map((ref) => {
|
|
5229
|
+
const meta = Object.values(ctx.tables).find((table) => refOf(table) === ref);
|
|
5230
|
+
if (!meta || internal.has(ref) || meta.namespace !== canonical.namespace && !meta.exportRead) throw new Error(`SQL object not published: ${ref}`);
|
|
5231
|
+
const reads = /* @__PURE__ */ new Set([ref]);
|
|
5232
|
+
let sequence = 0;
|
|
5233
|
+
const predicate = (table, pred, alias) => compilePredIrToSql(pred, {
|
|
5234
|
+
alias,
|
|
5235
|
+
namespace: table.namespace ?? canonical.namespace,
|
|
5236
|
+
columnNames: Object.fromEntries(table.columns.map((c) => [c.name, c.columnName])),
|
|
5237
|
+
columnTypes: Object.fromEntries(table.columns.map((c) => [c.name, c.type])),
|
|
5238
|
+
exists: (node) => {
|
|
5239
|
+
const link = table.links.find((candidate) => candidate.name === node.link);
|
|
5240
|
+
const target = link?.target ? ctx.tables[link.target] : void 0;
|
|
5241
|
+
if (!link || !target) throw new Error("SQL policy link is unavailable");
|
|
5242
|
+
reads.add(refOf(target));
|
|
5243
|
+
const child = `p${++sequence}`;
|
|
5244
|
+
const col = (t, a, name2) => {
|
|
5245
|
+
const c = t.columns.find((c2) => c2.name === name2 || c2.columnName === name2);
|
|
5246
|
+
if (!c) throw new Error("SQL policy column is unavailable");
|
|
5247
|
+
return `${quoteIdentifier(a)}.${quoteIdentifier(c.columnName)}`;
|
|
5248
|
+
};
|
|
5249
|
+
const join = link.kind === "toMany" ? `${col(target, child, link.on)} = ${col(table, alias, link.references)}` : `${col(table, alias, link.on)} = ${col(target, child, link.references)}`;
|
|
5250
|
+
return `EXISTS (SELECT 1 FROM ${quoteIdentifier(target.schema)}.${quoteIdentifier(target.table)} AS ${quoteIdentifier(child)} WHERE ${join} AND (${predicate(target, node.where, child)}))`;
|
|
5251
|
+
}
|
|
5252
|
+
});
|
|
5253
|
+
const columns = meta.columns.filter((c) => meta.filterable.includes(c.name)).map((c) => ({
|
|
5254
|
+
name: c.name,
|
|
5255
|
+
type: typeof c.type === "string" ? c.type : "text",
|
|
5256
|
+
nullable: c.nullable
|
|
5257
|
+
}));
|
|
5258
|
+
const projection = columns.map((c) => {
|
|
5259
|
+
const physical = meta.columns.find((m) => m.name === c.name);
|
|
5260
|
+
return `${quoteIdentifier("p0")}.${quoteIdentifier(physical.columnName)}${typeof physical.type === "object" ? "::text" : ""} AS ${quoteIdentifier(c.name)}`;
|
|
5261
|
+
}).join(", ");
|
|
5262
|
+
const policy = ctx.readPolicies.get(meta.table) ?? { kind: "const", value: true };
|
|
5263
|
+
const scan = `SELECT ${projection} FROM ${quoteIdentifier(meta.schema)}.${quoteIdentifier(meta.table)} AS ${quoteIdentifier("p0")} WHERE ${predicate(meta, policy, "p0")}`;
|
|
5264
|
+
return { ref, table: meta.table, columns, scan, read: [...reads].sort() };
|
|
5265
|
+
});
|
|
5266
|
+
return { identity, objects };
|
|
5267
|
+
}
|
|
5268
|
+
|
|
5269
|
+
// src/sql/ast.ts
|
|
5270
|
+
function obj(value) {
|
|
5271
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("unsupported SQL node");
|
|
5272
|
+
return value;
|
|
5273
|
+
}
|
|
5274
|
+
function list(value) {
|
|
5275
|
+
return value === void 0 ? [] : Array.isArray(value) ? value : fail3("expected SQL list");
|
|
5276
|
+
}
|
|
5277
|
+
function str(value) {
|
|
5278
|
+
return typeof value === "string" ? value : fail3("expected SQL name");
|
|
5279
|
+
}
|
|
5280
|
+
function fail3(message) {
|
|
5281
|
+
throw new Error(message);
|
|
5282
|
+
}
|
|
5283
|
+
function strings(value) {
|
|
5284
|
+
return list(value).map((v) => str(obj(obj(v).String).sval));
|
|
5285
|
+
}
|
|
5286
|
+
function only(node, keys) {
|
|
5287
|
+
for (const [key, value] of Object.entries(node)) {
|
|
5288
|
+
if (!keys.includes(key) && value !== void 0 && value !== null && value !== false && value !== "" && value !== 0 && !(Array.isArray(value) && value.length === 0)) {
|
|
5289
|
+
fail3(`unsupported SQL clause: ${key}`);
|
|
5290
|
+
}
|
|
5291
|
+
}
|
|
5292
|
+
}
|
|
5293
|
+
|
|
5294
|
+
// src/sql/compiler.ts
|
|
5295
|
+
var parser;
|
|
5296
|
+
var SQL_TYPES = ["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"];
|
|
5297
|
+
var pgType = (type) => type === "double" ? "double precision" : SQL_TYPES.some((t) => t === type) ? type : fail3("unsupported SQL type");
|
|
5298
|
+
async function compileOntologySql({ catalog, sql, parameterTypes = [], maxRows = 1e3 }) {
|
|
5299
|
+
if (!Number.isInteger(maxRows) || maxRows < 1 || maxRows > 1e4 || Buffer.byteLength(sql) > 65536) fail3("SQL limit exceeded");
|
|
5300
|
+
parser ??= new PgParser({ version: 17 });
|
|
5301
|
+
const parsed = await parser.parse(sql);
|
|
5302
|
+
if (parsed.error || !parsed.tree) fail3("invalid SQL");
|
|
5303
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
5304
|
+
if (statements.length !== 1) fail3("one SQL SELECT is required");
|
|
5305
|
+
const emitter = new SqlEmitter({ catalog, parameterTypes });
|
|
5306
|
+
const result = emitter.select(obj(obj(statements[0]).stmt), { relations: [], ctes: /* @__PURE__ */ new Map() });
|
|
5307
|
+
if (emitter.used.size !== parameterTypes.length) fail3("SQL parameters must be used exactly as declared");
|
|
5308
|
+
if (!emitter.read.size) fail3("SQL operation must read an ontology object");
|
|
5309
|
+
return {
|
|
5310
|
+
...result,
|
|
5311
|
+
read: [...emitter.read].sort(),
|
|
5312
|
+
maxRows,
|
|
5313
|
+
queryHash: sqlHash(JSON.stringify(["ontology-sql-v1", catalog.identity, result.text, parameterTypes, maxRows]))
|
|
5314
|
+
};
|
|
5315
|
+
}
|
|
5316
|
+
var SqlEmitter = class {
|
|
5317
|
+
constructor(input) {
|
|
5318
|
+
this.input = input;
|
|
5319
|
+
}
|
|
5320
|
+
read = /* @__PURE__ */ new Set();
|
|
5321
|
+
used = /* @__PURE__ */ new Set();
|
|
5322
|
+
nodes = 0;
|
|
5323
|
+
depth = 0;
|
|
5324
|
+
budget() {
|
|
5325
|
+
if (++this.nodes > 5e3 || this.depth > 48) fail3("SQL complexity limit exceeded");
|
|
5326
|
+
}
|
|
5327
|
+
select(wrapper, parent, resolveUnknown = true) {
|
|
5328
|
+
this.budget();
|
|
5329
|
+
this.depth++;
|
|
5330
|
+
const node = obj(wrapper.SelectStmt);
|
|
5331
|
+
only(node, ["targetList", "fromClause", "whereClause", "groupClause", "havingClause", "sortClause", "limitCount", "limitOffset", "limitOption", "op", "withClause", "all", "larg", "rarg"]);
|
|
5332
|
+
if (!["SETOP_NONE", "SETOP_UNION"].includes(str(node.op))) fail3("unsupported SQL set operation");
|
|
5333
|
+
if (node.op === "SETOP_NONE" && (node.all || node.larg || node.rarg)) fail3("invalid SQL set operation");
|
|
5334
|
+
if (node.limitOption && !["LIMIT_OPTION_DEFAULT", "LIMIT_OPTION_COUNT"].includes(str(node.limitOption))) fail3("unsupported SQL limit");
|
|
5335
|
+
const scope = { relations: [], parent, ctes: new Map(parent.ctes) };
|
|
5336
|
+
const ctes = [];
|
|
5337
|
+
if (node.withClause) {
|
|
5338
|
+
const withNode = obj(node.withClause);
|
|
5339
|
+
only(withNode, ["ctes", "location"]);
|
|
5340
|
+
for (const entry of list(withNode.ctes)) {
|
|
5341
|
+
const cte = obj(obj(entry).CommonTableExpr);
|
|
5342
|
+
only(cte, ["ctename", "ctequery", "ctematerialized", "location"]);
|
|
5343
|
+
const name2 = str(cte.ctename);
|
|
5344
|
+
if (scope.ctes.has(name2)) fail3("duplicate CTE");
|
|
5345
|
+
const result2 = this.select(obj(cte.ctequery), { ...parent, ctes: scope.ctes });
|
|
5346
|
+
scope.ctes.set(name2, result2);
|
|
5347
|
+
ctes.push(`${quoteIdentifier(name2)} AS (${result2.text})`);
|
|
5348
|
+
}
|
|
5349
|
+
}
|
|
5350
|
+
const result = node.op === "SETOP_UNION" ? this.union(node, scope) : this.projection(node, scope);
|
|
5351
|
+
const { columns } = result;
|
|
5352
|
+
let text = `${ctes.length ? `WITH ${ctes.join(", ")} ` : ""}${result.text}`;
|
|
5353
|
+
if (list(node.sortClause).length) text += ` ORDER BY ${list(node.sortClause).map((n) => {
|
|
5354
|
+
const sort = obj(obj(n).SortBy);
|
|
5355
|
+
only(sort, ["node", "sortby_dir", "sortby_nulls", "location"]);
|
|
5356
|
+
const direction = sort.sortby_dir === "SORTBY_DESC" ? "DESC" : sort.sortby_dir === "SORTBY_ASC" || sort.sortby_dir === "SORTBY_DEFAULT" ? "ASC" : fail3("unsupported SQL sort");
|
|
5357
|
+
const nulls = sort.sortby_nulls === "SORTBY_NULLS_FIRST" ? " NULLS FIRST" : sort.sortby_nulls === "SORTBY_NULLS_LAST" ? " NULLS LAST" : "";
|
|
5358
|
+
const value = obj(sort.node);
|
|
5359
|
+
const reference = value.ColumnRef ? strings(obj(value.ColumnRef).fields) : [];
|
|
5360
|
+
const projected = reference.length === 1 ? columns.filter((c) => c.name === reference[0]) : [];
|
|
5361
|
+
return `${projected.length === 1 ? quoteIdentifier(projected[0].name) : this.expr(value, scope).text} ${direction}${nulls}`;
|
|
5362
|
+
}).join(", ")}`;
|
|
5363
|
+
for (const [key, keyword] of [["limitCount", "LIMIT"], ["limitOffset", "OFFSET"]]) {
|
|
5364
|
+
if (node[key]) text += ` ${keyword} ${this.expr(obj(node[key]), scope).text}`;
|
|
5365
|
+
}
|
|
5366
|
+
this.depth--;
|
|
5367
|
+
return { text, columns: resolveUnknown ? columns.map((c) => c.type === "unknown" ? { ...c, type: "text" } : c) : columns };
|
|
5368
|
+
}
|
|
5369
|
+
union(node, scope) {
|
|
5370
|
+
for (const key of ["targetList", "fromClause", "whereClause", "groupClause", "havingClause"]) {
|
|
5371
|
+
if (node[key]) fail3("unsupported UNION clause");
|
|
5372
|
+
}
|
|
5373
|
+
const left = this.select({ SelectStmt: obj(node.larg) }, scope, false);
|
|
5374
|
+
const right = this.select({ SelectStmt: obj(node.rarg) }, scope, false);
|
|
5375
|
+
if (left.columns.length !== right.columns.length) fail3("UNION branches must have the same column count");
|
|
5376
|
+
const numeric = ["integer", "bigint", "numeric", "double"];
|
|
5377
|
+
const columns = left.columns.map((column, index) => {
|
|
5378
|
+
const other = right.columns[index];
|
|
5379
|
+
const type = column.type === "unknown" && other.type === "unknown" ? "text" : column.type === "unknown" ? other.type : other.type === "unknown" ? column.type : column.type === other.type ? column.type : numeric.includes(column.type) && numeric.includes(other.type) ? numeric[Math.max(numeric.indexOf(column.type), numeric.indexOf(other.type))] : fail3("UNION columns require compatible types; use explicit casts");
|
|
5380
|
+
return { name: column.name, type, nullable: column.nullable || other.nullable };
|
|
5381
|
+
});
|
|
5382
|
+
return { text: `(${left.text}) UNION${node.all ? " ALL" : ""} (${right.text})`, columns };
|
|
5383
|
+
}
|
|
5384
|
+
projection(node, scope) {
|
|
5385
|
+
const from = list(node.fromClause).map((n) => this.from(obj(n), scope)).join(", ");
|
|
5386
|
+
const columns = [];
|
|
5387
|
+
const targets = [];
|
|
5388
|
+
for (const item of list(node.targetList)) {
|
|
5389
|
+
const target = obj(obj(item).ResTarget);
|
|
5390
|
+
only(target, ["name", "val", "location"]);
|
|
5391
|
+
const value = obj(target.val);
|
|
5392
|
+
const fields = value.ColumnRef ? list(obj(value.ColumnRef).fields) : [];
|
|
5393
|
+
if (fields.length && obj(fields[fields.length - 1]).A_Star) {
|
|
5394
|
+
const matches = fields.length === 1 ? scope.relations : scope.relations.filter((r) => r.alias === strings(fields.slice(0, -1))[0]);
|
|
5395
|
+
if (!matches.length || fields.length > 2 || target.name) fail3("invalid wildcard");
|
|
5396
|
+
for (const relation of matches) for (const column of relation.columns) {
|
|
5397
|
+
columns.push(column);
|
|
5398
|
+
targets.push(`${quoteIdentifier(relation.alias)}.${quoteIdentifier(column.name)}`);
|
|
5399
|
+
}
|
|
5400
|
+
} else {
|
|
5401
|
+
const expression = this.expr(value, scope);
|
|
5402
|
+
const name2 = typeof target.name === "string" && target.name ? target.name : expression.name ?? `column${columns.length + 1}`;
|
|
5403
|
+
columns.push({ name: name2, type: expression.type, nullable: true });
|
|
5404
|
+
targets.push(`${expression.text} AS ${quoteIdentifier(name2)}`);
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
if (!targets.length || targets.length > 256) fail3("invalid SQL projection");
|
|
5408
|
+
let text = `SELECT ${targets.join(", ")}${from ? ` FROM ${from}` : ""}`;
|
|
5409
|
+
if (node.whereClause) text += ` WHERE ${this.expr(obj(node.whereClause), scope).text}`;
|
|
5410
|
+
if (list(node.groupClause).length) text += ` GROUP BY ${list(node.groupClause).map((n) => this.expr(obj(n), scope).text).join(", ")}`;
|
|
5411
|
+
if (node.havingClause) text += ` HAVING ${this.expr(obj(node.havingClause), scope).text}`;
|
|
5412
|
+
return { text, columns };
|
|
5413
|
+
}
|
|
5414
|
+
from(node, scope) {
|
|
5415
|
+
this.budget();
|
|
5416
|
+
this.depth++;
|
|
5417
|
+
try {
|
|
5418
|
+
return this.relation(node, scope);
|
|
5419
|
+
} finally {
|
|
5420
|
+
this.depth--;
|
|
5421
|
+
}
|
|
5422
|
+
}
|
|
5423
|
+
relation(node, scope) {
|
|
5424
|
+
if (node.RangeVar) {
|
|
5425
|
+
const range = obj(node.RangeVar);
|
|
5426
|
+
only(range, ["schemaname", "relname", "inh", "relpersistence", "alias", "location"]);
|
|
5427
|
+
if (range.inh !== true) fail3("ONLY is not supported for ontology objects");
|
|
5428
|
+
const name2 = str(range.relname);
|
|
5429
|
+
const alias = range.alias ? str(obj(range.alias).aliasname) : name2;
|
|
5430
|
+
if (range.alias) only(obj(range.alias), ["aliasname"]);
|
|
5431
|
+
if (scope.relations.some((r) => r.alias === alias)) fail3("duplicate SQL alias");
|
|
5432
|
+
const cte = !range.schemaname ? scope.ctes.get(name2) : void 0;
|
|
5433
|
+
if (cte) {
|
|
5434
|
+
scope.relations.push({ alias, columns: cte.columns });
|
|
5435
|
+
return `${quoteIdentifier(name2)} AS ${quoteIdentifier(alias)}`;
|
|
5436
|
+
}
|
|
5437
|
+
const object = this.input.catalog.objects.find((o) => o.ref === `${range.schemaname}/${name2}`);
|
|
5438
|
+
if (!object) fail3("SQL object is not declared or available");
|
|
5439
|
+
for (const ref of object.read) this.read.add(ref);
|
|
5440
|
+
scope.relations.push({ alias, columns: object.columns });
|
|
5441
|
+
const source = `(WITH ${quoteIdentifier("__orm_scan")} AS MATERIALIZED (${object.scan}) SELECT * FROM ${quoteIdentifier("__orm_scan")})`;
|
|
5442
|
+
return `${source} AS ${quoteIdentifier(alias)}`;
|
|
5443
|
+
}
|
|
5444
|
+
if (node.JoinExpr) {
|
|
5445
|
+
const join = obj(node.JoinExpr);
|
|
5446
|
+
only(join, ["jointype", "larg", "rarg", "quals"]);
|
|
5447
|
+
if (!["JOIN_INNER", "JOIN_LEFT", "JOIN_RIGHT", "JOIN_FULL"].includes(str(join.jointype))) fail3("unsupported SQL join");
|
|
5448
|
+
const start = scope.relations.length;
|
|
5449
|
+
const left = this.from(obj(join.larg), scope);
|
|
5450
|
+
const middle = scope.relations.length;
|
|
5451
|
+
const right = this.from(obj(join.rarg), scope);
|
|
5452
|
+
const nullable = (relations) => {
|
|
5453
|
+
for (const relation of relations) relation.columns = relation.columns.map((c) => ({ ...c, nullable: true }));
|
|
5454
|
+
};
|
|
5455
|
+
if (["JOIN_RIGHT", "JOIN_FULL"].includes(str(join.jointype))) nullable(scope.relations.slice(start, middle));
|
|
5456
|
+
if (["JOIN_LEFT", "JOIN_FULL"].includes(str(join.jointype))) nullable(scope.relations.slice(middle));
|
|
5457
|
+
const kind = str(join.jointype).slice("JOIN_".length);
|
|
5458
|
+
return `(${left} ${kind} JOIN ${right} ON ${join.quals ? this.expr(obj(join.quals), scope).text : "TRUE"})`;
|
|
5459
|
+
}
|
|
5460
|
+
if (node.RangeSubselect) {
|
|
5461
|
+
const range = obj(node.RangeSubselect);
|
|
5462
|
+
only(range, ["subquery", "alias"]);
|
|
5463
|
+
const alias = str(obj(range.alias).aliasname);
|
|
5464
|
+
only(obj(range.alias), ["aliasname"]);
|
|
5465
|
+
if (scope.relations.some((r) => r.alias === alias)) fail3("duplicate SQL alias");
|
|
5466
|
+
const result = this.select(obj(range.subquery), { relations: [], ctes: scope.ctes });
|
|
5467
|
+
scope.relations.push({ alias, columns: result.columns });
|
|
5468
|
+
return `(${result.text}) AS ${quoteIdentifier(alias)}`;
|
|
5469
|
+
}
|
|
5470
|
+
return fail3("unsupported SQL relation");
|
|
5471
|
+
}
|
|
5472
|
+
column(fields, scope) {
|
|
5473
|
+
if (fields.length !== 1 && fields.length !== 2) fail3("unsupported column reference");
|
|
5474
|
+
const name2 = fields[fields.length - 1];
|
|
5475
|
+
const matches = scope.relations.flatMap((r) => fields.length === 2 && r.alias !== fields[0] ? [] : r.columns.filter((c) => c.name === name2).map((c) => ({ r, c })));
|
|
5476
|
+
if (matches.length > 1) fail3("ambiguous SQL column");
|
|
5477
|
+
if (matches.length === 1) return { text: `${quoteIdentifier(matches[0].r.alias)}.${quoteIdentifier(name2)}`, type: matches[0].c.type, name: name2 };
|
|
5478
|
+
if (scope.parent && !(fields.length === 2 && scope.relations.some((r) => r.alias === fields[0]))) return this.column(fields, scope.parent);
|
|
5479
|
+
return fail3("unknown ontology property");
|
|
5480
|
+
}
|
|
5481
|
+
expr(node, scope) {
|
|
5482
|
+
this.budget();
|
|
5483
|
+
this.depth++;
|
|
5484
|
+
try {
|
|
5485
|
+
return this.expression(node, scope);
|
|
5486
|
+
} finally {
|
|
5487
|
+
this.depth--;
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
expression(node, scope) {
|
|
5491
|
+
if (node.ColumnRef) return this.column(strings(obj(node.ColumnRef).fields), scope);
|
|
5492
|
+
if (node.ParamRef) {
|
|
5493
|
+
const number = obj(node.ParamRef).number;
|
|
5494
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number < 1 || number > this.input.parameterTypes.length) fail3("undeclared SQL parameter");
|
|
5495
|
+
this.used.add(number);
|
|
5496
|
+
const type = this.input.parameterTypes[number - 1];
|
|
5497
|
+
return { text: `$${number}::${pgType(type)}`, type };
|
|
5498
|
+
}
|
|
5499
|
+
if (node.A_Const) {
|
|
5500
|
+
const literal = obj(node.A_Const);
|
|
5501
|
+
if (literal.isnull) return { text: "NULL", type: "unknown" };
|
|
5502
|
+
if (literal.sval) return { text: quoteLiteral(str(obj(literal.sval).sval)), type: "unknown" };
|
|
5503
|
+
if (literal.boolval) return { text: obj(literal.boolval).boolval ? "TRUE" : "FALSE", type: "boolean" };
|
|
5504
|
+
const value = literal.ival ? obj(literal.ival).ival : obj(literal.fval).fval;
|
|
5505
|
+
if (!/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(String(value))) fail3("unsupported SQL constant");
|
|
5506
|
+
return { text: String(value), type: literal.ival ? "integer" : "numeric" };
|
|
5507
|
+
}
|
|
5508
|
+
if (node.A_Expr) {
|
|
5509
|
+
const value = obj(node.A_Expr);
|
|
5510
|
+
only(value, ["kind", "name", "lexpr", "rexpr", "location"]);
|
|
5511
|
+
const names = strings(value.name);
|
|
5512
|
+
if (names.length !== 1 || value.kind !== "AEXPR_OP") fail3("unsupported SQL operator");
|
|
5513
|
+
const operator = names[0];
|
|
5514
|
+
if (!["=", "<>", "!=", "<", ">", "<=", ">=", "+", "-", "*", "/"].includes(operator)) fail3("unsupported SQL operator");
|
|
5515
|
+
const left = this.expr(obj(value.lexpr), scope), right = this.expr(obj(value.rexpr), scope);
|
|
5516
|
+
const numericTypes = ["integer", "bigint", "numeric", "double"];
|
|
5517
|
+
const arithmetic = ["+", "-", "*", "/"].includes(operator);
|
|
5518
|
+
if (arithmetic && (!numericTypes.includes(left.type) || !numericTypes.includes(right.type))) fail3("arithmetic requires numeric operands");
|
|
5519
|
+
const type = arithmetic ? numericTypes[Math.max(numericTypes.indexOf(left.type), numericTypes.indexOf(right.type))] : "boolean";
|
|
5520
|
+
return { text: `(${left.text} OPERATOR(pg_catalog.${operator}) ${right.text})`, type };
|
|
5521
|
+
}
|
|
5522
|
+
if (node.BoolExpr) {
|
|
5523
|
+
const value = obj(node.BoolExpr);
|
|
5524
|
+
const args = list(value.args).map((a) => this.expr(obj(a), scope).text);
|
|
5525
|
+
if (value.boolop === "NOT_EXPR" && args.length === 1) return { text: `(NOT ${args[0]})`, type: "boolean" };
|
|
5526
|
+
if (!["AND_EXPR", "OR_EXPR"].includes(str(value.boolop))) fail3("unsupported boolean expression");
|
|
5527
|
+
return { text: `(${args.join(value.boolop === "AND_EXPR" ? " AND " : " OR ")})`, type: "boolean" };
|
|
5528
|
+
}
|
|
5529
|
+
if (node.NullTest) {
|
|
5530
|
+
const value = obj(node.NullTest);
|
|
5531
|
+
only(value, ["arg", "nulltesttype", "location"]);
|
|
5532
|
+
if (!["IS_NULL", "IS_NOT_NULL"].includes(str(value.nulltesttype))) fail3("invalid null comparison");
|
|
5533
|
+
return { text: `(${this.expr(obj(value.arg), scope).text} IS ${value.nulltesttype === "IS_NOT_NULL" ? "NOT " : ""}NULL)`, type: "boolean" };
|
|
5534
|
+
}
|
|
5535
|
+
if (node.SubLink) {
|
|
5536
|
+
const value = obj(node.SubLink);
|
|
5537
|
+
only(value, ["subLinkType", "subselect", "location"]);
|
|
5538
|
+
if (value.subLinkType !== "EXISTS_SUBLINK") fail3("only EXISTS subqueries are supported");
|
|
5539
|
+
return { text: `EXISTS (${this.select(obj(value.subselect), scope).text})`, type: "boolean" };
|
|
5540
|
+
}
|
|
5541
|
+
if (node.TypeCast) {
|
|
5542
|
+
const value = obj(node.TypeCast), typeName2 = obj(value.typeName);
|
|
5543
|
+
only(typeName2, ["names", "typemod", "location"]);
|
|
5544
|
+
const names = strings(typeName2.names);
|
|
5545
|
+
if (names.length > 2 || names.length === 2 && names[0] !== "pg_catalog") fail3("unsupported SQL cast");
|
|
5546
|
+
const aliases = { int4: "integer", int8: "bigint", float8: "double", bool: "boolean" };
|
|
5547
|
+
const name2 = names[names.length - 1], type = aliases[name2] ?? name2;
|
|
5548
|
+
return { text: `(${this.expr(obj(value.arg), scope).text})::${pgType(type)}`, type };
|
|
5549
|
+
}
|
|
5550
|
+
if (node.FuncCall) {
|
|
5551
|
+
const value = obj(node.FuncCall);
|
|
5552
|
+
only(value, ["funcname", "args", "agg_star", "agg_distinct", "funcformat", "location"]);
|
|
5553
|
+
const names = strings(value.funcname);
|
|
5554
|
+
if (names.length !== 1) fail3("SQL function qualification is not supported");
|
|
5555
|
+
const name2 = names[0];
|
|
5556
|
+
if (!["count", "sum", "avg", "min", "max", "lower", "upper", "length"].includes(name2)) fail3("SQL function is not allowed");
|
|
5557
|
+
const args = list(value.args).map((a) => this.expr(obj(a), scope));
|
|
5558
|
+
if (value.agg_star && (name2 !== "count" || args.length) || !value.agg_star && args.length !== 1) fail3("unsupported SQL function arguments");
|
|
5559
|
+
const argType = args[0]?.type ?? "integer";
|
|
5560
|
+
if (["lower", "upper", "length"].includes(name2) && !["text", "unknown"].includes(argType)) fail3("text function requires text");
|
|
5561
|
+
if (["sum", "avg"].includes(name2) && !["integer", "bigint", "numeric", "double"].includes(argType)) fail3("aggregate requires numeric input");
|
|
5562
|
+
const type = name2 === "count" ? "bigint" : name2 === "length" ? "integer" : ["lower", "upper"].includes(name2) ? "text" : name2 === "avg" ? argType === "double" ? "double" : "numeric" : name2 === "sum" ? argType === "integer" ? "bigint" : argType === "bigint" ? "numeric" : argType : argType;
|
|
5563
|
+
return { text: `pg_catalog.${name2}(${value.agg_distinct ? "DISTINCT " : ""}${value.agg_star ? "*" : args.map((a) => a.text).join(", ")})`, type, name: name2 };
|
|
5564
|
+
}
|
|
5565
|
+
return fail3(`unsupported SQL expression: ${Object.keys(node).join(",")}`);
|
|
5566
|
+
}
|
|
5567
|
+
};
|
|
5568
|
+
var sqlScalarSchema = z.enum(["text", "boolean", "integer", "bigint", "numeric", "double", "uuid", "date", "timestamptz"]);
|
|
5569
|
+
var name = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved name");
|
|
5570
|
+
var base = z.object({
|
|
5571
|
+
name,
|
|
5572
|
+
namespace: name,
|
|
5573
|
+
language: z.literal("sql"),
|
|
5574
|
+
kind: z.literal("query"),
|
|
5575
|
+
requires: z.object({ capability: z.string().min(1).max(128) }).strict(),
|
|
5576
|
+
ontology: z.object({ read: z.array(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*\/[A-Za-z_][A-Za-z0-9_]*$/)).min(1).max(256) }).strict(),
|
|
5577
|
+
maxRows: z.number().int().min(1).max(1e4).default(1e3)
|
|
5578
|
+
});
|
|
5579
|
+
var fixed = base.extend({
|
|
5580
|
+
parameters: z.record(name, z.object({ type: sqlScalarSchema, required: z.boolean().default(true) }).strict()).default({}),
|
|
5581
|
+
sql: z.string().min(1).max(65536)
|
|
5582
|
+
}).strict();
|
|
5583
|
+
var runtime = base.extend({ mode: z.literal("runtime") }).strict();
|
|
5584
|
+
var sqlOperationDefinitionSchema = z.union([fixed, runtime]);
|
|
5585
|
+
function defineOperation(input) {
|
|
5586
|
+
return sqlOperationDefinitionSchema.parse(input);
|
|
5587
|
+
}
|
|
5588
|
+
|
|
5589
|
+
// src/sql/parameters.ts
|
|
5590
|
+
function bindNamedParameters({ sql, names }) {
|
|
5591
|
+
let result = "", i = 0;
|
|
5592
|
+
while (i < sql.length) {
|
|
5593
|
+
const start = i;
|
|
5594
|
+
if (sql.startsWith("--", i)) {
|
|
5595
|
+
i = sql.indexOf("\n", i);
|
|
5596
|
+
if (i < 0) i = sql.length;
|
|
5597
|
+
} else if (sql.startsWith("/*", i)) {
|
|
5598
|
+
i += 2;
|
|
5599
|
+
let depth = 1;
|
|
5600
|
+
while (i < sql.length && depth) {
|
|
5601
|
+
if (sql.startsWith("/*", i)) {
|
|
5602
|
+
depth++;
|
|
5603
|
+
i += 2;
|
|
5604
|
+
} else if (sql.startsWith("*/", i)) {
|
|
5605
|
+
depth--;
|
|
5606
|
+
i += 2;
|
|
5607
|
+
} else i++;
|
|
5608
|
+
}
|
|
5609
|
+
if (depth) throw new Error("unterminated SQL comment");
|
|
5610
|
+
} else if (sql[i] === "'" || sql[i] === '"') {
|
|
5611
|
+
const escapes = sql[i] === "'" && /[eE]/.test(sql[i - 1] ?? "") && !/[A-Za-z0-9_$]/.test(sql[i - 2] ?? "");
|
|
5612
|
+
const quote = sql[i++];
|
|
5613
|
+
while (i < sql.length) {
|
|
5614
|
+
if (sql[i] === quote) {
|
|
5615
|
+
if (sql[i + 1] === quote) i += 2;
|
|
5616
|
+
else {
|
|
5617
|
+
i++;
|
|
5618
|
+
break;
|
|
5619
|
+
}
|
|
5620
|
+
} else if (escapes && sql[i] === "\\") i += 2;
|
|
5621
|
+
else i++;
|
|
5622
|
+
}
|
|
5623
|
+
} else if (sql[i] === "$" && /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.test(sql.slice(i))) {
|
|
5624
|
+
const marker = /^\$(?:[A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql.slice(i))[0];
|
|
5625
|
+
const end = sql.indexOf(marker, i + marker.length);
|
|
5626
|
+
if (end < 0) throw new Error("unterminated SQL string");
|
|
5627
|
+
i = end + marker.length;
|
|
5628
|
+
} else if (sql[i] === ":" && sql[i - 1] !== ":" && sql[i + 1] !== ":") {
|
|
5629
|
+
const name2 = /^[A-Za-z_][A-Za-z0-9_]*/.exec(sql.slice(i + 1))?.[0];
|
|
5630
|
+
if (!name2 || !names.includes(name2)) throw new Error("undeclared named SQL parameter");
|
|
5631
|
+
result += `$${names.indexOf(name2) + 1}`;
|
|
5632
|
+
i += name2.length + 1;
|
|
5633
|
+
continue;
|
|
5634
|
+
} else i++;
|
|
5635
|
+
result += sql.slice(start, i);
|
|
5636
|
+
}
|
|
5637
|
+
return result;
|
|
5638
|
+
}
|
|
5639
|
+
|
|
5640
|
+
// src/sql/operations.ts
|
|
5641
|
+
function isSqlOperationDocument(path, contents) {
|
|
5642
|
+
if (path.endsWith(".operation.json")) return true;
|
|
5643
|
+
if (!contents?.trimStart().startsWith("{")) return false;
|
|
5644
|
+
try {
|
|
5645
|
+
const value = JSON.parse(contents);
|
|
5646
|
+
return !!value && typeof value === "object" && "language" in value && value.language === "sql";
|
|
5647
|
+
} catch {
|
|
5648
|
+
return false;
|
|
5649
|
+
}
|
|
5650
|
+
}
|
|
5651
|
+
async function compileOperationDocuments(input) {
|
|
5652
|
+
const sqlDocuments = input.documents.filter((d) => isSqlOperationDocument(d.path, d.contents));
|
|
5653
|
+
const result = compileNamespace({ ...input, documents: input.documents.filter((d) => !isSqlOperationDocument(d.path, d.contents)) });
|
|
5654
|
+
if (!result.ok || !sqlDocuments.length) return result;
|
|
5655
|
+
for (const document of sqlDocuments) {
|
|
5656
|
+
try {
|
|
5657
|
+
const definition = defineOperation(JSON.parse(document.contents));
|
|
5658
|
+
if (definition.namespace !== input.canonical.namespace) throw new Error("SQL operation namespace does not match its owner");
|
|
5659
|
+
if (result.artifact.operations[definition.name]) throw new Error("duplicate operation name");
|
|
5660
|
+
const roles = input.canonical.capabilities.roles;
|
|
5661
|
+
const localCapabilities = roles && typeof roles === "object" && "capabilities" in roles ? roles.capabilities : void 0;
|
|
5662
|
+
const declaredLocally = localCapabilities && typeof localCapabilities === "object" && Object.hasOwn(localCapabilities, definition.requires.capability);
|
|
5663
|
+
if (!declaredLocally && !input.canonical.capabilities.uses.includes(definition.requires.capability)) throw new Error("SQL operation capability is not declared by its namespace");
|
|
5664
|
+
const read = [...definition.ontology.read].sort();
|
|
5665
|
+
const catalog = createSqlCatalog({ canonical: input.canonical, publishedObjects: read, executionTarget: "registered-operation" });
|
|
5666
|
+
if ("mode" in definition) {
|
|
5667
|
+
if (catalog.objects.some((object) => object.read.some((ref) => !read.includes(ref)))) throw new Error("SQL policy accesses an undeclared ontology object");
|
|
5668
|
+
const opHash2 = sqlHash(JSON.stringify([
|
|
5669
|
+
"sequence-runtime-sql-operation-v1",
|
|
5670
|
+
definition.name,
|
|
5671
|
+
definition.namespace,
|
|
5672
|
+
definition.requires.capability,
|
|
5673
|
+
read,
|
|
5674
|
+
catalog.identity,
|
|
5675
|
+
definition.maxRows
|
|
5676
|
+
]));
|
|
5677
|
+
result.artifact.operations[definition.name] = compiledOperationSchema.parse({
|
|
5678
|
+
irVersion: RUNTIME_SQL_IR_VERSION,
|
|
5679
|
+
language: "sql",
|
|
5680
|
+
name: definition.name,
|
|
5681
|
+
opHash: opHash2,
|
|
5682
|
+
kind: "query",
|
|
5683
|
+
requires: definition.requires,
|
|
5684
|
+
variables: [
|
|
5685
|
+
{ name: "sql", type: { kind: "scalar", scalar: "text" }, nullable: false, list: false },
|
|
5686
|
+
{ name: "parameters", type: { kind: "scalar", scalar: "jsonb" }, nullable: true, list: false }
|
|
5687
|
+
],
|
|
5688
|
+
roots: [],
|
|
5689
|
+
tableSet: read.map((ref) => ref.split("/")[1]).sort(),
|
|
5690
|
+
sqlQuery: { catalog, read, maxRows: definition.maxRows }
|
|
5691
|
+
});
|
|
5692
|
+
result.manifest.operations[definition.name] = { opHash: opHash2, kind: "query", requires: definition.requires };
|
|
5693
|
+
continue;
|
|
5694
|
+
}
|
|
5695
|
+
const names = Object.keys(definition.parameters).sort();
|
|
5696
|
+
if (names.length > 128) throw new Error("too many SQL parameters");
|
|
5697
|
+
const plan = await compileOntologySql({
|
|
5698
|
+
catalog,
|
|
5699
|
+
sql: bindNamedParameters({ sql: definition.sql, names }),
|
|
5700
|
+
parameterTypes: names.map((name2) => definition.parameters[name2].type),
|
|
5701
|
+
maxRows: definition.maxRows
|
|
5702
|
+
});
|
|
5703
|
+
if (plan.read.some((ref) => !read.includes(ref))) throw new Error("SQL policy/query accesses an undeclared ontology object");
|
|
5704
|
+
const variables = names.map((name2) => ({
|
|
5705
|
+
name: name2,
|
|
5706
|
+
type: { kind: "scalar", scalar: definition.parameters[name2].type },
|
|
5707
|
+
nullable: !definition.parameters[name2].required,
|
|
5708
|
+
list: false
|
|
5709
|
+
}));
|
|
5710
|
+
const opHash = sqlHash(JSON.stringify([
|
|
5711
|
+
"sequence-sql-operation-v1",
|
|
5712
|
+
definition.name,
|
|
5713
|
+
definition.namespace,
|
|
5714
|
+
definition.requires.capability,
|
|
5715
|
+
read,
|
|
5716
|
+
variables,
|
|
5717
|
+
plan.queryHash
|
|
5718
|
+
]));
|
|
5719
|
+
const operation = compiledOperationSchema.parse({
|
|
5720
|
+
irVersion: SQL_IR_VERSION,
|
|
5721
|
+
language: "sql",
|
|
5722
|
+
name: definition.name,
|
|
5723
|
+
opHash,
|
|
5724
|
+
kind: "query",
|
|
5725
|
+
requires: definition.requires,
|
|
5726
|
+
variables,
|
|
5727
|
+
roots: [],
|
|
5728
|
+
tableSet: plan.read.map((ref) => ref.split("/")[1]).sort(),
|
|
5729
|
+
sql: { text: plan.text, columns: plan.columns, parameters: names, read, maxRows: plan.maxRows }
|
|
5730
|
+
});
|
|
5731
|
+
result.artifact.operations[definition.name] = operation;
|
|
5732
|
+
result.manifest.operations[definition.name] = { opHash, kind: "query", requires: definition.requires };
|
|
5733
|
+
} catch (error) {
|
|
5734
|
+
return { ok: false, diagnostics: [...result.diagnostics, {
|
|
5735
|
+
severity: "error",
|
|
5736
|
+
code: "invalid-sql-operation",
|
|
5737
|
+
document: document.path,
|
|
5738
|
+
message: error instanceof Error ? error.message : "invalid SQL operation"
|
|
5739
|
+
}] };
|
|
5740
|
+
}
|
|
5741
|
+
}
|
|
5742
|
+
const required = requiredArtifactIrVersion(Object.values(result.artifact.operations));
|
|
5743
|
+
if (required > result.artifact.irVersion) result.artifact.irVersion = required;
|
|
5744
|
+
result.artifact.opsHash = computeOpsHash(Object.values(result.artifact.operations).map((op) => op.opHash));
|
|
5745
|
+
result.manifest.opsHash = result.artifact.opsHash;
|
|
5746
|
+
return { ...result, artifact: normalizeIr(result.artifact) };
|
|
5747
|
+
}
|
|
5748
|
+
var parameterName = z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/).max(63).refine((value) => !["__proto__", "prototype", "constructor"].includes(value), "reserved parameter name");
|
|
5749
|
+
var parameter = z.discriminatedUnion("type", [
|
|
5750
|
+
z.object({ type: z.literal("text"), value: z.string().nullable() }).strict(),
|
|
5751
|
+
z.object({ type: z.literal("uuid"), value: z.guid().nullable() }).strict(),
|
|
5752
|
+
z.object({ type: z.literal("date"), value: z.string().nullable() }).strict(),
|
|
5753
|
+
z.object({ type: z.literal("timestamptz"), value: z.string().nullable() }).strict(),
|
|
5754
|
+
z.object({ type: z.literal("numeric"), value: z.string().regex(/^-?\d+(\.\d+)?$/).nullable() }).strict(),
|
|
5755
|
+
z.object({ type: z.literal("bigint"), value: z.string().regex(/^-?\d+$/).nullable() }).strict(),
|
|
5756
|
+
z.object({ type: z.literal("integer"), value: z.number().int().min(-2147483648).max(2147483647).nullable() }).strict(),
|
|
5757
|
+
z.object({ type: z.literal("double"), value: z.number().nullable() }).strict(),
|
|
5758
|
+
z.object({ type: z.literal("boolean"), value: z.boolean().nullable() }).strict()
|
|
5759
|
+
]);
|
|
5760
|
+
var sqlQueryInputSchema = z.object({
|
|
5761
|
+
sql: z.string().min(1).max(65536).refine((value) => Buffer.byteLength(value) <= 65536, "SQL exceeds 64 KiB"),
|
|
5762
|
+
parameters: z.record(parameterName, parameter).refine((value) => Object.keys(value).length <= 128, "too many SQL parameters").default({})
|
|
5763
|
+
}).strict();
|
|
5764
|
+
async function compileRuntimeSql({ operation, input }) {
|
|
5765
|
+
const contract = operation.sqlQuery;
|
|
5766
|
+
if (!contract) throw new Error("missing runtime SQL contract");
|
|
5767
|
+
const names = Object.keys(input.parameters).sort();
|
|
5768
|
+
const plan = await compileOntologySql({
|
|
5769
|
+
catalog: contract.catalog,
|
|
5770
|
+
sql: bindNamedParameters({ sql: input.sql, names }),
|
|
5771
|
+
parameterTypes: names.map((name2) => input.parameters[name2].type),
|
|
5772
|
+
maxRows: contract.maxRows
|
|
5773
|
+
});
|
|
5774
|
+
if (plan.read.some((ref) => !contract.read.includes(ref))) throw new Error("SQL accesses an undeclared ontology object");
|
|
5775
|
+
return compiledOperationSchema.parse({
|
|
5776
|
+
...operation,
|
|
5777
|
+
sqlQuery: void 0,
|
|
5778
|
+
irVersion: SQL_IR_VERSION,
|
|
5779
|
+
variables: names.map((name2) => ({ name: name2, type: { kind: "scalar", scalar: input.parameters[name2].type }, nullable: true, list: false })),
|
|
5780
|
+
tableSet: plan.read.map((ref) => ref.split("/")[1]).sort(),
|
|
5781
|
+
sql: { text: plan.text, columns: plan.columns, parameters: names, read: plan.read, maxRows: plan.maxRows }
|
|
5782
|
+
});
|
|
5783
|
+
}
|
|
5784
|
+
var DATASET_PREVIEW_LIMIT = 100;
|
|
5785
|
+
function compileDatasetPreviews(input) {
|
|
5786
|
+
const canonical = normalizeCanonical(input);
|
|
5787
|
+
const { ctx, diagnostics } = buildCompileContext(canonical);
|
|
5788
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) throw new NamespaceCompileError(diagnostics);
|
|
5789
|
+
const schemaHash = computeSchemaHash(canonical);
|
|
5790
|
+
const operations = {};
|
|
5791
|
+
for (const relation of canonicalRelations(canonical)) {
|
|
5792
|
+
if (relation.internal || relation.source !== void 0) continue;
|
|
5793
|
+
const columns = relation.columns.filter((column) => column.cell === void 0);
|
|
5794
|
+
const { roots, tableSet } = injectPolicies({ ctx, roots: [{
|
|
5795
|
+
kind: "select",
|
|
5796
|
+
table: relation.table,
|
|
5797
|
+
alias: "rows",
|
|
5798
|
+
where: { kind: "const", value: true },
|
|
5799
|
+
orderBy: [],
|
|
5800
|
+
limit: { value: DATASET_PREVIEW_LIMIT, default: DATASET_PREVIEW_LIMIT, max: DATASET_PREVIEW_LIMIT },
|
|
5801
|
+
selection: {
|
|
5802
|
+
typename: ctx.tables[relation.table].typename,
|
|
5803
|
+
columns: columns.map((column) => ({ column: column.name, alias: column.name })),
|
|
5804
|
+
relations: [],
|
|
5805
|
+
aggregates: []
|
|
5806
|
+
}
|
|
5807
|
+
}] });
|
|
5808
|
+
for (const table of tableSet) {
|
|
5809
|
+
const meta = ctx.tables[table];
|
|
5810
|
+
if (meta.namespace && meta.namespace !== ctx.namespace && !meta.exportRead) {
|
|
5811
|
+
throw new DocumentCompileError(`object '${meta.namespace}/${table}' is not exported for reads`, "unexported-object");
|
|
5812
|
+
}
|
|
5813
|
+
}
|
|
5814
|
+
const hash = createHash("sha256").update("sequence-orm-dataset-preview-v1\0").update(stableJson({ schemaHash, roots, tableSet })).digest("hex");
|
|
5815
|
+
operations[relation.table] = compiledOperationSchema.parse({
|
|
5816
|
+
irVersion: canonical.imports?.length ? COMPOSED_IR_VERSION : LEGACY_IR_VERSION,
|
|
5817
|
+
name: `DatasetPreview_${relation.table}`,
|
|
5818
|
+
opHash: `sha256:${hash}`,
|
|
5819
|
+
kind: "query",
|
|
5820
|
+
requires: null,
|
|
5821
|
+
variables: [],
|
|
5822
|
+
tableSet,
|
|
5823
|
+
roots
|
|
5824
|
+
});
|
|
5825
|
+
}
|
|
5826
|
+
return operations;
|
|
5827
|
+
}
|
|
5828
|
+
|
|
5829
|
+
export { COMPILER_MAJOR, CONST_TRUE, DATASET_PREVIEW_LIMIT, DEFAULT_LIMIT, DocumentCompileError, MAX_LIMIT, NamespaceCompileError, ORDER_BY_DIRECTIONS, OperationLowerer, PLATFORM_USER_TABLE, PLATFORM_USER_TYPENAME, POLICY_COMPARE_OPS, POLICY_MAX_LINK_DEPTH, PolicyCompileError, PolicyParseError, allowedBoolExpOperators, allowedOps, andPred, assertClaimsOnlyWhen, buildCompileContext, camelCase, canonicalOperationText, classifyInputType, collectPinnedColumns, compileDatasetPreviews, compileNamespace, compileNamespaceOrThrow, compileOperationDocuments, compilePolicy, compilePredicateV2, compileRuntimeSql, computeOpHash, computeOpsHash, computePinnedColumnsByTable, computeSchemaHash, defineOperation, enumTypeName, flattenOperation, injectPolicies, inputTypeName, isSqlOperationDocument, lowerOperation, mutationRootNames, parseDocuments, parsePolicy, parseSdl, pascalCase, platformUserMeta, predContains, predContainsExists, queryRootNames, restrictOperation, sqlOperationDefinitionSchema, sqlQueryInputSchema, typeNameForTable, validateAppendInput, validateBoolExpInput, validateInsertInput, validateOnConflictInput, validateOrderByInput, validateSelectColumns, validateSetInput, walkPolicy, walkPred };
|
|
4827
5830
|
//# sourceMappingURL=compiler.js.map
|
|
4828
5831
|
//# sourceMappingURL=compiler.js.map
|