@sequenceholdings/orm 0.1.2 → 2.0.0
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 +49 -24
- package/dist/authoring.cjs +1072 -60
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +291 -26
- package/dist/authoring.d.ts +291 -26
- package/dist/authoring.js +1062 -61
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +415 -9
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +235 -1
- package/dist/canonical.d.ts +235 -1
- package/dist/canonical.js +411 -10
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1860 -228
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1861 -229
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +512 -54
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +39 -231
- package/dist/compiler.d.ts +39 -231
- package/dist/compiler.js +510 -55
- package/dist/compiler.js.map +1 -1
- 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 +2229 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -9
- package/dist/index.d.ts +125 -9
- package/dist/index.js +2205 -221
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +249 -14
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +20 -80
- package/dist/ir.d.ts +20 -80
- package/dist/ir.js +249 -14
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +1099 -0
- package/dist/predicate-v2.cjs.map +1 -0
- package/dist/predicate-v2.d.cts +142 -0
- package/dist/predicate-v2.d.ts +142 -0
- package/dist/predicate-v2.js +1089 -0
- package/dist/predicate-v2.js.map +1 -0
- package/dist/sdl.cjs +344 -55
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +1 -3
- package/dist/sdl.d.ts +1 -3
- package/dist/sdl.js +344 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +20 -3
- package/src/codegen-gql/preset.cjs +64 -0
- package/src/codegen-gql/server-fns.cjs +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -5
- package/templates/orm-namespace/codegen.preset.cjs +17 -0
- package/templates/orm-namespace/codegen.ts +1 -1
- package/templates/orm-namespace/package.json +3 -0
- package/templates/orm-namespace/schema/tables.ts +1 -1
- package/templates/orm-namespace/sequence.config.ts +1 -1
package/dist/cli.cjs
CHANGED
|
@@ -7,9 +7,9 @@ var url = require('url');
|
|
|
7
7
|
var jiti = require('jiti');
|
|
8
8
|
var crypto = require('crypto');
|
|
9
9
|
var zod = require('zod');
|
|
10
|
-
var graphql = require('graphql');
|
|
11
10
|
var drizzleOrm = require('drizzle-orm');
|
|
12
11
|
var pgCore = require('drizzle-orm/pg-core');
|
|
12
|
+
var graphql = require('graphql');
|
|
13
13
|
var api = require('drizzle-kit/api');
|
|
14
14
|
var jsYaml = require('js-yaml');
|
|
15
15
|
var customRolesManifest = require('@sequenceholdings/custom-roles-manifest');
|
|
@@ -111,8 +111,190 @@ var policyExpressionSchema = zod.z.union([
|
|
|
111
111
|
]);
|
|
112
112
|
var canonicalPoliciesSchema = zod.z.object({
|
|
113
113
|
read: policyExpressionSchema.optional(),
|
|
114
|
-
write: policyExpressionSchema.optional()
|
|
114
|
+
write: policyExpressionSchema.optional(),
|
|
115
|
+
insert: policyExpressionSchema.optional(),
|
|
116
|
+
update: policyExpressionSchema.optional()
|
|
115
117
|
});
|
|
118
|
+
function isSingleViewQuerySql(value) {
|
|
119
|
+
const query = value.trim();
|
|
120
|
+
if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
|
|
121
|
+
let state = "normal";
|
|
122
|
+
for (let index3 = 0; index3 < query.length; index3 += 1) {
|
|
123
|
+
const char = query[index3];
|
|
124
|
+
const next = query[index3 + 1];
|
|
125
|
+
if (state === "single") {
|
|
126
|
+
if (char === "'" && next === "'") index3 += 1;
|
|
127
|
+
else if (char === "'") state = "normal";
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (state === "double") {
|
|
131
|
+
if (char === '"' && next === '"') index3 += 1;
|
|
132
|
+
else if (char === '"') state = "normal";
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (char === "'") state = "single";
|
|
136
|
+
else if (char === '"') state = "double";
|
|
137
|
+
else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
|
|
138
|
+
}
|
|
139
|
+
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
140
|
+
}
|
|
141
|
+
var VIEW_QUERY_FUNCTION_ALLOWLIST = [
|
|
142
|
+
"abs",
|
|
143
|
+
"age",
|
|
144
|
+
"array_agg",
|
|
145
|
+
"array_append",
|
|
146
|
+
"array_cat",
|
|
147
|
+
"array_dims",
|
|
148
|
+
"array_length",
|
|
149
|
+
"array_lower",
|
|
150
|
+
"array_ndims",
|
|
151
|
+
"array_position",
|
|
152
|
+
"array_positions",
|
|
153
|
+
"array_prepend",
|
|
154
|
+
"array_remove",
|
|
155
|
+
"array_replace",
|
|
156
|
+
"array_to_string",
|
|
157
|
+
"array_trim",
|
|
158
|
+
"array_upper",
|
|
159
|
+
"ascii",
|
|
160
|
+
"avg",
|
|
161
|
+
"bit_and",
|
|
162
|
+
"bit_or",
|
|
163
|
+
"bit_xor",
|
|
164
|
+
"bool_and",
|
|
165
|
+
"bool_or",
|
|
166
|
+
"btrim",
|
|
167
|
+
"cardinality",
|
|
168
|
+
"cbrt",
|
|
169
|
+
"ceil",
|
|
170
|
+
"ceiling",
|
|
171
|
+
"char_length",
|
|
172
|
+
"character_length",
|
|
173
|
+
"chr",
|
|
174
|
+
"concat",
|
|
175
|
+
"concat_ws",
|
|
176
|
+
"count",
|
|
177
|
+
"cume_dist",
|
|
178
|
+
"date_bin",
|
|
179
|
+
"date_part",
|
|
180
|
+
"date_trunc",
|
|
181
|
+
"decode",
|
|
182
|
+
"degrees",
|
|
183
|
+
"dense_rank",
|
|
184
|
+
"encode",
|
|
185
|
+
"enum_first",
|
|
186
|
+
"enum_last",
|
|
187
|
+
"enum_range",
|
|
188
|
+
"every",
|
|
189
|
+
"exp",
|
|
190
|
+
"first_value",
|
|
191
|
+
"floor",
|
|
192
|
+
"format",
|
|
193
|
+
"grouping",
|
|
194
|
+
"initcap",
|
|
195
|
+
"json_agg",
|
|
196
|
+
"json_array_length",
|
|
197
|
+
"json_build_array",
|
|
198
|
+
"json_build_object",
|
|
199
|
+
"json_extract_path",
|
|
200
|
+
"json_extract_path_text",
|
|
201
|
+
"json_object_agg",
|
|
202
|
+
"json_object_keys",
|
|
203
|
+
"json_strip_nulls",
|
|
204
|
+
"json_typeof",
|
|
205
|
+
"jsonb_agg",
|
|
206
|
+
"jsonb_array_length",
|
|
207
|
+
"jsonb_build_array",
|
|
208
|
+
"jsonb_build_object",
|
|
209
|
+
"jsonb_extract_path",
|
|
210
|
+
"jsonb_extract_path_text",
|
|
211
|
+
"jsonb_insert",
|
|
212
|
+
"jsonb_object_agg",
|
|
213
|
+
"jsonb_object_keys",
|
|
214
|
+
"jsonb_path_exists",
|
|
215
|
+
"jsonb_path_match",
|
|
216
|
+
"jsonb_path_query",
|
|
217
|
+
"jsonb_path_query_array",
|
|
218
|
+
"jsonb_path_query_first",
|
|
219
|
+
"jsonb_pretty",
|
|
220
|
+
"jsonb_set",
|
|
221
|
+
"jsonb_set_lax",
|
|
222
|
+
"jsonb_strip_nulls",
|
|
223
|
+
"jsonb_typeof",
|
|
224
|
+
"justify_days",
|
|
225
|
+
"justify_hours",
|
|
226
|
+
"justify_interval",
|
|
227
|
+
"lag",
|
|
228
|
+
"last_value",
|
|
229
|
+
"lead",
|
|
230
|
+
"left",
|
|
231
|
+
"length",
|
|
232
|
+
"ln",
|
|
233
|
+
"log",
|
|
234
|
+
"lower",
|
|
235
|
+
"lpad",
|
|
236
|
+
"ltrim",
|
|
237
|
+
"make_date",
|
|
238
|
+
"make_interval",
|
|
239
|
+
"make_time",
|
|
240
|
+
"make_timestamp",
|
|
241
|
+
"make_timestamptz",
|
|
242
|
+
"max",
|
|
243
|
+
"md5",
|
|
244
|
+
"min",
|
|
245
|
+
"mod",
|
|
246
|
+
"nth_value",
|
|
247
|
+
"ntile",
|
|
248
|
+
"octet_length",
|
|
249
|
+
"percent_rank",
|
|
250
|
+
"pi",
|
|
251
|
+
"power",
|
|
252
|
+
"quote_ident",
|
|
253
|
+
"quote_literal",
|
|
254
|
+
"quote_nullable",
|
|
255
|
+
"radians",
|
|
256
|
+
"rank",
|
|
257
|
+
"regexp_count",
|
|
258
|
+
"regexp_instr",
|
|
259
|
+
"regexp_like",
|
|
260
|
+
"regexp_match",
|
|
261
|
+
"regexp_matches",
|
|
262
|
+
"regexp_replace",
|
|
263
|
+
"regexp_split_to_array",
|
|
264
|
+
"regexp_split_to_table",
|
|
265
|
+
"repeat",
|
|
266
|
+
"replace",
|
|
267
|
+
"reverse",
|
|
268
|
+
"right",
|
|
269
|
+
"round",
|
|
270
|
+
"row_number",
|
|
271
|
+
"rpad",
|
|
272
|
+
"rtrim",
|
|
273
|
+
"sign",
|
|
274
|
+
"split_part",
|
|
275
|
+
"sqrt",
|
|
276
|
+
"starts_with",
|
|
277
|
+
"string_agg",
|
|
278
|
+
"string_to_array",
|
|
279
|
+
"strpos",
|
|
280
|
+
"substr",
|
|
281
|
+
"substring",
|
|
282
|
+
"sum",
|
|
283
|
+
"timezone",
|
|
284
|
+
"to_ascii",
|
|
285
|
+
"to_char",
|
|
286
|
+
"to_date",
|
|
287
|
+
"to_hex",
|
|
288
|
+
"to_json",
|
|
289
|
+
"to_jsonb",
|
|
290
|
+
"to_number",
|
|
291
|
+
"to_timestamp",
|
|
292
|
+
"translate",
|
|
293
|
+
"trunc",
|
|
294
|
+
"unnest",
|
|
295
|
+
"upper",
|
|
296
|
+
"width_bucket"
|
|
297
|
+
];
|
|
116
298
|
var canonicalActionSchema = zod.z.object({
|
|
117
299
|
name: identifier,
|
|
118
300
|
requires: zod.z.string().max(1e3).optional(),
|
|
@@ -124,6 +306,37 @@ var canonicalActionSchema = zod.z.object({
|
|
|
124
306
|
returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
|
|
125
307
|
audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
|
|
126
308
|
});
|
|
309
|
+
var canonicalCowMutationSchema = zod.z.object({
|
|
310
|
+
kind: zod.z.enum(["override", "revert"]),
|
|
311
|
+
name: identifier,
|
|
312
|
+
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
313
|
+
synced: identifier,
|
|
314
|
+
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
315
|
+
pk: zod.z.string().min(1).max(63),
|
|
316
|
+
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
317
|
+
copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
|
|
318
|
+
/** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
|
|
319
|
+
set: zod.z.array(zod.z.string().min(1).max(63)).default([])
|
|
320
|
+
}).superRefine((mutation, ctx) => {
|
|
321
|
+
if (mutation.kind === "override") {
|
|
322
|
+
if (mutation.set.length === 0) {
|
|
323
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
|
|
324
|
+
}
|
|
325
|
+
if (!mutation.copy.includes(mutation.pk)) {
|
|
326
|
+
ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
|
|
327
|
+
}
|
|
328
|
+
} else if (mutation.set.length > 0 || mutation.copy.length > 0) {
|
|
329
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
|
|
330
|
+
}
|
|
331
|
+
if (mutation.set.includes(mutation.pk)) {
|
|
332
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
|
|
333
|
+
}
|
|
334
|
+
for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
|
|
335
|
+
if (new Set(values).size !== values.length) {
|
|
336
|
+
ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
127
340
|
var canonicalTableSchema = zod.z.object({
|
|
128
341
|
table: identifier,
|
|
129
342
|
description: zod.z.string().max(2e3).optional(),
|
|
@@ -136,7 +349,45 @@ var canonicalTableSchema = zod.z.object({
|
|
|
136
349
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
137
350
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
138
351
|
policies: canonicalPoliciesSchema.default({}),
|
|
139
|
-
|
|
352
|
+
/**
|
|
353
|
+
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
354
|
+
* neither compiles nor executes these; writes are mutations. Kept parseable
|
|
355
|
+
* so stored definitions and their content hashes stay stable.
|
|
356
|
+
*/
|
|
357
|
+
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
358
|
+
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
359
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
360
|
+
});
|
|
361
|
+
var canonicalViewSchema = zod.z.object({
|
|
362
|
+
view: identifier,
|
|
363
|
+
description: zod.z.string().max(2e3).optional(),
|
|
364
|
+
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
365
|
+
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
366
|
+
primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
|
|
367
|
+
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
368
|
+
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
369
|
+
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
370
|
+
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
371
|
+
}),
|
|
372
|
+
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
373
|
+
dependencies: zod.z.array(identifier).max(500)
|
|
374
|
+
}).superRefine((view, ctx) => {
|
|
375
|
+
for (const [index3, column] of view.columns.entries()) {
|
|
376
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
377
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare a default` });
|
|
378
|
+
}
|
|
379
|
+
if (column.unique) {
|
|
380
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare unique` });
|
|
381
|
+
}
|
|
382
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
383
|
+
ctx.addIssue({ code: "custom", path: ["columns", index3], message: `view column '${column.name}' cannot declare migration hints` });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
for (const [index3, link] of view.links.entries()) {
|
|
387
|
+
if (link.enforce) {
|
|
388
|
+
ctx.addIssue({ code: "custom", path: ["links", index3, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
140
391
|
});
|
|
141
392
|
var canonicalDefinitionSchema = zod.z.object({
|
|
142
393
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
@@ -144,24 +395,174 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
144
395
|
description: zod.z.string().max(2e3).optional(),
|
|
145
396
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
146
397
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
398
|
+
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
399
|
+
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
147
400
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
148
401
|
capabilities: zod.z.object({
|
|
149
402
|
roles: zod.z.unknown().optional(),
|
|
150
403
|
uses: zod.z.array(zod.z.string()).default([])
|
|
151
404
|
}).default({ uses: [] })
|
|
405
|
+
}).superRefine((definition, ctx) => {
|
|
406
|
+
const seen = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const [index3, table] of definition.tables.entries()) {
|
|
408
|
+
if (seen.has(table.table)) {
|
|
409
|
+
ctx.addIssue({ code: "custom", path: ["tables", index3, "table"], message: `duplicate relation '${table.table}'` });
|
|
410
|
+
}
|
|
411
|
+
seen.add(table.table);
|
|
412
|
+
}
|
|
413
|
+
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
414
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
415
|
+
const cowMutations = table.cowMutations ?? [];
|
|
416
|
+
if (cowMutations.length === 0) continue;
|
|
417
|
+
const path = (index3, ...rest) => ["tables", tableIndex, "cowMutations", index3, ...rest];
|
|
418
|
+
if (table.source !== void 0) {
|
|
419
|
+
ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
for (const [index3, mutation] of cowMutations.entries()) {
|
|
423
|
+
const synced = tableByName.get(mutation.synced);
|
|
424
|
+
if (!synced || synced.source === void 0) {
|
|
425
|
+
ctx.addIssue({
|
|
426
|
+
code: "custom",
|
|
427
|
+
path: path(index3, "synced"),
|
|
428
|
+
message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
|
|
429
|
+
});
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
|
|
433
|
+
const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
|
|
434
|
+
let columnsExist = true;
|
|
435
|
+
for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
|
|
436
|
+
if (!overlayColumnsByName.has(property)) {
|
|
437
|
+
columnsExist = false;
|
|
438
|
+
ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
442
|
+
if (!syncedColumnsByName.has(property)) {
|
|
443
|
+
columnsExist = false;
|
|
444
|
+
ctx.addIssue({ code: "custom", path: path(index3), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!columnsExist) continue;
|
|
448
|
+
const overlayPk = table.primaryKey ?? [];
|
|
449
|
+
const syncedPk = synced.primaryKey ?? [];
|
|
450
|
+
if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
|
|
451
|
+
ctx.addIssue({
|
|
452
|
+
code: "custom",
|
|
453
|
+
path: path(index3, "pk"),
|
|
454
|
+
message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
458
|
+
const overlayColumn = overlayColumnsByName.get(property);
|
|
459
|
+
const syncedColumn = syncedColumnsByName.get(property);
|
|
460
|
+
if (!overlayColumn || !syncedColumn) continue;
|
|
461
|
+
if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
|
|
462
|
+
ctx.addIssue({
|
|
463
|
+
code: "custom",
|
|
464
|
+
path: path(index3),
|
|
465
|
+
message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
if (overlayColumn.columnName !== syncedColumn.columnName) {
|
|
469
|
+
ctx.addIssue({
|
|
470
|
+
code: "custom",
|
|
471
|
+
path: path(index3),
|
|
472
|
+
message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
|
|
476
|
+
ctx.addIssue({
|
|
477
|
+
code: "custom",
|
|
478
|
+
path: path(index3),
|
|
479
|
+
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`
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
for (const [index3, view] of (definition.views ?? []).entries()) {
|
|
486
|
+
if (seen.has(view.view)) {
|
|
487
|
+
ctx.addIssue({ code: "custom", path: ["views", index3, "view"], message: `duplicate relation '${view.view}'` });
|
|
488
|
+
}
|
|
489
|
+
seen.add(view.view);
|
|
490
|
+
}
|
|
491
|
+
const views = definition.views ?? [];
|
|
492
|
+
const viewByName = new Map(views.map((view) => [view.view, view]));
|
|
493
|
+
for (const [viewIndex, view] of views.entries()) {
|
|
494
|
+
for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
|
|
495
|
+
if (!seen.has(dependency)) {
|
|
496
|
+
ctx.addIssue({
|
|
497
|
+
code: "custom",
|
|
498
|
+
path: ["views", viewIndex, "dependencies", dependencyIndex],
|
|
499
|
+
message: `view '${view.view}' depends on unknown relation '${dependency}'`
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
505
|
+
const visited = /* @__PURE__ */ new Set();
|
|
506
|
+
let cycleReported = false;
|
|
507
|
+
const visit = (name, path) => {
|
|
508
|
+
if (visited.has(name) || cycleReported) return;
|
|
509
|
+
if (visiting.has(name)) {
|
|
510
|
+
cycleReported = true;
|
|
511
|
+
ctx.addIssue({
|
|
512
|
+
code: "custom",
|
|
513
|
+
path: ["views"],
|
|
514
|
+
message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
visiting.add(name);
|
|
519
|
+
const view = viewByName.get(name);
|
|
520
|
+
for (const dependency of view?.dependencies ?? []) {
|
|
521
|
+
if (viewByName.has(dependency)) visit(dependency, [...path, name]);
|
|
522
|
+
}
|
|
523
|
+
visiting.delete(name);
|
|
524
|
+
visited.add(name);
|
|
525
|
+
};
|
|
526
|
+
for (const view of views) visit(view.view, []);
|
|
152
527
|
});
|
|
528
|
+
function canonicalRelations(definition) {
|
|
529
|
+
const views = (definition.views ?? []).map((view) => ({
|
|
530
|
+
table: view.view,
|
|
531
|
+
...view.description !== void 0 ? { description: view.description } : {},
|
|
532
|
+
columns: view.columns,
|
|
533
|
+
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
534
|
+
links: view.links,
|
|
535
|
+
indexes: [],
|
|
536
|
+
policies: { read: view.policies.read },
|
|
537
|
+
actions: []
|
|
538
|
+
}));
|
|
539
|
+
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
540
|
+
}
|
|
153
541
|
function normalizeCanonical(definition) {
|
|
154
542
|
const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
155
|
-
|
|
543
|
+
const normalized = {
|
|
156
544
|
...definition,
|
|
157
545
|
enums: sortByName(definition.enums),
|
|
158
|
-
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) =>
|
|
159
|
-
...t2
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
546
|
+
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t2) => {
|
|
547
|
+
const { cowMutations, ...rest } = t2;
|
|
548
|
+
return {
|
|
549
|
+
...rest,
|
|
550
|
+
links: sortByName(t2.links),
|
|
551
|
+
indexes: [...t2.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
552
|
+
actions: sortByName(t2.actions),
|
|
553
|
+
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
554
|
+
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
555
|
+
};
|
|
556
|
+
})
|
|
164
557
|
};
|
|
558
|
+
if (definition.views !== void 0) {
|
|
559
|
+
normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
|
|
560
|
+
...view,
|
|
561
|
+
links: sortByName(view.links),
|
|
562
|
+
dependencies: [...view.dependencies].sort()
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
return normalized;
|
|
165
566
|
}
|
|
166
567
|
|
|
167
568
|
// src/constants.ts
|
|
@@ -202,9 +603,13 @@ function canonicalTable(table) {
|
|
|
202
603
|
source: table.source ?? null,
|
|
203
604
|
links: emptyToNull(table.links),
|
|
204
605
|
indexes: emptyToNull(table.indexes),
|
|
606
|
+
// insert/update are omitted rather than nulled when absent, so definitions
|
|
607
|
+
// predating split policies keep the hash they were registered under.
|
|
205
608
|
policies: {
|
|
206
609
|
read: table.policies?.read?.source ?? null,
|
|
207
|
-
write: table.policies?.write?.source ?? null
|
|
610
|
+
write: table.policies?.write?.source ?? null,
|
|
611
|
+
...table.policies?.insert !== void 0 ? { insert: table.policies.insert.source } : {},
|
|
612
|
+
...table.policies?.update !== void 0 ? { update: table.policies.update.source } : {}
|
|
208
613
|
},
|
|
209
614
|
actions: emptyToNull(
|
|
210
615
|
Object.fromEntries(
|
|
@@ -276,7 +681,7 @@ function contextStack() {
|
|
|
276
681
|
return created;
|
|
277
682
|
}
|
|
278
683
|
function beginAuthoringContext() {
|
|
279
|
-
const context = { currentFile: void 0, tables: [], enums: [], configs: [] };
|
|
684
|
+
const context = { currentFile: void 0, tables: [], views: [], enums: [], configs: [] };
|
|
280
685
|
contextStack().push(context);
|
|
281
686
|
return context;
|
|
282
687
|
}
|
|
@@ -294,6 +699,155 @@ var TABLE_BRAND = "seq-table";
|
|
|
294
699
|
function isSeqTable(value) {
|
|
295
700
|
return typeof value === "object" && value !== null && value.kind === TABLE_BRAND;
|
|
296
701
|
}
|
|
702
|
+
var VIEW_BRAND = "seq-view";
|
|
703
|
+
function isSeqView(value) {
|
|
704
|
+
return typeof value === "object" && value !== null && value.kind === VIEW_BRAND;
|
|
705
|
+
}
|
|
706
|
+
var QUERY_COLUMN_FACTORIES = {
|
|
707
|
+
uuid: (name) => pgCore.uuid(name),
|
|
708
|
+
text: (name) => pgCore.text(name),
|
|
709
|
+
integer: (name) => pgCore.integer(name),
|
|
710
|
+
bigint: (name) => pgCore.bigint(name, { mode: "bigint" }),
|
|
711
|
+
numeric: (name) => pgCore.numeric(name),
|
|
712
|
+
double: (name) => pgCore.doublePrecision(name),
|
|
713
|
+
boolean: (name) => pgCore.boolean(name),
|
|
714
|
+
timestamptz: (name) => pgCore.timestamp(name, { withTimezone: true }),
|
|
715
|
+
date: (name) => pgCore.date(name),
|
|
716
|
+
jsonb: (name) => pgCore.jsonb(name)
|
|
717
|
+
};
|
|
718
|
+
function compileViewQueries(input) {
|
|
719
|
+
const schemaName = `orm_${input.namespace}`;
|
|
720
|
+
const schema = pgCore.pgSchema(schemaName);
|
|
721
|
+
const enumFactories = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const entry of input.enums) {
|
|
723
|
+
const values = entry.values;
|
|
724
|
+
enumFactories.set(entry.name, schema.enum(entry.name, values));
|
|
725
|
+
}
|
|
726
|
+
const handles = /* @__PURE__ */ new Map();
|
|
727
|
+
const physicalToLogical = /* @__PURE__ */ new Map();
|
|
728
|
+
const tableByName = new Map(input.tables.map((table) => [table.table, table]));
|
|
729
|
+
for (const authored of input.authoredTables) {
|
|
730
|
+
const canonical = tableByName.get(authored.tableName);
|
|
731
|
+
if (!canonical) continue;
|
|
732
|
+
const physicalSchema = schemaName;
|
|
733
|
+
const physicalName = canonical.table;
|
|
734
|
+
const relationSchema = pgCore.pgSchema(physicalSchema);
|
|
735
|
+
const columns = buildQueryColumns(canonical.columns, enumFactories);
|
|
736
|
+
const handle = relationSchema.table(physicalName, columns);
|
|
737
|
+
handles.set(authored, handle);
|
|
738
|
+
physicalToLogical.set(`${physicalSchema}.${physicalName}`, canonical.table);
|
|
739
|
+
}
|
|
740
|
+
const viewByName = new Map(input.views.map((view) => [view.view, view]));
|
|
741
|
+
for (const authored of input.authoredViews) {
|
|
742
|
+
const canonical = viewByName.get(authored.viewName);
|
|
743
|
+
if (!canonical) continue;
|
|
744
|
+
const columns = buildQueryColumns(canonical.columns, enumFactories);
|
|
745
|
+
const handle = schema.view(canonical.view, columns).existing();
|
|
746
|
+
handles.set(authored, handle);
|
|
747
|
+
physicalToLogical.set(`${schemaName}.${canonical.view}`, canonical.view);
|
|
748
|
+
}
|
|
749
|
+
const dialect = new pgCore.PgDialect();
|
|
750
|
+
const compiled = /* @__PURE__ */ new Map();
|
|
751
|
+
for (const authored of input.authoredViews) {
|
|
752
|
+
const expectedFields = Object.keys(authored.columns);
|
|
753
|
+
const declaredDependencies = /* @__PURE__ */ new Set();
|
|
754
|
+
const relation = (ref) => {
|
|
755
|
+
let resolved;
|
|
756
|
+
try {
|
|
757
|
+
resolved = typeof ref === "function" ? ref() : ref;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
throw new OrmDefinitionError(
|
|
760
|
+
`view "${authored.viewName}" relation thunk threw: ${error instanceof Error ? error.message : String(error)}`,
|
|
761
|
+
authored.file
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
765
|
+
throw new OrmDefinitionError(
|
|
766
|
+
`view "${authored.viewName}" relation() expects a table or view defined by this ORM`,
|
|
767
|
+
authored.file
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const handle = handles.get(resolved);
|
|
771
|
+
if (!handle) {
|
|
772
|
+
const name = isSeqTable(resolved) ? resolved.tableName : resolved.viewName;
|
|
773
|
+
throw new OrmDefinitionError(
|
|
774
|
+
`view "${authored.viewName}" references relation "${name}" which is not part of this namespace`,
|
|
775
|
+
authored.file
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
declaredDependencies.add(isSeqTable(resolved) ? resolved.tableName : resolved.viewName);
|
|
779
|
+
return handle;
|
|
780
|
+
};
|
|
781
|
+
let queryBuilder;
|
|
782
|
+
try {
|
|
783
|
+
queryBuilder = authored.config.query({ qb: new pgCore.QueryBuilder(), relation });
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (error instanceof OrmDefinitionError) throw error;
|
|
786
|
+
throw new OrmDefinitionError(
|
|
787
|
+
`view "${authored.viewName}" query callback failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
788
|
+
authored.file
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
let config;
|
|
792
|
+
try {
|
|
793
|
+
config = pgCore.getViewConfig(schema.view(authored.viewName).as(queryBuilder));
|
|
794
|
+
} catch (error) {
|
|
795
|
+
throw new OrmDefinitionError(
|
|
796
|
+
`view "${authored.viewName}" query must return a Drizzle select builder: ${error instanceof Error ? error.message : String(error)}`,
|
|
797
|
+
authored.file
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
const selectedFields = Object.keys(config.selectedFields);
|
|
801
|
+
if (selectedFields.length !== expectedFields.length || selectedFields.some((field, index3) => field !== expectedFields[index3])) {
|
|
802
|
+
throw new OrmDefinitionError(
|
|
803
|
+
`view "${authored.viewName}" query selects [${selectedFields.join(", ")}], but declared columns are [${expectedFields.join(", ")}]`,
|
|
804
|
+
authored.file
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (!config.query) throw new OrmDefinitionError(`view "${authored.viewName}" produced no query`, authored.file);
|
|
808
|
+
const tableDependencies = pgCore.extractUsedTable(config.query).map((physical) => {
|
|
809
|
+
const logical = physicalToLogical.get(physical);
|
|
810
|
+
if (!logical) {
|
|
811
|
+
throw new OrmDefinitionError(
|
|
812
|
+
`view "${authored.viewName}" query references unmanaged relation "${physical}" \u2014 use relation(...) with a namespace table or view`,
|
|
813
|
+
authored.file
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
return logical;
|
|
817
|
+
});
|
|
818
|
+
const renderedQuery = dialect.sqlToQuery(config.query.inlineParams());
|
|
819
|
+
if (renderedQuery.params.length > 0) {
|
|
820
|
+
throw new OrmDefinitionError(
|
|
821
|
+
`view "${authored.viewName}" query produced bind parameters which cannot be persisted in view DDL`,
|
|
822
|
+
authored.file
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
compiled.set(authored.viewName, {
|
|
826
|
+
query: renderedQuery.sql,
|
|
827
|
+
dependencies: [.../* @__PURE__ */ new Set([...declaredDependencies, ...tableDependencies])].sort()
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
return compiled;
|
|
831
|
+
}
|
|
832
|
+
function buildQueryColumns(columns, enumFactories) {
|
|
833
|
+
return Object.fromEntries(columns.map((column) => [column.name, buildQueryColumn(column, enumFactories)]));
|
|
834
|
+
}
|
|
835
|
+
function buildQueryColumn(column, enumFactories) {
|
|
836
|
+
const factory = typeof column.type === "string" ? QUERY_COLUMN_FACTORIES[column.type] : enumFactories.get(column.type.enum);
|
|
837
|
+
if (!factory) {
|
|
838
|
+
const type = typeof column.type === "string" ? column.type : column.type.enum;
|
|
839
|
+
throw new OrmDefinitionError(`view query column '${column.columnName}' references unknown type '${type}'`);
|
|
840
|
+
}
|
|
841
|
+
let builder = factory(column.columnName);
|
|
842
|
+
if (!column.nullable) builder = builder.notNull();
|
|
843
|
+
return builder;
|
|
844
|
+
}
|
|
845
|
+
function relationName(relation) {
|
|
846
|
+
return isSeqTable(relation) ? relation.tableName : relation.viewName;
|
|
847
|
+
}
|
|
848
|
+
function relationKind(relation) {
|
|
849
|
+
return isSeqTable(relation) ? "table" : "view";
|
|
850
|
+
}
|
|
297
851
|
|
|
298
852
|
// src/authoring/namespace.ts
|
|
299
853
|
var NAMESPACE_CONFIG_BRAND = "seq-namespace-config";
|
|
@@ -302,6 +856,7 @@ function isSeqNamespaceConfig(value) {
|
|
|
302
856
|
}
|
|
303
857
|
function serializeNamespace(input) {
|
|
304
858
|
const { config, tables } = input;
|
|
859
|
+
const views = input.views ?? [];
|
|
305
860
|
const enums = input.enums ?? [];
|
|
306
861
|
const tablesByName = /* @__PURE__ */ new Map();
|
|
307
862
|
for (const table of tables) {
|
|
@@ -314,6 +869,29 @@ function serializeNamespace(input) {
|
|
|
314
869
|
}
|
|
315
870
|
tablesByName.set(table.tableName, table);
|
|
316
871
|
}
|
|
872
|
+
const viewsByName = /* @__PURE__ */ new Map();
|
|
873
|
+
for (const view of views) {
|
|
874
|
+
const existing = viewsByName.get(view.viewName);
|
|
875
|
+
if (existing) {
|
|
876
|
+
throw new OrmDefinitionError(
|
|
877
|
+
`duplicate view "${view.viewName}" (defined in ${describeFile(existing)} and ${describeFile(view)})`,
|
|
878
|
+
view.file
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
viewsByName.set(view.viewName, view);
|
|
882
|
+
}
|
|
883
|
+
const relationsByName = /* @__PURE__ */ new Map();
|
|
884
|
+
for (const relation of [...tables, ...views]) {
|
|
885
|
+
const name = relationName(relation);
|
|
886
|
+
const existing = relationsByName.get(name);
|
|
887
|
+
if (existing) {
|
|
888
|
+
throw new OrmDefinitionError(
|
|
889
|
+
`duplicate relation "${name}" (defined as ${relationKind(existing)} in ${describeFile(existing)} and ${relationKind(relation)} in ${describeFile(relation)})`,
|
|
890
|
+
relation.file
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
relationsByName.set(name, relation);
|
|
894
|
+
}
|
|
317
895
|
const enumsByName = /* @__PURE__ */ new Map();
|
|
318
896
|
for (const ref of enums) {
|
|
319
897
|
if (enumsByName.has(ref.name)) {
|
|
@@ -321,11 +899,26 @@ function serializeNamespace(input) {
|
|
|
321
899
|
}
|
|
322
900
|
enumsByName.set(ref.name, ref);
|
|
323
901
|
}
|
|
324
|
-
const canonicalTables = tables.map((table) => buildCanonicalTable({ table,
|
|
902
|
+
const canonicalTables = tables.map((table) => buildCanonicalTable({ table, relationsByName, enumsByName }));
|
|
325
903
|
const canonicalEnums = [...enumsByName.values()].map((ref) => ({
|
|
326
904
|
name: ref.name,
|
|
327
905
|
values: [...ref.values]
|
|
328
906
|
}));
|
|
907
|
+
const viewDrafts = views.map((view) => buildCanonicalView({ view, relationsByName, enumsByName }));
|
|
908
|
+
const compiledViews = compileViewQueries({
|
|
909
|
+
namespace: config.namespace,
|
|
910
|
+
tables: canonicalTables,
|
|
911
|
+
views: viewDrafts,
|
|
912
|
+
enums: canonicalEnums,
|
|
913
|
+
authoredTables: tables,
|
|
914
|
+
authoredViews: views
|
|
915
|
+
});
|
|
916
|
+
const canonicalViews = viewDrafts.map((draft) => {
|
|
917
|
+
const compiled = compiledViews.get(draft.view);
|
|
918
|
+
if (!compiled) throw new OrmDefinitionError(`view "${draft.view}" did not compile`, viewsByName.get(draft.view)?.file);
|
|
919
|
+
return canonicalViewSchema.parse({ ...draft, ...compiled });
|
|
920
|
+
});
|
|
921
|
+
assertAcyclicViewDependencies(canonicalViews);
|
|
329
922
|
const candidate = {
|
|
330
923
|
definition_format: CANONICAL_FORMAT,
|
|
331
924
|
namespace: config.namespace,
|
|
@@ -337,6 +930,7 @@ function serializeNamespace(input) {
|
|
|
337
930
|
}
|
|
338
931
|
};
|
|
339
932
|
if (config.description !== void 0) candidate.description = config.description;
|
|
933
|
+
if (canonicalViews.length > 0) candidate.views = canonicalViews;
|
|
340
934
|
let parsed;
|
|
341
935
|
try {
|
|
342
936
|
parsed = canonicalDefinitionSchema.parse(candidate);
|
|
@@ -351,7 +945,7 @@ function serializeNamespace(input) {
|
|
|
351
945
|
function computeCanonicalHash(definition) {
|
|
352
946
|
return crypto.createHash("sha256").update(HASH_DOMAIN).update(stableJson(definition)).digest("hex");
|
|
353
947
|
}
|
|
354
|
-
function buildCanonicalTable({ table,
|
|
948
|
+
function buildCanonicalTable({ table, relationsByName, enumsByName }) {
|
|
355
949
|
const candidate = {
|
|
356
950
|
table: table.tableName,
|
|
357
951
|
columns: [],
|
|
@@ -363,6 +957,31 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
|
|
|
363
957
|
if (table.config.description !== void 0) candidate.description = table.config.description;
|
|
364
958
|
if (table.config.renamedFrom !== void 0) candidate.renamedFrom = table.config.renamedFrom;
|
|
365
959
|
if (table.source !== void 0) candidate.source = table.source;
|
|
960
|
+
if (table.source !== void 0) {
|
|
961
|
+
if (table.config.indexes !== void 0 && table.config.indexes.length > 0) {
|
|
962
|
+
throw new OrmDefinitionError(
|
|
963
|
+
`synced table "${table.tableName}" is system-owned; indexes are not allowed`,
|
|
964
|
+
table.file
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
if (table.config.cowMutations !== void 0 && table.config.cowMutations.length > 0) {
|
|
968
|
+
throw new OrmDefinitionError(
|
|
969
|
+
`synced table "${table.tableName}" is read-only; cow mutations belong on the overlay table`,
|
|
970
|
+
table.file
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (table.config.policies?.write !== void 0 || table.config.policies?.insert !== void 0 || table.config.policies?.update !== void 0) {
|
|
974
|
+
throw new OrmDefinitionError(`synced table "${table.tableName}" is read-only`, table.file);
|
|
975
|
+
}
|
|
976
|
+
for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
|
|
977
|
+
if (link.enforce) {
|
|
978
|
+
throw new OrmDefinitionError(
|
|
979
|
+
`synced table "${table.tableName}": link "${linkName}" cannot enforce foreign keys \u2014 its DDL is owned by the sync`,
|
|
980
|
+
table.file
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
366
985
|
const columnLevelPk = [];
|
|
367
986
|
for (const [propertyName, builder] of Object.entries(table.columns)) {
|
|
368
987
|
const column = builder.toCanonicalColumn(propertyName);
|
|
@@ -384,40 +1003,38 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
|
|
|
384
1003
|
);
|
|
385
1004
|
}
|
|
386
1005
|
candidate.primaryKey = table.config.primaryKey.map(
|
|
387
|
-
(ref) => resolveColumnRef({ table, ref, role: "primaryKey" })
|
|
1006
|
+
(ref) => resolveColumnRef({ relation: table, ref, role: "primaryKey" })
|
|
388
1007
|
);
|
|
389
1008
|
} else if (columnLevelPk.length > 0) {
|
|
390
1009
|
candidate.primaryKey = columnLevelPk;
|
|
391
1010
|
}
|
|
392
1011
|
for (const index3 of table.config.indexes ?? []) {
|
|
393
1012
|
const entry = {
|
|
394
|
-
columns: index3.columns.map((ref) => resolveColumnRef({ table, ref, role: "index" }))
|
|
1013
|
+
columns: index3.columns.map((ref) => resolveColumnRef({ relation: table, ref, role: "index" }))
|
|
395
1014
|
};
|
|
396
1015
|
if (index3.unique !== void 0) entry.unique = index3.unique;
|
|
397
1016
|
if (index3.name !== void 0) entry.name = index3.name;
|
|
398
1017
|
candidate.indexes?.push(entry);
|
|
399
1018
|
}
|
|
400
1019
|
for (const [linkName, link] of Object.entries(table.config.links ?? {})) {
|
|
401
|
-
candidate.links?.push(buildCanonicalLink({ table, linkName, link,
|
|
1020
|
+
candidate.links?.push(buildCanonicalLink({ relation: table, linkName, link, relationsByName }));
|
|
402
1021
|
}
|
|
403
1022
|
const policies = {};
|
|
404
1023
|
if (table.config.policies?.read !== void 0) policies.read = toCanonicalPolicy(table.config.policies.read);
|
|
405
1024
|
if (table.config.policies?.write !== void 0) policies.write = toCanonicalPolicy(table.config.policies.write);
|
|
1025
|
+
if (table.config.policies?.insert !== void 0) policies.insert = toCanonicalPolicy(table.config.policies.insert);
|
|
1026
|
+
if (table.config.policies?.update !== void 0) policies.update = toCanonicalPolicy(table.config.policies.update);
|
|
406
1027
|
candidate.policies = policies;
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
executes: action.executes,
|
|
418
|
-
returns: action.returns ?? "rowsAffected",
|
|
419
|
-
...action.audit !== void 0 ? { audit: action.audit } : {}
|
|
420
|
-
});
|
|
1028
|
+
const cowMutations = table.config.cowMutations ?? [];
|
|
1029
|
+
if (cowMutations.length > 0) {
|
|
1030
|
+
candidate.cowMutations = cowMutations.map((mutation) => ({
|
|
1031
|
+
kind: mutation.kind,
|
|
1032
|
+
name: mutation.name,
|
|
1033
|
+
synced: mutation.synced,
|
|
1034
|
+
pk: mutation.pk,
|
|
1035
|
+
copy: [...mutation.copy],
|
|
1036
|
+
set: [...mutation.set]
|
|
1037
|
+
}));
|
|
421
1038
|
}
|
|
422
1039
|
try {
|
|
423
1040
|
return canonicalTableSchema.parse(candidate);
|
|
@@ -428,14 +1045,71 @@ function buildCanonicalTable({ table, tablesByName, enumsByName }) {
|
|
|
428
1045
|
throw error;
|
|
429
1046
|
}
|
|
430
1047
|
}
|
|
1048
|
+
function buildCanonicalView({
|
|
1049
|
+
view,
|
|
1050
|
+
relationsByName,
|
|
1051
|
+
enumsByName
|
|
1052
|
+
}) {
|
|
1053
|
+
if (view.config.policies?.read === void 0) {
|
|
1054
|
+
throw new OrmDefinitionError(`view "${view.viewName}" must declare policies.read`, view.file);
|
|
1055
|
+
}
|
|
1056
|
+
const columns = [];
|
|
1057
|
+
const columnLevelPk = [];
|
|
1058
|
+
for (const [propertyName, builder] of Object.entries(view.columns)) {
|
|
1059
|
+
const column = builder.toCanonicalColumn(propertyName);
|
|
1060
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
1061
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare a default`, view.file);
|
|
1062
|
+
}
|
|
1063
|
+
if (column.unique) {
|
|
1064
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare unique`, view.file);
|
|
1065
|
+
}
|
|
1066
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
1067
|
+
throw new OrmDefinitionError(`view "${view.viewName}": column "${propertyName}" cannot declare migration hints`, view.file);
|
|
1068
|
+
}
|
|
1069
|
+
const enumName = builder.enumName;
|
|
1070
|
+
if (enumName !== void 0 && !enumsByName.has(enumName)) {
|
|
1071
|
+
throw new OrmDefinitionError(
|
|
1072
|
+
`view "${view.viewName}": column "${propertyName}" references enum "${enumName}" which is not part of this namespace`,
|
|
1073
|
+
view.file
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
if (builder.isPrimaryKey) columnLevelPk.push(column.columnName);
|
|
1077
|
+
columns.push(column);
|
|
1078
|
+
}
|
|
1079
|
+
let primaryKey2;
|
|
1080
|
+
if (view.config.primaryKey !== void 0) {
|
|
1081
|
+
if (columnLevelPk.length > 0) {
|
|
1082
|
+
throw new OrmDefinitionError(
|
|
1083
|
+
`view "${view.viewName}": declare the logical primary key either on columns or with config.primaryKey \u2014 not both`,
|
|
1084
|
+
view.file
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
primaryKey2 = view.config.primaryKey.map((ref) => resolveColumnRef({ relation: view, ref, role: "primaryKey" }));
|
|
1088
|
+
} else if (columnLevelPk.length > 0) {
|
|
1089
|
+
primaryKey2 = columnLevelPk;
|
|
1090
|
+
}
|
|
1091
|
+
const links = Object.entries(view.config.links ?? {}).map(
|
|
1092
|
+
([linkName, link]) => buildCanonicalLink({ relation: view, linkName, link, relationsByName })
|
|
1093
|
+
);
|
|
1094
|
+
return {
|
|
1095
|
+
view: view.viewName,
|
|
1096
|
+
...view.config.description !== void 0 ? { description: view.config.description } : {},
|
|
1097
|
+
columns,
|
|
1098
|
+
...primaryKey2 !== void 0 ? { primaryKey: primaryKey2 } : {},
|
|
1099
|
+
links,
|
|
1100
|
+
policies: { read: toCanonicalPolicy(view.config.policies.read) }
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
431
1103
|
function buildCanonicalLink({
|
|
432
|
-
|
|
1104
|
+
relation,
|
|
433
1105
|
linkName,
|
|
434
1106
|
link,
|
|
435
|
-
|
|
1107
|
+
relationsByName
|
|
436
1108
|
}) {
|
|
1109
|
+
const ownerKind = relationKind(relation);
|
|
1110
|
+
const ownerName = relationName(relation);
|
|
437
1111
|
const fail2 = (message) => {
|
|
438
|
-
throw new OrmDefinitionError(
|
|
1112
|
+
throw new OrmDefinitionError(`${ownerKind} "${ownerName}": link "${linkName}" ${message}`, relation.file);
|
|
439
1113
|
};
|
|
440
1114
|
if (link.enforce && link.linkKind !== "toOne") {
|
|
441
1115
|
return fail2("sets enforce: true, which is only supported on toOne links");
|
|
@@ -445,29 +1119,34 @@ function buildCanonicalLink({
|
|
|
445
1119
|
return {
|
|
446
1120
|
name: linkName,
|
|
447
1121
|
kind: "toPlatformUser",
|
|
448
|
-
on: resolveColumnRef({
|
|
1122
|
+
on: resolveColumnRef({ relation, ref: link.on, role: `link "${linkName}" on` }),
|
|
449
1123
|
...link.references !== void 0 ? { references: deriveColumnName(link.references) } : {},
|
|
450
1124
|
enforce: false,
|
|
451
1125
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
452
1126
|
};
|
|
453
1127
|
}
|
|
454
|
-
const target =
|
|
455
|
-
|
|
456
|
-
|
|
1128
|
+
const target = resolveRelationRef({ ref: link.target, fail: fail2 });
|
|
1129
|
+
const targetName = relationName(target);
|
|
1130
|
+
if (!relationsByName.has(targetName)) {
|
|
1131
|
+
return fail2(`targets ${relationKind(target)} "${targetName}" which is not part of this namespace`);
|
|
1132
|
+
}
|
|
1133
|
+
if (link.enforce && (!isSeqTable(relation) || !isSeqTable(target))) {
|
|
1134
|
+
return fail2("cannot enforce a PostgreSQL foreign key involving a view");
|
|
457
1135
|
}
|
|
458
|
-
const owningSide = link.linkKind === "toOne" ?
|
|
1136
|
+
const owningSide = link.linkKind === "toOne" ? relation : target;
|
|
1137
|
+
const referencedSide = link.linkKind === "toOne" ? target : relation;
|
|
459
1138
|
return {
|
|
460
1139
|
name: linkName,
|
|
461
1140
|
kind: link.linkKind,
|
|
462
|
-
target:
|
|
463
|
-
on: resolveColumnRef({
|
|
464
|
-
...link.references !== void 0 ? { references: resolveColumnRef({
|
|
1141
|
+
target: targetName,
|
|
1142
|
+
on: resolveColumnRef({ relation: owningSide, ref: link.on, role: `link "${linkName}" on` }),
|
|
1143
|
+
...link.references !== void 0 ? { references: resolveColumnRef({ relation: referencedSide, ref: link.references, role: `link "${linkName}" references` }) } : {},
|
|
465
1144
|
enforce: link.enforce,
|
|
466
1145
|
...link.description !== void 0 ? { description: link.description } : {}
|
|
467
1146
|
};
|
|
468
1147
|
}
|
|
469
|
-
function
|
|
470
|
-
if (ref === void 0) return fail2("declares no target
|
|
1148
|
+
function resolveRelationRef({ ref, fail: fail2 }) {
|
|
1149
|
+
if (ref === void 0) return fail2("declares no target relation");
|
|
471
1150
|
if (typeof ref !== "function") return ref;
|
|
472
1151
|
let resolved;
|
|
473
1152
|
try {
|
|
@@ -477,21 +1156,36 @@ function resolveTableRef({ ref, fail: fail2 }) {
|
|
|
477
1156
|
`target thunk threw (${error instanceof Error ? error.message : String(error)}) \u2014 lazy refs resolve at serialize time; make sure the target module ran`
|
|
478
1157
|
);
|
|
479
1158
|
}
|
|
480
|
-
if (!isSeqTable(resolved)
|
|
1159
|
+
if (!isSeqTable(resolved) && !isSeqView(resolved)) {
|
|
1160
|
+
return fail2("target thunk did not return a relation defined with defineTable/defineView");
|
|
1161
|
+
}
|
|
481
1162
|
return resolved;
|
|
482
1163
|
}
|
|
483
|
-
function resolveColumnRef({
|
|
484
|
-
const direct =
|
|
1164
|
+
function resolveColumnRef({ relation, ref, role }) {
|
|
1165
|
+
const direct = relation.columns[ref];
|
|
485
1166
|
if (direct) return direct.columnNameFor(ref);
|
|
486
|
-
for (const [propertyName, builder] of Object.entries(
|
|
1167
|
+
for (const [propertyName, builder] of Object.entries(relation.columns)) {
|
|
487
1168
|
if (builder.columnNameFor(propertyName) === ref) return ref;
|
|
488
1169
|
}
|
|
489
|
-
const known = Object.keys(
|
|
1170
|
+
const known = Object.keys(relation.columns).join(", ");
|
|
490
1171
|
throw new OrmDefinitionError(
|
|
491
|
-
|
|
492
|
-
|
|
1172
|
+
`${relationKind(relation)} "${relationName(relation)}": ${role} references unknown column "${ref}" (known columns: ${known || "none"})`,
|
|
1173
|
+
relation.file
|
|
493
1174
|
);
|
|
494
1175
|
}
|
|
1176
|
+
function assertAcyclicViewDependencies(views) {
|
|
1177
|
+
const viewNames = new Set(views.map((view) => view.view));
|
|
1178
|
+
const remaining = new Map(
|
|
1179
|
+
views.map((view) => [view.view, new Set(view.dependencies.filter((dependency) => viewNames.has(dependency)))])
|
|
1180
|
+
);
|
|
1181
|
+
while (remaining.size > 0) {
|
|
1182
|
+
const ready = [...remaining.entries()].filter(([, dependencies]) => [...dependencies].every((dependency) => !remaining.has(dependency))).map(([name]) => name);
|
|
1183
|
+
if (ready.length === 0) {
|
|
1184
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining.keys()].sort().join(", ")}`);
|
|
1185
|
+
}
|
|
1186
|
+
for (const name of ready) remaining.delete(name);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
495
1189
|
function toCanonicalPolicy(policy) {
|
|
496
1190
|
if (typeof policy === "string") return policy;
|
|
497
1191
|
return {
|
|
@@ -503,8 +1197,8 @@ function toCanonicalPolicy(policy) {
|
|
|
503
1197
|
}))
|
|
504
1198
|
};
|
|
505
1199
|
}
|
|
506
|
-
function describeFile(
|
|
507
|
-
return
|
|
1200
|
+
function describeFile(relation) {
|
|
1201
|
+
return relation.file ?? "<unknown file>";
|
|
508
1202
|
}
|
|
509
1203
|
function formatZodIssues(error) {
|
|
510
1204
|
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
@@ -528,7 +1222,12 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
528
1222
|
const jiti$1 = jiti.createJiti((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('cli.cjs', document.baseURI).href)), {
|
|
529
1223
|
interopDefault: true,
|
|
530
1224
|
fsCache: false,
|
|
531
|
-
...options.ormModulePath !== void 0 ? {
|
|
1225
|
+
...options.ormModulePath !== void 0 ? {
|
|
1226
|
+
alias: {
|
|
1227
|
+
"@sequenceholdings/orm": options.ormModulePath,
|
|
1228
|
+
"@sequenceholdings/orm/authoring": options.ormModulePath
|
|
1229
|
+
}
|
|
1230
|
+
} : {}
|
|
532
1231
|
});
|
|
533
1232
|
const context = beginAuthoringContext();
|
|
534
1233
|
try {
|
|
@@ -569,16 +1268,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
569
1268
|
for (const entry of context.tables) {
|
|
570
1269
|
if (isSeqTable(entry.value)) tables.push(entry.value);
|
|
571
1270
|
}
|
|
572
|
-
|
|
1271
|
+
const views = [];
|
|
1272
|
+
for (const entry of context.views) {
|
|
1273
|
+
if (isSeqView(entry.value)) views.push(entry.value);
|
|
1274
|
+
}
|
|
1275
|
+
if (tables.length === 0 && views.length === 0) {
|
|
573
1276
|
throw new OrmDefinitionError(
|
|
574
|
-
`no
|
|
1277
|
+
`no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
|
|
575
1278
|
);
|
|
576
1279
|
}
|
|
577
1280
|
const enums = [];
|
|
578
1281
|
for (const entry of context.enums) {
|
|
579
1282
|
if (isEnumRef(entry.value)) enums.push(entry.value);
|
|
580
1283
|
}
|
|
581
|
-
const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
|
|
1284
|
+
const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
|
|
582
1285
|
return { canonical, hash: computeCanonicalHash(canonical), files };
|
|
583
1286
|
}
|
|
584
1287
|
async function listSchemaFiles(root) {
|
|
@@ -612,10 +1315,10 @@ var manifestRequiresSchema = zod.z.object({
|
|
|
612
1315
|
}).nullable();
|
|
613
1316
|
var manifestOperationSchema = zod.z.object({
|
|
614
1317
|
hash: zod.z.string().regex(OP_HASH_RE),
|
|
615
|
-
kind: zod.z.enum(["query", "mutation"
|
|
1318
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
616
1319
|
requires: manifestRequiresSchema
|
|
617
1320
|
});
|
|
618
|
-
zod.z.object({
|
|
1321
|
+
var operationsManifestSchema = zod.z.object({
|
|
619
1322
|
manifestVersion: zod.z.literal(MANIFEST_VERSION),
|
|
620
1323
|
schemaHash: zod.z.string().min(1),
|
|
621
1324
|
opsHash: zod.z.string().regex(OP_HASH_RE),
|
|
@@ -775,14 +1478,14 @@ function generateTypePolicies(canonical) {
|
|
|
775
1478
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
776
1479
|
const entries = [];
|
|
777
1480
|
let hasPlatformUser = false;
|
|
778
|
-
for (const table of def
|
|
1481
|
+
for (const table of canonicalRelations(def)) {
|
|
779
1482
|
if (table.source !== void 0) continue;
|
|
780
1483
|
if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
|
|
781
1484
|
if (!table.primaryKey || table.primaryKey.length === 0) continue;
|
|
782
1485
|
const keyFields = table.primaryKey.map((identifier2) => {
|
|
783
1486
|
const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
|
|
784
1487
|
if (!column) {
|
|
785
|
-
throw new CodegenGqlError(`Primary key column "${identifier2}" not found on
|
|
1488
|
+
throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
|
|
786
1489
|
}
|
|
787
1490
|
return fieldName(column.name);
|
|
788
1491
|
});
|
|
@@ -936,9 +1639,23 @@ var onConflictIrSchema = zod.z.union([
|
|
|
936
1639
|
]);
|
|
937
1640
|
var mutationRootSchema = zod.z.object({
|
|
938
1641
|
kind: zod.z.literal("mutation"),
|
|
939
|
-
|
|
1642
|
+
/**
|
|
1643
|
+
* CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
|
|
1644
|
+
* synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
|
|
1645
|
+
* applies `set` — both statements generated by the executor from this
|
|
1646
|
+
* metadata, never authored SQL. `cowRevert` deletes the override so the
|
|
1647
|
+
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
1648
|
+
* row and carry the write policy in `where` like update/delete.
|
|
1649
|
+
*/
|
|
1650
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
940
1651
|
alias: zod.z.string(),
|
|
941
1652
|
table: zod.z.string(),
|
|
1653
|
+
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
1654
|
+
cowSynced: zod.z.string().optional(),
|
|
1655
|
+
/** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
|
|
1656
|
+
cowCopyColumns: zod.z.array(zod.z.string()).optional(),
|
|
1657
|
+
/** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
|
|
1658
|
+
cowCopyCheck: predIrSchema.optional(),
|
|
942
1659
|
/** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
|
|
943
1660
|
objects: zod.z.union([
|
|
944
1661
|
zod.z.object({ slot: zod.z.string() }),
|
|
@@ -959,19 +1676,11 @@ var mutationRootSchema = zod.z.object({
|
|
|
959
1676
|
affectedRows: zod.z.boolean().default(false),
|
|
960
1677
|
includeIf: includeIfSchema.optional()
|
|
961
1678
|
});
|
|
962
|
-
var actionRootSchema = zod.z.object({
|
|
963
|
-
kind: zod.z.literal("action"),
|
|
964
|
-
alias: zod.z.string(),
|
|
965
|
-
name: zod.z.string(),
|
|
966
|
-
params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
|
|
967
|
-
returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
|
|
968
|
-
});
|
|
969
1679
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
970
1680
|
selectRootSchema,
|
|
971
1681
|
byPkRootSchema,
|
|
972
1682
|
aggregateRootSchema,
|
|
973
|
-
mutationRootSchema
|
|
974
|
-
actionRootSchema
|
|
1683
|
+
mutationRootSchema
|
|
975
1684
|
]);
|
|
976
1685
|
var INPUT_REF_KINDS = [
|
|
977
1686
|
"boolExp",
|
|
@@ -1003,7 +1712,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1003
1712
|
irVersion: zod.z.literal(IR_VERSION),
|
|
1004
1713
|
name: zod.z.string(),
|
|
1005
1714
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
1006
|
-
kind: zod.z.enum(["query", "mutation"
|
|
1715
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
1007
1716
|
requires: zod.z.object({
|
|
1008
1717
|
capability: zod.z.string().optional(),
|
|
1009
1718
|
role: zod.z.string().optional()
|
|
@@ -1017,13 +1726,24 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1017
1726
|
table: zod.z.string(),
|
|
1018
1727
|
schema: zod.z.string(),
|
|
1019
1728
|
typename: zod.z.string(),
|
|
1729
|
+
/**
|
|
1730
|
+
* True when statements over this relation evaluate a claims-gated synced
|
|
1731
|
+
* view: synced twins (`source: gold.*`) materialize as views whose baked
|
|
1732
|
+
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
1733
|
+
* inherit the gate transitively through their declared dependencies. The
|
|
1734
|
+
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
1735
|
+
* touching such a relation, or the synced arm yields zero rows regardless
|
|
1736
|
+
* of the compiled predicates.
|
|
1737
|
+
*/
|
|
1738
|
+
claimsGated: zod.z.boolean().default(false),
|
|
1020
1739
|
columns: zod.z.array(zod.z.object({
|
|
1021
1740
|
name: zod.z.string(),
|
|
1022
1741
|
columnName: zod.z.string(),
|
|
1023
1742
|
type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
1024
1743
|
nullable: zod.z.boolean()
|
|
1025
1744
|
})),
|
|
1026
|
-
|
|
1745
|
+
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
1746
|
+
primaryKey: zod.z.array(zod.z.string()),
|
|
1027
1747
|
links: zod.z.array(zod.z.object({
|
|
1028
1748
|
name: zod.z.string(),
|
|
1029
1749
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
@@ -1209,6 +1929,10 @@ var Parser = class {
|
|
|
1209
1929
|
this.expect("rparen", "')'");
|
|
1210
1930
|
return inner;
|
|
1211
1931
|
}
|
|
1932
|
+
if (this.peekKeyword("true") || this.peekKeyword("false")) {
|
|
1933
|
+
const token = this.advance();
|
|
1934
|
+
return { kind: "literal", value: token.value === "true" };
|
|
1935
|
+
}
|
|
1212
1936
|
if (this.peekKeyword("capability") || this.peekKeyword("role")) {
|
|
1213
1937
|
const fn = this.peek().value;
|
|
1214
1938
|
const node = this.parseBooleanCall(fn);
|
|
@@ -1392,6 +2116,8 @@ var PolicyCompiler = class {
|
|
|
1392
2116
|
}
|
|
1393
2117
|
compile(node, table, depth) {
|
|
1394
2118
|
switch (node.kind) {
|
|
2119
|
+
case "literal":
|
|
2120
|
+
return { kind: "const", value: node.value };
|
|
1395
2121
|
case "and":
|
|
1396
2122
|
case "or":
|
|
1397
2123
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
@@ -1562,6 +2288,7 @@ var NamespaceCompileError = class extends Error {
|
|
|
1562
2288
|
|
|
1563
2289
|
// src/compiler-gql/pred-utils.ts
|
|
1564
2290
|
var CONST_TRUE = { kind: "const", value: true };
|
|
2291
|
+
var CONST_FALSE = { kind: "const", value: false };
|
|
1565
2292
|
function andPred(...preds) {
|
|
1566
2293
|
const operands = preds.filter(
|
|
1567
2294
|
(pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
|
|
@@ -1647,6 +2374,12 @@ var PolicyInjector = class {
|
|
|
1647
2374
|
write(table) {
|
|
1648
2375
|
return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
|
|
1649
2376
|
}
|
|
2377
|
+
insert(table) {
|
|
2378
|
+
return this.ctx.insertPolicies.get(table) ?? this.write(table);
|
|
2379
|
+
}
|
|
2380
|
+
update(table) {
|
|
2381
|
+
return this.ctx.updatePolicies.get(table) ?? this.write(table);
|
|
2382
|
+
}
|
|
1650
2383
|
linkTarget(table, link) {
|
|
1651
2384
|
const meta = this.ctx.tables[table];
|
|
1652
2385
|
const resolved = meta?.links.find((l) => l.name === link);
|
|
@@ -1721,31 +2454,50 @@ var PolicyInjector = class {
|
|
|
1721
2454
|
};
|
|
1722
2455
|
case "mutation":
|
|
1723
2456
|
return this.injectMutation(root);
|
|
1724
|
-
case "action":
|
|
1725
|
-
return root;
|
|
1726
2457
|
}
|
|
1727
2458
|
}
|
|
1728
2459
|
injectMutation(root) {
|
|
1729
2460
|
const table = root.table;
|
|
1730
|
-
const writePolicy = this.write(table);
|
|
1731
2461
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
1732
2462
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1733
|
-
|
|
2463
|
+
const insertPolicy = this.insert(table);
|
|
2464
|
+
if (predContainsExists(insertPolicy)) {
|
|
1734
2465
|
throw new DocumentCompileError(
|
|
1735
|
-
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its
|
|
2466
|
+
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
1736
2467
|
"insert-check-unrepresentable"
|
|
1737
2468
|
);
|
|
1738
2469
|
}
|
|
1739
2470
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
1740
2471
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
1741
2472
|
);
|
|
1742
|
-
|
|
2473
|
+
if (root.onConflict && this.ctx.splitWritePolicyTables.has(table)) {
|
|
2474
|
+
throw new DocumentCompileError(
|
|
2475
|
+
`table '${table}' cannot use onConflict while insert and update policies are split \u2014 use separate insert and update mutations`,
|
|
2476
|
+
"on-conflict-split-policies"
|
|
2477
|
+
);
|
|
2478
|
+
}
|
|
2479
|
+
return { ...root, pinnedColumns: pins, insertCheck: insertPolicy, returning };
|
|
1743
2480
|
}
|
|
1744
|
-
const
|
|
2481
|
+
const updatePolicy = this.update(table);
|
|
2482
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
1745
2483
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
1746
|
-
const check = predContainsExists(
|
|
2484
|
+
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
1747
2485
|
return { ...root, where, ...check, returning };
|
|
1748
2486
|
}
|
|
2487
|
+
if (root.op === "cowOverride") {
|
|
2488
|
+
const insertPolicy = this.insert(table);
|
|
2489
|
+
if (predContainsExists(insertPolicy)) {
|
|
2490
|
+
throw new DocumentCompileError(
|
|
2491
|
+
`table '${table}' cannot be copy-on-write overridden: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
2492
|
+
"insert-check-unrepresentable"
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2496
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2497
|
+
);
|
|
2498
|
+
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
2499
|
+
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
2500
|
+
}
|
|
1749
2501
|
return { ...root, where, returning };
|
|
1750
2502
|
}
|
|
1751
2503
|
};
|
|
@@ -1791,7 +2543,6 @@ function collectTableSet(roots, ctx) {
|
|
|
1791
2543
|
}
|
|
1792
2544
|
};
|
|
1793
2545
|
for (const root of roots) {
|
|
1794
|
-
if (root.kind === "action") continue;
|
|
1795
2546
|
tables.add(root.table);
|
|
1796
2547
|
if (root.kind === "select" || root.kind === "byPk") {
|
|
1797
2548
|
visitPred(root.where, root.table);
|
|
@@ -1802,8 +2553,10 @@ function collectTableSet(roots, ctx) {
|
|
|
1802
2553
|
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1803
2554
|
}
|
|
1804
2555
|
if (root.kind === "mutation") {
|
|
2556
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
1805
2557
|
if (root.where) visitPred(root.where, root.table);
|
|
1806
2558
|
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2559
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
1807
2560
|
if (root.returning) visitSelection(root.returning, root.table);
|
|
1808
2561
|
}
|
|
1809
2562
|
}
|
|
@@ -1870,6 +2623,7 @@ function platformUserMeta() {
|
|
|
1870
2623
|
table: "users_directory",
|
|
1871
2624
|
schema: "platform",
|
|
1872
2625
|
typename: PLATFORM_USER_TYPENAME,
|
|
2626
|
+
claimsGated: false,
|
|
1873
2627
|
columns: columns.map((c) => ({ ...c })),
|
|
1874
2628
|
primaryKey: ["sub"],
|
|
1875
2629
|
links: [],
|
|
@@ -1912,15 +2666,30 @@ function buildCompileContext(rawCanonical) {
|
|
|
1912
2666
|
const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
|
|
1913
2667
|
const diagnostics = [];
|
|
1914
2668
|
const enums = new Map(canonical.enums.map((e) => [e.name, e]));
|
|
2669
|
+
const relations = canonicalRelations(canonical);
|
|
2670
|
+
const viewNames = new Set((canonical.views ?? []).map((view) => view.view));
|
|
1915
2671
|
const columnEntries = /* @__PURE__ */ new Map();
|
|
1916
2672
|
const tables = {};
|
|
1917
|
-
const usesPlatformUsers =
|
|
2673
|
+
const usesPlatformUsers = relations.some((t2) => t2.links.some((l) => l.kind === "toPlatformUser"));
|
|
1918
2674
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
1919
2675
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
1920
|
-
|
|
2676
|
+
const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
|
|
2677
|
+
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
2678
|
+
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
2679
|
+
const relationIsClaimsGated = (name) => {
|
|
2680
|
+
if (syncedTableNames.has(name)) return true;
|
|
2681
|
+
const memo = claimsGatedMemo.get(name);
|
|
2682
|
+
if (memo !== void 0) return memo;
|
|
2683
|
+
claimsGatedMemo.set(name, false);
|
|
2684
|
+
const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
|
|
2685
|
+
claimsGatedMemo.set(name, gated);
|
|
2686
|
+
return gated;
|
|
2687
|
+
};
|
|
2688
|
+
for (const table of relations) {
|
|
2689
|
+
const isView = viewNames.has(table.table);
|
|
1921
2690
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
1922
2691
|
columnEntries.set(table.table, entries);
|
|
1923
|
-
const pkRefs = table.primaryKey ?? ["id"];
|
|
2692
|
+
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1924
2693
|
const primaryKey2 = [];
|
|
1925
2694
|
for (const ref of pkRefs) {
|
|
1926
2695
|
const resolved = logicalColumn(entries, ref);
|
|
@@ -1941,7 +2710,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1941
2710
|
for (const column of table.columns) {
|
|
1942
2711
|
if (column.unique) {
|
|
1943
2712
|
const logical = logicalColumn(entries, column.columnName);
|
|
1944
|
-
constraints.push({ name: `${table.table}_${column.columnName}
|
|
2713
|
+
constraints.push({ name: `${table.table}_${column.columnName}_unique`, columns: [logical] });
|
|
1945
2714
|
}
|
|
1946
2715
|
}
|
|
1947
2716
|
for (const index3 of table.indexes) {
|
|
@@ -1953,20 +2722,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
1953
2722
|
table: table.table,
|
|
1954
2723
|
schema: ormSchemaName(canonical.namespace),
|
|
1955
2724
|
typename: typeNameForTable(table.table),
|
|
2725
|
+
// Synced twins (and views reading them, resolved transitively below)
|
|
2726
|
+
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2727
|
+
claimsGated: relationIsClaimsGated(table.table),
|
|
1956
2728
|
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1957
|
-
primaryKey: primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2729
|
+
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
1958
2730
|
links: [],
|
|
1959
2731
|
filterable: entries.map((c) => c.name),
|
|
1960
2732
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
1961
|
-
insertable: active.map((c) => c.name),
|
|
1962
|
-
settable: active.map((c) => c.name),
|
|
1963
|
-
appendable: active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1964
|
-
constraints,
|
|
2733
|
+
insertable: isView ? [] : active.map((c) => c.name),
|
|
2734
|
+
settable: isView ? [] : active.map((c) => c.name),
|
|
2735
|
+
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2736
|
+
constraints: isView ? [] : constraints,
|
|
1965
2737
|
dynamicFilterMaxDepth: 6,
|
|
1966
2738
|
dynamicFilterMaxNodes: 64
|
|
1967
2739
|
};
|
|
1968
2740
|
}
|
|
1969
|
-
for (const table of
|
|
2741
|
+
for (const table of relations) {
|
|
1970
2742
|
const meta = tables[table.table];
|
|
1971
2743
|
const entries = columnEntries.get(table.table);
|
|
1972
2744
|
for (const link of table.links) {
|
|
@@ -2001,17 +2773,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
2001
2773
|
}
|
|
2002
2774
|
const readPolicies = /* @__PURE__ */ new Map();
|
|
2003
2775
|
const writePolicies = /* @__PURE__ */ new Map();
|
|
2776
|
+
const insertPolicies = /* @__PURE__ */ new Map();
|
|
2777
|
+
const updatePolicies = /* @__PURE__ */ new Map();
|
|
2004
2778
|
const pins = /* @__PURE__ */ new Map();
|
|
2779
|
+
const splitWritePolicyTables = /* @__PURE__ */ new Set();
|
|
2005
2780
|
const resolver = { tables };
|
|
2006
|
-
for (const table of
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
["write", writePolicies]
|
|
2010
|
-
]) {
|
|
2781
|
+
for (const table of relations) {
|
|
2782
|
+
const isView = viewNames.has(table.table);
|
|
2783
|
+
const compilePolicyFor = (access, store) => {
|
|
2011
2784
|
const expression = table.policies[access];
|
|
2012
2785
|
if (expression === void 0) {
|
|
2013
|
-
|
|
2014
|
-
continue;
|
|
2786
|
+
return;
|
|
2015
2787
|
}
|
|
2016
2788
|
try {
|
|
2017
2789
|
store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
|
|
@@ -2024,37 +2796,117 @@ function buildCompileContext(rawCanonical) {
|
|
|
2024
2796
|
table: table.table
|
|
2025
2797
|
});
|
|
2026
2798
|
store.set(table.table, CONST_TRUE);
|
|
2027
|
-
|
|
2799
|
+
return;
|
|
2028
2800
|
}
|
|
2029
2801
|
throw error;
|
|
2030
2802
|
}
|
|
2803
|
+
};
|
|
2804
|
+
compilePolicyFor("read", readPolicies);
|
|
2805
|
+
compilePolicyFor("write", writePolicies);
|
|
2806
|
+
compilePolicyFor("insert", insertPolicies);
|
|
2807
|
+
compilePolicyFor("update", updatePolicies);
|
|
2808
|
+
if (isView) {
|
|
2809
|
+
writePolicies.set(table.table, CONST_FALSE);
|
|
2810
|
+
insertPolicies.set(table.table, CONST_FALSE);
|
|
2811
|
+
updatePolicies.set(table.table, CONST_FALSE);
|
|
2812
|
+
pins.set(table.table, []);
|
|
2813
|
+
continue;
|
|
2814
|
+
}
|
|
2815
|
+
if (table.policies.insert !== void 0 || table.policies.update !== void 0) {
|
|
2816
|
+
splitWritePolicyTables.add(table.table);
|
|
2817
|
+
}
|
|
2818
|
+
const fallbackWrite = writePolicies.get(table.table);
|
|
2819
|
+
const hasSplitPolicy = table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2820
|
+
if (!insertPolicies.has(table.table)) {
|
|
2821
|
+
if (fallbackWrite !== void 0) insertPolicies.set(table.table, fallbackWrite);
|
|
2822
|
+
else if (hasSplitPolicy) insertPolicies.set(table.table, CONST_FALSE);
|
|
2823
|
+
else insertPolicies.set(table.table, CONST_TRUE);
|
|
2031
2824
|
}
|
|
2032
|
-
|
|
2825
|
+
if (!updatePolicies.has(table.table)) {
|
|
2826
|
+
if (fallbackWrite !== void 0) updatePolicies.set(table.table, fallbackWrite);
|
|
2827
|
+
else if (hasSplitPolicy) updatePolicies.set(table.table, CONST_FALSE);
|
|
2828
|
+
else updatePolicies.set(table.table, CONST_TRUE);
|
|
2829
|
+
}
|
|
2830
|
+
const tablePins = collectPinnedColumns(insertPolicies.get(table.table));
|
|
2033
2831
|
pins.set(table.table, tablePins);
|
|
2034
2832
|
const pinned = new Set(tablePins.map((pin) => pin.column));
|
|
2035
2833
|
const meta = tables[table.table];
|
|
2036
2834
|
meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
|
|
2037
|
-
meta.settable = meta.settable.filter((column) => !pinned.has(column));
|
|
2038
2835
|
}
|
|
2039
2836
|
if (platformMeta) {
|
|
2040
2837
|
readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
|
|
2041
2838
|
writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2839
|
+
insertPolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2840
|
+
updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2042
2841
|
pins.set(PLATFORM_USER_TABLE, []);
|
|
2043
2842
|
}
|
|
2044
2843
|
const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
|
|
2045
2844
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2046
2845
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2047
|
-
for (const table of
|
|
2846
|
+
for (const table of relations) {
|
|
2048
2847
|
const roots = queryRootNames(table.table);
|
|
2049
2848
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2050
|
-
|
|
2849
|
+
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
2850
|
+
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
2851
|
+
}
|
|
2051
2852
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
2052
|
-
|
|
2853
|
+
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2854
|
+
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
2053
2855
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
2054
2856
|
mutationRoots.set(field, { op, table: table.table });
|
|
2055
2857
|
}
|
|
2056
2858
|
}
|
|
2057
2859
|
}
|
|
2860
|
+
for (const table of relations) {
|
|
2861
|
+
for (const mutation of table.cowMutations ?? []) {
|
|
2862
|
+
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2863
|
+
if (!hasWritePolicy) {
|
|
2864
|
+
diagnostics.push({
|
|
2865
|
+
severity: "error",
|
|
2866
|
+
code: "cow-mutation-unpoliced",
|
|
2867
|
+
message: `cow mutation '${mutation.name}' on '${table.table}' requires a write (or insert/update) policy on the overlay table`,
|
|
2868
|
+
table: table.table
|
|
2869
|
+
});
|
|
2870
|
+
continue;
|
|
2871
|
+
}
|
|
2872
|
+
const field = camelCase(mutation.name);
|
|
2873
|
+
if (mutationRoots.has(field) || queryRoots.has(field)) {
|
|
2874
|
+
diagnostics.push({
|
|
2875
|
+
severity: "error",
|
|
2876
|
+
code: "cow-mutation-name-collision",
|
|
2877
|
+
message: `cow mutation '${mutation.name}' on '${table.table}' collides with the generated root '${field}'`,
|
|
2878
|
+
table: table.table
|
|
2879
|
+
});
|
|
2880
|
+
continue;
|
|
2881
|
+
}
|
|
2882
|
+
if (mutation.kind === "override") {
|
|
2883
|
+
const pinned = new Set((pins.get(table.table) ?? []).map((pin) => pin.column));
|
|
2884
|
+
const covered = /* @__PURE__ */ new Set([mutation.pk, ...mutation.copy, ...pinned]);
|
|
2885
|
+
const gaps = table.columns.filter((column) => {
|
|
2886
|
+
if (covered.has(column.name)) return false;
|
|
2887
|
+
const hasDefault = column.default !== void 0 || column.defaultFn !== void 0;
|
|
2888
|
+
return column.nullable === false && !hasDefault;
|
|
2889
|
+
});
|
|
2890
|
+
if (gaps.length > 0) {
|
|
2891
|
+
diagnostics.push({
|
|
2892
|
+
severity: "error",
|
|
2893
|
+
code: "cow-copy-uncovered-column",
|
|
2894
|
+
message: `cow mutation '${mutation.name}' on '${table.table}': overlay-only column(s) ${gaps.map((column) => `'${column.name}'`).join(", ")} are NOT NULL without a default, a policy pin, or a synced contract column \u2014 the copy insert cannot populate them`,
|
|
2895
|
+
table: table.table
|
|
2896
|
+
});
|
|
2897
|
+
continue;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
mutationRoots.set(field, {
|
|
2901
|
+
op: mutation.kind === "override" ? "cowOverride" : "cowRevert",
|
|
2902
|
+
table: table.table,
|
|
2903
|
+
synced: mutation.synced,
|
|
2904
|
+
pk: mutation.pk,
|
|
2905
|
+
set: mutation.set,
|
|
2906
|
+
copyColumns: mutation.copy
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2058
2910
|
const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
|
|
2059
2911
|
return {
|
|
2060
2912
|
ctx: {
|
|
@@ -2066,7 +2918,10 @@ function buildCompileContext(rawCanonical) {
|
|
|
2066
2918
|
enumsByTypeName,
|
|
2067
2919
|
readPolicies,
|
|
2068
2920
|
writePolicies,
|
|
2069
|
-
|
|
2921
|
+
insertPolicies,
|
|
2922
|
+
updatePolicies,
|
|
2923
|
+
pins,
|
|
2924
|
+
splitWritePolicyTables
|
|
2070
2925
|
},
|
|
2071
2926
|
diagnostics
|
|
2072
2927
|
};
|
|
@@ -2618,6 +3473,9 @@ var OperationLowerer = class {
|
|
|
2618
3473
|
const alias = node.alias?.value ?? name;
|
|
2619
3474
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2620
3475
|
if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
3476
|
+
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
3477
|
+
return this.lowerCowRoot(node, rootInfo);
|
|
3478
|
+
}
|
|
2621
3479
|
const { op, table } = rootInfo;
|
|
2622
3480
|
const args = argsByName(node);
|
|
2623
3481
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
@@ -2687,6 +3545,55 @@ var OperationLowerer = class {
|
|
|
2687
3545
|
}
|
|
2688
3546
|
}
|
|
2689
3547
|
}
|
|
3548
|
+
/**
|
|
3549
|
+
* Copy-on-write roots: flat args — the pk plus (override only) every
|
|
3550
|
+
* declared set column. Overrides are full snapshots of the set columns, so
|
|
3551
|
+
* every set argument must be supplied explicitly (pass null to clear a
|
|
3552
|
+
* nullable column); the executor generates the copy/update/delete SQL from
|
|
3553
|
+
* the structured fields, never from authored statements.
|
|
3554
|
+
*/
|
|
3555
|
+
lowerCowRoot(node, rootInfo) {
|
|
3556
|
+
const name = node.name.value;
|
|
3557
|
+
const alias = node.alias?.value ?? name;
|
|
3558
|
+
const { table } = rootInfo;
|
|
3559
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
3560
|
+
const meta = this.meta(table);
|
|
3561
|
+
const args = argsByName(node);
|
|
3562
|
+
const pkArg = args.get(rootInfo.pk);
|
|
3563
|
+
if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
3564
|
+
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
3565
|
+
const base = {
|
|
3566
|
+
kind: "mutation",
|
|
3567
|
+
op: rootInfo.op,
|
|
3568
|
+
alias,
|
|
3569
|
+
table,
|
|
3570
|
+
pinnedColumns: [],
|
|
3571
|
+
affectedRows: false,
|
|
3572
|
+
pk,
|
|
3573
|
+
cowSynced: rootInfo.synced,
|
|
3574
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
3575
|
+
};
|
|
3576
|
+
if (rootInfo.op === "cowRevert") {
|
|
3577
|
+
assertKnownArgs(node, [rootInfo.pk]);
|
|
3578
|
+
return base;
|
|
3579
|
+
}
|
|
3580
|
+
assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
|
|
3581
|
+
const entries = rootInfo.set.map((column) => {
|
|
3582
|
+
const arg = args.get(column);
|
|
3583
|
+
if (!arg) {
|
|
3584
|
+
fail(
|
|
3585
|
+
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
3586
|
+
"missing-argument",
|
|
3587
|
+
[...rootInfo.set]
|
|
3588
|
+
);
|
|
3589
|
+
}
|
|
3590
|
+
if (!meta.settable.includes(column)) {
|
|
3591
|
+
fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
3592
|
+
}
|
|
3593
|
+
return { column, value: this.lowerValue(arg.value) };
|
|
3594
|
+
});
|
|
3595
|
+
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
3596
|
+
}
|
|
2690
3597
|
lowerMutationResponse(selectionSet, table) {
|
|
2691
3598
|
let affectedRows = false;
|
|
2692
3599
|
let returning = null;
|
|
@@ -2780,8 +3687,8 @@ var OperationLowerer = class {
|
|
|
2780
3687
|
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
2781
3688
|
fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
2782
3689
|
}
|
|
2783
|
-
const
|
|
2784
|
-
result.inc = { entries: this.lowerColumnEntries(inc.value, table,
|
|
3690
|
+
const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
|
|
3691
|
+
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
|
|
2785
3692
|
}
|
|
2786
3693
|
if (!result.set && !result.append && !result.inc) {
|
|
2787
3694
|
fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
@@ -3132,13 +4039,17 @@ function generateNamespaceTypes(definition) {
|
|
|
3132
4039
|
`export type ${typesInterfaceName(definition.namespace)} = {`
|
|
3133
4040
|
];
|
|
3134
4041
|
for (const table of definition.tables) {
|
|
3135
|
-
|
|
4042
|
+
const columns = Object.entries(table.columns ?? {});
|
|
4043
|
+
if (table.source && columns.length === 0) {
|
|
3136
4044
|
lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
|
|
3137
4045
|
lines.push(` ${table.table}: Record<string, unknown>`);
|
|
3138
4046
|
continue;
|
|
3139
4047
|
}
|
|
4048
|
+
if (table.source) {
|
|
4049
|
+
lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
|
|
4050
|
+
}
|
|
3140
4051
|
lines.push(` ${table.table}: {`);
|
|
3141
|
-
for (const [name, column] of
|
|
4052
|
+
for (const [name, column] of columns) {
|
|
3142
4053
|
if (column.deprecated) continue;
|
|
3143
4054
|
lines.push(` ${name}: ${columnWireType(column)}`);
|
|
3144
4055
|
}
|
|
@@ -3604,8 +4515,8 @@ var actionSchema = zod.z.strictObject({
|
|
|
3604
4515
|
}
|
|
3605
4516
|
}
|
|
3606
4517
|
});
|
|
3607
|
-
function extractNamedParams(
|
|
3608
|
-
const withoutStrings =
|
|
4518
|
+
function extractNamedParams(sql4) {
|
|
4519
|
+
const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
|
|
3609
4520
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
3610
4521
|
return [...new Set(names)];
|
|
3611
4522
|
}
|
|
@@ -3659,7 +4570,9 @@ var indexSchema = zod.z.union([
|
|
|
3659
4570
|
]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
|
|
3660
4571
|
var policiesSchema = zod.z.strictObject({
|
|
3661
4572
|
read: parsedPredicateSchema.optional(),
|
|
3662
|
-
write: parsedPredicateSchema.optional()
|
|
4573
|
+
write: parsedPredicateSchema.optional(),
|
|
4574
|
+
insert: parsedPredicateSchema.optional(),
|
|
4575
|
+
update: parsedPredicateSchema.optional()
|
|
3663
4576
|
});
|
|
3664
4577
|
var tableFileSchema = zod.z.strictObject({
|
|
3665
4578
|
namespace: namespaceSchema,
|
|
@@ -3695,7 +4608,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
|
3695
4608
|
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
3696
4609
|
function validateSyncedTable(table, ctx) {
|
|
3697
4610
|
const forbidden = [
|
|
3698
|
-
["columns", "synced tables reflect their source; columns are not declared"],
|
|
3699
4611
|
["actions", "synced tables are read-only; actions are not allowed"],
|
|
3700
4612
|
["indexes", "synced tables are system-owned; indexes are not allowed"]
|
|
3701
4613
|
];
|
|
@@ -3705,6 +4617,39 @@ function validateSyncedTable(table, ctx) {
|
|
|
3705
4617
|
if (table.policies?.write) {
|
|
3706
4618
|
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
3707
4619
|
}
|
|
4620
|
+
if (table.policies?.insert) {
|
|
4621
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
4622
|
+
}
|
|
4623
|
+
if (table.policies?.update) {
|
|
4624
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
4625
|
+
}
|
|
4626
|
+
const columns = table.columns ?? {};
|
|
4627
|
+
const names = Object.keys(columns);
|
|
4628
|
+
if (names.length === 0) return;
|
|
4629
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
4630
|
+
if (primaryKeys.length !== 1) {
|
|
4631
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
4632
|
+
}
|
|
4633
|
+
validateRenames(columns, names, ctx);
|
|
4634
|
+
for (const name of names) {
|
|
4635
|
+
const column = columns[name];
|
|
4636
|
+
if (!column) continue;
|
|
4637
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
4638
|
+
if (column.cast && cast?.column !== name) {
|
|
4639
|
+
ctx.addIssue({
|
|
4640
|
+
code: "custom",
|
|
4641
|
+
message: "cast must reference the column it is declared on",
|
|
4642
|
+
path: ["columns", name, "cast"]
|
|
4643
|
+
});
|
|
4644
|
+
}
|
|
4645
|
+
if (cast && cast.type !== column.type) {
|
|
4646
|
+
ctx.addIssue({
|
|
4647
|
+
code: "custom",
|
|
4648
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
4649
|
+
path: ["columns", name, "cast"]
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
}
|
|
3708
4653
|
}
|
|
3709
4654
|
function validateRenames(columns, names, ctx) {
|
|
3710
4655
|
const renameSources = /* @__PURE__ */ new Map();
|
|
@@ -3782,7 +4727,7 @@ function validateColumnTable(table, ctx) {
|
|
|
3782
4727
|
function validateLinks(table, ctx) {
|
|
3783
4728
|
const columnNames = Object.keys(table.columns ?? {});
|
|
3784
4729
|
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
3785
|
-
if (
|
|
4730
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
3786
4731
|
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
|
|
3787
4732
|
}
|
|
3788
4733
|
if (link.enforce && link.to.includes(".")) {
|
|
@@ -3855,10 +4800,10 @@ var TYPE_GROUPS = {
|
|
|
3855
4800
|
};
|
|
3856
4801
|
var columnTypeGroup = (type) => TYPE_GROUPS[type];
|
|
3857
4802
|
function validatePolicyColumns(table, ctx) {
|
|
3858
|
-
if (table.source) return;
|
|
3859
4803
|
const columns = table.columns ?? {};
|
|
3860
4804
|
const columnNames = Object.keys(columns);
|
|
3861
|
-
|
|
4805
|
+
if (columnNames.length === 0) return;
|
|
4806
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
3862
4807
|
const predicate = table.policies?.[kind];
|
|
3863
4808
|
if (!predicate) continue;
|
|
3864
4809
|
for (const column of referencedColumns(predicate.ast)) {
|
|
@@ -4013,10 +4958,15 @@ function policyStatements(schema, table) {
|
|
|
4013
4958
|
if (read) {
|
|
4014
4959
|
statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
|
|
4015
4960
|
}
|
|
4016
|
-
const
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
statements.push(
|
|
4961
|
+
const insertPolicy = table.policies?.insert ?? table.policies?.write;
|
|
4962
|
+
const updatePolicy = table.policies?.update ?? table.policies?.write;
|
|
4963
|
+
if (insertPolicy) {
|
|
4964
|
+
statements.push(
|
|
4965
|
+
`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
|
|
4966
|
+
);
|
|
4967
|
+
}
|
|
4968
|
+
if (updatePolicy) {
|
|
4969
|
+
const predicate = compilePredicate(updatePolicy.ast, columnTypes);
|
|
4020
4970
|
statements.push(
|
|
4021
4971
|
`CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
|
|
4022
4972
|
);
|
|
@@ -4240,11 +5190,268 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
4240
5190
|
imports[table.table] = drizzleTable;
|
|
4241
5191
|
tables[table.table] = drizzleTable;
|
|
4242
5192
|
}
|
|
5193
|
+
for (const view of definition.views ?? []) {
|
|
5194
|
+
const columns = Object.fromEntries(
|
|
5195
|
+
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
5196
|
+
);
|
|
5197
|
+
imports[view.view] = schema.view(view.view, columns).as(
|
|
5198
|
+
drizzleOrm.sql.raw(
|
|
5199
|
+
rewriteSyncedSourceRelationsInViewQuery({
|
|
5200
|
+
query: view.query,
|
|
5201
|
+
canonical: definition,
|
|
5202
|
+
schemaName,
|
|
5203
|
+
dependencies: view.dependencies
|
|
5204
|
+
})
|
|
5205
|
+
)
|
|
5206
|
+
);
|
|
5207
|
+
}
|
|
4243
5208
|
return { imports, tables, schemaName };
|
|
4244
5209
|
}
|
|
5210
|
+
function rewriteSyncedSourceRelationsInViewQuery({
|
|
5211
|
+
query,
|
|
5212
|
+
canonical,
|
|
5213
|
+
schemaName,
|
|
5214
|
+
dependencies
|
|
5215
|
+
}) {
|
|
5216
|
+
const tablesBySource = /* @__PURE__ */ new Map();
|
|
5217
|
+
for (const table of canonical.tables) {
|
|
5218
|
+
if (!table.source) continue;
|
|
5219
|
+
tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
|
|
5220
|
+
}
|
|
5221
|
+
const declared = new Set(dependencies ?? []);
|
|
5222
|
+
const boundTableBySource = /* @__PURE__ */ new Map();
|
|
5223
|
+
for (const [source, tables] of tablesBySource) {
|
|
5224
|
+
if (tables.length === 1) {
|
|
5225
|
+
boundTableBySource.set(source, tables[0]);
|
|
5226
|
+
continue;
|
|
5227
|
+
}
|
|
5228
|
+
const [sourceSchema, sourceName] = source.split(".");
|
|
5229
|
+
if (!sourceSchema || !sourceName) continue;
|
|
5230
|
+
const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
|
|
5231
|
+
if (!referenced) continue;
|
|
5232
|
+
const declaredMatches = tables.filter((table) => declared.has(table));
|
|
5233
|
+
if (declaredMatches.length === 1) {
|
|
5234
|
+
boundTableBySource.set(source, declaredMatches[0]);
|
|
5235
|
+
continue;
|
|
5236
|
+
}
|
|
5237
|
+
throw new OrmDefinitionError(
|
|
5238
|
+
`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`
|
|
5239
|
+
);
|
|
5240
|
+
}
|
|
5241
|
+
return transformSqlOutsideLiterals(query, (segment) => {
|
|
5242
|
+
let rewritten = segment;
|
|
5243
|
+
for (const table of canonical.tables) {
|
|
5244
|
+
if (!table.source) continue;
|
|
5245
|
+
const boundTable = boundTableBySource.get(table.source);
|
|
5246
|
+
if (boundTable === void 0 || boundTable !== table.table) continue;
|
|
5247
|
+
const [sourceSchema, sourceName] = table.source.split(".");
|
|
5248
|
+
if (!sourceSchema || !sourceName) continue;
|
|
5249
|
+
rewritten = replaceQualifiedRelation(
|
|
5250
|
+
rewritten,
|
|
5251
|
+
sourceSchema,
|
|
5252
|
+
sourceName,
|
|
5253
|
+
schemaName,
|
|
5254
|
+
table.table
|
|
5255
|
+
);
|
|
5256
|
+
}
|
|
5257
|
+
return rewritten;
|
|
5258
|
+
});
|
|
5259
|
+
}
|
|
5260
|
+
function transformSqlOutsideLiterals(sql4, transform) {
|
|
5261
|
+
const out = [];
|
|
5262
|
+
let state = { kind: "normal" };
|
|
5263
|
+
let i = 0;
|
|
5264
|
+
let codeStart = 0;
|
|
5265
|
+
const emitCode = (end) => {
|
|
5266
|
+
if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
|
|
5267
|
+
codeStart = end;
|
|
5268
|
+
};
|
|
5269
|
+
while (i < sql4.length) {
|
|
5270
|
+
const c = sql4[i];
|
|
5271
|
+
const next = i + 1 < sql4.length ? sql4[i + 1] : "";
|
|
5272
|
+
switch (state.kind) {
|
|
5273
|
+
case "normal": {
|
|
5274
|
+
if (c === "-" && next === "-") {
|
|
5275
|
+
emitCode(i);
|
|
5276
|
+
state = { kind: "line-comment" };
|
|
5277
|
+
out.push("--");
|
|
5278
|
+
i += 2;
|
|
5279
|
+
codeStart = i;
|
|
5280
|
+
break;
|
|
5281
|
+
}
|
|
5282
|
+
if (c === "/" && next === "*") {
|
|
5283
|
+
emitCode(i);
|
|
5284
|
+
state = { kind: "block-comment", depth: 1 };
|
|
5285
|
+
out.push("/*");
|
|
5286
|
+
i += 2;
|
|
5287
|
+
codeStart = i;
|
|
5288
|
+
break;
|
|
5289
|
+
}
|
|
5290
|
+
if ((c === "E" || c === "e") && next === "'") {
|
|
5291
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
5292
|
+
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
5293
|
+
emitCode(i);
|
|
5294
|
+
state = { kind: "e-single" };
|
|
5295
|
+
out.push("E'");
|
|
5296
|
+
i += 2;
|
|
5297
|
+
codeStart = i;
|
|
5298
|
+
break;
|
|
5299
|
+
}
|
|
5300
|
+
}
|
|
5301
|
+
if (c === "'") {
|
|
5302
|
+
emitCode(i);
|
|
5303
|
+
state = { kind: "single" };
|
|
5304
|
+
out.push("'");
|
|
5305
|
+
i += 1;
|
|
5306
|
+
codeStart = i;
|
|
5307
|
+
break;
|
|
5308
|
+
}
|
|
5309
|
+
if (c === "$") {
|
|
5310
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
5311
|
+
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
5312
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
|
|
5313
|
+
if (tagMatch) {
|
|
5314
|
+
emitCode(i);
|
|
5315
|
+
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
5316
|
+
out.push(tagMatch[0]);
|
|
5317
|
+
i += tagMatch[0].length;
|
|
5318
|
+
codeStart = i;
|
|
5319
|
+
break;
|
|
5320
|
+
}
|
|
5321
|
+
}
|
|
5322
|
+
}
|
|
5323
|
+
i += 1;
|
|
5324
|
+
break;
|
|
5325
|
+
}
|
|
5326
|
+
case "single": {
|
|
5327
|
+
if (c === "'" && next === "'") {
|
|
5328
|
+
out.push("''");
|
|
5329
|
+
i += 2;
|
|
5330
|
+
break;
|
|
5331
|
+
}
|
|
5332
|
+
if (c === "'") {
|
|
5333
|
+
out.push("'");
|
|
5334
|
+
state = { kind: "normal" };
|
|
5335
|
+
i += 1;
|
|
5336
|
+
codeStart = i;
|
|
5337
|
+
break;
|
|
5338
|
+
}
|
|
5339
|
+
out.push(c);
|
|
5340
|
+
i += 1;
|
|
5341
|
+
break;
|
|
5342
|
+
}
|
|
5343
|
+
case "e-single": {
|
|
5344
|
+
if (c === "\\" && i + 1 < sql4.length) {
|
|
5345
|
+
out.push(c, sql4[i + 1]);
|
|
5346
|
+
i += 2;
|
|
5347
|
+
break;
|
|
5348
|
+
}
|
|
5349
|
+
if (c === "'" && next === "'") {
|
|
5350
|
+
out.push("''");
|
|
5351
|
+
i += 2;
|
|
5352
|
+
break;
|
|
5353
|
+
}
|
|
5354
|
+
if (c === "'") {
|
|
5355
|
+
out.push("'");
|
|
5356
|
+
state = { kind: "normal" };
|
|
5357
|
+
i += 1;
|
|
5358
|
+
codeStart = i;
|
|
5359
|
+
break;
|
|
5360
|
+
}
|
|
5361
|
+
out.push(c);
|
|
5362
|
+
i += 1;
|
|
5363
|
+
break;
|
|
5364
|
+
}
|
|
5365
|
+
case "dollar": {
|
|
5366
|
+
const closer = `$${state.tag}$`;
|
|
5367
|
+
if (sql4.startsWith(closer, i)) {
|
|
5368
|
+
out.push(closer);
|
|
5369
|
+
state = { kind: "normal" };
|
|
5370
|
+
i += closer.length;
|
|
5371
|
+
codeStart = i;
|
|
5372
|
+
break;
|
|
5373
|
+
}
|
|
5374
|
+
out.push(c);
|
|
5375
|
+
i += 1;
|
|
5376
|
+
break;
|
|
5377
|
+
}
|
|
5378
|
+
case "line-comment": {
|
|
5379
|
+
if (c === "\n") {
|
|
5380
|
+
out.push("\n");
|
|
5381
|
+
state = { kind: "normal" };
|
|
5382
|
+
i += 1;
|
|
5383
|
+
codeStart = i;
|
|
5384
|
+
break;
|
|
5385
|
+
}
|
|
5386
|
+
out.push(c);
|
|
5387
|
+
i += 1;
|
|
5388
|
+
break;
|
|
5389
|
+
}
|
|
5390
|
+
case "block-comment": {
|
|
5391
|
+
if (c === "/" && next === "*") {
|
|
5392
|
+
out.push("/*");
|
|
5393
|
+
state = { kind: "block-comment", depth: state.depth + 1 };
|
|
5394
|
+
i += 2;
|
|
5395
|
+
break;
|
|
5396
|
+
}
|
|
5397
|
+
if (c === "*" && next === "/") {
|
|
5398
|
+
out.push("*/");
|
|
5399
|
+
const newDepth = state.depth - 1;
|
|
5400
|
+
state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
|
|
5401
|
+
i += 2;
|
|
5402
|
+
if (state.kind === "normal") codeStart = i;
|
|
5403
|
+
break;
|
|
5404
|
+
}
|
|
5405
|
+
out.push(c);
|
|
5406
|
+
i += 1;
|
|
5407
|
+
break;
|
|
5408
|
+
}
|
|
5409
|
+
}
|
|
5410
|
+
}
|
|
5411
|
+
emitCode(sql4.length);
|
|
5412
|
+
return out.join("");
|
|
5413
|
+
}
|
|
5414
|
+
function escapeRegExp(value) {
|
|
5415
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5416
|
+
}
|
|
5417
|
+
function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
|
|
5418
|
+
let referenced = false;
|
|
5419
|
+
transformSqlOutsideLiterals(sql4, (segment) => {
|
|
5420
|
+
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
5421
|
+
return segment;
|
|
5422
|
+
});
|
|
5423
|
+
return referenced;
|
|
5424
|
+
}
|
|
5425
|
+
function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
5426
|
+
const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
|
|
5427
|
+
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
5428
|
+
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
5429
|
+
}
|
|
5430
|
+
function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
5431
|
+
return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
5432
|
+
}
|
|
4245
5433
|
function generateSnapshotV2(definition, previousId3) {
|
|
4246
5434
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
4247
|
-
|
|
5435
|
+
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5436
|
+
if ((definition.views?.length ?? 0) > 0) {
|
|
5437
|
+
const extended = snapshot;
|
|
5438
|
+
extended._sequenceOrm = {
|
|
5439
|
+
viewDependencies: Object.fromEntries(
|
|
5440
|
+
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5441
|
+
)
|
|
5442
|
+
};
|
|
5443
|
+
}
|
|
5444
|
+
return snapshot;
|
|
5445
|
+
}
|
|
5446
|
+
function buildViewColumnV2({
|
|
5447
|
+
view,
|
|
5448
|
+
column,
|
|
5449
|
+
enums
|
|
5450
|
+
}) {
|
|
5451
|
+
const factory = resolveColumnFactory(view, column, enums);
|
|
5452
|
+
let builder = factory(column.columnName);
|
|
5453
|
+
if (!column.nullable) builder = builder.notNull();
|
|
5454
|
+
return builder;
|
|
4248
5455
|
}
|
|
4249
5456
|
function buildTable2(schema, table, built, enums, byName) {
|
|
4250
5457
|
const primaryKeyColumns2 = table.primaryKey ?? [];
|
|
@@ -4488,16 +5695,270 @@ async function planMigrationV2({
|
|
|
4488
5695
|
}) {
|
|
4489
5696
|
const schema = ormSchemaName(canonical.namespace);
|
|
4490
5697
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
5698
|
+
assertValidViewDependencies(canonical);
|
|
4491
5699
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
4492
5700
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
4493
|
-
const
|
|
5701
|
+
const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
5702
|
+
const previousViews = snapshotViews(hints.adjusted);
|
|
5703
|
+
const currentViews = snapshotViews(snapshot);
|
|
5704
|
+
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
5705
|
+
const currentDependencies = snapshotViewDependencies(snapshot);
|
|
5706
|
+
const affected = affectedViewNames({
|
|
5707
|
+
previousViews,
|
|
5708
|
+
currentViews,
|
|
5709
|
+
previousDependencies,
|
|
5710
|
+
currentDependencies,
|
|
5711
|
+
baseChanged: hints.statements.length > 0 || baseDdl.length > 0
|
|
5712
|
+
});
|
|
5713
|
+
const dropOrder = topologicalViewNames(
|
|
5714
|
+
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
5715
|
+
previousDependencies
|
|
5716
|
+
).reverse();
|
|
5717
|
+
const createOrder = topologicalViewNames(
|
|
5718
|
+
new Set([...affected].filter((name) => currentViews.has(name))),
|
|
5719
|
+
currentDependencies
|
|
5720
|
+
);
|
|
5721
|
+
const dropDdl = await generatedViewStatements({
|
|
5722
|
+
snapshot: hints.adjusted,
|
|
5723
|
+
names: new Set(dropOrder),
|
|
5724
|
+
mode: "drop",
|
|
5725
|
+
schema
|
|
5726
|
+
});
|
|
5727
|
+
const createDdl = await generatedViewStatements({
|
|
5728
|
+
snapshot,
|
|
5729
|
+
names: new Set(createOrder),
|
|
5730
|
+
mode: "create",
|
|
5731
|
+
schema
|
|
5732
|
+
});
|
|
5733
|
+
const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
|
|
4494
5734
|
const statements = [
|
|
5735
|
+
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
4495
5736
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4496
|
-
...
|
|
5737
|
+
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
5738
|
+
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
5739
|
+
...createOrder.map((name) => {
|
|
5740
|
+
const view = currentViewDefinitions.get(name);
|
|
5741
|
+
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
5742
|
+
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
5743
|
+
})
|
|
4497
5744
|
];
|
|
4498
5745
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4499
5746
|
}
|
|
5747
|
+
function validateViewCatalogStatement({
|
|
5748
|
+
canonical,
|
|
5749
|
+
view,
|
|
5750
|
+
schema
|
|
5751
|
+
}) {
|
|
5752
|
+
const qualified = `${schema}.${view.view}`;
|
|
5753
|
+
const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
|
|
5754
|
+
const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
|
|
5755
|
+
const relationIdentity = /* @__PURE__ */ new Map();
|
|
5756
|
+
for (const table of canonical.tables) {
|
|
5757
|
+
relationIdentity.set(table.table, `${schema}.${table.table}`);
|
|
5758
|
+
}
|
|
5759
|
+
for (const candidate of canonical.views ?? []) {
|
|
5760
|
+
relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
|
|
5761
|
+
}
|
|
5762
|
+
const expectedDependencies = sqlTextArray(
|
|
5763
|
+
view.dependencies.map((dependency) => {
|
|
5764
|
+
const physical = relationIdentity.get(dependency);
|
|
5765
|
+
if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5766
|
+
return physical;
|
|
5767
|
+
}).sort()
|
|
5768
|
+
);
|
|
5769
|
+
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5770
|
+
return `DO $sequence_orm_validate_view$
|
|
5771
|
+
DECLARE
|
|
5772
|
+
actual_columns text[];
|
|
5773
|
+
actual_types oid[];
|
|
5774
|
+
actual_dependencies text[];
|
|
5775
|
+
BEGIN
|
|
5776
|
+
SELECT
|
|
5777
|
+
array_agg(attribute.attname ORDER BY attribute.attnum),
|
|
5778
|
+
array_agg(attribute.atttypid ORDER BY attribute.attnum)
|
|
5779
|
+
INTO actual_columns, actual_types
|
|
5780
|
+
FROM pg_catalog.pg_attribute AS attribute
|
|
5781
|
+
WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5782
|
+
AND attribute.attnum > 0
|
|
5783
|
+
AND NOT attribute.attisdropped;
|
|
5784
|
+
|
|
5785
|
+
IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
|
|
5786
|
+
RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
|
|
5787
|
+
USING ERRCODE = '42804';
|
|
5788
|
+
END IF;
|
|
5789
|
+
IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
|
|
5790
|
+
RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
|
|
5791
|
+
USING ERRCODE = '42804';
|
|
5792
|
+
END IF;
|
|
5793
|
+
|
|
5794
|
+
SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
|
|
5795
|
+
INTO actual_dependencies
|
|
5796
|
+
FROM (
|
|
5797
|
+
SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
|
|
5798
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
5799
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
5800
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
5801
|
+
AND dependency.objid = rewrite.oid
|
|
5802
|
+
AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
|
|
5803
|
+
JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
|
|
5804
|
+
JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
|
|
5805
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5806
|
+
AND dependency.refobjid <> rewrite.ev_class
|
|
5807
|
+
AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
|
|
5808
|
+
) AS found;
|
|
5809
|
+
|
|
5810
|
+
IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
|
|
5811
|
+
RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
|
|
5812
|
+
USING ERRCODE = '2BP01';
|
|
5813
|
+
END IF;
|
|
5814
|
+
|
|
5815
|
+
IF EXISTS (
|
|
5816
|
+
SELECT 1
|
|
5817
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
5818
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
5819
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
5820
|
+
AND dependency.objid = rewrite.oid
|
|
5821
|
+
AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
|
|
5822
|
+
JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
|
|
5823
|
+
JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
|
|
5824
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5825
|
+
AND (
|
|
5826
|
+
function_namespace.nspname <> 'pg_catalog'
|
|
5827
|
+
OR dependency_function.proname <> ALL (${allowedFunctions})
|
|
5828
|
+
)
|
|
5829
|
+
) THEN
|
|
5830
|
+
RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
|
|
5831
|
+
USING ERRCODE = '42501';
|
|
5832
|
+
END IF;
|
|
5833
|
+
|
|
5834
|
+
IF EXISTS (
|
|
5835
|
+
SELECT 1
|
|
5836
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
5837
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
5838
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
5839
|
+
AND dependency.objid = rewrite.oid
|
|
5840
|
+
AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
|
|
5841
|
+
JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
|
|
5842
|
+
JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
|
|
5843
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5844
|
+
AND operator_namespace.nspname <> 'pg_catalog'
|
|
5845
|
+
) THEN
|
|
5846
|
+
RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
|
|
5847
|
+
USING ERRCODE = '42501';
|
|
5848
|
+
END IF;
|
|
5849
|
+
END
|
|
5850
|
+
$sequence_orm_validate_view$`;
|
|
5851
|
+
}
|
|
5852
|
+
function sqlTextArray(values) {
|
|
5853
|
+
return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
|
|
5854
|
+
}
|
|
5855
|
+
function viewTypeRegtype(column, schema) {
|
|
5856
|
+
if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
|
|
5857
|
+
return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
|
|
5858
|
+
}
|
|
4500
5859
|
var previousId = (snapshot) => snapshot.id;
|
|
5860
|
+
function snapshotViews(snapshot) {
|
|
5861
|
+
const views = snapshot.views ?? {};
|
|
5862
|
+
return new Map(Object.values(views).map((view) => [view.name, view]));
|
|
5863
|
+
}
|
|
5864
|
+
function snapshotViewDependencies(snapshot) {
|
|
5865
|
+
const metadata = snapshot._sequenceOrm;
|
|
5866
|
+
return metadata?.viewDependencies ?? {};
|
|
5867
|
+
}
|
|
5868
|
+
function withoutViews(snapshot) {
|
|
5869
|
+
const clone = structuredClone(snapshot);
|
|
5870
|
+
clone.views = {};
|
|
5871
|
+
delete clone._sequenceOrm;
|
|
5872
|
+
return clone;
|
|
5873
|
+
}
|
|
5874
|
+
function affectedViewNames({
|
|
5875
|
+
previousViews,
|
|
5876
|
+
currentViews,
|
|
5877
|
+
previousDependencies,
|
|
5878
|
+
currentDependencies,
|
|
5879
|
+
baseChanged
|
|
5880
|
+
}) {
|
|
5881
|
+
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
5882
|
+
if (baseChanged) return allNames;
|
|
5883
|
+
const affected = new Set(
|
|
5884
|
+
[...allNames].filter((name) => {
|
|
5885
|
+
const before = previousViews.get(name);
|
|
5886
|
+
const after = currentViews.get(name);
|
|
5887
|
+
return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
|
|
5888
|
+
})
|
|
5889
|
+
);
|
|
5890
|
+
let grew = true;
|
|
5891
|
+
while (grew) {
|
|
5892
|
+
grew = false;
|
|
5893
|
+
for (const name of allNames) {
|
|
5894
|
+
if (affected.has(name)) continue;
|
|
5895
|
+
const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
5896
|
+
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
5897
|
+
affected.add(name);
|
|
5898
|
+
grew = true;
|
|
5899
|
+
}
|
|
5900
|
+
}
|
|
5901
|
+
}
|
|
5902
|
+
return affected;
|
|
5903
|
+
}
|
|
5904
|
+
function topologicalViewNames(names, dependencies) {
|
|
5905
|
+
const remaining = new Set(names);
|
|
5906
|
+
const ordered = [];
|
|
5907
|
+
while (remaining.size > 0) {
|
|
5908
|
+
const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
|
|
5909
|
+
if (ready.length === 0) {
|
|
5910
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
|
|
5911
|
+
}
|
|
5912
|
+
for (const name of ready) {
|
|
5913
|
+
ordered.push(name);
|
|
5914
|
+
remaining.delete(name);
|
|
5915
|
+
}
|
|
5916
|
+
}
|
|
5917
|
+
return ordered;
|
|
5918
|
+
}
|
|
5919
|
+
async function generatedViewStatements({
|
|
5920
|
+
snapshot,
|
|
5921
|
+
names,
|
|
5922
|
+
mode,
|
|
5923
|
+
schema
|
|
5924
|
+
}) {
|
|
5925
|
+
if (names.size === 0) return /* @__PURE__ */ new Map();
|
|
5926
|
+
const without = structuredClone(snapshot);
|
|
5927
|
+
const holder = without;
|
|
5928
|
+
const views = holder.views ?? {};
|
|
5929
|
+
holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
|
|
5930
|
+
const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
|
|
5931
|
+
const statements = /* @__PURE__ */ new Map();
|
|
5932
|
+
for (const name of names) {
|
|
5933
|
+
const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
5934
|
+
const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
|
|
5935
|
+
const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
|
|
5936
|
+
if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
|
|
5937
|
+
}
|
|
5938
|
+
return statements;
|
|
5939
|
+
}
|
|
5940
|
+
function requiredViewStatement(statements, name, schema, operation) {
|
|
5941
|
+
const statement = statements.get(name);
|
|
5942
|
+
if (!statement) {
|
|
5943
|
+
throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
|
|
5944
|
+
}
|
|
5945
|
+
return statement;
|
|
5946
|
+
}
|
|
5947
|
+
function assertValidViewDependencies(canonical) {
|
|
5948
|
+
const views = canonical.views ?? [];
|
|
5949
|
+
const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
|
|
5950
|
+
for (const view of views) {
|
|
5951
|
+
for (const dependency of view.dependencies) {
|
|
5952
|
+
if (!known.has(dependency)) {
|
|
5953
|
+
throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5957
|
+
topologicalViewNames(
|
|
5958
|
+
new Set(views.map((view) => view.view)),
|
|
5959
|
+
Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
|
|
5960
|
+
);
|
|
5961
|
+
}
|
|
4501
5962
|
var QUOTED_STRING_PATTERN2 = /'(?:[^']|'')*'/g;
|
|
4502
5963
|
var TOKEN_PATTERN = /\bclaim\(\s*'((?:[^']|'')*)'\s*\)|\bcapability\(\s*'((?:[^']|'')*)'\s*\)|'(?:[^']|'')*'|(?<!:):([a-z][a-z0-9_]*)/g;
|
|
4503
5964
|
var BARE_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
@@ -4518,8 +5979,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
4518
5979
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
4519
5980
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
4520
5981
|
}
|
|
4521
|
-
function compileStatement(
|
|
4522
|
-
const compiled =
|
|
5982
|
+
function compileStatement(sql4, renderParam) {
|
|
5983
|
+
const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
4523
5984
|
if (claimKey !== void 0) {
|
|
4524
5985
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
4525
5986
|
return claimExpression(claimKey);
|
|
@@ -4572,8 +6033,8 @@ function actionPrepareChecks(definition) {
|
|
|
4572
6033
|
const order = actionParamOrder(action);
|
|
4573
6034
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
4574
6035
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
4575
|
-
compiled.forEach((
|
|
4576
|
-
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns,
|
|
6036
|
+
compiled.forEach((sql4, index3) => {
|
|
6037
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
|
|
4577
6038
|
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
4578
6039
|
});
|
|
4579
6040
|
}
|
|
@@ -4689,15 +6150,15 @@ async function planMigration({
|
|
|
4689
6150
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4690
6151
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4691
6152
|
const statements = [
|
|
4692
|
-
...policyDrops.map((
|
|
4693
|
-
...hints.statements.map((
|
|
4694
|
-
...ddl.map((
|
|
4695
|
-
...securityStatements(definition).map((
|
|
6153
|
+
...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
6154
|
+
...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
|
|
6155
|
+
...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
|
|
6156
|
+
...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
4696
6157
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4697
6158
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4698
6159
|
// above creates, so they close the plan.
|
|
4699
|
-
...actionPrepareChecks(definition).map((
|
|
4700
|
-
...actionFunctionStatements(definition).map((
|
|
6160
|
+
...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
|
|
6161
|
+
...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
|
|
4701
6162
|
];
|
|
4702
6163
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4703
6164
|
}
|
|
@@ -4826,6 +6287,8 @@ function validateCapabilityRefs(files, declared) {
|
|
|
4826
6287
|
const referenced = [
|
|
4827
6288
|
...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
|
|
4828
6289
|
...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
|
|
6290
|
+
...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
|
|
6291
|
+
...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
|
|
4829
6292
|
...Object.values(table.actions ?? {}).flatMap((action) => [
|
|
4830
6293
|
...action.requires ? referencedCapabilities(action.requires.ast) : [],
|
|
4831
6294
|
...referencedExecuteCapabilities(action)
|
|
@@ -4983,7 +6446,7 @@ async function readMigrationSql(dir, entry) {
|
|
|
4983
6446
|
throw new OrmDefinitionError("missing migration file", path$1);
|
|
4984
6447
|
});
|
|
4985
6448
|
}
|
|
4986
|
-
var checksumOf = (
|
|
6449
|
+
var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
|
|
4987
6450
|
var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
|
|
4988
6451
|
function renderMigrationSql({
|
|
4989
6452
|
name,
|
|
@@ -5009,12 +6472,12 @@ async function writeMigration({
|
|
|
5009
6472
|
const tip = workspace.journal.entries.at(-1);
|
|
5010
6473
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
5011
6474
|
const name = migrationName(ordinal, contentHash);
|
|
5012
|
-
const
|
|
6475
|
+
const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
5013
6476
|
const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
5014
6477
|
await promises.mkdir(metaDir, { recursive: true });
|
|
5015
6478
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
5016
6479
|
`;
|
|
5017
|
-
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
6480
|
+
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
|
|
5018
6481
|
await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
5019
6482
|
const journal = {
|
|
5020
6483
|
...workspace.journal,
|
|
@@ -5025,7 +6488,7 @@ async function writeMigration({
|
|
|
5025
6488
|
name,
|
|
5026
6489
|
content_hash: contentHash,
|
|
5027
6490
|
previous_content_hash: tip?.content_hash ?? null,
|
|
5028
|
-
checksum: checksumOf(
|
|
6491
|
+
checksum: checksumOf(sql4),
|
|
5029
6492
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5030
6493
|
destructive
|
|
5031
6494
|
}
|
|
@@ -5140,8 +6603,8 @@ async function checkWorkspace({
|
|
|
5140
6603
|
let previousId3;
|
|
5141
6604
|
let previousHash = null;
|
|
5142
6605
|
for (const entry of workspace.journal.entries) {
|
|
5143
|
-
const
|
|
5144
|
-
if (checksumOf(
|
|
6606
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
6607
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
5145
6608
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
5146
6609
|
return 1;
|
|
5147
6610
|
}
|
|
@@ -5181,7 +6644,7 @@ async function checkWorkspace({
|
|
|
5181
6644
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
5182
6645
|
return 0;
|
|
5183
6646
|
}
|
|
5184
|
-
var firstLine = (
|
|
6647
|
+
var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
5185
6648
|
|
|
5186
6649
|
// src/migrations/from-yaml.ts
|
|
5187
6650
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -5213,7 +6676,7 @@ function convertTable(table) {
|
|
|
5213
6676
|
description: table.description,
|
|
5214
6677
|
source: table.source,
|
|
5215
6678
|
columns: columns.map(([name, column]) => convertColumn(name, column)),
|
|
5216
|
-
// Synced tables
|
|
6679
|
+
// Synced tables may declare a typed column/PK contract; untyped remain empty.
|
|
5217
6680
|
primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
|
|
5218
6681
|
links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
|
|
5219
6682
|
indexes: (table.indexes ?? []).map((index3) => ({
|
|
@@ -5226,9 +6689,20 @@ function convertTable(table) {
|
|
|
5226
6689
|
read: table.policies?.read?.source,
|
|
5227
6690
|
write: table.policies?.write?.source
|
|
5228
6691
|
},
|
|
5229
|
-
|
|
6692
|
+
// v2 has no action surface (writes are mutations); the transition sweep
|
|
6693
|
+
// drops the plpgsql action_* functions, so carrying the metadata would be
|
|
6694
|
+
// dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
|
|
6695
|
+
actions: []
|
|
5230
6696
|
};
|
|
5231
6697
|
}
|
|
6698
|
+
function droppedV1Actions(definition) {
|
|
6699
|
+
const dropped = /* @__PURE__ */ new Map();
|
|
6700
|
+
for (const table of definition.tables) {
|
|
6701
|
+
const names = Object.keys(table.actions ?? {});
|
|
6702
|
+
if (names.length > 0) dropped.set(table.table, names);
|
|
6703
|
+
}
|
|
6704
|
+
return dropped;
|
|
6705
|
+
}
|
|
5232
6706
|
function convertColumn(name, column) {
|
|
5233
6707
|
const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
|
|
5234
6708
|
return {
|
|
@@ -5262,25 +6736,6 @@ function convertLink(table, name, link) {
|
|
|
5262
6736
|
description: link.description
|
|
5263
6737
|
};
|
|
5264
6738
|
}
|
|
5265
|
-
function convertAction(table, name, action) {
|
|
5266
|
-
return {
|
|
5267
|
-
name,
|
|
5268
|
-
requires: action.requires?.source,
|
|
5269
|
-
params: Object.fromEntries(
|
|
5270
|
-
Object.entries(action.params ?? {}).map(([param, definition]) => [
|
|
5271
|
-
param,
|
|
5272
|
-
{ type: definition.type, nullable: definition.optional === true }
|
|
5273
|
-
])
|
|
5274
|
-
),
|
|
5275
|
-
// The v2 contract carries one SQL string; statement boundaries survive as
|
|
5276
|
-
// `;\n` (v1 validation guarantees no bare `;` inside a single statement).
|
|
5277
|
-
executes: actionStatementSources(action.executes).join(";\n"),
|
|
5278
|
-
// v1 declared the RETURNING columns inline; v2 types them at compile time,
|
|
5279
|
-
// so the shape collapses to rows-vs-rowsAffected.
|
|
5280
|
-
returns: action.returns ? "rows" : "rowsAffected",
|
|
5281
|
-
audit: action.audit ? { action: action.audit.action, resource: action.audit.resource ?? table } : void 0
|
|
5282
|
-
};
|
|
5283
|
-
}
|
|
5284
6739
|
|
|
5285
6740
|
// src/migrations/v2.ts
|
|
5286
6741
|
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
@@ -5303,7 +6758,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5303
6758
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5304
6759
|
}
|
|
5305
6760
|
const name = migrationName(ordinal, newHash);
|
|
5306
|
-
const
|
|
6761
|
+
const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
5307
6762
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5308
6763
|
`;
|
|
5309
6764
|
const entry = {
|
|
@@ -5311,11 +6766,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5311
6766
|
name,
|
|
5312
6767
|
content_hash: newHash,
|
|
5313
6768
|
previous_content_hash: previousHash,
|
|
5314
|
-
checksum: checksumOf(
|
|
6769
|
+
checksum: checksumOf(sql4),
|
|
5315
6770
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5316
6771
|
destructive: false
|
|
5317
6772
|
};
|
|
5318
|
-
return { name, entry, sql:
|
|
6773
|
+
return { name, entry, sql: sql4, snapshotText };
|
|
5319
6774
|
}
|
|
5320
6775
|
function renderRebaseMigrationSql({
|
|
5321
6776
|
name,
|
|
@@ -5353,15 +6808,15 @@ var PLATFORM_USER_FIELDS = [
|
|
|
5353
6808
|
];
|
|
5354
6809
|
function generateSdl(canonical, options = {}) {
|
|
5355
6810
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
5356
|
-
const tables = def.
|
|
6811
|
+
const tables = canonicalRelations(def).filter((table) => table.source === void 0);
|
|
5357
6812
|
if (tables.length === 0) {
|
|
5358
6813
|
throw new SdlGenerationError(
|
|
5359
|
-
`Namespace "${def.namespace}" exposes no
|
|
6814
|
+
`Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
|
|
5360
6815
|
);
|
|
5361
6816
|
}
|
|
5362
6817
|
for (const table of tables) {
|
|
5363
6818
|
if (table.columns.length === 0) {
|
|
5364
|
-
throw new SdlGenerationError(`
|
|
6819
|
+
throw new SdlGenerationError(`Relation "${table.table}" has no columns \u2014 cannot generate an object type.`);
|
|
5365
6820
|
}
|
|
5366
6821
|
}
|
|
5367
6822
|
const namesByTable = new Map(tables.map((table) => [table.table, tableNames(table.table)]));
|
|
@@ -5455,8 +6910,8 @@ function renderEnum(name, values) {
|
|
|
5455
6910
|
${values.map((value) => ` ${value}`).join("\n")}
|
|
5456
6911
|
}`;
|
|
5457
6912
|
}
|
|
5458
|
-
function descriptionLines(
|
|
5459
|
-
const escaped =
|
|
6913
|
+
function descriptionLines(text4, indent) {
|
|
6914
|
+
const escaped = text4.replace(/"""/g, '\\"""');
|
|
5460
6915
|
return [`${indent}"""`, ...escaped.split("\n").map((line) => `${indent}${line}`), `${indent}"""`];
|
|
5461
6916
|
}
|
|
5462
6917
|
function renderComparisonExp(gqlTypeName) {
|
|
@@ -5541,10 +6996,20 @@ function renderTableBlocks(ctx) {
|
|
|
5541
6996
|
${fields}
|
|
5542
6997
|
}`);
|
|
5543
6998
|
}
|
|
5544
|
-
if (
|
|
6999
|
+
if (allowsInsert(table) || allowsUpdate(table)) {
|
|
7000
|
+
blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
|
|
7001
|
+
}
|
|
5545
7002
|
return blocks;
|
|
5546
7003
|
}
|
|
5547
|
-
function
|
|
7004
|
+
function allowsInsert(table) {
|
|
7005
|
+
return table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
7006
|
+
}
|
|
7007
|
+
function allowsUpdate(table) {
|
|
7008
|
+
return table.policies.write !== void 0 || table.policies.update !== void 0;
|
|
7009
|
+
}
|
|
7010
|
+
function allowsOnConflict(table) {
|
|
7011
|
+
if (!allowsInsert(table) || !allowsUpdate(table)) return false;
|
|
7012
|
+
if (table.policies.insert !== void 0 || table.policies.update !== void 0) return false;
|
|
5548
7013
|
return table.policies.write !== void 0;
|
|
5549
7014
|
}
|
|
5550
7015
|
function renderObjectType(ctx) {
|
|
@@ -5676,46 +7141,50 @@ function constraintValues(table) {
|
|
|
5676
7141
|
const values = /* @__PURE__ */ new Set();
|
|
5677
7142
|
if (table.primaryKey && table.primaryKey.length > 0) values.add(`${table.table}_pkey`);
|
|
5678
7143
|
for (const column of table.columns) {
|
|
5679
|
-
if (column.unique) values.add(`${table.table}_${column.columnName}
|
|
7144
|
+
if (column.unique) values.add(`${table.table}_${column.columnName}_unique`);
|
|
5680
7145
|
}
|
|
5681
7146
|
for (const index3 of table.indexes) {
|
|
5682
7147
|
if (index3.unique) values.add(index3.name ?? `${table.table}_${index3.columns.join("_")}_key`);
|
|
5683
7148
|
}
|
|
5684
7149
|
return [...values].sort();
|
|
5685
7150
|
}
|
|
5686
|
-
function renderWriteBlocks(ctx) {
|
|
7151
|
+
function renderWriteBlocks(ctx, access) {
|
|
5687
7152
|
const { table, names, pinned } = ctx;
|
|
5688
7153
|
const blocks = [];
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
if (insertable.length === 0) {
|
|
5694
|
-
throw new SdlGenerationError(
|
|
5695
|
-
`Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
|
|
7154
|
+
if (access.insert) {
|
|
7155
|
+
const pinnedSet = new Set(pinned);
|
|
7156
|
+
const insertable = table.columns.filter(
|
|
7157
|
+
(column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
|
|
5696
7158
|
);
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
7159
|
+
if (insertable.length === 0) {
|
|
7160
|
+
throw new SdlGenerationError(
|
|
7161
|
+
`Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
|
|
7162
|
+
);
|
|
7163
|
+
}
|
|
7164
|
+
const insertLines = insertable.map(
|
|
7165
|
+
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7166
|
+
);
|
|
7167
|
+
blocks.push(`input ${names.insertInput} {
|
|
5702
7168
|
${insertLines.join("\n")}
|
|
5703
7169
|
}`);
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
7170
|
+
}
|
|
7171
|
+
if (access.update) {
|
|
7172
|
+
const setLines = table.columns.map(
|
|
7173
|
+
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7174
|
+
);
|
|
7175
|
+
blocks.push(`input ${names.setInput} {
|
|
5708
7176
|
${setLines.join("\n")}
|
|
5709
7177
|
}`);
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
7178
|
+
}
|
|
7179
|
+
const jsonb4 = jsonbColumns(table);
|
|
7180
|
+
if (access.update && jsonb4.length > 0) {
|
|
7181
|
+
const appendLines = jsonb4.map((column) => ` ${fieldName(column.name)}: jsonb`);
|
|
5713
7182
|
blocks.push(`input ${names.appendInput} {
|
|
5714
7183
|
${appendLines.join("\n")}
|
|
5715
7184
|
}`);
|
|
5716
7185
|
}
|
|
5717
7186
|
const numericish = numericishColumns(table);
|
|
5718
|
-
if (numericish.length > 0) {
|
|
7187
|
+
if (access.update && numericish.length > 0) {
|
|
5719
7188
|
const incLines = numericish.map(
|
|
5720
7189
|
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
5721
7190
|
);
|
|
@@ -5724,7 +7193,7 @@ ${incLines.join("\n")}
|
|
|
5724
7193
|
}`);
|
|
5725
7194
|
}
|
|
5726
7195
|
const constraints = constraintValues(table);
|
|
5727
|
-
if (constraints.length > 0) {
|
|
7196
|
+
if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
|
|
5728
7197
|
for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
|
|
5729
7198
|
blocks.push(renderEnum(names.constraintEnum, constraints));
|
|
5730
7199
|
const updateColumns = table.columns.map((column) => fieldName(column.name));
|
|
@@ -5737,12 +7206,14 @@ ${incLines.join("\n")}
|
|
|
5737
7206
|
}`
|
|
5738
7207
|
);
|
|
5739
7208
|
}
|
|
5740
|
-
|
|
5741
|
-
|
|
7209
|
+
if (access.insert || access.update) {
|
|
7210
|
+
blocks.push(
|
|
7211
|
+
`type ${names.mutationResponse} {
|
|
5742
7212
|
affectedRows: Int!
|
|
5743
7213
|
returning: [${names.typename}!]!
|
|
5744
7214
|
}`
|
|
5745
|
-
|
|
7215
|
+
);
|
|
7216
|
+
}
|
|
5746
7217
|
return blocks;
|
|
5747
7218
|
}
|
|
5748
7219
|
function renderQueryRoot(tables, namesByTable) {
|
|
@@ -5767,35 +7238,64 @@ function columnGqlTypeOf(table, column) {
|
|
|
5767
7238
|
function renderMutationRoot(tables, namesByTable) {
|
|
5768
7239
|
const lines = [];
|
|
5769
7240
|
for (const table of tables) {
|
|
5770
|
-
|
|
7241
|
+
const canInsert = allowsInsert(table);
|
|
7242
|
+
const canUpdate = allowsUpdate(table);
|
|
7243
|
+
if (!canInsert && !canUpdate) continue;
|
|
5771
7244
|
const names = namesByTable.get(table.table);
|
|
5772
7245
|
if (!names) continue;
|
|
5773
7246
|
const constraints = constraintValues(table);
|
|
5774
|
-
const onConflictArg = constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
7247
|
+
const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
5775
7248
|
const updateArgs = [`_set: ${names.setInput}`];
|
|
5776
7249
|
if (jsonbColumns(table).length > 0) updateArgs.push(`_append: ${names.appendInput}`);
|
|
5777
7250
|
if (numericishColumns(table).length > 0) updateArgs.push(`_inc: ${names.incInput}`);
|
|
5778
7251
|
const updateArgList = updateArgs.join(", ");
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
if (
|
|
5784
|
-
lines.push(
|
|
5785
|
-
|
|
5786
|
-
)
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
lines.push(` ${names.
|
|
7252
|
+
if (canInsert) {
|
|
7253
|
+
lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
|
|
7254
|
+
lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
|
|
7255
|
+
}
|
|
7256
|
+
if (canUpdate) {
|
|
7257
|
+
lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
|
|
7258
|
+
const pk = primaryKeyColumns(table);
|
|
7259
|
+
if (pk.length > 0) {
|
|
7260
|
+
lines.push(
|
|
7261
|
+
` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
|
|
7262
|
+
);
|
|
7263
|
+
}
|
|
7264
|
+
lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
|
|
7265
|
+
if (pk.length > 0) {
|
|
7266
|
+
const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
|
|
7267
|
+
lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
|
|
7268
|
+
}
|
|
5792
7269
|
}
|
|
7270
|
+
lines.push(...renderCowMutationFields(table, names));
|
|
5793
7271
|
}
|
|
5794
7272
|
if (lines.length === 0) return null;
|
|
5795
7273
|
return `type Mutation {
|
|
5796
7274
|
${lines.join("\n")}
|
|
5797
7275
|
}`;
|
|
5798
7276
|
}
|
|
7277
|
+
function renderCowMutationFields(table, names) {
|
|
7278
|
+
const lines = [];
|
|
7279
|
+
const columnByName = new Map(table.columns.map((column) => [column.name, column]));
|
|
7280
|
+
for (const mutation of table.cowMutations ?? []) {
|
|
7281
|
+
const pkColumn = columnByName.get(mutation.pk);
|
|
7282
|
+
if (!pkColumn) continue;
|
|
7283
|
+
const pkArg = `${fieldName(mutation.pk)}: ${columnGqlTypeOf(table, pkColumn)}!`;
|
|
7284
|
+
if (mutation.kind === "revert") {
|
|
7285
|
+
lines.push(` ${fieldName(mutation.name)}(${pkArg}): ${names.typename}`);
|
|
7286
|
+
continue;
|
|
7287
|
+
}
|
|
7288
|
+
const setArgs = mutation.set.map((property) => {
|
|
7289
|
+
const column = columnByName.get(property);
|
|
7290
|
+
if (!column) return null;
|
|
7291
|
+
const bang = column.nullable === false ? "!" : "";
|
|
7292
|
+
return `${fieldName(property)}: ${columnGqlTypeOf(table, column)}${bang}`;
|
|
7293
|
+
});
|
|
7294
|
+
if (setArgs.some((arg) => arg === null)) continue;
|
|
7295
|
+
lines.push(` ${fieldName(mutation.name)}(${pkArg}, ${setArgs.join(", ")}): ${names.typename}`);
|
|
7296
|
+
}
|
|
7297
|
+
return lines;
|
|
7298
|
+
}
|
|
5799
7299
|
|
|
5800
7300
|
// src/cli.ts
|
|
5801
7301
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
@@ -5809,6 +7309,8 @@ var USAGE = `usage:
|
|
|
5809
7309
|
seq-studio orm apply [dir] -e <env> author the migration, register, apply, and refresh the generated outputs (the everyday command)
|
|
5810
7310
|
seq-studio orm apply --dry-run -e <env> rehearse the migration on a copy of the environment's real data, then discard it (no real apply)
|
|
5811
7311
|
seq-studio orm apply --no-create -e <env> apply only if the namespace is already registered (CI guard; never provisions a new namespace)
|
|
7312
|
+
seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>
|
|
7313
|
+
execute one named persisted operation as the signed-in caller
|
|
5812
7314
|
seq-studio orm diff [dir] write/verify the committed migration standalone (--check is the offline CI gate; --allow-destructive consents; --rebase records a v1 -> v2 conversion as a verified no-op)
|
|
5813
7315
|
seq-studio orm migrate-from-yaml <dir> convert a v1 YAML namespace to the v2 TypeScript format (migrations/ preserved)
|
|
5814
7316
|
|
|
@@ -5832,6 +7334,10 @@ async function runCli(argv) {
|
|
|
5832
7334
|
console.log(USAGE);
|
|
5833
7335
|
return command ? 0 : 1;
|
|
5834
7336
|
}
|
|
7337
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
7338
|
+
console.log(USAGE);
|
|
7339
|
+
return 0;
|
|
7340
|
+
}
|
|
5835
7341
|
try {
|
|
5836
7342
|
switch (command) {
|
|
5837
7343
|
case "init":
|
|
@@ -5869,6 +7375,8 @@ async function runCli(argv) {
|
|
|
5869
7375
|
const dir = positional(rest) ?? ".";
|
|
5870
7376
|
return await detectProjectFormat(dir) === "v2" ? await applyV2Command(rest, dir) : await applyCommand(rest);
|
|
5871
7377
|
}
|
|
7378
|
+
case "execute":
|
|
7379
|
+
return await executeV2Command(rest);
|
|
5872
7380
|
default:
|
|
5873
7381
|
console.error(`unknown command: ${command}`);
|
|
5874
7382
|
console.log(USAGE);
|
|
@@ -5879,13 +7387,14 @@ async function runCli(argv) {
|
|
|
5879
7387
|
return 1;
|
|
5880
7388
|
}
|
|
5881
7389
|
}
|
|
7390
|
+
var NON_DEFINITION_YAML_FILES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "pnpm-workspace.yaml"]);
|
|
5882
7391
|
async function detectProjectFormat(dir) {
|
|
5883
7392
|
const entries = await promises.readdir(dir, { withFileTypes: true }).catch(() => {
|
|
5884
7393
|
throw new OrmDefinitionError(`namespace directory not found: ${dir}`);
|
|
5885
7394
|
});
|
|
5886
7395
|
const hasConfig = entries.some((entry) => entry.isFile() && entry.name === NAMESPACE_CONFIG_FILE);
|
|
5887
7396
|
const hasYaml = entries.some(
|
|
5888
|
-
(entry) => entry.isFile() && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
|
|
7397
|
+
(entry) => entry.isFile() && !NON_DEFINITION_YAML_FILES.has(entry.name) && DEFINITION_FILE_EXTENSIONS.some((extension) => entry.name.endsWith(extension))
|
|
5889
7398
|
);
|
|
5890
7399
|
if (hasConfig && hasYaml) {
|
|
5891
7400
|
throw new OrmDefinitionError(
|
|
@@ -6033,7 +7542,7 @@ async function generateCommand(rest) {
|
|
|
6033
7542
|
await writeGenerateOutputs(dir, compiled.project);
|
|
6034
7543
|
const { loaded, manifest } = compiled.project;
|
|
6035
7544
|
console.log(
|
|
6036
|
-
`wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE} (${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s))`
|
|
7545
|
+
`wrote ${SCHEMA_GRAPHQL_FILE}, ${OPERATIONS_MANIFEST_FILE}, ${TYPE_POLICIES_FILE} (${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s))`
|
|
6037
7546
|
);
|
|
6038
7547
|
return runConsumerCodegen(dir);
|
|
6039
7548
|
}
|
|
@@ -6080,13 +7589,86 @@ async function runConsumerCodegen(dir) {
|
|
|
6080
7589
|
console.log(`ran consumer graphql-codegen (${CODEGEN_CONFIG_FILE})`);
|
|
6081
7590
|
return 0;
|
|
6082
7591
|
}
|
|
7592
|
+
function isVariablesObject(value) {
|
|
7593
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
7594
|
+
}
|
|
7595
|
+
function parseExecuteArgs(rest) {
|
|
7596
|
+
const args = [...rest];
|
|
7597
|
+
const variablesIndex = args.indexOf("--variables-file");
|
|
7598
|
+
if (variablesIndex === -1) {
|
|
7599
|
+
throw new OrmDefinitionError("execute requires --variables-file <file>");
|
|
7600
|
+
}
|
|
7601
|
+
const variablesFile = args[variablesIndex + 1];
|
|
7602
|
+
if (!variablesFile) {
|
|
7603
|
+
throw new OrmDefinitionError("execute requires --variables-file <file>");
|
|
7604
|
+
}
|
|
7605
|
+
args.splice(variablesIndex, 2);
|
|
7606
|
+
const envIndex = args.indexOf("--env");
|
|
7607
|
+
if (envIndex !== -1) args.splice(envIndex, 2);
|
|
7608
|
+
if (args.some((arg) => arg.startsWith("-"))) {
|
|
7609
|
+
throw new OrmDefinitionError("execute accepts only <operation>, [dir], --variables-file, and --env");
|
|
7610
|
+
}
|
|
7611
|
+
const operation = args[0];
|
|
7612
|
+
if (!operation || args.length > 2) {
|
|
7613
|
+
throw new OrmDefinitionError("usage: seq-studio orm execute <operation> [dir] --variables-file <file> -e <env>");
|
|
7614
|
+
}
|
|
7615
|
+
return {
|
|
7616
|
+
operation,
|
|
7617
|
+
dir: args[1] ?? ".",
|
|
7618
|
+
variablesFile
|
|
7619
|
+
};
|
|
7620
|
+
}
|
|
7621
|
+
async function readExecuteVariables(variablesFile) {
|
|
7622
|
+
let parsed;
|
|
7623
|
+
try {
|
|
7624
|
+
parsed = JSON.parse(await promises.readFile(path.resolve(variablesFile), "utf8"));
|
|
7625
|
+
} catch (error) {
|
|
7626
|
+
throw new OrmDefinitionError(
|
|
7627
|
+
`could not read JSON variables from ${variablesFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
7628
|
+
);
|
|
7629
|
+
}
|
|
7630
|
+
if (!isVariablesObject(parsed)) {
|
|
7631
|
+
throw new OrmDefinitionError("--variables-file must contain a JSON object");
|
|
7632
|
+
}
|
|
7633
|
+
return parsed;
|
|
7634
|
+
}
|
|
7635
|
+
async function executeV2Command(rest) {
|
|
7636
|
+
const { dir, operation, variablesFile } = parseExecuteArgs(rest);
|
|
7637
|
+
if (await detectProjectFormat(dir) !== "v2") {
|
|
7638
|
+
throw new OrmDefinitionError("execute supports TypeScript ORM v2 namespaces only");
|
|
7639
|
+
}
|
|
7640
|
+
const loaded = await loadV2(dir);
|
|
7641
|
+
const manifestPath = path.join(dir, OPERATIONS_MANIFEST_FILE);
|
|
7642
|
+
let manifest;
|
|
7643
|
+
try {
|
|
7644
|
+
manifest = operationsManifestSchema.parse(JSON.parse(await promises.readFile(manifestPath, "utf8")));
|
|
7645
|
+
} catch (error) {
|
|
7646
|
+
throw new OrmDefinitionError(
|
|
7647
|
+
`could not read ${OPERATIONS_MANIFEST_FILE}: ${error instanceof Error ? error.message : String(error)} \u2014 run \`seq-studio orm generate ${dir}\` first`
|
|
7648
|
+
);
|
|
7649
|
+
}
|
|
7650
|
+
const selected = manifest.operations[operation];
|
|
7651
|
+
if (!selected) {
|
|
7652
|
+
throw new OrmDefinitionError(
|
|
7653
|
+
`operation "${operation}" is not in ${OPERATIONS_MANIFEST_FILE}; run \`seq-studio orm generate ${dir}\` after editing GraphQL documents`
|
|
7654
|
+
);
|
|
7655
|
+
}
|
|
7656
|
+
const variables = await readExecuteVariables(variablesFile);
|
|
7657
|
+
const response = await apiRequestStrict(
|
|
7658
|
+
"POST",
|
|
7659
|
+
`/_data/v2/${loaded.canonical.namespace}`,
|
|
7660
|
+
{ op: selected.hash, variables }
|
|
7661
|
+
);
|
|
7662
|
+
console.log(JSON.stringify(response, null, 2));
|
|
7663
|
+
return 0;
|
|
7664
|
+
}
|
|
6083
7665
|
async function validateV2Command(dir) {
|
|
6084
7666
|
const compiled = await compileV2Project(dir);
|
|
6085
7667
|
printDiagnostics(compiled.ok ? compiled.project.diagnostics : compiled.diagnostics);
|
|
6086
7668
|
if (!compiled.ok) return 1;
|
|
6087
7669
|
const { loaded, manifest } = compiled.project;
|
|
6088
7670
|
console.log(
|
|
6089
|
-
`namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s) valid`
|
|
7671
|
+
`namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${loaded.canonical.views?.length ?? 0} view(s), ${Object.keys(manifest.operations).length} operation(s) valid`
|
|
6090
7672
|
);
|
|
6091
7673
|
console.log(`content hash ${loaded.hash}`);
|
|
6092
7674
|
const workspace = await readMigrationWorkspace(dir, loaded.canonical.namespace);
|
|
@@ -6216,8 +7798,8 @@ async function checkWorkspaceV2({
|
|
|
6216
7798
|
let previousId3;
|
|
6217
7799
|
let previousHash = null;
|
|
6218
7800
|
for (const entry of workspace.journal.entries) {
|
|
6219
|
-
const
|
|
6220
|
-
if (checksumOf(
|
|
7801
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
7802
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
6221
7803
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
6222
7804
|
return 1;
|
|
6223
7805
|
}
|
|
@@ -6404,9 +7986,15 @@ async function migrateFromYamlCommand(rest) {
|
|
|
6404
7986
|
}
|
|
6405
7987
|
const v1 = await loadNamespaceDefinition(dir);
|
|
6406
7988
|
const canonical = convertV1DefinitionToCanonical(v1.definition);
|
|
7989
|
+
const droppedActions = droppedV1Actions(v1.definition);
|
|
6407
7990
|
await writeConverted(path.join(dir, NAMESPACE_CONFIG_FILE), emitSequenceConfig(canonical));
|
|
6408
7991
|
await promises.mkdir(path.join(dir, SCHEMA_DIR), { recursive: true });
|
|
6409
|
-
await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical));
|
|
7992
|
+
await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical, droppedActions));
|
|
7993
|
+
for (const [table, names] of droppedActions) {
|
|
7994
|
+
console.log(
|
|
7995
|
+
`note: v1 actions on '${table}' were dropped (${names.join(", ")}) \u2014 v2 has no action surface; re-author them as persisted mutations (see the TODO in schema/tables.ts)`
|
|
7996
|
+
);
|
|
7997
|
+
}
|
|
6410
7998
|
const reloaded = await loadV2(dir);
|
|
6411
7999
|
if (JSON.stringify(sortKeys(reloaded.canonical)) !== JSON.stringify(sortKeys(canonical))) {
|
|
6412
8000
|
console.error(
|
|
@@ -6470,14 +8058,15 @@ function emitSequenceConfig(canonical) {
|
|
|
6470
8058
|
lines.push("})", "");
|
|
6471
8059
|
return lines.join("\n");
|
|
6472
8060
|
}
|
|
6473
|
-
function emitSchemaTables(canonical) {
|
|
8061
|
+
function emitSchemaTables(canonical, droppedActions) {
|
|
6474
8062
|
const columnTables2 = canonical.tables.filter((table) => table.source === void 0);
|
|
6475
8063
|
const syncedTables = canonical.tables.filter((table) => table.source !== void 0);
|
|
6476
8064
|
const hasLinks = canonical.tables.some((table) => table.links.length > 0);
|
|
8065
|
+
const syncedHasColumns = syncedTables.some((table) => table.columns.length > 0);
|
|
6477
8066
|
const imports = [
|
|
6478
8067
|
...columnTables2.length > 0 ? ["defineTable"] : [],
|
|
6479
8068
|
...syncedTables.length > 0 ? ["defineSyncedTable"] : [],
|
|
6480
|
-
...columnTables2.length > 0 ? ["t"] : [],
|
|
8069
|
+
...columnTables2.length > 0 || syncedHasColumns ? ["t"] : [],
|
|
6481
8070
|
...hasLinks ? ["toOne"] : []
|
|
6482
8071
|
].sort((a, b) => a.localeCompare(b));
|
|
6483
8072
|
const parts = [
|
|
@@ -6491,18 +8080,62 @@ function emitSchemaTables(canonical) {
|
|
|
6491
8080
|
""
|
|
6492
8081
|
];
|
|
6493
8082
|
for (const table of canonical.tables) {
|
|
8083
|
+
const dropped = droppedActions?.get(table.table) ?? [];
|
|
8084
|
+
if (dropped.length > 0) {
|
|
8085
|
+
parts.push(
|
|
8086
|
+
`// TODO: v1 actions dropped by the conversion \u2014 v2 has no action surface (writes`,
|
|
8087
|
+
`// are persisted mutations). Re-author for '${table.table}': ${dropped.join(", ")}`
|
|
8088
|
+
);
|
|
8089
|
+
}
|
|
6494
8090
|
parts.push(table.source !== void 0 ? emitSyncedTable(table) : emitTable(table));
|
|
6495
8091
|
}
|
|
6496
8092
|
return parts.join("\n");
|
|
6497
8093
|
}
|
|
6498
8094
|
function emitSyncedTable(table) {
|
|
8095
|
+
const singlePrimaryKey = table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
8096
|
+
if (table.columns.length === 0) {
|
|
8097
|
+
const lines2 = [
|
|
8098
|
+
`export const ${tableVarName(table.table)} = defineSyncedTable(${quote(table.table)}, {`,
|
|
8099
|
+
` source: ${quote(table.source ?? "")},`
|
|
8100
|
+
];
|
|
8101
|
+
if (table.description !== void 0) lines2.push(` description: ${quote(table.description)},`);
|
|
8102
|
+
if (table.policies.read !== void 0) {
|
|
8103
|
+
lines2.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
|
|
8104
|
+
}
|
|
8105
|
+
lines2.push("})", "");
|
|
8106
|
+
return lines2.join("\n");
|
|
8107
|
+
}
|
|
6499
8108
|
const lines = [
|
|
6500
|
-
`export const ${tableVarName(table.table)} = defineSyncedTable(
|
|
6501
|
-
|
|
6502
|
-
|
|
8109
|
+
`export const ${tableVarName(table.table)} = defineSyncedTable(`,
|
|
8110
|
+
` ${quote(table.table)},`,
|
|
8111
|
+
" {"
|
|
6503
8112
|
];
|
|
6504
|
-
|
|
6505
|
-
lines.push("}
|
|
8113
|
+
for (const column of table.columns) lines.push(` ${columnProperty(table.table, column, singlePrimaryKey)}`);
|
|
8114
|
+
lines.push(" },", " {");
|
|
8115
|
+
lines.push(` source: ${quote(table.source ?? "")},`);
|
|
8116
|
+
if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
|
|
8117
|
+
if (singlePrimaryKey === void 0 && table.primaryKey && table.primaryKey.length > 0) {
|
|
8118
|
+
lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
|
|
8119
|
+
}
|
|
8120
|
+
if (table.links.length > 0) {
|
|
8121
|
+
lines.push(" links: {");
|
|
8122
|
+
for (const link of table.links) {
|
|
8123
|
+
if (link.kind !== "toOne" || link.target === void 0) {
|
|
8124
|
+
throw new OrmDefinitionError(
|
|
8125
|
+
`link '${link.name}' on synced '${table.table}' is not a toOne link \u2014 not a v1 conversion output`
|
|
8126
|
+
);
|
|
8127
|
+
}
|
|
8128
|
+
const options = [`on: ${quote(link.on)}`];
|
|
8129
|
+
if (link.references !== void 0) options.push(`references: ${quote(link.references)}`);
|
|
8130
|
+
if (link.description !== void 0) options.push(`description: ${quote(link.description)}`);
|
|
8131
|
+
lines.push(` ${objectKey(link.name)}: toOne(() => ${tableVarName(link.target)}, { ${options.join(", ")} }),`);
|
|
8132
|
+
}
|
|
8133
|
+
lines.push(" },");
|
|
8134
|
+
}
|
|
8135
|
+
if (table.policies.read !== void 0) {
|
|
8136
|
+
lines.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
|
|
8137
|
+
}
|
|
8138
|
+
lines.push(" },", ")", "");
|
|
6506
8139
|
return lines.join("\n");
|
|
6507
8140
|
}
|
|
6508
8141
|
function emitTable(table) {
|
|
@@ -6572,7 +8205,6 @@ function emitTableConfig(table, singlePrimaryKey) {
|
|
|
6572
8205
|
if (table.policies.write !== void 0) lines.push(` write: ${policyLiteral(table.policies.write)},`);
|
|
6573
8206
|
lines.push(" },");
|
|
6574
8207
|
}
|
|
6575
|
-
if (table.actions.length > 0) lines.push(` actions: ${jsonLiteral(table.actions, 4)},`);
|
|
6576
8208
|
return lines;
|
|
6577
8209
|
}
|
|
6578
8210
|
function policyLiteral(policy) {
|
|
@@ -6873,7 +8505,7 @@ function positional(rest) {
|
|
|
6873
8505
|
if (envIndex !== -1) args.splice(envIndex, 2);
|
|
6874
8506
|
return args.find((arg) => !arg.startsWith("-"));
|
|
6875
8507
|
}
|
|
6876
|
-
var firstLine2 = (
|
|
8508
|
+
var firstLine2 = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
6877
8509
|
|
|
6878
8510
|
exports.BASE_URL_ENV = BASE_URL_ENV;
|
|
6879
8511
|
exports.ORM_MODULE_PATH_ENV = ORM_MODULE_PATH_ENV;
|