@sequenceholdings/orm 2.5.0 → 2.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/cli.cjs +23 -11
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +23 -11
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +10 -5
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +37 -30
- package/dist/client.d.ts +37 -30
- package/dist/client.js +10 -6
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +127 -9
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +25 -2
- package/dist/compiler.d.ts +25 -2
- package/dist/compiler.js +125 -10
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +260 -138
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -3
- package/dist/index.d.ts +25 -3
- package/dist/index.js +260 -139
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +5 -1
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.js +5 -1
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +5 -1
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.js +5 -1
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +7 -1
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.js +7 -1
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/templates/orm-namespace/package.json +2 -0
- package/templates/orm-namespace/schema/tables.ts +7 -3
package/dist/index.cjs
CHANGED
|
@@ -1260,10 +1260,10 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1260
1260
|
const resolved = view.resolved;
|
|
1261
1261
|
if (!resolved) return [];
|
|
1262
1262
|
const errors = [];
|
|
1263
|
-
const
|
|
1263
|
+
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1264
1264
|
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1265
1265
|
if (!pkColumn) {
|
|
1266
|
-
|
|
1266
|
+
fail3("requires exactly one primary-key column");
|
|
1267
1267
|
return errors;
|
|
1268
1268
|
}
|
|
1269
1269
|
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
@@ -1273,93 +1273,93 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1273
1273
|
pkColumn,
|
|
1274
1274
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1275
1275
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
1276
|
-
fail:
|
|
1276
|
+
fail: fail3
|
|
1277
1277
|
};
|
|
1278
|
-
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1)
|
|
1278
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail3("requires exactly one manual source");
|
|
1279
1279
|
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1280
1280
|
for (const source of resolved.sources) {
|
|
1281
|
-
if (scope.sourceByName.has(source.name))
|
|
1281
|
+
if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
|
|
1282
1282
|
scope.sourceByName.set(source.name, source);
|
|
1283
1283
|
const table = tableByName.get(source.table);
|
|
1284
|
-
if (!table)
|
|
1284
|
+
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1285
1285
|
else validateSource({ scope, source, table });
|
|
1286
1286
|
}
|
|
1287
1287
|
validateRules(scope);
|
|
1288
1288
|
validateViewCells(scope);
|
|
1289
1289
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1290
1290
|
for (const name2 of resolved.deletedBy) {
|
|
1291
|
-
if (scope.sourceByName.get(name2)?.kind !== "manual")
|
|
1291
|
+
if (scope.sourceByName.get(name2)?.kind !== "manual") fail3(`deletedBy '${name2}' must be the manual source`);
|
|
1292
1292
|
}
|
|
1293
1293
|
return errors;
|
|
1294
1294
|
}
|
|
1295
1295
|
function validateSource({ scope, source, table }) {
|
|
1296
|
-
const { fail:
|
|
1297
|
-
if (source.kind === "etl" && table.source === void 0)
|
|
1298
|
-
if (source.kind === "etl" && table.primaryKey?.length === 0)
|
|
1296
|
+
const { fail: fail3 } = scope;
|
|
1297
|
+
if (source.kind === "etl" && table.source === void 0) fail3(`etl source '${source.name}' must reference a synced table`);
|
|
1298
|
+
if (source.kind === "etl" && table.primaryKey?.length === 0) fail3(`etl source '${source.name}' cannot use an unkeyed synced table for resolved row identity`);
|
|
1299
1299
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1300
|
-
|
|
1300
|
+
fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1301
1301
|
}
|
|
1302
1302
|
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1303
|
-
if (source.kind === "manual" && !hasRev)
|
|
1304
|
-
if (source.kind === "prefetch" && hasRev)
|
|
1303
|
+
if (source.kind === "manual" && !hasRev) fail3(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
1304
|
+
if (source.kind === "prefetch" && hasRev) fail3(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
1305
1305
|
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1306
1306
|
validateSourceColumns({ scope, source, byName });
|
|
1307
1307
|
if (source.kind === "etl") {
|
|
1308
1308
|
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1309
1309
|
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1310
|
-
|
|
1310
|
+
fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1311
1311
|
}
|
|
1312
1312
|
}
|
|
1313
1313
|
validatePrefetchFields({ scope, source });
|
|
1314
1314
|
}
|
|
1315
1315
|
function validatePrefetchFields({ scope, source }) {
|
|
1316
|
-
const { fail:
|
|
1316
|
+
const { fail: fail3, pkColumn } = scope;
|
|
1317
1317
|
if (source.kind !== "prefetch") {
|
|
1318
1318
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1319
|
-
|
|
1319
|
+
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1320
1320
|
}
|
|
1321
1321
|
return;
|
|
1322
1322
|
}
|
|
1323
1323
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1324
|
-
|
|
1324
|
+
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1325
1325
|
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1326
|
-
|
|
1326
|
+
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1327
1327
|
}
|
|
1328
1328
|
for (const column of source.writeback?.columns ?? []) {
|
|
1329
|
-
if (!source.columns.includes(column))
|
|
1329
|
+
if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1330
1330
|
}
|
|
1331
1331
|
}
|
|
1332
1332
|
function validateSourceColumns({ scope, source, byName }) {
|
|
1333
|
-
const { fail:
|
|
1333
|
+
const { fail: fail3, pkColumn, dataColumns } = scope;
|
|
1334
1334
|
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1335
1335
|
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1336
1336
|
const theirs = byName.get(column);
|
|
1337
|
-
if (!own)
|
|
1338
|
-
else if (!theirs)
|
|
1339
|
-
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type))
|
|
1337
|
+
if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1338
|
+
else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1339
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
|
|
1340
1340
|
}
|
|
1341
1341
|
}
|
|
1342
|
-
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail:
|
|
1342
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
|
|
1343
1343
|
const ruled = /* @__PURE__ */ new Set();
|
|
1344
1344
|
for (const rule of resolved.resolve) {
|
|
1345
1345
|
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1346
|
-
|
|
1346
|
+
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
1347
1347
|
continue;
|
|
1348
1348
|
}
|
|
1349
|
-
if (ruled.has(rule.column))
|
|
1349
|
+
if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
|
|
1350
1350
|
ruled.add(rule.column);
|
|
1351
1351
|
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1352
|
-
|
|
1352
|
+
fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1353
1353
|
}
|
|
1354
|
-
if (new Set(rule.candidates).size !== rule.candidates.length)
|
|
1354
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail3(`column '${rule.column}' repeats a candidate`);
|
|
1355
1355
|
for (const candidate of rule.candidates) {
|
|
1356
1356
|
const source = sourceByName.get(candidate);
|
|
1357
|
-
if (!source)
|
|
1358
|
-
else if (!source.columns.includes(rule.column))
|
|
1357
|
+
if (!source) fail3(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
1358
|
+
else if (!source.columns.includes(rule.column)) fail3(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
1359
1359
|
}
|
|
1360
1360
|
}
|
|
1361
1361
|
for (const column of dataColumns.keys()) {
|
|
1362
|
-
if (column !== pkColumn.name && !ruled.has(column))
|
|
1362
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
1363
1363
|
}
|
|
1364
1364
|
}
|
|
1365
1365
|
var VIEW_CELL_TYPES = {
|
|
@@ -1368,7 +1368,7 @@ var VIEW_CELL_TYPES = {
|
|
|
1368
1368
|
by: "text",
|
|
1369
1369
|
conflicts: "jsonb"
|
|
1370
1370
|
};
|
|
1371
|
-
function validateViewCells({ view, dataColumns, pkColumn, fail:
|
|
1371
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
|
|
1372
1372
|
const expected = /* @__PURE__ */ new Map();
|
|
1373
1373
|
for (const column of dataColumns.values()) {
|
|
1374
1374
|
if (column.name === pkColumn.name) continue;
|
|
@@ -1380,34 +1380,34 @@ function validateViewCells({ view, dataColumns, pkColumn, fail: fail2 }) {
|
|
|
1380
1380
|
if (!column.cell) continue;
|
|
1381
1381
|
const want = expected.get(column.name);
|
|
1382
1382
|
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1383
|
-
|
|
1383
|
+
fail3(`unexpected cell column '${column.name}'`);
|
|
1384
1384
|
continue;
|
|
1385
1385
|
}
|
|
1386
|
-
if (column.type !== VIEW_CELL_TYPES[column.cell.part])
|
|
1386
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail3(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
1387
1387
|
expected.delete(column.name);
|
|
1388
1388
|
}
|
|
1389
|
-
for (const name2 of expected.keys())
|
|
1389
|
+
for (const name2 of expected.keys()) fail3(`missing cell column '${name2}'`);
|
|
1390
1390
|
}
|
|
1391
1391
|
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1392
1392
|
function validateMaterialized({ scope, tableByName, views }) {
|
|
1393
|
-
const { view, resolved, fail:
|
|
1393
|
+
const { view, resolved, fail: fail3 } = scope;
|
|
1394
1394
|
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1395
1395
|
const table = tableByName.get(tableName);
|
|
1396
1396
|
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1397
|
-
|
|
1397
|
+
fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1398
1398
|
} else {
|
|
1399
|
-
if (columnSignature(table.columns) !== columnSignature(view.columns))
|
|
1400
|
-
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? []))
|
|
1399
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail3(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
1400
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail3(`materialized table '${tableName}' must share the view's primary key`);
|
|
1401
1401
|
}
|
|
1402
1402
|
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1403
|
-
if (!compute || compute.internal !== true)
|
|
1404
|
-
else if (columnSignature(compute.columns) !== columnSignature(view.columns))
|
|
1403
|
+
if (!compute || compute.internal !== true) fail3(`compute view '${computeName}' must be an internal view`);
|
|
1404
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail3(`compute view '${computeName}' must have exactly the view's columns`);
|
|
1405
1405
|
for (const source of resolved.sources) {
|
|
1406
1406
|
if (source.kind !== "etl") continue;
|
|
1407
1407
|
const read = tableByName.get(source.table)?.policies?.read;
|
|
1408
1408
|
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1409
1409
|
if (!unconditional) {
|
|
1410
|
-
|
|
1410
|
+
fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1411
1411
|
}
|
|
1412
1412
|
}
|
|
1413
1413
|
}
|
|
@@ -1782,26 +1782,26 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1782
1782
|
materializes: identifier2.optional()
|
|
1783
1783
|
}).superRefine((table, ctx) => {
|
|
1784
1784
|
if (table.primaryKey?.length !== 0) return;
|
|
1785
|
-
const
|
|
1785
|
+
const fail3 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
|
|
1786
1786
|
if (table.source === void 0 || table.columns.length === 0) {
|
|
1787
|
-
|
|
1787
|
+
fail3("requires a typed synced source");
|
|
1788
1788
|
}
|
|
1789
1789
|
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
1790
|
-
|
|
1790
|
+
fail3("is read-only and cannot declare write policies", ["policies"]);
|
|
1791
1791
|
}
|
|
1792
1792
|
const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
1793
1793
|
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
1794
1794
|
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
1795
|
-
|
|
1795
|
+
fail3("cannot declare owned storage, constraints or identity operations");
|
|
1796
1796
|
}
|
|
1797
1797
|
for (const [index3, column] of table.columns.entries()) {
|
|
1798
1798
|
const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
|
|
1799
1799
|
if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
|
|
1800
|
-
|
|
1800
|
+
fail3("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
|
|
1801
1801
|
}
|
|
1802
1802
|
}
|
|
1803
1803
|
for (const [index3, link] of table.links.entries()) {
|
|
1804
|
-
if (link.enforce || link.kind === "toMany")
|
|
1804
|
+
if (link.enforce || link.kind === "toMany") fail3("cannot provide an identity for this link", ["links", index3]);
|
|
1805
1805
|
}
|
|
1806
1806
|
});
|
|
1807
1807
|
var MAX_VIEW_QUERY = 1e5;
|
|
@@ -1892,49 +1892,49 @@ function trackedCellColumns(table) {
|
|
|
1892
1892
|
}
|
|
1893
1893
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1894
1894
|
function validateCellMetadata(table, index3, ctx) {
|
|
1895
|
-
const
|
|
1895
|
+
const fail3 = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
|
|
1896
1896
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1897
1897
|
if (table.materializes !== void 0) return;
|
|
1898
1898
|
if (table.cellMetadata !== true) {
|
|
1899
|
-
for (const { position } of cells)
|
|
1899
|
+
for (const { position } of cells) fail3(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index3, "columns", position]);
|
|
1900
1900
|
return;
|
|
1901
1901
|
}
|
|
1902
1902
|
if (table.source !== void 0) {
|
|
1903
|
-
|
|
1903
|
+
fail3(`synced table '${table.table}' cannot track cell metadata`);
|
|
1904
1904
|
return;
|
|
1905
1905
|
}
|
|
1906
1906
|
const tracked = trackedCellColumns(table);
|
|
1907
1907
|
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1908
1908
|
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1909
|
-
if (withRev && table.internal !== true)
|
|
1909
|
+
if (withRev && table.internal !== true) fail3(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
1910
1910
|
const seen = /* @__PURE__ */ new Set();
|
|
1911
1911
|
for (const { column, position, cell } of cells) {
|
|
1912
1912
|
const path = ["tables", index3, "columns", position];
|
|
1913
1913
|
const owner = byName.get(cell.of);
|
|
1914
1914
|
if (!owner) {
|
|
1915
|
-
|
|
1915
|
+
fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1916
1916
|
continue;
|
|
1917
1917
|
}
|
|
1918
|
-
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev")
|
|
1918
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail3(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1919
1919
|
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1920
|
-
|
|
1920
|
+
fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1921
1921
|
}
|
|
1922
1922
|
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1923
|
-
|
|
1923
|
+
fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1924
1924
|
}
|
|
1925
1925
|
const key = `${cell.of}:${cell.part}`;
|
|
1926
|
-
if (seen.has(key))
|
|
1926
|
+
if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
|
|
1927
1927
|
seen.add(key);
|
|
1928
1928
|
}
|
|
1929
1929
|
for (const column of tracked) {
|
|
1930
1930
|
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1931
|
-
|
|
1931
|
+
fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1932
1932
|
}
|
|
1933
1933
|
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1934
|
-
|
|
1934
|
+
fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1935
1935
|
}
|
|
1936
1936
|
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1937
|
-
if (!seen.has(`${column.name}:${part}`))
|
|
1937
|
+
if (!seen.has(`${column.name}:${part}`)) fail3(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1938
1938
|
}
|
|
1939
1939
|
}
|
|
1940
1940
|
}
|
|
@@ -2002,38 +2002,38 @@ var localDefinitionSchema = zod.z.object({
|
|
|
2002
2002
|
}
|
|
2003
2003
|
}
|
|
2004
2004
|
for (const [index3, check] of (table.checks ?? []).entries()) {
|
|
2005
|
-
const
|
|
2006
|
-
if (table.source)
|
|
2007
|
-
if (names.has(check.name))
|
|
2005
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index3], message });
|
|
2006
|
+
if (table.source) fail3("synced tables cannot declare checks");
|
|
2007
|
+
if (names.has(check.name)) fail3(`duplicate constraint '${check.name}'`);
|
|
2008
2008
|
names.add(check.name);
|
|
2009
2009
|
try {
|
|
2010
2010
|
compileCheckExpression(check.expression, table.columns);
|
|
2011
2011
|
} catch (error) {
|
|
2012
|
-
|
|
2012
|
+
fail3(error instanceof Error ? error.message : String(error));
|
|
2013
2013
|
}
|
|
2014
2014
|
}
|
|
2015
2015
|
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
2016
|
-
const
|
|
2017
|
-
if (names.has(fk.name))
|
|
2016
|
+
const fail3 = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index3], message });
|
|
2017
|
+
if (names.has(fk.name)) fail3(`duplicate foreign key '${fk.name}'`);
|
|
2018
2018
|
names.add(fk.name);
|
|
2019
2019
|
const target = fkTables.get(fk.target);
|
|
2020
2020
|
if (table.source || !target || target.source) {
|
|
2021
|
-
|
|
2021
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
2022
2022
|
continue;
|
|
2023
2023
|
}
|
|
2024
|
-
if (fk.columns.length !== fk.references.length)
|
|
2025
|
-
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length)
|
|
2024
|
+
if (fk.columns.length !== fk.references.length) fail3("foreign key columns and references must have the same number of columns");
|
|
2025
|
+
if (new Set(fk.columns).size !== fk.columns.length || new Set(fk.references).size !== fk.references.length) fail3("foreign key columns must not repeat");
|
|
2026
2026
|
for (const [position, name2] of fk.columns.entries()) {
|
|
2027
2027
|
const sourceColumn = table.columns.find((column) => column.columnName === name2);
|
|
2028
2028
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
2029
|
-
if (!sourceColumn || !targetColumn)
|
|
2030
|
-
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type))
|
|
2029
|
+
if (!sourceColumn || !targetColumn) fail3("foreign key references an unknown column");
|
|
2030
|
+
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail3("foreign key column types must match");
|
|
2031
2031
|
}
|
|
2032
2032
|
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
2033
|
-
if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2)))
|
|
2033
|
+
if (fk.columns.some((name2) => table.columns.find((column) => column.columnName === name2)?.nullable === false || table.primaryKey?.includes(name2))) fail3("SET NULL requires nullable source columns");
|
|
2034
2034
|
}
|
|
2035
2035
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index4) => index4.unique).map((index4) => index4.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
2036
|
-
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column))))
|
|
2036
|
+
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail3("foreign key target columns must form a unique key");
|
|
2037
2037
|
}
|
|
2038
2038
|
}
|
|
2039
2039
|
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
@@ -2221,8 +2221,8 @@ function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
|
|
|
2221
2221
|
return clone;
|
|
2222
2222
|
}
|
|
2223
2223
|
function planSource(input) {
|
|
2224
|
-
const { sourceName, input: source, fail:
|
|
2225
|
-
if (!IDENTIFIER_PATTERN.test(sourceName))
|
|
2224
|
+
const { sourceName, input: source, fail: fail3 } = input;
|
|
2225
|
+
if (!IDENTIFIER_PATTERN.test(sourceName)) fail3(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
|
|
2226
2226
|
switch (source.kind) {
|
|
2227
2227
|
case "manual":
|
|
2228
2228
|
return {
|
|
@@ -2262,27 +2262,27 @@ function assertColumns(input, list2, what) {
|
|
|
2262
2262
|
for (const column of list2) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2263
2263
|
}
|
|
2264
2264
|
function planEtl(input, source) {
|
|
2265
|
-
const { sourceName, pk, dataColumns, fail:
|
|
2265
|
+
const { sourceName, pk, dataColumns, fail: fail3 } = input;
|
|
2266
2266
|
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2267
|
-
if (!isSeqTable(table) || table.source === void 0)
|
|
2267
|
+
if (!isSeqTable(table) || table.source === void 0) fail3(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2268
2268
|
const synced = table;
|
|
2269
|
-
if (synced.config.primaryKey?.length === 0)
|
|
2270
|
-
if (!(pk in synced.columns))
|
|
2269
|
+
if (synced.config.primaryKey?.length === 0) fail3(`etl source "${sourceName}" cannot use an unkeyed synced table for resolved row identity`);
|
|
2270
|
+
if (!(pk in synced.columns)) fail3(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
|
|
2271
2271
|
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2272
2272
|
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2273
2273
|
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2274
|
-
|
|
2274
|
+
fail3(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2275
2275
|
}
|
|
2276
2276
|
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2277
2277
|
}
|
|
2278
2278
|
function planPrefetch(input, source) {
|
|
2279
|
-
const { sourceName, pk, fail:
|
|
2279
|
+
const { sourceName, pk, fail: fail3 } = input;
|
|
2280
2280
|
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2281
2281
|
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2282
|
-
|
|
2282
|
+
fail3(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2283
2283
|
}
|
|
2284
2284
|
for (const column of source.writeback?.columns ?? []) {
|
|
2285
|
-
if (!source.columns.includes(column))
|
|
2285
|
+
if (!source.columns.includes(column)) fail3(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2286
2286
|
}
|
|
2287
2287
|
const writeback = source.writeback;
|
|
2288
2288
|
return {
|
|
@@ -2488,34 +2488,34 @@ var SYSTEM_VIEW_COLUMNS = {
|
|
|
2488
2488
|
};
|
|
2489
2489
|
function defineResolvedTable(name2, columns, config) {
|
|
2490
2490
|
const file = activeAuthoringContext()?.currentFile;
|
|
2491
|
-
const
|
|
2491
|
+
const fail3 = (message) => {
|
|
2492
2492
|
throw new OrmDefinitionError(`resolved table "${name2}": ${message}`, file);
|
|
2493
2493
|
};
|
|
2494
|
-
if (!IDENTIFIER_PATTERN.test(name2))
|
|
2494
|
+
if (!IDENTIFIER_PATTERN.test(name2)) fail3(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2495
2495
|
for (const property of Object.keys(columns)) {
|
|
2496
2496
|
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2497
|
-
|
|
2497
|
+
fail3(`column "${property}" collides with a reserved system column`);
|
|
2498
2498
|
}
|
|
2499
2499
|
}
|
|
2500
2500
|
for (const property of Object.keys(columns)) {
|
|
2501
2501
|
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2502
|
-
|
|
2502
|
+
fail3(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2503
2503
|
}
|
|
2504
2504
|
}
|
|
2505
2505
|
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2506
|
-
if (pkEntries.length !== 1)
|
|
2506
|
+
if (pkEntries.length !== 1) fail3("declare exactly one .primaryKey() column");
|
|
2507
2507
|
const [pk, pkBuilder] = pkEntries[0];
|
|
2508
2508
|
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2509
|
-
if (dataColumns.length === 0)
|
|
2509
|
+
if (dataColumns.length === 0) fail3("declare at least one non-key column");
|
|
2510
2510
|
const sourceEntries = Object.entries(config.sources);
|
|
2511
|
-
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1)
|
|
2511
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail3("declare exactly one manualSource()");
|
|
2512
2512
|
const plans = sourceEntries.map(
|
|
2513
|
-
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail:
|
|
2513
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, pk, pkBuilder, dataColumns, fail: fail3 })
|
|
2514
2514
|
);
|
|
2515
|
-
const rules = planRules({ config, plans, dataColumns, fail:
|
|
2515
|
+
const rules = planRules({ config, plans, dataColumns, fail: fail3 });
|
|
2516
2516
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2517
2517
|
for (const source of deletedBy) {
|
|
2518
|
-
if (plans.find((plan) => plan.name === source)?.kind !== "manual")
|
|
2518
|
+
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail3(`rows.deletedBy "${source}" must be the manual source`);
|
|
2519
2519
|
}
|
|
2520
2520
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2521
2521
|
const viewColumns = {};
|
|
@@ -2578,11 +2578,11 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2578
2578
|
}
|
|
2579
2579
|
});
|
|
2580
2580
|
}
|
|
2581
|
-
function planRules({ config, plans, dataColumns, fail:
|
|
2581
|
+
function planRules({ config, plans, dataColumns, fail: fail3 }) {
|
|
2582
2582
|
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2583
2583
|
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2584
2584
|
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2585
|
-
if (!dataColumns.includes(column))
|
|
2585
|
+
if (!dataColumns.includes(column)) fail3(`resolve names unknown column "${column}"`);
|
|
2586
2586
|
}
|
|
2587
2587
|
return dataColumns.map((column) => {
|
|
2588
2588
|
const explicit = config.resolve?.[column];
|
|
@@ -2590,11 +2590,11 @@ function planRules({ config, plans, dataColumns, fail: fail2 }) {
|
|
|
2590
2590
|
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name2) => plans.find((plan) => plan.name === name2)?.columns.includes(column));
|
|
2591
2591
|
for (const candidate of candidates) {
|
|
2592
2592
|
const plan = plans.find((entry) => entry.name === candidate);
|
|
2593
|
-
if (!plan)
|
|
2594
|
-
else if (!plan.columns.includes(column))
|
|
2593
|
+
if (!plan) fail3(`column "${column}" names unknown source "${candidate}"`);
|
|
2594
|
+
else if (!plan.columns.includes(column)) fail3(`column "${column}" candidate "${candidate}" does not supply it`);
|
|
2595
2595
|
}
|
|
2596
|
-
if (candidates.length === 0)
|
|
2597
|
-
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail:
|
|
2596
|
+
if (candidates.length === 0) fail3(`column "${column}" has no candidate source`);
|
|
2597
|
+
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail: fail3 });
|
|
2598
2598
|
return {
|
|
2599
2599
|
column,
|
|
2600
2600
|
comparator: comparator.kind,
|
|
@@ -2645,18 +2645,18 @@ var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
2645
2645
|
]);
|
|
2646
2646
|
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2647
2647
|
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2648
|
-
function assertSafeOrderBy({ orderBy, column, fail:
|
|
2648
|
+
function assertSafeOrderBy({ orderBy, column, fail: fail3 }) {
|
|
2649
2649
|
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2650
2650
|
let consumed = 0;
|
|
2651
2651
|
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2652
2652
|
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2653
2653
|
const token = match[0].toLowerCase();
|
|
2654
2654
|
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2655
|
-
|
|
2655
|
+
fail3(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2656
2656
|
}
|
|
2657
2657
|
}
|
|
2658
2658
|
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2659
|
-
|
|
2659
|
+
fail3(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2660
2660
|
}
|
|
2661
2661
|
}
|
|
2662
2662
|
function stableJson(value) {
|
|
@@ -3184,17 +3184,17 @@ function buildCanonicalLink({
|
|
|
3184
3184
|
}) {
|
|
3185
3185
|
const ownerKind = relationKind(relation);
|
|
3186
3186
|
const ownerName = relationName(relation);
|
|
3187
|
-
const
|
|
3187
|
+
const fail3 = (message) => {
|
|
3188
3188
|
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
3189
3189
|
};
|
|
3190
3190
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
3191
|
-
return
|
|
3191
|
+
return fail3("sets enforce: true, which is only supported on toOne links");
|
|
3192
3192
|
}
|
|
3193
3193
|
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3194
|
-
return
|
|
3194
|
+
return fail3("sets a referential action without enforce: true");
|
|
3195
3195
|
}
|
|
3196
3196
|
if (link.linkKind === "toPlatformUser") {
|
|
3197
|
-
if (link.target !== void 0) return
|
|
3197
|
+
if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
|
|
3198
3198
|
return {
|
|
3199
3199
|
name: linkName,
|
|
3200
3200
|
kind: "toPlatformUser",
|
|
@@ -3205,7 +3205,7 @@ function buildCanonicalLink({
|
|
|
3205
3205
|
};
|
|
3206
3206
|
}
|
|
3207
3207
|
if (typeof link.target === "object" && link.target?.kind === "seq-external-object") {
|
|
3208
|
-
if (link.enforce) return
|
|
3208
|
+
if (link.enforce) return fail3("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
|
|
3209
3209
|
return {
|
|
3210
3210
|
name: linkName,
|
|
3211
3211
|
kind: link.linkKind,
|
|
@@ -3217,13 +3217,13 @@ function buildCanonicalLink({
|
|
|
3217
3217
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
3218
3218
|
};
|
|
3219
3219
|
}
|
|
3220
|
-
const target = resolveRelationRef({ ref: link.target, fail:
|
|
3220
|
+
const target = resolveRelationRef({ ref: link.target, fail: fail3 });
|
|
3221
3221
|
const targetName = relationName(target);
|
|
3222
3222
|
if (!relationsByName.has(targetName)) {
|
|
3223
|
-
return
|
|
3223
|
+
return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
3224
3224
|
}
|
|
3225
3225
|
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
3226
|
-
return
|
|
3226
|
+
return fail3("cannot enforce a PostgreSQL foreign key involving a view");
|
|
3227
3227
|
}
|
|
3228
3228
|
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
3229
3229
|
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
@@ -3239,19 +3239,19 @@ function buildCanonicalLink({
|
|
|
3239
3239
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
3240
3240
|
};
|
|
3241
3241
|
}
|
|
3242
|
-
function resolveRelationRef({ ref, fail:
|
|
3243
|
-
if (ref === void 0) return
|
|
3242
|
+
function resolveRelationRef({ ref, fail: fail3 }) {
|
|
3243
|
+
if (ref === void 0) return fail3("declares no target relation");
|
|
3244
3244
|
if (typeof ref !== "function") return ref;
|
|
3245
3245
|
let resolved;
|
|
3246
3246
|
try {
|
|
3247
3247
|
resolved = ref();
|
|
3248
3248
|
} catch (error) {
|
|
3249
|
-
return
|
|
3249
|
+
return fail3(
|
|
3250
3250
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
3251
3251
|
);
|
|
3252
3252
|
}
|
|
3253
3253
|
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
3254
|
-
return
|
|
3254
|
+
return fail3("target thunk did not return a relation defined with defineTable/defineView");
|
|
3255
3255
|
}
|
|
3256
3256
|
return resolved;
|
|
3257
3257
|
}
|
|
@@ -4123,19 +4123,19 @@ function compileComparison(op, left, right, columnTypes) {
|
|
|
4123
4123
|
}
|
|
4124
4124
|
return `${compileOperand(left, right, columnTypes)} ${SQL_COMPARE_OPS[op]} ${compileOperand(right, left, columnTypes)}`;
|
|
4125
4125
|
}
|
|
4126
|
-
function compilePredicate(predicate, columnTypes) {
|
|
4126
|
+
function compilePredicate(predicate, columnTypes, options = {}) {
|
|
4127
4127
|
switch (predicate.kind) {
|
|
4128
4128
|
case "and":
|
|
4129
4129
|
case "or": {
|
|
4130
|
-
const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes)).join(` ${predicate.kind.toUpperCase()} `);
|
|
4130
|
+
const joined = predicate.operands.map((operand) => compilePredicate(operand, columnTypes, options)).join(` ${predicate.kind.toUpperCase()} `);
|
|
4131
4131
|
return `(${joined})`;
|
|
4132
4132
|
}
|
|
4133
4133
|
case "not":
|
|
4134
|
-
return `(NOT ${compilePredicate(predicate.operand, columnTypes)})`;
|
|
4134
|
+
return `(NOT ${compilePredicate(predicate.operand, columnTypes, options)})`;
|
|
4135
4135
|
case "compare":
|
|
4136
4136
|
return `(${compileComparison(predicate.op, predicate.left, predicate.right, columnTypes)})`;
|
|
4137
4137
|
case "capability":
|
|
4138
|
-
return `(${capabilityExpression(predicate.key)})`;
|
|
4138
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(predicate.key)})`;
|
|
4139
4139
|
}
|
|
4140
4140
|
}
|
|
4141
4141
|
|
|
@@ -4221,9 +4221,9 @@ function compilePredIrToSql(pred, options = {}) {
|
|
|
4221
4221
|
case "cmp":
|
|
4222
4222
|
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
4223
4223
|
case "capability":
|
|
4224
|
-
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4224
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4225
4225
|
case "role":
|
|
4226
|
-
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4226
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4227
4227
|
case "exists":
|
|
4228
4228
|
if (options.exists) return options.exists(pred);
|
|
4229
4229
|
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
@@ -4799,7 +4799,9 @@ function syncedViewStatements({
|
|
|
4799
4799
|
columnNamesByTable,
|
|
4800
4800
|
columnTypesByTable,
|
|
4801
4801
|
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE,
|
|
4802
|
-
backingPrivileges = "grant"
|
|
4802
|
+
backingPrivileges = "grant",
|
|
4803
|
+
viewPrivileges = "grant",
|
|
4804
|
+
statementClaimChecks = true
|
|
4803
4805
|
}) {
|
|
4804
4806
|
const schema = quoteIdentifier(ormSchemaName(definition.namespace));
|
|
4805
4807
|
const role = quoteIdentifier(consumerRoleName(definition.namespace));
|
|
@@ -4842,9 +4844,10 @@ function syncedViewStatements({
|
|
|
4842
4844
|
const readPolicy = table.policies?.read;
|
|
4843
4845
|
const policyClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
|
|
4844
4846
|
namespace: definition.namespace,
|
|
4847
|
+
statementClaimChecks,
|
|
4845
4848
|
columnTypes,
|
|
4846
4849
|
columnNames: columnNamesByTable?.get(table.table)
|
|
4847
|
-
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
4850
|
+
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes), { statementClaimChecks })}` : " WHERE false";
|
|
4848
4851
|
if (backingPrivileges === "grant") {
|
|
4849
4852
|
const selectPrivilege = backing.selectColumns ? `SELECT (${backing.selectColumns.map(quoteIdentifier).join(", ")})` : "SELECT";
|
|
4850
4853
|
const hasSelectPrivilege = backing.selectColumns ? backing.selectColumns.map((column) => `pg_catalog.has_column_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physicalTarget)}, ${quoteLiteral(column)}, 'SELECT')`).join("\n AND ") : `pg_catalog.has_table_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physicalTarget)}, 'SELECT')`;
|
|
@@ -4861,7 +4864,7 @@ END`)}`);
|
|
|
4861
4864
|
statements.push(
|
|
4862
4865
|
`CREATE OR REPLACE VIEW ${viewName} AS SELECT ${projection} FROM ${physicalTarget}${whereClause}`
|
|
4863
4866
|
);
|
|
4864
|
-
statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
|
|
4867
|
+
if (viewPrivileges === "grant") statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
|
|
4865
4868
|
}
|
|
4866
4869
|
return statements;
|
|
4867
4870
|
}
|
|
@@ -5199,15 +5202,15 @@ var sqlValues = /* @__PURE__ */ new Set([
|
|
|
5199
5202
|
async function validateManagedViewQueries({ canonical }) {
|
|
5200
5203
|
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
5201
5204
|
for (const view of definition.views ?? []) {
|
|
5202
|
-
const
|
|
5205
|
+
const fail3 = (message) => {
|
|
5203
5206
|
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
5204
5207
|
};
|
|
5205
5208
|
parser ??= new pgParser.PgParser({ version: 17 });
|
|
5206
5209
|
const parsed = await parser.parse(view.query);
|
|
5207
|
-
if (parsed.error || !parsed.tree)
|
|
5210
|
+
if (parsed.error || !parsed.tree) fail3("invalid SELECT query");
|
|
5208
5211
|
const statements = list(obj(parsed.tree).stmts);
|
|
5209
|
-
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt)
|
|
5210
|
-
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail:
|
|
5212
|
+
if (statements.length !== 1 || !obj(obj(statements[0]).stmt).SelectStmt) fail3("one SELECT query is required");
|
|
5213
|
+
validateTree({ tree: parsed.tree, schema: ormSchemaName(definition.namespace), enums: definition.enums.map((entry) => entry.name), fail: fail3 });
|
|
5211
5214
|
}
|
|
5212
5215
|
}
|
|
5213
5216
|
}
|
|
@@ -5236,29 +5239,29 @@ function validateTree({ tree, ...scope }) {
|
|
|
5236
5239
|
}
|
|
5237
5240
|
}
|
|
5238
5241
|
function validateNode({ kind, child, scope }) {
|
|
5239
|
-
const { fail:
|
|
5240
|
-
if (kind.endsWith("Stmt") && kind !== "SelectStmt")
|
|
5242
|
+
const { fail: fail3 } = scope;
|
|
5243
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail3("only SELECT statements are permitted");
|
|
5241
5244
|
if (kind === "FuncCall") {
|
|
5242
5245
|
const call = obj(child);
|
|
5243
5246
|
const parts = strings(call.funcname);
|
|
5244
5247
|
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
5245
|
-
if (!name2 || !functions.has(name2))
|
|
5248
|
+
if (!name2 || !functions.has(name2)) fail3(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
5246
5249
|
}
|
|
5247
5250
|
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
5248
|
-
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0)
|
|
5249
|
-
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0)
|
|
5250
|
-
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op)))
|
|
5251
|
-
if (kind === "ParamRef")
|
|
5251
|
+
if (kind === "A_Expr" && pgCatalogName(strings(obj(child).name)) === void 0) fail3("only pg_catalog operators are permitted");
|
|
5252
|
+
if (kind === "SortBy" && list(obj(child).useOp).length && pgCatalogName(strings(obj(child).useOp)) === void 0) fail3("only pg_catalog sort operators are permitted");
|
|
5253
|
+
if (kind === "SQLValueFunction" && !sqlValues.has(String(obj(child).op))) fail3("unsupported SQL value expression");
|
|
5254
|
+
if (kind === "ParamRef") fail3("view queries cannot contain parameters");
|
|
5252
5255
|
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
5253
|
-
|
|
5256
|
+
fail3("composite field notation is not permitted in managed views");
|
|
5254
5257
|
}
|
|
5255
5258
|
}
|
|
5256
|
-
function validateType({ node, schema, enums, fail:
|
|
5259
|
+
function validateType({ node, schema, enums, fail: fail3 }) {
|
|
5257
5260
|
const parts = strings(node.names);
|
|
5258
5261
|
const builtin = pgCatalogName(parts);
|
|
5259
5262
|
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
5260
5263
|
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
5261
|
-
|
|
5264
|
+
fail3(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
5262
5265
|
}
|
|
5263
5266
|
}
|
|
5264
5267
|
|
|
@@ -6470,6 +6473,118 @@ async function guardedRun(run) {
|
|
|
6470
6473
|
process.exit = originalExit;
|
|
6471
6474
|
}
|
|
6472
6475
|
}
|
|
6476
|
+
var parser2;
|
|
6477
|
+
var locations = /* @__PURE__ */ new Set(["location", "stmt_location", "stmt_len"]);
|
|
6478
|
+
async function assertSyncedViewDefinitionMatches({
|
|
6479
|
+
viewDefinition,
|
|
6480
|
+
expectedCreateStatements
|
|
6481
|
+
}) {
|
|
6482
|
+
if (!expectedCreateStatements.length) fail2();
|
|
6483
|
+
const expected = [];
|
|
6484
|
+
for (const statement of expectedCreateStatements) {
|
|
6485
|
+
const node = await parseStatement(statement);
|
|
6486
|
+
const view = obj(node.ViewStmt);
|
|
6487
|
+
requireKeys(view, ["view", "query", "replace", "withCheckOption"]);
|
|
6488
|
+
if (view.replace !== true || view.withCheckOption !== "NO_CHECK_OPTION") fail2();
|
|
6489
|
+
backingOf(obj(view.view));
|
|
6490
|
+
expected.push(canonicalSelect(obj(obj(view.query).SelectStmt)));
|
|
6491
|
+
}
|
|
6492
|
+
const actual = await parseStatement(viewDefinition);
|
|
6493
|
+
if (!expected.includes(canonicalSelect(obj(actual.SelectStmt)))) fail2();
|
|
6494
|
+
}
|
|
6495
|
+
function fail2() {
|
|
6496
|
+
throw new OrmDefinitionError("synced view definition does not match the compiler");
|
|
6497
|
+
}
|
|
6498
|
+
async function parseStatement(sql7) {
|
|
6499
|
+
if (!sql7.length || sql7.length > 262144) fail2();
|
|
6500
|
+
parser2 ??= new pgParser.PgParser({ version: 17 });
|
|
6501
|
+
const parsed = await parser2.parse(sql7);
|
|
6502
|
+
if (parsed.error || !parsed.tree) fail2();
|
|
6503
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
6504
|
+
if (statements.length !== 1) fail2();
|
|
6505
|
+
return obj(obj(statements[0]).stmt);
|
|
6506
|
+
}
|
|
6507
|
+
function requireKeys(node, allowed) {
|
|
6508
|
+
for (const key of Object.keys(node)) if (!locations.has(key) && !allowed.includes(key)) fail2();
|
|
6509
|
+
}
|
|
6510
|
+
function backingOf(node) {
|
|
6511
|
+
requireKeys(node, ["catalogname", "schemaname", "relname", "inh", "relpersistence"]);
|
|
6512
|
+
if (node.catalogname !== "" || node.inh !== true || node.relpersistence !== "p") fail2();
|
|
6513
|
+
const schema = str(node.schemaname);
|
|
6514
|
+
const relation = str(node.relname);
|
|
6515
|
+
if (!schema || !relation) fail2();
|
|
6516
|
+
return { schema, relation };
|
|
6517
|
+
}
|
|
6518
|
+
function validateSelect(select, scalar2) {
|
|
6519
|
+
requireKeys(select, ["targetList", "fromClause", "whereClause", "groupDistinct", "limitOption", "op", "all"]);
|
|
6520
|
+
if (select.groupDistinct !== false || select.limitOption !== "LIMIT_OPTION_DEFAULT" || select.op !== "SETOP_NONE" || select.all !== false) fail2();
|
|
6521
|
+
if (scalar2 && (list(select.fromClause).length || select.whereClause !== void 0 || list(select.targetList).length !== 1)) fail2();
|
|
6522
|
+
}
|
|
6523
|
+
function canonicalSelect(select) {
|
|
6524
|
+
validateSelect(select, false);
|
|
6525
|
+
const from = list(select.fromClause);
|
|
6526
|
+
if (from.length !== 1) fail2();
|
|
6527
|
+
const backing = backingOf(obj(obj(from[0]).RangeVar));
|
|
6528
|
+
const targets = list(select.targetList);
|
|
6529
|
+
if (!targets.length) fail2();
|
|
6530
|
+
for (const target of targets) {
|
|
6531
|
+
const node = obj(obj(target).ResTarget);
|
|
6532
|
+
requireKeys(node, ["name", "val"]);
|
|
6533
|
+
if (node.name !== "" || !obj(node.val).ColumnRef) fail2();
|
|
6534
|
+
}
|
|
6535
|
+
return JSON.stringify(normalize({ value: select, backing, scalar: false }));
|
|
6536
|
+
}
|
|
6537
|
+
function normalize({ value, backing, scalar: scalar2 }) {
|
|
6538
|
+
if (Array.isArray(value)) return value.map((entry) => normalize({ value: entry, backing, scalar: scalar2 }));
|
|
6539
|
+
if (!value || typeof value !== "object") return value;
|
|
6540
|
+
const node = obj(value);
|
|
6541
|
+
if (node.ColumnRef) return normalizeColumnRef({ ref: obj(node.ColumnRef), backing, scalar: scalar2 });
|
|
6542
|
+
if (node.TypeCast && isLiteralTextCast(obj(node.TypeCast))) {
|
|
6543
|
+
return normalize({ value: obj(node.TypeCast).arg, backing, scalar: scalar2 });
|
|
6544
|
+
}
|
|
6545
|
+
if (node.SubLink) return normalizeScalarSubLink({ sublink: obj(node.SubLink), backing });
|
|
6546
|
+
const result = {};
|
|
6547
|
+
for (const key of Object.keys(node).sort()) {
|
|
6548
|
+
if (locations.has(key)) continue;
|
|
6549
|
+
if (key === "ParamRef" || key === "RangeSubselect" || key === "JoinExpr" || key.endsWith("Stmt") && key !== "SelectStmt" || scalar2 && key === "RangeVar") fail2();
|
|
6550
|
+
result[key] = normalize({ value: node[key], backing, scalar: scalar2 });
|
|
6551
|
+
}
|
|
6552
|
+
return result;
|
|
6553
|
+
}
|
|
6554
|
+
function normalizeColumnRef({ ref, backing, scalar: scalar2 }) {
|
|
6555
|
+
if (scalar2) fail2();
|
|
6556
|
+
requireKeys(ref, ["fields"]);
|
|
6557
|
+
const fields = strings(ref.fields);
|
|
6558
|
+
const qualified = fields.length === 2 && fields[0] === backing.relation || fields.length === 3 && fields[0] === backing.schema && fields[1] === backing.relation;
|
|
6559
|
+
if (fields.length !== 1 && !qualified) fail2();
|
|
6560
|
+
const column = fields.at(-1);
|
|
6561
|
+
if (!column) fail2();
|
|
6562
|
+
return { ColumnRef: { fields: [{ String: { sval: column } }] } };
|
|
6563
|
+
}
|
|
6564
|
+
function normalizeScalarSubLink({ sublink, backing }) {
|
|
6565
|
+
requireKeys(sublink, ["subLinkType", "subLinkId", "subselect"]);
|
|
6566
|
+
if (sublink.subLinkType !== "EXPR_SUBLINK" || sublink.subLinkId !== 0) fail2();
|
|
6567
|
+
const select = obj(obj(sublink.subselect).SelectStmt);
|
|
6568
|
+
validateSelect(select, true);
|
|
6569
|
+
const target = obj(obj(list(select.targetList)[0]).ResTarget);
|
|
6570
|
+
requireKeys(target, ["name", "val"]);
|
|
6571
|
+
const normalizedSelect = { ...select, targetList: [{ ResTarget: { ...target, name: "" } }] };
|
|
6572
|
+
return { SubLink: normalize({ value: { ...sublink, subselect: { SelectStmt: normalizedSelect } }, backing, scalar: true }) };
|
|
6573
|
+
}
|
|
6574
|
+
function isLiteralTextCast(cast) {
|
|
6575
|
+
requireKeys(cast, ["arg", "typeName"]);
|
|
6576
|
+
const arg = obj(cast.arg);
|
|
6577
|
+
if (!arg.A_Const) return false;
|
|
6578
|
+
const constant = obj(arg.A_Const);
|
|
6579
|
+
if (constant.isnull !== false || !constant.sval || typeof obj(constant.sval).sval !== "string") return false;
|
|
6580
|
+
const type = obj(cast.typeName);
|
|
6581
|
+
const names = strings(type.names);
|
|
6582
|
+
if (!(names.length === 1 && names[0] === "text" || names.length === 2 && names[0] === "pg_catalog" && names[1] === "text")) return false;
|
|
6583
|
+
if (list(type.arrayBounds).length || list(type.typmods).length) return false;
|
|
6584
|
+
requireKeys(type, ["names", "typeOid", "setof", "pct_type", "typemod"]);
|
|
6585
|
+
if (type.typeOid !== 0 || type.setof !== false || type.pct_type !== false || type.typemod !== -1) return false;
|
|
6586
|
+
return true;
|
|
6587
|
+
}
|
|
6473
6588
|
|
|
6474
6589
|
// src/engine/index.ts
|
|
6475
6590
|
var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
|
|
@@ -6539,6 +6654,11 @@ function renderOrmMigrationExport(exported) {
|
|
|
6539
6654
|
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
6540
6655
|
return text4;
|
|
6541
6656
|
}
|
|
6657
|
+
|
|
6658
|
+
// src/client/sql.ts
|
|
6659
|
+
var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
|
|
6660
|
+
|
|
6661
|
+
// src/ir/index.ts
|
|
6542
6662
|
var IR_VERSION = 5;
|
|
6543
6663
|
var LEGACY_IR_VERSION = 1;
|
|
6544
6664
|
var GUARDED_IR_VERSION = 2;
|
|
@@ -6871,7 +6991,8 @@ var compiledOperationSchema = zod.z.object({
|
|
|
6871
6991
|
}).superRefine((operation, ctx) => {
|
|
6872
6992
|
if (operation.language === "sql") {
|
|
6873
6993
|
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
6874
|
-
|
|
6994
|
+
const builtin = operation.name === "BuiltinSqlRead" && operation.opHash === BUILTIN_SQL_READ_HASH && operation.requires === null;
|
|
6995
|
+
if (!validPlan || operation.kind !== "query" || operation.roots.length !== 0 || !builtin && !operation.requires?.capability) {
|
|
6875
6996
|
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
6876
6997
|
}
|
|
6877
6998
|
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
@@ -8343,6 +8464,7 @@ exports.actionsInterfaceName = actionsInterfaceName;
|
|
|
8343
8464
|
exports.activeAuthoringContext = activeAuthoringContext;
|
|
8344
8465
|
exports.applyMigrationHints = applyMigrationHints;
|
|
8345
8466
|
exports.applyMigrationHintsV2 = applyMigrationHintsV2;
|
|
8467
|
+
exports.assertSyncedViewDefinitionMatches = assertSyncedViewDefinitionMatches;
|
|
8346
8468
|
exports.beginAuthoringContext = beginAuthoringContext;
|
|
8347
8469
|
exports.buildActionParamsSchema = buildActionParamsSchema;
|
|
8348
8470
|
exports.buildDrizzleEntities = buildDrizzleEntities;
|