@sequenceholdings/orm 2.4.0 → 2.6.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 +23 -0
- package/dist/authoring.cjs +53 -7
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +10 -6
- package/dist/authoring.d.ts +10 -6
- package/dist/authoring.js +53 -7
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +48 -2
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +1 -1
- package/dist/canonical.d.ts +1 -1
- package/dist/canonical.js +48 -2
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +260 -31
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +260 -31
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +11 -6
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +38 -31
- package/dist/client.d.ts +38 -31
- package/dist/client.js +11 -7
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +513 -23
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +39 -3
- package/dist/compiler.d.ts +39 -3
- package/dist/compiler.js +510 -24
- package/dist/compiler.js.map +1 -1
- package/dist/{constants-C6EG6Wq7.d.cts → constants-Dou1RkTo.d.cts} +7 -2
- package/dist/{constants-C6EG6Wq7.d.ts → constants-Dou1RkTo.d.ts} +7 -2
- package/dist/dynamic-input.d.cts +1 -1
- package/dist/dynamic-input.d.ts +1 -1
- package/dist/index.cjs +461 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +460 -141
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +55 -4
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +1 -1
- package/dist/ir.d.ts +1 -1
- package/dist/ir.js +55 -4
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +55 -4
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +1 -1
- package/dist/predicate-v2.d.ts +1 -1
- package/dist/predicate-v2.js +55 -4
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +60 -4
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +3 -1
- package/dist/sdl.d.ts +3 -1
- package/dist/sdl.js +60 -4
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -9,6 +9,7 @@ var promises = require('fs/promises');
|
|
|
9
9
|
var module$1 = require('module');
|
|
10
10
|
var path = require('path');
|
|
11
11
|
var api = require('drizzle-kit/api');
|
|
12
|
+
var pgParser = require('@supabase/pg-parser');
|
|
12
13
|
var jsYaml = require('js-yaml');
|
|
13
14
|
var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
|
|
14
15
|
|
|
@@ -157,20 +158,24 @@ var t = {
|
|
|
157
158
|
};
|
|
158
159
|
|
|
159
160
|
// src/constants.ts
|
|
160
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
161
|
+
var NAMESPACE_PATTERN = /^(?!(?:events|native_sources)$)[a-z][a-z0-9_]{0,40}$/;
|
|
161
162
|
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
162
163
|
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
163
164
|
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
164
165
|
var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
165
166
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
166
167
|
var CAPABILITY_KEY_PATTERN = /^(?:org\/|app\/[a-z][a-z0-9-]{0,63}\/)?[a-z0-9-]{1,64}$/;
|
|
168
|
+
var MAX_PERSISTED_READ_ROWS = 50001;
|
|
167
169
|
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
168
170
|
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]{0,62}\.[a-z][a-z0-9_]{0,62}$/;
|
|
169
171
|
var SYNCED_SOURCE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
170
172
|
var ORM_SCHEMA_PREFIX = "orm_";
|
|
171
173
|
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
172
|
-
var ormSchemaName = (namespace) =>
|
|
173
|
-
|
|
174
|
+
var ormSchemaName = (namespace) => {
|
|
175
|
+
if (!NAMESPACE_PATTERN.test(namespace)) throw new Error(`invalid or reserved ORM namespace '${namespace}'`);
|
|
176
|
+
return `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
177
|
+
};
|
|
178
|
+
var consumerRoleName = (namespace) => `${ormSchemaName(namespace)}${CONSUMER_ROLE_SUFFIX}`;
|
|
174
179
|
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
175
180
|
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
176
181
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
@@ -399,10 +404,10 @@ function defineSyncedTable(name2, columnsOrConfig, maybeConfig) {
|
|
|
399
404
|
if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
|
|
400
405
|
}
|
|
401
406
|
if (entries.length > 0) {
|
|
402
|
-
const hasConfigPk = config.primaryKey !== void 0
|
|
407
|
+
const hasConfigPk = config.primaryKey !== void 0;
|
|
403
408
|
if (columnLevelPk.length === 0 && !hasConfigPk) {
|
|
404
409
|
throw new OrmDefinitionError(
|
|
405
|
-
`synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey()
|
|
410
|
+
`synced table "${name2}" declares columns but no primary key \u2014 use .primaryKey(), config.primaryKey, or explicit primaryKey: [] for an unkeyed read-only source`,
|
|
406
411
|
file
|
|
407
412
|
);
|
|
408
413
|
}
|
|
@@ -1255,10 +1260,10 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1255
1260
|
const resolved = view.resolved;
|
|
1256
1261
|
if (!resolved) return [];
|
|
1257
1262
|
const errors = [];
|
|
1258
|
-
const
|
|
1263
|
+
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1259
1264
|
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1260
1265
|
if (!pkColumn) {
|
|
1261
|
-
|
|
1266
|
+
fail3("requires exactly one primary-key column");
|
|
1262
1267
|
return errors;
|
|
1263
1268
|
}
|
|
1264
1269
|
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
@@ -1268,92 +1273,93 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1268
1273
|
pkColumn,
|
|
1269
1274
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1270
1275
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
1271
|
-
fail
|
|
1276
|
+
fail: fail3
|
|
1272
1277
|
};
|
|
1273
|
-
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");
|
|
1274
1279
|
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1275
1280
|
for (const source of resolved.sources) {
|
|
1276
|
-
if (scope.sourceByName.has(source.name))
|
|
1281
|
+
if (scope.sourceByName.has(source.name)) fail3(`duplicate source '${source.name}'`);
|
|
1277
1282
|
scope.sourceByName.set(source.name, source);
|
|
1278
1283
|
const table = tableByName.get(source.table);
|
|
1279
|
-
if (!table)
|
|
1284
|
+
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1280
1285
|
else validateSource({ scope, source, table });
|
|
1281
1286
|
}
|
|
1282
1287
|
validateRules(scope);
|
|
1283
1288
|
validateViewCells(scope);
|
|
1284
1289
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1285
1290
|
for (const name2 of resolved.deletedBy) {
|
|
1286
|
-
if (scope.sourceByName.get(name2)?.kind !== "manual")
|
|
1291
|
+
if (scope.sourceByName.get(name2)?.kind !== "manual") fail3(`deletedBy '${name2}' must be the manual source`);
|
|
1287
1292
|
}
|
|
1288
1293
|
return errors;
|
|
1289
1294
|
}
|
|
1290
1295
|
function validateSource({ scope, source, table }) {
|
|
1291
|
-
const { fail } = scope;
|
|
1292
|
-
if (source.kind === "etl" && table.source === void 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`);
|
|
1293
1299
|
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1294
|
-
|
|
1300
|
+
fail3(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1295
1301
|
}
|
|
1296
1302
|
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1297
|
-
if (source.kind === "manual" && !hasRev)
|
|
1298
|
-
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`);
|
|
1299
1305
|
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1300
1306
|
validateSourceColumns({ scope, source, byName });
|
|
1301
1307
|
if (source.kind === "etl") {
|
|
1302
1308
|
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1303
1309
|
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1304
|
-
|
|
1310
|
+
fail3(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1305
1311
|
}
|
|
1306
1312
|
}
|
|
1307
1313
|
validatePrefetchFields({ scope, source });
|
|
1308
1314
|
}
|
|
1309
1315
|
function validatePrefetchFields({ scope, source }) {
|
|
1310
|
-
const { fail, pkColumn } = scope;
|
|
1316
|
+
const { fail: fail3, pkColumn } = scope;
|
|
1311
1317
|
if (source.kind !== "prefetch") {
|
|
1312
1318
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1313
|
-
|
|
1319
|
+
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1314
1320
|
}
|
|
1315
1321
|
return;
|
|
1316
1322
|
}
|
|
1317
1323
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1318
|
-
|
|
1324
|
+
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1319
1325
|
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1320
|
-
|
|
1326
|
+
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1321
1327
|
}
|
|
1322
1328
|
for (const column of source.writeback?.columns ?? []) {
|
|
1323
|
-
if (!source.columns.includes(column))
|
|
1329
|
+
if (!source.columns.includes(column)) fail3(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1324
1330
|
}
|
|
1325
1331
|
}
|
|
1326
1332
|
function validateSourceColumns({ scope, source, byName }) {
|
|
1327
|
-
const { fail, pkColumn, dataColumns } = scope;
|
|
1333
|
+
const { fail: fail3, pkColumn, dataColumns } = scope;
|
|
1328
1334
|
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1329
1335
|
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1330
1336
|
const theirs = byName.get(column);
|
|
1331
|
-
if (!own)
|
|
1332
|
-
else if (!theirs)
|
|
1333
|
-
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}'`);
|
|
1334
1340
|
}
|
|
1335
1341
|
}
|
|
1336
|
-
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
1342
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fail3 }) {
|
|
1337
1343
|
const ruled = /* @__PURE__ */ new Set();
|
|
1338
1344
|
for (const rule of resolved.resolve) {
|
|
1339
1345
|
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1340
|
-
|
|
1346
|
+
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
1341
1347
|
continue;
|
|
1342
1348
|
}
|
|
1343
|
-
if (ruled.has(rule.column))
|
|
1349
|
+
if (ruled.has(rule.column)) fail3(`column '${rule.column}' has more than one resolve rule`);
|
|
1344
1350
|
ruled.add(rule.column);
|
|
1345
1351
|
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1346
|
-
|
|
1352
|
+
fail3(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1347
1353
|
}
|
|
1348
|
-
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`);
|
|
1349
1355
|
for (const candidate of rule.candidates) {
|
|
1350
1356
|
const source = sourceByName.get(candidate);
|
|
1351
|
-
if (!source)
|
|
1352
|
-
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`);
|
|
1353
1359
|
}
|
|
1354
1360
|
}
|
|
1355
1361
|
for (const column of dataColumns.keys()) {
|
|
1356
|
-
if (column !== pkColumn.name && !ruled.has(column))
|
|
1362
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
1357
1363
|
}
|
|
1358
1364
|
}
|
|
1359
1365
|
var VIEW_CELL_TYPES = {
|
|
@@ -1362,7 +1368,7 @@ var VIEW_CELL_TYPES = {
|
|
|
1362
1368
|
by: "text",
|
|
1363
1369
|
conflicts: "jsonb"
|
|
1364
1370
|
};
|
|
1365
|
-
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
1371
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail: fail3 }) {
|
|
1366
1372
|
const expected = /* @__PURE__ */ new Map();
|
|
1367
1373
|
for (const column of dataColumns.values()) {
|
|
1368
1374
|
if (column.name === pkColumn.name) continue;
|
|
@@ -1374,34 +1380,34 @@ function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
|
1374
1380
|
if (!column.cell) continue;
|
|
1375
1381
|
const want = expected.get(column.name);
|
|
1376
1382
|
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1377
|
-
|
|
1383
|
+
fail3(`unexpected cell column '${column.name}'`);
|
|
1378
1384
|
continue;
|
|
1379
1385
|
}
|
|
1380
|
-
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]}`);
|
|
1381
1387
|
expected.delete(column.name);
|
|
1382
1388
|
}
|
|
1383
|
-
for (const name2 of expected.keys())
|
|
1389
|
+
for (const name2 of expected.keys()) fail3(`missing cell column '${name2}'`);
|
|
1384
1390
|
}
|
|
1385
1391
|
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1386
1392
|
function validateMaterialized({ scope, tableByName, views }) {
|
|
1387
|
-
const { view, resolved, fail } = scope;
|
|
1393
|
+
const { view, resolved, fail: fail3 } = scope;
|
|
1388
1394
|
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1389
1395
|
const table = tableByName.get(tableName);
|
|
1390
1396
|
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1391
|
-
|
|
1397
|
+
fail3(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1392
1398
|
} else {
|
|
1393
|
-
if (columnSignature(table.columns) !== columnSignature(view.columns))
|
|
1394
|
-
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`);
|
|
1395
1401
|
}
|
|
1396
1402
|
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1397
|
-
if (!compute || compute.internal !== true)
|
|
1398
|
-
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`);
|
|
1399
1405
|
for (const source of resolved.sources) {
|
|
1400
1406
|
if (source.kind !== "etl") continue;
|
|
1401
1407
|
const read = tableByName.get(source.table)?.policies?.read;
|
|
1402
1408
|
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1403
1409
|
if (!unconditional) {
|
|
1404
|
-
|
|
1410
|
+
fail3(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1405
1411
|
}
|
|
1406
1412
|
}
|
|
1407
1413
|
}
|
|
@@ -1738,7 +1744,7 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1738
1744
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1739
1745
|
source: zod.z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
1740
1746
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
1741
|
-
primaryKey: zod.z.array(columnIdentifier).
|
|
1747
|
+
primaryKey: zod.z.array(columnIdentifier).max(8).optional(),
|
|
1742
1748
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1743
1749
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
1744
1750
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
@@ -1774,6 +1780,29 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1774
1780
|
* data plane. Omitted otherwise.
|
|
1775
1781
|
*/
|
|
1776
1782
|
materializes: identifier2.optional()
|
|
1783
|
+
}).superRefine((table, ctx) => {
|
|
1784
|
+
if (table.primaryKey?.length !== 0) return;
|
|
1785
|
+
const fail3 = (message, path = ["primaryKey"]) => ctx.addIssue({ code: "custom", path, message: `unkeyed table '${table.table}' ${message}` });
|
|
1786
|
+
if (table.source === void 0 || table.columns.length === 0) {
|
|
1787
|
+
fail3("requires a typed synced source");
|
|
1788
|
+
}
|
|
1789
|
+
if ([table.policies.write, table.policies.insert, table.policies.update].some((policy) => policy !== void 0)) {
|
|
1790
|
+
fail3("is read-only and cannot declare write policies", ["policies"]);
|
|
1791
|
+
}
|
|
1792
|
+
const ownedCollections = [table.indexes, table.actions, table.checks, table.foreignKeys, table.cowMutations];
|
|
1793
|
+
const ownedMetadata = [table.cellMetadata, table.internal, table.materializes, table.renamedFrom];
|
|
1794
|
+
if (ownedCollections.some((items) => items !== void 0 && items.length > 0) || ownedMetadata.some((value) => value !== void 0)) {
|
|
1795
|
+
fail3("cannot declare owned storage, constraints or identity operations");
|
|
1796
|
+
}
|
|
1797
|
+
for (const [index3, column] of table.columns.entries()) {
|
|
1798
|
+
const ownedColumnMetadata = [column.default, column.defaultFn, column.onUpdateFn, column.renamedFrom, column.cast, column.cell];
|
|
1799
|
+
if (column.unique || ownedColumnMetadata.some((value) => value !== void 0)) {
|
|
1800
|
+
fail3("cannot declare column uniqueness, mutation defaults or migration metadata", ["columns", index3]);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
for (const [index3, link] of table.links.entries()) {
|
|
1804
|
+
if (link.enforce || link.kind === "toMany") fail3("cannot provide an identity for this link", ["links", index3]);
|
|
1805
|
+
}
|
|
1777
1806
|
});
|
|
1778
1807
|
var MAX_VIEW_QUERY = 1e5;
|
|
1779
1808
|
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
@@ -1830,6 +1859,26 @@ var canonicalViewSchema = zod.z.object({
|
|
|
1830
1859
|
}
|
|
1831
1860
|
}
|
|
1832
1861
|
});
|
|
1862
|
+
function validateUnkeyedLinkIdentities({ definition, ctx }) {
|
|
1863
|
+
const tables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1864
|
+
const relations = [
|
|
1865
|
+
...definition.tables.map((table, index3) => ({ kind: "tables", index: index3, links: table.links })),
|
|
1866
|
+
...(definition.views ?? []).map((view, index3) => ({ kind: "views", index: index3, links: view.links }))
|
|
1867
|
+
];
|
|
1868
|
+
for (const relation of relations) {
|
|
1869
|
+
for (const [linkIndex, link] of relation.links.entries()) {
|
|
1870
|
+
if (link.kind !== "toOne" || link.namespace !== void 0 && link.namespace !== definition.namespace) continue;
|
|
1871
|
+
const target = link.target === void 0 ? void 0 : tables.get(link.target);
|
|
1872
|
+
if (target?.source !== void 0 && target.primaryKey?.length === 0) {
|
|
1873
|
+
ctx.addIssue({
|
|
1874
|
+
code: "custom",
|
|
1875
|
+
path: [relation.kind, relation.index, "links", linkIndex],
|
|
1876
|
+
message: `link '${link.name}' cannot use unkeyed table '${target.table}' as a to-one identity`
|
|
1877
|
+
});
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1833
1882
|
var CELL_PART_TYPES = {
|
|
1834
1883
|
at: "timestamptz",
|
|
1835
1884
|
by: "text",
|
|
@@ -1843,49 +1892,49 @@ function trackedCellColumns(table) {
|
|
|
1843
1892
|
}
|
|
1844
1893
|
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1845
1894
|
function validateCellMetadata(table, index3, ctx) {
|
|
1846
|
-
const
|
|
1895
|
+
const fail3 = (message, path = ["tables", index3]) => ctx.addIssue({ code: "custom", path, message });
|
|
1847
1896
|
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1848
1897
|
if (table.materializes !== void 0) return;
|
|
1849
1898
|
if (table.cellMetadata !== true) {
|
|
1850
|
-
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]);
|
|
1851
1900
|
return;
|
|
1852
1901
|
}
|
|
1853
1902
|
if (table.source !== void 0) {
|
|
1854
|
-
|
|
1903
|
+
fail3(`synced table '${table.table}' cannot track cell metadata`);
|
|
1855
1904
|
return;
|
|
1856
1905
|
}
|
|
1857
1906
|
const tracked = trackedCellColumns(table);
|
|
1858
1907
|
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1859
1908
|
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1860
|
-
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}')`);
|
|
1861
1910
|
const seen = /* @__PURE__ */ new Set();
|
|
1862
1911
|
for (const { column, position, cell } of cells) {
|
|
1863
1912
|
const path = ["tables", index3, "columns", position];
|
|
1864
1913
|
const owner = byName.get(cell.of);
|
|
1865
1914
|
if (!owner) {
|
|
1866
|
-
|
|
1915
|
+
fail3(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1867
1916
|
continue;
|
|
1868
1917
|
}
|
|
1869
|
-
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);
|
|
1870
1919
|
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1871
|
-
|
|
1920
|
+
fail3(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1872
1921
|
}
|
|
1873
1922
|
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1874
|
-
|
|
1923
|
+
fail3(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1875
1924
|
}
|
|
1876
1925
|
const key = `${cell.of}:${cell.part}`;
|
|
1877
|
-
if (seen.has(key))
|
|
1926
|
+
if (seen.has(key)) fail3(`duplicate cell column '${column.name}'`, path);
|
|
1878
1927
|
seen.add(key);
|
|
1879
1928
|
}
|
|
1880
1929
|
for (const column of tracked) {
|
|
1881
1930
|
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1882
|
-
|
|
1931
|
+
fail3(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1883
1932
|
}
|
|
1884
1933
|
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1885
|
-
|
|
1934
|
+
fail3(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1886
1935
|
}
|
|
1887
1936
|
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1888
|
-
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`);
|
|
1889
1938
|
}
|
|
1890
1939
|
}
|
|
1891
1940
|
}
|
|
@@ -1893,7 +1942,7 @@ var exportGateSchema = zod.z.union([zod.z.literal(true), zod.z.object({ capabili
|
|
|
1893
1942
|
var relationExportSchema = zod.z.object({ read: exportGateSchema, write: exportGateSchema.optional() }).strict();
|
|
1894
1943
|
var localDefinitionSchema = zod.z.object({
|
|
1895
1944
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
1896
|
-
namespace: identifier2.max(41),
|
|
1945
|
+
namespace: identifier2.max(41).regex(NAMESPACE_PATTERN, "invalid or reserved ORM namespace"),
|
|
1897
1946
|
description: zod.z.string().max(2e3).optional(),
|
|
1898
1947
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
1899
1948
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1927,6 +1976,7 @@ var localDefinitionSchema = zod.z.object({
|
|
|
1927
1976
|
}
|
|
1928
1977
|
}
|
|
1929
1978
|
const fkTables = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1979
|
+
validateUnkeyedLinkIdentities({ definition, ctx });
|
|
1930
1980
|
const relations = /* @__PURE__ */ new Set([...fkTables.keys(), ...(definition.views ?? []).map((view) => view.view)]);
|
|
1931
1981
|
for (const name2 of Object.keys(definition.exports ?? {})) {
|
|
1932
1982
|
if (!relations.has(name2)) ctx.addIssue({ code: "custom", path: ["exports", name2], message: `export '${name2}' is not a local object` });
|
|
@@ -1952,38 +2002,38 @@ var localDefinitionSchema = zod.z.object({
|
|
|
1952
2002
|
}
|
|
1953
2003
|
}
|
|
1954
2004
|
for (const [index3, check] of (table.checks ?? []).entries()) {
|
|
1955
|
-
const
|
|
1956
|
-
if (table.source)
|
|
1957
|
-
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}'`);
|
|
1958
2008
|
names.add(check.name);
|
|
1959
2009
|
try {
|
|
1960
2010
|
compileCheckExpression(check.expression, table.columns);
|
|
1961
2011
|
} catch (error) {
|
|
1962
|
-
|
|
2012
|
+
fail3(error instanceof Error ? error.message : String(error));
|
|
1963
2013
|
}
|
|
1964
2014
|
}
|
|
1965
2015
|
for (const [index3, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1966
|
-
const
|
|
1967
|
-
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}'`);
|
|
1968
2018
|
names.add(fk.name);
|
|
1969
2019
|
const target = fkTables.get(fk.target);
|
|
1970
2020
|
if (table.source || !target || target.source) {
|
|
1971
|
-
|
|
2021
|
+
fail3("foreign keys must connect owned tables in the same namespace");
|
|
1972
2022
|
continue;
|
|
1973
2023
|
}
|
|
1974
|
-
if (fk.columns.length !== fk.references.length)
|
|
1975
|
-
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");
|
|
1976
2026
|
for (const [position, name2] of fk.columns.entries()) {
|
|
1977
2027
|
const sourceColumn = table.columns.find((column) => column.columnName === name2);
|
|
1978
2028
|
const targetColumn = target.columns.find((column) => column.columnName === fk.references[position]);
|
|
1979
|
-
if (!sourceColumn || !targetColumn)
|
|
1980
|
-
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");
|
|
1981
2031
|
}
|
|
1982
2032
|
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1983
|
-
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");
|
|
1984
2034
|
}
|
|
1985
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])];
|
|
1986
|
-
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");
|
|
1987
2037
|
}
|
|
1988
2038
|
}
|
|
1989
2039
|
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
@@ -2171,8 +2221,8 @@ function cloneColumn({ builder, property, notNull, primaryKey: primaryKey2 }) {
|
|
|
2171
2221
|
return clone;
|
|
2172
2222
|
}
|
|
2173
2223
|
function planSource(input) {
|
|
2174
|
-
const { sourceName, input: source, fail } = input;
|
|
2175
|
-
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}`);
|
|
2176
2226
|
switch (source.kind) {
|
|
2177
2227
|
case "manual":
|
|
2178
2228
|
return {
|
|
@@ -2208,30 +2258,31 @@ function internalTable(input, supplied, system, options = {}) {
|
|
|
2208
2258
|
internal: true
|
|
2209
2259
|
});
|
|
2210
2260
|
}
|
|
2211
|
-
function assertColumns(input,
|
|
2212
|
-
for (const column of
|
|
2261
|
+
function assertColumns(input, list2, what) {
|
|
2262
|
+
for (const column of list2) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2213
2263
|
}
|
|
2214
2264
|
function planEtl(input, source) {
|
|
2215
|
-
const { sourceName, pk, dataColumns, fail } = input;
|
|
2265
|
+
const { sourceName, pk, dataColumns, fail: fail3 } = input;
|
|
2216
2266
|
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2217
|
-
if (!isSeqTable(table) || table.source === void 0)
|
|
2267
|
+
if (!isSeqTable(table) || table.source === void 0) fail3(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2218
2268
|
const synced = table;
|
|
2219
|
-
if (
|
|
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`);
|
|
2220
2271
|
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2221
2272
|
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2222
2273
|
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2223
|
-
|
|
2274
|
+
fail3(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2224
2275
|
}
|
|
2225
2276
|
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2226
2277
|
}
|
|
2227
2278
|
function planPrefetch(input, source) {
|
|
2228
|
-
const { sourceName, pk, fail } = input;
|
|
2279
|
+
const { sourceName, pk, fail: fail3 } = input;
|
|
2229
2280
|
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2230
2281
|
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2231
|
-
|
|
2282
|
+
fail3(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2232
2283
|
}
|
|
2233
2284
|
for (const column of source.writeback?.columns ?? []) {
|
|
2234
|
-
if (!source.columns.includes(column))
|
|
2285
|
+
if (!source.columns.includes(column)) fail3(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2235
2286
|
}
|
|
2236
2287
|
const writeback = source.writeback;
|
|
2237
2288
|
return {
|
|
@@ -2437,34 +2488,34 @@ var SYSTEM_VIEW_COLUMNS = {
|
|
|
2437
2488
|
};
|
|
2438
2489
|
function defineResolvedTable(name2, columns, config) {
|
|
2439
2490
|
const file = activeAuthoringContext()?.currentFile;
|
|
2440
|
-
const
|
|
2491
|
+
const fail3 = (message) => {
|
|
2441
2492
|
throw new OrmDefinitionError(`resolved table "${name2}": ${message}`, file);
|
|
2442
2493
|
};
|
|
2443
|
-
if (!IDENTIFIER_PATTERN.test(name2))
|
|
2494
|
+
if (!IDENTIFIER_PATTERN.test(name2)) fail3(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2444
2495
|
for (const property of Object.keys(columns)) {
|
|
2445
2496
|
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2446
|
-
|
|
2497
|
+
fail3(`column "${property}" collides with a reserved system column`);
|
|
2447
2498
|
}
|
|
2448
2499
|
}
|
|
2449
2500
|
for (const property of Object.keys(columns)) {
|
|
2450
2501
|
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2451
|
-
|
|
2502
|
+
fail3(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2452
2503
|
}
|
|
2453
2504
|
}
|
|
2454
2505
|
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2455
|
-
if (pkEntries.length !== 1)
|
|
2506
|
+
if (pkEntries.length !== 1) fail3("declare exactly one .primaryKey() column");
|
|
2456
2507
|
const [pk, pkBuilder] = pkEntries[0];
|
|
2457
2508
|
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2458
|
-
if (dataColumns.length === 0)
|
|
2509
|
+
if (dataColumns.length === 0) fail3("declare at least one non-key column");
|
|
2459
2510
|
const sourceEntries = Object.entries(config.sources);
|
|
2460
|
-
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1)
|
|
2511
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail3("declare exactly one manualSource()");
|
|
2461
2512
|
const plans = sourceEntries.map(
|
|
2462
|
-
([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 })
|
|
2463
2514
|
);
|
|
2464
|
-
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2515
|
+
const rules = planRules({ config, plans, dataColumns, fail: fail3 });
|
|
2465
2516
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2466
2517
|
for (const source of deletedBy) {
|
|
2467
|
-
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`);
|
|
2468
2519
|
}
|
|
2469
2520
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2470
2521
|
const viewColumns = {};
|
|
@@ -2527,11 +2578,11 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2527
2578
|
}
|
|
2528
2579
|
});
|
|
2529
2580
|
}
|
|
2530
|
-
function planRules({ config, plans, dataColumns, fail }) {
|
|
2581
|
+
function planRules({ config, plans, dataColumns, fail: fail3 }) {
|
|
2531
2582
|
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2532
2583
|
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2533
2584
|
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2534
|
-
if (!dataColumns.includes(column))
|
|
2585
|
+
if (!dataColumns.includes(column)) fail3(`resolve names unknown column "${column}"`);
|
|
2535
2586
|
}
|
|
2536
2587
|
return dataColumns.map((column) => {
|
|
2537
2588
|
const explicit = config.resolve?.[column];
|
|
@@ -2539,11 +2590,11 @@ function planRules({ config, plans, dataColumns, fail }) {
|
|
|
2539
2590
|
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name2) => plans.find((plan) => plan.name === name2)?.columns.includes(column));
|
|
2540
2591
|
for (const candidate of candidates) {
|
|
2541
2592
|
const plan = plans.find((entry) => entry.name === candidate);
|
|
2542
|
-
if (!plan)
|
|
2543
|
-
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`);
|
|
2544
2595
|
}
|
|
2545
|
-
if (candidates.length === 0)
|
|
2546
|
-
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 });
|
|
2547
2598
|
return {
|
|
2548
2599
|
column,
|
|
2549
2600
|
comparator: comparator.kind,
|
|
@@ -2594,18 +2645,18 @@ var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
2594
2645
|
]);
|
|
2595
2646
|
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2596
2647
|
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2597
|
-
function assertSafeOrderBy({ orderBy, column, fail }) {
|
|
2648
|
+
function assertSafeOrderBy({ orderBy, column, fail: fail3 }) {
|
|
2598
2649
|
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2599
2650
|
let consumed = 0;
|
|
2600
2651
|
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2601
2652
|
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2602
2653
|
const token = match[0].toLowerCase();
|
|
2603
2654
|
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2604
|
-
|
|
2655
|
+
fail3(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2605
2656
|
}
|
|
2606
2657
|
}
|
|
2607
2658
|
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2608
|
-
|
|
2659
|
+
fail3(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2609
2660
|
}
|
|
2610
2661
|
}
|
|
2611
2662
|
function stableJson(value) {
|
|
@@ -3133,17 +3184,17 @@ function buildCanonicalLink({
|
|
|
3133
3184
|
}) {
|
|
3134
3185
|
const ownerKind = relationKind(relation);
|
|
3135
3186
|
const ownerName = relationName(relation);
|
|
3136
|
-
const
|
|
3187
|
+
const fail3 = (message) => {
|
|
3137
3188
|
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
3138
3189
|
};
|
|
3139
3190
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
3140
|
-
return
|
|
3191
|
+
return fail3("sets enforce: true, which is only supported on toOne links");
|
|
3141
3192
|
}
|
|
3142
3193
|
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
3143
|
-
return
|
|
3194
|
+
return fail3("sets a referential action without enforce: true");
|
|
3144
3195
|
}
|
|
3145
3196
|
if (link.linkKind === "toPlatformUser") {
|
|
3146
|
-
if (link.target !== void 0) return
|
|
3197
|
+
if (link.target !== void 0) return fail3("is toPlatformUser and must not declare a target table");
|
|
3147
3198
|
return {
|
|
3148
3199
|
name: linkName,
|
|
3149
3200
|
kind: "toPlatformUser",
|
|
@@ -3154,7 +3205,7 @@ function buildCanonicalLink({
|
|
|
3154
3205
|
};
|
|
3155
3206
|
}
|
|
3156
3207
|
if (typeof link.target === "object" && link.target?.kind === "seq-external-object") {
|
|
3157
|
-
if (link.enforce) return
|
|
3208
|
+
if (link.enforce) return fail3("cannot enforce a cross-namespace physical foreign key; use an ORM link without enforce");
|
|
3158
3209
|
return {
|
|
3159
3210
|
name: linkName,
|
|
3160
3211
|
kind: link.linkKind,
|
|
@@ -3166,13 +3217,13 @@ function buildCanonicalLink({
|
|
|
3166
3217
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
3167
3218
|
};
|
|
3168
3219
|
}
|
|
3169
|
-
const target = resolveRelationRef({ ref: link.target, fail });
|
|
3220
|
+
const target = resolveRelationRef({ ref: link.target, fail: fail3 });
|
|
3170
3221
|
const targetName = relationName(target);
|
|
3171
3222
|
if (!relationsByName.has(targetName)) {
|
|
3172
|
-
return
|
|
3223
|
+
return fail3(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
3173
3224
|
}
|
|
3174
3225
|
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
3175
|
-
return
|
|
3226
|
+
return fail3("cannot enforce a PostgreSQL foreign key involving a view");
|
|
3176
3227
|
}
|
|
3177
3228
|
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
3178
3229
|
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
@@ -3188,19 +3239,19 @@ function buildCanonicalLink({
|
|
|
3188
3239
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
3189
3240
|
};
|
|
3190
3241
|
}
|
|
3191
|
-
function resolveRelationRef({ ref, fail }) {
|
|
3192
|
-
if (ref === void 0) return
|
|
3242
|
+
function resolveRelationRef({ ref, fail: fail3 }) {
|
|
3243
|
+
if (ref === void 0) return fail3("declares no target relation");
|
|
3193
3244
|
if (typeof ref !== "function") return ref;
|
|
3194
3245
|
let resolved;
|
|
3195
3246
|
try {
|
|
3196
3247
|
resolved = ref();
|
|
3197
3248
|
} catch (error) {
|
|
3198
|
-
return
|
|
3249
|
+
return fail3(
|
|
3199
3250
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
3200
3251
|
);
|
|
3201
3252
|
}
|
|
3202
3253
|
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
3203
|
-
return
|
|
3254
|
+
return fail3("target thunk did not return a relation defined with defineTable/defineView");
|
|
3204
3255
|
}
|
|
3205
3256
|
return resolved;
|
|
3206
3257
|
}
|
|
@@ -3303,9 +3354,9 @@ function extractOrmSyncedTableContract({
|
|
|
3303
3354
|
);
|
|
3304
3355
|
}
|
|
3305
3356
|
const primaryKey2 = table.primaryKey;
|
|
3306
|
-
if (primaryKey2 === void 0
|
|
3357
|
+
if (primaryKey2 === void 0) {
|
|
3307
3358
|
throw new OrmDefinitionError(
|
|
3308
|
-
`synced table "${table.table}" has no primary key \u2014
|
|
3359
|
+
`synced table "${table.table}" has no primary key contract \u2014 declare a key or explicit primaryKey: []`
|
|
3309
3360
|
);
|
|
3310
3361
|
}
|
|
3311
3362
|
const pkSet = new Set(primaryKey2);
|
|
@@ -4072,19 +4123,19 @@ function compileComparison(op, left, right, columnTypes) {
|
|
|
4072
4123
|
}
|
|
4073
4124
|
return `${compileOperand(left, right, columnTypes)} ${SQL_COMPARE_OPS[op]} ${compileOperand(right, left, columnTypes)}`;
|
|
4074
4125
|
}
|
|
4075
|
-
function compilePredicate(predicate, columnTypes) {
|
|
4126
|
+
function compilePredicate(predicate, columnTypes, options = {}) {
|
|
4076
4127
|
switch (predicate.kind) {
|
|
4077
4128
|
case "and":
|
|
4078
4129
|
case "or": {
|
|
4079
|
-
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()} `);
|
|
4080
4131
|
return `(${joined})`;
|
|
4081
4132
|
}
|
|
4082
4133
|
case "not":
|
|
4083
|
-
return `(NOT ${compilePredicate(predicate.operand, columnTypes)})`;
|
|
4134
|
+
return `(NOT ${compilePredicate(predicate.operand, columnTypes, options)})`;
|
|
4084
4135
|
case "compare":
|
|
4085
4136
|
return `(${compileComparison(predicate.op, predicate.left, predicate.right, columnTypes)})`;
|
|
4086
4137
|
case "capability":
|
|
4087
|
-
return `(${capabilityExpression(predicate.key)})`;
|
|
4138
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(predicate.key)})`;
|
|
4088
4139
|
}
|
|
4089
4140
|
}
|
|
4090
4141
|
|
|
@@ -4110,9 +4161,9 @@ function buildActionParamsSchema(action) {
|
|
|
4110
4161
|
for (const [name2, param] of entries) {
|
|
4111
4162
|
shape[name2] = param.optional ? PARAM_SCHEMAS[param.type].nullish() : PARAM_SCHEMAS[param.type];
|
|
4112
4163
|
}
|
|
4113
|
-
return zod.z.strictObject(shape).superRefine((
|
|
4164
|
+
return zod.z.strictObject(shape).superRefine((obj2, ctx) => {
|
|
4114
4165
|
for (const [name2, param] of entries) {
|
|
4115
|
-
if (!param.optional && param.type === "jsonb" &&
|
|
4166
|
+
if (!param.optional && param.type === "jsonb" && obj2[name2] === void 0) {
|
|
4116
4167
|
ctx.addIssue({ code: "custom", message: "Required", path: [name2] });
|
|
4117
4168
|
}
|
|
4118
4169
|
}
|
|
@@ -4170,9 +4221,9 @@ function compilePredIrToSql(pred, options = {}) {
|
|
|
4170
4221
|
case "cmp":
|
|
4171
4222
|
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
4172
4223
|
case "capability":
|
|
4173
|
-
return `(${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4224
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${capabilityExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4174
4225
|
case "role":
|
|
4175
|
-
return `(${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4226
|
+
return `(${options.statementClaimChecks ? "SELECT " : ""}${roleExpression(pred.key, pred.namespace ?? options.namespace)})`;
|
|
4176
4227
|
case "exists":
|
|
4177
4228
|
if (options.exists) return options.exists(pred);
|
|
4178
4229
|
throw new OrmDefinitionError("synced view read policy cannot use 'exists' without a relationship resolver");
|
|
@@ -4748,7 +4799,9 @@ function syncedViewStatements({
|
|
|
4748
4799
|
columnNamesByTable,
|
|
4749
4800
|
columnTypesByTable,
|
|
4750
4801
|
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE,
|
|
4751
|
-
backingPrivileges = "grant"
|
|
4802
|
+
backingPrivileges = "grant",
|
|
4803
|
+
viewPrivileges = "grant",
|
|
4804
|
+
statementClaimChecks = true
|
|
4752
4805
|
}) {
|
|
4753
4806
|
const schema = quoteIdentifier(ormSchemaName(definition.namespace));
|
|
4754
4807
|
const role = quoteIdentifier(consumerRoleName(definition.namespace));
|
|
@@ -4770,6 +4823,17 @@ function syncedViewStatements({
|
|
|
4770
4823
|
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
4771
4824
|
const physical2 = splitPhysicalRelation(backing.physicalRelation);
|
|
4772
4825
|
const physicalTarget = `${quoteIdentifier(physical2.schema)}.${quoteIdentifier(physical2.table)}`;
|
|
4826
|
+
const declaredColumns = Object.keys(table.columns ?? {});
|
|
4827
|
+
const projection = declaredColumns.length > 0 ? declaredColumns.map(quoteIdentifier).join(", ") : "*";
|
|
4828
|
+
if (backing.selectColumns) {
|
|
4829
|
+
if (declaredColumns.length === 0) {
|
|
4830
|
+
throw new OrmDefinitionError(`synced backing for "${table.table}" requires declared columns`);
|
|
4831
|
+
}
|
|
4832
|
+
const approved = new Set(backing.selectColumns);
|
|
4833
|
+
if (declaredColumns.some((column) => !approved.has(column))) {
|
|
4834
|
+
throw new OrmDefinitionError(`synced backing for "${table.table}" projects outside its approved column ceiling`);
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4773
4837
|
const columnTypes = mergeSyncedViewColumnTypes({
|
|
4774
4838
|
table,
|
|
4775
4839
|
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
@@ -4778,25 +4842,29 @@ function syncedViewStatements({
|
|
|
4778
4842
|
});
|
|
4779
4843
|
const readPolicyIr = readPolicyIrByTable?.get(table.table);
|
|
4780
4844
|
const readPolicy = table.policies?.read;
|
|
4781
|
-
const
|
|
4845
|
+
const policyClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
|
|
4782
4846
|
namespace: definition.namespace,
|
|
4847
|
+
statementClaimChecks,
|
|
4783
4848
|
columnTypes,
|
|
4784
4849
|
columnNames: columnNamesByTable?.get(table.table)
|
|
4785
|
-
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
4850
|
+
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes), { statementClaimChecks })}` : " WHERE false";
|
|
4786
4851
|
if (backingPrivileges === "grant") {
|
|
4852
|
+
const selectPrivilege = backing.selectColumns ? `SELECT (${backing.selectColumns.map(quoteIdentifier).join(", ")})` : "SELECT";
|
|
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')`;
|
|
4787
4854
|
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical2.schema)} TO ${owner}`);
|
|
4788
|
-
statements.push(`GRANT
|
|
4855
|
+
statements.push(`GRANT ${selectPrivilege} ON ${physicalTarget} TO ${owner}`);
|
|
4789
4856
|
statements.push(`DO ${quoteLiteral(`BEGIN
|
|
4790
4857
|
IF NOT (pg_catalog.has_schema_privilege(${quoteLiteral(executionOwnerRole)}, ${quoteLiteral(physical2.schema)}, 'USAGE')
|
|
4791
|
-
AND
|
|
4858
|
+
AND ${hasSelectPrivilege}) THEN
|
|
4792
4859
|
RAISE EXCEPTION 'Synced backing owner lacks effective USAGE/SELECT; repair managed serving reader access before ORM apply' USING ERRCODE = '42501';
|
|
4793
4860
|
END IF;
|
|
4794
4861
|
END`)}`);
|
|
4795
4862
|
}
|
|
4863
|
+
const whereClause = backing.readGuard ? ` WHERE (SELECT orm_native_sources.require_read_guard(${quoteLiteral(backing.readGuard)})) AND (${policyClause.slice(" WHERE ".length)})` : policyClause;
|
|
4796
4864
|
statements.push(
|
|
4797
|
-
`CREATE OR REPLACE VIEW ${viewName} AS SELECT
|
|
4865
|
+
`CREATE OR REPLACE VIEW ${viewName} AS SELECT ${projection} FROM ${physicalTarget}${whereClause}`
|
|
4798
4866
|
);
|
|
4799
|
-
statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
|
|
4867
|
+
if (viewPrivileges === "grant") statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
|
|
4800
4868
|
}
|
|
4801
4869
|
return statements;
|
|
4802
4870
|
}
|
|
@@ -5065,10 +5133,142 @@ function snapshotTableNames(snapshot, schema) {
|
|
|
5065
5133
|
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
5066
5134
|
}
|
|
5067
5135
|
|
|
5136
|
+
// src/sql/ast.ts
|
|
5137
|
+
function obj(value) {
|
|
5138
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("unsupported SQL node");
|
|
5139
|
+
return value;
|
|
5140
|
+
}
|
|
5141
|
+
function list(value) {
|
|
5142
|
+
return value === void 0 ? [] : Array.isArray(value) ? value : fail("expected SQL list");
|
|
5143
|
+
}
|
|
5144
|
+
function str(value) {
|
|
5145
|
+
return typeof value === "string" ? value : fail("expected SQL name");
|
|
5146
|
+
}
|
|
5147
|
+
function fail(message) {
|
|
5148
|
+
throw new Error(message);
|
|
5149
|
+
}
|
|
5150
|
+
function strings(value) {
|
|
5151
|
+
return list(value).map((v) => str(obj(obj(v).String).sval));
|
|
5152
|
+
}
|
|
5153
|
+
|
|
5154
|
+
// src/canonical/view-query.ts
|
|
5155
|
+
var parser;
|
|
5156
|
+
var functions = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5157
|
+
var scalarTypes = /* @__PURE__ */ new Set([
|
|
5158
|
+
"bool",
|
|
5159
|
+
"boolean",
|
|
5160
|
+
"int2",
|
|
5161
|
+
"int4",
|
|
5162
|
+
"int8",
|
|
5163
|
+
"integer",
|
|
5164
|
+
"bigint",
|
|
5165
|
+
"smallint",
|
|
5166
|
+
"numeric",
|
|
5167
|
+
"decimal",
|
|
5168
|
+
"float4",
|
|
5169
|
+
"float8",
|
|
5170
|
+
"real",
|
|
5171
|
+
"text",
|
|
5172
|
+
"varchar",
|
|
5173
|
+
"bpchar",
|
|
5174
|
+
"uuid",
|
|
5175
|
+
"date",
|
|
5176
|
+
"timestamp",
|
|
5177
|
+
"timestamptz",
|
|
5178
|
+
"time",
|
|
5179
|
+
"timetz",
|
|
5180
|
+
"interval",
|
|
5181
|
+
"json",
|
|
5182
|
+
"jsonb",
|
|
5183
|
+
"bytea"
|
|
5184
|
+
]);
|
|
5185
|
+
var sqlValues = /* @__PURE__ */ new Set([
|
|
5186
|
+
"SVFOP_CURRENT_DATE",
|
|
5187
|
+
"SVFOP_CURRENT_TIME",
|
|
5188
|
+
"SVFOP_CURRENT_TIME_N",
|
|
5189
|
+
"SVFOP_CURRENT_TIMESTAMP",
|
|
5190
|
+
"SVFOP_CURRENT_TIMESTAMP_N",
|
|
5191
|
+
"SVFOP_LOCALTIME",
|
|
5192
|
+
"SVFOP_LOCALTIME_N",
|
|
5193
|
+
"SVFOP_LOCALTIMESTAMP",
|
|
5194
|
+
"SVFOP_LOCALTIMESTAMP_N",
|
|
5195
|
+
"SVFOP_CURRENT_ROLE",
|
|
5196
|
+
"SVFOP_CURRENT_USER",
|
|
5197
|
+
"SVFOP_USER",
|
|
5198
|
+
"SVFOP_SESSION_USER",
|
|
5199
|
+
"SVFOP_CURRENT_CATALOG",
|
|
5200
|
+
"SVFOP_CURRENT_SCHEMA"
|
|
5201
|
+
]);
|
|
5202
|
+
async function validateManagedViewQueries({ canonical }) {
|
|
5203
|
+
for (const definition of [canonical, ...canonical.imports ?? []]) {
|
|
5204
|
+
for (const view of definition.views ?? []) {
|
|
5205
|
+
const fail3 = (message) => {
|
|
5206
|
+
throw new OrmDefinitionError(`managed view '${definition.namespace}/${view.view}': ${message}`);
|
|
5207
|
+
};
|
|
5208
|
+
parser ??= new pgParser.PgParser({ version: 17 });
|
|
5209
|
+
const parsed = await parser.parse(view.query);
|
|
5210
|
+
if (parsed.error || !parsed.tree) fail3("invalid SELECT query");
|
|
5211
|
+
const statements = list(obj(parsed.tree).stmts);
|
|
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 });
|
|
5214
|
+
}
|
|
5215
|
+
}
|
|
5216
|
+
}
|
|
5217
|
+
function pgCatalogName(parts) {
|
|
5218
|
+
if (parts.length === 1) return parts[0];
|
|
5219
|
+
if (parts.length === 2 && parts[0] === "pg_catalog") return parts[1];
|
|
5220
|
+
return void 0;
|
|
5221
|
+
}
|
|
5222
|
+
function reviewedFunctionName({ parts, format }) {
|
|
5223
|
+
return format === "COERCE_SQL_SYNTAX" && parts.length === 2 && parts[0] === "pg_catalog" && parts[1] === "position" ? "strpos" : pgCatalogName(parts);
|
|
5224
|
+
}
|
|
5225
|
+
function validateTree({ tree, ...scope }) {
|
|
5226
|
+
const pending = [tree];
|
|
5227
|
+
while (pending.length) {
|
|
5228
|
+
const value = pending.pop();
|
|
5229
|
+
if (Array.isArray(value)) {
|
|
5230
|
+
pending.push(...value);
|
|
5231
|
+
continue;
|
|
5232
|
+
}
|
|
5233
|
+
if (!value || typeof value !== "object") continue;
|
|
5234
|
+
const node = obj(value);
|
|
5235
|
+
for (const [kind, child] of Object.entries(node)) {
|
|
5236
|
+
validateNode({ kind, child, scope });
|
|
5237
|
+
pending.push(child);
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
5240
|
+
}
|
|
5241
|
+
function validateNode({ kind, child, scope }) {
|
|
5242
|
+
const { fail: fail3 } = scope;
|
|
5243
|
+
if (kind.endsWith("Stmt") && kind !== "SelectStmt") fail3("only SELECT statements are permitted");
|
|
5244
|
+
if (kind === "FuncCall") {
|
|
5245
|
+
const call = obj(child);
|
|
5246
|
+
const parts = strings(call.funcname);
|
|
5247
|
+
const name2 = reviewedFunctionName({ parts, format: call.funcformat });
|
|
5248
|
+
if (!name2 || !functions.has(name2)) fail3(`only explicitly allowlisted pg_catalog functions are permitted (found '${parts.join(".")}')`);
|
|
5249
|
+
}
|
|
5250
|
+
if (kind === "typeName" || kind === "TypeName") validateType({ node: obj(child), ...scope });
|
|
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");
|
|
5255
|
+
if (kind === "A_Indirection" && list(obj(child).indirection).some((entry) => !obj(entry).A_Indices)) {
|
|
5256
|
+
fail3("composite field notation is not permitted in managed views");
|
|
5257
|
+
}
|
|
5258
|
+
}
|
|
5259
|
+
function validateType({ node, schema, enums, fail: fail3 }) {
|
|
5260
|
+
const parts = strings(node.names);
|
|
5261
|
+
const builtin = pgCatalogName(parts);
|
|
5262
|
+
const managedEnum = parts.length === 1 && enums.includes(parts[0]) || parts.length === 2 && parts[0] === schema && enums.includes(parts[1]);
|
|
5263
|
+
if (node.setof || node.pct_type || !managedEnum && (!builtin || !scalarTypes.has(builtin))) {
|
|
5264
|
+
fail3(`casts may only use supported pg_catalog scalar types or declared enums (found '${parts.join(".")}')`);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
|
|
5068
5268
|
// src/engine/materialized-refresh.ts
|
|
5069
5269
|
function materializedRefreshFunctions(canonical) {
|
|
5070
5270
|
const schema = ormSchemaName(canonical.namespace);
|
|
5071
|
-
const
|
|
5271
|
+
const functions2 = /* @__PURE__ */ new Map();
|
|
5072
5272
|
for (const view of canonical.views ?? []) {
|
|
5073
5273
|
const materialized = view.resolved?.materialized;
|
|
5074
5274
|
if (!materialized) continue;
|
|
@@ -5077,7 +5277,7 @@ function materializedRefreshFunctions(canonical) {
|
|
|
5077
5277
|
if (!table || pk.length === 0) {
|
|
5078
5278
|
throw new OrmDefinitionError(`materialized resolved table '${view.view}' needs its table and primary key`);
|
|
5079
5279
|
}
|
|
5080
|
-
|
|
5280
|
+
functions2.set(view.view, refreshFunctionSql({
|
|
5081
5281
|
schema,
|
|
5082
5282
|
name: view.view,
|
|
5083
5283
|
table: materialized.table,
|
|
@@ -5086,13 +5286,13 @@ function materializedRefreshFunctions(canonical) {
|
|
|
5086
5286
|
pk
|
|
5087
5287
|
}));
|
|
5088
5288
|
}
|
|
5089
|
-
return
|
|
5289
|
+
return functions2;
|
|
5090
5290
|
}
|
|
5091
5291
|
function refreshFunctionSql({ schema, name: name2, table, compute, columns, pk }) {
|
|
5092
5292
|
const fn = `${quoteIdentifier(schema)}.${quoteIdentifier(materializedRefreshFunctionName(name2))}`;
|
|
5093
5293
|
const target = `${quoteIdentifier(schema)}.${quoteIdentifier(table)}`;
|
|
5094
5294
|
const source = `${quoteIdentifier(schema)}.${quoteIdentifier(compute)}`;
|
|
5095
|
-
const
|
|
5295
|
+
const list2 = columns.map(quoteIdentifier).join(", ");
|
|
5096
5296
|
const others = columns.filter((column) => !pk.includes(column));
|
|
5097
5297
|
const keys = pk.map(quoteIdentifier).join(", ");
|
|
5098
5298
|
const matches = pk.map((column) => `c.${quoteIdentifier(column)} = m.${quoteIdentifier(column)}`).join(" AND ");
|
|
@@ -5118,8 +5318,8 @@ BEGIN
|
|
|
5118
5318
|
PERFORM set_config('request.jwt.claims', ${quoteLiteral(MATERIALIZE_CLAIMS_JSON)}, true);
|
|
5119
5319
|
RETURN QUERY
|
|
5120
5320
|
WITH up AS (
|
|
5121
|
-
INSERT INTO ${target} AS m (${
|
|
5122
|
-
SELECT ${
|
|
5321
|
+
INSERT INTO ${target} AS m (${list2})
|
|
5322
|
+
SELECT ${list2} FROM ${source}
|
|
5123
5323
|
ON CONFLICT (${keys}) ${onConflict}
|
|
5124
5324
|
RETURNING 1
|
|
5125
5325
|
), gone AS (
|
|
@@ -5730,6 +5930,7 @@ async function planWithViewRecreation({
|
|
|
5730
5930
|
previousSnapshot,
|
|
5731
5931
|
viewRecreation
|
|
5732
5932
|
}) {
|
|
5933
|
+
await validateManagedViewQueries({ canonical });
|
|
5733
5934
|
const schema = ormSchemaName(canonical.namespace);
|
|
5734
5935
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
5735
5936
|
assertValidViewDependencies(canonical);
|
|
@@ -6272,6 +6473,118 @@ async function guardedRun(run) {
|
|
|
6272
6473
|
process.exit = originalExit;
|
|
6273
6474
|
}
|
|
6274
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
|
+
}
|
|
6275
6588
|
|
|
6276
6589
|
// src/engine/index.ts
|
|
6277
6590
|
var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
|
|
@@ -6341,6 +6654,11 @@ function renderOrmMigrationExport(exported) {
|
|
|
6341
6654
|
if (Buffer.byteLength(text4, "utf8") > ORM_MIGRATION_EXPORT_MAX_BYTES) throw new Error("ORM migration export exceeds the size limit");
|
|
6342
6655
|
return text4;
|
|
6343
6656
|
}
|
|
6657
|
+
|
|
6658
|
+
// src/client/sql.ts
|
|
6659
|
+
var BUILTIN_SQL_READ_HASH = "sha256:a50c477c952a5e0fe68ed158a4530e4aab573c09bcbb1a8ac454dc88a39a6258";
|
|
6660
|
+
|
|
6661
|
+
// src/ir/index.ts
|
|
6344
6662
|
var IR_VERSION = 5;
|
|
6345
6663
|
var LEGACY_IR_VERSION = 1;
|
|
6346
6664
|
var GUARDED_IR_VERSION = 2;
|
|
@@ -6428,7 +6746,7 @@ var limitIrSchema = zod.z.object({
|
|
|
6428
6746
|
variable: zod.z.string().optional(),
|
|
6429
6747
|
value: zod.z.number().int().positive().optional(),
|
|
6430
6748
|
default: zod.z.number().int().positive(),
|
|
6431
|
-
max: zod.z.number().int().positive()
|
|
6749
|
+
max: zod.z.number().int().positive().max(MAX_PERSISTED_READ_ROWS)
|
|
6432
6750
|
});
|
|
6433
6751
|
var selectionIrSchema = zod.z.lazy(
|
|
6434
6752
|
() => zod.z.object({
|
|
@@ -6673,7 +6991,8 @@ var compiledOperationSchema = zod.z.object({
|
|
|
6673
6991
|
}).superRefine((operation, ctx) => {
|
|
6674
6992
|
if (operation.language === "sql") {
|
|
6675
6993
|
const validPlan = operation.sqlQuery ? !operation.sql && operation.irVersion === RUNTIME_SQL_IR_VERSION : !!operation.sql && operation.irVersion === SQL_IR_VERSION;
|
|
6676
|
-
|
|
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) {
|
|
6677
6996
|
ctx.addIssue({ code: "custom", message: "SQL operations require a query-only v4 plan or v5 runtime contract, capability, and no GraphQL roots" });
|
|
6678
6997
|
}
|
|
6679
6998
|
} else if (operation.sql || operation.sqlQuery || operation.roots.length === 0) {
|
|
@@ -8104,6 +8423,7 @@ exports.MATERIALIZE_CLAIMS_JSON = MATERIALIZE_CLAIMS_JSON;
|
|
|
8104
8423
|
exports.MAX_DEFINITION_FILE_BYTES = MAX_DEFINITION_FILE_BYTES;
|
|
8105
8424
|
exports.MAX_GENERATED_SDL_BYTES = MAX_GENERATED_SDL_BYTES;
|
|
8106
8425
|
exports.MAX_IDENTIFIER_LENGTH = MAX_IDENTIFIER_LENGTH;
|
|
8426
|
+
exports.MAX_PERSISTED_READ_ROWS = MAX_PERSISTED_READ_ROWS;
|
|
8107
8427
|
exports.MAX_TRACKED_COLUMN_NAME = MAX_TRACKED_COLUMN_NAME;
|
|
8108
8428
|
exports.MIGRATIONS_DIRNAME = MIGRATIONS_DIRNAME;
|
|
8109
8429
|
exports.NAMESPACE_CONFIG_BRAND = NAMESPACE_CONFIG_BRAND;
|
|
@@ -8144,6 +8464,7 @@ exports.actionsInterfaceName = actionsInterfaceName;
|
|
|
8144
8464
|
exports.activeAuthoringContext = activeAuthoringContext;
|
|
8145
8465
|
exports.applyMigrationHints = applyMigrationHints;
|
|
8146
8466
|
exports.applyMigrationHintsV2 = applyMigrationHintsV2;
|
|
8467
|
+
exports.assertSyncedViewDefinitionMatches = assertSyncedViewDefinitionMatches;
|
|
8147
8468
|
exports.beginAuthoringContext = beginAuthoringContext;
|
|
8148
8469
|
exports.buildActionParamsSchema = buildActionParamsSchema;
|
|
8149
8470
|
exports.buildDrizzleEntities = buildDrizzleEntities;
|