@sequenceholdings/orm 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -1
- package/dist/authoring.cjs +1358 -45
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +223 -2
- package/dist/authoring.d.ts +223 -2
- package/dist/authoring.js +1349 -47
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +662 -38
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +696 -53
- package/dist/canonical.d.ts +696 -53
- package/dist/canonical.js +647 -39
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1808 -247
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1809 -248
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +90 -25
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +39 -22
- package/dist/client.d.ts +39 -22
- package/dist/client.js +87 -26
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +1325 -285
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +23 -2
- package/dist/compiler.d.ts +23 -2
- package/dist/compiler.js +1325 -285
- package/dist/compiler.js.map +1 -1
- package/dist/constants-YoFGjIBM.d.cts +99 -0
- package/dist/constants-YoFGjIBM.d.ts +99 -0
- package/dist/dynamic-input.d.cts +1 -0
- package/dist/dynamic-input.d.ts +1 -0
- package/dist/index.cjs +1605 -140
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +230 -113
- package/dist/index.d.ts +230 -113
- package/dist/index.js +1583 -142
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +727 -38
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +539 -21
- package/dist/ir.d.ts +539 -21
- package/dist/ir.js +722 -39
- package/dist/ir.js.map +1 -1
- package/dist/{naming-DCn94LJ4.d.ts → naming-CPfx7EfF.d.ts} +14 -1
- package/dist/{naming-DPglwzkB.d.cts → naming-CU9ThP_E.d.cts} +14 -1
- package/dist/policy-1iZbGnZZ.d.cts +24 -0
- package/dist/policy-1iZbGnZZ.d.ts +24 -0
- package/dist/predicate-v2.cjs +726 -42
- package/dist/predicate-v2.cjs.map +1 -1
- package/dist/predicate-v2.d.cts +2 -1
- package/dist/predicate-v2.d.ts +2 -1
- package/dist/predicate-v2.js +726 -42
- package/dist/predicate-v2.js.map +1 -1
- package/dist/sdl.cjs +858 -54
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +7 -3
- package/dist/sdl.d.ts +7 -3
- package/dist/sdl.js +856 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +9 -4
- package/templates/orm-namespace/CLAUDE.md +6 -4
- package/templates/orm-namespace/codegen.preset.d.cts +7 -0
- package/templates/orm-namespace/codegen.ts +2 -1
- package/templates/orm-namespace/graphql/example/queries.ts +4 -3
- package/templates/orm-namespace/package.json +2 -1
- package/templates/orm-namespace/schema/tables.ts +3 -2
- package/templates/orm-namespace/sequence.config.ts +7 -11
package/dist/authoring.cjs
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
var drizzleOrm = require('drizzle-orm');
|
|
4
4
|
var pgCore = require('drizzle-orm/pg-core');
|
|
5
|
-
var crypto = require('crypto');
|
|
6
5
|
var zod = require('zod');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
7
|
var jiti = require('jiti');
|
|
8
8
|
var promises = require('fs/promises');
|
|
9
9
|
var module$1 = require('module');
|
|
@@ -37,7 +37,8 @@ var ColumnBuilder = class {
|
|
|
37
37
|
description: void 0,
|
|
38
38
|
renamedFrom: void 0,
|
|
39
39
|
cast: void 0,
|
|
40
|
-
deprecated: false
|
|
40
|
+
deprecated: false,
|
|
41
|
+
cell: void 0
|
|
41
42
|
};
|
|
42
43
|
}
|
|
43
44
|
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
@@ -97,6 +98,11 @@ var ColumnBuilder = class {
|
|
|
97
98
|
this.state.deprecated = true;
|
|
98
99
|
return this;
|
|
99
100
|
}
|
|
101
|
+
/** @internal System cell-metadata part of logical column `of` (generated resolved views only). */
|
|
102
|
+
cellPart(of, part) {
|
|
103
|
+
this.state.cell = { of, part };
|
|
104
|
+
return this;
|
|
105
|
+
}
|
|
100
106
|
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
101
107
|
get isPrimaryKey() {
|
|
102
108
|
return this.state.primaryKey;
|
|
@@ -125,6 +131,7 @@ var ColumnBuilder = class {
|
|
|
125
131
|
if (this.state.description !== void 0) column.description = this.state.description;
|
|
126
132
|
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
127
133
|
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
134
|
+
if (this.state.cell !== void 0) column.cell = { ...this.state.cell };
|
|
128
135
|
return column;
|
|
129
136
|
}
|
|
130
137
|
};
|
|
@@ -153,6 +160,21 @@ var COLUMN_IDENTIFIER_PATTERN = /^_?[a-z][a-z0-9_]*$/;
|
|
|
153
160
|
var MAX_IDENTIFIER_LENGTH = 63;
|
|
154
161
|
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
155
162
|
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
163
|
+
var CELL_PARTS = ["at", "by", "rev", "source", "conflicts"];
|
|
164
|
+
var CELL_PART_SUFFIX = {
|
|
165
|
+
at: "__at",
|
|
166
|
+
by: "__by",
|
|
167
|
+
rev: "__rev",
|
|
168
|
+
source: "__src",
|
|
169
|
+
conflicts: "__lost"
|
|
170
|
+
};
|
|
171
|
+
var MAX_TRACKED_COLUMN_NAME = MAX_IDENTIFIER_LENGTH - 6;
|
|
172
|
+
var CELLS_FIELD = "cells";
|
|
173
|
+
function cellColumnName(column, part) {
|
|
174
|
+
return `${column}${CELL_PART_SUFFIX[part]}`;
|
|
175
|
+
}
|
|
176
|
+
var MATERIALIZE_ACTOR = "orm:materialize";
|
|
177
|
+
JSON.stringify({ sub: MATERIALIZE_ACTOR, org: null, roles: [], capabilities: [] });
|
|
156
178
|
|
|
157
179
|
// src/errors.ts
|
|
158
180
|
var OrmDefinitionError = class extends Error {
|
|
@@ -221,6 +243,8 @@ function toOne(target, options) {
|
|
|
221
243
|
on: options.on,
|
|
222
244
|
references: options.references,
|
|
223
245
|
enforce: options.enforce ?? false,
|
|
246
|
+
onDelete: options.onDelete,
|
|
247
|
+
onUpdate: options.onUpdate,
|
|
224
248
|
description: options.description
|
|
225
249
|
};
|
|
226
250
|
}
|
|
@@ -232,6 +256,8 @@ function toMany(target, options) {
|
|
|
232
256
|
on: options.on,
|
|
233
257
|
references: options.references,
|
|
234
258
|
enforce: false,
|
|
259
|
+
onDelete: void 0,
|
|
260
|
+
onUpdate: void 0,
|
|
235
261
|
description: options.description
|
|
236
262
|
};
|
|
237
263
|
}
|
|
@@ -243,6 +269,8 @@ function toPlatformUser(options) {
|
|
|
243
269
|
on: options.on,
|
|
244
270
|
references: options.references,
|
|
245
271
|
enforce: false,
|
|
272
|
+
onDelete: void 0,
|
|
273
|
+
onUpdate: void 0,
|
|
246
274
|
description: options.description
|
|
247
275
|
};
|
|
248
276
|
}
|
|
@@ -277,6 +305,12 @@ function defineTable(name, columns, config = {}) {
|
|
|
277
305
|
}
|
|
278
306
|
const columnName = builder.columnNameFor(propertyName);
|
|
279
307
|
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file, pattern: COLUMN_IDENTIFIER_PATTERN });
|
|
308
|
+
if (config.materializes === void 0 && (propertyName === CELLS_FIELD || Object.values(CELL_PART_SUFFIX).some((suffix) => columnName.endsWith(suffix) || propertyName.endsWith(suffix)))) {
|
|
309
|
+
throw new OrmDefinitionError(
|
|
310
|
+
`table "${name}": "${propertyName}" collides with the reserved cell-metadata names ('${CELLS_FIELD}', or a ${Object.values(CELL_PART_SUFFIX).join("/")} suffix)`,
|
|
311
|
+
file
|
|
312
|
+
);
|
|
313
|
+
}
|
|
280
314
|
const existing = seenColumnNames.get(columnName);
|
|
281
315
|
if (existing !== void 0) {
|
|
282
316
|
throw new OrmDefinitionError(
|
|
@@ -524,11 +558,11 @@ function compileViewQueries(input) {
|
|
|
524
558
|
);
|
|
525
559
|
}
|
|
526
560
|
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
527
|
-
const tableDependencies = pgCore.extractUsedTable(config.query).map((
|
|
528
|
-
const logical = physicalToLogical.get(
|
|
561
|
+
const tableDependencies = pgCore.extractUsedTable(config.query).map((physical2) => {
|
|
562
|
+
const logical = physicalToLogical.get(physical2);
|
|
529
563
|
if (!logical) {
|
|
530
564
|
throw new OrmDefinitionError(
|
|
531
|
-
`view "${authored.viewName}" query references unmanaged relation "${
|
|
565
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical2}" \u2014 use relation(...) with a namespace table or view`,
|
|
532
566
|
authored.file
|
|
533
567
|
);
|
|
534
568
|
}
|
|
@@ -852,9 +886,468 @@ function scalarColumnType(table, property, helper) {
|
|
|
852
886
|
}
|
|
853
887
|
return type;
|
|
854
888
|
}
|
|
889
|
+
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
890
|
+
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
891
|
+
|
|
892
|
+
// src/checks/compile.ts
|
|
893
|
+
function tokenize(source) {
|
|
894
|
+
const tokens = [];
|
|
895
|
+
for (let position = 0; position < source.length; ) {
|
|
896
|
+
const char = source[position];
|
|
897
|
+
if (/\s/.test(char)) {
|
|
898
|
+
position++;
|
|
899
|
+
continue;
|
|
900
|
+
}
|
|
901
|
+
if (char === "(" || char === ")" || char === ",") {
|
|
902
|
+
tokens.push({ kind: char === "(" ? "lparen" : char === ")" ? "rparen" : "comma", value: char, position });
|
|
903
|
+
position++;
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (char === "'") {
|
|
907
|
+
const start = position++;
|
|
908
|
+
let value = "";
|
|
909
|
+
let closed = false;
|
|
910
|
+
while (position < source.length) {
|
|
911
|
+
if (source[position] !== "'") {
|
|
912
|
+
value += source[position++];
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (source[position + 1] === "'") {
|
|
916
|
+
value += "'";
|
|
917
|
+
position += 2;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
position++;
|
|
921
|
+
closed = true;
|
|
922
|
+
break;
|
|
923
|
+
}
|
|
924
|
+
if (!closed) throw new Error(`unterminated string at position ${start}`);
|
|
925
|
+
if (/[\x00-\x1f\x7f]/.test(value)) throw new Error(`control character in string at position ${start}`);
|
|
926
|
+
tokens.push({ kind: "string", value, position: start });
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const operator = /^(<=|>=|<>|!=|!~|=|<|>|~)/.exec(source.slice(position));
|
|
930
|
+
if (operator) {
|
|
931
|
+
tokens.push({ kind: "operator", value: operator[0] === "<>" ? "!=" : operator[0], position });
|
|
932
|
+
position += operator[0].length;
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const number = /^-?\d+(?:\.\d+)?/.exec(source.slice(position));
|
|
936
|
+
if (number) {
|
|
937
|
+
tokens.push({ kind: "number", value: number[0], position });
|
|
938
|
+
position += number[0].length;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const identifier3 = /^[a-zA-Z_][a-zA-Z_0-9]*/.exec(source.slice(position));
|
|
942
|
+
if (identifier3) {
|
|
943
|
+
tokens.push({ kind: "identifier", value: identifier3[0], position });
|
|
944
|
+
position += identifier3[0].length;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
throw new Error(`unexpected character '${char}' at position ${position}`);
|
|
948
|
+
}
|
|
949
|
+
tokens.push({ kind: "end", value: "", position: source.length });
|
|
950
|
+
if (tokens.length > 256) throw new Error("check expression exceeds 256 tokens");
|
|
951
|
+
return tokens;
|
|
952
|
+
}
|
|
953
|
+
var Parser = class {
|
|
954
|
+
constructor(tokens, columns) {
|
|
955
|
+
this.tokens = tokens;
|
|
956
|
+
this.columns = /* @__PURE__ */ new Map();
|
|
957
|
+
for (const column of columns) {
|
|
958
|
+
for (const alias of [column.name, column.columnName]) {
|
|
959
|
+
const previous = this.columns.get(alias);
|
|
960
|
+
if (previous && previous.columnName !== column.columnName) {
|
|
961
|
+
throw new Error(`ambiguous check column '${alias}'`);
|
|
962
|
+
}
|
|
963
|
+
this.columns.set(alias, column);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
index = 0;
|
|
968
|
+
depth = 0;
|
|
969
|
+
columns;
|
|
970
|
+
parse() {
|
|
971
|
+
const expression = this.parseOr();
|
|
972
|
+
this.expect("end", "end of expression");
|
|
973
|
+
return expression;
|
|
974
|
+
}
|
|
975
|
+
parseOr() {
|
|
976
|
+
let left = this.parseAnd();
|
|
977
|
+
while (this.matchKeyword("or")) left = `(${left} OR ${this.parseAnd()})`;
|
|
978
|
+
return left;
|
|
979
|
+
}
|
|
980
|
+
parseAnd() {
|
|
981
|
+
let left = this.parseNot();
|
|
982
|
+
while (this.matchKeyword("and")) left = `(${left} AND ${this.parseNot()})`;
|
|
983
|
+
return left;
|
|
984
|
+
}
|
|
985
|
+
parseNot() {
|
|
986
|
+
if (this.matchKeyword("not")) return `(NOT ${this.parseNot()})`;
|
|
987
|
+
return this.parsePrimary();
|
|
988
|
+
}
|
|
989
|
+
parsePrimary() {
|
|
990
|
+
if (this.peek().kind === "lparen") {
|
|
991
|
+
if (++this.depth > 32) throw new Error("check expression nesting exceeds 32 levels");
|
|
992
|
+
this.advance();
|
|
993
|
+
const expression = this.parseOr();
|
|
994
|
+
this.expect("rparen", "')'");
|
|
995
|
+
this.depth--;
|
|
996
|
+
return `(${expression})`;
|
|
997
|
+
}
|
|
998
|
+
return this.parseComparison();
|
|
999
|
+
}
|
|
1000
|
+
parseComparison() {
|
|
1001
|
+
const left = this.parseValue();
|
|
1002
|
+
if (this.matchKeyword("is")) {
|
|
1003
|
+
const negate = this.matchKeyword("not");
|
|
1004
|
+
this.expectKeyword("null");
|
|
1005
|
+
return `${left.sql} IS ${negate ? "NOT " : ""}NULL`;
|
|
1006
|
+
}
|
|
1007
|
+
if (this.matchKeyword("between")) {
|
|
1008
|
+
const lower = this.parseValue();
|
|
1009
|
+
this.expectKeyword("and");
|
|
1010
|
+
const upper = this.parseValue();
|
|
1011
|
+
return `${left.sql} BETWEEN ${lower.sql} AND ${upper.sql}`;
|
|
1012
|
+
}
|
|
1013
|
+
if (this.matchKeyword("in")) {
|
|
1014
|
+
this.expect("lparen", "'(' after IN");
|
|
1015
|
+
const values = [this.parseValue()];
|
|
1016
|
+
while (this.peek().kind === "comma") {
|
|
1017
|
+
this.advance();
|
|
1018
|
+
values.push(this.parseValue());
|
|
1019
|
+
}
|
|
1020
|
+
this.expect("rparen", "')' after IN values");
|
|
1021
|
+
if (values.some((value) => !value.literal)) throw new Error("IN values must be literals");
|
|
1022
|
+
return `${left.sql} IN (${values.map((value) => value.sql).join(", ")})`;
|
|
1023
|
+
}
|
|
1024
|
+
const operator = this.expect("operator", "a comparison operator");
|
|
1025
|
+
const right = this.parseValue();
|
|
1026
|
+
if (operator.value === "~" || operator.value === "!~") {
|
|
1027
|
+
if (left.column && left.column.type !== "text") throw new Error("regex checks require a text column");
|
|
1028
|
+
if (!right.literal) throw new Error("regex pattern must be a literal");
|
|
1029
|
+
}
|
|
1030
|
+
return `${left.sql} ${operator.value} ${right.sql}`;
|
|
1031
|
+
}
|
|
1032
|
+
parseValue() {
|
|
1033
|
+
const token = this.advance();
|
|
1034
|
+
if (token.kind === "string") return { sql: quoteLiteral(token.value), literal: true };
|
|
1035
|
+
if (token.kind === "number") {
|
|
1036
|
+
const number = Number(token.value);
|
|
1037
|
+
if (!Number.isFinite(number) || Number.isInteger(number) && !Number.isSafeInteger(number)) {
|
|
1038
|
+
throw new Error(`unsafe numeric literal at position ${token.position}`);
|
|
1039
|
+
}
|
|
1040
|
+
return { sql: token.value, literal: true };
|
|
1041
|
+
}
|
|
1042
|
+
if (token.kind !== "identifier") throw new Error(`expected a column or literal at position ${token.position}`);
|
|
1043
|
+
const keyword = token.value.toLowerCase();
|
|
1044
|
+
if (keyword === "true" || keyword === "false" || keyword === "null") return { sql: keyword.toUpperCase(), literal: true };
|
|
1045
|
+
if (keyword === "btrim") {
|
|
1046
|
+
this.expect("lparen", "'(' after btrim");
|
|
1047
|
+
const column2 = this.parseColumn();
|
|
1048
|
+
this.expect("rparen", "')' after btrim column");
|
|
1049
|
+
if (column2.type !== "text") throw new Error("btrim requires a text column");
|
|
1050
|
+
return { sql: `btrim(${quoteIdentifier(column2.columnName)})`, column: column2 };
|
|
1051
|
+
}
|
|
1052
|
+
const column = this.columns.get(token.value);
|
|
1053
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1054
|
+
return { sql: quoteIdentifier(column.columnName), column };
|
|
1055
|
+
}
|
|
1056
|
+
parseColumn() {
|
|
1057
|
+
const token = this.expect("identifier", "a column name");
|
|
1058
|
+
const column = this.columns.get(token.value);
|
|
1059
|
+
if (!column) throw new Error(`unknown check column '${token.value}'`);
|
|
1060
|
+
return column;
|
|
1061
|
+
}
|
|
1062
|
+
peek() {
|
|
1063
|
+
return this.tokens[this.index];
|
|
1064
|
+
}
|
|
1065
|
+
advance() {
|
|
1066
|
+
const token = this.peek();
|
|
1067
|
+
if (token.kind !== "end") this.index++;
|
|
1068
|
+
return token;
|
|
1069
|
+
}
|
|
1070
|
+
matchKeyword(keyword) {
|
|
1071
|
+
if (this.peek().kind !== "identifier" || this.peek().value.toLowerCase() !== keyword) return false;
|
|
1072
|
+
this.advance();
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
expectKeyword(keyword) {
|
|
1076
|
+
if (!this.matchKeyword(keyword)) throw new Error(`expected '${keyword}' at position ${this.peek().position}`);
|
|
1077
|
+
}
|
|
1078
|
+
expect(kind, description) {
|
|
1079
|
+
const token = this.peek();
|
|
1080
|
+
if (token.kind !== kind) throw new Error(`expected ${description} at position ${token.position}`);
|
|
1081
|
+
return this.advance();
|
|
1082
|
+
}
|
|
1083
|
+
};
|
|
1084
|
+
function compileCheckExpression(source, columns) {
|
|
1085
|
+
return new Parser(tokenize(source), columns).parse();
|
|
1086
|
+
}
|
|
1087
|
+
var policyVariantSchema = zod.z.object({
|
|
1088
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
1089
|
+
when: zod.z.string().min(1).max(1e3),
|
|
1090
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
1091
|
+
allow: zod.z.literal(true).optional(),
|
|
1092
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
1093
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
1094
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
1095
|
+
});
|
|
1096
|
+
var policyExpressionSchema = zod.z.union([
|
|
1097
|
+
zod.z.string().min(1).max(2e3),
|
|
1098
|
+
zod.z.object({
|
|
1099
|
+
default: zod.z.string().min(1).max(2e3),
|
|
1100
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
1101
|
+
})
|
|
1102
|
+
]);
|
|
1103
|
+
function parseDurationMs(input) {
|
|
1104
|
+
if (typeof input === "number") return Number.isInteger(input) && input > 0 ? input : void 0;
|
|
1105
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m)$/.exec(input);
|
|
1106
|
+
if (!match) return void 0;
|
|
1107
|
+
const factor = match[2] === "ms" ? 1 : match[2] === "s" ? 1e3 : 6e4;
|
|
1108
|
+
const value = Math.round(Number(match[1]) * factor);
|
|
1109
|
+
return value > 0 ? value : void 0;
|
|
1110
|
+
}
|
|
1111
|
+
var identifier = zod.z.string().min(1).max(63).regex(/^[a-z][a-z0-9_]*$/);
|
|
1112
|
+
var durationMs = zod.z.number().int().positive().max(24 * 60 * 60 * 1e3);
|
|
1113
|
+
var RESOLVED_VIEW_SYSTEM_COLUMNS = {
|
|
1114
|
+
writebackStatus: { name: "writebackStatus", columnName: "_writeback_status", type: "text" },
|
|
1115
|
+
writebackError: { name: "writebackError", columnName: "_writeback_error", type: "text" }
|
|
1116
|
+
};
|
|
1117
|
+
var RESOLVED_SOURCE_SYSTEM_COLUMNS = {
|
|
1118
|
+
tombstoned: { name: "sysTombstoned", columnName: "_tombstoned" },
|
|
1119
|
+
fetchedAt: { name: "sysFetchedAt", columnName: "_fetched_at" },
|
|
1120
|
+
writebackStatus: { name: "sysWritebackStatus", columnName: "_writeback_status" },
|
|
1121
|
+
writebackError: { name: "sysWritebackError", columnName: "_writeback_error" },
|
|
1122
|
+
writebackAt: { name: "sysWritebackAt", columnName: "_writeback_at" }
|
|
1123
|
+
};
|
|
1124
|
+
var RESOLVED_VIEW_CELL_PARTS = ["source", "at", "by", "conflicts"];
|
|
1125
|
+
var RESERVED_RESOLVED_COLUMN_NAMES = /* @__PURE__ */ new Set([
|
|
1126
|
+
...Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName]),
|
|
1127
|
+
...Object.values(RESOLVED_SOURCE_SYSTEM_COLUMNS).flatMap((c) => [c.name, c.columnName])
|
|
1128
|
+
]);
|
|
1129
|
+
var COMPARATORS = ["latest", "highest", "lowest", "priority", "nonNullFirst", "custom"];
|
|
1130
|
+
var canonicalFunctionRefSchema = zod.z.object({
|
|
1131
|
+
/** `managed` = a Managed Function id/slug; `native` = an in-process Atlas function (reserved). */
|
|
1132
|
+
kind: zod.z.enum(["managed", "native"]),
|
|
1133
|
+
id: zod.z.string().min(1).max(200)
|
|
1134
|
+
}).strict();
|
|
1135
|
+
var canonicalFetchShapeSchema = zod.z.union([
|
|
1136
|
+
zod.z.object({ shape: zod.z.literal("single") }).strict(),
|
|
1137
|
+
zod.z.object({ shape: zod.z.literal("batch"), maxKeys: zod.z.number().int().min(2).max(500) }).strict()
|
|
1138
|
+
]);
|
|
1139
|
+
var canonicalResolvedSourceSchema = zod.z.object({
|
|
1140
|
+
name: identifier,
|
|
1141
|
+
kind: zod.z.enum(["manual", "etl", "prefetch"]),
|
|
1142
|
+
/** Backing relation in this namespace: an internal table (manual/prefetch) or a synced table (etl). */
|
|
1143
|
+
table: identifier,
|
|
1144
|
+
/** Resolved columns (logical names) this source supplies — the primary key is implicit. */
|
|
1145
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
1146
|
+
/** etl: logical timestamptz/date column on the synced table used as every cell's updated_at. */
|
|
1147
|
+
updatedAt: zod.z.string().min(1).max(63).optional(),
|
|
1148
|
+
/** prefetch: resolved column holding the source system's key (may be the primary key). */
|
|
1149
|
+
key: zod.z.string().min(1).max(63).optional(),
|
|
1150
|
+
fetch: canonicalFetchShapeSchema.optional(),
|
|
1151
|
+
function: canonicalFunctionRefSchema.optional(),
|
|
1152
|
+
prefetchDefaults: zod.z.object({
|
|
1153
|
+
maxStalenessMs: durationMs,
|
|
1154
|
+
timeoutMs: durationMs,
|
|
1155
|
+
onError: zod.z.enum(["fallback", "fail"])
|
|
1156
|
+
}).strict().optional(),
|
|
1157
|
+
writeback: zod.z.object({
|
|
1158
|
+
columns: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(500),
|
|
1159
|
+
defaultMode: zod.z.enum(["sync", "async"]),
|
|
1160
|
+
timeoutMs: durationMs,
|
|
1161
|
+
/** Remove confirmed cells from the manual source so a stale edit cannot shadow the source. */
|
|
1162
|
+
clearManualOnConfirm: zod.z.boolean()
|
|
1163
|
+
}).strict().optional()
|
|
1164
|
+
}).strict();
|
|
1165
|
+
var canonicalResolveRuleSchema = zod.z.object({
|
|
1166
|
+
column: zod.z.string().min(1).max(63),
|
|
1167
|
+
comparator: zod.z.enum(COMPARATORS),
|
|
1168
|
+
/** Source names, in tie-break order. */
|
|
1169
|
+
candidates: zod.z.array(identifier).min(1).max(16),
|
|
1170
|
+
/** custom only: SQL ORDER BY key over value / updated_at / source / actor / ord. */
|
|
1171
|
+
orderBy: zod.z.string().min(1).max(2e3).optional()
|
|
1172
|
+
}).strict();
|
|
1173
|
+
var canonicalResolvedSchema = zod.z.object({
|
|
1174
|
+
sources: zod.z.array(canonicalResolvedSourceSchema).min(1).max(16),
|
|
1175
|
+
/** One rule per resolved (non-pk) column; documents the generated view for tooling. */
|
|
1176
|
+
resolve: zod.z.array(canonicalResolveRuleSchema).max(500),
|
|
1177
|
+
/** Sources whose tombstone hides the resolved row (v1: the manual source only). */
|
|
1178
|
+
deletedBy: zod.z.array(identifier).max(1).default([]),
|
|
1179
|
+
/**
|
|
1180
|
+
* Materialized: the view reads an indexed internal table (`table`) that the
|
|
1181
|
+
* data plane keeps in step with `compute`, the internal view holding the
|
|
1182
|
+
* resolution query. Omitted when the view computes on read.
|
|
1183
|
+
*/
|
|
1184
|
+
materialized: zod.z.object({ table: identifier, compute: identifier }).strict().optional(),
|
|
1185
|
+
/** Write policies evaluated against the RESOLVED row (reads use the view's read policy). */
|
|
1186
|
+
policies: zod.z.object({
|
|
1187
|
+
write: policyExpressionSchema.optional(),
|
|
1188
|
+
insert: policyExpressionSchema.optional(),
|
|
1189
|
+
update: policyExpressionSchema.optional()
|
|
1190
|
+
}).strict().default({})
|
|
1191
|
+
}).strict();
|
|
1192
|
+
function validateResolvedView({ view, tables, views = [] }) {
|
|
1193
|
+
const resolved = view.resolved;
|
|
1194
|
+
if (!resolved) return [];
|
|
1195
|
+
const errors = [];
|
|
1196
|
+
const fail = (message) => errors.push(`resolved table '${view.view}': ${message}`);
|
|
1197
|
+
const pkColumn = view.primaryKey?.length === 1 ? view.columns.find((c) => c.columnName === view.primaryKey?.[0]) : void 0;
|
|
1198
|
+
if (!pkColumn) {
|
|
1199
|
+
fail("requires exactly one primary-key column");
|
|
1200
|
+
return errors;
|
|
1201
|
+
}
|
|
1202
|
+
const systemNames = new Set(Object.values(RESOLVED_VIEW_SYSTEM_COLUMNS).map((c) => c.name));
|
|
1203
|
+
const scope = {
|
|
1204
|
+
view,
|
|
1205
|
+
resolved,
|
|
1206
|
+
pkColumn,
|
|
1207
|
+
dataColumns: new Map(view.columns.filter((c) => !systemNames.has(c.name) && c.cell === void 0).map((c) => [c.name, c])),
|
|
1208
|
+
sourceByName: /* @__PURE__ */ new Map(),
|
|
1209
|
+
fail
|
|
1210
|
+
};
|
|
1211
|
+
if (resolved.sources.filter((source) => source.kind === "manual").length !== 1) fail("requires exactly one manual source");
|
|
1212
|
+
const tableByName = new Map(tables.map((table) => [table.table, table]));
|
|
1213
|
+
for (const source of resolved.sources) {
|
|
1214
|
+
if (scope.sourceByName.has(source.name)) fail(`duplicate source '${source.name}'`);
|
|
1215
|
+
scope.sourceByName.set(source.name, source);
|
|
1216
|
+
const table = tableByName.get(source.table);
|
|
1217
|
+
if (!table) fail(`source '${source.name}' references unknown table '${source.table}'`);
|
|
1218
|
+
else validateSource({ scope, source, table });
|
|
1219
|
+
}
|
|
1220
|
+
validateRules(scope);
|
|
1221
|
+
validateViewCells(scope);
|
|
1222
|
+
if (resolved.materialized) validateMaterialized({ scope, tableByName, views });
|
|
1223
|
+
for (const name of resolved.deletedBy) {
|
|
1224
|
+
if (scope.sourceByName.get(name)?.kind !== "manual") fail(`deletedBy '${name}' must be the manual source`);
|
|
1225
|
+
}
|
|
1226
|
+
return errors;
|
|
1227
|
+
}
|
|
1228
|
+
function validateSource({ scope, source, table }) {
|
|
1229
|
+
const { fail } = scope;
|
|
1230
|
+
if (source.kind === "etl" && table.source === void 0) fail(`etl source '${source.name}' must reference a synced table`);
|
|
1231
|
+
if (source.kind !== "etl" && (table.internal !== true || table.cellMetadata !== true)) {
|
|
1232
|
+
fail(`${source.kind} source '${source.name}' must reference an internal table with cell metadata`);
|
|
1233
|
+
}
|
|
1234
|
+
const hasRev = table.columns.some((column) => column.cell?.part === "rev");
|
|
1235
|
+
if (source.kind === "manual" && !hasRev) fail(`manual source '${source.name}' must carry '__rev' cell columns`);
|
|
1236
|
+
if (source.kind === "prefetch" && hasRev) fail(`prefetch source '${source.name}' must not carry '__rev' cell columns`);
|
|
1237
|
+
const byName = new Map(table.columns.map((c) => [c.name, c]));
|
|
1238
|
+
validateSourceColumns({ scope, source, byName });
|
|
1239
|
+
if (source.kind === "etl") {
|
|
1240
|
+
const updatedAt = source.updatedAt === void 0 ? void 0 : byName.get(source.updatedAt);
|
|
1241
|
+
if (source.updatedAt !== void 0 && updatedAt?.type !== "timestamptz" && updatedAt?.type !== "date") {
|
|
1242
|
+
fail(`etl source '${source.name}' updatedAt must name a timestamptz/date column on '${source.table}'`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
validatePrefetchFields({ scope, source });
|
|
1246
|
+
}
|
|
1247
|
+
function validatePrefetchFields({ scope, source }) {
|
|
1248
|
+
const { fail, pkColumn } = scope;
|
|
1249
|
+
if (source.kind !== "prefetch") {
|
|
1250
|
+
if (source.key || source.fetch || source.function || source.prefetchDefaults || source.writeback) {
|
|
1251
|
+
fail(`only prefetch sources may declare key/fetch/function/prefetchDefaults/writeback ('${source.name}')`);
|
|
1252
|
+
}
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
if (!source.key || !source.fetch || !source.function || !source.prefetchDefaults) {
|
|
1256
|
+
fail(`prefetch source '${source.name}' requires key, fetch, function and prefetchDefaults`);
|
|
1257
|
+
} else if (source.key !== pkColumn.name && !source.columns.includes(source.key)) {
|
|
1258
|
+
fail(`prefetch source '${source.name}' key '${source.key}' must be the primary key or one of its columns`);
|
|
1259
|
+
}
|
|
1260
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
1261
|
+
if (!source.columns.includes(column)) fail(`prefetch source '${source.name}' writes back '${column}', which it does not supply`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function validateSourceColumns({ scope, source, byName }) {
|
|
1265
|
+
const { fail, pkColumn, dataColumns } = scope;
|
|
1266
|
+
for (const column of [pkColumn.name, ...source.columns]) {
|
|
1267
|
+
const own = column === pkColumn.name ? pkColumn : dataColumns.get(column);
|
|
1268
|
+
const theirs = byName.get(column);
|
|
1269
|
+
if (!own) fail(`source '${source.name}' supplies unknown column '${column}'`);
|
|
1270
|
+
else if (!theirs) fail(`source '${source.name}' table '${source.table}' has no column '${column}'`);
|
|
1271
|
+
else if (JSON.stringify(theirs.type) !== JSON.stringify(own.type)) fail(`column '${column}' has a different type on source '${source.name}'`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function validateRules({ resolved, pkColumn, dataColumns, sourceByName, fail }) {
|
|
1275
|
+
const ruled = /* @__PURE__ */ new Set();
|
|
1276
|
+
for (const rule of resolved.resolve) {
|
|
1277
|
+
if (!dataColumns.has(rule.column) || rule.column === pkColumn.name) {
|
|
1278
|
+
fail(`resolve rule targets unknown column '${rule.column}'`);
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (ruled.has(rule.column)) fail(`column '${rule.column}' has more than one resolve rule`);
|
|
1282
|
+
ruled.add(rule.column);
|
|
1283
|
+
if (rule.comparator === "custom" !== (rule.orderBy !== void 0)) {
|
|
1284
|
+
fail(`column '${rule.column}': orderBy is required for custom comparators and only allowed there`);
|
|
1285
|
+
}
|
|
1286
|
+
if (new Set(rule.candidates).size !== rule.candidates.length) fail(`column '${rule.column}' repeats a candidate`);
|
|
1287
|
+
for (const candidate of rule.candidates) {
|
|
1288
|
+
const source = sourceByName.get(candidate);
|
|
1289
|
+
if (!source) fail(`column '${rule.column}' names unknown candidate source '${candidate}'`);
|
|
1290
|
+
else if (!source.columns.includes(rule.column)) fail(`column '${rule.column}' candidate '${candidate}' does not supply it`);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
for (const column of dataColumns.keys()) {
|
|
1294
|
+
if (column !== pkColumn.name && !ruled.has(column)) fail(`column '${column}' has no resolve rule`);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
var VIEW_CELL_TYPES = {
|
|
1298
|
+
source: "text",
|
|
1299
|
+
at: "timestamptz",
|
|
1300
|
+
by: "text",
|
|
1301
|
+
conflicts: "jsonb"
|
|
1302
|
+
};
|
|
1303
|
+
function validateViewCells({ view, dataColumns, pkColumn, fail }) {
|
|
1304
|
+
const expected = /* @__PURE__ */ new Map();
|
|
1305
|
+
for (const column of dataColumns.values()) {
|
|
1306
|
+
if (column.name === pkColumn.name) continue;
|
|
1307
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
1308
|
+
expected.set(cellColumnName(column.name, part), { part, columnName: cellColumnName(column.columnName, part) });
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
for (const column of view.columns) {
|
|
1312
|
+
if (!column.cell) continue;
|
|
1313
|
+
const want = expected.get(column.name);
|
|
1314
|
+
if (!want || want.part !== column.cell.part || want.columnName !== column.columnName || cellColumnName(column.cell.of, column.cell.part) !== column.name) {
|
|
1315
|
+
fail(`unexpected cell column '${column.name}'`);
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (column.type !== VIEW_CELL_TYPES[column.cell.part]) fail(`cell column '${column.name}' must be ${VIEW_CELL_TYPES[column.cell.part]}`);
|
|
1319
|
+
expected.delete(column.name);
|
|
1320
|
+
}
|
|
1321
|
+
for (const name of expected.keys()) fail(`missing cell column '${name}'`);
|
|
1322
|
+
}
|
|
1323
|
+
var columnSignature = (columns) => JSON.stringify(columns.map((column) => [column.name, column.columnName, column.type]));
|
|
1324
|
+
function validateMaterialized({ scope, tableByName, views }) {
|
|
1325
|
+
const { view, resolved, fail } = scope;
|
|
1326
|
+
const { table: tableName, compute: computeName } = resolved.materialized;
|
|
1327
|
+
const table = tableByName.get(tableName);
|
|
1328
|
+
if (!table || table.internal !== true || table.materializes !== view.view) {
|
|
1329
|
+
fail(`materialized table '${tableName}' must be an internal table that materializes '${view.view}'`);
|
|
1330
|
+
} else {
|
|
1331
|
+
if (columnSignature(table.columns) !== columnSignature(view.columns)) fail(`materialized table '${tableName}' must have exactly the view's columns`);
|
|
1332
|
+
if (JSON.stringify(table.primaryKey ?? []) !== JSON.stringify(view.primaryKey ?? [])) fail(`materialized table '${tableName}' must share the view's primary key`);
|
|
1333
|
+
}
|
|
1334
|
+
const compute = views.find((candidate) => candidate.view === computeName);
|
|
1335
|
+
if (!compute || compute.internal !== true) fail(`compute view '${computeName}' must be an internal view`);
|
|
1336
|
+
else if (columnSignature(compute.columns) !== columnSignature(view.columns)) fail(`compute view '${computeName}' must have exactly the view's columns`);
|
|
1337
|
+
for (const source of resolved.sources) {
|
|
1338
|
+
if (source.kind !== "etl") continue;
|
|
1339
|
+
const read = tableByName.get(source.table)?.policies?.read;
|
|
1340
|
+
const unconditional = typeof read === "string" ? read.trim() === "true" : read !== void 0 && read.default.trim() === "true" && read.variants.length === 0;
|
|
1341
|
+
if (!unconditional) {
|
|
1342
|
+
fail(`etl source '${source.name}' must have read policy 'true' to be materialized (the copy is shared by every caller)`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// src/canonical/v2.ts
|
|
855
1348
|
var CANONICAL_FORMAT = 2;
|
|
856
1349
|
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
857
|
-
var
|
|
1350
|
+
var identifier2 = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
858
1351
|
var columnIdentifier = zod.z.string().min(1).max(63).regex(COLUMN_IDENTIFIER_PATTERN);
|
|
859
1352
|
var COLUMN_TYPES = [
|
|
860
1353
|
"uuid",
|
|
@@ -870,14 +1363,14 @@ var COLUMN_TYPES = [
|
|
|
870
1363
|
];
|
|
871
1364
|
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
872
1365
|
var canonicalEnumSchema = zod.z.object({
|
|
873
|
-
name:
|
|
1366
|
+
name: identifier2,
|
|
874
1367
|
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
875
1368
|
});
|
|
876
1369
|
var canonicalColumnSchema = zod.z.object({
|
|
877
1370
|
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
878
1371
|
name: zod.z.string().min(1).max(63),
|
|
879
1372
|
columnName: columnIdentifier,
|
|
880
|
-
type: zod.z.union([columnTypeSchema, zod.z.object({ enum:
|
|
1373
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier2 })]),
|
|
881
1374
|
nullable: zod.z.boolean().default(true),
|
|
882
1375
|
unique: zod.z.boolean().default(false),
|
|
883
1376
|
/** Literal default, or one of the allowlisted SQL functions. */
|
|
@@ -893,7 +1386,14 @@ var canonicalColumnSchema = zod.z.object({
|
|
|
893
1386
|
/** Migration hints — same semantics as v1. */
|
|
894
1387
|
renamedFrom: columnIdentifier.optional(),
|
|
895
1388
|
cast: zod.z.string().max(200).optional(),
|
|
896
|
-
deprecated: zod.z.boolean().default(false)
|
|
1389
|
+
deprecated: zod.z.boolean().default(false),
|
|
1390
|
+
/**
|
|
1391
|
+
* Marks a system cell-metadata column (see CELL_PARTS in constants): part
|
|
1392
|
+
* `part` of the cell of logical column `of`. Synthesized by authoring,
|
|
1393
|
+
* never client-writable, absent from GraphQL scalar fields. Omitted on
|
|
1394
|
+
* ordinary columns so their hashes are unchanged.
|
|
1395
|
+
*/
|
|
1396
|
+
cell: zod.z.object({ of: zod.z.string().min(1).max(63), part: zod.z.enum(CELL_PARTS) }).strict().optional()
|
|
897
1397
|
}).superRefine((column, ctx) => {
|
|
898
1398
|
if (column.onUpdateFn === void 0) return;
|
|
899
1399
|
if (typeof column.type !== "string" || column.type !== "timestamptz" && column.type !== "date") {
|
|
@@ -908,36 +1408,26 @@ var canonicalLinkSchema = zod.z.object({
|
|
|
908
1408
|
name: zod.z.string().min(1).max(63),
|
|
909
1409
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
910
1410
|
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
911
|
-
target:
|
|
1411
|
+
target: identifier2.optional(),
|
|
912
1412
|
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
913
1413
|
on: columnIdentifier,
|
|
914
1414
|
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
915
1415
|
references: columnIdentifier.optional(),
|
|
916
1416
|
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
917
1417
|
enforce: zod.z.boolean().default(false),
|
|
1418
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1419
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
918
1420
|
description: zod.z.string().max(500).optional()
|
|
919
1421
|
});
|
|
920
1422
|
var canonicalIndexSchema = zod.z.object({
|
|
921
1423
|
columns: zod.z.array(columnIdentifier).min(1),
|
|
922
1424
|
unique: zod.z.boolean().default(false),
|
|
923
|
-
name:
|
|
1425
|
+
name: identifier2.optional()
|
|
924
1426
|
});
|
|
925
|
-
var
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
allow: zod.z.literal(true).optional(),
|
|
930
|
-
policy: zod.z.string().min(1).max(2e3).optional()
|
|
931
|
-
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
932
|
-
message: "A variant declares exactly one of allow: true or policy"
|
|
933
|
-
});
|
|
934
|
-
var policyExpressionSchema = zod.z.union([
|
|
935
|
-
zod.z.string().min(1).max(2e3),
|
|
936
|
-
zod.z.object({
|
|
937
|
-
default: zod.z.string().min(1).max(2e3),
|
|
938
|
-
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
939
|
-
})
|
|
940
|
-
]);
|
|
1427
|
+
var canonicalCheckSchema = zod.z.object({
|
|
1428
|
+
name: identifier2,
|
|
1429
|
+
expression: zod.z.string().min(1).max(2e3)
|
|
1430
|
+
}).strict();
|
|
941
1431
|
var canonicalPoliciesSchema = zod.z.object({
|
|
942
1432
|
read: policyExpressionSchema.optional(),
|
|
943
1433
|
write: policyExpressionSchema.optional(),
|
|
@@ -967,8 +1457,165 @@ function isSingleViewQuerySql(value) {
|
|
|
967
1457
|
}
|
|
968
1458
|
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
969
1459
|
}
|
|
1460
|
+
var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
1461
|
+
"abs",
|
|
1462
|
+
"age",
|
|
1463
|
+
"array_agg",
|
|
1464
|
+
"array_append",
|
|
1465
|
+
"array_cat",
|
|
1466
|
+
"array_dims",
|
|
1467
|
+
"array_length",
|
|
1468
|
+
"array_lower",
|
|
1469
|
+
"array_ndims",
|
|
1470
|
+
"array_position",
|
|
1471
|
+
"array_positions",
|
|
1472
|
+
"array_prepend",
|
|
1473
|
+
"array_remove",
|
|
1474
|
+
"array_replace",
|
|
1475
|
+
"array_to_string",
|
|
1476
|
+
"array_trim",
|
|
1477
|
+
"array_upper",
|
|
1478
|
+
"ascii",
|
|
1479
|
+
"avg",
|
|
1480
|
+
"bit_and",
|
|
1481
|
+
"bit_or",
|
|
1482
|
+
"bit_xor",
|
|
1483
|
+
"bool_and",
|
|
1484
|
+
"bool_or",
|
|
1485
|
+
"btrim",
|
|
1486
|
+
"cardinality",
|
|
1487
|
+
"cbrt",
|
|
1488
|
+
"ceil",
|
|
1489
|
+
"ceiling",
|
|
1490
|
+
"char_length",
|
|
1491
|
+
"character_length",
|
|
1492
|
+
"chr",
|
|
1493
|
+
"concat",
|
|
1494
|
+
"concat_ws",
|
|
1495
|
+
"count",
|
|
1496
|
+
"cume_dist",
|
|
1497
|
+
"date_bin",
|
|
1498
|
+
"date_part",
|
|
1499
|
+
"date_trunc",
|
|
1500
|
+
"decode",
|
|
1501
|
+
"degrees",
|
|
1502
|
+
"dense_rank",
|
|
1503
|
+
"encode",
|
|
1504
|
+
"enum_first",
|
|
1505
|
+
"enum_last",
|
|
1506
|
+
"enum_range",
|
|
1507
|
+
"every",
|
|
1508
|
+
"exp",
|
|
1509
|
+
"first_value",
|
|
1510
|
+
"floor",
|
|
1511
|
+
"format",
|
|
1512
|
+
"grouping",
|
|
1513
|
+
"initcap",
|
|
1514
|
+
"json_agg",
|
|
1515
|
+
"json_array_length",
|
|
1516
|
+
"json_build_array",
|
|
1517
|
+
"json_build_object",
|
|
1518
|
+
"json_extract_path",
|
|
1519
|
+
"json_extract_path_text",
|
|
1520
|
+
"json_object_agg",
|
|
1521
|
+
"json_object_keys",
|
|
1522
|
+
"json_strip_nulls",
|
|
1523
|
+
"json_typeof",
|
|
1524
|
+
"jsonb_agg",
|
|
1525
|
+
"jsonb_array_length",
|
|
1526
|
+
"jsonb_build_array",
|
|
1527
|
+
"jsonb_build_object",
|
|
1528
|
+
"jsonb_extract_path",
|
|
1529
|
+
"jsonb_extract_path_text",
|
|
1530
|
+
"jsonb_insert",
|
|
1531
|
+
"jsonb_object_agg",
|
|
1532
|
+
"jsonb_object_keys",
|
|
1533
|
+
"jsonb_path_exists",
|
|
1534
|
+
"jsonb_path_match",
|
|
1535
|
+
"jsonb_path_query",
|
|
1536
|
+
"jsonb_path_query_array",
|
|
1537
|
+
"jsonb_path_query_first",
|
|
1538
|
+
"jsonb_pretty",
|
|
1539
|
+
"jsonb_set",
|
|
1540
|
+
"jsonb_set_lax",
|
|
1541
|
+
"jsonb_strip_nulls",
|
|
1542
|
+
"jsonb_typeof",
|
|
1543
|
+
"justify_days",
|
|
1544
|
+
"justify_hours",
|
|
1545
|
+
"justify_interval",
|
|
1546
|
+
"lag",
|
|
1547
|
+
"last_value",
|
|
1548
|
+
"lead",
|
|
1549
|
+
"left",
|
|
1550
|
+
"length",
|
|
1551
|
+
"ln",
|
|
1552
|
+
"log",
|
|
1553
|
+
"lower",
|
|
1554
|
+
"lpad",
|
|
1555
|
+
"ltrim",
|
|
1556
|
+
"make_date",
|
|
1557
|
+
"make_interval",
|
|
1558
|
+
"make_time",
|
|
1559
|
+
"make_timestamp",
|
|
1560
|
+
"make_timestamptz",
|
|
1561
|
+
"max",
|
|
1562
|
+
"md5",
|
|
1563
|
+
"min",
|
|
1564
|
+
"mod",
|
|
1565
|
+
"nth_value",
|
|
1566
|
+
"ntile",
|
|
1567
|
+
"octet_length",
|
|
1568
|
+
"percent_rank",
|
|
1569
|
+
"pi",
|
|
1570
|
+
"power",
|
|
1571
|
+
"quote_ident",
|
|
1572
|
+
"quote_literal",
|
|
1573
|
+
"quote_nullable",
|
|
1574
|
+
"radians",
|
|
1575
|
+
"rank",
|
|
1576
|
+
"regexp_count",
|
|
1577
|
+
"regexp_instr",
|
|
1578
|
+
"regexp_like",
|
|
1579
|
+
"regexp_match",
|
|
1580
|
+
"regexp_matches",
|
|
1581
|
+
"regexp_replace",
|
|
1582
|
+
"regexp_split_to_array",
|
|
1583
|
+
"regexp_split_to_table",
|
|
1584
|
+
"repeat",
|
|
1585
|
+
"replace",
|
|
1586
|
+
"reverse",
|
|
1587
|
+
"right",
|
|
1588
|
+
"round",
|
|
1589
|
+
"row_number",
|
|
1590
|
+
"rpad",
|
|
1591
|
+
"rtrim",
|
|
1592
|
+
"sign",
|
|
1593
|
+
"split_part",
|
|
1594
|
+
"sqrt",
|
|
1595
|
+
"starts_with",
|
|
1596
|
+
"string_agg",
|
|
1597
|
+
"string_to_array",
|
|
1598
|
+
"strpos",
|
|
1599
|
+
"substr",
|
|
1600
|
+
"substring",
|
|
1601
|
+
"sum",
|
|
1602
|
+
"timezone",
|
|
1603
|
+
"to_ascii",
|
|
1604
|
+
"to_char",
|
|
1605
|
+
"to_date",
|
|
1606
|
+
"to_hex",
|
|
1607
|
+
"to_json",
|
|
1608
|
+
"to_jsonb",
|
|
1609
|
+
"to_number",
|
|
1610
|
+
"to_timestamp",
|
|
1611
|
+
"translate",
|
|
1612
|
+
"trunc",
|
|
1613
|
+
"unnest",
|
|
1614
|
+
"upper",
|
|
1615
|
+
"width_bucket"
|
|
1616
|
+
];
|
|
970
1617
|
var canonicalActionSchema = zod.z.object({
|
|
971
|
-
name:
|
|
1618
|
+
name: identifier2,
|
|
972
1619
|
requires: zod.z.string().max(1e3).optional(),
|
|
973
1620
|
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
974
1621
|
type: columnTypeSchema,
|
|
@@ -980,9 +1627,9 @@ var canonicalActionSchema = zod.z.object({
|
|
|
980
1627
|
});
|
|
981
1628
|
var canonicalCowMutationSchema = zod.z.object({
|
|
982
1629
|
kind: zod.z.enum(["override", "revert"]),
|
|
983
|
-
name:
|
|
1630
|
+
name: identifier2,
|
|
984
1631
|
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
985
|
-
synced:
|
|
1632
|
+
synced: identifier2,
|
|
986
1633
|
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
987
1634
|
pk: zod.z.string().min(1).max(63),
|
|
988
1635
|
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
@@ -1010,23 +1657,27 @@ var canonicalCowMutationSchema = zod.z.object({
|
|
|
1010
1657
|
}
|
|
1011
1658
|
});
|
|
1012
1659
|
var canonicalForeignKeySchema = zod.z.object({
|
|
1013
|
-
name:
|
|
1660
|
+
name: identifier2,
|
|
1014
1661
|
columns: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1015
|
-
target:
|
|
1662
|
+
target: identifier2,
|
|
1016
1663
|
references: zod.z.array(columnIdentifier).min(1).max(8),
|
|
1017
|
-
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional()
|
|
1664
|
+
deferrable: zod.z.enum(["initially-immediate", "initially-deferred"]).optional(),
|
|
1665
|
+
onDelete: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional(),
|
|
1666
|
+
onUpdate: zod.z.enum(["cascade", "restrict", "no action", "set null", "set default"]).optional()
|
|
1018
1667
|
}).strict();
|
|
1019
1668
|
var canonicalTableSchema = zod.z.object({
|
|
1020
|
-
table:
|
|
1669
|
+
table: identifier2,
|
|
1021
1670
|
description: zod.z.string().max(2e3).optional(),
|
|
1022
1671
|
/** Table-level rename hint (new in v2). */
|
|
1023
|
-
renamedFrom:
|
|
1672
|
+
renamedFrom: identifier2.optional(),
|
|
1024
1673
|
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
1025
1674
|
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
1026
1675
|
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
1027
1676
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1028
1677
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1029
1678
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
1679
|
+
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1680
|
+
checks: zod.z.array(canonicalCheckSchema).max(100).optional(),
|
|
1030
1681
|
policies: canonicalPoliciesSchema.default({}),
|
|
1031
1682
|
/**
|
|
1032
1683
|
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
@@ -1037,22 +1688,55 @@ var canonicalTableSchema = zod.z.object({
|
|
|
1037
1688
|
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1038
1689
|
cowMutations: zod.z.array(canonicalCowMutationSchema).optional(),
|
|
1039
1690
|
/** Omitted by old definitions, preserving their existing content hashes. */
|
|
1040
|
-
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional()
|
|
1691
|
+
foreignKeys: zod.z.array(canonicalForeignKeySchema).max(100).optional(),
|
|
1692
|
+
/**
|
|
1693
|
+
* Per-cell edit metadata (`cellMetadata: true` in authoring): every tracked
|
|
1694
|
+
* column `c` carries typed `c__at` / `c__by` system columns (plus `c__rev`
|
|
1695
|
+
* on a resolved table's internal sources), listed explicitly in `columns`
|
|
1696
|
+
* with a `cell` marker so DDL and migrations treat them as ordinary columns.
|
|
1697
|
+
* Omitted when off so existing definitions hash unchanged.
|
|
1698
|
+
*/
|
|
1699
|
+
cellMetadata: zod.z.literal(true).optional(),
|
|
1700
|
+
/**
|
|
1701
|
+
* System-owned backing table of a resolved table (its manual or prefetch
|
|
1702
|
+
* source). Internal tables get DDL and IR metadata but no GraphQL surface;
|
|
1703
|
+
* only the data plane writes them. Omitted when false.
|
|
1704
|
+
*/
|
|
1705
|
+
internal: zod.z.literal(true).optional(),
|
|
1706
|
+
/**
|
|
1707
|
+
* The materialized copy of resolved table `materializes` (internal): the
|
|
1708
|
+
* same columns as that view, including its cell parts, maintained by the
|
|
1709
|
+
* data plane. Omitted otherwise.
|
|
1710
|
+
*/
|
|
1711
|
+
materializes: identifier2.optional()
|
|
1041
1712
|
});
|
|
1713
|
+
var MAX_VIEW_QUERY = 1e5;
|
|
1714
|
+
var MAX_RESOLVED_VIEW_QUERY = 5e6;
|
|
1042
1715
|
var canonicalViewSchema = zod.z.object({
|
|
1043
|
-
view:
|
|
1716
|
+
view: identifier2,
|
|
1044
1717
|
description: zod.z.string().max(2e3).optional(),
|
|
1045
1718
|
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
1046
1719
|
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1047
1720
|
primaryKey: zod.z.array(columnIdentifier).min(1).max(8).optional(),
|
|
1048
1721
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1049
1722
|
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
1050
|
-
query: zod.z.string().min(1).max(
|
|
1723
|
+
query: zod.z.string().min(1).max(MAX_RESOLVED_VIEW_QUERY).refine(isSingleViewQuerySql, {
|
|
1051
1724
|
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1052
1725
|
}),
|
|
1053
1726
|
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1054
|
-
dependencies: zod.z.array(
|
|
1727
|
+
dependencies: zod.z.array(identifier2).max(500),
|
|
1728
|
+
/**
|
|
1729
|
+
* Present when this view is a resolved table (defineResolvedTable): the
|
|
1730
|
+
* multi-source metadata behind its generated query. Omitted otherwise so
|
|
1731
|
+
* existing views hash unchanged.
|
|
1732
|
+
*/
|
|
1733
|
+
resolved: canonicalResolvedSchema.optional(),
|
|
1734
|
+
/** System-owned view (a materialized resolved table's compute view): no GraphQL surface. */
|
|
1735
|
+
internal: zod.z.literal(true).optional()
|
|
1055
1736
|
}).superRefine((view, ctx) => {
|
|
1737
|
+
if (view.resolved === void 0 && view.query.length > MAX_VIEW_QUERY) {
|
|
1738
|
+
ctx.addIssue({ code: "custom", path: ["query"], message: `view query must be at most ${MAX_VIEW_QUERY} characters` });
|
|
1739
|
+
}
|
|
1056
1740
|
for (const [index, column] of view.columns.entries()) {
|
|
1057
1741
|
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1058
1742
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
@@ -1067,15 +1751,82 @@ var canonicalViewSchema = zod.z.object({
|
|
|
1067
1751
|
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1068
1752
|
}
|
|
1069
1753
|
}
|
|
1754
|
+
if (view.resolved === void 0 && view.internal !== true) {
|
|
1755
|
+
for (const [index, column] of view.columns.entries()) {
|
|
1756
|
+
if (column.cell) ctx.addIssue({ code: "custom", path: ["columns", index], message: `only resolved tables carry cell columns ('${column.name}')` });
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1070
1759
|
for (const [index, link] of view.links.entries()) {
|
|
1071
1760
|
if (link.enforce) {
|
|
1072
1761
|
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1073
1762
|
}
|
|
1763
|
+
if (link.onDelete !== void 0 || link.onUpdate !== void 0) {
|
|
1764
|
+
ctx.addIssue({ code: "custom", path: ["links", index], message: `view link '${link.name}' cannot declare referential actions` });
|
|
1765
|
+
}
|
|
1074
1766
|
}
|
|
1075
1767
|
});
|
|
1768
|
+
var CELL_PART_TYPES = {
|
|
1769
|
+
at: "timestamptz",
|
|
1770
|
+
by: "text",
|
|
1771
|
+
rev: "uuid",
|
|
1772
|
+
source: "text",
|
|
1773
|
+
conflicts: "jsonb"
|
|
1774
|
+
};
|
|
1775
|
+
function trackedCellColumns(table) {
|
|
1776
|
+
const pk = new Set(table.primaryKey ?? []);
|
|
1777
|
+
return table.columns.filter((column) => column.cell === void 0 && !pk.has(column.columnName) && column.onUpdateFn === void 0);
|
|
1778
|
+
}
|
|
1779
|
+
var SUFFIXES = Object.values(CELL_PART_SUFFIX);
|
|
1780
|
+
function validateCellMetadata(table, index, ctx) {
|
|
1781
|
+
const fail = (message, path = ["tables", index]) => ctx.addIssue({ code: "custom", path, message });
|
|
1782
|
+
const cells = table.columns.flatMap((column, position) => column.cell ? [{ column, position, cell: column.cell }] : []);
|
|
1783
|
+
if (table.materializes !== void 0) return;
|
|
1784
|
+
if (table.cellMetadata !== true) {
|
|
1785
|
+
for (const { position } of cells) fail(`cell-metadata columns require cellMetadata on table '${table.table}'`, ["tables", index, "columns", position]);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (table.source !== void 0) {
|
|
1789
|
+
fail(`synced table '${table.table}' cannot track cell metadata`);
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const tracked = trackedCellColumns(table);
|
|
1793
|
+
const byName = new Map(tracked.map((column) => [column.name, column]));
|
|
1794
|
+
const withRev = cells.some(({ cell }) => cell.part === "rev");
|
|
1795
|
+
if (withRev && table.internal !== true) fail(`only a resolved table's internal sources carry '__rev' cell columns ('${table.table}')`);
|
|
1796
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1797
|
+
for (const { column, position, cell } of cells) {
|
|
1798
|
+
const path = ["tables", index, "columns", position];
|
|
1799
|
+
const owner = byName.get(cell.of);
|
|
1800
|
+
if (!owner) {
|
|
1801
|
+
fail(`cell column '${column.name}' belongs to '${cell.of}', which is not a tracked column`, path);
|
|
1802
|
+
continue;
|
|
1803
|
+
}
|
|
1804
|
+
if (cell.part !== "at" && cell.part !== "by" && cell.part !== "rev") fail(`a table cell column must be part at/by/rev, not '${cell.part}'`, path);
|
|
1805
|
+
if (column.name !== cellColumnName(owner.name, cell.part) || column.columnName !== cellColumnName(owner.columnName, cell.part)) {
|
|
1806
|
+
fail(`cell column for '${owner.name}' ${cell.part} must be named '${cellColumnName(owner.name, cell.part)}' / '${cellColumnName(owner.columnName, cell.part)}'`, path);
|
|
1807
|
+
}
|
|
1808
|
+
if (column.type !== CELL_PART_TYPES[cell.part] || !column.nullable || column.unique || column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1809
|
+
fail(`cell column '${column.name}' must be a nullable, default-less ${CELL_PART_TYPES[cell.part]}`, path);
|
|
1810
|
+
}
|
|
1811
|
+
const key = `${cell.of}:${cell.part}`;
|
|
1812
|
+
if (seen.has(key)) fail(`duplicate cell column '${column.name}'`, path);
|
|
1813
|
+
seen.add(key);
|
|
1814
|
+
}
|
|
1815
|
+
for (const column of tracked) {
|
|
1816
|
+
if (column.columnName.length > MAX_TRACKED_COLUMN_NAME) {
|
|
1817
|
+
fail(`column '${column.columnName}' is too long to track cell metadata (max ${MAX_TRACKED_COLUMN_NAME} characters)`);
|
|
1818
|
+
}
|
|
1819
|
+
if (SUFFIXES.some((suffix) => column.columnName.endsWith(suffix))) {
|
|
1820
|
+
fail(`column '${column.columnName}' ends in a reserved cell-metadata suffix (${SUFFIXES.join(", ")})`);
|
|
1821
|
+
}
|
|
1822
|
+
for (const part of withRev ? ["at", "by", "rev"] : ["at", "by"]) {
|
|
1823
|
+
if (!seen.has(`${column.name}:${part}`)) fail(`tracked column '${column.name}' is missing its '${cellColumnName(column.columnName, part)}' cell column`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1076
1827
|
var canonicalDefinitionSchema = zod.z.object({
|
|
1077
1828
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
1078
|
-
namespace:
|
|
1829
|
+
namespace: identifier2.max(41),
|
|
1079
1830
|
description: zod.z.string().max(2e3).optional(),
|
|
1080
1831
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
1081
1832
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
@@ -1093,6 +1844,7 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1093
1844
|
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
1094
1845
|
}
|
|
1095
1846
|
seen.add(table.table);
|
|
1847
|
+
validateCellMetadata(table, index, ctx);
|
|
1096
1848
|
if (table.source !== void 0) {
|
|
1097
1849
|
for (const [columnIndex, column] of table.columns.entries()) {
|
|
1098
1850
|
if (column.onUpdateFn !== void 0) {
|
|
@@ -1112,6 +1864,26 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1112
1864
|
...table.columns.filter((column) => column.unique && !(table.primaryKey?.length === 1 && table.primaryKey[0] === column.columnName)).map((column) => `${table.table}_${column.columnName}_unique`),
|
|
1113
1865
|
...table.links.filter((link) => link.enforce).map((link) => `fk_${table.table}_${link.name}`)
|
|
1114
1866
|
]);
|
|
1867
|
+
for (const [index, link] of table.links.entries()) {
|
|
1868
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && (!link.enforce || link.kind !== "toOne")) {
|
|
1869
|
+
ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "referential actions require an enforced toOne link" });
|
|
1870
|
+
}
|
|
1871
|
+
if (link.onDelete === "set null" || link.onUpdate === "set null") {
|
|
1872
|
+
const source = table.columns.find((column) => column.columnName === link.on);
|
|
1873
|
+
if (source?.nullable === false || table.primaryKey?.includes(link.on)) ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "links", index], message: "SET NULL requires a nullable source column" });
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
for (const [index, check] of (table.checks ?? []).entries()) {
|
|
1877
|
+
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "checks", index], message });
|
|
1878
|
+
if (table.source) fail("synced tables cannot declare checks");
|
|
1879
|
+
if (names.has(check.name)) fail(`duplicate constraint '${check.name}'`);
|
|
1880
|
+
names.add(check.name);
|
|
1881
|
+
try {
|
|
1882
|
+
compileCheckExpression(check.expression, table.columns);
|
|
1883
|
+
} catch (error) {
|
|
1884
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1115
1887
|
for (const [index, fk] of (table.foreignKeys ?? []).entries()) {
|
|
1116
1888
|
const fail = (message) => ctx.addIssue({ code: "custom", path: ["tables", tableIndex, "foreignKeys", index], message });
|
|
1117
1889
|
if (names.has(fk.name)) fail(`duplicate foreign key '${fk.name}'`);
|
|
@@ -1129,10 +1901,18 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
1129
1901
|
if (!sourceColumn || !targetColumn) fail("foreign key references an unknown column");
|
|
1130
1902
|
else if (JSON.stringify(sourceColumn.type) !== JSON.stringify(targetColumn.type)) fail("foreign key column types must match");
|
|
1131
1903
|
}
|
|
1904
|
+
if (fk.onDelete === "set null" || fk.onUpdate === "set null") {
|
|
1905
|
+
if (fk.columns.some((name) => table.columns.find((column) => column.columnName === name)?.nullable === false || table.primaryKey?.includes(name))) fail("SET NULL requires nullable source columns");
|
|
1906
|
+
}
|
|
1132
1907
|
const uniqueKeys = [target.primaryKey ?? [], ...target.indexes.filter((index2) => index2.unique).map((index2) => index2.columns), ...target.columns.filter((column) => column.unique).map((column) => [column.columnName])];
|
|
1133
1908
|
if (!uniqueKeys.some((key) => key.length === fk.references.length && key.every((column) => fk.references.includes(column)))) fail("foreign key target columns must form a unique key");
|
|
1134
1909
|
}
|
|
1135
1910
|
}
|
|
1911
|
+
for (const [viewIndex, view] of (definition.views ?? []).entries()) {
|
|
1912
|
+
for (const message of validateResolvedView({ view, tables: definition.tables, views: definition.views ?? [] })) {
|
|
1913
|
+
ctx.addIssue({ code: "custom", path: ["views", viewIndex, "resolved"], message });
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1136
1916
|
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1137
1917
|
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1138
1918
|
const cowMutations = table.cowMutations ?? [];
|
|
@@ -1254,13 +2034,14 @@ function normalizeCanonical(definition) {
|
|
|
1254
2034
|
...definition,
|
|
1255
2035
|
enums: sortByName(definition.enums),
|
|
1256
2036
|
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1257
|
-
const { cowMutations, foreignKeys, ...rest } = t2;
|
|
2037
|
+
const { cowMutations, foreignKeys, checks, ...rest } = t2;
|
|
1258
2038
|
return {
|
|
1259
2039
|
...rest,
|
|
1260
2040
|
links: sortByName(t2.links),
|
|
1261
2041
|
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1262
2042
|
actions: sortByName(t2.actions),
|
|
1263
2043
|
...foreignKeys?.length ? { foreignKeys: sortByName(foreignKeys) } : {},
|
|
2044
|
+
...checks?.length ? { checks: sortByName(checks) } : {},
|
|
1264
2045
|
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1265
2046
|
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1266
2047
|
};
|
|
@@ -1275,6 +2056,454 @@ function normalizeCanonical(definition) {
|
|
|
1275
2056
|
}
|
|
1276
2057
|
return normalized;
|
|
1277
2058
|
}
|
|
2059
|
+
|
|
2060
|
+
// src/authoring/resolved-sources.ts
|
|
2061
|
+
function cloneColumn({ builder, property, notNull, primaryKey }) {
|
|
2062
|
+
const canonical = builder.toCanonicalColumn(property);
|
|
2063
|
+
let clone = new ColumnBuilder(canonical.type, canonical.columnName);
|
|
2064
|
+
if (primaryKey) clone = clone.primaryKey();
|
|
2065
|
+
if (notNull) clone = clone.notNull();
|
|
2066
|
+
if (canonical.description !== void 0) clone = clone.description(canonical.description);
|
|
2067
|
+
return clone;
|
|
2068
|
+
}
|
|
2069
|
+
function planSource(input) {
|
|
2070
|
+
const { sourceName, input: source, fail } = input;
|
|
2071
|
+
if (!IDENTIFIER_PATTERN.test(sourceName)) fail(`source name "${sourceName}" must match ${IDENTIFIER_PATTERN}`);
|
|
2072
|
+
switch (source.kind) {
|
|
2073
|
+
case "manual":
|
|
2074
|
+
return {
|
|
2075
|
+
name: sourceName,
|
|
2076
|
+
kind: "manual",
|
|
2077
|
+
columns: input.dataColumns,
|
|
2078
|
+
table: internalTable(input, input.dataColumns, {
|
|
2079
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned(),
|
|
2080
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2081
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.name]: t.text(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName),
|
|
2082
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackAt.columnName)
|
|
2083
|
+
}, { revisions: true })
|
|
2084
|
+
};
|
|
2085
|
+
case "etl":
|
|
2086
|
+
return planEtl(input, source);
|
|
2087
|
+
case "prefetch":
|
|
2088
|
+
return planPrefetch(input, source);
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
var tombstoned = () => t.boolean(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName).notNull().default(false);
|
|
2092
|
+
function internalTable(input, supplied, system, options = {}) {
|
|
2093
|
+
const { viewName, sourceName, columns, pk, pkBuilder } = input;
|
|
2094
|
+
const tableColumns = { [pk]: cloneColumn({ builder: pkBuilder, property: pk, notNull: true, primaryKey: true }) };
|
|
2095
|
+
for (const property of supplied) {
|
|
2096
|
+
tableColumns[property] = cloneColumn({ builder: columns[property], property, notNull: false, primaryKey: false });
|
|
2097
|
+
}
|
|
2098
|
+
return defineTable(`${viewName}__${sourceName}`, { ...tableColumns, ...system }, {
|
|
2099
|
+
description: `Internal ${input.input.kind} source "${sourceName}" of resolved table "${viewName}". System-maintained.`,
|
|
2100
|
+
cellMetadata: true,
|
|
2101
|
+
// The manual source identifies each edit (`c__rev`) so an async
|
|
2102
|
+
// confirmation clears only the exact edit it delivered.
|
|
2103
|
+
...options.revisions ? { cellRevisions: true } : {},
|
|
2104
|
+
internal: true
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
function assertColumns(input, list, what) {
|
|
2108
|
+
for (const column of list) if (!input.dataColumns.includes(column)) input.fail(`${what} names unknown column "${column}"`);
|
|
2109
|
+
}
|
|
2110
|
+
function planEtl(input, source) {
|
|
2111
|
+
const { sourceName, pk, dataColumns, fail } = input;
|
|
2112
|
+
const table = typeof source.table === "function" ? source.table() : source.table;
|
|
2113
|
+
if (!isSeqTable(table) || table.source === void 0) fail(`etl source "${sourceName}" must reference a defineSyncedTable`);
|
|
2114
|
+
const synced = table;
|
|
2115
|
+
if (!(pk in synced.columns)) fail(`etl source "${sourceName}" table "${synced.tableName}" has no "${pk}" column`);
|
|
2116
|
+
const supplied = source.columns ? [...source.columns] : dataColumns.filter((column) => column in synced.columns);
|
|
2117
|
+
assertColumns(input, supplied, `etl source "${sourceName}"`);
|
|
2118
|
+
if (source.updatedAt !== void 0 && !(source.updatedAt in synced.columns)) {
|
|
2119
|
+
fail(`etl source "${sourceName}" updatedAt "${source.updatedAt}" is not a column of "${synced.tableName}"`);
|
|
2120
|
+
}
|
|
2121
|
+
return { name: sourceName, kind: "etl", table: synced, columns: supplied, ...source.updatedAt ? { updatedAt: source.updatedAt } : {} };
|
|
2122
|
+
}
|
|
2123
|
+
function planPrefetch(input, source) {
|
|
2124
|
+
const { sourceName, pk, fail } = input;
|
|
2125
|
+
assertColumns(input, source.columns, `prefetch source "${sourceName}"`);
|
|
2126
|
+
if (source.key !== pk && !source.columns.includes(source.key)) {
|
|
2127
|
+
fail(`prefetch source "${sourceName}" key "${source.key}" must be the primary key or one of its columns`);
|
|
2128
|
+
}
|
|
2129
|
+
for (const column of source.writeback?.columns ?? []) {
|
|
2130
|
+
if (!source.columns.includes(column)) fail(`prefetch source "${sourceName}" writes back "${column}", which it does not supply`);
|
|
2131
|
+
}
|
|
2132
|
+
const writeback = source.writeback;
|
|
2133
|
+
return {
|
|
2134
|
+
name: sourceName,
|
|
2135
|
+
kind: "prefetch",
|
|
2136
|
+
columns: [...source.columns],
|
|
2137
|
+
key: source.key,
|
|
2138
|
+
function: source.function,
|
|
2139
|
+
fetch: source.fetch === "single" ? { shape: "single" } : { shape: "batch", maxKeys: source.fetch.batch.maxKeys },
|
|
2140
|
+
prefetchDefaults: {
|
|
2141
|
+
maxStalenessMs: parseDuration(source.prefetchDefaults?.maxStaleness ?? "30s"),
|
|
2142
|
+
timeoutMs: parseDuration(source.prefetchDefaults?.timeout ?? "1500ms"),
|
|
2143
|
+
onError: source.prefetchDefaults?.onError ?? "fallback"
|
|
2144
|
+
},
|
|
2145
|
+
...writeback ? {
|
|
2146
|
+
writeback: {
|
|
2147
|
+
columns: [...writeback.columns],
|
|
2148
|
+
defaultMode: writeback.defaultMode ?? "sync",
|
|
2149
|
+
timeoutMs: parseDuration(writeback.timeout ?? "5s"),
|
|
2150
|
+
clearManualOnConfirm: writeback.clearManualOnConfirm ?? true
|
|
2151
|
+
}
|
|
2152
|
+
} : {},
|
|
2153
|
+
table: internalTable(input, source.columns, {
|
|
2154
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.name]: t.timestamptz(RESOLVED_SOURCE_SYSTEM_COLUMNS.fetchedAt.columnName).notNull().default("now()"),
|
|
2155
|
+
[RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.name]: tombstoned()
|
|
2156
|
+
})
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
function parseDuration(input) {
|
|
2160
|
+
const value = parseDurationMs(input);
|
|
2161
|
+
if (value === void 0) throw new OrmDefinitionError(`invalid duration "${input}" \u2014 use e.g. '1500ms', '30s', '5m'`);
|
|
2162
|
+
return value;
|
|
2163
|
+
}
|
|
2164
|
+
var PG_TYPE = {
|
|
2165
|
+
uuid: "uuid",
|
|
2166
|
+
text: "text",
|
|
2167
|
+
integer: "integer",
|
|
2168
|
+
bigint: "bigint",
|
|
2169
|
+
numeric: "numeric",
|
|
2170
|
+
double: "double precision",
|
|
2171
|
+
boolean: "boolean",
|
|
2172
|
+
timestamptz: "timestamptz",
|
|
2173
|
+
date: "date",
|
|
2174
|
+
jsonb: "jsonb"
|
|
2175
|
+
};
|
|
2176
|
+
var ident = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
2177
|
+
var literal = (value) => `'${value.replace(/'/g, "''")}'`;
|
|
2178
|
+
var physical = ({ source, property }) => source.table.columns[property].columnNameFor(property);
|
|
2179
|
+
function candidateSql({ source, column, pk, pgType }) {
|
|
2180
|
+
const a = ident(`s_${source.name}`);
|
|
2181
|
+
const name = physical({ source, property: column });
|
|
2182
|
+
const value = `CAST(${a}.${ident(name)} AS ${pgType})`;
|
|
2183
|
+
if (source.kind === "etl") {
|
|
2184
|
+
return {
|
|
2185
|
+
source: source.name,
|
|
2186
|
+
value,
|
|
2187
|
+
has: `(${a}.${ident(physical({ source, property: pk }))} IS NOT NULL)`,
|
|
2188
|
+
at: source.updatedAt ? `CAST(${a}.${ident(physical({ source, property: source.updatedAt }))} AS timestamptz)` : "CAST(NULL AS timestamptz)",
|
|
2189
|
+
by: "CAST(NULL AS text)"
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
const at = `${a}.${ident(cellColumnName(name, "at"))}`;
|
|
2193
|
+
const tombstoned2 = source.kind === "prefetch" ? ` AND NOT ${a}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName)}` : "";
|
|
2194
|
+
return { source: source.name, value, has: `(${at} IS NOT NULL${tombstoned2})`, at, by: `${a}.${ident(cellColumnName(name, "by"))}` };
|
|
2195
|
+
}
|
|
2196
|
+
function outranks({ i, j, key, direction, inclusive }) {
|
|
2197
|
+
const ki = i[key];
|
|
2198
|
+
const kj = j[key];
|
|
2199
|
+
const op = inclusive ? `${direction}=` : direction;
|
|
2200
|
+
return inclusive ? `(${kj} IS NULL OR (${ki} IS NOT NULL AND ${ki} ${op} ${kj}))` : `(${ki} IS NOT NULL AND (${kj} IS NULL OR ${ki} ${op} ${kj}))`;
|
|
2201
|
+
}
|
|
2202
|
+
function rankedWinner({ candidates, key, direction }) {
|
|
2203
|
+
const branches = candidates.map((candidate, index) => {
|
|
2204
|
+
const beats = candidates.map((other, otherIndex) => otherIndex === index ? null : `(NOT ${other.has} OR ${outranks({ i: candidate, j: other, key, direction, inclusive: index < otherIndex })})`).filter((clause) => clause !== null);
|
|
2205
|
+
return `WHEN ${[candidate.has, ...beats].join(" AND ")} THEN ${index + 1}`;
|
|
2206
|
+
});
|
|
2207
|
+
return `CASE ${branches.join(" ")} END`;
|
|
2208
|
+
}
|
|
2209
|
+
function winnerIndex({ comparator, candidates }) {
|
|
2210
|
+
const firstWith = (test) => candidates.map((candidate, index) => `WHEN ${test(candidate)} THEN ${index + 1}`).join(" ");
|
|
2211
|
+
if (candidates.length === 1 || comparator === "priority") return `CASE ${firstWith((c) => c.has)} END`;
|
|
2212
|
+
switch (comparator) {
|
|
2213
|
+
case "latest":
|
|
2214
|
+
return rankedWinner({ candidates, key: "at", direction: ">" });
|
|
2215
|
+
case "highest":
|
|
2216
|
+
return rankedWinner({ candidates, key: "value", direction: ">" });
|
|
2217
|
+
case "lowest":
|
|
2218
|
+
return rankedWinner({ candidates, key: "value", direction: "<" });
|
|
2219
|
+
case "nonNullFirst":
|
|
2220
|
+
return `CASE ${firstWith((c) => `${c.has} AND ${c.value} IS NOT NULL`)} ${firstWith((c) => c.has)} END`;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
function pick(winner, candidates, field) {
|
|
2224
|
+
return `CASE ${winner} ${candidates.map((candidate, index) => `WHEN ${index + 1} THEN ${field(candidate)}`).join(" ")} END`;
|
|
2225
|
+
}
|
|
2226
|
+
function loserSql({ loser, winner }) {
|
|
2227
|
+
return `CASE WHEN ${loser.has} AND ${loser.value} IS DISTINCT FROM ${winner.value} AND ${loser.at} > ${winner.at} THEN jsonb_build_array(jsonb_build_object('source', ${literal(loser.source)}, 'value', to_jsonb(${loser.value}), 'at', ${loser.at})) ELSE '[]'::jsonb END`;
|
|
2228
|
+
}
|
|
2229
|
+
function losersAgainst({ candidates, winner, isWinner }) {
|
|
2230
|
+
const losers = candidates.flatMap((loser, index) => {
|
|
2231
|
+
const excluded = isWinner(loser, index);
|
|
2232
|
+
if (excluded === true) return [];
|
|
2233
|
+
const clause = loserSql({ loser, winner });
|
|
2234
|
+
return [excluded === false ? clause : `CASE WHEN ${excluded} THEN '[]'::jsonb ELSE ${clause} END`];
|
|
2235
|
+
});
|
|
2236
|
+
return losers.length === 0 ? "CAST(NULL AS jsonb)" : `NULLIF(${losers.join(" || ")}, '[]'::jsonb)`;
|
|
2237
|
+
}
|
|
2238
|
+
function builtinCell({ comparator, candidates }) {
|
|
2239
|
+
const winner = winnerIndex({ comparator, candidates });
|
|
2240
|
+
const conflicts = comparator === "latest" || candidates.length < 2 ? "CAST(NULL AS jsonb)" : `CASE ${winner} ${candidates.map((known, index) => `WHEN ${index + 1} THEN ${losersAgainst({ candidates, winner: known, isWinner: (_c, other) => other === index })}`).join(" ")} END`;
|
|
2241
|
+
return {
|
|
2242
|
+
value: pick(winner, candidates, (c) => c.value),
|
|
2243
|
+
parts: {
|
|
2244
|
+
source: pick(winner, candidates, (c) => literal(c.source)),
|
|
2245
|
+
at: pick(winner, candidates, (c) => c.at),
|
|
2246
|
+
by: pick(winner, candidates, (c) => c.by),
|
|
2247
|
+
conflicts
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
function customCell({ candidates, orderBy, index }) {
|
|
2252
|
+
const w = ident(`w_${index}`);
|
|
2253
|
+
const rows = candidates.map((c, ord) => `(${literal(c.source)}, ${c.has}, ${c.value}, ${c.at}, ${c.by}, ${ord})`);
|
|
2254
|
+
const lateral = ` LEFT JOIN LATERAL (SELECT max(winner.source) AS source, (array_agg(winner.value))[1] AS value, max(winner.updated_at) AS updated_at, max(winner.actor) AS actor FROM (WITH cand(source, has, value, updated_at, actor, ord) AS (VALUES ${rows.join(", ")}) SELECT c.source, c.value, c.updated_at, c.actor FROM cand c WHERE c.has ORDER BY ${orderBy}, c.ord LIMIT 1) AS winner) AS ${w} ON true`;
|
|
2255
|
+
const winner = { value: `${w}.value`, at: `${w}.updated_at` };
|
|
2256
|
+
return {
|
|
2257
|
+
value: winner.value,
|
|
2258
|
+
lateral,
|
|
2259
|
+
parts: {
|
|
2260
|
+
source: `${w}.source`,
|
|
2261
|
+
at: winner.at,
|
|
2262
|
+
by: `${w}.actor`,
|
|
2263
|
+
conflicts: candidates.length < 2 ? "CAST(NULL AS jsonb)" : losersAgainst({ candidates, winner, isWinner: (c) => `${w}.source = ${literal(c.source)}` })
|
|
2264
|
+
}
|
|
2265
|
+
};
|
|
2266
|
+
}
|
|
2267
|
+
function buildResolvedQuery({ context, plan }) {
|
|
2268
|
+
const { qb, relation } = context;
|
|
2269
|
+
const pkColumn = plan.columns[plan.pk].columnNameFor(plan.pk);
|
|
2270
|
+
const alias = (source) => ident(`s_${source}`);
|
|
2271
|
+
const tombstoned2 = RESOLVED_SOURCE_SYSTEM_COLUMNS.tombstoned.columnName;
|
|
2272
|
+
const sourceByName = new Map(plan.plans.map((source) => [source.name, source]));
|
|
2273
|
+
const schema = pgCore.getTableConfig(relation(plan.plans[0].table)).schema;
|
|
2274
|
+
const keyBranches = plan.plans.map((source) => {
|
|
2275
|
+
const where = source.kind === "prefetch" ? drizzleOrm.sql.raw(` WHERE NOT ${ident(tombstoned2)}`) : drizzleOrm.sql.raw("");
|
|
2276
|
+
const sourcePk = ident(physical({ source, property: plan.pk }));
|
|
2277
|
+
const selectedPk = sourcePk === ident(pkColumn) ? sourcePk : `${sourcePk} AS ${ident(pkColumn)}`;
|
|
2278
|
+
return drizzleOrm.sql`SELECT ${drizzleOrm.sql.raw(selectedPk)} FROM ${relation(source.table)}${where}`;
|
|
2279
|
+
});
|
|
2280
|
+
const joins = plan.plans.map(
|
|
2281
|
+
(source) => drizzleOrm.sql` LEFT JOIN ${relation(source.table)} AS ${drizzleOrm.sql.raw(alias(source.name))} ON ${drizzleOrm.sql.raw(`${alias(source.name)}.${ident(physical({ source, property: plan.pk }))} = "k".${ident(pkColumn)}`)}`
|
|
2282
|
+
);
|
|
2283
|
+
const cells = /* @__PURE__ */ new Map();
|
|
2284
|
+
plan.rules.forEach((rule, index) => {
|
|
2285
|
+
const canonical = plan.columns[rule.column].toCanonicalColumn(rule.column);
|
|
2286
|
+
const pgType = typeof canonical.type === "string" ? PG_TYPE[canonical.type] : `${schema === void 0 ? "" : `${ident(schema)}.`}${ident(canonical.type.enum)}`;
|
|
2287
|
+
const candidates = rule.candidates.map((name) => candidateSql({ source: sourceByName.get(name), column: rule.column, pk: plan.pk, pgType }));
|
|
2288
|
+
cells.set(rule.column, rule.comparator === "custom" ? customCell({ candidates, orderBy: rule.orderBy, index }) : builtinCell({ comparator: rule.comparator, candidates }));
|
|
2289
|
+
});
|
|
2290
|
+
const laterals = [...cells.values()].flatMap((cell) => cell.lateral ? [drizzleOrm.sql.raw(cell.lateral)] : []);
|
|
2291
|
+
const from = drizzleOrm.sql.join([drizzleOrm.sql`(${drizzleOrm.sql.join(keyBranches, drizzleOrm.sql.raw(" UNION "))}) AS "k"`, ...joins, ...laterals], drizzleOrm.sql.raw(""));
|
|
2292
|
+
const selected = {
|
|
2293
|
+
[plan.pk]: drizzleOrm.sql.raw(`"k".${ident(pkColumn)}`).as(pkColumn)
|
|
2294
|
+
};
|
|
2295
|
+
for (const [property, builder] of Object.entries(plan.columns)) {
|
|
2296
|
+
if (property === plan.pk) continue;
|
|
2297
|
+
const cell = cells.get(property);
|
|
2298
|
+
const columnName = builder.columnNameFor(property);
|
|
2299
|
+
selected[property] = drizzleOrm.sql.raw(cell.value).as(columnName);
|
|
2300
|
+
for (const [part, expression] of Object.entries(cell.parts)) {
|
|
2301
|
+
selected[cellColumnName(property, part)] = drizzleOrm.sql.raw(expression).as(cellColumnName(columnName, part));
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
const manual = plan.plans.find((source) => source.kind === "manual");
|
|
2305
|
+
const system = RESOLVED_VIEW_SYSTEM_COLUMNS;
|
|
2306
|
+
selected[system.writebackStatus.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackStatus.columnName)}`).as(system.writebackStatus.columnName);
|
|
2307
|
+
selected[system.writebackError.name] = drizzleOrm.sql.raw(`${alias(manual.name)}.${ident(RESOLVED_SOURCE_SYSTEM_COLUMNS.writebackError.columnName)}`).as(system.writebackError.columnName);
|
|
2308
|
+
const hidden = plan.deletedBy.map((name) => `NOT COALESCE(${alias(name)}.${ident(tombstoned2)}, false)`);
|
|
2309
|
+
const query = qb.select(selected).from(from);
|
|
2310
|
+
return hidden.length > 0 ? query.where(drizzleOrm.sql.raw(hidden.join(" AND "))) : query;
|
|
2311
|
+
}
|
|
2312
|
+
function managedFn(id) {
|
|
2313
|
+
return { kind: "managed", id };
|
|
2314
|
+
}
|
|
2315
|
+
function manualSource() {
|
|
2316
|
+
return { kind: "manual" };
|
|
2317
|
+
}
|
|
2318
|
+
function etlSource(input) {
|
|
2319
|
+
return { kind: "etl", ...input };
|
|
2320
|
+
}
|
|
2321
|
+
function prefetchSource(input) {
|
|
2322
|
+
return { kind: "prefetch", ...input };
|
|
2323
|
+
}
|
|
2324
|
+
var latest = (candidates) => ({ kind: "latest", candidates });
|
|
2325
|
+
var highest = (candidates) => ({ kind: "highest", candidates });
|
|
2326
|
+
var lowest = (candidates) => ({ kind: "lowest", candidates });
|
|
2327
|
+
var priority = (candidates) => ({ kind: "priority", candidates });
|
|
2328
|
+
var nonNullFirst = (candidates) => ({ kind: "nonNullFirst", candidates });
|
|
2329
|
+
var custom = (candidates, orderBy) => ({ kind: "custom", candidates, orderBy });
|
|
2330
|
+
var SYSTEM_VIEW_COLUMNS = {
|
|
2331
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackStatus.columnName),
|
|
2332
|
+
[RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.name]: () => t.text(RESOLVED_VIEW_SYSTEM_COLUMNS.writebackError.columnName)
|
|
2333
|
+
};
|
|
2334
|
+
function defineResolvedTable(name, columns, config) {
|
|
2335
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
2336
|
+
const fail = (message) => {
|
|
2337
|
+
throw new OrmDefinitionError(`resolved table "${name}": ${message}`, file);
|
|
2338
|
+
};
|
|
2339
|
+
if (!IDENTIFIER_PATTERN.test(name)) fail(`name must match ${IDENTIFIER_PATTERN}`);
|
|
2340
|
+
for (const property of Object.keys(columns)) {
|
|
2341
|
+
if (RESERVED_RESOLVED_COLUMN_NAMES.has(property) || RESERVED_RESOLVED_COLUMN_NAMES.has(columns[property].columnNameFor(property))) {
|
|
2342
|
+
fail(`column "${property}" collides with a reserved system column`);
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
for (const property of Object.keys(columns)) {
|
|
2346
|
+
if (columns[property].columnNameFor(property).length > MAX_TRACKED_COLUMN_NAME) {
|
|
2347
|
+
fail(`column "${property}" is too long for a resolved table (max ${MAX_TRACKED_COLUMN_NAME} characters, room for cell parts)`);
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const pkEntries = Object.entries(columns).filter(([, builder]) => builder.isPrimaryKey);
|
|
2351
|
+
if (pkEntries.length !== 1) fail("declare exactly one .primaryKey() column");
|
|
2352
|
+
const [pk, pkBuilder] = pkEntries[0];
|
|
2353
|
+
const dataColumns = Object.keys(columns).filter((property) => property !== pk);
|
|
2354
|
+
if (dataColumns.length === 0) fail("declare at least one non-key column");
|
|
2355
|
+
const sourceEntries = Object.entries(config.sources);
|
|
2356
|
+
if (sourceEntries.filter(([, source]) => source.kind === "manual").length !== 1) fail("declare exactly one manualSource()");
|
|
2357
|
+
const plans = sourceEntries.map(
|
|
2358
|
+
([sourceName, input]) => planSource({ viewName: name, sourceName, input, columns, pk, pkBuilder, dataColumns, fail })
|
|
2359
|
+
);
|
|
2360
|
+
const rules = planRules({ config, plans, dataColumns, fail });
|
|
2361
|
+
const deletedBy = [...config.rows?.deletedBy ?? []];
|
|
2362
|
+
for (const source of deletedBy) {
|
|
2363
|
+
if (plans.find((plan) => plan.name === source)?.kind !== "manual") fail(`rows.deletedBy "${source}" must be the manual source`);
|
|
2364
|
+
}
|
|
2365
|
+
const partType = { source: t.text, at: t.timestamptz, by: t.text, conflicts: t.jsonb };
|
|
2366
|
+
const viewColumns = {};
|
|
2367
|
+
for (const [property, builder] of Object.entries(columns)) {
|
|
2368
|
+
viewColumns[property] = cloneColumn({ builder, property, notNull: property === pk, primaryKey: property === pk });
|
|
2369
|
+
if (property === pk) continue;
|
|
2370
|
+
const columnName = builder.columnNameFor(property);
|
|
2371
|
+
for (const part of RESOLVED_VIEW_CELL_PARTS) {
|
|
2372
|
+
viewColumns[cellColumnName(property, part)] = partType[part](cellColumnName(columnName, part)).cellPart(property, part);
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
for (const [property, make] of Object.entries(SYSTEM_VIEW_COLUMNS)) viewColumns[property] = make();
|
|
2376
|
+
const { read, ...writePolicies } = config.policies;
|
|
2377
|
+
const resolved = {
|
|
2378
|
+
internalTables: plans.filter((plan) => plan.kind !== "etl").map((plan) => plan.table),
|
|
2379
|
+
sources: plans,
|
|
2380
|
+
rules,
|
|
2381
|
+
deletedBy,
|
|
2382
|
+
policies: writePolicies
|
|
2383
|
+
};
|
|
2384
|
+
const queryPlan = { columns, pk, plans, rules, deletedBy };
|
|
2385
|
+
const typedColumns = viewColumns;
|
|
2386
|
+
const surface = {
|
|
2387
|
+
...config.description !== void 0 ? { description: config.description } : {},
|
|
2388
|
+
...config.links !== void 0 ? { links: config.links } : {},
|
|
2389
|
+
policies: { read }
|
|
2390
|
+
};
|
|
2391
|
+
if (!config.materialize) {
|
|
2392
|
+
return defineView(name, typedColumns, { ...surface, query: (context) => buildResolvedQuery({ context, plan: queryPlan }), resolved });
|
|
2393
|
+
}
|
|
2394
|
+
const materialize = config.materialize === true ? {} : config.materialize;
|
|
2395
|
+
const computeName = `${name}__compute`;
|
|
2396
|
+
const tableName = `${name}__materialized`;
|
|
2397
|
+
const compute = defineView(computeName, typedColumns, {
|
|
2398
|
+
policies: { read },
|
|
2399
|
+
query: (context) => buildResolvedQuery({ context, plan: queryPlan }),
|
|
2400
|
+
internal: true
|
|
2401
|
+
});
|
|
2402
|
+
const table = defineTable(tableName, viewColumns, {
|
|
2403
|
+
description: `Materialized rows of resolved table "${name}". System-maintained from "${computeName}".`,
|
|
2404
|
+
internal: true,
|
|
2405
|
+
materializes: name,
|
|
2406
|
+
...materialize.indexes !== void 0 ? { indexes: materialize.indexes } : {}
|
|
2407
|
+
});
|
|
2408
|
+
return defineView(name, typedColumns, {
|
|
2409
|
+
...surface,
|
|
2410
|
+
query: (context) => {
|
|
2411
|
+
const handle = context.relation(table);
|
|
2412
|
+
const selected = Object.fromEntries(Object.entries(viewColumns).map(([property, builder]) => [
|
|
2413
|
+
property,
|
|
2414
|
+
drizzleOrm.sql`${handle[property]}`.as(builder.columnNameFor(property))
|
|
2415
|
+
]));
|
|
2416
|
+
return context.qb.select(selected).from(context.relation(table));
|
|
2417
|
+
},
|
|
2418
|
+
resolved: {
|
|
2419
|
+
...resolved,
|
|
2420
|
+
internalTables: [...resolved.internalTables, table],
|
|
2421
|
+
internalViews: [compute],
|
|
2422
|
+
materialized: { table: tableName, compute: computeName }
|
|
2423
|
+
}
|
|
2424
|
+
});
|
|
2425
|
+
}
|
|
2426
|
+
function planRules({ config, plans, dataColumns, fail }) {
|
|
2427
|
+
const kindOrder = { manual: 0, prefetch: 1, etl: 2 };
|
|
2428
|
+
const fallback = config.default ?? priority([...plans].sort((a, b) => kindOrder[a.kind] - kindOrder[b.kind]).map((plan) => plan.name));
|
|
2429
|
+
for (const column of Object.keys(config.resolve ?? {})) {
|
|
2430
|
+
if (!dataColumns.includes(column)) fail(`resolve names unknown column "${column}"`);
|
|
2431
|
+
}
|
|
2432
|
+
return dataColumns.map((column) => {
|
|
2433
|
+
const explicit = config.resolve?.[column];
|
|
2434
|
+
const comparator = explicit ?? fallback;
|
|
2435
|
+
const candidates = explicit ? [...explicit.candidates] : comparator.candidates.filter((name) => plans.find((plan) => plan.name === name)?.columns.includes(column));
|
|
2436
|
+
for (const candidate of candidates) {
|
|
2437
|
+
const plan = plans.find((entry) => entry.name === candidate);
|
|
2438
|
+
if (!plan) fail(`column "${column}" names unknown source "${candidate}"`);
|
|
2439
|
+
else if (!plan.columns.includes(column)) fail(`column "${column}" candidate "${candidate}" does not supply it`);
|
|
2440
|
+
}
|
|
2441
|
+
if (candidates.length === 0) fail(`column "${column}" has no candidate source`);
|
|
2442
|
+
if (comparator.kind === "custom") assertSafeOrderBy({ orderBy: comparator.orderBy ?? "", column, fail });
|
|
2443
|
+
return {
|
|
2444
|
+
column,
|
|
2445
|
+
comparator: comparator.kind,
|
|
2446
|
+
candidates,
|
|
2447
|
+
...comparator.kind === "custom" && comparator.orderBy !== void 0 ? { orderBy: comparator.orderBy } : {}
|
|
2448
|
+
};
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
var ORDER_BY_KEYWORDS = /* @__PURE__ */ new Set([
|
|
2452
|
+
"value",
|
|
2453
|
+
"updated_at",
|
|
2454
|
+
"source",
|
|
2455
|
+
"actor",
|
|
2456
|
+
"ord",
|
|
2457
|
+
"asc",
|
|
2458
|
+
"desc",
|
|
2459
|
+
"nulls",
|
|
2460
|
+
"first",
|
|
2461
|
+
"last",
|
|
2462
|
+
"case",
|
|
2463
|
+
"when",
|
|
2464
|
+
"then",
|
|
2465
|
+
"else",
|
|
2466
|
+
"end",
|
|
2467
|
+
"and",
|
|
2468
|
+
"or",
|
|
2469
|
+
"not",
|
|
2470
|
+
"is",
|
|
2471
|
+
"null",
|
|
2472
|
+
"true",
|
|
2473
|
+
"false",
|
|
2474
|
+
"in",
|
|
2475
|
+
"distinct",
|
|
2476
|
+
"from",
|
|
2477
|
+
"coalesce",
|
|
2478
|
+
"nullif",
|
|
2479
|
+
"greatest",
|
|
2480
|
+
"least",
|
|
2481
|
+
"text",
|
|
2482
|
+
"numeric",
|
|
2483
|
+
"integer",
|
|
2484
|
+
"bigint",
|
|
2485
|
+
"boolean",
|
|
2486
|
+
"timestamptz",
|
|
2487
|
+
"date",
|
|
2488
|
+
"jsonb",
|
|
2489
|
+
"interval"
|
|
2490
|
+
]);
|
|
2491
|
+
var ALLOWED_FUNCTIONS = new Set(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
2492
|
+
var ORDER_BY_TOKEN = /\s+|'(?:[^']|'')*'|\d+(?:\.\d+)?|[a-z_][a-z0-9_]*|::|<=|>=|<>|!=|[-+*/%<>=(),|]/giy;
|
|
2493
|
+
function assertSafeOrderBy({ orderBy, column, fail }) {
|
|
2494
|
+
ORDER_BY_TOKEN.lastIndex = 0;
|
|
2495
|
+
let consumed = 0;
|
|
2496
|
+
for (let match = ORDER_BY_TOKEN.exec(orderBy); match; match = ORDER_BY_TOKEN.exec(orderBy)) {
|
|
2497
|
+
consumed = ORDER_BY_TOKEN.lastIndex;
|
|
2498
|
+
const token = match[0].toLowerCase();
|
|
2499
|
+
if (/^[a-z_]/.test(token) && !ORDER_BY_KEYWORDS.has(token) && !ALLOWED_FUNCTIONS.has(token)) {
|
|
2500
|
+
fail(`column "${column}" custom orderBy uses "${match[0]}" \u2014 only value, updated_at, source, actor, ord, SQL keywords and allowlisted functions`);
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (consumed !== orderBy.length || orderBy.trim() === "") {
|
|
2504
|
+
fail(`column "${column}" custom orderBy contains an unsupported character near "${orderBy.slice(consumed, consumed + 10)}"`);
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
1278
2507
|
function stableJson(value) {
|
|
1279
2508
|
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1280
2509
|
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -1303,8 +2532,19 @@ function defineNamespace(input) {
|
|
|
1303
2532
|
return config;
|
|
1304
2533
|
}
|
|
1305
2534
|
function serializeNamespace(input) {
|
|
1306
|
-
const { config
|
|
1307
|
-
const views = input.views ?? [];
|
|
2535
|
+
const { config } = input;
|
|
2536
|
+
const views = [...input.views ?? []];
|
|
2537
|
+
const tables = [...input.tables];
|
|
2538
|
+
for (const view of views) {
|
|
2539
|
+
for (const internal of view.config.resolved?.internalTables ?? []) {
|
|
2540
|
+
if (!tables.includes(internal)) tables.push(internal);
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
for (const view of [...views]) {
|
|
2544
|
+
for (const internal of view.config.resolved?.internalViews ?? []) {
|
|
2545
|
+
if (!views.includes(internal)) views.push(internal);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
1308
2548
|
const enums = input.enums ?? [];
|
|
1309
2549
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
1310
2550
|
for (const table of tables) {
|
|
@@ -1406,6 +2646,9 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1406
2646
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1407
2647
|
if (table.source !== void 0) candidate.source = table.source;
|
|
1408
2648
|
if (table.source !== void 0) {
|
|
2649
|
+
if (table.config.checks?.length) {
|
|
2650
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is system-owned; checks are not allowed`, table.file);
|
|
2651
|
+
}
|
|
1409
2652
|
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1410
2653
|
throw new OrmDefinitionError(
|
|
1411
2654
|
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
@@ -1443,6 +2686,11 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1443
2686
|
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1444
2687
|
candidate.columns?.push(column);
|
|
1445
2688
|
}
|
|
2689
|
+
if (table.config.internal === true) candidate.internal = true;
|
|
2690
|
+
if (table.config.materializes !== void 0) candidate.materializes = table.config.materializes;
|
|
2691
|
+
if (table.config.cellMetadata === true && table.source !== void 0) {
|
|
2692
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" cannot track cell metadata \u2014 its DDL is owned by the sync`, table.file);
|
|
2693
|
+
}
|
|
1446
2694
|
if (table.config.primaryKey !== void 0) {
|
|
1447
2695
|
if (columnLevelPk.length > 0) {
|
|
1448
2696
|
throw new OrmDefinitionError(
|
|
@@ -1456,6 +2704,24 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1456
2704
|
} else if (columnLevelPk.length > 0) {
|
|
1457
2705
|
candidate.primaryKey = columnLevelPk;
|
|
1458
2706
|
}
|
|
2707
|
+
if (table.config.cellMetadata === true) {
|
|
2708
|
+
candidate.cellMetadata = true;
|
|
2709
|
+
const parts = table.config.cellRevisions === true ? ["at", "by", "rev"] : ["at", "by"];
|
|
2710
|
+
const tracked = trackedCellColumns({ columns: candidate.columns ?? [], primaryKey: candidate.primaryKey });
|
|
2711
|
+
for (const column of tracked) {
|
|
2712
|
+
for (const part of parts) {
|
|
2713
|
+
candidate.columns?.push({
|
|
2714
|
+
name: cellColumnName(column.name, part),
|
|
2715
|
+
columnName: cellColumnName(column.columnName, part),
|
|
2716
|
+
type: CELL_PART_TYPES[part],
|
|
2717
|
+
nullable: true,
|
|
2718
|
+
unique: false,
|
|
2719
|
+
deprecated: false,
|
|
2720
|
+
cell: { of: column.name, part }
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
1459
2725
|
if (table.config.foreignKeys?.length) {
|
|
1460
2726
|
if (table.source) throw new OrmDefinitionError(`synced table "${table.tableName}" cannot enforce foreign keys`, table.file);
|
|
1461
2727
|
candidate.foreignKeys = table.config.foreignKeys.map((fk) => {
|
|
@@ -1468,10 +2734,15 @@ function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
|
1468
2734
|
columns: fk.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: `foreign key "${fk.name}" column` })),
|
|
1469
2735
|
target: target.tableName,
|
|
1470
2736
|
references: fk.references.map((ref) => resolveColumnRef({ relation: target, ref, role: `foreign key "${fk.name}" reference` })),
|
|
1471
|
-
...fk.deferrable ? { deferrable: fk.deferrable } : {}
|
|
2737
|
+
...fk.deferrable ? { deferrable: fk.deferrable } : {},
|
|
2738
|
+
...fk.onDelete ? { onDelete: fk.onDelete } : {},
|
|
2739
|
+
...fk.onUpdate ? { onUpdate: fk.onUpdate } : {}
|
|
1472
2740
|
};
|
|
1473
2741
|
});
|
|
1474
2742
|
}
|
|
2743
|
+
if (table.config.checks?.length) {
|
|
2744
|
+
candidate.checks = table.config.checks.map((check) => ({ name: check.name, expression: check.expression }));
|
|
2745
|
+
}
|
|
1475
2746
|
for (const index of table.config.indexes ?? []) {
|
|
1476
2747
|
const entry = {
|
|
1477
2748
|
columns: index.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
@@ -1567,7 +2838,33 @@ function buildCanonicalView({
|
|
|
1567
2838
|
columns,
|
|
1568
2839
|
...primaryKey !== void 0 ? { primaryKey } : {},
|
|
1569
2840
|
links,
|
|
1570
|
-
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
2841
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) },
|
|
2842
|
+
...view.config.resolved !== void 0 ? { resolved: buildCanonicalResolved(view.config.resolved) } : {},
|
|
2843
|
+
...view.config.internal === true ? { internal: true } : {}
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
function buildCanonicalResolved(resolved) {
|
|
2847
|
+
const policies = {};
|
|
2848
|
+
if (resolved.policies.write !== void 0) policies.write = toCanonicalPolicy(resolved.policies.write);
|
|
2849
|
+
if (resolved.policies.insert !== void 0) policies.insert = toCanonicalPolicy(resolved.policies.insert);
|
|
2850
|
+
if (resolved.policies.update !== void 0) policies.update = toCanonicalPolicy(resolved.policies.update);
|
|
2851
|
+
return {
|
|
2852
|
+
sources: resolved.sources.map((source) => ({
|
|
2853
|
+
name: source.name,
|
|
2854
|
+
kind: source.kind,
|
|
2855
|
+
table: source.table.tableName,
|
|
2856
|
+
columns: [...source.columns],
|
|
2857
|
+
...source.updatedAt !== void 0 ? { updatedAt: source.updatedAt } : {},
|
|
2858
|
+
...source.key !== void 0 ? { key: source.key } : {},
|
|
2859
|
+
...source.fetch !== void 0 ? { fetch: source.fetch } : {},
|
|
2860
|
+
...source.function !== void 0 ? { function: { ...source.function } } : {},
|
|
2861
|
+
...source.prefetchDefaults !== void 0 ? { prefetchDefaults: { ...source.prefetchDefaults } } : {},
|
|
2862
|
+
...source.writeback !== void 0 ? { writeback: { ...source.writeback, columns: [...source.writeback.columns] } } : {}
|
|
2863
|
+
})),
|
|
2864
|
+
resolve: resolved.rules.map((rule) => ({ ...rule, candidates: [...rule.candidates] })),
|
|
2865
|
+
deletedBy: [...resolved.deletedBy],
|
|
2866
|
+
...resolved.materialized ? { materialized: { ...resolved.materialized } } : {},
|
|
2867
|
+
policies
|
|
1571
2868
|
};
|
|
1572
2869
|
}
|
|
1573
2870
|
function buildCanonicalLink({
|
|
@@ -1584,6 +2881,9 @@ function buildCanonicalLink({
|
|
|
1584
2881
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
1585
2882
|
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1586
2883
|
}
|
|
2884
|
+
if ((link.onDelete !== void 0 || link.onUpdate !== void 0) && !link.enforce) {
|
|
2885
|
+
return fail("sets a referential action without enforce: true");
|
|
2886
|
+
}
|
|
1587
2887
|
if (link.linkKind === "toPlatformUser") {
|
|
1588
2888
|
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1589
2889
|
return {
|
|
@@ -1612,6 +2912,8 @@ function buildCanonicalLink({
|
|
|
1612
2912
|
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1613
2913
|
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1614
2914
|
enforce: link.enforce,
|
|
2915
|
+
...link.onDelete !== void 0 ? { onDelete: link.onDelete } : {},
|
|
2916
|
+
...link.onUpdate !== void 0 ? { onUpdate: link.onUpdate } : {},
|
|
1615
2917
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
1616
2918
|
};
|
|
1617
2919
|
}
|
|
@@ -1885,23 +3187,34 @@ exports.activeAuthoringContext = activeAuthoringContext;
|
|
|
1885
3187
|
exports.beginAuthoringContext = beginAuthoringContext;
|
|
1886
3188
|
exports.computeCanonicalHash = computeCanonicalHash;
|
|
1887
3189
|
exports.computeSyncedContractHash = computeSyncedContractHash;
|
|
3190
|
+
exports.custom = custom;
|
|
1888
3191
|
exports.defineEnum = defineEnum;
|
|
1889
3192
|
exports.defineNamespace = defineNamespace;
|
|
3193
|
+
exports.defineResolvedTable = defineResolvedTable;
|
|
1890
3194
|
exports.defineSyncedTable = defineSyncedTable;
|
|
1891
3195
|
exports.defineTable = defineTable;
|
|
1892
3196
|
exports.defineView = defineView;
|
|
1893
3197
|
exports.deriveColumnName = deriveColumnName;
|
|
1894
3198
|
exports.endAuthoringContext = endAuthoringContext;
|
|
3199
|
+
exports.etlSource = etlSource;
|
|
1895
3200
|
exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
|
|
3201
|
+
exports.highest = highest;
|
|
1896
3202
|
exports.isColumnBuilder = isColumnBuilder;
|
|
1897
3203
|
exports.isEnumRef = isEnumRef;
|
|
1898
3204
|
exports.isSeqLink = isSeqLink;
|
|
1899
3205
|
exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
|
|
1900
3206
|
exports.isSeqTable = isSeqTable;
|
|
1901
3207
|
exports.isSeqView = isSeqView;
|
|
3208
|
+
exports.latest = latest;
|
|
1902
3209
|
exports.loadNamespaceFromDir = loadNamespaceFromDir;
|
|
3210
|
+
exports.lowest = lowest;
|
|
3211
|
+
exports.managedFn = managedFn;
|
|
3212
|
+
exports.manualSource = manualSource;
|
|
3213
|
+
exports.nonNullFirst = nonNullFirst;
|
|
1903
3214
|
exports.overlayWinsUnion = overlayWinsUnion;
|
|
1904
3215
|
exports.overrideRowMutation = overrideRowMutation;
|
|
3216
|
+
exports.prefetchSource = prefetchSource;
|
|
3217
|
+
exports.priority = priority;
|
|
1905
3218
|
exports.revertOverlayMutation = revertOverlayMutation;
|
|
1906
3219
|
exports.serializeNamespace = serializeNamespace;
|
|
1907
3220
|
exports.syncedContractSurface = syncedContractSurface;
|