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