@sequenceholdings/orm 0.1.1 → 0.1.3
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 +12 -2
- package/dist/authoring.cjs +1801 -0
- package/dist/authoring.cjs.map +1 -0
- package/dist/authoring.d.cts +539 -0
- package/dist/authoring.d.ts +539 -0
- package/dist/authoring.js +1759 -0
- package/dist/authoring.js.map +1 -0
- package/dist/canonical.cjs +552 -0
- package/dist/canonical.cjs.map +1 -0
- package/dist/canonical.d.cts +603 -0
- package/dist/canonical.d.ts +603 -0
- package/dist/canonical.js +532 -0
- package/dist/canonical.js.map +1 -0
- package/dist/cli.cjs +7843 -1815
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +8 -1
- package/dist/cli.d.ts +8 -1
- package/dist/cli.js +7854 -1828
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs +120 -0
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +74 -2
- package/dist/client.d.ts +74 -2
- package/dist/client.js +116 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +3453 -0
- package/dist/compiler.cjs.map +1 -0
- package/dist/compiler.d.cts +389 -0
- package/dist/compiler.d.ts +389 -0
- package/dist/compiler.js +3396 -0
- package/dist/compiler.js.map +1 -0
- package/dist/drizzle.cjs +14 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.js +3 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/dynamic-input.cjs +391 -0
- package/dist/dynamic-input.cjs.map +1 -0
- package/dist/dynamic-input.d.cts +111 -0
- package/dist/dynamic-input.d.ts +111 -0
- package/dist/dynamic-input.js +380 -0
- package/dist/dynamic-input.js.map +1 -0
- package/dist/index.cjs +3727 -344
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +236 -59
- package/dist/index.d.ts +236 -59
- package/dist/index.js +3757 -482
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +642 -0
- package/dist/ir.cjs.map +1 -0
- package/dist/ir.d.cts +1119 -0
- package/dist/ir.d.ts +1119 -0
- package/dist/ir.js +623 -0
- package/dist/ir.js.map +1 -0
- package/dist/naming-CTvWSRTf.d.ts +79 -0
- package/dist/naming-DRyinU4u.d.cts +79 -0
- package/dist/predicate-v2.cjs +1093 -0
- package/dist/predicate-v2.cjs.map +1 -0
- package/dist/predicate-v2.d.cts +139 -0
- package/dist/predicate-v2.d.ts +139 -0
- package/dist/predicate-v2.js +1083 -0
- package/dist/predicate-v2.js.map +1 -0
- package/dist/sdl.cjs +1122 -0
- package/dist/sdl.cjs.map +1 -0
- package/dist/sdl.d.cts +94 -0
- package/dist/sdl.d.ts +94 -0
- package/dist/sdl.js +1098 -0
- package/dist/sdl.js.map +1 -0
- package/package.json +51 -3
- package/src/codegen-gql/apollo-hooks.cjs +98 -0
- package/src/codegen-gql/server-fns.cjs +101 -0
- package/templates/README.md +13 -0
- package/templates/orm-namespace/CLAUDE.md +82 -0
- package/templates/orm-namespace/codegen.ts +22 -0
- package/templates/orm-namespace/gitignore +5 -0
- package/templates/orm-namespace/graphql/example/queries.ts +56 -0
- package/templates/orm-namespace/package.json +24 -0
- package/templates/orm-namespace/schema/tables.ts +71 -0
- package/templates/orm-namespace/sequence.config.ts +32 -0
- package/templates/orm-namespace/tsconfig.json +15 -0
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,2033 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var zod = require('zod');
|
|
4
|
-
var api = require('drizzle-kit/api');
|
|
5
|
-
var pgCore = require('drizzle-orm/pg-core');
|
|
6
3
|
var drizzleOrm = require('drizzle-orm');
|
|
4
|
+
var pgCore = require('drizzle-orm/pg-core');
|
|
7
5
|
var crypto = require('crypto');
|
|
6
|
+
var zod = require('zod');
|
|
7
|
+
var jiti = require('jiti');
|
|
8
8
|
var promises = require('fs/promises');
|
|
9
|
+
var module$1 = require('module');
|
|
9
10
|
var path = require('path');
|
|
11
|
+
var api = require('drizzle-kit/api');
|
|
10
12
|
var jsYaml = require('js-yaml');
|
|
13
|
+
var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
|
|
14
|
+
|
|
15
|
+
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
16
|
+
// src/authoring/columns.ts
|
|
17
|
+
var COLUMN_BRAND = "seq-column";
|
|
18
|
+
var ENUM_BRAND = "seq-enum";
|
|
19
|
+
function isEnumRef(value) {
|
|
20
|
+
return typeof value === "object" && value !== null && value.kind === ENUM_BRAND;
|
|
21
|
+
}
|
|
22
|
+
var COLUMN_DEFAULT_FNS = ["gen_random_uuid()", "now()"];
|
|
23
|
+
function deriveColumnName(propertyName) {
|
|
24
|
+
return propertyName.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
var ColumnBuilder = class {
|
|
27
|
+
kind = COLUMN_BRAND;
|
|
28
|
+
state;
|
|
29
|
+
constructor(type, columnName) {
|
|
30
|
+
this.state = {
|
|
31
|
+
type,
|
|
32
|
+
columnName,
|
|
33
|
+
primaryKey: false,
|
|
34
|
+
notNull: false,
|
|
35
|
+
unique: false,
|
|
36
|
+
hasDefault: false,
|
|
37
|
+
defaultValue: void 0,
|
|
38
|
+
defaultFn: void 0,
|
|
39
|
+
description: void 0,
|
|
40
|
+
renamedFrom: void 0,
|
|
41
|
+
cast: void 0,
|
|
42
|
+
deprecated: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Marks this column as (part of) the primary key. Implies NOT NULL. */
|
|
46
|
+
primaryKey() {
|
|
47
|
+
this.state.primaryKey = true;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
notNull() {
|
|
51
|
+
this.state.notNull = true;
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
unique() {
|
|
55
|
+
this.state.unique = true;
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Column default: a literal, or one of the allowlisted SQL functions
|
|
60
|
+
* (`'gen_random_uuid()'` / `'now()'`), which serialize as `defaultFn`.
|
|
61
|
+
*/
|
|
62
|
+
default(value) {
|
|
63
|
+
if (value === "gen_random_uuid()" || value === "now()") {
|
|
64
|
+
this.state.defaultFn = value;
|
|
65
|
+
this.state.hasDefault = false;
|
|
66
|
+
this.state.defaultValue = void 0;
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
this.state.hasDefault = true;
|
|
70
|
+
this.state.defaultValue = value;
|
|
71
|
+
this.state.defaultFn = void 0;
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
74
|
+
description(text4) {
|
|
75
|
+
this.state.description = text4;
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
/** Migration rename hint: the previous Postgres column name. */
|
|
79
|
+
renamedFrom(previousColumnName) {
|
|
80
|
+
this.state.renamedFrom = previousColumnName;
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
/** Migration cast hint: the `USING` SQL applied when this column changes type. */
|
|
84
|
+
cast(usingSql) {
|
|
85
|
+
this.state.cast = usingSql;
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
/** Emits `@deprecated` in the generated SDL; the column stays queryable. */
|
|
89
|
+
deprecated() {
|
|
90
|
+
this.state.deprecated = true;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
/** Whether `.primaryKey()` was called — collected into the table-level PK. */
|
|
94
|
+
get isPrimaryKey() {
|
|
95
|
+
return this.state.primaryKey;
|
|
96
|
+
}
|
|
97
|
+
/** The enum name when this is an enum column, else undefined. */
|
|
98
|
+
get enumName() {
|
|
99
|
+
return typeof this.state.type === "object" ? this.state.type.enum : void 0;
|
|
100
|
+
}
|
|
101
|
+
/** Postgres column name: explicit override, else snake_case of the property name. */
|
|
102
|
+
columnNameFor(propertyName) {
|
|
103
|
+
return this.state.columnName ?? deriveColumnName(propertyName);
|
|
104
|
+
}
|
|
105
|
+
/** Serialize to a canonical v2 column. Validation happens in serializeNamespace. */
|
|
106
|
+
toCanonicalColumn(propertyName) {
|
|
107
|
+
const column = {
|
|
108
|
+
name: propertyName,
|
|
109
|
+
columnName: this.columnNameFor(propertyName),
|
|
110
|
+
type: typeof this.state.type === "object" ? { enum: this.state.type.enum } : this.state.type,
|
|
111
|
+
nullable: !(this.state.notNull || this.state.primaryKey),
|
|
112
|
+
unique: this.state.unique,
|
|
113
|
+
deprecated: this.state.deprecated
|
|
114
|
+
};
|
|
115
|
+
if (this.state.hasDefault) column.default = this.state.defaultValue ?? null;
|
|
116
|
+
if (this.state.defaultFn !== void 0) column.defaultFn = this.state.defaultFn;
|
|
117
|
+
if (this.state.description !== void 0) column.description = this.state.description;
|
|
118
|
+
if (this.state.renamedFrom !== void 0) column.renamedFrom = this.state.renamedFrom;
|
|
119
|
+
if (this.state.cast !== void 0) column.cast = this.state.cast;
|
|
120
|
+
return column;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
function isColumnBuilder(value) {
|
|
124
|
+
return typeof value === "object" && value !== null && value.kind === COLUMN_BRAND;
|
|
125
|
+
}
|
|
126
|
+
var scalar = (type) => (columnName) => new ColumnBuilder(type, columnName);
|
|
127
|
+
var t = {
|
|
128
|
+
uuid: scalar("uuid"),
|
|
129
|
+
text: scalar("text"),
|
|
130
|
+
integer: scalar("integer"),
|
|
131
|
+
bigint: scalar("bigint"),
|
|
132
|
+
numeric: scalar("numeric"),
|
|
133
|
+
double: scalar("double"),
|
|
134
|
+
boolean: scalar("boolean"),
|
|
135
|
+
timestamptz: scalar("timestamptz"),
|
|
136
|
+
date: scalar("date"),
|
|
137
|
+
jsonb: scalar("jsonb"),
|
|
138
|
+
enum: (ref, columnName) => new ColumnBuilder({ enum: ref.name }, columnName)
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// src/constants.ts
|
|
142
|
+
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
143
|
+
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
144
|
+
var MAX_IDENTIFIER_LENGTH = 63;
|
|
145
|
+
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
146
|
+
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
147
|
+
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
148
|
+
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
149
|
+
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
150
|
+
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
151
|
+
var ORM_SCHEMA_PREFIX = "orm_";
|
|
152
|
+
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
153
|
+
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
154
|
+
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
155
|
+
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
156
|
+
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
157
|
+
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
158
|
+
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
159
|
+
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
160
|
+
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
161
|
+
|
|
162
|
+
// src/errors.ts
|
|
163
|
+
var OrmDefinitionError = class extends Error {
|
|
164
|
+
constructor(message, file) {
|
|
165
|
+
super(file ? `${file}: ${message}` : message);
|
|
166
|
+
this.file = file;
|
|
167
|
+
}
|
|
168
|
+
name = "OrmDefinitionError";
|
|
169
|
+
};
|
|
170
|
+
var PredicateParseError = class extends Error {
|
|
171
|
+
constructor(message, position) {
|
|
172
|
+
super(`${message} (at position ${position})`);
|
|
173
|
+
this.position = position;
|
|
174
|
+
}
|
|
175
|
+
name = "PredicateParseError";
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/authoring/registry.ts
|
|
179
|
+
var CONTEXT_STACK_KEY = Symbol.for("sequence.orm.authoring.context-stack.v2");
|
|
180
|
+
function contextStack() {
|
|
181
|
+
const holder = globalThis;
|
|
182
|
+
const existing = holder[CONTEXT_STACK_KEY];
|
|
183
|
+
if (Array.isArray(existing)) {
|
|
184
|
+
return existing;
|
|
185
|
+
}
|
|
186
|
+
const created = [];
|
|
187
|
+
holder[CONTEXT_STACK_KEY] = created;
|
|
188
|
+
return created;
|
|
189
|
+
}
|
|
190
|
+
function beginAuthoringContext() {
|
|
191
|
+
const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
|
|
192
|
+
contextStack().push(context);
|
|
193
|
+
return context;
|
|
194
|
+
}
|
|
195
|
+
function endAuthoringContext(context) {
|
|
196
|
+
const stack = contextStack();
|
|
197
|
+
const top = stack[stack.length - 1];
|
|
198
|
+
if (top !== context) {
|
|
199
|
+
throw new Error("endAuthoringContext: the given context is not the active one \u2014 begin/end calls are mismatched");
|
|
200
|
+
}
|
|
201
|
+
stack.pop();
|
|
202
|
+
}
|
|
203
|
+
function activeAuthoringContext() {
|
|
204
|
+
const stack = contextStack();
|
|
205
|
+
return stack[stack.length - 1];
|
|
206
|
+
}
|
|
207
|
+
function register(bucket, value) {
|
|
208
|
+
const context = activeAuthoringContext();
|
|
209
|
+
if (!context) return;
|
|
210
|
+
context[bucket].push({ value, file: context.currentFile });
|
|
211
|
+
}
|
|
212
|
+
function registerTable(value) {
|
|
213
|
+
register("tables", value);
|
|
214
|
+
}
|
|
215
|
+
function registerView(value) {
|
|
216
|
+
register("views", value);
|
|
217
|
+
}
|
|
218
|
+
function registerEnum(value) {
|
|
219
|
+
register("enums", value);
|
|
220
|
+
}
|
|
221
|
+
function registerNamespaceConfig(value) {
|
|
222
|
+
register("configs", value);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// src/authoring/table.ts
|
|
226
|
+
var TABLE_BRAND = "seq-table";
|
|
227
|
+
var LINK_BRAND = "seq-link";
|
|
228
|
+
function toOne(target, options) {
|
|
229
|
+
return {
|
|
230
|
+
kind: LINK_BRAND,
|
|
231
|
+
linkKind: "toOne",
|
|
232
|
+
target,
|
|
233
|
+
on: options.on,
|
|
234
|
+
references: options.references,
|
|
235
|
+
enforce: options.enforce ?? false,
|
|
236
|
+
description: options.description
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function toMany(target, options) {
|
|
240
|
+
return {
|
|
241
|
+
kind: LINK_BRAND,
|
|
242
|
+
linkKind: "toMany",
|
|
243
|
+
target,
|
|
244
|
+
on: options.on,
|
|
245
|
+
references: options.references,
|
|
246
|
+
enforce: false,
|
|
247
|
+
description: options.description
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function toPlatformUser(options) {
|
|
251
|
+
return {
|
|
252
|
+
kind: LINK_BRAND,
|
|
253
|
+
linkKind: "toPlatformUser",
|
|
254
|
+
target: void 0,
|
|
255
|
+
on: options.on,
|
|
256
|
+
references: options.references,
|
|
257
|
+
enforce: false,
|
|
258
|
+
description: options.description
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function isSeqLink(value) {
|
|
262
|
+
return typeof value === "object" && value !== null && value.kind === LINK_BRAND;
|
|
263
|
+
}
|
|
264
|
+
function isSeqTable(value) {
|
|
265
|
+
return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
|
|
266
|
+
}
|
|
267
|
+
function assertIdentifier({ value, what, file }) {
|
|
268
|
+
if (!IDENTIFIER_PATTERN.test(value) || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
269
|
+
throw new OrmDefinitionError(
|
|
270
|
+
`${what} "${value}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
271
|
+
file
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function defineTable(name, columns, config = {}) {
|
|
276
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
277
|
+
assertIdentifier({ value: name, what: "table name", file });
|
|
278
|
+
const entries = Object.entries(columns);
|
|
279
|
+
if (entries.length === 0) {
|
|
280
|
+
throw new OrmDefinitionError(`table "${name}" declares no columns`, file);
|
|
281
|
+
}
|
|
282
|
+
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const [propertyName, builder] of entries) {
|
|
284
|
+
if (!isColumnBuilder(builder)) {
|
|
285
|
+
throw new OrmDefinitionError(
|
|
286
|
+
`table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
287
|
+
file
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
const columnName = builder.columnNameFor(propertyName);
|
|
291
|
+
assertIdentifier({ value: columnName, what: `table "${name}": column "${propertyName}" column name`, file });
|
|
292
|
+
const existing = seenColumnNames.get(columnName);
|
|
293
|
+
if (existing !== void 0) {
|
|
294
|
+
throw new OrmDefinitionError(
|
|
295
|
+
`table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
296
|
+
file
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
seenColumnNames.set(columnName, propertyName);
|
|
300
|
+
}
|
|
301
|
+
const table = { kind: TABLE_BRAND, tableName: name, columns, config, source: void 0, file };
|
|
302
|
+
registerTable(table);
|
|
303
|
+
return table;
|
|
304
|
+
}
|
|
305
|
+
function defineSyncedTable(name, columnsOrConfig, maybeConfig) {
|
|
306
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
307
|
+
assertIdentifier({ value: name, what: "table name", file });
|
|
308
|
+
const isTyped = maybeConfig !== void 0;
|
|
309
|
+
const columns = isTyped ? columnsOrConfig : {};
|
|
310
|
+
const config = isTyped ? maybeConfig : columnsOrConfig;
|
|
311
|
+
if (!SYNCED_SOURCE_PATTERN.test(config.source)) {
|
|
312
|
+
throw new OrmDefinitionError(
|
|
313
|
+
`synced table "${name}": source "${config.source}" must match ${SYNCED_SOURCE_PATTERN} (gold layer only)`,
|
|
314
|
+
file
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
const entries = Object.entries(columns);
|
|
318
|
+
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
319
|
+
const columnLevelPk = [];
|
|
320
|
+
for (const [propertyName, builder] of entries) {
|
|
321
|
+
if (!isColumnBuilder(builder)) {
|
|
322
|
+
throw new OrmDefinitionError(
|
|
323
|
+
`synced table "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories (t.uuid(), t.text(), \u2026)`,
|
|
324
|
+
file
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
const columnName = builder.columnNameFor(propertyName);
|
|
328
|
+
assertIdentifier({
|
|
329
|
+
value: columnName,
|
|
330
|
+
what: `synced table "${name}": column "${propertyName}" column name`,
|
|
331
|
+
file
|
|
332
|
+
});
|
|
333
|
+
const existing = seenColumnNames.get(columnName);
|
|
334
|
+
if (existing !== void 0) {
|
|
335
|
+
throw new OrmDefinitionError(
|
|
336
|
+
`synced table "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
337
|
+
file
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
seenColumnNames.set(columnName, propertyName);
|
|
341
|
+
if (builder.isPrimaryKey) columnLevelPk.push(propertyName);
|
|
342
|
+
}
|
|
343
|
+
if (entries.length > 0) {
|
|
344
|
+
const hasConfigPk = config.primaryKey !== void 0 && config.primaryKey.length > 0;
|
|
345
|
+
if (columnLevelPk.length === 0 && !hasConfigPk) {
|
|
346
|
+
throw new OrmDefinitionError(
|
|
347
|
+
`synced table "${name}" declares columns but no primary key \u2014 use .primaryKey() or config.primaryKey`,
|
|
348
|
+
file
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
if (columnLevelPk.length > 0 && hasConfigPk) {
|
|
352
|
+
throw new OrmDefinitionError(
|
|
353
|
+
`synced table "${name}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
|
|
354
|
+
file
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
const tableConfig = {};
|
|
359
|
+
if (config.description !== void 0) tableConfig.description = config.description;
|
|
360
|
+
if (config.primaryKey !== void 0) tableConfig.primaryKey = config.primaryKey;
|
|
361
|
+
if (config.links !== void 0) tableConfig.links = config.links;
|
|
362
|
+
if (config.policies !== void 0) tableConfig.policies = { read: config.policies.read };
|
|
363
|
+
const table = {
|
|
364
|
+
kind: TABLE_BRAND,
|
|
365
|
+
tableName: name,
|
|
366
|
+
columns,
|
|
367
|
+
config: tableConfig,
|
|
368
|
+
source: config.source,
|
|
369
|
+
file
|
|
370
|
+
};
|
|
371
|
+
registerTable(table);
|
|
372
|
+
return table;
|
|
373
|
+
}
|
|
374
|
+
function defineEnum(name, values) {
|
|
375
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
376
|
+
assertIdentifier({ value: name, what: "enum name", file });
|
|
377
|
+
if (values.length === 0 || values.length > 100) {
|
|
378
|
+
throw new OrmDefinitionError(`enum "${name}" must declare between 1 and 100 values`, file);
|
|
379
|
+
}
|
|
380
|
+
const seen = /* @__PURE__ */ new Set();
|
|
381
|
+
for (const value of values) {
|
|
382
|
+
if (value.length === 0 || value.length > MAX_IDENTIFIER_LENGTH) {
|
|
383
|
+
throw new OrmDefinitionError(
|
|
384
|
+
`enum "${name}": value ${JSON.stringify(value)} must be 1-${MAX_IDENTIFIER_LENGTH} characters`,
|
|
385
|
+
file
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (seen.has(value)) {
|
|
389
|
+
throw new OrmDefinitionError(`enum "${name}" declares duplicate value "${value}"`, file);
|
|
390
|
+
}
|
|
391
|
+
seen.add(value);
|
|
392
|
+
}
|
|
393
|
+
const ref = { kind: ENUM_BRAND, name, values: [...values] };
|
|
394
|
+
registerEnum(ref);
|
|
395
|
+
return ref;
|
|
396
|
+
}
|
|
397
|
+
var VIEW_BRAND = "seq-view";
|
|
398
|
+
function isSeqView(value) {
|
|
399
|
+
return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
|
|
400
|
+
}
|
|
401
|
+
function defineView(name, columns, config) {
|
|
402
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
403
|
+
if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
|
|
404
|
+
throw new OrmDefinitionError(
|
|
405
|
+
`view name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`,
|
|
406
|
+
file
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const entries = Object.entries(columns);
|
|
410
|
+
if (entries.length === 0) throw new OrmDefinitionError(`view "${name}" declares no columns`, file);
|
|
411
|
+
const seenColumnNames = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const [propertyName, builder] of entries) {
|
|
413
|
+
if (!isColumnBuilder(builder)) {
|
|
414
|
+
throw new OrmDefinitionError(
|
|
415
|
+
`view "${name}": column "${propertyName}" is not a column builder \u2014 use the t.* factories`,
|
|
416
|
+
file
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
const columnName = builder.columnNameFor(propertyName);
|
|
420
|
+
if (!IDENTIFIER_PATTERN.test(columnName) || columnName.length > MAX_IDENTIFIER_LENGTH) {
|
|
421
|
+
throw new OrmDefinitionError(`view "${name}": column "${propertyName}" has invalid column name "${columnName}"`, file);
|
|
422
|
+
}
|
|
423
|
+
const existing = seenColumnNames.get(columnName);
|
|
424
|
+
if (existing !== void 0) {
|
|
425
|
+
throw new OrmDefinitionError(
|
|
426
|
+
`view "${name}": columns "${existing}" and "${propertyName}" both map to Postgres column "${columnName}"`,
|
|
427
|
+
file
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
seenColumnNames.set(columnName, propertyName);
|
|
431
|
+
}
|
|
432
|
+
const view = { kind: VIEW_BRAND, viewName: name, columns, config, file };
|
|
433
|
+
registerView(view);
|
|
434
|
+
return view;
|
|
435
|
+
}
|
|
436
|
+
var QUERY_COLUMN_FACTORIES = {
|
|
437
|
+
uuid: (name) => pgCore.uuid(name),
|
|
438
|
+
text: (name) => pgCore.text(name),
|
|
439
|
+
integer: (name) => pgCore.integer(name),
|
|
440
|
+
bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
|
|
441
|
+
numeric: (name) => pgCore.numeric(name),
|
|
442
|
+
double: (name) => pgCore.doublePrecision(name),
|
|
443
|
+
boolean: (name) => pgCore.boolean(name),
|
|
444
|
+
timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
|
|
445
|
+
date: (name) => pgCore.date(name),
|
|
446
|
+
jsonb: (name) => pgCore.jsonb(name)
|
|
447
|
+
};
|
|
448
|
+
function compileViewQueries(input) {
|
|
449
|
+
const schemaName = `orm_${input.namespace}`;
|
|
450
|
+
const schema = pgCore.pgSchema(schemaName);
|
|
451
|
+
const enumFactories = /* @__PURE__ */ new Map();
|
|
452
|
+
for (const entry of input.enums) {
|
|
453
|
+
const values = entry.values;
|
|
454
|
+
enumFactories.set(entry.name, schema.enum(entry.name, values));
|
|
455
|
+
}
|
|
456
|
+
const handles = /* @__PURE__ */ new Map();
|
|
457
|
+
const physicalToLogical = /* @__PURE__ */ new Map();
|
|
458
|
+
const tableByName = new Map(input.tables.map((table) => [table.table, table]));
|
|
459
|
+
for (const authored of input.authoredTables) {
|
|
460
|
+
const canonical = tableByName.get(authored.tableName);
|
|
461
|
+
if (!canonical) continue;
|
|
462
|
+
const physicalSchema = schemaName;
|
|
463
|
+
const physicalName = canonical.table;
|
|
464
|
+
const relationSchema = pgCore.pgSchema(physicalSchema);
|
|
465
|
+
const columns = buildQueryColumns(canonical.columns, enumFactories);
|
|
466
|
+
const handle = relationSchema.table(physicalName, columns);
|
|
467
|
+
handles.set(authored, handle);
|
|
468
|
+
physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
|
|
469
|
+
}
|
|
470
|
+
const viewByName = new Map(input.views.map((view) => [view.view, view]));
|
|
471
|
+
for (const authored of input.authoredViews) {
|
|
472
|
+
const canonical = viewByName.get(authored.viewName);
|
|
473
|
+
if (!canonical) continue;
|
|
474
|
+
const columns = buildQueryColumns(canonical.columns, enumFactories);
|
|
475
|
+
const handle = schema.view(canonical.view, columns).existing();
|
|
476
|
+
handles.set(authored, handle);
|
|
477
|
+
physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
|
|
478
|
+
}
|
|
479
|
+
const dialect = new pgCore.PgDialect();
|
|
480
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
481
|
+
for (const authored of input.authoredViews) {
|
|
482
|
+
const expectedFields = Object.keys(authored.columns);
|
|
483
|
+
const declaredDependencies = /* @__PURE__ */ new Set();
|
|
484
|
+
const relation = (ref) => {
|
|
485
|
+
let resolved;
|
|
486
|
+
try {
|
|
487
|
+
resolved = typeof ref === "function" ? ref() : ref;
|
|
488
|
+
} catch (error) {
|
|
489
|
+
throw new OrmDefinitionError(
|
|
490
|
+
`view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
491
|
+
authored.file
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
495
|
+
throw new OrmDefinitionError(
|
|
496
|
+
`view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
|
|
497
|
+
authored.file
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
const handle = handles.get(resolved);
|
|
501
|
+
if (!handle) {
|
|
502
|
+
const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
|
|
503
|
+
throw new OrmDefinitionError(
|
|
504
|
+
`view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
|
|
505
|
+
authored.file
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
|
|
509
|
+
return handle;
|
|
510
|
+
};
|
|
511
|
+
let queryBuilder;
|
|
512
|
+
try {
|
|
513
|
+
queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
|
|
514
|
+
} catch (error) {
|
|
515
|
+
if (error instanceof OrmDefinitionError) throw error;
|
|
516
|
+
throw new OrmDefinitionError(
|
|
517
|
+
`view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
518
|
+
authored.file
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
let config;
|
|
522
|
+
try {
|
|
523
|
+
config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
|
|
524
|
+
} catch (error) {
|
|
525
|
+
throw new OrmDefinitionError(
|
|
526
|
+
`view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
|
|
527
|
+
authored.file
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
const selectedFields = Object.keys(config.selectedFields);
|
|
531
|
+
if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
|
|
532
|
+
throw new OrmDefinitionError(
|
|
533
|
+
`view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
|
|
534
|
+
authored.file
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
538
|
+
const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
|
|
539
|
+
const logical = physicalToLogical.get(physical);
|
|
540
|
+
if (!logical) {
|
|
541
|
+
throw new OrmDefinitionError(
|
|
542
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
|
|
543
|
+
authored.file
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return logical;
|
|
547
|
+
});
|
|
548
|
+
const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
|
|
549
|
+
if (renderedQuery.params.length > 0) {
|
|
550
|
+
throw new OrmDefinitionError(
|
|
551
|
+
`view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
|
|
552
|
+
authored.file
|
|
553
|
+
);
|
|
554
|
+
}
|
|
555
|
+
compiled.set(authored.viewName, {
|
|
556
|
+
query: renderedQuery.sql,
|
|
557
|
+
dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return compiled;
|
|
561
|
+
}
|
|
562
|
+
function buildQueryColumns(columns, enumFactories) {
|
|
563
|
+
return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
|
|
564
|
+
}
|
|
565
|
+
function buildQueryColumn(column, enumFactories) {
|
|
566
|
+
const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
|
|
567
|
+
if (!factory) {
|
|
568
|
+
const type = typeof column.type === "string" ? column.type : column.type.enum;
|
|
569
|
+
throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
|
|
570
|
+
}
|
|
571
|
+
let builder = factory(column.columnName);
|
|
572
|
+
if (!column.nullable) builder = builder.notNull();
|
|
573
|
+
return builder;
|
|
574
|
+
}
|
|
575
|
+
function relationName(relation) {
|
|
576
|
+
return isSeqTable(relation) ? relation.tableName : relation.viewName;
|
|
577
|
+
}
|
|
578
|
+
function relationKind(relation) {
|
|
579
|
+
return isSeqTable(relation) ? "table" : "view";
|
|
580
|
+
}
|
|
581
|
+
function declaredPrimaryKeyProperties(table) {
|
|
582
|
+
const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
|
|
583
|
+
if (fromColumns.length > 0) return fromColumns;
|
|
584
|
+
const configPk = table.config.primaryKey ?? [];
|
|
585
|
+
const resolved = [];
|
|
586
|
+
for (const ref of configPk) {
|
|
587
|
+
if (ref in table.columns) {
|
|
588
|
+
resolved.push(ref);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const byColumnName = Object.entries(table.columns).find(
|
|
592
|
+
([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
|
|
593
|
+
);
|
|
594
|
+
if (byColumnName) resolved.push(byColumnName[0]);
|
|
595
|
+
}
|
|
596
|
+
return resolved;
|
|
597
|
+
}
|
|
598
|
+
function assertOverlayWinsPrimaryKey({
|
|
599
|
+
table,
|
|
600
|
+
pk,
|
|
601
|
+
role
|
|
602
|
+
}) {
|
|
603
|
+
if (!(pk in table.columns)) {
|
|
604
|
+
throw new OrmDefinitionError(
|
|
605
|
+
`overlayWinsUnion: pk "${pk}" must exist as a property on both overlay and synced tables`
|
|
606
|
+
);
|
|
607
|
+
}
|
|
608
|
+
const declared = declaredPrimaryKeyProperties(table);
|
|
609
|
+
if (declared.length !== 1 || declared[0] !== pk) {
|
|
610
|
+
throw new OrmDefinitionError(
|
|
611
|
+
`overlayWinsUnion: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function formatCanonicalColumnType(type) {
|
|
616
|
+
return typeof type === "string" ? type : `enum(${type.enum})`;
|
|
617
|
+
}
|
|
618
|
+
function assertOverlayWinsMatchingColumnTypes({
|
|
619
|
+
overlay,
|
|
620
|
+
synced,
|
|
621
|
+
columns
|
|
622
|
+
}) {
|
|
623
|
+
for (const column of columns) {
|
|
624
|
+
const overlayType = overlay.columns[column].toCanonicalColumn(column).type;
|
|
625
|
+
const syncedType = synced.columns[column].toCanonicalColumn(column).type;
|
|
626
|
+
if (JSON.stringify(overlayType) !== JSON.stringify(syncedType)) {
|
|
627
|
+
throw new OrmDefinitionError(
|
|
628
|
+
`overlayWinsUnion: column "${column}" must have the same type on overlay and synced (overlay: ${formatCanonicalColumnType(overlayType)}, synced: ${formatCanonicalColumnType(syncedType)})`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
function overlayWinsUnion(input) {
|
|
634
|
+
const { overlay, synced, pk, tombstone } = input;
|
|
635
|
+
return ({ qb, relation }) => {
|
|
636
|
+
assertOverlayWinsPrimaryKey({ table: overlay, pk, role: "overlay" });
|
|
637
|
+
assertOverlayWinsPrimaryKey({ table: synced, pk, role: "synced" });
|
|
638
|
+
const overlayHandle = relation(overlay);
|
|
639
|
+
const syncedHandle = relation(synced);
|
|
640
|
+
const overlayCols = overlayHandle;
|
|
641
|
+
const syncedCols = syncedHandle;
|
|
642
|
+
const overlayPk = overlayCols[pk];
|
|
643
|
+
const syncedPk = syncedCols[pk];
|
|
644
|
+
if (tombstone !== void 0) {
|
|
645
|
+
const tombstoneBuilder = overlay.columns[tombstone];
|
|
646
|
+
if (!tombstoneBuilder) {
|
|
647
|
+
throw new OrmDefinitionError(
|
|
648
|
+
`overlayWinsUnion: tombstone "${tombstone}" must exist as a property on the overlay table`
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const tombstoneCanonical = tombstoneBuilder.toCanonicalColumn(tombstone);
|
|
652
|
+
if (tombstoneCanonical.type !== "boolean") {
|
|
653
|
+
throw new OrmDefinitionError(
|
|
654
|
+
`overlayWinsUnion: tombstone "${tombstone}" must be a boolean column`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (tombstoneCanonical.nullable) {
|
|
658
|
+
throw new OrmDefinitionError(
|
|
659
|
+
`overlayWinsUnion: tombstone "${tombstone}" must be notNull \u2014 nullable tombstones silently drop rows`
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
const overlayColumns = Object.keys(overlay.columns);
|
|
664
|
+
const syncedColumns = Object.keys(synced.columns);
|
|
665
|
+
const sharedColumns = overlayColumns.filter((col) => syncedColumns.includes(col) && col !== pk);
|
|
666
|
+
assertOverlayWinsMatchingColumnTypes({
|
|
667
|
+
overlay,
|
|
668
|
+
synced,
|
|
669
|
+
columns: [pk, ...sharedColumns]
|
|
670
|
+
});
|
|
671
|
+
const overlaySelect = {
|
|
672
|
+
[pk]: overlayPk,
|
|
673
|
+
...Object.fromEntries(sharedColumns.map((col) => [col, overlayCols[col]])),
|
|
674
|
+
provenance: drizzleOrm.sql`'overlay'`.as("provenance")
|
|
675
|
+
};
|
|
676
|
+
const syncedSelect = {
|
|
677
|
+
[pk]: syncedPk,
|
|
678
|
+
...Object.fromEntries(sharedColumns.map((col) => [col, syncedCols[col]])),
|
|
679
|
+
provenance: drizzleOrm.sql`'synced'`.as("provenance")
|
|
680
|
+
};
|
|
681
|
+
const overlayFrom = qb.select(overlaySelect).from(overlayHandle);
|
|
682
|
+
const overlayQuery = tombstone !== void 0 ? overlayFrom.where(drizzleOrm.eq(overlayCols[tombstone], false)) : overlayFrom;
|
|
683
|
+
const syncedQuery = qb.select(syncedSelect).from(syncedHandle).where(
|
|
684
|
+
drizzleOrm.notExists(
|
|
685
|
+
qb.select({ one: drizzleOrm.sql`1` }).from(overlayHandle).where(drizzleOrm.eq(overlayPk, syncedPk))
|
|
686
|
+
)
|
|
687
|
+
);
|
|
688
|
+
return overlayQuery.unionAll(syncedQuery);
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// src/authoring/cow-mutations.ts
|
|
693
|
+
function overrideRowMutation(input) {
|
|
694
|
+
const { overlay, synced, pk } = input;
|
|
695
|
+
const name = input.name ?? `override_${overlay.tableName}`;
|
|
696
|
+
assertMutationName(name, "overrideRowMutation");
|
|
697
|
+
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "overrideRowMutation" });
|
|
698
|
+
const sharedProperties = sharedNonPkProperties({ overlay, synced, pk });
|
|
699
|
+
const setProperties = resolveSetProperties({
|
|
700
|
+
overlay,
|
|
701
|
+
synced,
|
|
702
|
+
pk,
|
|
703
|
+
set: input.set,
|
|
704
|
+
sharedProperties,
|
|
705
|
+
helper: "overrideRowMutation"
|
|
706
|
+
});
|
|
707
|
+
assertMatchingColumns({
|
|
708
|
+
overlay,
|
|
709
|
+
synced,
|
|
710
|
+
properties: [pk, ...sharedProperties],
|
|
711
|
+
helper: "overrideRowMutation"
|
|
712
|
+
});
|
|
713
|
+
return {
|
|
714
|
+
kind: "override",
|
|
715
|
+
name,
|
|
716
|
+
synced: synced.tableName,
|
|
717
|
+
pk,
|
|
718
|
+
copy: [pk, ...sharedProperties],
|
|
719
|
+
set: setProperties
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function revertOverlayMutation(input) {
|
|
723
|
+
const { overlay, synced, pk } = input;
|
|
724
|
+
const name = input.name ?? `revert_${overlay.tableName}`;
|
|
725
|
+
assertMutationName(name, "revertOverlayMutation");
|
|
726
|
+
assertOverlayPairPrimaryKey({ overlay, synced, pk, helper: "revertOverlayMutation" });
|
|
727
|
+
assertMatchingColumns({ overlay, synced, properties: [pk], helper: "revertOverlayMutation" });
|
|
728
|
+
return {
|
|
729
|
+
kind: "revert",
|
|
730
|
+
name,
|
|
731
|
+
synced: synced.tableName,
|
|
732
|
+
pk,
|
|
733
|
+
copy: [],
|
|
734
|
+
set: []
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function assertMutationName(name, helper) {
|
|
738
|
+
if (!IDENTIFIER_PATTERN.test(name) || name.length > MAX_IDENTIFIER_LENGTH) {
|
|
739
|
+
throw new OrmDefinitionError(
|
|
740
|
+
`${helper}: mutation name "${name}" must match ${IDENTIFIER_PATTERN} and be at most ${MAX_IDENTIFIER_LENGTH} characters`
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
function assertOverlayPairPrimaryKey({
|
|
745
|
+
overlay,
|
|
746
|
+
synced,
|
|
747
|
+
pk,
|
|
748
|
+
helper
|
|
749
|
+
}) {
|
|
750
|
+
assertSinglePrimaryKey({ table: overlay, pk, role: "overlay", helper });
|
|
751
|
+
assertSinglePrimaryKey({ table: synced, pk, role: "synced", helper });
|
|
752
|
+
}
|
|
753
|
+
function assertSinglePrimaryKey({
|
|
754
|
+
table,
|
|
755
|
+
pk,
|
|
756
|
+
role,
|
|
757
|
+
helper
|
|
758
|
+
}) {
|
|
759
|
+
if (!(pk in table.columns)) {
|
|
760
|
+
throw new OrmDefinitionError(`${helper}: pk "${pk}" must exist as a property on the ${role} table`);
|
|
761
|
+
}
|
|
762
|
+
const declared = declaredPrimaryKeyProperties2(table);
|
|
763
|
+
if (declared.length !== 1 || declared[0] !== pk) {
|
|
764
|
+
throw new OrmDefinitionError(
|
|
765
|
+
`${helper}: ${role} primary key must be exactly "${pk}" (declared: ${declared.length > 0 ? declared.join(", ") : "none"})`
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function declaredPrimaryKeyProperties2(table) {
|
|
770
|
+
const fromColumns = Object.entries(table.columns).filter(([, builder]) => builder.isPrimaryKey).map(([propertyName]) => propertyName);
|
|
771
|
+
if (fromColumns.length > 0) return fromColumns;
|
|
772
|
+
const configPk = table.config?.primaryKey ?? [];
|
|
773
|
+
const resolved = [];
|
|
774
|
+
for (const ref of configPk) {
|
|
775
|
+
if (ref in table.columns) {
|
|
776
|
+
resolved.push(ref);
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const byColumnName = Object.entries(table.columns).find(
|
|
780
|
+
([propertyName, builder]) => builder.columnNameFor(propertyName) === ref
|
|
781
|
+
);
|
|
782
|
+
if (byColumnName) resolved.push(byColumnName[0]);
|
|
783
|
+
}
|
|
784
|
+
return resolved;
|
|
785
|
+
}
|
|
786
|
+
function sharedNonPkProperties({
|
|
787
|
+
overlay,
|
|
788
|
+
synced,
|
|
789
|
+
pk
|
|
790
|
+
}) {
|
|
791
|
+
return Object.keys(overlay.columns).filter((property) => property !== pk && property in synced.columns);
|
|
792
|
+
}
|
|
793
|
+
function resolveSetProperties({
|
|
794
|
+
overlay,
|
|
795
|
+
synced,
|
|
796
|
+
pk,
|
|
797
|
+
set,
|
|
798
|
+
sharedProperties,
|
|
799
|
+
helper
|
|
800
|
+
}) {
|
|
801
|
+
const properties = set !== void 0 ? [...set] : [...sharedProperties];
|
|
802
|
+
if (properties.length === 0) {
|
|
803
|
+
throw new OrmDefinitionError(
|
|
804
|
+
`${helper}: set must declare at least one non-PK column to update after the copy`
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
const seen = /* @__PURE__ */ new Set();
|
|
808
|
+
for (const property of properties) {
|
|
809
|
+
if (property === pk) {
|
|
810
|
+
throw new OrmDefinitionError(`${helper}: set must not include pk "${pk}" \u2014 identity columns are pinned`);
|
|
811
|
+
}
|
|
812
|
+
if (!(property in overlay.columns)) {
|
|
813
|
+
throw new OrmDefinitionError(`${helper}: set column "${property}" must exist on the overlay table`);
|
|
814
|
+
}
|
|
815
|
+
if (!(property in synced.columns)) {
|
|
816
|
+
throw new OrmDefinitionError(
|
|
817
|
+
`${helper}: set column "${property}" must exist on the synced table (contract columns only)`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (seen.has(property)) {
|
|
821
|
+
throw new OrmDefinitionError(`${helper}: set column "${property}" is duplicated`);
|
|
822
|
+
}
|
|
823
|
+
seen.add(property);
|
|
824
|
+
}
|
|
825
|
+
return properties;
|
|
826
|
+
}
|
|
827
|
+
function assertMatchingColumns({
|
|
828
|
+
overlay,
|
|
829
|
+
synced,
|
|
830
|
+
properties,
|
|
831
|
+
helper
|
|
832
|
+
}) {
|
|
833
|
+
for (const property of properties) {
|
|
834
|
+
const overlayCanonical = overlay.columns[property].toCanonicalColumn(property);
|
|
835
|
+
const syncedCanonical = synced.columns[property].toCanonicalColumn(property);
|
|
836
|
+
const overlayType = scalarColumnType(overlay, property, helper);
|
|
837
|
+
const syncedType = scalarColumnType(synced, property, helper);
|
|
838
|
+
if (overlayType !== syncedType) {
|
|
839
|
+
throw new OrmDefinitionError(
|
|
840
|
+
`${helper}: column "${property}" must have the same scalar type on overlay and synced (overlay: ${overlayType}, synced: ${syncedType})`
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
const overlayColumn = overlay.columns[property].columnNameFor(property);
|
|
844
|
+
const syncedColumn = synced.columns[property].columnNameFor(property);
|
|
845
|
+
if (overlayColumn !== syncedColumn) {
|
|
846
|
+
throw new OrmDefinitionError(
|
|
847
|
+
`${helper}: column "${property}" maps to different Postgres names on overlay (${overlayColumn}) and synced (${syncedColumn})`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
if (syncedCanonical.nullable && !overlayCanonical.nullable) {
|
|
851
|
+
throw new OrmDefinitionError(
|
|
852
|
+
`${helper}: column "${property}" is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy-from-synced insert; make overlay nullable or synced notNull`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function scalarColumnType(table, property, helper) {
|
|
858
|
+
const type = table.columns[property].toCanonicalColumn(property).type;
|
|
859
|
+
if (typeof type !== "string") {
|
|
860
|
+
throw new OrmDefinitionError(
|
|
861
|
+
`${helper}: column "${property}" on "${table.tableName}" must be a scalar type (enums are not supported in cow mutation arguments)`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
return type;
|
|
865
|
+
}
|
|
866
|
+
var CANONICAL_FORMAT = 2;
|
|
867
|
+
var IDENTIFIER_RE = /^[a-z][a-z0-9_]*$/;
|
|
868
|
+
var identifier = zod.z.string().min(1).max(63).regex(IDENTIFIER_RE);
|
|
869
|
+
var COLUMN_TYPES = [
|
|
870
|
+
"uuid",
|
|
871
|
+
"text",
|
|
872
|
+
"integer",
|
|
873
|
+
"bigint",
|
|
874
|
+
"numeric",
|
|
875
|
+
"double",
|
|
876
|
+
"boolean",
|
|
877
|
+
"timestamptz",
|
|
878
|
+
"date",
|
|
879
|
+
"jsonb"
|
|
880
|
+
];
|
|
881
|
+
var columnTypeSchema = zod.z.enum(COLUMN_TYPES);
|
|
882
|
+
var canonicalEnumSchema = zod.z.object({
|
|
883
|
+
name: identifier,
|
|
884
|
+
values: zod.z.array(zod.z.string().min(1).max(63)).min(1).max(100)
|
|
885
|
+
});
|
|
886
|
+
var canonicalColumnSchema = zod.z.object({
|
|
887
|
+
/** Property name in TS (camelCase); columnName is the Postgres name. */
|
|
888
|
+
name: zod.z.string().min(1).max(63),
|
|
889
|
+
columnName: identifier,
|
|
890
|
+
type: zod.z.union([columnTypeSchema, zod.z.object({ enum: identifier })]),
|
|
891
|
+
nullable: zod.z.boolean().default(true),
|
|
892
|
+
unique: zod.z.boolean().default(false),
|
|
893
|
+
/** Literal default, or one of the allowlisted SQL functions. */
|
|
894
|
+
default: zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]).optional(),
|
|
895
|
+
defaultFn: zod.z.enum(["gen_random_uuid()", "now()"]).optional(),
|
|
896
|
+
description: zod.z.string().max(500).optional(),
|
|
897
|
+
/** Migration hints — same semantics as v1. */
|
|
898
|
+
renamedFrom: identifier.optional(),
|
|
899
|
+
cast: zod.z.string().max(200).optional(),
|
|
900
|
+
deprecated: zod.z.boolean().default(false)
|
|
901
|
+
});
|
|
902
|
+
var canonicalLinkSchema = zod.z.object({
|
|
903
|
+
name: zod.z.string().min(1).max(63),
|
|
904
|
+
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
905
|
+
/** Target table name (namespace-local). Absent for toPlatformUser. */
|
|
906
|
+
target: identifier.optional(),
|
|
907
|
+
/** FK column on the owning side (toOne/toPlatformUser: on this table; toMany: on the target). */
|
|
908
|
+
on: identifier,
|
|
909
|
+
/** Referenced column on the target (default: target's PK / platform user `sub`). */
|
|
910
|
+
references: identifier.optional(),
|
|
911
|
+
/** enforce: true emits a real FK constraint (same-namespace toOne only). */
|
|
912
|
+
enforce: zod.z.boolean().default(false),
|
|
913
|
+
description: zod.z.string().max(500).optional()
|
|
914
|
+
});
|
|
915
|
+
var canonicalIndexSchema = zod.z.object({
|
|
916
|
+
columns: zod.z.array(identifier).min(1),
|
|
917
|
+
unique: zod.z.boolean().default(false),
|
|
918
|
+
name: identifier.optional()
|
|
919
|
+
});
|
|
920
|
+
var policyVariantSchema = zod.z.object({
|
|
921
|
+
/** Claims-only guard (capability()/role()/claim() comparisons — no columns). */
|
|
922
|
+
when: zod.z.string().min(1).max(1e3),
|
|
923
|
+
/** Exactly one of: allow-all bypass, or a row predicate. */
|
|
924
|
+
allow: zod.z.literal(true).optional(),
|
|
925
|
+
policy: zod.z.string().min(1).max(2e3).optional()
|
|
926
|
+
}).refine((v) => v.allow === true !== (typeof v.policy === "string"), {
|
|
927
|
+
message: "A variant declares exactly one of allow: true or policy"
|
|
928
|
+
});
|
|
929
|
+
var policyExpressionSchema = zod.z.union([
|
|
930
|
+
zod.z.string().min(1).max(2e3),
|
|
931
|
+
zod.z.object({
|
|
932
|
+
default: zod.z.string().min(1).max(2e3),
|
|
933
|
+
variants: zod.z.array(policyVariantSchema).max(10).default([])
|
|
934
|
+
})
|
|
935
|
+
]);
|
|
936
|
+
var canonicalPoliciesSchema = zod.z.object({
|
|
937
|
+
read: policyExpressionSchema.optional(),
|
|
938
|
+
write: policyExpressionSchema.optional(),
|
|
939
|
+
insert: policyExpressionSchema.optional(),
|
|
940
|
+
update: policyExpressionSchema.optional()
|
|
941
|
+
});
|
|
942
|
+
function isSingleViewQuerySql(value) {
|
|
943
|
+
const query = value.trim();
|
|
944
|
+
if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
|
|
945
|
+
let state = "normal";
|
|
946
|
+
for (let index3 = 0; index3 < query.length; index3 += 1) {
|
|
947
|
+
const char = query[index3];
|
|
948
|
+
const next = query[index3 + 1];
|
|
949
|
+
if (state === "single") {
|
|
950
|
+
if (char === "'" && next === "'") index3 += 1;
|
|
951
|
+
else if (char === "'") state = "normal";
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (state === "double") {
|
|
955
|
+
if (char === '"' && next === '"') index3 += 1;
|
|
956
|
+
else if (char === '"') state = "normal";
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (char === "'") state = "single";
|
|
960
|
+
else if (char === '"') state = "double";
|
|
961
|
+
else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
|
|
962
|
+
}
|
|
963
|
+
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
964
|
+
}
|
|
965
|
+
var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
966
|
+
"abs",
|
|
967
|
+
"age",
|
|
968
|
+
"array_agg",
|
|
969
|
+
"array_append",
|
|
970
|
+
"array_cat",
|
|
971
|
+
"array_dims",
|
|
972
|
+
"array_length",
|
|
973
|
+
"array_lower",
|
|
974
|
+
"array_ndims",
|
|
975
|
+
"array_position",
|
|
976
|
+
"array_positions",
|
|
977
|
+
"array_prepend",
|
|
978
|
+
"array_remove",
|
|
979
|
+
"array_replace",
|
|
980
|
+
"array_to_string",
|
|
981
|
+
"array_trim",
|
|
982
|
+
"array_upper",
|
|
983
|
+
"ascii",
|
|
984
|
+
"avg",
|
|
985
|
+
"bit_and",
|
|
986
|
+
"bit_or",
|
|
987
|
+
"bit_xor",
|
|
988
|
+
"bool_and",
|
|
989
|
+
"bool_or",
|
|
990
|
+
"btrim",
|
|
991
|
+
"cardinality",
|
|
992
|
+
"cbrt",
|
|
993
|
+
"ceil",
|
|
994
|
+
"ceiling",
|
|
995
|
+
"char_length",
|
|
996
|
+
"character_length",
|
|
997
|
+
"chr",
|
|
998
|
+
"concat",
|
|
999
|
+
"concat_ws",
|
|
1000
|
+
"count",
|
|
1001
|
+
"cume_dist",
|
|
1002
|
+
"date_bin",
|
|
1003
|
+
"date_part",
|
|
1004
|
+
"date_trunc",
|
|
1005
|
+
"decode",
|
|
1006
|
+
"degrees",
|
|
1007
|
+
"dense_rank",
|
|
1008
|
+
"encode",
|
|
1009
|
+
"enum_first",
|
|
1010
|
+
"enum_last",
|
|
1011
|
+
"enum_range",
|
|
1012
|
+
"every",
|
|
1013
|
+
"exp",
|
|
1014
|
+
"first_value",
|
|
1015
|
+
"floor",
|
|
1016
|
+
"format",
|
|
1017
|
+
"grouping",
|
|
1018
|
+
"initcap",
|
|
1019
|
+
"json_agg",
|
|
1020
|
+
"json_array_length",
|
|
1021
|
+
"json_build_array",
|
|
1022
|
+
"json_build_object",
|
|
1023
|
+
"json_extract_path",
|
|
1024
|
+
"json_extract_path_text",
|
|
1025
|
+
"json_object_agg",
|
|
1026
|
+
"json_object_keys",
|
|
1027
|
+
"json_strip_nulls",
|
|
1028
|
+
"json_typeof",
|
|
1029
|
+
"jsonb_agg",
|
|
1030
|
+
"jsonb_array_length",
|
|
1031
|
+
"jsonb_build_array",
|
|
1032
|
+
"jsonb_build_object",
|
|
1033
|
+
"jsonb_extract_path",
|
|
1034
|
+
"jsonb_extract_path_text",
|
|
1035
|
+
"jsonb_insert",
|
|
1036
|
+
"jsonb_object_agg",
|
|
1037
|
+
"jsonb_object_keys",
|
|
1038
|
+
"jsonb_path_exists",
|
|
1039
|
+
"jsonb_path_match",
|
|
1040
|
+
"jsonb_path_query",
|
|
1041
|
+
"jsonb_path_query_array",
|
|
1042
|
+
"jsonb_path_query_first",
|
|
1043
|
+
"jsonb_pretty",
|
|
1044
|
+
"jsonb_set",
|
|
1045
|
+
"jsonb_set_lax",
|
|
1046
|
+
"jsonb_strip_nulls",
|
|
1047
|
+
"jsonb_typeof",
|
|
1048
|
+
"justify_days",
|
|
1049
|
+
"justify_hours",
|
|
1050
|
+
"justify_interval",
|
|
1051
|
+
"lag",
|
|
1052
|
+
"last_value",
|
|
1053
|
+
"lead",
|
|
1054
|
+
"left",
|
|
1055
|
+
"length",
|
|
1056
|
+
"ln",
|
|
1057
|
+
"log",
|
|
1058
|
+
"lower",
|
|
1059
|
+
"lpad",
|
|
1060
|
+
"ltrim",
|
|
1061
|
+
"make_date",
|
|
1062
|
+
"make_interval",
|
|
1063
|
+
"make_time",
|
|
1064
|
+
"make_timestamp",
|
|
1065
|
+
"make_timestamptz",
|
|
1066
|
+
"max",
|
|
1067
|
+
"md5",
|
|
1068
|
+
"min",
|
|
1069
|
+
"mod",
|
|
1070
|
+
"nth_value",
|
|
1071
|
+
"ntile",
|
|
1072
|
+
"octet_length",
|
|
1073
|
+
"percent_rank",
|
|
1074
|
+
"pi",
|
|
1075
|
+
"power",
|
|
1076
|
+
"quote_ident",
|
|
1077
|
+
"quote_literal",
|
|
1078
|
+
"quote_nullable",
|
|
1079
|
+
"radians",
|
|
1080
|
+
"rank",
|
|
1081
|
+
"regexp_count",
|
|
1082
|
+
"regexp_instr",
|
|
1083
|
+
"regexp_like",
|
|
1084
|
+
"regexp_match",
|
|
1085
|
+
"regexp_matches",
|
|
1086
|
+
"regexp_replace",
|
|
1087
|
+
"regexp_split_to_array",
|
|
1088
|
+
"regexp_split_to_table",
|
|
1089
|
+
"repeat",
|
|
1090
|
+
"replace",
|
|
1091
|
+
"reverse",
|
|
1092
|
+
"right",
|
|
1093
|
+
"round",
|
|
1094
|
+
"row_number",
|
|
1095
|
+
"rpad",
|
|
1096
|
+
"rtrim",
|
|
1097
|
+
"sign",
|
|
1098
|
+
"split_part",
|
|
1099
|
+
"sqrt",
|
|
1100
|
+
"starts_with",
|
|
1101
|
+
"string_agg",
|
|
1102
|
+
"string_to_array",
|
|
1103
|
+
"strpos",
|
|
1104
|
+
"substr",
|
|
1105
|
+
"substring",
|
|
1106
|
+
"sum",
|
|
1107
|
+
"timezone",
|
|
1108
|
+
"to_ascii",
|
|
1109
|
+
"to_char",
|
|
1110
|
+
"to_date",
|
|
1111
|
+
"to_hex",
|
|
1112
|
+
"to_json",
|
|
1113
|
+
"to_jsonb",
|
|
1114
|
+
"to_number",
|
|
1115
|
+
"to_timestamp",
|
|
1116
|
+
"translate",
|
|
1117
|
+
"trunc",
|
|
1118
|
+
"unnest",
|
|
1119
|
+
"upper",
|
|
1120
|
+
"width_bucket"
|
|
1121
|
+
];
|
|
1122
|
+
var canonicalActionSchema = zod.z.object({
|
|
1123
|
+
name: identifier,
|
|
1124
|
+
requires: zod.z.string().max(1e3).optional(),
|
|
1125
|
+
params: zod.z.record(zod.z.string(), zod.z.object({
|
|
1126
|
+
type: columnTypeSchema,
|
|
1127
|
+
nullable: zod.z.boolean().default(false)
|
|
1128
|
+
})).default({}),
|
|
1129
|
+
executes: zod.z.string().min(1).max(1e4),
|
|
1130
|
+
returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
|
|
1131
|
+
audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
|
|
1132
|
+
});
|
|
1133
|
+
var canonicalCowMutationSchema = zod.z.object({
|
|
1134
|
+
kind: zod.z.enum(["override", "revert"]),
|
|
1135
|
+
name: identifier,
|
|
1136
|
+
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
1137
|
+
synced: identifier,
|
|
1138
|
+
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
1139
|
+
pk: zod.z.string().min(1).max(63),
|
|
1140
|
+
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
1141
|
+
copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
|
|
1142
|
+
/** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
|
|
1143
|
+
set: zod.z.array(zod.z.string().min(1).max(63)).default([])
|
|
1144
|
+
}).superRefine((mutation, ctx) => {
|
|
1145
|
+
if (mutation.kind === "override") {
|
|
1146
|
+
if (mutation.set.length === 0) {
|
|
1147
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
|
|
1148
|
+
}
|
|
1149
|
+
if (!mutation.copy.includes(mutation.pk)) {
|
|
1150
|
+
ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
|
|
1151
|
+
}
|
|
1152
|
+
} else if (mutation.set.length > 0 || mutation.copy.length > 0) {
|
|
1153
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
|
|
1154
|
+
}
|
|
1155
|
+
if (mutation.set.includes(mutation.pk)) {
|
|
1156
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
|
|
1157
|
+
}
|
|
1158
|
+
for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
|
|
1159
|
+
if (new Set(values).size !== values.length) {
|
|
1160
|
+
ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
});
|
|
1164
|
+
var canonicalTableSchema = zod.z.object({
|
|
1165
|
+
table: identifier,
|
|
1166
|
+
description: zod.z.string().max(2e3).optional(),
|
|
1167
|
+
/** Table-level rename hint (new in v2). */
|
|
1168
|
+
renamedFrom: identifier.optional(),
|
|
1169
|
+
/** Synced/reflected read-only tables (`source: gold.*`) — v1 semantics. */
|
|
1170
|
+
source: zod.z.string().regex(/^gold\.[a-z][a-z0-9_]*$/).optional(),
|
|
1171
|
+
columns: zod.z.array(canonicalColumnSchema).default([]),
|
|
1172
|
+
primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
|
|
1173
|
+
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1174
|
+
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
1175
|
+
policies: canonicalPoliciesSchema.default({}),
|
|
1176
|
+
/**
|
|
1177
|
+
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
1178
|
+
* neither compiles nor executes these; writes are mutations. Kept parseable
|
|
1179
|
+
* so stored definitions and their content hashes stay stable.
|
|
1180
|
+
*/
|
|
1181
|
+
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
1182
|
+
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
1183
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
1184
|
+
});
|
|
1185
|
+
var canonicalViewSchema = zod.z.object({
|
|
1186
|
+
view: identifier,
|
|
1187
|
+
description: zod.z.string().max(2e3).optional(),
|
|
1188
|
+
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
1189
|
+
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
1190
|
+
primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
|
|
1191
|
+
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
1192
|
+
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
1193
|
+
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
1194
|
+
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
1195
|
+
}),
|
|
1196
|
+
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
1197
|
+
dependencies: zod.z.array(identifier).max(500)
|
|
1198
|
+
}).superRefine((view, ctx) => {
|
|
1199
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
1200
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1201
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
1202
|
+
}
|
|
1203
|
+
if (column.unique) {
|
|
1204
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
1205
|
+
}
|
|
1206
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
1207
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const [index3, link] of view.links.entries()) {
|
|
1211
|
+
if (link.enforce) {
|
|
1212
|
+
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
var canonicalDefinitionSchema = zod.z.object({
|
|
1217
|
+
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
1218
|
+
namespace: identifier.max(41),
|
|
1219
|
+
description: zod.z.string().max(2e3).optional(),
|
|
1220
|
+
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
1221
|
+
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
1222
|
+
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
1223
|
+
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
1224
|
+
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
1225
|
+
capabilities: zod.z.object({
|
|
1226
|
+
roles: zod.z.unknown().optional(),
|
|
1227
|
+
uses: zod.z.array(zod.z.string()).default([])
|
|
1228
|
+
}).default({ uses: [] })
|
|
1229
|
+
}).superRefine((definition, ctx) => {
|
|
1230
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1231
|
+
for (const [index3, table] of definition.tables.entries()) {
|
|
1232
|
+
if (seen.has(table.table)) {
|
|
1233
|
+
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
1234
|
+
}
|
|
1235
|
+
seen.add(table.table);
|
|
1236
|
+
}
|
|
1237
|
+
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
1238
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
1239
|
+
const cowMutations = table.cowMutations ?? [];
|
|
1240
|
+
if (cowMutations.length === 0) continue;
|
|
1241
|
+
const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
|
|
1242
|
+
if (table.source !== void 0) {
|
|
1243
|
+
ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
for (const [index3, mutation] of cowMutations.entries()) {
|
|
1247
|
+
const synced = tableByName.get(mutation.synced);
|
|
1248
|
+
if (!synced || synced.source === void 0) {
|
|
1249
|
+
ctx.addIssue({
|
|
1250
|
+
code: "custom",
|
|
1251
|
+
path: path(index3, "synced"),
|
|
1252
|
+
message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
|
|
1253
|
+
});
|
|
1254
|
+
continue;
|
|
1255
|
+
}
|
|
1256
|
+
const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
|
|
1257
|
+
const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
|
|
1258
|
+
let columnsExist = true;
|
|
1259
|
+
for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
|
|
1260
|
+
if (!overlayColumnsByName.has(property)) {
|
|
1261
|
+
columnsExist = false;
|
|
1262
|
+
ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
1266
|
+
if (!syncedColumnsByName.has(property)) {
|
|
1267
|
+
columnsExist = false;
|
|
1268
|
+
ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
if (!columnsExist) continue;
|
|
1272
|
+
const overlayPk = table.primaryKey ?? [];
|
|
1273
|
+
const syncedPk = synced.primaryKey ?? [];
|
|
1274
|
+
if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
|
|
1275
|
+
ctx.addIssue({
|
|
1276
|
+
code: "custom",
|
|
1277
|
+
path: path(index3, "pk"),
|
|
1278
|
+
message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
1282
|
+
const overlayColumn = overlayColumnsByName.get(property);
|
|
1283
|
+
const syncedColumn = syncedColumnsByName.get(property);
|
|
1284
|
+
if (!overlayColumn || !syncedColumn) continue;
|
|
1285
|
+
if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
|
|
1286
|
+
ctx.addIssue({
|
|
1287
|
+
code: "custom",
|
|
1288
|
+
path: path(index3),
|
|
1289
|
+
message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
if (overlayColumn.columnName !== syncedColumn.columnName) {
|
|
1293
|
+
ctx.addIssue({
|
|
1294
|
+
code: "custom",
|
|
1295
|
+
path: path(index3),
|
|
1296
|
+
message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
|
|
1300
|
+
ctx.addIssue({
|
|
1301
|
+
code: "custom",
|
|
1302
|
+
path: path(index3),
|
|
1303
|
+
message: `cow mutation '${mutation.name}': column '${property}' is nullable on synced but NOT NULL on overlay \u2014 a NULL synced value would fail the copy`
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
for (const [index3, view] of (definition.views ?? []).entries()) {
|
|
1310
|
+
if (seen.has(view.view)) {
|
|
1311
|
+
ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
|
|
1312
|
+
}
|
|
1313
|
+
seen.add(view.view);
|
|
1314
|
+
}
|
|
1315
|
+
const views = definition.views ?? [];
|
|
1316
|
+
const viewByName = new Map(views.map((view) => [view.view, view]));
|
|
1317
|
+
for (const [viewIndex, view] of views.entries()) {
|
|
1318
|
+
for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
|
|
1319
|
+
if (!seen.has(dependency)) {
|
|
1320
|
+
ctx.addIssue({
|
|
1321
|
+
code: "custom",
|
|
1322
|
+
path: ["views", viewIndex, "dependencies", dependencyIndex],
|
|
1323
|
+
message: `view '${view.view}' depends on unknown relation '${dependency}'`
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
1329
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1330
|
+
let cycleReported = false;
|
|
1331
|
+
const visit = (name, path) => {
|
|
1332
|
+
if (visited.has(name) || cycleReported) return;
|
|
1333
|
+
if (visiting.has(name)) {
|
|
1334
|
+
cycleReported = true;
|
|
1335
|
+
ctx.addIssue({
|
|
1336
|
+
code: "custom",
|
|
1337
|
+
path: ["views"],
|
|
1338
|
+
message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
|
|
1339
|
+
});
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
visiting.add(name);
|
|
1343
|
+
const view = viewByName.get(name);
|
|
1344
|
+
for (const dependency of view?.dependencies ?? []) {
|
|
1345
|
+
if (viewByName.has(dependency)) visit(dependency, [...path, name]);
|
|
1346
|
+
}
|
|
1347
|
+
visiting.delete(name);
|
|
1348
|
+
visited.add(name);
|
|
1349
|
+
};
|
|
1350
|
+
for (const view of views) visit(view.view, []);
|
|
1351
|
+
});
|
|
1352
|
+
function canonicalRelations(definition) {
|
|
1353
|
+
const views = (definition.views ?? []).map((view) => ({
|
|
1354
|
+
table: view.view,
|
|
1355
|
+
...view.description !== void 0 ? { description: view.description } : {},
|
|
1356
|
+
columns: view.columns,
|
|
1357
|
+
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
1358
|
+
links: view.links,
|
|
1359
|
+
indexes: [],
|
|
1360
|
+
policies: { read: view.policies.read },
|
|
1361
|
+
actions: []
|
|
1362
|
+
}));
|
|
1363
|
+
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
1364
|
+
}
|
|
1365
|
+
function normalizeCanonical(definition) {
|
|
1366
|
+
const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
1367
|
+
const normalized = {
|
|
1368
|
+
...definition,
|
|
1369
|
+
enums: sortByName(definition.enums),
|
|
1370
|
+
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
1371
|
+
const { cowMutations, ...rest } = t2;
|
|
1372
|
+
return {
|
|
1373
|
+
...rest,
|
|
1374
|
+
links: sortByName(t2.links),
|
|
1375
|
+
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
1376
|
+
actions: sortByName(t2.actions),
|
|
1377
|
+
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
1378
|
+
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
1379
|
+
};
|
|
1380
|
+
})
|
|
1381
|
+
};
|
|
1382
|
+
if (definition.views !== void 0) {
|
|
1383
|
+
normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
|
|
1384
|
+
...view,
|
|
1385
|
+
links: sortByName(view.links),
|
|
1386
|
+
dependencies: [...view.dependencies].sort()
|
|
1387
|
+
}));
|
|
1388
|
+
}
|
|
1389
|
+
return normalized;
|
|
1390
|
+
}
|
|
1391
|
+
function stableJson(value) {
|
|
1392
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value);
|
|
1393
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
1394
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([a], [b]) => a < b ? -1 : 1).map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`);
|
|
1395
|
+
return `{${entries.join(",")}}`;
|
|
1396
|
+
}
|
|
1397
|
+
function emptyToNull(value) {
|
|
1398
|
+
if (value == null) return null;
|
|
1399
|
+
if (Array.isArray(value)) return value.length === 0 ? null : value;
|
|
1400
|
+
if (typeof value === "object" && Object.keys(value).length === 0) return null;
|
|
1401
|
+
return value;
|
|
1402
|
+
}
|
|
1403
|
+
function canonicalTable(table) {
|
|
1404
|
+
return {
|
|
1405
|
+
table: table.table,
|
|
1406
|
+
description: table.description ?? null,
|
|
1407
|
+
columns: table.columns ?? null,
|
|
1408
|
+
source: table.source ?? null,
|
|
1409
|
+
links: emptyToNull(table.links),
|
|
1410
|
+
indexes: emptyToNull(table.indexes),
|
|
1411
|
+
// insert/update are omitted rather than nulled when absent, so definitions
|
|
1412
|
+
// predating split policies keep the hash they were registered under.
|
|
1413
|
+
policies: {
|
|
1414
|
+
read: table.policies?.read?.source ?? null,
|
|
1415
|
+
write: table.policies?.write?.source ?? null,
|
|
1416
|
+
...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
|
|
1417
|
+
...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
|
|
1418
|
+
},
|
|
1419
|
+
actions: emptyToNull(
|
|
1420
|
+
Object.fromEntries(
|
|
1421
|
+
Object.entries(table.actions ?? {}).map(([name, action]) => [
|
|
1422
|
+
name,
|
|
1423
|
+
{
|
|
1424
|
+
description: action.description ?? null,
|
|
1425
|
+
requires: action.requires?.source ?? null,
|
|
1426
|
+
params: emptyToNull(action.params),
|
|
1427
|
+
validates: emptyToNull(action.validates?.map((entry) => entry.source)),
|
|
1428
|
+
executes: action.executes,
|
|
1429
|
+
returns: emptyToNull(action.returns),
|
|
1430
|
+
audit: action.audit ?? null
|
|
1431
|
+
}
|
|
1432
|
+
])
|
|
1433
|
+
)
|
|
1434
|
+
)
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
function canonicalCapabilities(capabilities) {
|
|
1438
|
+
const { definitions, capabilities: declared } = capabilities.roles;
|
|
1439
|
+
const isEmpty = Object.keys(definitions).length === 0 && Object.keys(declared).length === 0 && capabilities.uses.length === 0;
|
|
1440
|
+
if (isEmpty) return null;
|
|
1441
|
+
return {
|
|
1442
|
+
roles: {
|
|
1443
|
+
definitions: Object.fromEntries(
|
|
1444
|
+
Object.entries(definitions).map(([key, def]) => [
|
|
1445
|
+
key,
|
|
1446
|
+
{
|
|
1447
|
+
...def.description !== void 0 ? { description: def.description } : {},
|
|
1448
|
+
default: def.default,
|
|
1449
|
+
inherits: [...def.inherits].sort(),
|
|
1450
|
+
grants: [...def.grants].sort()
|
|
1451
|
+
}
|
|
1452
|
+
])
|
|
1453
|
+
),
|
|
1454
|
+
capabilities: Object.fromEntries(
|
|
1455
|
+
Object.entries(declared).map(([key, cap]) => [
|
|
1456
|
+
key,
|
|
1457
|
+
cap.description !== void 0 ? { description: cap.description } : {}
|
|
1458
|
+
])
|
|
1459
|
+
)
|
|
1460
|
+
},
|
|
1461
|
+
uses: [...capabilities.uses].sort()
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function canonicalizeDefinition(definition) {
|
|
1465
|
+
return {
|
|
1466
|
+
namespace: definition.namespace,
|
|
1467
|
+
description: definition.description,
|
|
1468
|
+
capabilities: canonicalCapabilities(definition.capabilities),
|
|
1469
|
+
tables: definition.tables.map(canonicalTable)
|
|
1470
|
+
};
|
|
1471
|
+
}
|
|
1472
|
+
function computeDefinitionHash(definition) {
|
|
1473
|
+
return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
// src/authoring/namespace.ts
|
|
1477
|
+
var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
|
|
1478
|
+
function isSeqNamespaceConfig(value) {
|
|
1479
|
+
return typeof value === "object" && value !== null && value.kind === NAMESPACE_CONFIG_BRAND;
|
|
1480
|
+
}
|
|
1481
|
+
function defineNamespace(input) {
|
|
1482
|
+
const file = activeAuthoringContext()?.currentFile;
|
|
1483
|
+
if (!NAMESPACE_PATTERN.test(input.namespace)) {
|
|
1484
|
+
throw new OrmDefinitionError(`namespace "${input.namespace}" must match ${NAMESPACE_PATTERN}`, file);
|
|
1485
|
+
}
|
|
1486
|
+
const config = {
|
|
1487
|
+
kind: NAMESPACE_CONFIG_BRAND,
|
|
1488
|
+
namespace: input.namespace,
|
|
1489
|
+
description: input.description,
|
|
1490
|
+
capabilities: input.capabilities ?? {},
|
|
1491
|
+
file
|
|
1492
|
+
};
|
|
1493
|
+
registerNamespaceConfig(config);
|
|
1494
|
+
return config;
|
|
1495
|
+
}
|
|
1496
|
+
function serializeNamespace(input) {
|
|
1497
|
+
const { config, tables } = input;
|
|
1498
|
+
const views = input.views ?? [];
|
|
1499
|
+
const enums = input.enums ?? [];
|
|
1500
|
+
const tablesByName = /* @__PURE__ */ new Map();
|
|
1501
|
+
for (const table of tables) {
|
|
1502
|
+
const existing = tablesByName.get(table.tableName);
|
|
1503
|
+
if (existing) {
|
|
1504
|
+
throw new OrmDefinitionError(
|
|
1505
|
+
`duplicate table "${table.tableName}" (defined in ${describeFile(existing)} and ${describeFile(table)})`,
|
|
1506
|
+
table.file
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
tablesByName.set(table.tableName, table);
|
|
1510
|
+
}
|
|
1511
|
+
const viewsByName = /* @__PURE__ */ new Map();
|
|
1512
|
+
for (const view of views) {
|
|
1513
|
+
const existing = viewsByName.get(view.viewName);
|
|
1514
|
+
if (existing) {
|
|
1515
|
+
throw new OrmDefinitionError(
|
|
1516
|
+
`duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
|
|
1517
|
+
view.file
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
viewsByName.set(view.viewName, view);
|
|
1521
|
+
}
|
|
1522
|
+
const relationsByName = /* @__PURE__ */ new Map();
|
|
1523
|
+
for (const relation of [...tables, ...views]) {
|
|
1524
|
+
const name = relationName(relation);
|
|
1525
|
+
const existing = relationsByName.get(name);
|
|
1526
|
+
if (existing) {
|
|
1527
|
+
throw new OrmDefinitionError(
|
|
1528
|
+
`duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
|
|
1529
|
+
relation.file
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
relationsByName.set(name, relation);
|
|
1533
|
+
}
|
|
1534
|
+
const enumsByName = /* @__PURE__ */ new Map();
|
|
1535
|
+
for (const ref of enums) {
|
|
1536
|
+
if (enumsByName.has(ref.name)) {
|
|
1537
|
+
throw new OrmDefinitionError(`duplicate enum "${ref.name}"`, config.file);
|
|
1538
|
+
}
|
|
1539
|
+
enumsByName.set(ref.name, ref);
|
|
1540
|
+
}
|
|
1541
|
+
const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
|
|
1542
|
+
const canonicalEnums = [...enumsByName.values()].map((ref) => ({
|
|
1543
|
+
name: ref.name,
|
|
1544
|
+
values: [...ref.values]
|
|
1545
|
+
}));
|
|
1546
|
+
const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
|
|
1547
|
+
const compiledViews = compileViewQueries({
|
|
1548
|
+
namespace: config.namespace,
|
|
1549
|
+
tables: canonicalTables,
|
|
1550
|
+
views: viewDrafts,
|
|
1551
|
+
enums: canonicalEnums,
|
|
1552
|
+
authoredTables: tables,
|
|
1553
|
+
authoredViews: views
|
|
1554
|
+
});
|
|
1555
|
+
const canonicalViews = viewDrafts.map((draft) => {
|
|
1556
|
+
const compiled = compiledViews.get(draft.view);
|
|
1557
|
+
if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
|
|
1558
|
+
return canonicalViewSchema.parse({ ...draft, ...compiled });
|
|
1559
|
+
});
|
|
1560
|
+
assertAcyclicViewDependencies(canonicalViews);
|
|
1561
|
+
const candidate = {
|
|
1562
|
+
definition_format: CANONICAL_FORMAT,
|
|
1563
|
+
namespace: config.namespace,
|
|
1564
|
+
enums: canonicalEnums,
|
|
1565
|
+
tables: canonicalTables,
|
|
1566
|
+
capabilities: {
|
|
1567
|
+
...config.capabilities.roles !== void 0 ? { roles: config.capabilities.roles } : {},
|
|
1568
|
+
uses: [...config.capabilities.uses ?? []]
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
if (config.description !== void 0) candidate.description = config.description;
|
|
1572
|
+
if (canonicalViews.length > 0) candidate.views = canonicalViews;
|
|
1573
|
+
let parsed;
|
|
1574
|
+
try {
|
|
1575
|
+
parsed = canonicalDefinitionSchema.parse(candidate);
|
|
1576
|
+
} catch (error) {
|
|
1577
|
+
if (error instanceof zod.z.ZodError) {
|
|
1578
|
+
throw new OrmDefinitionError(`namespace "${config.namespace}": ${formatZodIssues(error)}`, config.file);
|
|
1579
|
+
}
|
|
1580
|
+
throw error;
|
|
1581
|
+
}
|
|
1582
|
+
return normalizeCanonical(parsed);
|
|
1583
|
+
}
|
|
1584
|
+
function computeCanonicalHash(definition) {
|
|
1585
|
+
return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
|
|
1586
|
+
}
|
|
1587
|
+
function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
1588
|
+
const candidate = {
|
|
1589
|
+
table: table.tableName,
|
|
1590
|
+
columns: [],
|
|
1591
|
+
links: [],
|
|
1592
|
+
indexes: [],
|
|
1593
|
+
policies: {},
|
|
1594
|
+
actions: []
|
|
1595
|
+
};
|
|
1596
|
+
if (table.config.description !== void 0) candidate.description = table.config.description;
|
|
1597
|
+
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
1598
|
+
if (table.source !== void 0) candidate.source = table.source;
|
|
1599
|
+
if (table.source !== void 0) {
|
|
1600
|
+
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
1601
|
+
throw new OrmDefinitionError(
|
|
1602
|
+
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
1603
|
+
table.file
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
|
|
1607
|
+
throw new OrmDefinitionError(
|
|
1608
|
+
`synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
|
|
1609
|
+
table.file
|
|
1610
|
+
);
|
|
1611
|
+
}
|
|
1612
|
+
if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
|
|
1613
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
|
|
1614
|
+
}
|
|
1615
|
+
for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
|
|
1616
|
+
if (link.enforce) {
|
|
1617
|
+
throw new OrmDefinitionError(
|
|
1618
|
+
`synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
|
|
1619
|
+
table.file
|
|
1620
|
+
);
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
const columnLevelPk = [];
|
|
1625
|
+
for (const [propertyName, builder] of Object.entries(table.columns)) {
|
|
1626
|
+
const column = builder.toCanonicalColumn(propertyName);
|
|
1627
|
+
const enumName = builder.enumName;
|
|
1628
|
+
if (enumName !== void 0 && !enumsByName.has(enumName)) {
|
|
1629
|
+
throw new OrmDefinitionError(
|
|
1630
|
+
`table "${table.tableName}": column "${propertyName}" references enum "${enumName}" which is not passed to serializeNamespace \u2014 declare it with defineEnum and include it`,
|
|
1631
|
+
table.file
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1635
|
+
candidate.columns?.push(column);
|
|
1636
|
+
}
|
|
1637
|
+
if (table.config.primaryKey !== void 0) {
|
|
1638
|
+
if (columnLevelPk.length > 0) {
|
|
1639
|
+
throw new OrmDefinitionError(
|
|
1640
|
+
`table "${table.tableName}": declare the primary key either with .primaryKey() on columns or with config.primaryKey \u2014 not both`,
|
|
1641
|
+
table.file
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1644
|
+
candidate.primaryKey = table.config.primaryKey.map(
|
|
1645
|
+
(ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
|
|
1646
|
+
);
|
|
1647
|
+
} else if (columnLevelPk.length > 0) {
|
|
1648
|
+
candidate.primaryKey = columnLevelPk;
|
|
1649
|
+
}
|
|
1650
|
+
for (const index3 of table.config.indexes ?? []) {
|
|
1651
|
+
const entry = {
|
|
1652
|
+
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
1653
|
+
};
|
|
1654
|
+
if (index3.unique !== void 0) entry.unique = index3.unique;
|
|
1655
|
+
if (index3.name !== void 0) entry.name = index3.name;
|
|
1656
|
+
candidate.indexes?.push(entry);
|
|
1657
|
+
}
|
|
1658
|
+
for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
|
|
1659
|
+
candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
|
|
1660
|
+
}
|
|
1661
|
+
const policies = {};
|
|
1662
|
+
if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
|
|
1663
|
+
if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
|
|
1664
|
+
if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
|
|
1665
|
+
if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
|
|
1666
|
+
candidate.policies = policies;
|
|
1667
|
+
const cowMutations = table.config.cowMutations ?? [];
|
|
1668
|
+
if (cowMutations.length > 0) {
|
|
1669
|
+
candidate.cowMutations = cowMutations.map((mutation) => ({
|
|
1670
|
+
kind: mutation.kind,
|
|
1671
|
+
name: mutation.name,
|
|
1672
|
+
synced: mutation.synced,
|
|
1673
|
+
pk: mutation.pk,
|
|
1674
|
+
copy: [...mutation.copy],
|
|
1675
|
+
set: [...mutation.set]
|
|
1676
|
+
}));
|
|
1677
|
+
}
|
|
1678
|
+
try {
|
|
1679
|
+
return canonicalTableSchema.parse(candidate);
|
|
1680
|
+
} catch (error) {
|
|
1681
|
+
if (error instanceof zod.z.ZodError) {
|
|
1682
|
+
throw new OrmDefinitionError(`table "${table.tableName}": ${formatZodIssues(error)}`, table.file);
|
|
1683
|
+
}
|
|
1684
|
+
throw error;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function buildCanonicalView({
|
|
1688
|
+
view,
|
|
1689
|
+
relationsByName,
|
|
1690
|
+
enumsByName
|
|
1691
|
+
}) {
|
|
1692
|
+
if (view.config.policies?.read === void 0) {
|
|
1693
|
+
throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
|
|
1694
|
+
}
|
|
1695
|
+
const columns = [];
|
|
1696
|
+
const columnLevelPk = [];
|
|
1697
|
+
for (const [propertyName, builder] of Object.entries(view.columns)) {
|
|
1698
|
+
const column = builder.toCanonicalColumn(propertyName);
|
|
1699
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1700
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1701
|
+
}
|
|
1702
|
+
if (column.unique) {
|
|
1703
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1704
|
+
}
|
|
1705
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
1706
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
|
|
1707
|
+
}
|
|
1708
|
+
const enumName = builder.enumName;
|
|
1709
|
+
if (enumName !== void 0 && !enumsByName.has(enumName)) {
|
|
1710
|
+
throw new OrmDefinitionError(
|
|
1711
|
+
`view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
|
|
1712
|
+
view.file
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1716
|
+
columns.push(column);
|
|
1717
|
+
}
|
|
1718
|
+
let primaryKey2;
|
|
1719
|
+
if (view.config.primaryKey !== void 0) {
|
|
1720
|
+
if (columnLevelPk.length > 0) {
|
|
1721
|
+
throw new OrmDefinitionError(
|
|
1722
|
+
`view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
|
|
1723
|
+
view.file
|
|
1724
|
+
);
|
|
1725
|
+
}
|
|
1726
|
+
primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
|
|
1727
|
+
} else if (columnLevelPk.length > 0) {
|
|
1728
|
+
primaryKey2 = columnLevelPk;
|
|
1729
|
+
}
|
|
1730
|
+
const links = Object.entries(view.config.links ?? {}).map(
|
|
1731
|
+
([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
|
|
1732
|
+
);
|
|
1733
|
+
return {
|
|
1734
|
+
view: view.viewName,
|
|
1735
|
+
...view.config.description !== void 0 ? { description: view.config.description } : {},
|
|
1736
|
+
columns,
|
|
1737
|
+
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1738
|
+
links,
|
|
1739
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function buildCanonicalLink({
|
|
1743
|
+
relation,
|
|
1744
|
+
linkName,
|
|
1745
|
+
link,
|
|
1746
|
+
relationsByName
|
|
1747
|
+
}) {
|
|
1748
|
+
const ownerKind = relationKind(relation);
|
|
1749
|
+
const ownerName = relationName(relation);
|
|
1750
|
+
const fail = (message) => {
|
|
1751
|
+
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
1752
|
+
};
|
|
1753
|
+
if (link.enforce && link.linkKind !== "toOne") {
|
|
1754
|
+
return fail("sets enforce: true, which is only supported on toOne links");
|
|
1755
|
+
}
|
|
1756
|
+
if (link.linkKind === "toPlatformUser") {
|
|
1757
|
+
if (link.target !== void 0) return fail("is toPlatformUser and must not declare a target table");
|
|
1758
|
+
return {
|
|
1759
|
+
name: linkName,
|
|
1760
|
+
kind: "toPlatformUser",
|
|
1761
|
+
on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
|
|
1762
|
+
...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
|
|
1763
|
+
enforce: false,
|
|
1764
|
+
...link.description !== void 0 ? { description: link.description } : {}
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
const target = resolveRelationRef({ ref: link.target, fail });
|
|
1768
|
+
const targetName = relationName(target);
|
|
1769
|
+
if (!relationsByName.has(targetName)) {
|
|
1770
|
+
return fail(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
1771
|
+
}
|
|
1772
|
+
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
1773
|
+
return fail("cannot enforce a PostgreSQL foreign key involving a view");
|
|
1774
|
+
}
|
|
1775
|
+
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
1776
|
+
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
1777
|
+
return {
|
|
1778
|
+
name: linkName,
|
|
1779
|
+
kind: link.linkKind,
|
|
1780
|
+
target: targetName,
|
|
1781
|
+
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1782
|
+
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
1783
|
+
enforce: link.enforce,
|
|
1784
|
+
...link.description !== void 0 ? { description: link.description } : {}
|
|
1785
|
+
};
|
|
1786
|
+
}
|
|
1787
|
+
function resolveRelationRef({ ref, fail }) {
|
|
1788
|
+
if (ref === void 0) return fail("declares no target relation");
|
|
1789
|
+
if (typeof ref !== "function") return ref;
|
|
1790
|
+
let resolved;
|
|
1791
|
+
try {
|
|
1792
|
+
resolved = ref();
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
return fail(
|
|
1795
|
+
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
1799
|
+
return fail("target thunk did not return a relation defined with defineTable/defineView");
|
|
1800
|
+
}
|
|
1801
|
+
return resolved;
|
|
1802
|
+
}
|
|
1803
|
+
function resolveColumnRef({ relation, ref, role }) {
|
|
1804
|
+
const direct = relation.columns[ref];
|
|
1805
|
+
if (direct) return direct.columnNameFor(ref);
|
|
1806
|
+
for (const [propertyName, builder] of Object.entries(relation.columns)) {
|
|
1807
|
+
if (builder.columnNameFor(propertyName) === ref) return ref;
|
|
1808
|
+
}
|
|
1809
|
+
const known = Object.keys(relation.columns).join(", ");
|
|
1810
|
+
throw new OrmDefinitionError(
|
|
1811
|
+
`${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
|
|
1812
|
+
relation.file
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
function assertAcyclicViewDependencies(views) {
|
|
1816
|
+
const viewNames = new Set(views.map((view) => view.view));
|
|
1817
|
+
const remaining = new Map(
|
|
1818
|
+
views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
|
|
1819
|
+
);
|
|
1820
|
+
while (remaining.size > 0) {
|
|
1821
|
+
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
|
|
1822
|
+
if (ready.length === 0) {
|
|
1823
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
|
|
1824
|
+
}
|
|
1825
|
+
for (const name of ready) remaining.delete(name);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function toCanonicalPolicy(policy) {
|
|
1829
|
+
if (typeof policy === "string") return policy;
|
|
1830
|
+
return {
|
|
1831
|
+
default: policy.default,
|
|
1832
|
+
variants: (policy.variants ?? []).map((variant) => ({
|
|
1833
|
+
when: variant.when,
|
|
1834
|
+
...variant.allow === true ? { allow: true } : {},
|
|
1835
|
+
...variant.policy !== void 0 ? { policy: variant.policy } : {}
|
|
1836
|
+
}))
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
function describeFile(relation) {
|
|
1840
|
+
return relation.file ?? "<unknown file>";
|
|
1841
|
+
}
|
|
1842
|
+
function formatZodIssues(error) {
|
|
1843
|
+
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
1844
|
+
}
|
|
1845
|
+
var SYNCED_CONTRACT_HASH_DOMAIN = "sequence-orm-synced-contract\0";
|
|
1846
|
+
var ORM_TO_PIPELINE_TYPE = {
|
|
1847
|
+
uuid: "uuid",
|
|
1848
|
+
text: "string",
|
|
1849
|
+
integer: "int",
|
|
1850
|
+
bigint: "long",
|
|
1851
|
+
numeric: "numeric",
|
|
1852
|
+
double: "double",
|
|
1853
|
+
boolean: "boolean",
|
|
1854
|
+
timestamptz: "timestamp",
|
|
1855
|
+
date: "date",
|
|
1856
|
+
jsonb: "json"
|
|
1857
|
+
};
|
|
1858
|
+
function isColumnType(value) {
|
|
1859
|
+
return typeof value === "string" && value in ORM_TO_PIPELINE_TYPE;
|
|
1860
|
+
}
|
|
1861
|
+
function syncedContractSurface({
|
|
1862
|
+
namespace,
|
|
1863
|
+
table,
|
|
1864
|
+
columns,
|
|
1865
|
+
primary_key
|
|
1866
|
+
}) {
|
|
1867
|
+
return {
|
|
1868
|
+
namespace,
|
|
1869
|
+
table,
|
|
1870
|
+
columns: Object.fromEntries(
|
|
1871
|
+
Object.entries(columns).map(([name, column]) => [
|
|
1872
|
+
name,
|
|
1873
|
+
{ type: column.type, nullable: column.nullable, key: column.key }
|
|
1874
|
+
])
|
|
1875
|
+
),
|
|
1876
|
+
primary_key: [...primary_key]
|
|
1877
|
+
};
|
|
1878
|
+
}
|
|
1879
|
+
function computeSyncedContractHash(surface) {
|
|
1880
|
+
return crypto.createHash("sha256").update(SYNCED_CONTRACT_HASH_DOMAIN).update(stableJson(surface)).digest("hex");
|
|
1881
|
+
}
|
|
1882
|
+
function extractOrmSyncedTableContract({
|
|
1883
|
+
namespace,
|
|
1884
|
+
table
|
|
1885
|
+
}) {
|
|
1886
|
+
if (!NAMESPACE_PATTERN.test(namespace)) {
|
|
1887
|
+
throw new OrmDefinitionError(
|
|
1888
|
+
`namespace "${namespace}" must match ${NAMESPACE_PATTERN}`
|
|
1889
|
+
);
|
|
1890
|
+
}
|
|
1891
|
+
if (table.source === void 0) {
|
|
1892
|
+
throw new OrmDefinitionError(
|
|
1893
|
+
`table "${table.table}" is not a synced table \u2014 extractOrmSyncedTableContract requires source: gold.*`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
if (table.columns.length === 0) {
|
|
1897
|
+
throw new OrmDefinitionError(
|
|
1898
|
+
`synced table "${table.table}" has no typed columns \u2014 declare a typed defineSyncedTable contract before exporting`
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
const primaryKey2 = table.primaryKey;
|
|
1902
|
+
if (primaryKey2 === void 0 || primaryKey2.length === 0) {
|
|
1903
|
+
throw new OrmDefinitionError(
|
|
1904
|
+
`synced table "${table.table}" has no primary key \u2014 typed synced contracts require a primary key`
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
const pkSet = new Set(primaryKey2);
|
|
1908
|
+
const columns = {};
|
|
1909
|
+
for (const column of table.columns) {
|
|
1910
|
+
if (!isColumnType(column.type)) {
|
|
1911
|
+
const enumName = typeof column.type === "object" && column.type !== null && "enum" in column.type ? column.type.enum : String(column.type);
|
|
1912
|
+
throw new OrmDefinitionError(
|
|
1913
|
+
`synced table "${table.table}": column "${column.columnName}" has enum/type '${enumName}', which has no pipeline-type mapping \u2014 use an ORM-mappable scalar`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
columns[column.columnName] = {
|
|
1917
|
+
type: ORM_TO_PIPELINE_TYPE[column.type],
|
|
1918
|
+
nullable: column.nullable,
|
|
1919
|
+
key: pkSet.has(column.columnName)
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
for (const key of primaryKey2) {
|
|
1923
|
+
if (!(key in columns)) {
|
|
1924
|
+
throw new OrmDefinitionError(
|
|
1925
|
+
`synced table "${table.table}": primary key column "${key}" is not declared in columns`
|
|
1926
|
+
);
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
const surface = syncedContractSurface({
|
|
1930
|
+
namespace,
|
|
1931
|
+
table: table.table,
|
|
1932
|
+
columns,
|
|
1933
|
+
primary_key: primaryKey2
|
|
1934
|
+
});
|
|
1935
|
+
return {
|
|
1936
|
+
namespace,
|
|
1937
|
+
table: table.table,
|
|
1938
|
+
columns,
|
|
1939
|
+
primary_key: [...primaryKey2],
|
|
1940
|
+
content_hash: computeSyncedContractHash(surface)
|
|
1941
|
+
};
|
|
1942
|
+
}
|
|
1943
|
+
var NAMESPACE_CONFIG_FILE = "sequence.config.ts";
|
|
1944
|
+
var SCHEMA_DIR = "schema";
|
|
1945
|
+
async function loadNamespaceFromDir(dir, options = {}) {
|
|
1946
|
+
const root = await promises.realpath(dir).catch(() => {
|
|
1947
|
+
throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
|
|
1948
|
+
});
|
|
1949
|
+
const configPath = path.join(root, NAMESPACE_CONFIG_FILE);
|
|
1950
|
+
const configStat = await promises.stat(configPath).catch(() => void 0);
|
|
1951
|
+
if (!configStat?.isFile()) {
|
|
1952
|
+
throw new OrmDefinitionError(`${NAMESPACE_CONFIG_FILE} not found in ${dir}`);
|
|
1953
|
+
}
|
|
1954
|
+
const schemaFiles = await listSchemaFiles(root);
|
|
1955
|
+
const files = [NAMESPACE_CONFIG_FILE, ...schemaFiles];
|
|
1956
|
+
purgeModuleCache(root);
|
|
1957
|
+
const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)), {
|
|
1958
|
+
interopDefault: true,
|
|
1959
|
+
fsCache: false,
|
|
1960
|
+
...options.ormModulePath !== void 0 ? { alias: { "@sequenceholdings/orm": options.ormModulePath } } : {}
|
|
1961
|
+
});
|
|
1962
|
+
const context = beginAuthoringContext();
|
|
1963
|
+
try {
|
|
1964
|
+
for (const file of files) {
|
|
1965
|
+
context.currentFile = file;
|
|
1966
|
+
try {
|
|
1967
|
+
await jiti$1.import(path.join(root, file));
|
|
1968
|
+
} catch (error) {
|
|
1969
|
+
if (error instanceof Error && error.name === "OrmDefinitionError") throw error;
|
|
1970
|
+
throw new OrmDefinitionError(
|
|
1971
|
+
`failed to load: ${error instanceof Error ? error.message : String(error)}`,
|
|
1972
|
+
file
|
|
1973
|
+
);
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
} finally {
|
|
1977
|
+
context.currentFile = void 0;
|
|
1978
|
+
endAuthoringContext(context);
|
|
1979
|
+
}
|
|
1980
|
+
const configs = [];
|
|
1981
|
+
for (const entry of context.configs) {
|
|
1982
|
+
if (isSeqNamespaceConfig(entry.value)) configs.push({ value: entry.value, file: entry.file });
|
|
1983
|
+
}
|
|
1984
|
+
const configEntry = configs[0];
|
|
1985
|
+
if (!configEntry) {
|
|
1986
|
+
throw new OrmDefinitionError(
|
|
1987
|
+
"no namespace config registered \u2014 sequence.config.ts must call defineNamespace(...) at import time",
|
|
1988
|
+
NAMESPACE_CONFIG_FILE
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
if (configs.length > 1) {
|
|
1992
|
+
const where = configs.map((entry) => entry.file ?? "<unknown file>").join(", ");
|
|
1993
|
+
throw new OrmDefinitionError(
|
|
1994
|
+
`defineNamespace called ${configs.length} times (${where}) \u2014 declare exactly one namespace config`
|
|
1995
|
+
);
|
|
1996
|
+
}
|
|
1997
|
+
const tables = [];
|
|
1998
|
+
for (const entry of context.tables) {
|
|
1999
|
+
if (isSeqTable(entry.value)) tables.push(entry.value);
|
|
2000
|
+
}
|
|
2001
|
+
const views = [];
|
|
2002
|
+
for (const entry of context.views) {
|
|
2003
|
+
if (isSeqView(entry.value)) views.push(entry.value);
|
|
2004
|
+
}
|
|
2005
|
+
if (tables.length === 0 && views.length === 0) {
|
|
2006
|
+
throw new OrmDefinitionError(
|
|
2007
|
+
`no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
2010
|
+
const enums = [];
|
|
2011
|
+
for (const entry of context.enums) {
|
|
2012
|
+
if (isEnumRef(entry.value)) enums.push(entry.value);
|
|
2013
|
+
}
|
|
2014
|
+
const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
|
|
2015
|
+
return { canonical, hash: computeCanonicalHash(canonical), files };
|
|
2016
|
+
}
|
|
2017
|
+
async function listSchemaFiles(root) {
|
|
2018
|
+
const schemaDir = path.join(root, SCHEMA_DIR);
|
|
2019
|
+
const entries = await promises.readdir(schemaDir, { withFileTypes: true }).catch(() => []);
|
|
2020
|
+
return entries.filter(
|
|
2021
|
+
(entry) => entry.isFile() && entry.name.endsWith(".ts") && !entry.name.endsWith(".d.ts") && !entry.name.startsWith(".")
|
|
2022
|
+
).map((entry) => path.join(SCHEMA_DIR, entry.name)).sort();
|
|
2023
|
+
}
|
|
2024
|
+
function purgeModuleCache(root) {
|
|
2025
|
+
const require2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
2026
|
+
const prefix = root.endsWith(path.sep) ? root : `${root}${path.sep}`;
|
|
2027
|
+
for (const key of Object.keys(require2.cache)) {
|
|
2028
|
+
if (key.startsWith(prefix)) delete require2.cache[key];
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
11
2031
|
|
|
12
2032
|
// src/codegen/types.ts
|
|
13
2033
|
var WIRE_TYPES = {
|
|
@@ -29,13 +2049,17 @@ function generateNamespaceTypes(definition) {
|
|
|
29
2049
|
`export type ${typesInterfaceName(definition.namespace)} = {`
|
|
30
2050
|
];
|
|
31
2051
|
for (const table of definition.tables) {
|
|
32
|
-
|
|
2052
|
+
const columns = Object.entries(table.columns ?? {});
|
|
2053
|
+
if (table.source && columns.length === 0) {
|
|
33
2054
|
lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
|
|
34
2055
|
lines.push(` ${table.table}: Record<string, unknown>`);
|
|
35
2056
|
continue;
|
|
36
2057
|
}
|
|
2058
|
+
if (table.source) {
|
|
2059
|
+
lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
|
|
2060
|
+
}
|
|
37
2061
|
lines.push(` ${table.table}: {`);
|
|
38
|
-
for (const [name, column] of
|
|
2062
|
+
for (const [name, column] of columns) {
|
|
39
2063
|
if (column.deprecated) continue;
|
|
40
2064
|
lines.push(` ${name}: ${columnWireType(column)}`);
|
|
41
2065
|
}
|
|
@@ -86,43 +2110,6 @@ function columnWireType(column) {
|
|
|
86
2110
|
return notNull || base === "unknown" ? base : `${base} | null`;
|
|
87
2111
|
}
|
|
88
2112
|
|
|
89
|
-
// src/constants.ts
|
|
90
|
-
var NAMESPACE_PATTERN = /^[a-z][a-z0-9_]{0,40}$/;
|
|
91
|
-
var IDENTIFIER_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
92
|
-
var MAX_IDENTIFIER_LENGTH = 63;
|
|
93
|
-
var ORM_DATA_TOKEN_HEADER = "x-sequence-data-token";
|
|
94
|
-
var ORM_SERVICE_DATA_TOKEN_HEADER = "x-sequence-data-token-service";
|
|
95
|
-
var CAPABILITY_KEY_PATTERN = /^(?:org\/)?[a-z0-9-]{1,64}$/;
|
|
96
|
-
var CLAIM_KEY_PATTERN = /^[a-z][a-z0-9_]*$/;
|
|
97
|
-
var QUALIFIED_TABLE_PATTERN = /^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$/;
|
|
98
|
-
var SYNCED_SOURCE_PATTERN = /^gold\.[a-z][a-z0-9_]*$/;
|
|
99
|
-
var ORM_SCHEMA_PREFIX = "orm_";
|
|
100
|
-
var CONSUMER_ROLE_SUFFIX = "_consumer";
|
|
101
|
-
var ormSchemaName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}`;
|
|
102
|
-
var consumerRoleName = (namespace) => `${ORM_SCHEMA_PREFIX}${namespace}${CONSUMER_ROLE_SUFFIX}`;
|
|
103
|
-
var DEFINITION_FILE_EXTENSIONS = [".yml", ".yaml"];
|
|
104
|
-
var NAMESPACE_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["namespace.yaml", "namespace.yml"]);
|
|
105
|
-
var HASH_DOMAIN = "sequence-orm-definitions\0";
|
|
106
|
-
var MAX_DEFINITION_FILE_BYTES = 1024 * 1024;
|
|
107
|
-
var RESERVED_TABLE_NAMES = /* @__PURE__ */ new Set(["actions", "query"]);
|
|
108
|
-
var RESERVED_COLUMN_NAMES = /* @__PURE__ */ new Set(["select", "order", "limit", "offset"]);
|
|
109
|
-
|
|
110
|
-
// src/errors.ts
|
|
111
|
-
var OrmDefinitionError = class extends Error {
|
|
112
|
-
constructor(message, file) {
|
|
113
|
-
super(file ? `${file}: ${message}` : message);
|
|
114
|
-
this.file = file;
|
|
115
|
-
}
|
|
116
|
-
name = "OrmDefinitionError";
|
|
117
|
-
};
|
|
118
|
-
var PredicateParseError = class extends Error {
|
|
119
|
-
constructor(message, position) {
|
|
120
|
-
super(`${message} (at position ${position})`);
|
|
121
|
-
this.position = position;
|
|
122
|
-
}
|
|
123
|
-
name = "PredicateParseError";
|
|
124
|
-
};
|
|
125
|
-
|
|
126
2113
|
// src/predicate/ast.ts
|
|
127
2114
|
var COMPARE_OPS = ["=", "!=", "<", "<=", ">", ">="];
|
|
128
2115
|
function walkPredicate(predicate, visit) {
|
|
@@ -161,7 +2148,7 @@ var tableReferenceSchema = zod.z.string().refine(
|
|
|
161
2148
|
var isQualifiedReference = (reference) => reference.includes(".");
|
|
162
2149
|
|
|
163
2150
|
// src/schema/columns.ts
|
|
164
|
-
var
|
|
2151
|
+
var COLUMN_TYPES2 = [
|
|
165
2152
|
"uuid",
|
|
166
2153
|
"text",
|
|
167
2154
|
"integer",
|
|
@@ -173,7 +2160,7 @@ var COLUMN_TYPES = [
|
|
|
173
2160
|
"date",
|
|
174
2161
|
"jsonb"
|
|
175
2162
|
];
|
|
176
|
-
var
|
|
2163
|
+
var columnTypeSchema2 = zod.z.enum(COLUMN_TYPES2);
|
|
177
2164
|
var FUNCTION_DEFAULTS = {
|
|
178
2165
|
"gen_random_uuid()": ["uuid"],
|
|
179
2166
|
"now()": ["timestamptz", "date"]
|
|
@@ -202,7 +2189,7 @@ function defaultIssue(value, type) {
|
|
|
202
2189
|
return null;
|
|
203
2190
|
}
|
|
204
2191
|
var columnSchema = zod.z.strictObject({
|
|
205
|
-
type:
|
|
2192
|
+
type: columnTypeSchema2,
|
|
206
2193
|
primary_key: zod.z.boolean().optional(),
|
|
207
2194
|
nullable: zod.z.boolean().optional(),
|
|
208
2195
|
unique: zod.z.boolean().optional(),
|
|
@@ -469,7 +2456,7 @@ var NAMED_PARAM_PATTERN = /(?<!:):([a-z][a-z0-9_]*)/g;
|
|
|
469
2456
|
var QUOTED_STRING_PATTERN = /'(?:[^']|'')*'/g;
|
|
470
2457
|
var SQL_COMMENT_PATTERN = /--|\/\*/;
|
|
471
2458
|
var actionParamSchema = zod.z.strictObject({
|
|
472
|
-
type:
|
|
2459
|
+
type: columnTypeSchema2,
|
|
473
2460
|
optional: zod.z.boolean().optional(),
|
|
474
2461
|
description: zod.z.string().optional()
|
|
475
2462
|
});
|
|
@@ -502,7 +2489,7 @@ var actionSchema = zod.z.strictObject({
|
|
|
502
2489
|
// function returns those rows (typed in the client); absent → the action
|
|
503
2490
|
// reports rows_affected only. The column names/types must match the
|
|
504
2491
|
// RETURNING list, which Postgres enforces at CREATE FUNCTION.
|
|
505
|
-
returns: zod.z.record(identifierSchema,
|
|
2492
|
+
returns: zod.z.record(identifierSchema, columnTypeSchema2).optional(),
|
|
506
2493
|
audit: auditSchema.optional()
|
|
507
2494
|
}).superRefine((action, ctx) => {
|
|
508
2495
|
const params = Object.keys(action.params ?? {});
|
|
@@ -519,18 +2506,18 @@ var actionSchema = zod.z.strictObject({
|
|
|
519
2506
|
if (action.returns && Object.keys(action.returns).length === 0) {
|
|
520
2507
|
ctx.addIssue({ code: "custom", message: "returns must declare at least one column", path: ["returns"] });
|
|
521
2508
|
}
|
|
522
|
-
for (const [
|
|
2509
|
+
for (const [index3, entry] of (action.validates ?? []).entries()) {
|
|
523
2510
|
if (!params.includes(entry.param)) {
|
|
524
2511
|
ctx.addIssue({
|
|
525
2512
|
code: "custom",
|
|
526
2513
|
message: `validates references undeclared param '${entry.param}'`,
|
|
527
|
-
path: ["validates",
|
|
2514
|
+
path: ["validates", index3]
|
|
528
2515
|
});
|
|
529
2516
|
}
|
|
530
2517
|
}
|
|
531
2518
|
});
|
|
532
|
-
function extractNamedParams(
|
|
533
|
-
const withoutStrings =
|
|
2519
|
+
function extractNamedParams(sql4) {
|
|
2520
|
+
const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
|
|
534
2521
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
535
2522
|
return [...new Set(names)];
|
|
536
2523
|
}
|
|
@@ -563,9 +2550,9 @@ function actionWriteTargets(action) {
|
|
|
563
2550
|
function executesIssues(executes, declaredParams, hasReturns) {
|
|
564
2551
|
const issues = [];
|
|
565
2552
|
const statements = actionStatementSources(executes);
|
|
566
|
-
statements.forEach((statement,
|
|
2553
|
+
statements.forEach((statement, index3) => {
|
|
567
2554
|
const withoutStrings = statement.replace(QUOTED_STRING_PATTERN, "''");
|
|
568
|
-
const at = statements.length > 1 ? ` (statement ${
|
|
2555
|
+
const at = statements.length > 1 ? ` (statement ${index3 + 1})` : "";
|
|
569
2556
|
if (!EXECUTES_VERB_PATTERN.test(statement)) issues.push(`executes must be INSERT, UPDATE, or DELETE${at}`);
|
|
570
2557
|
if (withoutStrings.replace(/;\s*$/, "").includes(";")) issues.push(`each executes entry must be a single statement${at}`);
|
|
571
2558
|
if (SQL_COMMENT_PATTERN.test(withoutStrings)) issues.push(`executes must not contain SQL comments${at}`);
|
|
@@ -575,7 +2562,7 @@ function executesIssues(executes, declaredParams, hasReturns) {
|
|
|
575
2562
|
if (BARE_CLAIM_CALL_PATTERN.test(withoutStrings.replace(MASKED_CLAIM_CALL_PATTERN, ""))) {
|
|
576
2563
|
issues.push(`executes has a malformed claim()/capability() call \u2014 use e.g. claim('sub')${at}`);
|
|
577
2564
|
}
|
|
578
|
-
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns &&
|
|
2565
|
+
if (RETURNING_PATTERN.test(withoutStrings) && !(hasReturns && index3 === statements.length - 1)) {
|
|
579
2566
|
issues.push(
|
|
580
2567
|
hasReturns ? `only the last executes statement may use RETURNING${at}` : `executes must not use RETURNING unless the action declares returns${at}`
|
|
581
2568
|
);
|
|
@@ -600,11 +2587,13 @@ var sqlTypeName = (type) => SQL_TYPE_NAMES[type] ?? type;
|
|
|
600
2587
|
// src/compiler/sql.ts
|
|
601
2588
|
var CLAIMS_SETTING = "request.jwt.claims";
|
|
602
2589
|
var CAPABILITIES_CLAIM_KEY = "capabilities";
|
|
2590
|
+
var ROLES_CLAIM_KEY = "roles";
|
|
603
2591
|
var quoteIdentifier = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
604
2592
|
var quoteLiteral = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
605
2593
|
var CLAIMS_SOURCE = `nullif(current_setting('${CLAIMS_SETTING}', true), '')::jsonb`;
|
|
606
2594
|
var claimExpression = (key) => `(${CLAIMS_SOURCE} ->> ${quoteLiteral(key)})`;
|
|
607
2595
|
var capabilityExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${CAPABILITIES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
2596
|
+
var roleExpression = (key) => `coalesce(${CLAIMS_SOURCE} -> '${ROLES_CLAIM_KEY}' ? ${quoteLiteral(key)}, false)`;
|
|
608
2597
|
|
|
609
2598
|
// src/compiler/predicates.ts
|
|
610
2599
|
var SQL_COMPARE_OPS = {
|
|
@@ -685,8 +2674,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
685
2674
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
686
2675
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
687
2676
|
}
|
|
688
|
-
function compileStatement(
|
|
689
|
-
const compiled =
|
|
2677
|
+
function compileStatement(sql4, renderParam) {
|
|
2678
|
+
const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
690
2679
|
if (claimKey !== void 0) {
|
|
691
2680
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
692
2681
|
return claimExpression(claimKey);
|
|
@@ -718,6 +2707,118 @@ function referencedExecuteCapabilities(action) {
|
|
|
718
2707
|
return keys;
|
|
719
2708
|
}
|
|
720
2709
|
|
|
2710
|
+
// src/compiler/pred-ir-sql.ts
|
|
2711
|
+
function compilePredIrToSql(pred, options = {}) {
|
|
2712
|
+
switch (pred.kind) {
|
|
2713
|
+
case "const":
|
|
2714
|
+
return pred.value ? "TRUE" : "FALSE";
|
|
2715
|
+
case "and":
|
|
2716
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" AND ")})`;
|
|
2717
|
+
case "or":
|
|
2718
|
+
return `(${pred.operands.map((operand) => compilePredIrToSql(operand, options)).join(" OR ")})`;
|
|
2719
|
+
case "not":
|
|
2720
|
+
return `(NOT ${compilePredIrToSql(pred.operand, options)})`;
|
|
2721
|
+
case "cmp":
|
|
2722
|
+
return compileCmp(pred.op, pred.column, pred.value, options);
|
|
2723
|
+
case "capability":
|
|
2724
|
+
return `(${capabilityExpression(pred.key)})`;
|
|
2725
|
+
case "role":
|
|
2726
|
+
return `(${roleExpression(pred.key)})`;
|
|
2727
|
+
case "exists":
|
|
2728
|
+
case "aggPredicate":
|
|
2729
|
+
case "dynamicFilter":
|
|
2730
|
+
throw new OrmDefinitionError(
|
|
2731
|
+
`synced view read policy cannot use '${pred.kind}' \u2014 use row filters and claim/capability/role checks only`
|
|
2732
|
+
);
|
|
2733
|
+
default: {
|
|
2734
|
+
const exhausted = pred;
|
|
2735
|
+
throw new OrmDefinitionError(`unsupported synced view predicate kind '${String(exhausted)}'`);
|
|
2736
|
+
}
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
function physicalColumn(logical, options) {
|
|
2740
|
+
return options.columnNames?.[logical] ?? logical;
|
|
2741
|
+
}
|
|
2742
|
+
function compileCmp(op, column, value, options) {
|
|
2743
|
+
const physical = physicalColumn(column, options);
|
|
2744
|
+
const declared = options.columnTypes?.[physical] ?? options.columnTypes?.[column];
|
|
2745
|
+
const isEnum = typeof declared === "object" && declared !== null;
|
|
2746
|
+
const col = isEnum ? `${quoteIdentifier(physical)}::text` : quoteIdentifier(physical);
|
|
2747
|
+
const columnType = isEnum ? "text" : declared;
|
|
2748
|
+
switch (op) {
|
|
2749
|
+
case "eq":
|
|
2750
|
+
return compileEquality(col, value, columnType, false);
|
|
2751
|
+
case "neq":
|
|
2752
|
+
return compileEquality(col, value, columnType, true);
|
|
2753
|
+
case "gt":
|
|
2754
|
+
return `${col} > ${compileValue(value, columnType)}`;
|
|
2755
|
+
case "gte":
|
|
2756
|
+
return `${col} >= ${compileValue(value, columnType)}`;
|
|
2757
|
+
case "lt":
|
|
2758
|
+
return `${col} < ${compileValue(value, columnType)}`;
|
|
2759
|
+
case "lte":
|
|
2760
|
+
return `${col} <= ${compileValue(value, columnType)}`;
|
|
2761
|
+
case "in":
|
|
2762
|
+
return `${col} = ANY(${compileArrayValue(value, columnType)})`;
|
|
2763
|
+
case "nin":
|
|
2764
|
+
return `${col} <> ALL(${compileArrayValue(value, columnType)})`;
|
|
2765
|
+
case "like":
|
|
2766
|
+
return `${col} LIKE ${compileValue(value, columnType)}`;
|
|
2767
|
+
case "ilike":
|
|
2768
|
+
return `${col} ILIKE ${compileValue(value, columnType)}`;
|
|
2769
|
+
case "isNull":
|
|
2770
|
+
return value.kind === "literal" && value.value === true ? `${col} IS NULL` : `${col} IS NOT NULL`;
|
|
2771
|
+
case "contains":
|
|
2772
|
+
case "hasKey":
|
|
2773
|
+
throw new OrmDefinitionError(`synced view read policy cannot use json operator '${op}'`);
|
|
2774
|
+
default: {
|
|
2775
|
+
const exhausted = op;
|
|
2776
|
+
throw new OrmDefinitionError(`unsupported synced view comparison '${String(exhausted)}'`);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
function compileEquality(col, value, columnType, negated, options) {
|
|
2781
|
+
if (value.kind === "literal" && value.value === null) {
|
|
2782
|
+
return negated ? `${col} IS NOT NULL` : `${col} IS NULL`;
|
|
2783
|
+
}
|
|
2784
|
+
const rhs = compileValue(value, columnType);
|
|
2785
|
+
return negated ? `${col} <> ${rhs}` : `${col} = ${rhs}`;
|
|
2786
|
+
}
|
|
2787
|
+
function compileValue(value, columnType, _options) {
|
|
2788
|
+
switch (value.kind) {
|
|
2789
|
+
case "literal": {
|
|
2790
|
+
const literal = value.value;
|
|
2791
|
+
if (literal === null) return "NULL";
|
|
2792
|
+
if (typeof literal === "string") return quoteLiteral(literal);
|
|
2793
|
+
if (typeof literal === "boolean") return literal ? "TRUE" : "FALSE";
|
|
2794
|
+
return String(literal);
|
|
2795
|
+
}
|
|
2796
|
+
case "claim": {
|
|
2797
|
+
const expression = claimExpression(value.key);
|
|
2798
|
+
return columnType && columnType !== "text" ? `(${expression})::${sqlTypeName(columnType)}` : expression;
|
|
2799
|
+
}
|
|
2800
|
+
case "variable":
|
|
2801
|
+
throw new OrmDefinitionError("synced view read policy cannot reference operation variables");
|
|
2802
|
+
default: {
|
|
2803
|
+
const exhausted = value;
|
|
2804
|
+
throw new OrmDefinitionError(`unsupported synced view value kind '${String(exhausted)}'`);
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
function compileArrayValue(value, columnType, _options) {
|
|
2809
|
+
if (value.kind !== "literal" || !Array.isArray(value.value)) {
|
|
2810
|
+
throw new OrmDefinitionError("synced view IN/NIN comparisons require literal arrays");
|
|
2811
|
+
}
|
|
2812
|
+
const entries = value.value.map((entry) => {
|
|
2813
|
+
if (entry === null) return "NULL";
|
|
2814
|
+
if (typeof entry === "string") return quoteLiteral(entry);
|
|
2815
|
+
if (typeof entry === "boolean") return entry ? "TRUE" : "FALSE";
|
|
2816
|
+
return String(entry);
|
|
2817
|
+
});
|
|
2818
|
+
const array = `ARRAY[${entries.join(", ")}]`;
|
|
2819
|
+
return columnType ? `${array}::${sqlTypeName(columnType)}[]` : array;
|
|
2820
|
+
}
|
|
2821
|
+
|
|
721
2822
|
// src/engine/actions.ts
|
|
722
2823
|
var ACTION_REQUIRES_DENIED_SQLSTATE = "ORMRQ";
|
|
723
2824
|
var ACTION_VALIDATES_FAILED_SQLSTATE = "ORMVL";
|
|
@@ -739,9 +2840,9 @@ function actionPrepareChecks(definition) {
|
|
|
739
2840
|
const order = actionParamOrder(action);
|
|
740
2841
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
741
2842
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
742
|
-
compiled.forEach((
|
|
743
|
-
const checked = action.returns &&
|
|
744
|
-
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${
|
|
2843
|
+
compiled.forEach((sql4, index3) => {
|
|
2844
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
|
|
2845
|
+
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
745
2846
|
});
|
|
746
2847
|
}
|
|
747
2848
|
return statements;
|
|
@@ -856,10 +2957,12 @@ var indexSchema = zod.z.union([
|
|
|
856
2957
|
unique: zod.z.boolean().optional(),
|
|
857
2958
|
name: identifierSchema.optional()
|
|
858
2959
|
})
|
|
859
|
-
]).transform((
|
|
2960
|
+
]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
|
|
860
2961
|
var policiesSchema = zod.z.strictObject({
|
|
861
2962
|
read: parsedPredicateSchema.optional(),
|
|
862
|
-
write: parsedPredicateSchema.optional()
|
|
2963
|
+
write: parsedPredicateSchema.optional(),
|
|
2964
|
+
insert: parsedPredicateSchema.optional(),
|
|
2965
|
+
update: parsedPredicateSchema.optional()
|
|
863
2966
|
});
|
|
864
2967
|
var tableFileSchema = zod.z.strictObject({
|
|
865
2968
|
namespace: namespaceSchema,
|
|
@@ -895,7 +2998,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
|
895
2998
|
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
896
2999
|
function validateSyncedTable(table, ctx) {
|
|
897
3000
|
const forbidden = [
|
|
898
|
-
["columns", "synced tables reflect their source; columns are not declared"],
|
|
899
3001
|
["actions", "synced tables are read-only; actions are not allowed"],
|
|
900
3002
|
["indexes", "synced tables are system-owned; indexes are not allowed"]
|
|
901
3003
|
];
|
|
@@ -905,6 +3007,39 @@ function validateSyncedTable(table, ctx) {
|
|
|
905
3007
|
if (table.policies?.write) {
|
|
906
3008
|
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
907
3009
|
}
|
|
3010
|
+
if (table.policies?.insert) {
|
|
3011
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
3012
|
+
}
|
|
3013
|
+
if (table.policies?.update) {
|
|
3014
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
3015
|
+
}
|
|
3016
|
+
const columns = table.columns ?? {};
|
|
3017
|
+
const names = Object.keys(columns);
|
|
3018
|
+
if (names.length === 0) return;
|
|
3019
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
3020
|
+
if (primaryKeys.length !== 1) {
|
|
3021
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
3022
|
+
}
|
|
3023
|
+
validateRenames(columns, names, ctx);
|
|
3024
|
+
for (const name of names) {
|
|
3025
|
+
const column = columns[name];
|
|
3026
|
+
if (!column) continue;
|
|
3027
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
3028
|
+
if (column.cast && cast?.column !== name) {
|
|
3029
|
+
ctx.addIssue({
|
|
3030
|
+
code: "custom",
|
|
3031
|
+
message: "cast must reference the column it is declared on",
|
|
3032
|
+
path: ["columns", name, "cast"]
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
if (cast && cast.type !== column.type) {
|
|
3036
|
+
ctx.addIssue({
|
|
3037
|
+
code: "custom",
|
|
3038
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
3039
|
+
path: ["columns", name, "cast"]
|
|
3040
|
+
});
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
908
3043
|
}
|
|
909
3044
|
function validateRenames(columns, names, ctx) {
|
|
910
3045
|
const renameSources = /* @__PURE__ */ new Map();
|
|
@@ -960,15 +3095,15 @@ function validateColumnTable(table, ctx) {
|
|
|
960
3095
|
});
|
|
961
3096
|
}
|
|
962
3097
|
}
|
|
963
|
-
for (const [position,
|
|
964
|
-
if (new Set(
|
|
3098
|
+
for (const [position, index3] of (table.indexes ?? []).entries()) {
|
|
3099
|
+
if (new Set(index3.columns).size !== index3.columns.length) {
|
|
965
3100
|
ctx.addIssue({
|
|
966
3101
|
code: "custom",
|
|
967
3102
|
message: "index names a column more than once",
|
|
968
3103
|
path: ["indexes", position]
|
|
969
3104
|
});
|
|
970
3105
|
}
|
|
971
|
-
for (const column of
|
|
3106
|
+
for (const column of index3.columns) {
|
|
972
3107
|
if (!names.includes(column)) {
|
|
973
3108
|
ctx.addIssue({
|
|
974
3109
|
code: "custom",
|
|
@@ -982,7 +3117,7 @@ function validateColumnTable(table, ctx) {
|
|
|
982
3117
|
function validateLinks(table, ctx) {
|
|
983
3118
|
const columnNames = Object.keys(table.columns ?? {});
|
|
984
3119
|
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
985
|
-
if (
|
|
3120
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
986
3121
|
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
|
|
987
3122
|
}
|
|
988
3123
|
if (link.enforce && link.to.includes(".")) {
|
|
@@ -1010,8 +3145,8 @@ function validateDerivedNameLengths(table, ctx) {
|
|
|
1010
3145
|
path: ["table"]
|
|
1011
3146
|
});
|
|
1012
3147
|
}
|
|
1013
|
-
for (const [position,
|
|
1014
|
-
const name = indexName(table.table,
|
|
3148
|
+
for (const [position, index3] of (table.indexes ?? []).entries()) {
|
|
3149
|
+
const name = indexName(table.table, index3);
|
|
1015
3150
|
if (name.length > MAX_IDENTIFIER_LENGTH) {
|
|
1016
3151
|
ctx.addIssue({
|
|
1017
3152
|
code: "custom",
|
|
@@ -1055,10 +3190,10 @@ var TYPE_GROUPS = {
|
|
|
1055
3190
|
};
|
|
1056
3191
|
var columnTypeGroup = (type) => TYPE_GROUPS[type];
|
|
1057
3192
|
function validatePolicyColumns(table, ctx) {
|
|
1058
|
-
if (table.source) return;
|
|
1059
3193
|
const columns = table.columns ?? {};
|
|
1060
3194
|
const columnNames = Object.keys(columns);
|
|
1061
|
-
|
|
3195
|
+
if (columnNames.length === 0) return;
|
|
3196
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
1062
3197
|
const predicate = table.policies?.[kind];
|
|
1063
3198
|
if (!predicate) continue;
|
|
1064
3199
|
for (const column of referencedColumns(predicate.ast)) {
|
|
@@ -1135,10 +3270,15 @@ function policyStatements(schema, table) {
|
|
|
1135
3270
|
if (read) {
|
|
1136
3271
|
statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
|
|
1137
3272
|
}
|
|
1138
|
-
const
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
statements.push(
|
|
3273
|
+
const insertPolicy = table.policies?.insert ?? table.policies?.write;
|
|
3274
|
+
const updatePolicy = table.policies?.update ?? table.policies?.write;
|
|
3275
|
+
if (insertPolicy) {
|
|
3276
|
+
statements.push(
|
|
3277
|
+
`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
if (updatePolicy) {
|
|
3281
|
+
const predicate = compilePredicate(updatePolicy.ast, columnTypes);
|
|
1142
3282
|
statements.push(
|
|
1143
3283
|
`CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
|
|
1144
3284
|
);
|
|
@@ -1147,6 +3287,118 @@ function policyStatements(schema, table) {
|
|
|
1147
3287
|
return statements;
|
|
1148
3288
|
}
|
|
1149
3289
|
var columnTables = (definition) => definition.tables.filter((table) => !table.source);
|
|
3290
|
+
var syncedTables = (definition) => definition.tables.filter((table) => !!table.source);
|
|
3291
|
+
var SYNCED_VIEW_EXECUTION_OWNER_ROLE = "orm_exec";
|
|
3292
|
+
function syncedViewStatements({
|
|
3293
|
+
definition,
|
|
3294
|
+
backings,
|
|
3295
|
+
readPolicyIrByTable,
|
|
3296
|
+
columnNamesByTable,
|
|
3297
|
+
columnTypesByTable,
|
|
3298
|
+
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
|
|
3299
|
+
}) {
|
|
3300
|
+
const schema = quoteIdentifier(ormSchemaName(definition.namespace));
|
|
3301
|
+
const role = quoteIdentifier(consumerRoleName(definition.namespace));
|
|
3302
|
+
const owner = quoteIdentifier(executionOwnerRole);
|
|
3303
|
+
const backingMap = /* @__PURE__ */ new Map();
|
|
3304
|
+
for (const backing of backings) {
|
|
3305
|
+
const existing = backingMap.get(backing.table);
|
|
3306
|
+
if (existing) {
|
|
3307
|
+
throw new OrmDefinitionError(
|
|
3308
|
+
`duplicate synced backing for table "${backing.table}" ('${existing.physicalRelation}' and '${backing.physicalRelation}') \u2014 refusing to materialize an ambiguous authorization boundary`
|
|
3309
|
+
);
|
|
3310
|
+
}
|
|
3311
|
+
backingMap.set(backing.table, backing);
|
|
3312
|
+
}
|
|
3313
|
+
const statements = [];
|
|
3314
|
+
for (const table of syncedTables(definition)) {
|
|
3315
|
+
const backing = backingMap.get(table.table);
|
|
3316
|
+
if (!backing) continue;
|
|
3317
|
+
const viewName = `${schema}.${quoteIdentifier(table.table)}`;
|
|
3318
|
+
const physical = splitPhysicalRelation(backing.physicalRelation);
|
|
3319
|
+
const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
|
|
3320
|
+
const columnTypes = mergeSyncedViewColumnTypes({
|
|
3321
|
+
table,
|
|
3322
|
+
canonicalColumnTypes: columnTypesByTable?.get(table.table),
|
|
3323
|
+
backingColumnTypes: backing.columnTypes,
|
|
3324
|
+
columnNames: columnNamesByTable?.get(table.table)
|
|
3325
|
+
});
|
|
3326
|
+
const readPolicyIr = readPolicyIrByTable?.get(table.table);
|
|
3327
|
+
const readPolicy = table.policies?.read;
|
|
3328
|
+
const whereClause = readPolicyIr ? ` WHERE ${compilePredIrToSql(readPolicyIr, {
|
|
3329
|
+
columnTypes,
|
|
3330
|
+
columnNames: columnNamesByTable?.get(table.table)
|
|
3331
|
+
})}` : readPolicy ? ` WHERE ${compilePredicate(readPolicy.ast, narrowToV1ColumnTypes(columnTypes))}` : " WHERE false";
|
|
3332
|
+
statements.push(`GRANT USAGE ON SCHEMA ${quoteIdentifier(physical.schema)} TO ${owner}`);
|
|
3333
|
+
statements.push(`GRANT SELECT ON ${physicalTarget} TO ${owner}`);
|
|
3334
|
+
statements.push(
|
|
3335
|
+
`CREATE OR REPLACE VIEW ${viewName} AS SELECT * FROM ${physicalTarget}${whereClause}`
|
|
3336
|
+
);
|
|
3337
|
+
statements.push(`GRANT SELECT ON ${viewName} TO ${role}`);
|
|
3338
|
+
}
|
|
3339
|
+
return statements;
|
|
3340
|
+
}
|
|
3341
|
+
function mergeSyncedViewColumnTypes({
|
|
3342
|
+
table,
|
|
3343
|
+
canonicalColumnTypes,
|
|
3344
|
+
backingColumnTypes,
|
|
3345
|
+
columnNames
|
|
3346
|
+
}) {
|
|
3347
|
+
const fromTable = {};
|
|
3348
|
+
for (const [name, def] of Object.entries(table.columns ?? {})) {
|
|
3349
|
+
if (def?.type) fromTable[name] = def.type;
|
|
3350
|
+
}
|
|
3351
|
+
if (columnNames) {
|
|
3352
|
+
for (const [logical, physical] of Object.entries(columnNames)) {
|
|
3353
|
+
const declared = fromTable[physical] ?? fromTable[logical];
|
|
3354
|
+
if (!declared) continue;
|
|
3355
|
+
fromTable[physical] = declared;
|
|
3356
|
+
fromTable[logical] = declared;
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
const canonicalEnums = Object.fromEntries(
|
|
3360
|
+
Object.entries(canonicalColumnTypes ?? {}).filter(([, type]) => typeof type === "object")
|
|
3361
|
+
);
|
|
3362
|
+
return { ...fromTable, ...canonicalColumnTypes ?? {}, ...backingColumnTypes ?? {}, ...canonicalEnums };
|
|
3363
|
+
}
|
|
3364
|
+
function narrowToV1ColumnTypes(columnTypes) {
|
|
3365
|
+
return Object.fromEntries(
|
|
3366
|
+
Object.entries(columnTypes).map(([column, type]) => [column, typeof type === "object" ? "text" : type])
|
|
3367
|
+
);
|
|
3368
|
+
}
|
|
3369
|
+
function splitPhysicalRelation(physicalRelation) {
|
|
3370
|
+
const parts = physicalRelation.split(".");
|
|
3371
|
+
if (parts.length === 1 && parts[0]) return { schema: "public", table: parts[0] };
|
|
3372
|
+
if (parts.length === 2 && parts[0] && parts[1]) return { schema: parts[0], table: parts[1] };
|
|
3373
|
+
throw new OrmDefinitionError(
|
|
3374
|
+
`synced view physical relation must be "table" or "schema.table" (got "${physicalRelation}")`
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
function syncedViewDropStatements({
|
|
3378
|
+
schemaName,
|
|
3379
|
+
tables
|
|
3380
|
+
}) {
|
|
3381
|
+
const schema = quoteIdentifier(schemaName);
|
|
3382
|
+
return tables.map(
|
|
3383
|
+
(table) => `DROP VIEW IF EXISTS ${schema}.${quoteIdentifier(table)} CASCADE`
|
|
3384
|
+
);
|
|
3385
|
+
}
|
|
3386
|
+
function syncedBackingRevokeStatements({
|
|
3387
|
+
backings,
|
|
3388
|
+
executionOwnerRole = SYNCED_VIEW_EXECUTION_OWNER_ROLE
|
|
3389
|
+
}) {
|
|
3390
|
+
const owner = quoteIdentifier(executionOwnerRole);
|
|
3391
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3392
|
+
const statements = [];
|
|
3393
|
+
for (const backing of backings) {
|
|
3394
|
+
const physical = splitPhysicalRelation(backing.physicalRelation);
|
|
3395
|
+
const physicalTarget = `${quoteIdentifier(physical.schema)}.${quoteIdentifier(physical.table)}`;
|
|
3396
|
+
if (seen.has(physicalTarget)) continue;
|
|
3397
|
+
seen.add(physicalTarget);
|
|
3398
|
+
statements.push(`REVOKE SELECT ON ${physicalTarget} FROM ${owner}`);
|
|
3399
|
+
}
|
|
3400
|
+
return statements;
|
|
3401
|
+
}
|
|
1150
3402
|
var FACTORIES = {
|
|
1151
3403
|
uuid: (name) => pgCore.uuid(name),
|
|
1152
3404
|
text: (name) => pgCore.text(name),
|
|
@@ -1251,9 +3503,9 @@ function sortByEnforcedLinks(tables) {
|
|
|
1251
3503
|
var enforcedTargets = (table) => Object.values(table.links ?? {}).filter((link) => link.enforce && !isQualifiedReference(link.to) && link.to !== table.table).map((link) => link.to);
|
|
1252
3504
|
|
|
1253
3505
|
// src/engine/snapshot.ts
|
|
1254
|
-
function generateSnapshot(definition,
|
|
3506
|
+
function generateSnapshot(definition, previousId3) {
|
|
1255
3507
|
const { imports, schemaName } = buildDrizzleEntities(definition);
|
|
1256
|
-
return api.generateDrizzleJson(imports,
|
|
3508
|
+
return api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
1257
3509
|
}
|
|
1258
3510
|
var emptySnapshot = (namespace) => api.generateDrizzleJson({}, void 0, [ormSchemaName(namespace)]);
|
|
1259
3511
|
var CURRENT_SNAPSHOT_VERSION = api.generateDrizzleJson({}).version;
|
|
@@ -1340,16 +3592,824 @@ function applyMigrationHints(previous, definition) {
|
|
|
1340
3592
|
}
|
|
1341
3593
|
}
|
|
1342
3594
|
}
|
|
1343
|
-
return { statements, adjusted };
|
|
3595
|
+
return { statements, adjusted };
|
|
3596
|
+
}
|
|
3597
|
+
function findTable(snapshot, schema, table) {
|
|
3598
|
+
const tables = snapshot.tables ?? {};
|
|
3599
|
+
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
3600
|
+
}
|
|
3601
|
+
function snapshotTableNames(snapshot, schema) {
|
|
3602
|
+
const tables = snapshot.tables ?? {};
|
|
3603
|
+
return Object.values(tables).filter((entry) => entry.schema === schema).map((entry) => entry.name);
|
|
3604
|
+
}
|
|
3605
|
+
var FACTORIES2 = {
|
|
3606
|
+
uuid: (name) => pgCore.uuid(name),
|
|
3607
|
+
text: (name) => pgCore.text(name),
|
|
3608
|
+
integer: (name) => pgCore.integer(name),
|
|
3609
|
+
// Mode 'bigint' keeps int8 lossless — mode 'number' silently rounds past
|
|
3610
|
+
// 2^53. The read executor serializes BigInt to string for JSON.
|
|
3611
|
+
bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
|
|
3612
|
+
numeric: (name) => pgCore.numeric(name),
|
|
3613
|
+
double: (name) => pgCore.doublePrecision(name),
|
|
3614
|
+
boolean: (name) => pgCore.boolean(name),
|
|
3615
|
+
timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
|
|
3616
|
+
date: (name) => pgCore.date(name),
|
|
3617
|
+
jsonb: (name) => pgCore.jsonb(name)
|
|
3618
|
+
};
|
|
3619
|
+
var SCHEMA_IMPORT_KEY2 = "$schema";
|
|
3620
|
+
var enumImportKey = (name) => `$enum_${name}`;
|
|
3621
|
+
var columnTablesV2 = (definition) => definition.tables.filter((table) => !table.source);
|
|
3622
|
+
function buildDrizzleEntitiesV2(definition) {
|
|
3623
|
+
const schemaName = ormSchemaName(definition.namespace);
|
|
3624
|
+
const schema = pgCore.pgSchema(schemaName);
|
|
3625
|
+
const imports = { [SCHEMA_IMPORT_KEY2]: schema };
|
|
3626
|
+
const tables = {};
|
|
3627
|
+
const built = /* @__PURE__ */ new Map();
|
|
3628
|
+
const enums = /* @__PURE__ */ new Map();
|
|
3629
|
+
for (const enumDefinition of definition.enums) {
|
|
3630
|
+
const values = enumDefinition.values;
|
|
3631
|
+
const builtEnum = schema.enum(enumDefinition.name, values);
|
|
3632
|
+
enums.set(enumDefinition.name, builtEnum);
|
|
3633
|
+
imports[enumImportKey(enumDefinition.name)] = builtEnum;
|
|
3634
|
+
}
|
|
3635
|
+
const byName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
3636
|
+
for (const table of sortByEnforcedLinks2(columnTablesV2(definition))) {
|
|
3637
|
+
const drizzleTable = buildTable2(schema, table, built, enums, byName);
|
|
3638
|
+
built.set(table.table, drizzleTable);
|
|
3639
|
+
imports[table.table] = drizzleTable;
|
|
3640
|
+
tables[table.table] = drizzleTable;
|
|
3641
|
+
}
|
|
3642
|
+
for (const view of definition.views ?? []) {
|
|
3643
|
+
const columns = Object.fromEntries(
|
|
3644
|
+
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
3645
|
+
);
|
|
3646
|
+
imports[view.view] = schema.view(view.view, columns).as(
|
|
3647
|
+
drizzleOrm.sql.raw(
|
|
3648
|
+
rewriteSyncedSourceRelationsInViewQuery({
|
|
3649
|
+
query: view.query,
|
|
3650
|
+
canonical: definition,
|
|
3651
|
+
schemaName,
|
|
3652
|
+
dependencies: view.dependencies
|
|
3653
|
+
})
|
|
3654
|
+
)
|
|
3655
|
+
);
|
|
3656
|
+
}
|
|
3657
|
+
return { imports, tables, schemaName };
|
|
3658
|
+
}
|
|
3659
|
+
function rewriteSyncedSourceRelationsInViewQuery({
|
|
3660
|
+
query,
|
|
3661
|
+
canonical,
|
|
3662
|
+
schemaName,
|
|
3663
|
+
dependencies
|
|
3664
|
+
}) {
|
|
3665
|
+
const tablesBySource = /* @__PURE__ */ new Map();
|
|
3666
|
+
for (const table of canonical.tables) {
|
|
3667
|
+
if (!table.source) continue;
|
|
3668
|
+
tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
|
|
3669
|
+
}
|
|
3670
|
+
const declared = new Set(dependencies ?? []);
|
|
3671
|
+
const boundTableBySource = /* @__PURE__ */ new Map();
|
|
3672
|
+
for (const [source, tables] of tablesBySource) {
|
|
3673
|
+
if (tables.length === 1) {
|
|
3674
|
+
boundTableBySource.set(source, tables[0]);
|
|
3675
|
+
continue;
|
|
3676
|
+
}
|
|
3677
|
+
const [sourceSchema, sourceName] = source.split(".");
|
|
3678
|
+
if (!sourceSchema || !sourceName) continue;
|
|
3679
|
+
const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
|
|
3680
|
+
if (!referenced) continue;
|
|
3681
|
+
const declaredMatches = tables.filter((table) => declared.has(table));
|
|
3682
|
+
if (declaredMatches.length === 1) {
|
|
3683
|
+
boundTableBySource.set(source, declaredMatches[0]);
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
throw new OrmDefinitionError(
|
|
3687
|
+
`synced source '${source}' is declared by multiple synced tables (${[...tables].sort().join(", ")}) \u2014 this view references it but its declared dependencies do not name exactly one of them, so the rewrite is ambiguous`
|
|
3688
|
+
);
|
|
3689
|
+
}
|
|
3690
|
+
return transformSqlOutsideLiterals(query, (segment) => {
|
|
3691
|
+
let rewritten = segment;
|
|
3692
|
+
for (const table of canonical.tables) {
|
|
3693
|
+
if (!table.source) continue;
|
|
3694
|
+
const boundTable = boundTableBySource.get(table.source);
|
|
3695
|
+
if (boundTable === void 0 || boundTable !== table.table) continue;
|
|
3696
|
+
const [sourceSchema, sourceName] = table.source.split(".");
|
|
3697
|
+
if (!sourceSchema || !sourceName) continue;
|
|
3698
|
+
rewritten = replaceQualifiedRelation(
|
|
3699
|
+
rewritten,
|
|
3700
|
+
sourceSchema,
|
|
3701
|
+
sourceName,
|
|
3702
|
+
schemaName,
|
|
3703
|
+
table.table
|
|
3704
|
+
);
|
|
3705
|
+
}
|
|
3706
|
+
return rewritten;
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
function transformSqlOutsideLiterals(sql4, transform) {
|
|
3710
|
+
const out = [];
|
|
3711
|
+
let state = { kind: "normal" };
|
|
3712
|
+
let i = 0;
|
|
3713
|
+
let codeStart = 0;
|
|
3714
|
+
const emitCode = (end) => {
|
|
3715
|
+
if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
|
|
3716
|
+
codeStart = end;
|
|
3717
|
+
};
|
|
3718
|
+
while (i < sql4.length) {
|
|
3719
|
+
const c = sql4[i];
|
|
3720
|
+
const next = i + 1 < sql4.length ? sql4[i + 1] : "";
|
|
3721
|
+
switch (state.kind) {
|
|
3722
|
+
case "normal": {
|
|
3723
|
+
if (c === "-" && next === "-") {
|
|
3724
|
+
emitCode(i);
|
|
3725
|
+
state = { kind: "line-comment" };
|
|
3726
|
+
out.push("--");
|
|
3727
|
+
i += 2;
|
|
3728
|
+
codeStart = i;
|
|
3729
|
+
break;
|
|
3730
|
+
}
|
|
3731
|
+
if (c === "/" && next === "*") {
|
|
3732
|
+
emitCode(i);
|
|
3733
|
+
state = { kind: "block-comment", depth: 1 };
|
|
3734
|
+
out.push("/*");
|
|
3735
|
+
i += 2;
|
|
3736
|
+
codeStart = i;
|
|
3737
|
+
break;
|
|
3738
|
+
}
|
|
3739
|
+
if ((c === "E" || c === "e") && next === "'") {
|
|
3740
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
3741
|
+
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
3742
|
+
emitCode(i);
|
|
3743
|
+
state = { kind: "e-single" };
|
|
3744
|
+
out.push("E'");
|
|
3745
|
+
i += 2;
|
|
3746
|
+
codeStart = i;
|
|
3747
|
+
break;
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
if (c === "'") {
|
|
3751
|
+
emitCode(i);
|
|
3752
|
+
state = { kind: "single" };
|
|
3753
|
+
out.push("'");
|
|
3754
|
+
i += 1;
|
|
3755
|
+
codeStart = i;
|
|
3756
|
+
break;
|
|
3757
|
+
}
|
|
3758
|
+
if (c === "$") {
|
|
3759
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
3760
|
+
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
3761
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
|
|
3762
|
+
if (tagMatch) {
|
|
3763
|
+
emitCode(i);
|
|
3764
|
+
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
3765
|
+
out.push(tagMatch[0]);
|
|
3766
|
+
i += tagMatch[0].length;
|
|
3767
|
+
codeStart = i;
|
|
3768
|
+
break;
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
i += 1;
|
|
3773
|
+
break;
|
|
3774
|
+
}
|
|
3775
|
+
case "single": {
|
|
3776
|
+
if (c === "'" && next === "'") {
|
|
3777
|
+
out.push("''");
|
|
3778
|
+
i += 2;
|
|
3779
|
+
break;
|
|
3780
|
+
}
|
|
3781
|
+
if (c === "'") {
|
|
3782
|
+
out.push("'");
|
|
3783
|
+
state = { kind: "normal" };
|
|
3784
|
+
i += 1;
|
|
3785
|
+
codeStart = i;
|
|
3786
|
+
break;
|
|
3787
|
+
}
|
|
3788
|
+
out.push(c);
|
|
3789
|
+
i += 1;
|
|
3790
|
+
break;
|
|
3791
|
+
}
|
|
3792
|
+
case "e-single": {
|
|
3793
|
+
if (c === "\\" && i + 1 < sql4.length) {
|
|
3794
|
+
out.push(c, sql4[i + 1]);
|
|
3795
|
+
i += 2;
|
|
3796
|
+
break;
|
|
3797
|
+
}
|
|
3798
|
+
if (c === "'" && next === "'") {
|
|
3799
|
+
out.push("''");
|
|
3800
|
+
i += 2;
|
|
3801
|
+
break;
|
|
3802
|
+
}
|
|
3803
|
+
if (c === "'") {
|
|
3804
|
+
out.push("'");
|
|
3805
|
+
state = { kind: "normal" };
|
|
3806
|
+
i += 1;
|
|
3807
|
+
codeStart = i;
|
|
3808
|
+
break;
|
|
3809
|
+
}
|
|
3810
|
+
out.push(c);
|
|
3811
|
+
i += 1;
|
|
3812
|
+
break;
|
|
3813
|
+
}
|
|
3814
|
+
case "dollar": {
|
|
3815
|
+
const closer = `$${state.tag}$`;
|
|
3816
|
+
if (sql4.startsWith(closer, i)) {
|
|
3817
|
+
out.push(closer);
|
|
3818
|
+
state = { kind: "normal" };
|
|
3819
|
+
i += closer.length;
|
|
3820
|
+
codeStart = i;
|
|
3821
|
+
break;
|
|
3822
|
+
}
|
|
3823
|
+
out.push(c);
|
|
3824
|
+
i += 1;
|
|
3825
|
+
break;
|
|
3826
|
+
}
|
|
3827
|
+
case "line-comment": {
|
|
3828
|
+
if (c === "\n") {
|
|
3829
|
+
out.push("\n");
|
|
3830
|
+
state = { kind: "normal" };
|
|
3831
|
+
i += 1;
|
|
3832
|
+
codeStart = i;
|
|
3833
|
+
break;
|
|
3834
|
+
}
|
|
3835
|
+
out.push(c);
|
|
3836
|
+
i += 1;
|
|
3837
|
+
break;
|
|
3838
|
+
}
|
|
3839
|
+
case "block-comment": {
|
|
3840
|
+
if (c === "/" && next === "*") {
|
|
3841
|
+
out.push("/*");
|
|
3842
|
+
state = { kind: "block-comment", depth: state.depth + 1 };
|
|
3843
|
+
i += 2;
|
|
3844
|
+
break;
|
|
3845
|
+
}
|
|
3846
|
+
if (c === "*" && next === "/") {
|
|
3847
|
+
out.push("*/");
|
|
3848
|
+
const newDepth = state.depth - 1;
|
|
3849
|
+
state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
|
|
3850
|
+
i += 2;
|
|
3851
|
+
if (state.kind === "normal") codeStart = i;
|
|
3852
|
+
break;
|
|
3853
|
+
}
|
|
3854
|
+
out.push(c);
|
|
3855
|
+
i += 1;
|
|
3856
|
+
break;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
emitCode(sql4.length);
|
|
3861
|
+
return out.join("");
|
|
3862
|
+
}
|
|
3863
|
+
function escapeRegExp(value) {
|
|
3864
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3865
|
+
}
|
|
3866
|
+
function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
|
|
3867
|
+
let referenced = false;
|
|
3868
|
+
transformSqlOutsideLiterals(sql4, (segment) => {
|
|
3869
|
+
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
3870
|
+
return segment;
|
|
3871
|
+
});
|
|
3872
|
+
return referenced;
|
|
3873
|
+
}
|
|
3874
|
+
function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
3875
|
+
const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
|
|
3876
|
+
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
3877
|
+
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
3878
|
+
}
|
|
3879
|
+
function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
3880
|
+
return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
3881
|
+
}
|
|
3882
|
+
function generateSnapshotV2(definition, previousId3) {
|
|
3883
|
+
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
3884
|
+
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
3885
|
+
if ((definition.views?.length ?? 0) > 0) {
|
|
3886
|
+
const extended = snapshot;
|
|
3887
|
+
extended._sequenceOrm = {
|
|
3888
|
+
viewDependencies: Object.fromEntries(
|
|
3889
|
+
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
3890
|
+
)
|
|
3891
|
+
};
|
|
3892
|
+
}
|
|
3893
|
+
return snapshot;
|
|
3894
|
+
}
|
|
3895
|
+
function buildViewColumnV2({
|
|
3896
|
+
view,
|
|
3897
|
+
column,
|
|
3898
|
+
enums
|
|
3899
|
+
}) {
|
|
3900
|
+
const factory = resolveColumnFactory(view, column, enums);
|
|
3901
|
+
let builder = factory(column.columnName);
|
|
3902
|
+
if (!column.nullable) builder = builder.notNull();
|
|
3903
|
+
return builder;
|
|
3904
|
+
}
|
|
3905
|
+
function buildTable2(schema, table, built, enums, byName) {
|
|
3906
|
+
const primaryKeyColumns = table.primaryKey ?? [];
|
|
3907
|
+
if (primaryKeyColumns.length === 0) {
|
|
3908
|
+
throw new OrmDefinitionError(`table '${table.table}' declares no primaryKey`);
|
|
3909
|
+
}
|
|
3910
|
+
const columnNames = new Set(table.columns.map((column) => column.columnName));
|
|
3911
|
+
for (const name of primaryKeyColumns) {
|
|
3912
|
+
if (!columnNames.has(name)) {
|
|
3913
|
+
throw new OrmDefinitionError(`primaryKey of '${table.table}' references unknown column '${name}'`);
|
|
3914
|
+
}
|
|
3915
|
+
}
|
|
3916
|
+
const singlePrimaryKey = primaryKeyColumns.length === 1 ? primaryKeyColumns[0] : void 0;
|
|
3917
|
+
const compositeMembers = new Set(singlePrimaryKey === void 0 ? primaryKeyColumns : []);
|
|
3918
|
+
const columns = Object.fromEntries(
|
|
3919
|
+
table.columns.map((column) => [
|
|
3920
|
+
column.columnName,
|
|
3921
|
+
buildColumnV2({ table: table.table, column, singlePrimaryKey, compositeMembers, enums })
|
|
3922
|
+
])
|
|
3923
|
+
);
|
|
3924
|
+
const drizzleTable = schema.table(table.table, columns, (self) => [
|
|
3925
|
+
...buildCompositePrimaryKey(table, primaryKeyColumns, self),
|
|
3926
|
+
...buildIndexes2(table, self),
|
|
3927
|
+
...buildForeignKeys2(table, self, built, byName)
|
|
3928
|
+
]);
|
|
3929
|
+
return drizzleTable.enableRLS();
|
|
3930
|
+
}
|
|
3931
|
+
function resolveColumnFactory(table, column, enums) {
|
|
3932
|
+
if (typeof column.type === "string") return FACTORIES2[column.type];
|
|
3933
|
+
const enumFactory = enums.get(column.type.enum);
|
|
3934
|
+
if (!enumFactory) {
|
|
3935
|
+
throw new OrmDefinitionError(`column '${table}.${column.columnName}' references undeclared enum '${column.type.enum}'`);
|
|
3936
|
+
}
|
|
3937
|
+
return enumFactory;
|
|
3938
|
+
}
|
|
3939
|
+
function buildColumnV2({
|
|
3940
|
+
table,
|
|
3941
|
+
column,
|
|
3942
|
+
singlePrimaryKey,
|
|
3943
|
+
compositeMembers,
|
|
3944
|
+
enums
|
|
3945
|
+
}) {
|
|
3946
|
+
const factory = resolveColumnFactory(table, column, enums);
|
|
3947
|
+
let builder = factory(column.columnName);
|
|
3948
|
+
const isSinglePrimaryKey = column.columnName === singlePrimaryKey;
|
|
3949
|
+
if (isSinglePrimaryKey) builder = builder.primaryKey();
|
|
3950
|
+
if ((column.nullable === false || compositeMembers.has(column.columnName)) && !isSinglePrimaryKey) {
|
|
3951
|
+
builder = builder.notNull();
|
|
3952
|
+
}
|
|
3953
|
+
if (column.unique && !isSinglePrimaryKey) builder = builder.unique();
|
|
3954
|
+
if (column.defaultFn !== void 0 && column.default !== void 0) {
|
|
3955
|
+
throw new OrmDefinitionError(`column '${table}.${column.columnName}' declares both default and defaultFn`);
|
|
3956
|
+
}
|
|
3957
|
+
if (column.defaultFn !== void 0) {
|
|
3958
|
+
if (typeof column.type !== "string" || !isFunctionDefault(column.defaultFn, column.type)) {
|
|
3959
|
+
throw new OrmDefinitionError(
|
|
3960
|
+
`column '${table}.${column.columnName}' declares defaultFn '${column.defaultFn}' which is invalid for its type`
|
|
3961
|
+
);
|
|
3962
|
+
}
|
|
3963
|
+
builder = builder.default(drizzleOrm.sql.raw(column.defaultFn));
|
|
3964
|
+
} else if (column.default !== void 0 && column.default !== null) {
|
|
3965
|
+
builder = builder.default(column.default);
|
|
3966
|
+
}
|
|
3967
|
+
return builder;
|
|
3968
|
+
}
|
|
3969
|
+
function buildCompositePrimaryKey(table, primaryKeyColumns, self) {
|
|
3970
|
+
if (primaryKeyColumns.length < 2) return [];
|
|
3971
|
+
return [
|
|
3972
|
+
pgCore.primaryKey({
|
|
3973
|
+
name: `${table.table}_pkey`,
|
|
3974
|
+
columns: primaryKeyColumns.map((column) => self[column])
|
|
3975
|
+
})
|
|
3976
|
+
];
|
|
3977
|
+
}
|
|
3978
|
+
function buildIndexes2(table, self) {
|
|
3979
|
+
return table.indexes.map((definition) => {
|
|
3980
|
+
for (const column of definition.columns) {
|
|
3981
|
+
if (!(column in self)) {
|
|
3982
|
+
throw new OrmDefinitionError(`index on '${table.table}' references unknown column '${column}'`);
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
const name = indexName(table.table, definition);
|
|
3986
|
+
const factory = definition.unique ? pgCore.uniqueIndex(name) : pgCore.index(name);
|
|
3987
|
+
const columns = definition.columns.map((column) => self[column]);
|
|
3988
|
+
return factory.on(...columns);
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
function buildForeignKeys2(table, self, built, byName) {
|
|
3992
|
+
return table.links.filter((link) => link.enforce).map((link) => {
|
|
3993
|
+
if (link.kind !== "toOne" || !link.target) {
|
|
3994
|
+
throw new OrmDefinitionError(
|
|
3995
|
+
`enforced link '${link.name}' on '${table.table}' must be a toOne link with a namespace-local target`
|
|
3996
|
+
);
|
|
3997
|
+
}
|
|
3998
|
+
const targetTable = byName.get(link.target);
|
|
3999
|
+
const target = link.target === table.table ? self : built.get(link.target);
|
|
4000
|
+
if (!targetTable || !target) {
|
|
4001
|
+
throw new OrmDefinitionError(`enforced link '${link.name}' on '${table.table}' has no built target '${link.target}'`);
|
|
4002
|
+
}
|
|
4003
|
+
const referencedColumn = link.references ?? singlePrimaryKeyOf(targetTable);
|
|
4004
|
+
if (!referencedColumn) {
|
|
4005
|
+
throw new OrmDefinitionError(
|
|
4006
|
+
`enforced link '${link.name}' on '${table.table}' targets '${link.target}' whose primary key is composite \u2014 declare 'references'`
|
|
4007
|
+
);
|
|
4008
|
+
}
|
|
4009
|
+
const targetColumn = target[referencedColumn];
|
|
4010
|
+
const sourceColumn = self[link.on];
|
|
4011
|
+
if (!targetColumn || !sourceColumn) {
|
|
4012
|
+
throw new OrmDefinitionError(
|
|
4013
|
+
`enforced link '${link.name}' on '${table.table}' references unknown column '${targetColumn ? link.on : referencedColumn}'`
|
|
4014
|
+
);
|
|
4015
|
+
}
|
|
4016
|
+
return pgCore.foreignKey({
|
|
4017
|
+
name: foreignKeyName(table.table, link.name),
|
|
4018
|
+
columns: [sourceColumn],
|
|
4019
|
+
foreignColumns: [targetColumn]
|
|
4020
|
+
});
|
|
4021
|
+
});
|
|
4022
|
+
}
|
|
4023
|
+
var singlePrimaryKeyOf = (table) => table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
4024
|
+
function sortByEnforcedLinks2(tables) {
|
|
4025
|
+
const remaining = new Map(tables.map((table) => [table.table, table]));
|
|
4026
|
+
const sorted = [];
|
|
4027
|
+
const placed = /* @__PURE__ */ new Set();
|
|
4028
|
+
while (remaining.size > 0) {
|
|
4029
|
+
const ready = [...remaining.values()].filter(
|
|
4030
|
+
(table) => enforcedTargets2(table).every((target) => placed.has(target) || !remaining.has(target))
|
|
4031
|
+
);
|
|
4032
|
+
if (ready.length === 0) {
|
|
4033
|
+
throw new OrmDefinitionError(`enforced links form a cycle among: ${[...remaining.keys()].join(", ")}`);
|
|
4034
|
+
}
|
|
4035
|
+
for (const table of ready) {
|
|
4036
|
+
sorted.push(table);
|
|
4037
|
+
placed.add(table.table);
|
|
4038
|
+
remaining.delete(table.table);
|
|
4039
|
+
}
|
|
4040
|
+
}
|
|
4041
|
+
return sorted;
|
|
4042
|
+
}
|
|
4043
|
+
var enforcedTargets2 = (table) => table.links.flatMap(
|
|
4044
|
+
(link) => (
|
|
4045
|
+
// A self-target is not an ordering dependency — the FK builds inline.
|
|
4046
|
+
link.enforce && link.target !== void 0 && link.target !== table.table ? [link.target] : []
|
|
4047
|
+
)
|
|
4048
|
+
);
|
|
4049
|
+
function applyMigrationHintsV2(previous, definition) {
|
|
4050
|
+
const adjusted = structuredClone(previous);
|
|
4051
|
+
const statements = [];
|
|
4052
|
+
const schemaName = ormSchemaName(definition.namespace);
|
|
4053
|
+
const qualified = (table) => `${quoteIdentifier(schemaName)}.${quoteIdentifier(table)}`;
|
|
4054
|
+
const enumsByName = new Map(definition.enums.map((entry) => [entry.name, entry]));
|
|
4055
|
+
for (const table of columnTablesV2(definition)) {
|
|
4056
|
+
if (table.renamedFrom) {
|
|
4057
|
+
applyTableRename({ adjusted, schemaName, table, renamedFrom: table.renamedFrom, statements, qualified });
|
|
4058
|
+
}
|
|
4059
|
+
const snapshotTable = findTable2(adjusted, schemaName, table.table);
|
|
4060
|
+
if (!snapshotTable) continue;
|
|
4061
|
+
for (const column of table.columns) {
|
|
4062
|
+
const name = column.columnName;
|
|
4063
|
+
if (column.renamedFrom) {
|
|
4064
|
+
const previousColumn = snapshotTable.columns[column.renamedFrom];
|
|
4065
|
+
if (previousColumn && snapshotTable.columns[name]) {
|
|
4066
|
+
throw new OrmDefinitionError(
|
|
4067
|
+
`'${table.table}.${name}' declares renamedFrom '${column.renamedFrom}', but both columns exist in the previous schema \u2014 a rename cannot overwrite a live column`
|
|
4068
|
+
);
|
|
4069
|
+
}
|
|
4070
|
+
if (previousColumn) {
|
|
4071
|
+
statements.push(
|
|
4072
|
+
`ALTER TABLE ${qualified(table.table)} RENAME COLUMN ${quoteIdentifier(column.renamedFrom)} TO ${quoteIdentifier(name)}`
|
|
4073
|
+
);
|
|
4074
|
+
delete snapshotTable.columns[column.renamedFrom];
|
|
4075
|
+
snapshotTable.columns[name] = { ...previousColumn, name };
|
|
4076
|
+
} else if (!snapshotTable.columns[name]) {
|
|
4077
|
+
throw new OrmDefinitionError(
|
|
4078
|
+
`'${table.table}.${name}' declares renamedFrom '${column.renamedFrom}', but the previous schema has neither column \u2014 check the declared source name`
|
|
4079
|
+
);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
4083
|
+
if (cast) {
|
|
4084
|
+
const current = snapshotTable.columns[name];
|
|
4085
|
+
const { snapshotType, sqlType } = castTargetTypes(column, enumsByName, schemaName, table.table);
|
|
4086
|
+
if (current && current.type !== snapshotType) {
|
|
4087
|
+
statements.push(
|
|
4088
|
+
`ALTER TABLE ${qualified(table.table)} ALTER COLUMN ${quoteIdentifier(name)} TYPE ${sqlType} USING ${quoteIdentifier(name)}::${sqlType}`
|
|
4089
|
+
);
|
|
4090
|
+
current.type = snapshotType;
|
|
4091
|
+
}
|
|
4092
|
+
}
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
return { statements, adjusted };
|
|
4096
|
+
}
|
|
4097
|
+
function castTargetTypes(column, enums, schemaName, table) {
|
|
4098
|
+
if (typeof column.type === "string") {
|
|
4099
|
+
const name = sqlTypeName(column.type);
|
|
4100
|
+
return { snapshotType: name, sqlType: name };
|
|
4101
|
+
}
|
|
4102
|
+
const enumName = column.type.enum;
|
|
4103
|
+
if (!enums.has(enumName)) {
|
|
4104
|
+
throw new OrmDefinitionError(`column '${table}.${column.columnName}' casts to undeclared enum '${enumName}'`);
|
|
4105
|
+
}
|
|
4106
|
+
return { snapshotType: enumName, sqlType: `${quoteIdentifier(schemaName)}.${quoteIdentifier(enumName)}` };
|
|
4107
|
+
}
|
|
4108
|
+
function applyTableRename({
|
|
4109
|
+
adjusted,
|
|
4110
|
+
schemaName,
|
|
4111
|
+
table,
|
|
4112
|
+
renamedFrom,
|
|
4113
|
+
statements,
|
|
4114
|
+
qualified
|
|
4115
|
+
}) {
|
|
4116
|
+
const tables = adjusted.tables ?? {};
|
|
4117
|
+
const previousEntry = Object.entries(tables).find(
|
|
4118
|
+
([, entry]) => entry.schema === schemaName && entry.name === renamedFrom
|
|
4119
|
+
);
|
|
4120
|
+
const currentTable = findTable2(adjusted, schemaName, table.table);
|
|
4121
|
+
if (previousEntry && currentTable) {
|
|
4122
|
+
throw new OrmDefinitionError(
|
|
4123
|
+
`'${table.table}' declares renamedFrom '${renamedFrom}', but both tables exist in the previous schema \u2014 a rename cannot overwrite a live table`
|
|
4124
|
+
);
|
|
4125
|
+
}
|
|
4126
|
+
if (!previousEntry) {
|
|
4127
|
+
if (currentTable) return;
|
|
4128
|
+
throw new OrmDefinitionError(
|
|
4129
|
+
`'${table.table}' declares renamedFrom '${renamedFrom}', but the previous schema has neither table \u2014 check the declared source name`
|
|
4130
|
+
);
|
|
4131
|
+
}
|
|
4132
|
+
statements.push(`ALTER TABLE ${qualified(renamedFrom)} RENAME TO ${quoteIdentifier(table.table)}`);
|
|
4133
|
+
const [previousKey, previousTable] = previousEntry;
|
|
4134
|
+
delete tables[previousKey];
|
|
4135
|
+
tables[`${schemaName}.${table.table}`] = { ...previousTable, name: table.table };
|
|
4136
|
+
}
|
|
4137
|
+
function findTable2(snapshot, schema, table) {
|
|
4138
|
+
const tables = snapshot.tables ?? {};
|
|
4139
|
+
return Object.values(tables).find((entry) => entry.schema === schema && entry.name === table);
|
|
4140
|
+
}
|
|
4141
|
+
async function planMigrationV2({
|
|
4142
|
+
canonical,
|
|
4143
|
+
previousSnapshot
|
|
4144
|
+
}) {
|
|
4145
|
+
const schema = ormSchemaName(canonical.namespace);
|
|
4146
|
+
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
4147
|
+
assertValidViewDependencies(canonical);
|
|
4148
|
+
const hints = applyMigrationHintsV2(previous, canonical);
|
|
4149
|
+
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
4150
|
+
const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
4151
|
+
const previousViews = snapshotViews(hints.adjusted);
|
|
4152
|
+
const currentViews = snapshotViews(snapshot);
|
|
4153
|
+
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
4154
|
+
const currentDependencies = snapshotViewDependencies(snapshot);
|
|
4155
|
+
const affected = affectedViewNames({
|
|
4156
|
+
previousViews,
|
|
4157
|
+
currentViews,
|
|
4158
|
+
previousDependencies,
|
|
4159
|
+
currentDependencies,
|
|
4160
|
+
baseChanged: hints.statements.length > 0 || baseDdl.length > 0
|
|
4161
|
+
});
|
|
4162
|
+
const dropOrder = topologicalViewNames(
|
|
4163
|
+
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
4164
|
+
previousDependencies
|
|
4165
|
+
).reverse();
|
|
4166
|
+
const createOrder = topologicalViewNames(
|
|
4167
|
+
new Set([...affected].filter((name) => currentViews.has(name))),
|
|
4168
|
+
currentDependencies
|
|
4169
|
+
);
|
|
4170
|
+
const dropDdl = await generatedViewStatements({
|
|
4171
|
+
snapshot: hints.adjusted,
|
|
4172
|
+
names: new Set(dropOrder),
|
|
4173
|
+
mode: "drop",
|
|
4174
|
+
schema
|
|
4175
|
+
});
|
|
4176
|
+
const createDdl = await generatedViewStatements({
|
|
4177
|
+
snapshot,
|
|
4178
|
+
names: new Set(createOrder),
|
|
4179
|
+
mode: "create",
|
|
4180
|
+
schema
|
|
4181
|
+
});
|
|
4182
|
+
const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
|
|
4183
|
+
const statements = [
|
|
4184
|
+
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
4185
|
+
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4186
|
+
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
4187
|
+
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
4188
|
+
...createOrder.map((name) => {
|
|
4189
|
+
const view = currentViewDefinitions.get(name);
|
|
4190
|
+
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
4191
|
+
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
4192
|
+
})
|
|
4193
|
+
];
|
|
4194
|
+
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4195
|
+
}
|
|
4196
|
+
function validateViewCatalogStatement({
|
|
4197
|
+
canonical,
|
|
4198
|
+
view,
|
|
4199
|
+
schema
|
|
4200
|
+
}) {
|
|
4201
|
+
const qualified = `${schema}.${view.view}`;
|
|
4202
|
+
const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
|
|
4203
|
+
const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
|
|
4204
|
+
const relationIdentity = /* @__PURE__ */ new Map();
|
|
4205
|
+
for (const table of canonical.tables) {
|
|
4206
|
+
relationIdentity.set(table.table, `${schema}.${table.table}`);
|
|
4207
|
+
}
|
|
4208
|
+
for (const candidate of canonical.views ?? []) {
|
|
4209
|
+
relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
|
|
4210
|
+
}
|
|
4211
|
+
const expectedDependencies = sqlTextArray(
|
|
4212
|
+
view.dependencies.map((dependency) => {
|
|
4213
|
+
const physical = relationIdentity.get(dependency);
|
|
4214
|
+
if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
4215
|
+
return physical;
|
|
4216
|
+
}).sort()
|
|
4217
|
+
);
|
|
4218
|
+
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
4219
|
+
return `DO $sequence_orm_validate_view$
|
|
4220
|
+
DECLARE
|
|
4221
|
+
actual_columns text[];
|
|
4222
|
+
actual_types oid[];
|
|
4223
|
+
actual_dependencies text[];
|
|
4224
|
+
BEGIN
|
|
4225
|
+
SELECT
|
|
4226
|
+
array_agg(attribute.attname ORDER BY attribute.attnum),
|
|
4227
|
+
array_agg(attribute.atttypid ORDER BY attribute.attnum)
|
|
4228
|
+
INTO actual_columns, actual_types
|
|
4229
|
+
FROM pg_catalog.pg_attribute AS attribute
|
|
4230
|
+
WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4231
|
+
AND attribute.attnum > 0
|
|
4232
|
+
AND NOT attribute.attisdropped;
|
|
4233
|
+
|
|
4234
|
+
IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
|
|
4235
|
+
RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
|
|
4236
|
+
USING ERRCODE = '42804';
|
|
4237
|
+
END IF;
|
|
4238
|
+
IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
|
|
4239
|
+
RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
|
|
4240
|
+
USING ERRCODE = '42804';
|
|
4241
|
+
END IF;
|
|
4242
|
+
|
|
4243
|
+
SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
|
|
4244
|
+
INTO actual_dependencies
|
|
4245
|
+
FROM (
|
|
4246
|
+
SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
|
|
4247
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
4248
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
4249
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
4250
|
+
AND dependency.objid = rewrite.oid
|
|
4251
|
+
AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
|
|
4252
|
+
JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
|
|
4253
|
+
JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
|
|
4254
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4255
|
+
AND dependency.refobjid <> rewrite.ev_class
|
|
4256
|
+
AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
|
|
4257
|
+
) AS found;
|
|
4258
|
+
|
|
4259
|
+
IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
|
|
4260
|
+
RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
|
|
4261
|
+
USING ERRCODE = '2BP01';
|
|
4262
|
+
END IF;
|
|
4263
|
+
|
|
4264
|
+
IF EXISTS (
|
|
4265
|
+
SELECT 1
|
|
4266
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
4267
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
4268
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
4269
|
+
AND dependency.objid = rewrite.oid
|
|
4270
|
+
AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
|
|
4271
|
+
JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
|
|
4272
|
+
JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
|
|
4273
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4274
|
+
AND (
|
|
4275
|
+
function_namespace.nspname <> 'pg_catalog'
|
|
4276
|
+
OR dependency_function.proname <> ALL (${allowedFunctions})
|
|
4277
|
+
)
|
|
4278
|
+
) THEN
|
|
4279
|
+
RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
|
|
4280
|
+
USING ERRCODE = '42501';
|
|
4281
|
+
END IF;
|
|
4282
|
+
|
|
4283
|
+
IF EXISTS (
|
|
4284
|
+
SELECT 1
|
|
4285
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
4286
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
4287
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
4288
|
+
AND dependency.objid = rewrite.oid
|
|
4289
|
+
AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
|
|
4290
|
+
JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
|
|
4291
|
+
JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
|
|
4292
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
4293
|
+
AND operator_namespace.nspname <> 'pg_catalog'
|
|
4294
|
+
) THEN
|
|
4295
|
+
RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
|
|
4296
|
+
USING ERRCODE = '42501';
|
|
4297
|
+
END IF;
|
|
4298
|
+
END
|
|
4299
|
+
$sequence_orm_validate_view$`;
|
|
4300
|
+
}
|
|
4301
|
+
function sqlTextArray(values) {
|
|
4302
|
+
return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
|
|
4303
|
+
}
|
|
4304
|
+
function viewTypeRegtype(column, schema) {
|
|
4305
|
+
if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
|
|
4306
|
+
return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
|
|
4307
|
+
}
|
|
4308
|
+
var previousId = (snapshot) => snapshot.id;
|
|
4309
|
+
function snapshotViews(snapshot) {
|
|
4310
|
+
const views = snapshot.views ?? {};
|
|
4311
|
+
return new Map(Object.values(views).map((view) => [view.name, view]));
|
|
4312
|
+
}
|
|
4313
|
+
function snapshotViewDependencies(snapshot) {
|
|
4314
|
+
const metadata = snapshot._sequenceOrm;
|
|
4315
|
+
return metadata?.viewDependencies ?? {};
|
|
4316
|
+
}
|
|
4317
|
+
function withoutViews(snapshot) {
|
|
4318
|
+
const clone = structuredClone(snapshot);
|
|
4319
|
+
clone.views = {};
|
|
4320
|
+
delete clone._sequenceOrm;
|
|
4321
|
+
return clone;
|
|
4322
|
+
}
|
|
4323
|
+
function affectedViewNames({
|
|
4324
|
+
previousViews,
|
|
4325
|
+
currentViews,
|
|
4326
|
+
previousDependencies,
|
|
4327
|
+
currentDependencies,
|
|
4328
|
+
baseChanged
|
|
4329
|
+
}) {
|
|
4330
|
+
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
4331
|
+
if (baseChanged) return allNames;
|
|
4332
|
+
const affected = new Set(
|
|
4333
|
+
[...allNames].filter((name) => {
|
|
4334
|
+
const before = previousViews.get(name);
|
|
4335
|
+
const after = currentViews.get(name);
|
|
4336
|
+
return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
|
|
4337
|
+
})
|
|
4338
|
+
);
|
|
4339
|
+
let grew = true;
|
|
4340
|
+
while (grew) {
|
|
4341
|
+
grew = false;
|
|
4342
|
+
for (const name of allNames) {
|
|
4343
|
+
if (affected.has(name)) continue;
|
|
4344
|
+
const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
4345
|
+
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
4346
|
+
affected.add(name);
|
|
4347
|
+
grew = true;
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
return affected;
|
|
1344
4352
|
}
|
|
1345
|
-
function
|
|
1346
|
-
const
|
|
1347
|
-
|
|
4353
|
+
function topologicalViewNames(names, dependencies) {
|
|
4354
|
+
const remaining = new Set(names);
|
|
4355
|
+
const ordered = [];
|
|
4356
|
+
while (remaining.size > 0) {
|
|
4357
|
+
const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
|
|
4358
|
+
if (ready.length === 0) {
|
|
4359
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
|
|
4360
|
+
}
|
|
4361
|
+
for (const name of ready) {
|
|
4362
|
+
ordered.push(name);
|
|
4363
|
+
remaining.delete(name);
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
return ordered;
|
|
1348
4367
|
}
|
|
1349
|
-
function
|
|
1350
|
-
|
|
1351
|
-
|
|
4368
|
+
async function generatedViewStatements({
|
|
4369
|
+
snapshot,
|
|
4370
|
+
names,
|
|
4371
|
+
mode,
|
|
4372
|
+
schema
|
|
4373
|
+
}) {
|
|
4374
|
+
if (names.size === 0) return /* @__PURE__ */ new Map();
|
|
4375
|
+
const without = structuredClone(snapshot);
|
|
4376
|
+
const holder = without;
|
|
4377
|
+
const views = holder.views ?? {};
|
|
4378
|
+
holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
|
|
4379
|
+
const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
|
|
4380
|
+
const statements = /* @__PURE__ */ new Map();
|
|
4381
|
+
for (const name of names) {
|
|
4382
|
+
const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
4383
|
+
const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
|
|
4384
|
+
const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
|
|
4385
|
+
if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
|
|
4386
|
+
}
|
|
4387
|
+
return statements;
|
|
4388
|
+
}
|
|
4389
|
+
function requiredViewStatement(statements, name, schema, operation) {
|
|
4390
|
+
const statement = statements.get(name);
|
|
4391
|
+
if (!statement) {
|
|
4392
|
+
throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
|
|
4393
|
+
}
|
|
4394
|
+
return statement;
|
|
4395
|
+
}
|
|
4396
|
+
function assertValidViewDependencies(canonical) {
|
|
4397
|
+
const views = canonical.views ?? [];
|
|
4398
|
+
const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
|
|
4399
|
+
for (const view of views) {
|
|
4400
|
+
for (const dependency of view.dependencies) {
|
|
4401
|
+
if (!known.has(dependency)) {
|
|
4402
|
+
throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
}
|
|
4406
|
+
topologicalViewNames(
|
|
4407
|
+
new Set(views.map((view) => view.view)),
|
|
4408
|
+
Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
|
|
4409
|
+
);
|
|
1352
4410
|
}
|
|
4411
|
+
|
|
4412
|
+
// src/engine/converge.ts
|
|
1353
4413
|
var OWNED_STATEMENT = new RegExp(
|
|
1354
4414
|
[
|
|
1355
4415
|
String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
|
|
@@ -1362,18 +4422,43 @@ var OWNED_STATEMENT = new RegExp(
|
|
|
1362
4422
|
].join("|"),
|
|
1363
4423
|
"i"
|
|
1364
4424
|
);
|
|
4425
|
+
var V2_OWNED_STATEMENT = new RegExp(
|
|
4426
|
+
[
|
|
4427
|
+
String.raw`^\s*(CREATE|DROP)\s+POLICY\b`,
|
|
4428
|
+
String.raw`^\s*(GRANT|REVOKE)\b`,
|
|
4429
|
+
String.raw`^\s*(CREATE|DROP|ALTER)\s+ROLE\b`,
|
|
4430
|
+
String.raw`\bROW LEVEL SECURITY\b`
|
|
4431
|
+
].join("|"),
|
|
4432
|
+
"i"
|
|
4433
|
+
);
|
|
1365
4434
|
function isOwnedStatement(statement) {
|
|
1366
4435
|
return OWNED_STATEMENT.test(statement);
|
|
1367
4436
|
}
|
|
4437
|
+
function isOwnedStatementV2(statement) {
|
|
4438
|
+
return V2_OWNED_STATEMENT.test(statement);
|
|
4439
|
+
}
|
|
1368
4440
|
async function pendingKitStatements({
|
|
1369
4441
|
definition,
|
|
1370
4442
|
db
|
|
1371
4443
|
}) {
|
|
1372
|
-
|
|
4444
|
+
return pendingStatements({ entities: buildDrizzleEntities(definition), db, owned: OWNED_STATEMENT });
|
|
4445
|
+
}
|
|
4446
|
+
async function pendingKitStatementsV2({
|
|
4447
|
+
canonical,
|
|
4448
|
+
db
|
|
4449
|
+
}) {
|
|
4450
|
+
return pendingStatements({ entities: buildDrizzleEntitiesV2(canonical), db, owned: V2_OWNED_STATEMENT });
|
|
4451
|
+
}
|
|
4452
|
+
async function pendingStatements({
|
|
4453
|
+
entities,
|
|
4454
|
+
db,
|
|
4455
|
+
owned
|
|
4456
|
+
}) {
|
|
4457
|
+
const { imports, schemaName } = entities;
|
|
1373
4458
|
const { statementsToExecute } = await withExitGuard(
|
|
1374
4459
|
() => api.pushSchema(imports, shimExecute(db), [schemaName])
|
|
1375
4460
|
);
|
|
1376
|
-
return statementsToExecute.filter((statement) => !
|
|
4461
|
+
return statementsToExecute.filter((statement) => !owned.test(statement));
|
|
1377
4462
|
}
|
|
1378
4463
|
function shimExecute(db) {
|
|
1379
4464
|
const execute = db.execute.bind(db);
|
|
@@ -1407,7 +4492,7 @@ async function guardedRun(run) {
|
|
|
1407
4492
|
}
|
|
1408
4493
|
|
|
1409
4494
|
// src/engine/index.ts
|
|
1410
|
-
var STATEMENT_KINDS = ["hint", "ddl", "security", "function"];
|
|
4495
|
+
var STATEMENT_KINDS = ["hint", "ddl", "security", "function", "synced_view"];
|
|
1411
4496
|
async function planMigration({
|
|
1412
4497
|
definition,
|
|
1413
4498
|
previousSnapshot
|
|
@@ -1415,254 +4500,308 @@ async function planMigration({
|
|
|
1415
4500
|
const schema = ormSchemaName(definition.namespace);
|
|
1416
4501
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(definition.namespace);
|
|
1417
4502
|
const hints = applyMigrationHints(previous, definition);
|
|
1418
|
-
const snapshot = generateSnapshot(definition,
|
|
4503
|
+
const snapshot = generateSnapshot(definition, previousId2(previous));
|
|
1419
4504
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
1420
4505
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
1421
4506
|
const statements = [
|
|
1422
|
-
...policyDrops.map((
|
|
1423
|
-
...hints.statements.map((
|
|
1424
|
-
...ddl.map((
|
|
1425
|
-
...securityStatements(definition).map((
|
|
4507
|
+
...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
4508
|
+
...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
|
|
4509
|
+
...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
|
|
4510
|
+
...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
1426
4511
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
1427
4512
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
1428
4513
|
// above creates, so they close the plan.
|
|
1429
|
-
...actionPrepareChecks(definition).map((
|
|
1430
|
-
...actionFunctionStatements(definition).map((
|
|
4514
|
+
...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
|
|
4515
|
+
...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
|
|
1431
4516
|
];
|
|
1432
4517
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
1433
4518
|
}
|
|
1434
4519
|
var compileNamespace = (definition) => planMigration({ definition });
|
|
1435
|
-
var
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
)
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
definitions: Object.fromEntries(
|
|
1485
|
-
Object.entries(definitions).map(([key, def]) => [
|
|
1486
|
-
key,
|
|
1487
|
-
{
|
|
1488
|
-
...def.description !== void 0 ? { description: def.description } : {},
|
|
1489
|
-
default: def.default,
|
|
1490
|
-
inherits: [...def.inherits].sort(),
|
|
1491
|
-
grants: [...def.grants].sort()
|
|
1492
|
-
}
|
|
1493
|
-
])
|
|
1494
|
-
),
|
|
1495
|
-
capabilities: Object.fromEntries(
|
|
1496
|
-
Object.entries(declared).map(([key, cap]) => [
|
|
1497
|
-
key,
|
|
1498
|
-
cap.description !== void 0 ? { description: cap.description } : {}
|
|
1499
|
-
])
|
|
1500
|
-
)
|
|
1501
|
-
},
|
|
1502
|
-
uses: [...capabilities.uses].sort()
|
|
1503
|
-
};
|
|
1504
|
-
}
|
|
1505
|
-
function canonicalizeDefinition(definition) {
|
|
1506
|
-
return {
|
|
1507
|
-
namespace: definition.namespace,
|
|
1508
|
-
description: definition.description,
|
|
1509
|
-
capabilities: canonicalCapabilities(definition.capabilities),
|
|
1510
|
-
tables: definition.tables.map(canonicalTable)
|
|
1511
|
-
};
|
|
1512
|
-
}
|
|
1513
|
-
function computeDefinitionHash(definition) {
|
|
1514
|
-
return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(canonicalizeDefinition(definition))).digest("hex");
|
|
1515
|
-
}
|
|
1516
|
-
var CUSTOM_ROLE_KEY_RE = /^[a-z0-9-]+$/;
|
|
1517
|
-
var customRoleKeySchema = zod.z.string().min(1).max(64).regex(
|
|
1518
|
-
CUSTOM_ROLE_KEY_RE,
|
|
1519
|
-
"Role and capability keys must match [a-z0-9-]+"
|
|
4520
|
+
var previousId2 = (snapshot) => snapshot.id;
|
|
4521
|
+
var IR_VERSION = 1;
|
|
4522
|
+
var CLAIMS_CONTRACT = {
|
|
4523
|
+
sub: "string",
|
|
4524
|
+
org: "string?",
|
|
4525
|
+
roles: "string[]",
|
|
4526
|
+
capabilities: "string[]"
|
|
4527
|
+
};
|
|
4528
|
+
var scalarLiteral = zod.z.union([zod.z.string(), zod.z.number(), zod.z.boolean(), zod.z.null()]);
|
|
4529
|
+
var valueIrSchema = zod.z.discriminatedUnion("kind", [
|
|
4530
|
+
zod.z.object({ kind: zod.z.literal("literal"), value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)]) }),
|
|
4531
|
+
zod.z.object({ kind: zod.z.literal("variable"), name: zod.z.string() }),
|
|
4532
|
+
zod.z.object({ kind: zod.z.literal("claim"), key: zod.z.enum(["sub", "org"]) })
|
|
4533
|
+
]);
|
|
4534
|
+
var CMP_OPS = [
|
|
4535
|
+
"eq",
|
|
4536
|
+
"neq",
|
|
4537
|
+
"gt",
|
|
4538
|
+
"gte",
|
|
4539
|
+
"lt",
|
|
4540
|
+
"lte",
|
|
4541
|
+
"in",
|
|
4542
|
+
"nin",
|
|
4543
|
+
"like",
|
|
4544
|
+
"ilike",
|
|
4545
|
+
"isNull",
|
|
4546
|
+
"contains",
|
|
4547
|
+
"hasKey"
|
|
4548
|
+
];
|
|
4549
|
+
var predIrSchema = zod.z.lazy(
|
|
4550
|
+
() => zod.z.discriminatedUnion("kind", [
|
|
4551
|
+
zod.z.object({ kind: zod.z.literal("and"), operands: zod.z.array(predIrSchema).min(1) }),
|
|
4552
|
+
zod.z.object({ kind: zod.z.literal("or"), operands: zod.z.array(predIrSchema).min(1) }),
|
|
4553
|
+
zod.z.object({ kind: zod.z.literal("not"), operand: predIrSchema }),
|
|
4554
|
+
zod.z.object({ kind: zod.z.literal("cmp"), op: zod.z.enum(CMP_OPS), column: zod.z.string(), value: valueIrSchema }),
|
|
4555
|
+
zod.z.object({ kind: zod.z.literal("exists"), link: zod.z.string(), where: predIrSchema }),
|
|
4556
|
+
zod.z.object({
|
|
4557
|
+
kind: zod.z.literal("aggPredicate"),
|
|
4558
|
+
link: zod.z.string(),
|
|
4559
|
+
fn: zod.z.literal("count"),
|
|
4560
|
+
op: zod.z.enum(["eq", "neq", "gt", "gte", "lt", "lte"]),
|
|
4561
|
+
value: zod.z.number(),
|
|
4562
|
+
where: predIrSchema.optional()
|
|
4563
|
+
}),
|
|
4564
|
+
zod.z.object({ kind: zod.z.literal("capability"), key: zod.z.string() }),
|
|
4565
|
+
zod.z.object({ kind: zod.z.literal("role"), key: zod.z.string() }),
|
|
4566
|
+
zod.z.object({ kind: zod.z.literal("dynamicFilter"), slot: zod.z.string() }),
|
|
4567
|
+
zod.z.object({ kind: zod.z.literal("const"), value: zod.z.boolean() })
|
|
4568
|
+
])
|
|
1520
4569
|
);
|
|
1521
|
-
var
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
4570
|
+
var orderIrSchema = zod.z.discriminatedUnion("kind", [
|
|
4571
|
+
zod.z.object({
|
|
4572
|
+
kind: zod.z.literal("column"),
|
|
4573
|
+
column: zod.z.string(),
|
|
4574
|
+
/** Order through a to-one link: `link` is set and `column` is on the target. */
|
|
4575
|
+
link: zod.z.string().optional(),
|
|
4576
|
+
direction: zod.z.enum(["asc", "desc"]),
|
|
4577
|
+
nulls: zod.z.enum(["first", "last"]).optional()
|
|
4578
|
+
}),
|
|
4579
|
+
/** §4f: `$orderBy: [FooOrderBy!]` variable — validated + lowered at runtime. */
|
|
4580
|
+
zod.z.object({ kind: zod.z.literal("dynamic"), slot: zod.z.string() })
|
|
4581
|
+
]);
|
|
4582
|
+
var limitIrSchema = zod.z.object({
|
|
4583
|
+
/** Fixed value or a variable, clamped to [1, max]; absent → default. */
|
|
4584
|
+
variable: zod.z.string().optional(),
|
|
4585
|
+
value: zod.z.number().int().positive().optional(),
|
|
4586
|
+
default: zod.z.number().int().positive(),
|
|
4587
|
+
max: zod.z.number().int().positive()
|
|
1525
4588
|
});
|
|
1526
|
-
var
|
|
1527
|
-
|
|
4589
|
+
var selectionIrSchema = zod.z.lazy(
|
|
4590
|
+
() => zod.z.object({
|
|
4591
|
+
typename: zod.z.string(),
|
|
4592
|
+
columns: zod.z.array(zod.z.object({ alias: zod.z.string(), column: zod.z.string() })),
|
|
4593
|
+
relations: zod.z.array(zod.z.object({
|
|
4594
|
+
alias: zod.z.string(),
|
|
4595
|
+
link: zod.z.string(),
|
|
4596
|
+
selection: selectionIrSchema,
|
|
4597
|
+
where: predIrSchema.optional(),
|
|
4598
|
+
orderBy: zod.z.array(orderIrSchema).optional(),
|
|
4599
|
+
limit: limitIrSchema.optional(),
|
|
4600
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional()
|
|
4601
|
+
})),
|
|
4602
|
+
aggregates: zod.z.array(zod.z.object({
|
|
4603
|
+
alias: zod.z.string(),
|
|
4604
|
+
link: zod.z.string().optional(),
|
|
4605
|
+
where: predIrSchema.optional(),
|
|
4606
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
4607
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
4608
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
4609
|
+
nodes: selectionIrSchema.optional()
|
|
4610
|
+
}))
|
|
4611
|
+
})
|
|
4612
|
+
);
|
|
4613
|
+
var includeIfSchema = zod.z.object({ variable: zod.z.string(), negated: zod.z.boolean() });
|
|
4614
|
+
var selectRootSchema = zod.z.object({
|
|
4615
|
+
kind: zod.z.literal("select"),
|
|
4616
|
+
alias: zod.z.string(),
|
|
4617
|
+
table: zod.z.string(),
|
|
4618
|
+
selection: selectionIrSchema,
|
|
4619
|
+
/** Composed at compile time: AND(policy, documentWhere?, dynamicFilter?). */
|
|
4620
|
+
where: predIrSchema,
|
|
4621
|
+
orderBy: zod.z.array(orderIrSchema).default([]),
|
|
4622
|
+
distinctOn: zod.z.union([zod.z.array(zod.z.string()), zod.z.object({ slot: zod.z.string() })]).optional(),
|
|
4623
|
+
limit: limitIrSchema,
|
|
4624
|
+
offset: zod.z.object({ variable: zod.z.string().optional(), value: zod.z.number().int().optional() }).optional(),
|
|
4625
|
+
includeIf: includeIfSchema.optional()
|
|
4626
|
+
});
|
|
4627
|
+
var byPkRootSchema = zod.z.object({
|
|
4628
|
+
kind: zod.z.literal("byPk"),
|
|
4629
|
+
alias: zod.z.string(),
|
|
4630
|
+
table: zod.z.string(),
|
|
4631
|
+
selection: selectionIrSchema,
|
|
4632
|
+
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).min(1),
|
|
4633
|
+
/** Policy predicate — ANDed with the PK match. */
|
|
4634
|
+
where: predIrSchema,
|
|
4635
|
+
includeIf: includeIfSchema.optional()
|
|
4636
|
+
});
|
|
4637
|
+
var aggregateRootSchema = zod.z.object({
|
|
4638
|
+
kind: zod.z.literal("aggregate"),
|
|
4639
|
+
alias: zod.z.string(),
|
|
4640
|
+
table: zod.z.string(),
|
|
4641
|
+
where: predIrSchema,
|
|
4642
|
+
count: zod.z.object({ columns: zod.z.array(zod.z.string()).optional(), distinct: zod.z.boolean().optional() }).optional(),
|
|
4643
|
+
sum: zod.z.array(zod.z.string()).optional(),
|
|
4644
|
+
avg: zod.z.array(zod.z.string()).optional(),
|
|
4645
|
+
nodes: selectionIrSchema.optional(),
|
|
4646
|
+
includeIf: includeIfSchema.optional()
|
|
4647
|
+
});
|
|
4648
|
+
var writeValuesSchema = zod.z.union([
|
|
4649
|
+
zod.z.object({ slot: zod.z.string() }),
|
|
4650
|
+
zod.z.object({ entries: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })) })
|
|
4651
|
+
]);
|
|
4652
|
+
var onConflictIrSchema = zod.z.union([
|
|
4653
|
+
zod.z.object({ slot: zod.z.string() }),
|
|
4654
|
+
zod.z.object({ constraint: zod.z.string(), updateColumns: zod.z.array(zod.z.string()) })
|
|
4655
|
+
]);
|
|
4656
|
+
var mutationRootSchema = zod.z.object({
|
|
4657
|
+
kind: zod.z.literal("mutation"),
|
|
1528
4658
|
/**
|
|
1529
|
-
*
|
|
1530
|
-
*
|
|
1531
|
-
*
|
|
4659
|
+
* CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
|
|
4660
|
+
* synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
|
|
4661
|
+
* applies `set` — both statements generated by the executor from this
|
|
4662
|
+
* metadata, never authored SQL. `cowRevert` deletes the override so the
|
|
4663
|
+
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
4664
|
+
* row and carry the write policy in `where` like update/delete.
|
|
1532
4665
|
*/
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
/**
|
|
1537
|
-
|
|
4666
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
4667
|
+
alias: zod.z.string(),
|
|
4668
|
+
table: zod.z.string(),
|
|
4669
|
+
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
4670
|
+
cowSynced: zod.z.string().optional(),
|
|
4671
|
+
/** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
|
|
4672
|
+
cowCopyColumns: zod.z.array(zod.z.string()).optional(),
|
|
4673
|
+
/** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
|
|
4674
|
+
cowCopyCheck: predIrSchema.optional(),
|
|
4675
|
+
/** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
|
|
4676
|
+
objects: zod.z.union([
|
|
4677
|
+
zod.z.object({ slot: zod.z.string() }),
|
|
4678
|
+
zod.z.object({ rows: zod.z.array(zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema }))) })
|
|
4679
|
+
]).optional(),
|
|
4680
|
+
/** Server-pinned columns (e.g. author_sub ← claim('sub')) merged into every inserted row. */
|
|
4681
|
+
pinnedColumns: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).default([]),
|
|
4682
|
+
onConflict: onConflictIrSchema.optional(),
|
|
4683
|
+
set: writeValuesSchema.optional(),
|
|
4684
|
+
append: writeValuesSchema.optional(),
|
|
4685
|
+
inc: writeValuesSchema.optional(),
|
|
4686
|
+
/** update/delete row filter: AND(writePolicy, documentWhere/pk, dynamicFilter?). */
|
|
4687
|
+
where: predIrSchema.optional(),
|
|
4688
|
+
pk: zod.z.array(zod.z.object({ column: zod.z.string(), value: valueIrSchema })).optional(),
|
|
4689
|
+
/** WITH CHECK analogue evaluated against NEW row values (insert/update). */
|
|
4690
|
+
insertCheck: predIrSchema.optional(),
|
|
4691
|
+
returning: selectionIrSchema.nullable(),
|
|
4692
|
+
affectedRows: zod.z.boolean().default(false),
|
|
4693
|
+
includeIf: includeIfSchema.optional()
|
|
1538
4694
|
});
|
|
1539
|
-
var
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
code: "custom",
|
|
1557
|
-
message: `At most ${MAX_CUSTOM_ROLE_CAPABILITIES} capabilities are allowed`,
|
|
1558
|
-
path: ["capabilities"]
|
|
1559
|
-
});
|
|
1560
|
-
}
|
|
1561
|
-
for (const key of roleKeys) {
|
|
1562
|
-
if (capabilityKeys.has(key)) {
|
|
1563
|
-
ctx.addIssue({
|
|
1564
|
-
code: "custom",
|
|
1565
|
-
message: `Key "${key}" is declared as both a role and a capability`,
|
|
1566
|
-
path: ["definitions", key]
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
const roleKeySet = new Set(roleKeys);
|
|
1571
|
-
for (const [roleKey, definition] of Object.entries(block.definitions)) {
|
|
1572
|
-
const seenInherits = /* @__PURE__ */ new Set();
|
|
1573
|
-
for (const inherited of definition.inherits) {
|
|
1574
|
-
if (seenInherits.has(inherited)) {
|
|
1575
|
-
ctx.addIssue({
|
|
1576
|
-
code: "custom",
|
|
1577
|
-
message: `Role "${roleKey}" lists "${inherited}" in inherits more than once`,
|
|
1578
|
-
path: ["definitions", roleKey, "inherits"]
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
seenInherits.add(inherited);
|
|
1582
|
-
if (!roleKeySet.has(inherited)) {
|
|
1583
|
-
ctx.addIssue({
|
|
1584
|
-
code: "custom",
|
|
1585
|
-
message: `Role "${roleKey}" inherits unknown role "${inherited}"`,
|
|
1586
|
-
path: ["definitions", roleKey, "inherits"]
|
|
1587
|
-
});
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
const seenGrants = /* @__PURE__ */ new Set();
|
|
1591
|
-
for (const granted of definition.grants) {
|
|
1592
|
-
if (seenGrants.has(granted)) {
|
|
1593
|
-
ctx.addIssue({
|
|
1594
|
-
code: "custom",
|
|
1595
|
-
message: `Role "${roleKey}" lists "${granted}" in grants more than once`,
|
|
1596
|
-
path: ["definitions", roleKey, "grants"]
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
seenGrants.add(granted);
|
|
1600
|
-
if (!capabilityKeys.has(granted)) {
|
|
1601
|
-
ctx.addIssue({
|
|
1602
|
-
code: "custom",
|
|
1603
|
-
message: `Role "${roleKey}" grants unknown capability "${granted}"`,
|
|
1604
|
-
path: ["definitions", roleKey, "grants"]
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
const state = /* @__PURE__ */ new Map();
|
|
1610
|
-
const visit = (key, path) => {
|
|
1611
|
-
if (state.get(key) === "done") return null;
|
|
1612
|
-
if (state.get(key) === "visiting") return [...path, key];
|
|
1613
|
-
state.set(key, "visiting");
|
|
1614
|
-
const definition = block.definitions[key];
|
|
1615
|
-
for (const inherited of definition?.inherits ?? []) {
|
|
1616
|
-
if (!roleKeySet.has(inherited)) continue;
|
|
1617
|
-
const cycle = visit(inherited, [...path, key]);
|
|
1618
|
-
if (cycle) return cycle;
|
|
1619
|
-
}
|
|
1620
|
-
state.set(key, "done");
|
|
1621
|
-
return null;
|
|
1622
|
-
};
|
|
1623
|
-
for (const roleKey of roleKeys) {
|
|
1624
|
-
const cycle = visit(roleKey, []);
|
|
1625
|
-
if (cycle) {
|
|
1626
|
-
ctx.addIssue({
|
|
1627
|
-
code: "custom",
|
|
1628
|
-
message: `Role inheritance cycle: ${cycle.join(" -> ")}`,
|
|
1629
|
-
path: ["definitions"]
|
|
1630
|
-
});
|
|
1631
|
-
break;
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
4695
|
+
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
4696
|
+
selectRootSchema,
|
|
4697
|
+
byPkRootSchema,
|
|
4698
|
+
aggregateRootSchema,
|
|
4699
|
+
mutationRootSchema
|
|
4700
|
+
]);
|
|
4701
|
+
var INPUT_REF_KINDS = [
|
|
4702
|
+
"boolExp",
|
|
4703
|
+
"orderBy",
|
|
4704
|
+
"selectColumn",
|
|
4705
|
+
"insertInput",
|
|
4706
|
+
"setInput",
|
|
4707
|
+
"appendInput",
|
|
4708
|
+
"onConflict"
|
|
4709
|
+
];
|
|
4710
|
+
var variableDefaultIrSchema = zod.z.object({
|
|
4711
|
+
value: zod.z.union([scalarLiteral, zod.z.array(scalarLiteral)])
|
|
1634
4712
|
});
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
)
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
4713
|
+
var variableIrSchema = zod.z.object({
|
|
4714
|
+
name: zod.z.string(),
|
|
4715
|
+
type: zod.z.union([
|
|
4716
|
+
zod.z.object({ kind: zod.z.literal("scalar"), scalar: zod.z.enum(COLUMN_TYPES) }),
|
|
4717
|
+
/** `values` is the declared membership whitelist — the executor validates against it. */
|
|
4718
|
+
zod.z.object({ kind: zod.z.literal("enum"), name: zod.z.string(), values: zod.z.array(zod.z.string()).min(1) }),
|
|
4719
|
+
/** §4f dynamic slot: validated against `tables[table]` whitelists at runtime. */
|
|
4720
|
+
zod.z.object({ kind: zod.z.literal("inputRef"), input: zod.z.enum(INPUT_REF_KINDS), table: zod.z.string() })
|
|
4721
|
+
]),
|
|
4722
|
+
nullable: zod.z.boolean(),
|
|
4723
|
+
list: zod.z.boolean(),
|
|
4724
|
+
/** Absent = no default declared; see {@link variableDefaultIrSchema}. */
|
|
4725
|
+
defaultValue: variableDefaultIrSchema.optional()
|
|
4726
|
+
});
|
|
4727
|
+
var compiledOperationSchema = zod.z.object({
|
|
4728
|
+
irVersion: zod.z.literal(IR_VERSION),
|
|
4729
|
+
name: zod.z.string(),
|
|
4730
|
+
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
4731
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
4732
|
+
requires: zod.z.object({
|
|
4733
|
+
capability: zod.z.string().optional(),
|
|
4734
|
+
role: zod.z.string().optional()
|
|
4735
|
+
}).nullable(),
|
|
4736
|
+
variables: zod.z.array(variableIrSchema),
|
|
4737
|
+
/** Every table the op touches — data-token scoping + audit. */
|
|
4738
|
+
tableSet: zod.z.array(zod.z.string()).min(1),
|
|
4739
|
+
roots: zod.z.array(rootIrSchema).min(1)
|
|
4740
|
+
});
|
|
4741
|
+
var tableMetaIrSchema = zod.z.object({
|
|
4742
|
+
table: zod.z.string(),
|
|
4743
|
+
schema: zod.z.string(),
|
|
4744
|
+
typename: zod.z.string(),
|
|
4745
|
+
/**
|
|
4746
|
+
* True when statements over this relation evaluate a claims-gated synced
|
|
4747
|
+
* view: synced twins (`source: gold.*`) materialize as views whose baked
|
|
4748
|
+
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
4749
|
+
* inherit the gate transitively through their declared dependencies. The
|
|
4750
|
+
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
4751
|
+
* touching such a relation, or the synced arm yields zero rows regardless
|
|
4752
|
+
* of the compiled predicates.
|
|
4753
|
+
*/
|
|
4754
|
+
claimsGated: zod.z.boolean().default(false),
|
|
4755
|
+
columns: zod.z.array(zod.z.object({
|
|
4756
|
+
name: zod.z.string(),
|
|
4757
|
+
columnName: zod.z.string(),
|
|
4758
|
+
type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
4759
|
+
nullable: zod.z.boolean()
|
|
4760
|
+
})),
|
|
4761
|
+
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
4762
|
+
primaryKey: zod.z.array(zod.z.string()),
|
|
4763
|
+
links: zod.z.array(zod.z.object({
|
|
4764
|
+
name: zod.z.string(),
|
|
4765
|
+
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
4766
|
+
target: zod.z.string(),
|
|
4767
|
+
on: zod.z.string(),
|
|
4768
|
+
references: zod.z.string()
|
|
4769
|
+
})),
|
|
4770
|
+
/** §4f whitelists: what dynamic inputs may reference. */
|
|
4771
|
+
filterable: zod.z.array(zod.z.string()),
|
|
4772
|
+
orderable: zod.z.array(zod.z.string()),
|
|
4773
|
+
insertable: zod.z.array(zod.z.string()),
|
|
4774
|
+
settable: zod.z.array(zod.z.string()),
|
|
4775
|
+
appendable: zod.z.array(zod.z.string()),
|
|
4776
|
+
constraints: zod.z.array(zod.z.object({ name: zod.z.string(), columns: zod.z.array(zod.z.string()) })),
|
|
4777
|
+
/** Depth/node caps for dynamic filter trees on this table. */
|
|
4778
|
+
dynamicFilterMaxDepth: zod.z.number().int().positive().default(6),
|
|
4779
|
+
dynamicFilterMaxNodes: zod.z.number().int().positive().default(64)
|
|
4780
|
+
});
|
|
4781
|
+
var compiledArtifactSchema = zod.z.object({
|
|
4782
|
+
irVersion: zod.z.literal(IR_VERSION),
|
|
4783
|
+
namespace: zod.z.string(),
|
|
4784
|
+
schemaHash: zod.z.string(),
|
|
4785
|
+
opsHash: zod.z.string(),
|
|
4786
|
+
tables: zod.z.record(zod.z.string(), tableMetaIrSchema),
|
|
4787
|
+
operations: zod.z.record(zod.z.string(), compiledOperationSchema)
|
|
4788
|
+
});
|
|
4789
|
+
var IrVersionError = class extends Error {
|
|
4790
|
+
constructor(found) {
|
|
4791
|
+
super(`Unsupported IR version ${String(found)} \u2014 this executor supports up to ${IR_VERSION}. Upgrade @sequenceholdings/orm or re-push the namespace.`);
|
|
4792
|
+
this.name = "IrVersionError";
|
|
1664
4793
|
}
|
|
1665
|
-
|
|
4794
|
+
};
|
|
4795
|
+
function normalizeIr(raw) {
|
|
4796
|
+
if (!raw || typeof raw !== "object") throw new IrVersionError(void 0);
|
|
4797
|
+
const version = raw.irVersion;
|
|
4798
|
+
if (version !== IR_VERSION) throw new IrVersionError(version);
|
|
4799
|
+
return compiledArtifactSchema.parse(raw);
|
|
4800
|
+
}
|
|
4801
|
+
function indexByHash(artifact) {
|
|
4802
|
+
const map = /* @__PURE__ */ new Map();
|
|
4803
|
+
for (const op of Object.values(artifact.operations)) map.set(op.opHash, op);
|
|
4804
|
+
return map;
|
|
1666
4805
|
}
|
|
1667
4806
|
|
|
1668
4807
|
// src/schema/namespace.ts
|
|
@@ -1696,7 +4835,7 @@ function buildNamespaceDefinition(files, config) {
|
|
|
1696
4835
|
}
|
|
1697
4836
|
}
|
|
1698
4837
|
const capabilities = config?.capabilities ? { roles: config.capabilities.roles ?? EMPTY_CAPABILITIES.roles, uses: config.capabilities.uses ?? [] } : EMPTY_CAPABILITIES;
|
|
1699
|
-
const pinIssues = validateCapabilityPins({ uses: capabilities.uses, roles: capabilities.roles });
|
|
4838
|
+
const pinIssues = customRolesManifest.validateCapabilityPins({ uses: capabilities.uses, roles: capabilities.roles });
|
|
1700
4839
|
if (pinIssues.length > 0) throw new OrmDefinitionError(pinIssues.join("; "), "namespace.yaml");
|
|
1701
4840
|
const tables = files.map(({ table }) => table).sort((a, b) => a.table.localeCompare(b.table));
|
|
1702
4841
|
const byName = new Map(tables.map((table) => [table.table, table]));
|
|
@@ -1787,6 +4926,8 @@ function validateCapabilityRefs(files, declared) {
|
|
|
1787
4926
|
const referenced = [
|
|
1788
4927
|
...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
|
|
1789
4928
|
...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
|
|
4929
|
+
...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
|
|
4930
|
+
...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
|
|
1790
4931
|
...Object.values(table.actions ?? {}).flatMap((action) => [
|
|
1791
4932
|
...action.requires ? referencedCapabilities(action.requires.ast) : [],
|
|
1792
4933
|
...referencedExecuteCapabilities(action)
|
|
@@ -1806,8 +4947,8 @@ var namespaceConfigSchema = zod.z.strictObject({
|
|
|
1806
4947
|
namespace: namespaceSchema,
|
|
1807
4948
|
description: zod.z.string().optional(),
|
|
1808
4949
|
capabilities: zod.z.strictObject({
|
|
1809
|
-
roles: customRolesBlockSchema.optional(),
|
|
1810
|
-
uses: capabilityPinsSchema.optional()
|
|
4950
|
+
roles: customRolesManifest.customRolesBlockSchema.optional(),
|
|
4951
|
+
uses: customRolesManifest.capabilityPinsSchema.optional()
|
|
1811
4952
|
}).optional()
|
|
1812
4953
|
});
|
|
1813
4954
|
|
|
@@ -1920,8 +5061,8 @@ async function readMigrationWorkspace(dir, namespace) {
|
|
|
1920
5061
|
journalPath
|
|
1921
5062
|
);
|
|
1922
5063
|
}
|
|
1923
|
-
parsed.data.entries.forEach((entry,
|
|
1924
|
-
if (entry.ordinal !==
|
|
5064
|
+
parsed.data.entries.forEach((entry, index3) => {
|
|
5065
|
+
if (entry.ordinal !== index3 + 1) {
|
|
1925
5066
|
throw new OrmDefinitionError(`migration journal entries must be contiguous from 1 (found ${entry.name})`, journalPath);
|
|
1926
5067
|
}
|
|
1927
5068
|
});
|
|
@@ -1944,7 +5085,7 @@ async function readMigrationSql(dir, entry) {
|
|
|
1944
5085
|
throw new OrmDefinitionError("missing migration file", path$1);
|
|
1945
5086
|
});
|
|
1946
5087
|
}
|
|
1947
|
-
var checksumOf = (
|
|
5088
|
+
var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
|
|
1948
5089
|
var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
|
|
1949
5090
|
function renderMigrationSql({
|
|
1950
5091
|
name,
|
|
@@ -1970,12 +5111,12 @@ async function writeMigration({
|
|
|
1970
5111
|
const tip = workspace.journal.entries.at(-1);
|
|
1971
5112
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
1972
5113
|
const name = migrationName(ordinal, contentHash);
|
|
1973
|
-
const
|
|
5114
|
+
const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
1974
5115
|
const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
1975
5116
|
await promises.mkdir(metaDir, { recursive: true });
|
|
1976
5117
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
1977
5118
|
`;
|
|
1978
|
-
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
5119
|
+
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
|
|
1979
5120
|
await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
1980
5121
|
const journal = {
|
|
1981
5122
|
...workspace.journal,
|
|
@@ -1986,7 +5127,7 @@ async function writeMigration({
|
|
|
1986
5127
|
name,
|
|
1987
5128
|
content_hash: contentHash,
|
|
1988
5129
|
previous_content_hash: tip?.content_hash ?? null,
|
|
1989
|
-
checksum: checksumOf(
|
|
5130
|
+
checksum: checksumOf(sql4),
|
|
1990
5131
|
snapshot_checksum: checksumOf(snapshotText),
|
|
1991
5132
|
destructive
|
|
1992
5133
|
}
|
|
@@ -2098,11 +5239,11 @@ async function checkWorkspace({
|
|
|
2098
5239
|
log(`no committed migrations for '${loaded.definition.namespace}' \u2014 run \`seq-studio orm diff\` and commit the result`);
|
|
2099
5240
|
return 1;
|
|
2100
5241
|
}
|
|
2101
|
-
let
|
|
5242
|
+
let previousId3;
|
|
2102
5243
|
let previousHash = null;
|
|
2103
5244
|
for (const entry of workspace.journal.entries) {
|
|
2104
|
-
const
|
|
2105
|
-
if (checksumOf(
|
|
5245
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
5246
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
2106
5247
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
2107
5248
|
return 1;
|
|
2108
5249
|
}
|
|
@@ -2112,7 +5253,7 @@ async function checkWorkspace({
|
|
|
2112
5253
|
return 1;
|
|
2113
5254
|
}
|
|
2114
5255
|
const snapshot = JSON.parse(snapshotText);
|
|
2115
|
-
if (entry.ordinal > 1 && snapshot.prevId !==
|
|
5256
|
+
if (entry.ordinal > 1 && snapshot.prevId !== previousId3) {
|
|
2116
5257
|
log(`snapshot chain broken at ${entry.name} \u2014 regenerate the workspace with \`seq-studio orm diff\``);
|
|
2117
5258
|
return 1;
|
|
2118
5259
|
}
|
|
@@ -2120,7 +5261,7 @@ async function checkWorkspace({
|
|
|
2120
5261
|
log(`content-hash chain broken at ${entry.name} \u2014 migrations must be appended in order by \`seq-studio orm diff\``);
|
|
2121
5262
|
return 1;
|
|
2122
5263
|
}
|
|
2123
|
-
|
|
5264
|
+
previousId3 = snapshot.id;
|
|
2124
5265
|
previousHash = entry.content_hash;
|
|
2125
5266
|
}
|
|
2126
5267
|
if (tip.content_hash !== loaded.contentHash) {
|
|
@@ -2142,12 +5283,145 @@ async function checkWorkspace({
|
|
|
2142
5283
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
2143
5284
|
return 0;
|
|
2144
5285
|
}
|
|
2145
|
-
var firstLine = (
|
|
5286
|
+
var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
5287
|
+
|
|
5288
|
+
// src/migrations/from-yaml.ts
|
|
5289
|
+
function convertV1DefinitionToCanonical(definition) {
|
|
5290
|
+
const raw = {
|
|
5291
|
+
definition_format: CANONICAL_FORMAT,
|
|
5292
|
+
namespace: definition.namespace,
|
|
5293
|
+
description: definition.description ?? void 0,
|
|
5294
|
+
enums: [],
|
|
5295
|
+
tables: definition.tables.map(convertTable),
|
|
5296
|
+
capabilities: {
|
|
5297
|
+
roles: definition.capabilities.roles,
|
|
5298
|
+
uses: [...definition.capabilities.uses]
|
|
5299
|
+
}
|
|
5300
|
+
};
|
|
5301
|
+
const parsed = canonicalDefinitionSchema.safeParse(raw);
|
|
5302
|
+
if (!parsed.success) {
|
|
5303
|
+
const issue = parsed.error.issues[0];
|
|
5304
|
+
throw new OrmDefinitionError(
|
|
5305
|
+
`converted definition is not valid canonical v2: ${issue ? `${issue.path.join(".")}: ${issue.message}` : "unknown issue"}`
|
|
5306
|
+
);
|
|
5307
|
+
}
|
|
5308
|
+
return normalizeCanonical(parsed.data);
|
|
5309
|
+
}
|
|
5310
|
+
function convertTable(table) {
|
|
5311
|
+
const columns = Object.entries(table.columns ?? {});
|
|
5312
|
+
const primaryKey2 = columns.filter(([, column]) => column.primary_key).map(([name]) => name);
|
|
5313
|
+
return {
|
|
5314
|
+
table: table.table,
|
|
5315
|
+
description: table.description,
|
|
5316
|
+
source: table.source,
|
|
5317
|
+
columns: columns.map(([name, column]) => convertColumn(name, column)),
|
|
5318
|
+
// Synced tables may declare a typed column/PK contract; untyped remain empty.
|
|
5319
|
+
primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
|
|
5320
|
+
links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
|
|
5321
|
+
indexes: (table.indexes ?? []).map((index3) => ({
|
|
5322
|
+
columns: [...index3.columns],
|
|
5323
|
+
unique: index3.unique ?? false,
|
|
5324
|
+
name: index3.name
|
|
5325
|
+
})),
|
|
5326
|
+
policies: {
|
|
5327
|
+
// Verbatim DSL source strings — the parsed v1 AST is derived state.
|
|
5328
|
+
read: table.policies?.read?.source,
|
|
5329
|
+
write: table.policies?.write?.source
|
|
5330
|
+
},
|
|
5331
|
+
// v2 has no action surface (writes are mutations); the transition sweep
|
|
5332
|
+
// drops the plpgsql action_* functions, so carrying the metadata would be
|
|
5333
|
+
// dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
|
|
5334
|
+
actions: []
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
function convertColumn(name, column) {
|
|
5338
|
+
const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
|
|
5339
|
+
return {
|
|
5340
|
+
name,
|
|
5341
|
+
columnName: name,
|
|
5342
|
+
type: column.type,
|
|
5343
|
+
nullable: column.primary_key ? false : column.nullable !== false,
|
|
5344
|
+
unique: column.unique === true,
|
|
5345
|
+
default: isDefaultFn ? void 0 : column.default,
|
|
5346
|
+
// isFunctionDefault guarantees one of the two allowlisted functions; the
|
|
5347
|
+
// canonicalDefinitionSchema parse below re-checks the enum.
|
|
5348
|
+
defaultFn: isDefaultFn ? column.default : void 0,
|
|
5349
|
+
description: column.description,
|
|
5350
|
+
renamedFrom: column.renamed_from,
|
|
5351
|
+
cast: column.cast,
|
|
5352
|
+
deprecated: column.deprecated === true
|
|
5353
|
+
};
|
|
5354
|
+
}
|
|
5355
|
+
function convertLink(table, name, link) {
|
|
5356
|
+
if (link.to.includes(".")) {
|
|
5357
|
+
throw new OrmDefinitionError(
|
|
5358
|
+
`link '${name}' on '${table}' targets '${link.to}' \u2014 cross-namespace links have no canonical v2 representation (links are namespace-local in v2)`
|
|
5359
|
+
);
|
|
5360
|
+
}
|
|
5361
|
+
return {
|
|
5362
|
+
name,
|
|
5363
|
+
kind: "toOne",
|
|
5364
|
+
target: link.to,
|
|
5365
|
+
on: link.on,
|
|
5366
|
+
enforce: link.enforce === true,
|
|
5367
|
+
description: link.description
|
|
5368
|
+
};
|
|
5369
|
+
}
|
|
5370
|
+
|
|
5371
|
+
// src/migrations/v2.ts
|
|
5372
|
+
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
5373
|
+
function rebaseViolations(statements, options) {
|
|
5374
|
+
const allowSecurity = options?.allowSecurityReemission ?? false;
|
|
5375
|
+
return statements.filter((statement) => !(statement.kind === "security" && allowSecurity));
|
|
5376
|
+
}
|
|
5377
|
+
var verifyRebaseIsNoop = (plan, options) => rebaseViolations(plan.statements, options).length === 0;
|
|
5378
|
+
function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, snapshot }) {
|
|
5379
|
+
if (!SHA256_HEX.test(previousHash)) {
|
|
5380
|
+
throw new OrmDefinitionError(`rebase previousHash is not a sha256 hex digest: '${previousHash}'`);
|
|
5381
|
+
}
|
|
5382
|
+
if (!SHA256_HEX.test(newHash)) {
|
|
5383
|
+
throw new OrmDefinitionError(`rebase newHash is not a sha256 hex digest: '${newHash}'`);
|
|
5384
|
+
}
|
|
5385
|
+
if (previousHash === newHash) {
|
|
5386
|
+
throw new OrmDefinitionError("rebase previousHash and newHash are identical \u2014 nothing to rebase");
|
|
5387
|
+
}
|
|
5388
|
+
if (!Number.isInteger(ordinal) || ordinal < 2) {
|
|
5389
|
+
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5390
|
+
}
|
|
5391
|
+
const name = migrationName(ordinal, newHash);
|
|
5392
|
+
const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
5393
|
+
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5394
|
+
`;
|
|
5395
|
+
const entry = {
|
|
5396
|
+
ordinal,
|
|
5397
|
+
name,
|
|
5398
|
+
content_hash: newHash,
|
|
5399
|
+
previous_content_hash: previousHash,
|
|
5400
|
+
checksum: checksumOf(sql4),
|
|
5401
|
+
snapshot_checksum: checksumOf(snapshotText),
|
|
5402
|
+
destructive: false
|
|
5403
|
+
};
|
|
5404
|
+
return { name, entry, sql: sql4, snapshotText };
|
|
5405
|
+
}
|
|
5406
|
+
function renderRebaseMigrationSql({
|
|
5407
|
+
name,
|
|
5408
|
+
namespace,
|
|
5409
|
+
previousHash,
|
|
5410
|
+
newHash
|
|
5411
|
+
}) {
|
|
5412
|
+
return [
|
|
5413
|
+
`-- ${name}`,
|
|
5414
|
+
`-- namespace: ${namespace}`,
|
|
5415
|
+
`-- content hash: ${newHash}`,
|
|
5416
|
+
`-- rebase: v1 definitions (${previousHash}) converted to canonical v2 \u2014 verified no-op, zero statements`,
|
|
5417
|
+
"-- generated by `seq-studio orm diff --rebase` \u2014 review and commit, do not edit"
|
|
5418
|
+
].join("\n") + "\n";
|
|
5419
|
+
}
|
|
2146
5420
|
|
|
2147
5421
|
// src/schema/canonical.ts
|
|
2148
5422
|
function parseCanonicalDefinition(value) {
|
|
2149
5423
|
const canonical = asCanonicalShape(value);
|
|
2150
|
-
const files = canonical.tables.map((table,
|
|
5424
|
+
const files = canonical.tables.map((table, index3) => {
|
|
2151
5425
|
const input = {
|
|
2152
5426
|
namespace: canonical.namespace,
|
|
2153
5427
|
table: table["table"],
|
|
@@ -2164,10 +5438,10 @@ function parseCanonicalDefinition(value) {
|
|
|
2164
5438
|
const issue = parsed.error.issues[0];
|
|
2165
5439
|
throw new OrmDefinitionError(
|
|
2166
5440
|
`stored definition for table ${String(table["table"])} is invalid: ${issue?.message ?? "unknown issue"}`,
|
|
2167
|
-
`canonical[${
|
|
5441
|
+
`canonical[${index3}]`
|
|
2168
5442
|
);
|
|
2169
5443
|
}
|
|
2170
|
-
return { file: `canonical[${
|
|
5444
|
+
return { file: `canonical[${index3}]`, table: parsed.data };
|
|
2171
5445
|
});
|
|
2172
5446
|
const config = namespaceConfigSchema.safeParse({
|
|
2173
5447
|
namespace: canonical.namespace,
|
|
@@ -2200,8 +5474,9 @@ function canonicalPolicies(value) {
|
|
|
2200
5474
|
if (value === null || typeof value !== "object") return void 0;
|
|
2201
5475
|
const record = value;
|
|
2202
5476
|
const policies = {};
|
|
2203
|
-
|
|
2204
|
-
|
|
5477
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
5478
|
+
if (typeof record[kind] === "string") policies[kind] = record[kind];
|
|
5479
|
+
}
|
|
2205
5480
|
return Object.keys(policies).length > 0 ? policies : void 0;
|
|
2206
5481
|
}
|
|
2207
5482
|
function canonicalActions(value) {
|
|
@@ -2224,34 +5499,75 @@ function canonicalActions(value) {
|
|
|
2224
5499
|
);
|
|
2225
5500
|
}
|
|
2226
5501
|
|
|
5502
|
+
Object.defineProperty(exports, "capabilityPinsSchema", {
|
|
5503
|
+
enumerable: true,
|
|
5504
|
+
get: function () { return customRolesManifest.capabilityPinsSchema; }
|
|
5505
|
+
});
|
|
5506
|
+
Object.defineProperty(exports, "customRolesBlockSchema", {
|
|
5507
|
+
enumerable: true,
|
|
5508
|
+
get: function () { return customRolesManifest.customRolesBlockSchema; }
|
|
5509
|
+
});
|
|
5510
|
+
Object.defineProperty(exports, "customRolesDeclared", {
|
|
5511
|
+
enumerable: true,
|
|
5512
|
+
get: function () { return customRolesManifest.customRolesDeclared; }
|
|
5513
|
+
});
|
|
5514
|
+
Object.defineProperty(exports, "parseCapabilityPin", {
|
|
5515
|
+
enumerable: true,
|
|
5516
|
+
get: function () { return customRolesManifest.parseCapabilityPin; }
|
|
5517
|
+
});
|
|
5518
|
+
Object.defineProperty(exports, "validateCapabilityPins", {
|
|
5519
|
+
enumerable: true,
|
|
5520
|
+
get: function () { return customRolesManifest.validateCapabilityPins; }
|
|
5521
|
+
});
|
|
2227
5522
|
exports.ACTION_REQUIRES_DENIED_SQLSTATE = ACTION_REQUIRES_DENIED_SQLSTATE;
|
|
2228
5523
|
exports.ACTION_VALIDATES_FAILED_SQLSTATE = ACTION_VALIDATES_FAILED_SQLSTATE;
|
|
2229
5524
|
exports.AUDIT_ACTIONS = AUDIT_ACTIONS;
|
|
5525
|
+
exports.CANONICAL_FORMAT = CANONICAL_FORMAT;
|
|
2230
5526
|
exports.CAPABILITIES_CLAIM_KEY = CAPABILITIES_CLAIM_KEY;
|
|
2231
5527
|
exports.CAPABILITY_KEY_PATTERN = CAPABILITY_KEY_PATTERN;
|
|
5528
|
+
exports.CLAIMS_CONTRACT = CLAIMS_CONTRACT;
|
|
2232
5529
|
exports.CLAIMS_SETTING = CLAIMS_SETTING;
|
|
2233
5530
|
exports.CLAIM_KEY_PATTERN = CLAIM_KEY_PATTERN;
|
|
2234
|
-
exports.
|
|
5531
|
+
exports.CMP_OPS = CMP_OPS;
|
|
5532
|
+
exports.COLUMN_BRAND = COLUMN_BRAND;
|
|
5533
|
+
exports.COLUMN_DEFAULT_FNS = COLUMN_DEFAULT_FNS;
|
|
5534
|
+
exports.COLUMN_TYPES = COLUMN_TYPES2;
|
|
2235
5535
|
exports.COMPARE_OPS = COMPARE_OPS;
|
|
2236
5536
|
exports.CONSUMER_ROLE_SUFFIX = CONSUMER_ROLE_SUFFIX;
|
|
5537
|
+
exports.ColumnBuilder = ColumnBuilder;
|
|
2237
5538
|
exports.DEFINITION_FILE_EXTENSIONS = DEFINITION_FILE_EXTENSIONS;
|
|
5539
|
+
exports.ENUM_BRAND = ENUM_BRAND;
|
|
2238
5540
|
exports.HASH_DOMAIN = HASH_DOMAIN;
|
|
2239
5541
|
exports.IDENTIFIER_PATTERN = IDENTIFIER_PATTERN;
|
|
5542
|
+
exports.IDENTIFIER_RE = IDENTIFIER_RE;
|
|
5543
|
+
exports.INPUT_REF_KINDS = INPUT_REF_KINDS;
|
|
5544
|
+
exports.IR_VERSION = IR_VERSION;
|
|
5545
|
+
exports.IrVersionError = IrVersionError;
|
|
5546
|
+
exports.LINK_BRAND = LINK_BRAND;
|
|
2240
5547
|
exports.MAX_DEFINITION_FILE_BYTES = MAX_DEFINITION_FILE_BYTES;
|
|
2241
5548
|
exports.MAX_IDENTIFIER_LENGTH = MAX_IDENTIFIER_LENGTH;
|
|
2242
5549
|
exports.MIGRATIONS_DIRNAME = MIGRATIONS_DIRNAME;
|
|
5550
|
+
exports.NAMESPACE_CONFIG_BRAND = NAMESPACE_CONFIG_BRAND;
|
|
5551
|
+
exports.NAMESPACE_CONFIG_FILE = NAMESPACE_CONFIG_FILE;
|
|
2243
5552
|
exports.NAMESPACE_CONFIG_FILENAMES = NAMESPACE_CONFIG_FILENAMES;
|
|
2244
5553
|
exports.NAMESPACE_PATTERN = NAMESPACE_PATTERN;
|
|
2245
5554
|
exports.ORM_DATA_TOKEN_HEADER = ORM_DATA_TOKEN_HEADER;
|
|
2246
5555
|
exports.ORM_SCHEMA_PREFIX = ORM_SCHEMA_PREFIX;
|
|
2247
5556
|
exports.ORM_SERVICE_DATA_TOKEN_HEADER = ORM_SERVICE_DATA_TOKEN_HEADER;
|
|
5557
|
+
exports.ORM_TO_PIPELINE_TYPE = ORM_TO_PIPELINE_TYPE;
|
|
2248
5558
|
exports.OrmDefinitionError = OrmDefinitionError;
|
|
2249
5559
|
exports.PredicateParseError = PredicateParseError;
|
|
2250
5560
|
exports.QUALIFIED_TABLE_PATTERN = QUALIFIED_TABLE_PATTERN;
|
|
2251
5561
|
exports.RESERVED_COLUMN_NAMES = RESERVED_COLUMN_NAMES;
|
|
2252
5562
|
exports.RESERVED_TABLE_NAMES = RESERVED_TABLE_NAMES;
|
|
5563
|
+
exports.ROLES_CLAIM_KEY = ROLES_CLAIM_KEY;
|
|
5564
|
+
exports.SCHEMA_DIR = SCHEMA_DIR;
|
|
2253
5565
|
exports.STATEMENT_KINDS = STATEMENT_KINDS;
|
|
5566
|
+
exports.SYNCED_CONTRACT_HASH_DOMAIN = SYNCED_CONTRACT_HASH_DOMAIN;
|
|
2254
5567
|
exports.SYNCED_SOURCE_PATTERN = SYNCED_SOURCE_PATTERN;
|
|
5568
|
+
exports.SYNCED_VIEW_EXECUTION_OWNER_ROLE = SYNCED_VIEW_EXECUTION_OWNER_ROLE;
|
|
5569
|
+
exports.TABLE_BRAND = TABLE_BRAND;
|
|
5570
|
+
exports.VIEW_BRAND = VIEW_BRAND;
|
|
2255
5571
|
exports.actionFunctionName = actionFunctionName;
|
|
2256
5572
|
exports.actionFunctionStatements = actionFunctionStatements;
|
|
2257
5573
|
exports.actionParamArg = actionParamArg;
|
|
@@ -2261,71 +5577,138 @@ exports.actionPrepareChecks = actionPrepareChecks;
|
|
|
2261
5577
|
exports.actionSchema = actionSchema;
|
|
2262
5578
|
exports.actionWriteTargets = actionWriteTargets;
|
|
2263
5579
|
exports.actionsInterfaceName = actionsInterfaceName;
|
|
5580
|
+
exports.activeAuthoringContext = activeAuthoringContext;
|
|
2264
5581
|
exports.applyMigrationHints = applyMigrationHints;
|
|
5582
|
+
exports.applyMigrationHintsV2 = applyMigrationHintsV2;
|
|
5583
|
+
exports.beginAuthoringContext = beginAuthoringContext;
|
|
2265
5584
|
exports.buildActionParamsSchema = buildActionParamsSchema;
|
|
2266
5585
|
exports.buildDrizzleEntities = buildDrizzleEntities;
|
|
5586
|
+
exports.buildDrizzleEntitiesV2 = buildDrizzleEntitiesV2;
|
|
2267
5587
|
exports.buildNamespaceDefinition = buildNamespaceDefinition;
|
|
5588
|
+
exports.buildRebaseJournalEntry = buildRebaseJournalEntry;
|
|
5589
|
+
exports.canonicalActionSchema = canonicalActionSchema;
|
|
5590
|
+
exports.canonicalColumnSchema = canonicalColumnSchema;
|
|
5591
|
+
exports.canonicalDefinitionSchema = canonicalDefinitionSchema;
|
|
5592
|
+
exports.canonicalEnumSchema = canonicalEnumSchema;
|
|
5593
|
+
exports.canonicalIndexSchema = canonicalIndexSchema;
|
|
5594
|
+
exports.canonicalLinkSchema = canonicalLinkSchema;
|
|
5595
|
+
exports.canonicalPoliciesSchema = canonicalPoliciesSchema;
|
|
5596
|
+
exports.canonicalRelations = canonicalRelations;
|
|
5597
|
+
exports.canonicalTableSchema = canonicalTableSchema;
|
|
5598
|
+
exports.canonicalViewSchema = canonicalViewSchema;
|
|
2268
5599
|
exports.canonicalizeDefinition = canonicalizeDefinition;
|
|
2269
5600
|
exports.capabilityExpression = capabilityExpression;
|
|
2270
|
-
exports.capabilityPinsSchema = capabilityPinsSchema;
|
|
2271
5601
|
exports.castParts = castParts;
|
|
2272
5602
|
exports.checksumOf = checksumOf;
|
|
2273
5603
|
exports.claimExpression = claimExpression;
|
|
2274
5604
|
exports.classifyDestructive = classifyDestructive;
|
|
2275
5605
|
exports.columnSchema = columnSchema;
|
|
2276
|
-
exports.columnTypeSchema =
|
|
5606
|
+
exports.columnTypeSchema = columnTypeSchema2;
|
|
2277
5607
|
exports.compileActionRequires = compileActionRequires;
|
|
2278
5608
|
exports.compileActionStatements = compileActionStatements;
|
|
2279
5609
|
exports.compileNamespace = compileNamespace;
|
|
5610
|
+
exports.compilePredIrToSql = compilePredIrToSql;
|
|
2280
5611
|
exports.compilePredicate = compilePredicate;
|
|
5612
|
+
exports.compiledArtifactSchema = compiledArtifactSchema;
|
|
5613
|
+
exports.compiledOperationSchema = compiledOperationSchema;
|
|
5614
|
+
exports.computeCanonicalHash = computeCanonicalHash;
|
|
2281
5615
|
exports.computeDefinitionHash = computeDefinitionHash;
|
|
5616
|
+
exports.computeSyncedContractHash = computeSyncedContractHash;
|
|
2282
5617
|
exports.consumerRoleName = consumerRoleName;
|
|
2283
|
-
exports.
|
|
2284
|
-
exports.customRolesDeclared = customRolesDeclared;
|
|
5618
|
+
exports.convertV1DefinitionToCanonical = convertV1DefinitionToCanonical;
|
|
2285
5619
|
exports.declaredCapabilityKeys = declaredCapabilityKeys;
|
|
5620
|
+
exports.defineEnum = defineEnum;
|
|
5621
|
+
exports.defineNamespace = defineNamespace;
|
|
5622
|
+
exports.defineSyncedTable = defineSyncedTable;
|
|
5623
|
+
exports.defineTable = defineTable;
|
|
5624
|
+
exports.defineView = defineView;
|
|
5625
|
+
exports.deriveColumnName = deriveColumnName;
|
|
2286
5626
|
exports.diffCommand = diffCommand;
|
|
2287
5627
|
exports.diffStatements = diffStatements;
|
|
2288
5628
|
exports.emptyJournal = emptyJournal;
|
|
2289
5629
|
exports.emptySnapshot = emptySnapshot;
|
|
5630
|
+
exports.endAuthoringContext = endAuthoringContext;
|
|
2290
5631
|
exports.extractNamedParams = extractNamedParams;
|
|
5632
|
+
exports.extractOrmSyncedTableContract = extractOrmSyncedTableContract;
|
|
2291
5633
|
exports.generateNamespaceTypes = generateNamespaceTypes;
|
|
2292
5634
|
exports.generateSnapshot = generateSnapshot;
|
|
5635
|
+
exports.generateSnapshotV2 = generateSnapshotV2;
|
|
2293
5636
|
exports.identifierSchema = identifierSchema;
|
|
5637
|
+
exports.indexByHash = indexByHash;
|
|
5638
|
+
exports.isColumnBuilder = isColumnBuilder;
|
|
5639
|
+
exports.isEnumRef = isEnumRef;
|
|
2294
5640
|
exports.isFunctionDefault = isFunctionDefault;
|
|
2295
5641
|
exports.isOwnedStatement = isOwnedStatement;
|
|
5642
|
+
exports.isOwnedStatementV2 = isOwnedStatementV2;
|
|
2296
5643
|
exports.isQualifiedReference = isQualifiedReference;
|
|
5644
|
+
exports.isSeqLink = isSeqLink;
|
|
5645
|
+
exports.isSeqNamespaceConfig = isSeqNamespaceConfig;
|
|
5646
|
+
exports.isSeqTable = isSeqTable;
|
|
5647
|
+
exports.isSeqView = isSeqView;
|
|
5648
|
+
exports.limitIrSchema = limitIrSchema;
|
|
2297
5649
|
exports.loadCommittedMigrations = loadCommittedMigrations;
|
|
2298
5650
|
exports.loadCommittedTipMigration = loadCommittedTipMigration;
|
|
2299
5651
|
exports.loadNamespaceDefinition = loadNamespaceDefinition;
|
|
5652
|
+
exports.loadNamespaceFromDir = loadNamespaceFromDir;
|
|
2300
5653
|
exports.migrationName = migrationName;
|
|
2301
5654
|
exports.namespaceConfigSchema = namespaceConfigSchema;
|
|
2302
5655
|
exports.namespaceSchema = namespaceSchema;
|
|
5656
|
+
exports.normalizeCanonical = normalizeCanonical;
|
|
5657
|
+
exports.normalizeIr = normalizeIr;
|
|
2303
5658
|
exports.normalizeSnapshot = normalizeSnapshot;
|
|
5659
|
+
exports.orderIrSchema = orderIrSchema;
|
|
2304
5660
|
exports.ormSchemaName = ormSchemaName;
|
|
5661
|
+
exports.overlayWinsUnion = overlayWinsUnion;
|
|
5662
|
+
exports.overrideRowMutation = overrideRowMutation;
|
|
2305
5663
|
exports.parseCanonicalDefinition = parseCanonicalDefinition;
|
|
2306
|
-
exports.parseCapabilityPin = parseCapabilityPin;
|
|
2307
5664
|
exports.parseDefinitionSources = parseDefinitionSources;
|
|
2308
5665
|
exports.parsePredicate = parsePredicate;
|
|
2309
5666
|
exports.parseTableFile = parseTableFile;
|
|
2310
5667
|
exports.parsedPredicateSchema = parsedPredicateSchema;
|
|
2311
5668
|
exports.pendingKitStatements = pendingKitStatements;
|
|
5669
|
+
exports.pendingKitStatementsV2 = pendingKitStatementsV2;
|
|
2312
5670
|
exports.planMigration = planMigration;
|
|
5671
|
+
exports.planMigrationV2 = planMigrationV2;
|
|
5672
|
+
exports.policyExpressionSchema = policyExpressionSchema;
|
|
5673
|
+
exports.policyVariantSchema = policyVariantSchema;
|
|
5674
|
+
exports.predIrSchema = predIrSchema;
|
|
2313
5675
|
exports.predicateOperands = predicateOperands;
|
|
2314
5676
|
exports.quoteIdentifier = quoteIdentifier;
|
|
2315
5677
|
exports.quoteLiteral = quoteLiteral;
|
|
2316
5678
|
exports.readMigrationSql = readMigrationSql;
|
|
2317
5679
|
exports.readMigrationWorkspace = readMigrationWorkspace;
|
|
2318
5680
|
exports.readSnapshot = readSnapshot;
|
|
5681
|
+
exports.rebaseViolations = rebaseViolations;
|
|
2319
5682
|
exports.referencedCapabilities = referencedCapabilities;
|
|
2320
5683
|
exports.referencedColumns = referencedColumns;
|
|
2321
5684
|
exports.renderMigrationSql = renderMigrationSql;
|
|
5685
|
+
exports.revertOverlayMutation = revertOverlayMutation;
|
|
5686
|
+
exports.rewriteSyncedSourceRelationsInViewQuery = rewriteSyncedSourceRelationsInViewQuery;
|
|
5687
|
+
exports.roleExpression = roleExpression;
|
|
5688
|
+
exports.rootIrSchema = rootIrSchema;
|
|
2322
5689
|
exports.securityStatements = securityStatements;
|
|
5690
|
+
exports.selectionIrSchema = selectionIrSchema;
|
|
5691
|
+
exports.serializeNamespace = serializeNamespace;
|
|
2323
5692
|
exports.sqlTypeName = sqlTypeName;
|
|
2324
5693
|
exports.stableJson = stableJson;
|
|
5694
|
+
exports.syncedBackingRevokeStatements = syncedBackingRevokeStatements;
|
|
5695
|
+
exports.syncedContractSurface = syncedContractSurface;
|
|
5696
|
+
exports.syncedTables = syncedTables;
|
|
5697
|
+
exports.syncedViewDropStatements = syncedViewDropStatements;
|
|
5698
|
+
exports.syncedViewStatements = syncedViewStatements;
|
|
5699
|
+
exports.t = t;
|
|
2325
5700
|
exports.tableFileSchema = tableFileSchema;
|
|
5701
|
+
exports.tableMetaIrSchema = tableMetaIrSchema;
|
|
2326
5702
|
exports.tableReferenceSchema = tableReferenceSchema;
|
|
5703
|
+
exports.toMany = toMany;
|
|
5704
|
+
exports.toOne = toOne;
|
|
5705
|
+
exports.toPlatformUser = toPlatformUser;
|
|
2327
5706
|
exports.typesInterfaceName = typesInterfaceName;
|
|
2328
|
-
exports.
|
|
5707
|
+
exports.validateViewCatalogStatement = validateViewCatalogStatement;
|
|
5708
|
+
exports.valueIrSchema = valueIrSchema;
|
|
5709
|
+
exports.variableDefaultIrSchema = variableDefaultIrSchema;
|
|
5710
|
+
exports.variableIrSchema = variableIrSchema;
|
|
5711
|
+
exports.verifyRebaseIsNoop = verifyRebaseIsNoop;
|
|
2329
5712
|
exports.walkPredicate = walkPredicate;
|
|
2330
5713
|
exports.writeMigration = writeMigration;
|
|
2331
5714
|
//# sourceMappingURL=index.cjs.map
|