@sequenceholdings/orm 2.8.3 → 2.8.5
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/dist/authoring.cjs +154 -45
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +12 -2
- package/dist/authoring.d.ts +12 -2
- package/dist/authoring.js +154 -45
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +64 -14
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +12 -7
- package/dist/canonical.d.ts +12 -7
- package/dist/canonical.js +64 -14
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +78 -20
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +78 -20
- package/dist/cli.js.map +1 -1
- package/dist/compiler.cjs +77 -20
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +4 -4
- package/dist/compiler.d.ts +4 -4
- package/dist/compiler.js +77 -20
- package/dist/compiler.js.map +1 -1
- package/dist/index.cjs +161 -47
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +11 -11
- package/dist/index.d.ts +11 -11
- package/dist/index.js +161 -47
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +71 -16
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +9 -9
- package/dist/ir.d.ts +9 -9
- package/dist/ir.js +71 -16
- package/dist/ir.js.map +1 -1
- package/dist/{naming-CU9ThP_E.d.cts → naming-BMZKFO0W.d.cts} +1 -2
- package/dist/{naming-CPfx7EfF.d.ts → naming-DUk7AcXs.d.ts} +1 -2
- package/dist/predicate-v2.cjs +71 -16
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +3 -3
- package/dist/predicate-v2.d.ts +3 -3
- package/dist/predicate-v2.js +71 -16
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +71 -16
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +2 -2
- package/dist/sdl.d.ts +2 -2
- package/dist/sdl.js +71 -16
- package/dist/sdl.js.map +1 -1
- package/package.json +1 -1
- package/src/codegen-gql/apollo-hooks.cjs +1 -1
- package/src/codegen-gql/server-fns.cjs +1 -1
- package/templates/orm-namespace/graphql/example/queries.ts +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -578,6 +578,12 @@ interface SyncedBacking {
|
|
|
578
578
|
*/
|
|
579
579
|
readonly readGuard?: 'crm-serving-v2';
|
|
580
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Atlas reassigns every relation in `orm_<ns>` to this role after apply.
|
|
583
|
+
* Synced views resolve backing access as their owner. Grant this role narrow
|
|
584
|
+
* SELECT before CREATE VIEW, or preserve a separately verified publisher grant.
|
|
585
|
+
*/
|
|
586
|
+
declare const SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
|
|
581
587
|
/**
|
|
582
588
|
* Emit CREATE OR REPLACE VIEW + SELECT-only grants for synced tables that have
|
|
583
589
|
* a registered backing. Synced tables cannot use table-level RLS (they may be
|
|
@@ -586,12 +592,6 @@ interface SyncedBacking {
|
|
|
586
592
|
* Each view lives in `orm_<ns>` alongside regular tables, named identically to
|
|
587
593
|
* the synced table definition so read routing is transparent.
|
|
588
594
|
*/
|
|
589
|
-
/**
|
|
590
|
-
* Atlas reassigns every relation in `orm_<ns>` to this role after apply.
|
|
591
|
-
* Synced views resolve backing access as their owner. Grant this role narrow
|
|
592
|
-
* SELECT before CREATE VIEW, or preserve a separately verified publisher grant.
|
|
593
|
-
*/
|
|
594
|
-
declare const SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
|
|
595
595
|
declare function syncedViewStatements({ definition, backings, readPolicyIrByTable, policyTables, columnNamesByTable, columnTypesByTable, executionOwnerRole, backingPrivileges, viewPrivileges, statementClaimChecks, }: {
|
|
596
596
|
definition: NamespaceDefinition;
|
|
597
597
|
backings: readonly SyncedBacking[];
|
|
@@ -747,9 +747,7 @@ declare function planCommittedMigrationV2({ canonical, previousSnapshot, reprodu
|
|
|
747
747
|
* and types the live view from the SELECT itself. Assert the live catalog in
|
|
748
748
|
* the same transaction so a stale alias/type or an incomplete dependency
|
|
749
749
|
* declaration can never be persisted as a successful schema version.
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Assert live view columns/types/dependencies match the canonical contract.
|
|
750
|
+
*
|
|
753
751
|
* Used by planMigrationV2 and by CASCADE recovery recreates (same transaction).
|
|
754
752
|
*/
|
|
755
753
|
declare function validateViewCatalogStatement({ canonical, view, schema, }: {
|
|
@@ -1070,8 +1068,8 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1070
1068
|
name: z.ZodString;
|
|
1071
1069
|
kind: z.ZodEnum<{
|
|
1072
1070
|
manual: "manual";
|
|
1073
|
-
prefetch: "prefetch";
|
|
1074
1071
|
etl: "etl";
|
|
1072
|
+
prefetch: "prefetch";
|
|
1075
1073
|
}>;
|
|
1076
1074
|
table: z.ZodString;
|
|
1077
1075
|
columns: z.ZodArray<z.ZodString>;
|
|
@@ -1122,6 +1120,7 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1122
1120
|
orderBy: z.ZodOptional<z.ZodString>;
|
|
1123
1121
|
}, z.core.$strict>>;
|
|
1124
1122
|
deletedBy: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
1123
|
+
existsIn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1125
1124
|
materialized: z.ZodOptional<z.ZodObject<{
|
|
1126
1125
|
table: z.ZodString;
|
|
1127
1126
|
compute: z.ZodString;
|
|
@@ -1458,8 +1457,8 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1458
1457
|
name: z.ZodString;
|
|
1459
1458
|
kind: z.ZodEnum<{
|
|
1460
1459
|
manual: "manual";
|
|
1461
|
-
prefetch: "prefetch";
|
|
1462
1460
|
etl: "etl";
|
|
1461
|
+
prefetch: "prefetch";
|
|
1463
1462
|
}>;
|
|
1464
1463
|
table: z.ZodString;
|
|
1465
1464
|
columns: z.ZodArray<z.ZodString>;
|
|
@@ -1510,6 +1509,7 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1510
1509
|
orderBy: z.ZodOptional<z.ZodString>;
|
|
1511
1510
|
}, z.core.$strict>>;
|
|
1512
1511
|
deletedBy: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
1512
|
+
existsIn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1513
1513
|
materialized: z.ZodOptional<z.ZodObject<{
|
|
1514
1514
|
table: z.ZodString;
|
|
1515
1515
|
compute: z.ZodString;
|
package/dist/index.d.ts
CHANGED
|
@@ -578,6 +578,12 @@ interface SyncedBacking {
|
|
|
578
578
|
*/
|
|
579
579
|
readonly readGuard?: 'crm-serving-v2';
|
|
580
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Atlas reassigns every relation in `orm_<ns>` to this role after apply.
|
|
583
|
+
* Synced views resolve backing access as their owner. Grant this role narrow
|
|
584
|
+
* SELECT before CREATE VIEW, or preserve a separately verified publisher grant.
|
|
585
|
+
*/
|
|
586
|
+
declare const SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
|
|
581
587
|
/**
|
|
582
588
|
* Emit CREATE OR REPLACE VIEW + SELECT-only grants for synced tables that have
|
|
583
589
|
* a registered backing. Synced tables cannot use table-level RLS (they may be
|
|
@@ -586,12 +592,6 @@ interface SyncedBacking {
|
|
|
586
592
|
* Each view lives in `orm_<ns>` alongside regular tables, named identically to
|
|
587
593
|
* the synced table definition so read routing is transparent.
|
|
588
594
|
*/
|
|
589
|
-
/**
|
|
590
|
-
* Atlas reassigns every relation in `orm_<ns>` to this role after apply.
|
|
591
|
-
* Synced views resolve backing access as their owner. Grant this role narrow
|
|
592
|
-
* SELECT before CREATE VIEW, or preserve a separately verified publisher grant.
|
|
593
|
-
*/
|
|
594
|
-
declare const SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
|
|
595
595
|
declare function syncedViewStatements({ definition, backings, readPolicyIrByTable, policyTables, columnNamesByTable, columnTypesByTable, executionOwnerRole, backingPrivileges, viewPrivileges, statementClaimChecks, }: {
|
|
596
596
|
definition: NamespaceDefinition;
|
|
597
597
|
backings: readonly SyncedBacking[];
|
|
@@ -747,9 +747,7 @@ declare function planCommittedMigrationV2({ canonical, previousSnapshot, reprodu
|
|
|
747
747
|
* and types the live view from the SELECT itself. Assert the live catalog in
|
|
748
748
|
* the same transaction so a stale alias/type or an incomplete dependency
|
|
749
749
|
* declaration can never be persisted as a successful schema version.
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Assert live view columns/types/dependencies match the canonical contract.
|
|
750
|
+
*
|
|
753
751
|
* Used by planMigrationV2 and by CASCADE recovery recreates (same transaction).
|
|
754
752
|
*/
|
|
755
753
|
declare function validateViewCatalogStatement({ canonical, view, schema, }: {
|
|
@@ -1070,8 +1068,8 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1070
1068
|
name: z.ZodString;
|
|
1071
1069
|
kind: z.ZodEnum<{
|
|
1072
1070
|
manual: "manual";
|
|
1073
|
-
prefetch: "prefetch";
|
|
1074
1071
|
etl: "etl";
|
|
1072
|
+
prefetch: "prefetch";
|
|
1075
1073
|
}>;
|
|
1076
1074
|
table: z.ZodString;
|
|
1077
1075
|
columns: z.ZodArray<z.ZodString>;
|
|
@@ -1122,6 +1120,7 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1122
1120
|
orderBy: z.ZodOptional<z.ZodString>;
|
|
1123
1121
|
}, z.core.$strict>>;
|
|
1124
1122
|
deletedBy: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
1123
|
+
existsIn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1125
1124
|
materialized: z.ZodOptional<z.ZodObject<{
|
|
1126
1125
|
table: z.ZodString;
|
|
1127
1126
|
compute: z.ZodString;
|
|
@@ -1458,8 +1457,8 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1458
1457
|
name: z.ZodString;
|
|
1459
1458
|
kind: z.ZodEnum<{
|
|
1460
1459
|
manual: "manual";
|
|
1461
|
-
prefetch: "prefetch";
|
|
1462
1460
|
etl: "etl";
|
|
1461
|
+
prefetch: "prefetch";
|
|
1463
1462
|
}>;
|
|
1464
1463
|
table: z.ZodString;
|
|
1465
1464
|
columns: z.ZodArray<z.ZodString>;
|
|
@@ -1510,6 +1509,7 @@ declare const ormMigrationExportSchema: z.ZodObject<{
|
|
|
1510
1509
|
orderBy: z.ZodOptional<z.ZodString>;
|
|
1511
1510
|
}, z.core.$strict>>;
|
|
1512
1511
|
deletedBy: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
1512
|
+
existsIn: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
1513
1513
|
materialized: z.ZodOptional<z.ZodObject<{
|
|
1514
1514
|
table: z.ZodString;
|
|
1515
1515
|
compute: z.ZodString;
|
package/dist/index.js
CHANGED
|
@@ -1187,6 +1187,48 @@ var policyExpressionSchema = z.union([
|
|
|
1187
1187
|
variants: z.array(policyVariantSchema).max(10).default([])
|
|
1188
1188
|
})
|
|
1189
1189
|
]);
|
|
1190
|
+
|
|
1191
|
+
// src/canonical/resolved-rows.ts
|
|
1192
|
+
function logicalKey(table) {
|
|
1193
|
+
return (table.primaryKey ?? []).map((columnName) => table.columns.find((c) => c.columnName === columnName)?.name ?? columnName);
|
|
1194
|
+
}
|
|
1195
|
+
function sameKey(key, expected) {
|
|
1196
|
+
return key.length === expected.length && key.every((name2) => expected.includes(name2));
|
|
1197
|
+
}
|
|
1198
|
+
function validateRowIdentity(input) {
|
|
1199
|
+
validateExistsIn(input);
|
|
1200
|
+
if (input.keys.length > 1) validateCompositeKey(input);
|
|
1201
|
+
}
|
|
1202
|
+
function validateExistsIn({ resolved, sourceByName, fail: fail3 }) {
|
|
1203
|
+
const name2 = resolved.existsIn?.[0];
|
|
1204
|
+
if (name2 === void 0) return;
|
|
1205
|
+
if (sourceByName.get(name2)?.kind !== "etl") fail3(`existsIn '${name2}' must be an etl source`);
|
|
1206
|
+
if (resolved.policies.insert !== void 0 || resolved.policies.write !== void 0) {
|
|
1207
|
+
fail3("existsIn allows only an update policy: rows come from the etl source");
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
function validateCompositeKey({ resolved, keys, tableByName, fail: fail3 }) {
|
|
1211
|
+
const keyList = `(${keys.join(", ")})`;
|
|
1212
|
+
if (resolved.existsIn === void 0) {
|
|
1213
|
+
fail3(`requires exactly one primary-key column: composite key ${keyList} requires existsIn`);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const writes = ["insert", "update", "write"].filter((kind) => resolved.policies[kind] !== void 0);
|
|
1217
|
+
if (writes.length > 0) fail3(`composite key ${keyList} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
1218
|
+
if (resolved.materialized) fail3(`composite key ${keyList} cannot be materialized`);
|
|
1219
|
+
for (const source of resolved.sources) {
|
|
1220
|
+
if (source.kind === "prefetch") {
|
|
1221
|
+
fail3(`composite key ${keyList} cannot have prefetch source '${source.name}'`);
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
const table = tableByName.get(source.table);
|
|
1225
|
+
if (table && !sameKey(logicalKey(table), keys)) {
|
|
1226
|
+
fail3(`composite key ${keyList} requires ${source.kind} source '${source.name}' to be keyed by it; table '${source.table}' is keyed by (${logicalKey(table).join(", ")})`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// src/canonical/resolved.ts
|
|
1190
1232
|
function parseDurationMs(input) {
|
|
1191
1233
|
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1192
1234
|
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
@@ -1263,6 +1305,13 @@ var canonicalResolvedSchema = z.object({
|
|
|
1263
1305
|
resolve: z.array(canonicalResolveRuleSchema).max(500),
|
|
1264
1306
|
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1265
1307
|
deletedBy: z.array(identifier).max(1).default([]),
|
|
1308
|
+
/**
|
|
1309
|
+
* `rows.existsIn`: the one etl source whose rows are the table's rows (read
|
|
1310
|
+
* directly as the row source). Only an update policy may write. It is also
|
|
1311
|
+
* what admits a composite primary key: a read-only table keyed exactly like
|
|
1312
|
+
* that source.
|
|
1313
|
+
*/
|
|
1314
|
+
existsIn: z.array(identifier).length(1).optional(),
|
|
1266
1315
|
/**
|
|
1267
1316
|
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1268
1317
|
* data plane keeps in step with `compute`, the internal view holding the
|
|
@@ -1281,8 +1330,8 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1281
1330
|
if (!resolved) return [];
|
|
1282
1331
|
const errors = [];
|
|
1283
1332
|
const fail3 = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1284
|
-
const
|
|
1285
|
-
if (
|
|
1333
|
+
const keyColumns = (view.primaryKey ?? []).map((columnName) => view.columns.find((c) => c.columnName === columnName)).filter((column) => column !== void 0);
|
|
1334
|
+
if (keyColumns.length === 0 || keyColumns.length !== view.primaryKey?.length) {
|
|
1286
1335
|
fail3("requires exactly one primary-key column");
|
|
1287
1336
|
return errors;
|
|
1288
1337
|
}
|
|
@@ -1290,7 +1339,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1290
1339
|
const scope = {
|
|
1291
1340
|
view,
|
|
1292
1341
|
resolved,
|
|
1293
|
-
|
|
1342
|
+
keys: new Map(keyColumns.map((column) => [column.name, column])),
|
|
1294
1343
|
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1295
1344
|
sourceByName: /* @__PURE__ */ new Map(),
|
|
1296
1345
|
fail: fail3
|
|
@@ -1304,6 +1353,7 @@ function validateResolvedView({ view, tables, views = [] }) {
|
|
|
1304
1353
|
if (!table) fail3(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1305
1354
|
else validateSource({ scope, source, table });
|
|
1306
1355
|
}
|
|
1356
|
+
validateRowIdentity({ resolved, keys: [...scope.keys.keys()], sourceByName: scope.sourceByName, tableByName, fail: fail3 });
|
|
1307
1357
|
validateRules(scope);
|
|
1308
1358
|
validateViewCells(scope);
|
|
1309
1359
|
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
@@ -1333,7 +1383,7 @@ function validateSource({ scope, source, table }) {
|
|
|
1333
1383
|
validatePrefetchFields({ scope, source });
|
|
1334
1384
|
}
|
|
1335
1385
|
function validatePrefetchFields({ scope, source }) {
|
|
1336
|
-
const { fail: fail3,
|
|
1386
|
+
const { fail: fail3, keys } = scope;
|
|
1337
1387
|
if (source.kind !== "prefetch") {
|
|
1338
1388
|
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1339
1389
|
fail3(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
@@ -1342,7 +1392,7 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1342
1392
|
}
|
|
1343
1393
|
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1344
1394
|
fail3(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1345
|
-
} else if (source.key
|
|
1395
|
+
} else if (!keys.has(source.key) && !source.columns.includes(source.key)) {
|
|
1346
1396
|
fail3(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1347
1397
|
}
|
|
1348
1398
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -1350,19 +1400,19 @@ function validatePrefetchFields({ scope, source }) {
|
|
|
1350
1400
|
}
|
|
1351
1401
|
}
|
|
1352
1402
|
function validateSourceColumns({ scope, source, byName }) {
|
|
1353
|
-
const { fail: fail3,
|
|
1354
|
-
for (const column of [
|
|
1355
|
-
const own = column
|
|
1403
|
+
const { fail: fail3, keys, dataColumns } = scope;
|
|
1404
|
+
for (const column of [...keys.keys(), ...source.columns]) {
|
|
1405
|
+
const own = keys.get(column) ?? dataColumns.get(column);
|
|
1356
1406
|
const theirs = byName.get(column);
|
|
1357
1407
|
if (!own) fail3(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1358
1408
|
else if (!theirs) fail3(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1359
1409
|
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail3(`column '${column}' has a different type on source '${source.name}'`);
|
|
1360
1410
|
}
|
|
1361
1411
|
}
|
|
1362
|
-
function validateRules({ resolved,
|
|
1412
|
+
function validateRules({ resolved, keys, dataColumns, sourceByName, fail: fail3 }) {
|
|
1363
1413
|
const ruled = /* @__PURE__ */ new Set();
|
|
1364
1414
|
for (const rule of resolved.resolve) {
|
|
1365
|
-
if (!dataColumns.has(rule.column) || rule.column
|
|
1415
|
+
if (!dataColumns.has(rule.column) || keys.has(rule.column)) {
|
|
1366
1416
|
fail3(`resolve rule targets unknown column '${rule.column}'`);
|
|
1367
1417
|
continue;
|
|
1368
1418
|
}
|
|
@@ -1379,7 +1429,7 @@ function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail: fa
|
|
|
1379
1429
|
}
|
|
1380
1430
|
}
|
|
1381
1431
|
for (const column of dataColumns.keys()) {
|
|
1382
|
-
if (column
|
|
1432
|
+
if (!keys.has(column) && !ruled.has(column)) fail3(`column '${column}' has no resolve rule`);
|
|
1383
1433
|
}
|
|
1384
1434
|
}
|
|
1385
1435
|
var VIEW_CELL_TYPES = {
|
|
@@ -1388,10 +1438,10 @@ var VIEW_CELL_TYPES = {
|
|
|
1388
1438
|
by: "text",
|
|
1389
1439
|
conflicts: "jsonb"
|
|
1390
1440
|
};
|
|
1391
|
-
function validateViewCells({ view, dataColumns,
|
|
1441
|
+
function validateViewCells({ view, dataColumns, keys, fail: fail3 }) {
|
|
1392
1442
|
const expected = /* @__PURE__ */ new Map();
|
|
1393
1443
|
for (const column of dataColumns.values()) {
|
|
1394
|
-
if (column.name
|
|
1444
|
+
if (keys.has(column.name)) continue;
|
|
1395
1445
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1396
1446
|
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1397
1447
|
}
|
|
@@ -1759,7 +1809,7 @@ var canonicalTableSchema = z.object({
|
|
|
1759
1809
|
table: identifier2,
|
|
1760
1810
|
displayName: z.string().trim().min(1).max(120).optional(),
|
|
1761
1811
|
description: z.string().max(2e3).optional(),
|
|
1762
|
-
/** Table-level rename hint
|
|
1812
|
+
/** Table-level rename hint. */
|
|
1763
1813
|
renamedFrom: identifier2.optional(),
|
|
1764
1814
|
/** Synced/reflected read-only tables (`source: schema.table`) — v1 semantics. */
|
|
1765
1815
|
source: z.string().regex(QUALIFIED_TABLE_PATTERN).optional(),
|
|
@@ -2264,8 +2314,9 @@ function planSource(input) {
|
|
|
2264
2314
|
}
|
|
2265
2315
|
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2266
2316
|
function internalTable(input, supplied, system, options = {}) {
|
|
2267
|
-
const { viewName, sourceName, columns,
|
|
2268
|
-
const tableColumns = {
|
|
2317
|
+
const { viewName, sourceName, columns, keys } = input;
|
|
2318
|
+
const tableColumns = {};
|
|
2319
|
+
for (const key of keys) tableColumns[key] = cloneColumn({ builder: columns[key], property: key, notNull: true, primaryKey: true });
|
|
2269
2320
|
for (const property of supplied) {
|
|
2270
2321
|
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2271
2322
|
}
|
|
@@ -2282,12 +2333,14 @@ function assertColumns(input, list2, what) {
|
|
|
2282
2333
|
for (const column of list2) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2283
2334
|
}
|
|
2284
2335
|
function planEtl(input, source) {
|
|
2285
|
-
const { sourceName,
|
|
2336
|
+
const { sourceName, keys, dataColumns, fail: fail3 } = input;
|
|
2286
2337
|
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2287
2338
|
if (!isSeqTable(table) || table.source === void 0) fail3(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2288
2339
|
const synced = table;
|
|
2289
2340
|
if (synced.config.primaryKey?.length === 0) fail3(`etl source "${sourceName}" cannot use an unkeyed synced table for resolved row identity`);
|
|
2290
|
-
|
|
2341
|
+
for (const key of keys) {
|
|
2342
|
+
if (!(key in synced.columns)) fail3(`etl source "${sourceName}" table "${synced.tableName}" has no "${key}" column`);
|
|
2343
|
+
}
|
|
2291
2344
|
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2292
2345
|
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2293
2346
|
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
@@ -2296,9 +2349,9 @@ function planEtl(input, source) {
|
|
|
2296
2349
|
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2297
2350
|
}
|
|
2298
2351
|
function planPrefetch(input, source) {
|
|
2299
|
-
const { sourceName,
|
|
2352
|
+
const { sourceName, keys, fail: fail3 } = input;
|
|
2300
2353
|
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2301
|
-
if (source.key
|
|
2354
|
+
if (!keys.includes(source.key) && !source.columns.includes(source.key)) {
|
|
2302
2355
|
fail3(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2303
2356
|
}
|
|
2304
2357
|
for (const column of source.writeback?.columns ?? []) {
|
|
@@ -2351,7 +2404,7 @@ var PG_TYPE = {
|
|
|
2351
2404
|
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2352
2405
|
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2353
2406
|
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2354
|
-
function candidateSql({ source, column,
|
|
2407
|
+
function candidateSql({ source, column, key, pgType }) {
|
|
2355
2408
|
const a = ident(`s_${source.name}`);
|
|
2356
2409
|
const name2 = physical({ source, property: column });
|
|
2357
2410
|
const value = `CAST(${a}.${ident(name2)} AS ${pgType})`;
|
|
@@ -2359,7 +2412,7 @@ function candidateSql({ source, column, pk, pgType }) {
|
|
|
2359
2412
|
return {
|
|
2360
2413
|
source: source.name,
|
|
2361
2414
|
value,
|
|
2362
|
-
has: `(${a}.${ident(physical({ source, property:
|
|
2415
|
+
has: `(${a}.${ident(physical({ source, property: key }))} IS NOT NULL)`,
|
|
2363
2416
|
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2364
2417
|
by: "CAST(NULL AS text)"
|
|
2365
2418
|
};
|
|
@@ -2423,6 +2476,12 @@ function builtinCell({ comparator, candidates }) {
|
|
|
2423
2476
|
}
|
|
2424
2477
|
};
|
|
2425
2478
|
}
|
|
2479
|
+
function existsInCell(candidate) {
|
|
2480
|
+
return {
|
|
2481
|
+
value: candidate.value,
|
|
2482
|
+
parts: { source: literal(candidate.source), at: candidate.at, by: candidate.by, conflicts: "CAST(NULL AS jsonb)" }
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2426
2485
|
function customCell({ candidates, orderBy, index: index3 }) {
|
|
2427
2486
|
const w = ident(`w_${index3}`);
|
|
2428
2487
|
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
@@ -2441,36 +2500,44 @@ function customCell({ candidates, orderBy, index: index3 }) {
|
|
|
2441
2500
|
}
|
|
2442
2501
|
function buildResolvedQuery({ context, plan }) {
|
|
2443
2502
|
const { qb, relation } = context;
|
|
2444
|
-
const
|
|
2503
|
+
const keyColumns = plan.keys.map((key) => plan.columns[key].columnNameFor(key));
|
|
2445
2504
|
const alias = (source) => ident(`s_${source}`);
|
|
2446
2505
|
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2447
2506
|
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2448
2507
|
const schema = getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2508
|
+
const keySource = plan.existsIn === void 0 ? void 0 : sourceByName.get(plan.existsIn);
|
|
2509
|
+
const keyRefs = plan.keys.map((key, index3) => keySource ? `${alias(keySource.name)}.${ident(physical({ source: keySource, property: key }))}` : `"k".${ident(keyColumns[index3])}`);
|
|
2449
2510
|
const keyBranches = plan.plans.map((source) => {
|
|
2450
2511
|
const where = source.kind === "prefetch" ? sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : sql.raw("");
|
|
2451
|
-
const
|
|
2452
|
-
|
|
2453
|
-
|
|
2512
|
+
const selectedKey = plan.keys.map((key, index3) => {
|
|
2513
|
+
const sourceKey = ident(physical({ source, property: key }));
|
|
2514
|
+
return sourceKey === ident(keyColumns[index3]) ? sourceKey : `${sourceKey} AS ${ident(keyColumns[index3])}`;
|
|
2515
|
+
});
|
|
2516
|
+
return sql`SELECT ${sql.raw(selectedKey.join(", "))} FROM ${relation(source.table)}${where}`;
|
|
2517
|
+
});
|
|
2518
|
+
const base2 = keySource ? sql`${relation(keySource.table)} AS ${sql.raw(alias(keySource.name))}` : sql`(${sql.join(keyBranches, sql.raw(" UNION "))}) AS "k"`;
|
|
2519
|
+
const joins = plan.plans.filter((source) => source !== keySource).map((source) => {
|
|
2520
|
+
const on = plan.keys.map((key, index3) => `${alias(source.name)}.${ident(physical({ source, property: key }))} = ${keyRefs[index3]}`);
|
|
2521
|
+
return sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(on.join(" AND "))}`;
|
|
2454
2522
|
});
|
|
2455
|
-
const joins = plan.plans.map(
|
|
2456
|
-
(source) => sql` LEFT JOIN ${relation(source.table)} AS ${sql.raw(alias(source.name))} ON ${sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2457
|
-
);
|
|
2458
2523
|
const cells = /* @__PURE__ */ new Map();
|
|
2459
2524
|
plan.rules.forEach((rule, index3) => {
|
|
2460
2525
|
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2461
2526
|
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2462
|
-
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column,
|
|
2463
|
-
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2527
|
+
const candidates = rule.candidates.map((name2) => candidateSql({ source: sourceByName.get(name2), column: rule.column, key: plan.keys[0], pgType }));
|
|
2528
|
+
cells.set(rule.column, keySource && rule.candidates.length === 1 && rule.candidates[0] === keySource.name ? existsInCell(candidates[0]) : rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index: index3 }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2464
2529
|
});
|
|
2465
2530
|
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [sql.raw(cell.lateral)] : []);
|
|
2466
|
-
const from = sql.join([
|
|
2467
|
-
const selected = {
|
|
2468
|
-
[plan.pk]: sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2469
|
-
};
|
|
2531
|
+
const from = sql.join([base2, ...joins, ...laterals], sql.raw(""));
|
|
2532
|
+
const selected = {};
|
|
2470
2533
|
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2471
|
-
if (property === plan.pk) continue;
|
|
2472
|
-
const cell = cells.get(property);
|
|
2473
2534
|
const columnName = builder.columnNameFor(property);
|
|
2535
|
+
const keyIndex = plan.keys.indexOf(property);
|
|
2536
|
+
if (keyIndex !== -1) {
|
|
2537
|
+
selected[property] = sql.raw(keyRefs[keyIndex]).as(columnName);
|
|
2538
|
+
continue;
|
|
2539
|
+
}
|
|
2540
|
+
const cell = cells.get(property);
|
|
2474
2541
|
selected[property] = sql.raw(cell.value).as(columnName);
|
|
2475
2542
|
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2476
2543
|
selected[cellColumnName(property, part)] = sql.raw(expression).as(cellColumnName(columnName, part));
|
|
@@ -2484,6 +2551,44 @@ function buildResolvedQuery({ context, plan }) {
|
|
|
2484
2551
|
const query = qb.select(selected).from(from);
|
|
2485
2552
|
return hidden.length > 0 ? query.where(sql.raw(hidden.join(" AND "))) : query;
|
|
2486
2553
|
}
|
|
2554
|
+
|
|
2555
|
+
// src/authoring/resolved-rows.ts
|
|
2556
|
+
function planKeys({ columns, config, fail: fail3 }) {
|
|
2557
|
+
const keys = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey).map(([property]) => property);
|
|
2558
|
+
if (keys.length === 0) fail3("declare exactly one .primaryKey() column");
|
|
2559
|
+
if (keys.length === 1) return keys;
|
|
2560
|
+
const key = `(${keys.join(", ")})`;
|
|
2561
|
+
if (config.rows?.existsIn === void 0) fail3(`declare exactly one .primaryKey() column: composite key ${key} requires rows.existsIn`);
|
|
2562
|
+
const writes = ["insert", "update", "write"].filter((kind) => config.policies[kind] !== void 0);
|
|
2563
|
+
if (writes.length > 0) fail3(`composite key ${key} requires a read-only table: remove the ${writes.join("/")} policy`);
|
|
2564
|
+
if (config.materialize) fail3(`composite key ${key} cannot be materialized`);
|
|
2565
|
+
const prefetch = Object.entries(config.sources).find(([, source]) => source.kind === "prefetch");
|
|
2566
|
+
if (prefetch) fail3(`composite key ${key} cannot have prefetch source "${prefetch[0]}"`);
|
|
2567
|
+
return keys;
|
|
2568
|
+
}
|
|
2569
|
+
function assertCompositeSourceKeys({ keys, plans, fail: fail3 }) {
|
|
2570
|
+
if (keys.length === 1) return;
|
|
2571
|
+
for (const plan of plans) {
|
|
2572
|
+
if (plan.kind !== "etl") continue;
|
|
2573
|
+
const sourceKey = declaredPrimaryKeyProperties(plan.table);
|
|
2574
|
+
if (sourceKey.length !== keys.length || !keys.every((key) => sourceKey.includes(key))) {
|
|
2575
|
+
fail3(`composite key (${keys.join(", ")}) requires etl source "${plan.name}" to be keyed by it; table "${plan.table.tableName}" is keyed by (${sourceKey.join(", ")})`);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
function planExistsIn({ config, plans, fail: fail3 }) {
|
|
2580
|
+
const names = config.rows?.existsIn;
|
|
2581
|
+
if (names === void 0) return void 0;
|
|
2582
|
+
if (names.length !== 1) fail3("rows.existsIn names exactly one etl source");
|
|
2583
|
+
const source = plans.find((plan) => plan.name === names[0]);
|
|
2584
|
+
if (source?.kind !== "etl") return fail3(`rows.existsIn "${names[0]}" must be an etl source`);
|
|
2585
|
+
if (config.policies.insert !== void 0 || config.policies.write !== void 0) fail3("rows.existsIn allows only an update policy: rows come from the etl source");
|
|
2586
|
+
return source;
|
|
2587
|
+
}
|
|
2588
|
+
function inheritedNotNullColumns({ existsIn, rules }) {
|
|
2589
|
+
if (!existsIn) return /* @__PURE__ */ new Set();
|
|
2590
|
+
return new Set(rules.filter((rule) => rule.candidates.length === 1 && rule.candidates[0] === existsIn.name).map((rule) => rule.column).filter((column) => existsIn.table.columns[column]?.toCanonicalColumn(column).nullable === false));
|
|
2591
|
+
}
|
|
2487
2592
|
function managedFn(id) {
|
|
2488
2593
|
return { kind: "managed", id };
|
|
2489
2594
|
}
|
|
@@ -2522,26 +2627,28 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2522
2627
|
fail3(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2523
2628
|
}
|
|
2524
2629
|
}
|
|
2525
|
-
const
|
|
2526
|
-
|
|
2527
|
-
const [pk, pkBuilder] = pkEntries[0];
|
|
2528
|
-
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2630
|
+
const keys = planKeys({ columns, config, fail: fail3 });
|
|
2631
|
+
const dataColumns = Object.keys(columns).filter((property) => !keys.includes(property));
|
|
2529
2632
|
if (dataColumns.length === 0) fail3("declare at least one non-key column");
|
|
2530
2633
|
const sourceEntries = Object.entries(config.sources);
|
|
2531
2634
|
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail3("declare exactly one manualSource()");
|
|
2532
2635
|
const plans = sourceEntries.map(
|
|
2533
|
-
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns,
|
|
2636
|
+
([sourceName, input]) => planSource({ viewName: name2, sourceName, input, columns, keys, dataColumns, fail: fail3 })
|
|
2534
2637
|
);
|
|
2535
2638
|
const rules = planRules({ config, plans, dataColumns, fail: fail3 });
|
|
2536
2639
|
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2537
2640
|
for (const source of deletedBy) {
|
|
2538
2641
|
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail3(`rows.deletedBy "${source}" must be the manual source`);
|
|
2539
2642
|
}
|
|
2643
|
+
assertCompositeSourceKeys({ keys, plans, fail: fail3 });
|
|
2644
|
+
const existsIn = planExistsIn({ config, plans, fail: fail3 });
|
|
2645
|
+
const notNullColumns = inheritedNotNullColumns({ existsIn, rules });
|
|
2540
2646
|
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2541
2647
|
const viewColumns = {};
|
|
2542
2648
|
for (const [property, builder] of Object.entries(columns)) {
|
|
2543
|
-
|
|
2544
|
-
|
|
2649
|
+
const isKey = keys.includes(property);
|
|
2650
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: isKey || notNullColumns.has(property), primaryKey: isKey });
|
|
2651
|
+
if (isKey) continue;
|
|
2545
2652
|
const columnName = builder.columnNameFor(property);
|
|
2546
2653
|
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2547
2654
|
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
@@ -2554,9 +2661,10 @@ function defineResolvedTable(name2, columns, config) {
|
|
|
2554
2661
|
sources: plans,
|
|
2555
2662
|
rules,
|
|
2556
2663
|
deletedBy,
|
|
2664
|
+
...existsIn ? { existsIn: [existsIn.name] } : {},
|
|
2557
2665
|
policies: writePolicies
|
|
2558
2666
|
};
|
|
2559
|
-
const queryPlan = { columns,
|
|
2667
|
+
const queryPlan = { columns, keys, plans, rules, deletedBy, ...existsIn ? { existsIn: existsIn.name } : {} };
|
|
2560
2668
|
const typedColumns = viewColumns;
|
|
2561
2669
|
const surface = {
|
|
2562
2670
|
...config.description !== void 0 ? { description: config.description } : {},
|
|
@@ -3197,6 +3305,7 @@ function buildCanonicalResolved(resolved) {
|
|
|
3197
3305
|
})),
|
|
3198
3306
|
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
3199
3307
|
deletedBy: [...resolved.deletedBy],
|
|
3308
|
+
...resolved.existsIn !== void 0 ? { existsIn: [...resolved.existsIn] } : {},
|
|
3200
3309
|
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
3201
3310
|
policies
|
|
3202
3311
|
};
|
|
@@ -7122,8 +7231,13 @@ var resolvedSourceIrSchema = z.object({
|
|
|
7122
7231
|
}).optional()
|
|
7123
7232
|
});
|
|
7124
7233
|
var resolvedMetaIrSchema = z.object({
|
|
7125
|
-
/**
|
|
7126
|
-
|
|
7234
|
+
/**
|
|
7235
|
+
* Logical pk column of the resolved view. Absent for a composite key, which
|
|
7236
|
+
* only a read-only `existsIn` table may declare: it has no writes, prefetch
|
|
7237
|
+
* or materialization (the paths that address one key column), and its reads
|
|
7238
|
+
* use the view's `primaryKey` like any keyed view.
|
|
7239
|
+
*/
|
|
7240
|
+
pk: z.string().optional(),
|
|
7127
7241
|
/** Source name of the manual source. */
|
|
7128
7242
|
manual: z.string(),
|
|
7129
7243
|
sources: z.array(resolvedSourceIrSchema),
|