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