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