@sequenceholdings/orm 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -1
- package/dist/authoring.cjs +1066 -59
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +289 -24
- package/dist/authoring.d.ts +289 -24
- package/dist/authoring.js +1056 -60
- 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 +1764 -225
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1765 -226
- 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 +506 -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 +504 -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 +2223 -219
- 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 +2199 -220
- 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 +1093 -0
- package/dist/predicate-v2.cjs.map +1 -0
- package/dist/predicate-v2.d.cts +139 -0
- package/dist/predicate-v2.d.ts +139 -0
- package/dist/predicate-v2.js +1083 -0
- package/dist/predicate-v2.js.map +1 -0
- package/dist/sdl.cjs +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 +16 -1
- package/templates/orm-namespace/CLAUDE.md +6 -5
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,20 +1156,35 @@ 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
|
|
1174
|
+
);
|
|
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)))])
|
|
493
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
|
+
}
|
|
494
1188
|
}
|
|
495
1189
|
function toCanonicalPolicy(policy) {
|
|
496
1190
|
if (typeof policy === "string") return policy;
|
|
@@ -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("; ");
|
|
@@ -569,16 +1263,20 @@ async function loadNamespaceFromDir(dir, options = {}) {
|
|
|
569
1263
|
for (const entry of context.tables) {
|
|
570
1264
|
if (isSeqTable(entry.value)) tables.push(entry.value);
|
|
571
1265
|
}
|
|
572
|
-
|
|
1266
|
+
const views = [];
|
|
1267
|
+
for (const entry of context.views) {
|
|
1268
|
+
if (isSeqView(entry.value)) views.push(entry.value);
|
|
1269
|
+
}
|
|
1270
|
+
if (tables.length === 0 && views.length === 0) {
|
|
573
1271
|
throw new OrmDefinitionError(
|
|
574
|
-
`no
|
|
1272
|
+
`no relations registered \u2014 ${SCHEMA_DIR}/*.ts modules must call defineTable(...) or defineView(...) at import time`
|
|
575
1273
|
);
|
|
576
1274
|
}
|
|
577
1275
|
const enums = [];
|
|
578
1276
|
for (const entry of context.enums) {
|
|
579
1277
|
if (isEnumRef(entry.value)) enums.push(entry.value);
|
|
580
1278
|
}
|
|
581
|
-
const canonical = serializeNamespace({ config: configEntry.value, tables, enums });
|
|
1279
|
+
const canonical = serializeNamespace({ config: configEntry.value, tables, views, enums });
|
|
582
1280
|
return { canonical, hash: computeCanonicalHash(canonical), files };
|
|
583
1281
|
}
|
|
584
1282
|
async function listSchemaFiles(root) {
|
|
@@ -612,7 +1310,7 @@ var manifestRequiresSchema = zod.z.object({
|
|
|
612
1310
|
}).nullable();
|
|
613
1311
|
var manifestOperationSchema = zod.z.object({
|
|
614
1312
|
hash: zod.z.string().regex(OP_HASH_RE),
|
|
615
|
-
kind: zod.z.enum(["query", "mutation"
|
|
1313
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
616
1314
|
requires: manifestRequiresSchema
|
|
617
1315
|
});
|
|
618
1316
|
zod.z.object({
|
|
@@ -775,14 +1473,14 @@ function generateTypePolicies(canonical) {
|
|
|
775
1473
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
776
1474
|
const entries = [];
|
|
777
1475
|
let hasPlatformUser = false;
|
|
778
|
-
for (const table of def
|
|
1476
|
+
for (const table of canonicalRelations(def)) {
|
|
779
1477
|
if (table.source !== void 0) continue;
|
|
780
1478
|
if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
|
|
781
1479
|
if (!table.primaryKey || table.primaryKey.length === 0) continue;
|
|
782
1480
|
const keyFields = table.primaryKey.map((identifier2) => {
|
|
783
1481
|
const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
|
|
784
1482
|
if (!column) {
|
|
785
|
-
throw new CodegenGqlError(`Primary key column "${identifier2}" not found on
|
|
1483
|
+
throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
|
|
786
1484
|
}
|
|
787
1485
|
return fieldName(column.name);
|
|
788
1486
|
});
|
|
@@ -936,9 +1634,23 @@ var onConflictIrSchema = zod.z.union([
|
|
|
936
1634
|
]);
|
|
937
1635
|
var mutationRootSchema = zod.z.object({
|
|
938
1636
|
kind: zod.z.literal("mutation"),
|
|
939
|
-
|
|
1637
|
+
/**
|
|
1638
|
+
* CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
|
|
1639
|
+
* synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
|
|
1640
|
+
* applies `set` — both statements generated by the executor from this
|
|
1641
|
+
* metadata, never authored SQL. `cowRevert` deletes the override so the
|
|
1642
|
+
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
1643
|
+
* row and carry the write policy in `where` like update/delete.
|
|
1644
|
+
*/
|
|
1645
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
940
1646
|
alias: zod.z.string(),
|
|
941
1647
|
table: zod.z.string(),
|
|
1648
|
+
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
1649
|
+
cowSynced: zod.z.string().optional(),
|
|
1650
|
+
/** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
|
|
1651
|
+
cowCopyColumns: zod.z.array(zod.z.string()).optional(),
|
|
1652
|
+
/** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
|
|
1653
|
+
cowCopyCheck: predIrSchema.optional(),
|
|
942
1654
|
/** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
|
|
943
1655
|
objects: zod.z.union([
|
|
944
1656
|
zod.z.object({ slot: zod.z.string() }),
|
|
@@ -959,19 +1671,11 @@ var mutationRootSchema = zod.z.object({
|
|
|
959
1671
|
affectedRows: zod.z.boolean().default(false),
|
|
960
1672
|
includeIf: includeIfSchema.optional()
|
|
961
1673
|
});
|
|
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
1674
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
970
1675
|
selectRootSchema,
|
|
971
1676
|
byPkRootSchema,
|
|
972
1677
|
aggregateRootSchema,
|
|
973
|
-
mutationRootSchema
|
|
974
|
-
actionRootSchema
|
|
1678
|
+
mutationRootSchema
|
|
975
1679
|
]);
|
|
976
1680
|
var INPUT_REF_KINDS = [
|
|
977
1681
|
"boolExp",
|
|
@@ -1003,7 +1707,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
1003
1707
|
irVersion: zod.z.literal(IR_VERSION),
|
|
1004
1708
|
name: zod.z.string(),
|
|
1005
1709
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
1006
|
-
kind: zod.z.enum(["query", "mutation"
|
|
1710
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
1007
1711
|
requires: zod.z.object({
|
|
1008
1712
|
capability: zod.z.string().optional(),
|
|
1009
1713
|
role: zod.z.string().optional()
|
|
@@ -1017,13 +1721,24 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
1017
1721
|
table: zod.z.string(),
|
|
1018
1722
|
schema: zod.z.string(),
|
|
1019
1723
|
typename: zod.z.string(),
|
|
1724
|
+
/**
|
|
1725
|
+
* True when statements over this relation evaluate a claims-gated synced
|
|
1726
|
+
* view: synced twins (`source: gold.*`) materialize as views whose baked
|
|
1727
|
+
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
1728
|
+
* inherit the gate transitively through their declared dependencies. The
|
|
1729
|
+
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
1730
|
+
* touching such a relation, or the synced arm yields zero rows regardless
|
|
1731
|
+
* of the compiled predicates.
|
|
1732
|
+
*/
|
|
1733
|
+
claimsGated: zod.z.boolean().default(false),
|
|
1020
1734
|
columns: zod.z.array(zod.z.object({
|
|
1021
1735
|
name: zod.z.string(),
|
|
1022
1736
|
columnName: zod.z.string(),
|
|
1023
1737
|
type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
1024
1738
|
nullable: zod.z.boolean()
|
|
1025
1739
|
})),
|
|
1026
|
-
|
|
1740
|
+
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
1741
|
+
primaryKey: zod.z.array(zod.z.string()),
|
|
1027
1742
|
links: zod.z.array(zod.z.object({
|
|
1028
1743
|
name: zod.z.string(),
|
|
1029
1744
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
@@ -1562,6 +2277,7 @@ var NamespaceCompileError = class extends Error {
|
|
|
1562
2277
|
|
|
1563
2278
|
// src/compiler-gql/pred-utils.ts
|
|
1564
2279
|
var CONST_TRUE = { kind: "const", value: true };
|
|
2280
|
+
var CONST_FALSE = { kind: "const", value: false };
|
|
1565
2281
|
function andPred(...preds) {
|
|
1566
2282
|
const operands = preds.filter(
|
|
1567
2283
|
(pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
|
|
@@ -1647,6 +2363,12 @@ var PolicyInjector = class {
|
|
|
1647
2363
|
write(table) {
|
|
1648
2364
|
return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
|
|
1649
2365
|
}
|
|
2366
|
+
insert(table) {
|
|
2367
|
+
return this.ctx.insertPolicies.get(table) ?? this.write(table);
|
|
2368
|
+
}
|
|
2369
|
+
update(table) {
|
|
2370
|
+
return this.ctx.updatePolicies.get(table) ?? this.write(table);
|
|
2371
|
+
}
|
|
1650
2372
|
linkTarget(table, link) {
|
|
1651
2373
|
const meta = this.ctx.tables[table];
|
|
1652
2374
|
const resolved = meta?.links.find((l) => l.name === link);
|
|
@@ -1721,31 +2443,50 @@ var PolicyInjector = class {
|
|
|
1721
2443
|
};
|
|
1722
2444
|
case "mutation":
|
|
1723
2445
|
return this.injectMutation(root);
|
|
1724
|
-
case "action":
|
|
1725
|
-
return root;
|
|
1726
2446
|
}
|
|
1727
2447
|
}
|
|
1728
2448
|
injectMutation(root) {
|
|
1729
2449
|
const table = root.table;
|
|
1730
|
-
const writePolicy = this.write(table);
|
|
1731
2450
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
1732
2451
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1733
|
-
|
|
2452
|
+
const insertPolicy = this.insert(table);
|
|
2453
|
+
if (predContainsExists(insertPolicy)) {
|
|
1734
2454
|
throw new DocumentCompileError(
|
|
1735
|
-
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its
|
|
2455
|
+
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
1736
2456
|
"insert-check-unrepresentable"
|
|
1737
2457
|
);
|
|
1738
2458
|
}
|
|
1739
2459
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
1740
2460
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
1741
2461
|
);
|
|
1742
|
-
|
|
2462
|
+
if (root.onConflict && this.ctx.splitWritePolicyTables.has(table)) {
|
|
2463
|
+
throw new DocumentCompileError(
|
|
2464
|
+
`table '${table}' cannot use onConflict while insert and update policies are split \u2014 use separate insert and update mutations`,
|
|
2465
|
+
"on-conflict-split-policies"
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
return { ...root, pinnedColumns: pins, insertCheck: insertPolicy, returning };
|
|
1743
2469
|
}
|
|
1744
|
-
const
|
|
2470
|
+
const updatePolicy = this.update(table);
|
|
2471
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
1745
2472
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
1746
|
-
const check = predContainsExists(
|
|
2473
|
+
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
1747
2474
|
return { ...root, where, ...check, returning };
|
|
1748
2475
|
}
|
|
2476
|
+
if (root.op === "cowOverride") {
|
|
2477
|
+
const insertPolicy = this.insert(table);
|
|
2478
|
+
if (predContainsExists(insertPolicy)) {
|
|
2479
|
+
throw new DocumentCompileError(
|
|
2480
|
+
`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)`,
|
|
2481
|
+
"insert-check-unrepresentable"
|
|
2482
|
+
);
|
|
2483
|
+
}
|
|
2484
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
2485
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
2486
|
+
);
|
|
2487
|
+
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
2488
|
+
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
2489
|
+
}
|
|
1749
2490
|
return { ...root, where, returning };
|
|
1750
2491
|
}
|
|
1751
2492
|
};
|
|
@@ -1791,7 +2532,6 @@ function collectTableSet(roots, ctx) {
|
|
|
1791
2532
|
}
|
|
1792
2533
|
};
|
|
1793
2534
|
for (const root of roots) {
|
|
1794
|
-
if (root.kind === "action") continue;
|
|
1795
2535
|
tables.add(root.table);
|
|
1796
2536
|
if (root.kind === "select" || root.kind === "byPk") {
|
|
1797
2537
|
visitPred(root.where, root.table);
|
|
@@ -1802,8 +2542,10 @@ function collectTableSet(roots, ctx) {
|
|
|
1802
2542
|
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1803
2543
|
}
|
|
1804
2544
|
if (root.kind === "mutation") {
|
|
2545
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
1805
2546
|
if (root.where) visitPred(root.where, root.table);
|
|
1806
2547
|
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
2548
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
1807
2549
|
if (root.returning) visitSelection(root.returning, root.table);
|
|
1808
2550
|
}
|
|
1809
2551
|
}
|
|
@@ -1870,6 +2612,7 @@ function platformUserMeta() {
|
|
|
1870
2612
|
table: "users_directory",
|
|
1871
2613
|
schema: "platform",
|
|
1872
2614
|
typename: PLATFORM_USER_TYPENAME,
|
|
2615
|
+
claimsGated: false,
|
|
1873
2616
|
columns: columns.map((c) => ({ ...c })),
|
|
1874
2617
|
primaryKey: ["sub"],
|
|
1875
2618
|
links: [],
|
|
@@ -1912,15 +2655,30 @@ function buildCompileContext(rawCanonical) {
|
|
|
1912
2655
|
const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
|
|
1913
2656
|
const diagnostics = [];
|
|
1914
2657
|
const enums = new Map(canonical.enums.map((e) => [e.name, e]));
|
|
2658
|
+
const relations = canonicalRelations(canonical);
|
|
2659
|
+
const viewNames = new Set((canonical.views ?? []).map((view) => view.view));
|
|
1915
2660
|
const columnEntries = /* @__PURE__ */ new Map();
|
|
1916
2661
|
const tables = {};
|
|
1917
|
-
const usesPlatformUsers =
|
|
2662
|
+
const usesPlatformUsers = relations.some((t2) => t2.links.some((l) => l.kind === "toPlatformUser"));
|
|
1918
2663
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
1919
2664
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
1920
|
-
|
|
2665
|
+
const syncedTableNames = new Set(canonical.tables.filter((t2) => t2.source !== void 0).map((t2) => t2.table));
|
|
2666
|
+
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
2667
|
+
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
2668
|
+
const relationIsClaimsGated = (name) => {
|
|
2669
|
+
if (syncedTableNames.has(name)) return true;
|
|
2670
|
+
const memo = claimsGatedMemo.get(name);
|
|
2671
|
+
if (memo !== void 0) return memo;
|
|
2672
|
+
claimsGatedMemo.set(name, false);
|
|
2673
|
+
const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
|
|
2674
|
+
claimsGatedMemo.set(name, gated);
|
|
2675
|
+
return gated;
|
|
2676
|
+
};
|
|
2677
|
+
for (const table of relations) {
|
|
2678
|
+
const isView = viewNames.has(table.table);
|
|
1921
2679
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
1922
2680
|
columnEntries.set(table.table, entries);
|
|
1923
|
-
const pkRefs = table.primaryKey ?? ["id"];
|
|
2681
|
+
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1924
2682
|
const primaryKey2 = [];
|
|
1925
2683
|
for (const ref of pkRefs) {
|
|
1926
2684
|
const resolved = logicalColumn(entries, ref);
|
|
@@ -1941,7 +2699,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1941
2699
|
for (const column of table.columns) {
|
|
1942
2700
|
if (column.unique) {
|
|
1943
2701
|
const logical = logicalColumn(entries, column.columnName);
|
|
1944
|
-
constraints.push({ name: `${table.table}_${column.columnName}
|
|
2702
|
+
constraints.push({ name: `${table.table}_${column.columnName}_unique`, columns: [logical] });
|
|
1945
2703
|
}
|
|
1946
2704
|
}
|
|
1947
2705
|
for (const index3 of table.indexes) {
|
|
@@ -1953,20 +2711,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
1953
2711
|
table: table.table,
|
|
1954
2712
|
schema: ormSchemaName(canonical.namespace),
|
|
1955
2713
|
typename: typeNameForTable(table.table),
|
|
2714
|
+
// Synced twins (and views reading them, resolved transitively below)
|
|
2715
|
+
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
2716
|
+
claimsGated: relationIsClaimsGated(table.table),
|
|
1956
2717
|
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1957
|
-
primaryKey: primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
2718
|
+
primaryKey: isView ? primaryKey2 : primaryKey2.length > 0 ? primaryKey2 : ["id"],
|
|
1958
2719
|
links: [],
|
|
1959
2720
|
filterable: entries.map((c) => c.name),
|
|
1960
2721
|
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,
|
|
2722
|
+
insertable: isView ? [] : active.map((c) => c.name),
|
|
2723
|
+
settable: isView ? [] : active.map((c) => c.name),
|
|
2724
|
+
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
2725
|
+
constraints: isView ? [] : constraints,
|
|
1965
2726
|
dynamicFilterMaxDepth: 6,
|
|
1966
2727
|
dynamicFilterMaxNodes: 64
|
|
1967
2728
|
};
|
|
1968
2729
|
}
|
|
1969
|
-
for (const table of
|
|
2730
|
+
for (const table of relations) {
|
|
1970
2731
|
const meta = tables[table.table];
|
|
1971
2732
|
const entries = columnEntries.get(table.table);
|
|
1972
2733
|
for (const link of table.links) {
|
|
@@ -2001,17 +2762,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
2001
2762
|
}
|
|
2002
2763
|
const readPolicies = /* @__PURE__ */ new Map();
|
|
2003
2764
|
const writePolicies = /* @__PURE__ */ new Map();
|
|
2765
|
+
const insertPolicies = /* @__PURE__ */ new Map();
|
|
2766
|
+
const updatePolicies = /* @__PURE__ */ new Map();
|
|
2004
2767
|
const pins = /* @__PURE__ */ new Map();
|
|
2768
|
+
const splitWritePolicyTables = /* @__PURE__ */ new Set();
|
|
2005
2769
|
const resolver = { tables };
|
|
2006
|
-
for (const table of
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
["write", writePolicies]
|
|
2010
|
-
]) {
|
|
2770
|
+
for (const table of relations) {
|
|
2771
|
+
const isView = viewNames.has(table.table);
|
|
2772
|
+
const compilePolicyFor = (access, store) => {
|
|
2011
2773
|
const expression = table.policies[access];
|
|
2012
2774
|
if (expression === void 0) {
|
|
2013
|
-
|
|
2014
|
-
continue;
|
|
2775
|
+
return;
|
|
2015
2776
|
}
|
|
2016
2777
|
try {
|
|
2017
2778
|
store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
|
|
@@ -2024,37 +2785,117 @@ function buildCompileContext(rawCanonical) {
|
|
|
2024
2785
|
table: table.table
|
|
2025
2786
|
});
|
|
2026
2787
|
store.set(table.table, CONST_TRUE);
|
|
2027
|
-
|
|
2788
|
+
return;
|
|
2028
2789
|
}
|
|
2029
2790
|
throw error;
|
|
2030
2791
|
}
|
|
2792
|
+
};
|
|
2793
|
+
compilePolicyFor("read", readPolicies);
|
|
2794
|
+
compilePolicyFor("write", writePolicies);
|
|
2795
|
+
compilePolicyFor("insert", insertPolicies);
|
|
2796
|
+
compilePolicyFor("update", updatePolicies);
|
|
2797
|
+
if (isView) {
|
|
2798
|
+
writePolicies.set(table.table, CONST_FALSE);
|
|
2799
|
+
insertPolicies.set(table.table, CONST_FALSE);
|
|
2800
|
+
updatePolicies.set(table.table, CONST_FALSE);
|
|
2801
|
+
pins.set(table.table, []);
|
|
2802
|
+
continue;
|
|
2031
2803
|
}
|
|
2032
|
-
|
|
2804
|
+
if (table.policies.insert !== void 0 || table.policies.update !== void 0) {
|
|
2805
|
+
splitWritePolicyTables.add(table.table);
|
|
2806
|
+
}
|
|
2807
|
+
const fallbackWrite = writePolicies.get(table.table);
|
|
2808
|
+
const hasSplitPolicy = table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2809
|
+
if (!insertPolicies.has(table.table)) {
|
|
2810
|
+
if (fallbackWrite !== void 0) insertPolicies.set(table.table, fallbackWrite);
|
|
2811
|
+
else if (hasSplitPolicy) insertPolicies.set(table.table, CONST_FALSE);
|
|
2812
|
+
else insertPolicies.set(table.table, CONST_TRUE);
|
|
2813
|
+
}
|
|
2814
|
+
if (!updatePolicies.has(table.table)) {
|
|
2815
|
+
if (fallbackWrite !== void 0) updatePolicies.set(table.table, fallbackWrite);
|
|
2816
|
+
else if (hasSplitPolicy) updatePolicies.set(table.table, CONST_FALSE);
|
|
2817
|
+
else updatePolicies.set(table.table, CONST_TRUE);
|
|
2818
|
+
}
|
|
2819
|
+
const tablePins = collectPinnedColumns(insertPolicies.get(table.table));
|
|
2033
2820
|
pins.set(table.table, tablePins);
|
|
2034
2821
|
const pinned = new Set(tablePins.map((pin) => pin.column));
|
|
2035
2822
|
const meta = tables[table.table];
|
|
2036
2823
|
meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
|
|
2037
|
-
meta.settable = meta.settable.filter((column) => !pinned.has(column));
|
|
2038
2824
|
}
|
|
2039
2825
|
if (platformMeta) {
|
|
2040
2826
|
readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
|
|
2041
2827
|
writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2828
|
+
insertPolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2829
|
+
updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
2042
2830
|
pins.set(PLATFORM_USER_TABLE, []);
|
|
2043
2831
|
}
|
|
2044
2832
|
const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
|
|
2045
2833
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
2046
2834
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
2047
|
-
for (const table of
|
|
2835
|
+
for (const table of relations) {
|
|
2048
2836
|
const roots = queryRootNames(table.table);
|
|
2049
2837
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
2050
|
-
|
|
2838
|
+
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
2839
|
+
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
2840
|
+
}
|
|
2051
2841
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
2052
|
-
|
|
2842
|
+
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2843
|
+
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
2053
2844
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
2054
2845
|
mutationRoots.set(field, { op, table: table.table });
|
|
2055
2846
|
}
|
|
2056
2847
|
}
|
|
2057
2848
|
}
|
|
2849
|
+
for (const table of relations) {
|
|
2850
|
+
for (const mutation of table.cowMutations ?? []) {
|
|
2851
|
+
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
2852
|
+
if (!hasWritePolicy) {
|
|
2853
|
+
diagnostics.push({
|
|
2854
|
+
severity: "error",
|
|
2855
|
+
code: "cow-mutation-unpoliced",
|
|
2856
|
+
message: `cow mutation '${mutation.name}' on '${table.table}' requires a write (or insert/update) policy on the overlay table`,
|
|
2857
|
+
table: table.table
|
|
2858
|
+
});
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
const field = camelCase(mutation.name);
|
|
2862
|
+
if (mutationRoots.has(field) || queryRoots.has(field)) {
|
|
2863
|
+
diagnostics.push({
|
|
2864
|
+
severity: "error",
|
|
2865
|
+
code: "cow-mutation-name-collision",
|
|
2866
|
+
message: `cow mutation '${mutation.name}' on '${table.table}' collides with the generated root '${field}'`,
|
|
2867
|
+
table: table.table
|
|
2868
|
+
});
|
|
2869
|
+
continue;
|
|
2870
|
+
}
|
|
2871
|
+
if (mutation.kind === "override") {
|
|
2872
|
+
const pinned = new Set((pins.get(table.table) ?? []).map((pin) => pin.column));
|
|
2873
|
+
const covered = /* @__PURE__ */ new Set([mutation.pk, ...mutation.copy, ...pinned]);
|
|
2874
|
+
const gaps = table.columns.filter((column) => {
|
|
2875
|
+
if (covered.has(column.name)) return false;
|
|
2876
|
+
const hasDefault = column.default !== void 0 || column.defaultFn !== void 0;
|
|
2877
|
+
return column.nullable === false && !hasDefault;
|
|
2878
|
+
});
|
|
2879
|
+
if (gaps.length > 0) {
|
|
2880
|
+
diagnostics.push({
|
|
2881
|
+
severity: "error",
|
|
2882
|
+
code: "cow-copy-uncovered-column",
|
|
2883
|
+
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`,
|
|
2884
|
+
table: table.table
|
|
2885
|
+
});
|
|
2886
|
+
continue;
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
mutationRoots.set(field, {
|
|
2890
|
+
op: mutation.kind === "override" ? "cowOverride" : "cowRevert",
|
|
2891
|
+
table: table.table,
|
|
2892
|
+
synced: mutation.synced,
|
|
2893
|
+
pk: mutation.pk,
|
|
2894
|
+
set: mutation.set,
|
|
2895
|
+
copyColumns: mutation.copy
|
|
2896
|
+
});
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2058
2899
|
const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
|
|
2059
2900
|
return {
|
|
2060
2901
|
ctx: {
|
|
@@ -2066,7 +2907,10 @@ function buildCompileContext(rawCanonical) {
|
|
|
2066
2907
|
enumsByTypeName,
|
|
2067
2908
|
readPolicies,
|
|
2068
2909
|
writePolicies,
|
|
2069
|
-
|
|
2910
|
+
insertPolicies,
|
|
2911
|
+
updatePolicies,
|
|
2912
|
+
pins,
|
|
2913
|
+
splitWritePolicyTables
|
|
2070
2914
|
},
|
|
2071
2915
|
diagnostics
|
|
2072
2916
|
};
|
|
@@ -2618,6 +3462,9 @@ var OperationLowerer = class {
|
|
|
2618
3462
|
const alias = node.alias?.value ?? name;
|
|
2619
3463
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2620
3464
|
if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
3465
|
+
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
3466
|
+
return this.lowerCowRoot(node, rootInfo);
|
|
3467
|
+
}
|
|
2621
3468
|
const { op, table } = rootInfo;
|
|
2622
3469
|
const args = argsByName(node);
|
|
2623
3470
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
@@ -2687,6 +3534,55 @@ var OperationLowerer = class {
|
|
|
2687
3534
|
}
|
|
2688
3535
|
}
|
|
2689
3536
|
}
|
|
3537
|
+
/**
|
|
3538
|
+
* Copy-on-write roots: flat args — the pk plus (override only) every
|
|
3539
|
+
* declared set column. Overrides are full snapshots of the set columns, so
|
|
3540
|
+
* every set argument must be supplied explicitly (pass null to clear a
|
|
3541
|
+
* nullable column); the executor generates the copy/update/delete SQL from
|
|
3542
|
+
* the structured fields, never from authored statements.
|
|
3543
|
+
*/
|
|
3544
|
+
lowerCowRoot(node, rootInfo) {
|
|
3545
|
+
const name = node.name.value;
|
|
3546
|
+
const alias = node.alias?.value ?? name;
|
|
3547
|
+
const { table } = rootInfo;
|
|
3548
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
3549
|
+
const meta = this.meta(table);
|
|
3550
|
+
const args = argsByName(node);
|
|
3551
|
+
const pkArg = args.get(rootInfo.pk);
|
|
3552
|
+
if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
3553
|
+
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
3554
|
+
const base = {
|
|
3555
|
+
kind: "mutation",
|
|
3556
|
+
op: rootInfo.op,
|
|
3557
|
+
alias,
|
|
3558
|
+
table,
|
|
3559
|
+
pinnedColumns: [],
|
|
3560
|
+
affectedRows: false,
|
|
3561
|
+
pk,
|
|
3562
|
+
cowSynced: rootInfo.synced,
|
|
3563
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
3564
|
+
};
|
|
3565
|
+
if (rootInfo.op === "cowRevert") {
|
|
3566
|
+
assertKnownArgs(node, [rootInfo.pk]);
|
|
3567
|
+
return base;
|
|
3568
|
+
}
|
|
3569
|
+
assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
|
|
3570
|
+
const entries = rootInfo.set.map((column) => {
|
|
3571
|
+
const arg = args.get(column);
|
|
3572
|
+
if (!arg) {
|
|
3573
|
+
fail(
|
|
3574
|
+
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
3575
|
+
"missing-argument",
|
|
3576
|
+
[...rootInfo.set]
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
if (!meta.settable.includes(column)) {
|
|
3580
|
+
fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
3581
|
+
}
|
|
3582
|
+
return { column, value: this.lowerValue(arg.value) };
|
|
3583
|
+
});
|
|
3584
|
+
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
3585
|
+
}
|
|
2690
3586
|
lowerMutationResponse(selectionSet, table) {
|
|
2691
3587
|
let affectedRows = false;
|
|
2692
3588
|
let returning = null;
|
|
@@ -2780,8 +3676,8 @@ var OperationLowerer = class {
|
|
|
2780
3676
|
if (inc.value.kind === graphql.Kind.VARIABLE) {
|
|
2781
3677
|
fail("_inc has no dynamic input type \u2014 inline the columns with variable leaf values", "invalid-inc");
|
|
2782
3678
|
}
|
|
2783
|
-
const
|
|
2784
|
-
result.inc = { entries: this.lowerColumnEntries(inc.value, table,
|
|
3679
|
+
const numeric4 = meta.columns.filter((c) => typeof c.type === "string" && NUMERIC_TYPES.has(c.type) && meta.settable.includes(c.name)).map((c) => c.name);
|
|
3680
|
+
result.inc = { entries: this.lowerColumnEntries(inc.value, table, numeric4, "_inc") };
|
|
2785
3681
|
}
|
|
2786
3682
|
if (!result.set && !result.append && !result.inc) {
|
|
2787
3683
|
fail(`an update on '${table}' needs at least one of _set/_append/_inc`, "missing-argument");
|
|
@@ -3132,13 +4028,17 @@ function generateNamespaceTypes(definition) {
|
|
|
3132
4028
|
`export type ${typesInterfaceName(definition.namespace)} = {`
|
|
3133
4029
|
];
|
|
3134
4030
|
for (const table of definition.tables) {
|
|
3135
|
-
|
|
4031
|
+
const columns = Object.entries(table.columns ?? {});
|
|
4032
|
+
if (table.source && columns.length === 0) {
|
|
3136
4033
|
lines.push(` /** Synced from ${table.source} \u2014 columns reflect the gold source. */`);
|
|
3137
4034
|
lines.push(` ${table.table}: Record<string, unknown>`);
|
|
3138
4035
|
continue;
|
|
3139
4036
|
}
|
|
4037
|
+
if (table.source) {
|
|
4038
|
+
lines.push(` /** Synced from ${table.source} \u2014 read-only typed contract. */`);
|
|
4039
|
+
}
|
|
3140
4040
|
lines.push(` ${table.table}: {`);
|
|
3141
|
-
for (const [name, column] of
|
|
4041
|
+
for (const [name, column] of columns) {
|
|
3142
4042
|
if (column.deprecated) continue;
|
|
3143
4043
|
lines.push(` ${name}: ${columnWireType(column)}`);
|
|
3144
4044
|
}
|
|
@@ -3604,8 +4504,8 @@ var actionSchema = zod.z.strictObject({
|
|
|
3604
4504
|
}
|
|
3605
4505
|
}
|
|
3606
4506
|
});
|
|
3607
|
-
function extractNamedParams(
|
|
3608
|
-
const withoutStrings =
|
|
4507
|
+
function extractNamedParams(sql4) {
|
|
4508
|
+
const withoutStrings = sql4.replace(QUOTED_STRING_PATTERN, "''");
|
|
3609
4509
|
const names = [...withoutStrings.matchAll(NAMED_PARAM_PATTERN)].map((match) => match[1]);
|
|
3610
4510
|
return [...new Set(names)];
|
|
3611
4511
|
}
|
|
@@ -3659,7 +4559,9 @@ var indexSchema = zod.z.union([
|
|
|
3659
4559
|
]).transform((index3) => Array.isArray(index3) ? { columns: index3 } : index3);
|
|
3660
4560
|
var policiesSchema = zod.z.strictObject({
|
|
3661
4561
|
read: parsedPredicateSchema.optional(),
|
|
3662
|
-
write: parsedPredicateSchema.optional()
|
|
4562
|
+
write: parsedPredicateSchema.optional(),
|
|
4563
|
+
insert: parsedPredicateSchema.optional(),
|
|
4564
|
+
update: parsedPredicateSchema.optional()
|
|
3663
4565
|
});
|
|
3664
4566
|
var tableFileSchema = zod.z.strictObject({
|
|
3665
4567
|
namespace: namespaceSchema,
|
|
@@ -3695,7 +4597,6 @@ var policyName = (table, suffix) => `${table}_${suffix}`;
|
|
|
3695
4597
|
var foreignKeyName = (table, link) => `fk_${table}_${link}`;
|
|
3696
4598
|
function validateSyncedTable(table, ctx) {
|
|
3697
4599
|
const forbidden = [
|
|
3698
|
-
["columns", "synced tables reflect their source; columns are not declared"],
|
|
3699
4600
|
["actions", "synced tables are read-only; actions are not allowed"],
|
|
3700
4601
|
["indexes", "synced tables are system-owned; indexes are not allowed"]
|
|
3701
4602
|
];
|
|
@@ -3705,6 +4606,39 @@ function validateSyncedTable(table, ctx) {
|
|
|
3705
4606
|
if (table.policies?.write) {
|
|
3706
4607
|
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "write"] });
|
|
3707
4608
|
}
|
|
4609
|
+
if (table.policies?.insert) {
|
|
4610
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "insert"] });
|
|
4611
|
+
}
|
|
4612
|
+
if (table.policies?.update) {
|
|
4613
|
+
ctx.addIssue({ code: "custom", message: "synced tables are read-only", path: ["policies", "update"] });
|
|
4614
|
+
}
|
|
4615
|
+
const columns = table.columns ?? {};
|
|
4616
|
+
const names = Object.keys(columns);
|
|
4617
|
+
if (names.length === 0) return;
|
|
4618
|
+
const primaryKeys = names.filter((name) => columns[name]?.primary_key);
|
|
4619
|
+
if (primaryKeys.length !== 1) {
|
|
4620
|
+
ctx.addIssue({ code: "custom", message: "exactly one column must be primary_key", path: ["columns"] });
|
|
4621
|
+
}
|
|
4622
|
+
validateRenames(columns, names, ctx);
|
|
4623
|
+
for (const name of names) {
|
|
4624
|
+
const column = columns[name];
|
|
4625
|
+
if (!column) continue;
|
|
4626
|
+
const cast = column.cast ? castParts(column.cast) : null;
|
|
4627
|
+
if (column.cast && cast?.column !== name) {
|
|
4628
|
+
ctx.addIssue({
|
|
4629
|
+
code: "custom",
|
|
4630
|
+
message: "cast must reference the column it is declared on",
|
|
4631
|
+
path: ["columns", name, "cast"]
|
|
4632
|
+
});
|
|
4633
|
+
}
|
|
4634
|
+
if (cast && cast.type !== column.type) {
|
|
4635
|
+
ctx.addIssue({
|
|
4636
|
+
code: "custom",
|
|
4637
|
+
message: `cast target '${cast.type}' must match the declared type '${column.type}'`,
|
|
4638
|
+
path: ["columns", name, "cast"]
|
|
4639
|
+
});
|
|
4640
|
+
}
|
|
4641
|
+
}
|
|
3708
4642
|
}
|
|
3709
4643
|
function validateRenames(columns, names, ctx) {
|
|
3710
4644
|
const renameSources = /* @__PURE__ */ new Map();
|
|
@@ -3782,7 +4716,7 @@ function validateColumnTable(table, ctx) {
|
|
|
3782
4716
|
function validateLinks(table, ctx) {
|
|
3783
4717
|
const columnNames = Object.keys(table.columns ?? {});
|
|
3784
4718
|
for (const [name, link] of Object.entries(table.links ?? {})) {
|
|
3785
|
-
if (
|
|
4719
|
+
if (columnNames.length > 0 && !columnNames.includes(link.on)) {
|
|
3786
4720
|
ctx.addIssue({ code: "custom", message: `link references unknown column '${link.on}'`, path: ["links", name, "on"] });
|
|
3787
4721
|
}
|
|
3788
4722
|
if (link.enforce && link.to.includes(".")) {
|
|
@@ -3855,10 +4789,10 @@ var TYPE_GROUPS = {
|
|
|
3855
4789
|
};
|
|
3856
4790
|
var columnTypeGroup = (type) => TYPE_GROUPS[type];
|
|
3857
4791
|
function validatePolicyColumns(table, ctx) {
|
|
3858
|
-
if (table.source) return;
|
|
3859
4792
|
const columns = table.columns ?? {};
|
|
3860
4793
|
const columnNames = Object.keys(columns);
|
|
3861
|
-
|
|
4794
|
+
if (columnNames.length === 0) return;
|
|
4795
|
+
for (const kind of ["read", "write", "insert", "update"]) {
|
|
3862
4796
|
const predicate = table.policies?.[kind];
|
|
3863
4797
|
if (!predicate) continue;
|
|
3864
4798
|
for (const column of referencedColumns(predicate.ast)) {
|
|
@@ -4013,10 +4947,15 @@ function policyStatements(schema, table) {
|
|
|
4013
4947
|
if (read) {
|
|
4014
4948
|
statements.push(`CREATE POLICY ${policyName2("read")} ON ${qualified} FOR SELECT USING (${compilePredicate(read.ast, columnTypes)})`);
|
|
4015
4949
|
}
|
|
4016
|
-
const
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
statements.push(
|
|
4950
|
+
const insertPolicy = table.policies?.insert ?? table.policies?.write;
|
|
4951
|
+
const updatePolicy = table.policies?.update ?? table.policies?.write;
|
|
4952
|
+
if (insertPolicy) {
|
|
4953
|
+
statements.push(
|
|
4954
|
+
`CREATE POLICY ${policyName2("insert")} ON ${qualified} FOR INSERT WITH CHECK (${compilePredicate(insertPolicy.ast, columnTypes)})`
|
|
4955
|
+
);
|
|
4956
|
+
}
|
|
4957
|
+
if (updatePolicy) {
|
|
4958
|
+
const predicate = compilePredicate(updatePolicy.ast, columnTypes);
|
|
4020
4959
|
statements.push(
|
|
4021
4960
|
`CREATE POLICY ${policyName2("update")} ON ${qualified} FOR UPDATE USING (${predicate}) WITH CHECK (${predicate})`
|
|
4022
4961
|
);
|
|
@@ -4240,11 +5179,268 @@ function buildDrizzleEntitiesV2(definition) {
|
|
|
4240
5179
|
imports[table.table] = drizzleTable;
|
|
4241
5180
|
tables[table.table] = drizzleTable;
|
|
4242
5181
|
}
|
|
5182
|
+
for (const view of definition.views ?? []) {
|
|
5183
|
+
const columns = Object.fromEntries(
|
|
5184
|
+
view.columns.map((column) => [column.columnName, buildViewColumnV2({ view: view.view, column, enums })])
|
|
5185
|
+
);
|
|
5186
|
+
imports[view.view] = schema.view(view.view, columns).as(
|
|
5187
|
+
drizzleOrm.sql.raw(
|
|
5188
|
+
rewriteSyncedSourceRelationsInViewQuery({
|
|
5189
|
+
query: view.query,
|
|
5190
|
+
canonical: definition,
|
|
5191
|
+
schemaName,
|
|
5192
|
+
dependencies: view.dependencies
|
|
5193
|
+
})
|
|
5194
|
+
)
|
|
5195
|
+
);
|
|
5196
|
+
}
|
|
4243
5197
|
return { imports, tables, schemaName };
|
|
4244
5198
|
}
|
|
5199
|
+
function rewriteSyncedSourceRelationsInViewQuery({
|
|
5200
|
+
query,
|
|
5201
|
+
canonical,
|
|
5202
|
+
schemaName,
|
|
5203
|
+
dependencies
|
|
5204
|
+
}) {
|
|
5205
|
+
const tablesBySource = /* @__PURE__ */ new Map();
|
|
5206
|
+
for (const table of canonical.tables) {
|
|
5207
|
+
if (!table.source) continue;
|
|
5208
|
+
tablesBySource.set(table.source, [...tablesBySource.get(table.source) ?? [], table.table]);
|
|
5209
|
+
}
|
|
5210
|
+
const declared = new Set(dependencies ?? []);
|
|
5211
|
+
const boundTableBySource = /* @__PURE__ */ new Map();
|
|
5212
|
+
for (const [source, tables] of tablesBySource) {
|
|
5213
|
+
if (tables.length === 1) {
|
|
5214
|
+
boundTableBySource.set(source, tables[0]);
|
|
5215
|
+
continue;
|
|
5216
|
+
}
|
|
5217
|
+
const [sourceSchema, sourceName] = source.split(".");
|
|
5218
|
+
if (!sourceSchema || !sourceName) continue;
|
|
5219
|
+
const referenced = sqlReferencesQualifiedRelation(query, sourceSchema, sourceName);
|
|
5220
|
+
if (!referenced) continue;
|
|
5221
|
+
const declaredMatches = tables.filter((table) => declared.has(table));
|
|
5222
|
+
if (declaredMatches.length === 1) {
|
|
5223
|
+
boundTableBySource.set(source, declaredMatches[0]);
|
|
5224
|
+
continue;
|
|
5225
|
+
}
|
|
5226
|
+
throw new OrmDefinitionError(
|
|
5227
|
+
`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`
|
|
5228
|
+
);
|
|
5229
|
+
}
|
|
5230
|
+
return transformSqlOutsideLiterals(query, (segment) => {
|
|
5231
|
+
let rewritten = segment;
|
|
5232
|
+
for (const table of canonical.tables) {
|
|
5233
|
+
if (!table.source) continue;
|
|
5234
|
+
const boundTable = boundTableBySource.get(table.source);
|
|
5235
|
+
if (boundTable === void 0 || boundTable !== table.table) continue;
|
|
5236
|
+
const [sourceSchema, sourceName] = table.source.split(".");
|
|
5237
|
+
if (!sourceSchema || !sourceName) continue;
|
|
5238
|
+
rewritten = replaceQualifiedRelation(
|
|
5239
|
+
rewritten,
|
|
5240
|
+
sourceSchema,
|
|
5241
|
+
sourceName,
|
|
5242
|
+
schemaName,
|
|
5243
|
+
table.table
|
|
5244
|
+
);
|
|
5245
|
+
}
|
|
5246
|
+
return rewritten;
|
|
5247
|
+
});
|
|
5248
|
+
}
|
|
5249
|
+
function transformSqlOutsideLiterals(sql4, transform) {
|
|
5250
|
+
const out = [];
|
|
5251
|
+
let state = { kind: "normal" };
|
|
5252
|
+
let i = 0;
|
|
5253
|
+
let codeStart = 0;
|
|
5254
|
+
const emitCode = (end) => {
|
|
5255
|
+
if (end > codeStart) out.push(transform(sql4.slice(codeStart, end)));
|
|
5256
|
+
codeStart = end;
|
|
5257
|
+
};
|
|
5258
|
+
while (i < sql4.length) {
|
|
5259
|
+
const c = sql4[i];
|
|
5260
|
+
const next = i + 1 < sql4.length ? sql4[i + 1] : "";
|
|
5261
|
+
switch (state.kind) {
|
|
5262
|
+
case "normal": {
|
|
5263
|
+
if (c === "-" && next === "-") {
|
|
5264
|
+
emitCode(i);
|
|
5265
|
+
state = { kind: "line-comment" };
|
|
5266
|
+
out.push("--");
|
|
5267
|
+
i += 2;
|
|
5268
|
+
codeStart = i;
|
|
5269
|
+
break;
|
|
5270
|
+
}
|
|
5271
|
+
if (c === "/" && next === "*") {
|
|
5272
|
+
emitCode(i);
|
|
5273
|
+
state = { kind: "block-comment", depth: 1 };
|
|
5274
|
+
out.push("/*");
|
|
5275
|
+
i += 2;
|
|
5276
|
+
codeStart = i;
|
|
5277
|
+
break;
|
|
5278
|
+
}
|
|
5279
|
+
if ((c === "E" || c === "e") && next === "'") {
|
|
5280
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
5281
|
+
if (!/[A-Za-z0-9_]/.test(prev)) {
|
|
5282
|
+
emitCode(i);
|
|
5283
|
+
state = { kind: "e-single" };
|
|
5284
|
+
out.push("E'");
|
|
5285
|
+
i += 2;
|
|
5286
|
+
codeStart = i;
|
|
5287
|
+
break;
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
if (c === "'") {
|
|
5291
|
+
emitCode(i);
|
|
5292
|
+
state = { kind: "single" };
|
|
5293
|
+
out.push("'");
|
|
5294
|
+
i += 1;
|
|
5295
|
+
codeStart = i;
|
|
5296
|
+
break;
|
|
5297
|
+
}
|
|
5298
|
+
if (c === "$") {
|
|
5299
|
+
const prev = i > 0 ? sql4[i - 1] : "";
|
|
5300
|
+
if (!/[A-Za-z0-9_$]/.test(prev)) {
|
|
5301
|
+
const tagMatch = /^\$([A-Za-z_][A-Za-z0-9_]*)?\$/.exec(sql4.slice(i));
|
|
5302
|
+
if (tagMatch) {
|
|
5303
|
+
emitCode(i);
|
|
5304
|
+
state = { kind: "dollar", tag: tagMatch[1] ?? "" };
|
|
5305
|
+
out.push(tagMatch[0]);
|
|
5306
|
+
i += tagMatch[0].length;
|
|
5307
|
+
codeStart = i;
|
|
5308
|
+
break;
|
|
5309
|
+
}
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
i += 1;
|
|
5313
|
+
break;
|
|
5314
|
+
}
|
|
5315
|
+
case "single": {
|
|
5316
|
+
if (c === "'" && next === "'") {
|
|
5317
|
+
out.push("''");
|
|
5318
|
+
i += 2;
|
|
5319
|
+
break;
|
|
5320
|
+
}
|
|
5321
|
+
if (c === "'") {
|
|
5322
|
+
out.push("'");
|
|
5323
|
+
state = { kind: "normal" };
|
|
5324
|
+
i += 1;
|
|
5325
|
+
codeStart = i;
|
|
5326
|
+
break;
|
|
5327
|
+
}
|
|
5328
|
+
out.push(c);
|
|
5329
|
+
i += 1;
|
|
5330
|
+
break;
|
|
5331
|
+
}
|
|
5332
|
+
case "e-single": {
|
|
5333
|
+
if (c === "\\" && i + 1 < sql4.length) {
|
|
5334
|
+
out.push(c, sql4[i + 1]);
|
|
5335
|
+
i += 2;
|
|
5336
|
+
break;
|
|
5337
|
+
}
|
|
5338
|
+
if (c === "'" && next === "'") {
|
|
5339
|
+
out.push("''");
|
|
5340
|
+
i += 2;
|
|
5341
|
+
break;
|
|
5342
|
+
}
|
|
5343
|
+
if (c === "'") {
|
|
5344
|
+
out.push("'");
|
|
5345
|
+
state = { kind: "normal" };
|
|
5346
|
+
i += 1;
|
|
5347
|
+
codeStart = i;
|
|
5348
|
+
break;
|
|
5349
|
+
}
|
|
5350
|
+
out.push(c);
|
|
5351
|
+
i += 1;
|
|
5352
|
+
break;
|
|
5353
|
+
}
|
|
5354
|
+
case "dollar": {
|
|
5355
|
+
const closer = `$${state.tag}$`;
|
|
5356
|
+
if (sql4.startsWith(closer, i)) {
|
|
5357
|
+
out.push(closer);
|
|
5358
|
+
state = { kind: "normal" };
|
|
5359
|
+
i += closer.length;
|
|
5360
|
+
codeStart = i;
|
|
5361
|
+
break;
|
|
5362
|
+
}
|
|
5363
|
+
out.push(c);
|
|
5364
|
+
i += 1;
|
|
5365
|
+
break;
|
|
5366
|
+
}
|
|
5367
|
+
case "line-comment": {
|
|
5368
|
+
if (c === "\n") {
|
|
5369
|
+
out.push("\n");
|
|
5370
|
+
state = { kind: "normal" };
|
|
5371
|
+
i += 1;
|
|
5372
|
+
codeStart = i;
|
|
5373
|
+
break;
|
|
5374
|
+
}
|
|
5375
|
+
out.push(c);
|
|
5376
|
+
i += 1;
|
|
5377
|
+
break;
|
|
5378
|
+
}
|
|
5379
|
+
case "block-comment": {
|
|
5380
|
+
if (c === "/" && next === "*") {
|
|
5381
|
+
out.push("/*");
|
|
5382
|
+
state = { kind: "block-comment", depth: state.depth + 1 };
|
|
5383
|
+
i += 2;
|
|
5384
|
+
break;
|
|
5385
|
+
}
|
|
5386
|
+
if (c === "*" && next === "/") {
|
|
5387
|
+
out.push("*/");
|
|
5388
|
+
const newDepth = state.depth - 1;
|
|
5389
|
+
state = newDepth === 0 ? { kind: "normal" } : { kind: "block-comment", depth: newDepth };
|
|
5390
|
+
i += 2;
|
|
5391
|
+
if (state.kind === "normal") codeStart = i;
|
|
5392
|
+
break;
|
|
5393
|
+
}
|
|
5394
|
+
out.push(c);
|
|
5395
|
+
i += 1;
|
|
5396
|
+
break;
|
|
5397
|
+
}
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
emitCode(sql4.length);
|
|
5401
|
+
return out.join("");
|
|
5402
|
+
}
|
|
5403
|
+
function escapeRegExp(value) {
|
|
5404
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5405
|
+
}
|
|
5406
|
+
function sqlReferencesQualifiedRelation(sql4, sourceSchema, sourceName) {
|
|
5407
|
+
let referenced = false;
|
|
5408
|
+
transformSqlOutsideLiterals(sql4, (segment) => {
|
|
5409
|
+
if (qualifiedRelationPattern(sourceSchema, sourceName).test(segment)) referenced = true;
|
|
5410
|
+
return segment;
|
|
5411
|
+
});
|
|
5412
|
+
return referenced;
|
|
5413
|
+
}
|
|
5414
|
+
function qualifiedRelationPattern(sourceSchema, sourceName) {
|
|
5415
|
+
const schemaPattern = `(?:"${escapeRegExp(sourceSchema)}"|${escapeRegExp(sourceSchema)})`;
|
|
5416
|
+
const namePattern = `(?:"${escapeRegExp(sourceName)}"|${escapeRegExp(sourceName)})`;
|
|
5417
|
+
return new RegExp(`(?<![\\w."])${schemaPattern}\\.${namePattern}(?![\\w"])`, "g");
|
|
5418
|
+
}
|
|
5419
|
+
function replaceQualifiedRelation(sql4, sourceSchema, sourceName, boundSchema, boundTable) {
|
|
5420
|
+
return sql4.replace(qualifiedRelationPattern(sourceSchema, sourceName), `"${boundSchema}"."${boundTable}"`);
|
|
5421
|
+
}
|
|
4245
5422
|
function generateSnapshotV2(definition, previousId3) {
|
|
4246
5423
|
const { imports, schemaName } = buildDrizzleEntitiesV2(definition);
|
|
4247
|
-
|
|
5424
|
+
const snapshot = api.generateDrizzleJson(imports, previousId3, [schemaName]);
|
|
5425
|
+
if ((definition.views?.length ?? 0) > 0) {
|
|
5426
|
+
const extended = snapshot;
|
|
5427
|
+
extended._sequenceOrm = {
|
|
5428
|
+
viewDependencies: Object.fromEntries(
|
|
5429
|
+
(definition.views ?? []).map((view) => [view.view, [...view.dependencies].sort()])
|
|
5430
|
+
)
|
|
5431
|
+
};
|
|
5432
|
+
}
|
|
5433
|
+
return snapshot;
|
|
5434
|
+
}
|
|
5435
|
+
function buildViewColumnV2({
|
|
5436
|
+
view,
|
|
5437
|
+
column,
|
|
5438
|
+
enums
|
|
5439
|
+
}) {
|
|
5440
|
+
const factory = resolveColumnFactory(view, column, enums);
|
|
5441
|
+
let builder = factory(column.columnName);
|
|
5442
|
+
if (!column.nullable) builder = builder.notNull();
|
|
5443
|
+
return builder;
|
|
4248
5444
|
}
|
|
4249
5445
|
function buildTable2(schema, table, built, enums, byName) {
|
|
4250
5446
|
const primaryKeyColumns2 = table.primaryKey ?? [];
|
|
@@ -4488,16 +5684,270 @@ async function planMigrationV2({
|
|
|
4488
5684
|
}) {
|
|
4489
5685
|
const schema = ormSchemaName(canonical.namespace);
|
|
4490
5686
|
const previous = previousSnapshot ? normalizeSnapshot(previousSnapshot) : emptySnapshot(canonical.namespace);
|
|
5687
|
+
assertValidViewDependencies(canonical);
|
|
4491
5688
|
const hints = applyMigrationHintsV2(previous, canonical);
|
|
4492
5689
|
const snapshot = generateSnapshotV2(canonical, previousId(previous));
|
|
4493
|
-
const
|
|
5690
|
+
const baseDdl = await diffStatements(withoutViews(hints.adjusted), withoutViews(snapshot));
|
|
5691
|
+
const previousViews = snapshotViews(hints.adjusted);
|
|
5692
|
+
const currentViews = snapshotViews(snapshot);
|
|
5693
|
+
const previousDependencies = snapshotViewDependencies(hints.adjusted);
|
|
5694
|
+
const currentDependencies = snapshotViewDependencies(snapshot);
|
|
5695
|
+
const affected = affectedViewNames({
|
|
5696
|
+
previousViews,
|
|
5697
|
+
currentViews,
|
|
5698
|
+
previousDependencies,
|
|
5699
|
+
currentDependencies,
|
|
5700
|
+
baseChanged: hints.statements.length > 0 || baseDdl.length > 0
|
|
5701
|
+
});
|
|
5702
|
+
const dropOrder = topologicalViewNames(
|
|
5703
|
+
new Set([...affected].filter((name) => previousViews.has(name))),
|
|
5704
|
+
previousDependencies
|
|
5705
|
+
).reverse();
|
|
5706
|
+
const createOrder = topologicalViewNames(
|
|
5707
|
+
new Set([...affected].filter((name) => currentViews.has(name))),
|
|
5708
|
+
currentDependencies
|
|
5709
|
+
);
|
|
5710
|
+
const dropDdl = await generatedViewStatements({
|
|
5711
|
+
snapshot: hints.adjusted,
|
|
5712
|
+
names: new Set(dropOrder),
|
|
5713
|
+
mode: "drop",
|
|
5714
|
+
schema
|
|
5715
|
+
});
|
|
5716
|
+
const createDdl = await generatedViewStatements({
|
|
5717
|
+
snapshot,
|
|
5718
|
+
names: new Set(createOrder),
|
|
5719
|
+
mode: "create",
|
|
5720
|
+
schema
|
|
5721
|
+
});
|
|
5722
|
+
const currentViewDefinitions = new Map((canonical.views ?? []).map((view) => [view.view, view]));
|
|
4494
5723
|
const statements = [
|
|
5724
|
+
...dropOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(dropDdl, name, schema, "DROP") })),
|
|
4495
5725
|
...hints.statements.map((statement) => ({ kind: "hint", sql: statement })),
|
|
4496
|
-
...
|
|
5726
|
+
...baseDdl.map((statement) => ({ kind: "ddl", sql: statement.replace(/;\s*$/, "") })),
|
|
5727
|
+
...createOrder.map((name) => ({ kind: "ddl", sql: requiredViewStatement(createDdl, name, schema, "CREATE") })),
|
|
5728
|
+
...createOrder.map((name) => {
|
|
5729
|
+
const view = currentViewDefinitions.get(name);
|
|
5730
|
+
if (!view) throw new OrmDefinitionError(`missing canonical definition for view '${name}'`);
|
|
5731
|
+
return { kind: "ddl", sql: validateViewCatalogStatement({ canonical, view, schema }) };
|
|
5732
|
+
})
|
|
4497
5733
|
];
|
|
4498
5734
|
return { namespace: canonical.namespace, schema, statements, snapshot };
|
|
4499
5735
|
}
|
|
5736
|
+
function validateViewCatalogStatement({
|
|
5737
|
+
canonical,
|
|
5738
|
+
view,
|
|
5739
|
+
schema
|
|
5740
|
+
}) {
|
|
5741
|
+
const qualified = `${schema}.${view.view}`;
|
|
5742
|
+
const expectedColumns = sqlTextArray(view.columns.map((column) => column.columnName));
|
|
5743
|
+
const expectedTypes = `ARRAY[${view.columns.map((column) => `${viewTypeRegtype(column, schema)}::oid`).join(", ")}]::oid[]`;
|
|
5744
|
+
const relationIdentity = /* @__PURE__ */ new Map();
|
|
5745
|
+
for (const table of canonical.tables) {
|
|
5746
|
+
relationIdentity.set(table.table, `${schema}.${table.table}`);
|
|
5747
|
+
}
|
|
5748
|
+
for (const candidate of canonical.views ?? []) {
|
|
5749
|
+
relationIdentity.set(candidate.view, `${schema}.${candidate.view}`);
|
|
5750
|
+
}
|
|
5751
|
+
const expectedDependencies = sqlTextArray(
|
|
5752
|
+
view.dependencies.map((dependency) => {
|
|
5753
|
+
const physical = relationIdentity.get(dependency);
|
|
5754
|
+
if (!physical) throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5755
|
+
return physical;
|
|
5756
|
+
}).sort()
|
|
5757
|
+
);
|
|
5758
|
+
const allowedFunctions = sqlTextArray(VIEW_QUERY_FUNCTION_ALLOWLIST);
|
|
5759
|
+
return `DO $sequence_orm_validate_view$
|
|
5760
|
+
DECLARE
|
|
5761
|
+
actual_columns text[];
|
|
5762
|
+
actual_types oid[];
|
|
5763
|
+
actual_dependencies text[];
|
|
5764
|
+
BEGIN
|
|
5765
|
+
SELECT
|
|
5766
|
+
array_agg(attribute.attname ORDER BY attribute.attnum),
|
|
5767
|
+
array_agg(attribute.atttypid ORDER BY attribute.attnum)
|
|
5768
|
+
INTO actual_columns, actual_types
|
|
5769
|
+
FROM pg_catalog.pg_attribute AS attribute
|
|
5770
|
+
WHERE attribute.attrelid = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5771
|
+
AND attribute.attnum > 0
|
|
5772
|
+
AND NOT attribute.attisdropped;
|
|
5773
|
+
|
|
5774
|
+
IF actual_columns IS DISTINCT FROM ${expectedColumns} THEN
|
|
5775
|
+
RAISE EXCEPTION 'managed view ${qualified} columns do not match its declaration: expected %, got %', ${expectedColumns}, actual_columns
|
|
5776
|
+
USING ERRCODE = '42804';
|
|
5777
|
+
END IF;
|
|
5778
|
+
IF actual_types IS DISTINCT FROM ${expectedTypes} THEN
|
|
5779
|
+
RAISE EXCEPTION 'managed view ${qualified} types do not match its declaration: expected %, got %', ${expectedTypes}, actual_types
|
|
5780
|
+
USING ERRCODE = '42804';
|
|
5781
|
+
END IF;
|
|
5782
|
+
|
|
5783
|
+
SELECT coalesce(array_agg(found.name ORDER BY found.name), ARRAY[]::text[])
|
|
5784
|
+
INTO actual_dependencies
|
|
5785
|
+
FROM (
|
|
5786
|
+
SELECT DISTINCT dependency_namespace.nspname || '.' || dependency_relation.relname AS name
|
|
5787
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
5788
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
5789
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
5790
|
+
AND dependency.objid = rewrite.oid
|
|
5791
|
+
AND dependency.refclassid = 'pg_catalog.pg_class'::regclass
|
|
5792
|
+
JOIN pg_catalog.pg_class AS dependency_relation ON dependency_relation.oid = dependency.refobjid
|
|
5793
|
+
JOIN pg_catalog.pg_namespace AS dependency_namespace ON dependency_namespace.oid = dependency_relation.relnamespace
|
|
5794
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5795
|
+
AND dependency.refobjid <> rewrite.ev_class
|
|
5796
|
+
AND dependency_relation.relkind IN ('r', 'p', 'v', 'm', 'f')
|
|
5797
|
+
) AS found;
|
|
5798
|
+
|
|
5799
|
+
IF actual_dependencies IS DISTINCT FROM ${expectedDependencies} THEN
|
|
5800
|
+
RAISE EXCEPTION 'managed view ${qualified} dependencies do not match its declaration: expected %, got %', ${expectedDependencies}, actual_dependencies
|
|
5801
|
+
USING ERRCODE = '2BP01';
|
|
5802
|
+
END IF;
|
|
5803
|
+
|
|
5804
|
+
IF EXISTS (
|
|
5805
|
+
SELECT 1
|
|
5806
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
5807
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
5808
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
5809
|
+
AND dependency.objid = rewrite.oid
|
|
5810
|
+
AND dependency.refclassid = 'pg_catalog.pg_proc'::regclass
|
|
5811
|
+
JOIN pg_catalog.pg_proc AS dependency_function ON dependency_function.oid = dependency.refobjid
|
|
5812
|
+
JOIN pg_catalog.pg_namespace AS function_namespace ON function_namespace.oid = dependency_function.pronamespace
|
|
5813
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5814
|
+
AND (
|
|
5815
|
+
function_namespace.nspname <> 'pg_catalog'
|
|
5816
|
+
OR dependency_function.proname <> ALL (${allowedFunctions})
|
|
5817
|
+
)
|
|
5818
|
+
) THEN
|
|
5819
|
+
RAISE EXCEPTION 'managed view ${qualified} may only call explicitly allowlisted side-effect-free pg_catalog functions'
|
|
5820
|
+
USING ERRCODE = '42501';
|
|
5821
|
+
END IF;
|
|
5822
|
+
|
|
5823
|
+
IF EXISTS (
|
|
5824
|
+
SELECT 1
|
|
5825
|
+
FROM pg_catalog.pg_rewrite AS rewrite
|
|
5826
|
+
JOIN pg_catalog.pg_depend AS dependency
|
|
5827
|
+
ON dependency.classid = 'pg_catalog.pg_rewrite'::regclass
|
|
5828
|
+
AND dependency.objid = rewrite.oid
|
|
5829
|
+
AND dependency.refclassid = 'pg_catalog.pg_operator'::regclass
|
|
5830
|
+
JOIN pg_catalog.pg_operator AS dependency_operator ON dependency_operator.oid = dependency.refobjid
|
|
5831
|
+
JOIN pg_catalog.pg_namespace AS operator_namespace ON operator_namespace.oid = dependency_operator.oprnamespace
|
|
5832
|
+
WHERE rewrite.ev_class = pg_catalog.to_regclass(${quoteLiteral(qualified)})
|
|
5833
|
+
AND operator_namespace.nspname <> 'pg_catalog'
|
|
5834
|
+
) THEN
|
|
5835
|
+
RAISE EXCEPTION 'managed view ${qualified} may only use pg_catalog operators'
|
|
5836
|
+
USING ERRCODE = '42501';
|
|
5837
|
+
END IF;
|
|
5838
|
+
END
|
|
5839
|
+
$sequence_orm_validate_view$`;
|
|
5840
|
+
}
|
|
5841
|
+
function sqlTextArray(values) {
|
|
5842
|
+
return values.length === 0 ? "ARRAY[]::text[]" : `ARRAY[${values.map((value) => quoteLiteral(value)).join(", ")}]::text[]`;
|
|
5843
|
+
}
|
|
5844
|
+
function viewTypeRegtype(column, schema) {
|
|
5845
|
+
if (typeof column.type === "string") return `${quoteLiteral(sqlTypeName(column.type))}::regtype`;
|
|
5846
|
+
return `${quoteLiteral(`${quoteIdentifier(schema)}.${quoteIdentifier(column.type.enum)}`)}::regtype`;
|
|
5847
|
+
}
|
|
4500
5848
|
var previousId = (snapshot) => snapshot.id;
|
|
5849
|
+
function snapshotViews(snapshot) {
|
|
5850
|
+
const views = snapshot.views ?? {};
|
|
5851
|
+
return new Map(Object.values(views).map((view) => [view.name, view]));
|
|
5852
|
+
}
|
|
5853
|
+
function snapshotViewDependencies(snapshot) {
|
|
5854
|
+
const metadata = snapshot._sequenceOrm;
|
|
5855
|
+
return metadata?.viewDependencies ?? {};
|
|
5856
|
+
}
|
|
5857
|
+
function withoutViews(snapshot) {
|
|
5858
|
+
const clone = structuredClone(snapshot);
|
|
5859
|
+
clone.views = {};
|
|
5860
|
+
delete clone._sequenceOrm;
|
|
5861
|
+
return clone;
|
|
5862
|
+
}
|
|
5863
|
+
function affectedViewNames({
|
|
5864
|
+
previousViews,
|
|
5865
|
+
currentViews,
|
|
5866
|
+
previousDependencies,
|
|
5867
|
+
currentDependencies,
|
|
5868
|
+
baseChanged
|
|
5869
|
+
}) {
|
|
5870
|
+
const allNames = /* @__PURE__ */ new Set([...previousViews.keys(), ...currentViews.keys()]);
|
|
5871
|
+
if (baseChanged) return allNames;
|
|
5872
|
+
const affected = new Set(
|
|
5873
|
+
[...allNames].filter((name) => {
|
|
5874
|
+
const before = previousViews.get(name);
|
|
5875
|
+
const after = currentViews.get(name);
|
|
5876
|
+
return before === void 0 || after === void 0 || stableJson(before) !== stableJson(after);
|
|
5877
|
+
})
|
|
5878
|
+
);
|
|
5879
|
+
let grew = true;
|
|
5880
|
+
while (grew) {
|
|
5881
|
+
grew = false;
|
|
5882
|
+
for (const name of allNames) {
|
|
5883
|
+
if (affected.has(name)) continue;
|
|
5884
|
+
const dependencies = /* @__PURE__ */ new Set([...previousDependencies[name] ?? [], ...currentDependencies[name] ?? []]);
|
|
5885
|
+
if ([...dependencies].some((dependency) => affected.has(dependency))) {
|
|
5886
|
+
affected.add(name);
|
|
5887
|
+
grew = true;
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
}
|
|
5891
|
+
return affected;
|
|
5892
|
+
}
|
|
5893
|
+
function topologicalViewNames(names, dependencies) {
|
|
5894
|
+
const remaining = new Set(names);
|
|
5895
|
+
const ordered = [];
|
|
5896
|
+
while (remaining.size > 0) {
|
|
5897
|
+
const ready = [...remaining].filter((name) => (dependencies[name] ?? []).every((dependency) => !remaining.has(dependency))).sort();
|
|
5898
|
+
if (ready.length === 0) {
|
|
5899
|
+
throw new OrmDefinitionError(`view dependencies form a cycle among: ${[...remaining].sort().join(", ")}`);
|
|
5900
|
+
}
|
|
5901
|
+
for (const name of ready) {
|
|
5902
|
+
ordered.push(name);
|
|
5903
|
+
remaining.delete(name);
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
return ordered;
|
|
5907
|
+
}
|
|
5908
|
+
async function generatedViewStatements({
|
|
5909
|
+
snapshot,
|
|
5910
|
+
names,
|
|
5911
|
+
mode,
|
|
5912
|
+
schema
|
|
5913
|
+
}) {
|
|
5914
|
+
if (names.size === 0) return /* @__PURE__ */ new Map();
|
|
5915
|
+
const without = structuredClone(snapshot);
|
|
5916
|
+
const holder = without;
|
|
5917
|
+
const views = holder.views ?? {};
|
|
5918
|
+
holder.views = Object.fromEntries(Object.entries(views).filter(([, view]) => !names.has(view.name)));
|
|
5919
|
+
const generated = mode === "drop" ? await diffStatements(snapshot, without) : await diffStatements(without, snapshot);
|
|
5920
|
+
const statements = /* @__PURE__ */ new Map();
|
|
5921
|
+
for (const name of names) {
|
|
5922
|
+
const qualified = `${quoteIdentifier(schema)}.${quoteIdentifier(name)}`;
|
|
5923
|
+
const prefix = mode === "drop" ? "DROP VIEW" : "CREATE VIEW";
|
|
5924
|
+
const statement = generated.find((candidate) => candidate.trimStart().startsWith(`${prefix} ${qualified}`));
|
|
5925
|
+
if (statement) statements.set(name, statement.replace(/;\s*$/, ""));
|
|
5926
|
+
}
|
|
5927
|
+
return statements;
|
|
5928
|
+
}
|
|
5929
|
+
function requiredViewStatement(statements, name, schema, operation) {
|
|
5930
|
+
const statement = statements.get(name);
|
|
5931
|
+
if (!statement) {
|
|
5932
|
+
throw new OrmDefinitionError(`drizzle-kit did not generate ${operation} VIEW for '${schema}.${name}'`);
|
|
5933
|
+
}
|
|
5934
|
+
return statement;
|
|
5935
|
+
}
|
|
5936
|
+
function assertValidViewDependencies(canonical) {
|
|
5937
|
+
const views = canonical.views ?? [];
|
|
5938
|
+
const known = /* @__PURE__ */ new Set([...canonical.tables.map((table) => table.table), ...views.map((view) => view.view)]);
|
|
5939
|
+
for (const view of views) {
|
|
5940
|
+
for (const dependency of view.dependencies) {
|
|
5941
|
+
if (!known.has(dependency)) {
|
|
5942
|
+
throw new OrmDefinitionError(`view '${view.view}' depends on unknown relation '${dependency}'`);
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
5946
|
+
topologicalViewNames(
|
|
5947
|
+
new Set(views.map((view) => view.view)),
|
|
5948
|
+
Object.fromEntries(views.map((view) => [view.view, view.dependencies]))
|
|
5949
|
+
);
|
|
5950
|
+
}
|
|
4501
5951
|
var QUOTED_STRING_PATTERN2 = /'(?:[^']|'')*'/g;
|
|
4502
5952
|
var TOKEN_PATTERN = /\bclaim\(\s*'((?:[^']|'')*)'\s*\)|\bcapability\(\s*'((?:[^']|'')*)'\s*\)|'(?:[^']|'')*'|(?<!:):([a-z][a-z0-9_]*)/g;
|
|
4503
5953
|
var BARE_CALL_PATTERN = /\b(?:claim|capability)\s*\(/;
|
|
@@ -4518,8 +5968,8 @@ var actionParamArg = (name) => `p_${name}`;
|
|
|
4518
5968
|
function compileActionStatements(action, renderParam = actionParamArg) {
|
|
4519
5969
|
return actionStatementSources(action.executes).map((statement) => compileStatement(statement, renderParam));
|
|
4520
5970
|
}
|
|
4521
|
-
function compileStatement(
|
|
4522
|
-
const compiled =
|
|
5971
|
+
function compileStatement(sql4, renderParam) {
|
|
5972
|
+
const compiled = sql4.replace(TOKEN_PATTERN, (token, claimKey, capabilityKey, paramName) => {
|
|
4523
5973
|
if (claimKey !== void 0) {
|
|
4524
5974
|
if (!CLAIM_KEY_PATTERN.test(claimKey)) throw new OrmDefinitionError(`invalid claim key in executes: ${token}`);
|
|
4525
5975
|
return claimExpression(claimKey);
|
|
@@ -4572,8 +6022,8 @@ function actionPrepareChecks(definition) {
|
|
|
4572
6022
|
const order = actionParamOrder(action);
|
|
4573
6023
|
const signature = order.length > 0 ? ` (${order.map((param) => sqlTypeName(action.params[param].type)).join(", ")})` : "";
|
|
4574
6024
|
const compiled = compileActionStatements(action, (param) => `$${order.indexOf(param) + 1}`);
|
|
4575
|
-
compiled.forEach((
|
|
4576
|
-
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns,
|
|
6025
|
+
compiled.forEach((sql4, index3) => {
|
|
6026
|
+
const checked = action.returns && index3 === compiled.length - 1 ? returnProjection(action.returns, sql4) : sql4;
|
|
4577
6027
|
statements.push(`PREPARE ${quoteIdentifier(`orm_check_${name}_${index3}`)}${signature} AS ${checked}`);
|
|
4578
6028
|
});
|
|
4579
6029
|
}
|
|
@@ -4689,15 +6139,15 @@ async function planMigration({
|
|
|
4689
6139
|
const ddl = await diffStatements(hints.adjusted, snapshot);
|
|
4690
6140
|
const policyDrops = policyDropStatements(schema, snapshotTableNames(previous, schema));
|
|
4691
6141
|
const statements = [
|
|
4692
|
-
...policyDrops.map((
|
|
4693
|
-
...hints.statements.map((
|
|
4694
|
-
...ddl.map((
|
|
4695
|
-
...securityStatements(definition).map((
|
|
6142
|
+
...policyDrops.map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
6143
|
+
...hints.statements.map((sql4) => ({ kind: "hint", sql: sql4 })),
|
|
6144
|
+
...ddl.map((sql4) => ({ kind: "ddl", sql: sql4.replace(/;\s*$/, "") })),
|
|
6145
|
+
...securityStatements(definition).map((sql4) => ({ kind: "security", sql: sql4 })),
|
|
4696
6146
|
// PREPARE each action's executes first (plpgsql bodies don't check columns
|
|
4697
6147
|
// at CREATE), then create the functions. Both reference the tables the DDL
|
|
4698
6148
|
// above creates, so they close the plan.
|
|
4699
|
-
...actionPrepareChecks(definition).map((
|
|
4700
|
-
...actionFunctionStatements(definition).map((
|
|
6149
|
+
...actionPrepareChecks(definition).map((sql4) => ({ kind: "function", sql: sql4 })),
|
|
6150
|
+
...actionFunctionStatements(definition).map((sql4) => ({ kind: "function", sql: sql4 }))
|
|
4701
6151
|
];
|
|
4702
6152
|
return { namespace: definition.namespace, schema, statements, snapshot };
|
|
4703
6153
|
}
|
|
@@ -4826,6 +6276,8 @@ function validateCapabilityRefs(files, declared) {
|
|
|
4826
6276
|
const referenced = [
|
|
4827
6277
|
...table.policies?.read ? referencedCapabilities(table.policies.read.ast) : [],
|
|
4828
6278
|
...table.policies?.write ? referencedCapabilities(table.policies.write.ast) : [],
|
|
6279
|
+
...table.policies?.insert ? referencedCapabilities(table.policies.insert.ast) : [],
|
|
6280
|
+
...table.policies?.update ? referencedCapabilities(table.policies.update.ast) : [],
|
|
4829
6281
|
...Object.values(table.actions ?? {}).flatMap((action) => [
|
|
4830
6282
|
...action.requires ? referencedCapabilities(action.requires.ast) : [],
|
|
4831
6283
|
...referencedExecuteCapabilities(action)
|
|
@@ -4983,7 +6435,7 @@ async function readMigrationSql(dir, entry) {
|
|
|
4983
6435
|
throw new OrmDefinitionError("missing migration file", path$1);
|
|
4984
6436
|
});
|
|
4985
6437
|
}
|
|
4986
|
-
var checksumOf = (
|
|
6438
|
+
var checksumOf = (text4) => crypto.createHash("sha256").update(text4).digest("hex");
|
|
4987
6439
|
var migrationName = (ordinal, contentHash) => `${pad(ordinal)}_${contentHash.slice(0, 12)}`;
|
|
4988
6440
|
function renderMigrationSql({
|
|
4989
6441
|
name,
|
|
@@ -5009,12 +6461,12 @@ async function writeMigration({
|
|
|
5009
6461
|
const tip = workspace.journal.entries.at(-1);
|
|
5010
6462
|
const ordinal = (tip?.ordinal ?? 0) + 1;
|
|
5011
6463
|
const name = migrationName(ordinal, contentHash);
|
|
5012
|
-
const
|
|
6464
|
+
const sql4 = renderMigrationSql({ name, namespace: workspace.journal.namespace, contentHash, plan });
|
|
5013
6465
|
const metaDir = path.join(dir, MIGRATIONS_DIRNAME, META_DIRNAME);
|
|
5014
6466
|
await promises.mkdir(metaDir, { recursive: true });
|
|
5015
6467
|
const snapshotText = `${JSON.stringify(plan.snapshot, null, 2)}
|
|
5016
6468
|
`;
|
|
5017
|
-
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`),
|
|
6469
|
+
await promises.writeFile(path.join(dir, MIGRATIONS_DIRNAME, `${name}.sql`), sql4);
|
|
5018
6470
|
await promises.writeFile(path.join(metaDir, `${pad(ordinal)}.snapshot.json`), snapshotText);
|
|
5019
6471
|
const journal = {
|
|
5020
6472
|
...workspace.journal,
|
|
@@ -5025,7 +6477,7 @@ async function writeMigration({
|
|
|
5025
6477
|
name,
|
|
5026
6478
|
content_hash: contentHash,
|
|
5027
6479
|
previous_content_hash: tip?.content_hash ?? null,
|
|
5028
|
-
checksum: checksumOf(
|
|
6480
|
+
checksum: checksumOf(sql4),
|
|
5029
6481
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5030
6482
|
destructive
|
|
5031
6483
|
}
|
|
@@ -5140,8 +6592,8 @@ async function checkWorkspace({
|
|
|
5140
6592
|
let previousId3;
|
|
5141
6593
|
let previousHash = null;
|
|
5142
6594
|
for (const entry of workspace.journal.entries) {
|
|
5143
|
-
const
|
|
5144
|
-
if (checksumOf(
|
|
6595
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
6596
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
5145
6597
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
5146
6598
|
return 1;
|
|
5147
6599
|
}
|
|
@@ -5181,7 +6633,7 @@ async function checkWorkspace({
|
|
|
5181
6633
|
log(`migrations in sync \u2014 ${workspace.journal.entries.length} committed, tip ${tip.name}`);
|
|
5182
6634
|
return 0;
|
|
5183
6635
|
}
|
|
5184
|
-
var firstLine = (
|
|
6636
|
+
var firstLine = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
5185
6637
|
|
|
5186
6638
|
// src/migrations/from-yaml.ts
|
|
5187
6639
|
function convertV1DefinitionToCanonical(definition) {
|
|
@@ -5213,7 +6665,7 @@ function convertTable(table) {
|
|
|
5213
6665
|
description: table.description,
|
|
5214
6666
|
source: table.source,
|
|
5215
6667
|
columns: columns.map(([name, column]) => convertColumn(name, column)),
|
|
5216
|
-
// Synced tables
|
|
6668
|
+
// Synced tables may declare a typed column/PK contract; untyped remain empty.
|
|
5217
6669
|
primaryKey: primaryKey2.length > 0 ? primaryKey2 : void 0,
|
|
5218
6670
|
links: Object.entries(table.links ?? {}).map(([name, link]) => convertLink(table.table, name, link)),
|
|
5219
6671
|
indexes: (table.indexes ?? []).map((index3) => ({
|
|
@@ -5226,9 +6678,20 @@ function convertTable(table) {
|
|
|
5226
6678
|
read: table.policies?.read?.source,
|
|
5227
6679
|
write: table.policies?.write?.source
|
|
5228
6680
|
},
|
|
5229
|
-
|
|
6681
|
+
// v2 has no action surface (writes are mutations); the transition sweep
|
|
6682
|
+
// drops the plpgsql action_* functions, so carrying the metadata would be
|
|
6683
|
+
// dead weight. droppedV1Actions() surfaces them for the emitter's TODO.
|
|
6684
|
+
actions: []
|
|
5230
6685
|
};
|
|
5231
6686
|
}
|
|
6687
|
+
function droppedV1Actions(definition) {
|
|
6688
|
+
const dropped = /* @__PURE__ */ new Map();
|
|
6689
|
+
for (const table of definition.tables) {
|
|
6690
|
+
const names = Object.keys(table.actions ?? {});
|
|
6691
|
+
if (names.length > 0) dropped.set(table.table, names);
|
|
6692
|
+
}
|
|
6693
|
+
return dropped;
|
|
6694
|
+
}
|
|
5232
6695
|
function convertColumn(name, column) {
|
|
5233
6696
|
const isDefaultFn = column.default !== void 0 && isFunctionDefault(column.default, column.type);
|
|
5234
6697
|
return {
|
|
@@ -5262,25 +6725,6 @@ function convertLink(table, name, link) {
|
|
|
5262
6725
|
description: link.description
|
|
5263
6726
|
};
|
|
5264
6727
|
}
|
|
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
6728
|
|
|
5285
6729
|
// src/migrations/v2.ts
|
|
5286
6730
|
var SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
@@ -5303,7 +6747,7 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5303
6747
|
throw new OrmDefinitionError(`rebase ordinal must be an integer \u2265 2 (a rebase sits on an existing journal tip); got ${ordinal}`);
|
|
5304
6748
|
}
|
|
5305
6749
|
const name = migrationName(ordinal, newHash);
|
|
5306
|
-
const
|
|
6750
|
+
const sql4 = renderRebaseMigrationSql({ name, namespace, previousHash, newHash });
|
|
5307
6751
|
const snapshotText = `${JSON.stringify(snapshot, null, 2)}
|
|
5308
6752
|
`;
|
|
5309
6753
|
const entry = {
|
|
@@ -5311,11 +6755,11 @@ function buildRebaseJournalEntry({ namespace, previousHash, newHash, ordinal, sn
|
|
|
5311
6755
|
name,
|
|
5312
6756
|
content_hash: newHash,
|
|
5313
6757
|
previous_content_hash: previousHash,
|
|
5314
|
-
checksum: checksumOf(
|
|
6758
|
+
checksum: checksumOf(sql4),
|
|
5315
6759
|
snapshot_checksum: checksumOf(snapshotText),
|
|
5316
6760
|
destructive: false
|
|
5317
6761
|
};
|
|
5318
|
-
return { name, entry, sql:
|
|
6762
|
+
return { name, entry, sql: sql4, snapshotText };
|
|
5319
6763
|
}
|
|
5320
6764
|
function renderRebaseMigrationSql({
|
|
5321
6765
|
name,
|
|
@@ -5353,15 +6797,15 @@ var PLATFORM_USER_FIELDS = [
|
|
|
5353
6797
|
];
|
|
5354
6798
|
function generateSdl(canonical, options = {}) {
|
|
5355
6799
|
const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
|
|
5356
|
-
const tables = def.
|
|
6800
|
+
const tables = canonicalRelations(def).filter((table) => table.source === void 0);
|
|
5357
6801
|
if (tables.length === 0) {
|
|
5358
6802
|
throw new SdlGenerationError(
|
|
5359
|
-
`Namespace "${def.namespace}" exposes no
|
|
6803
|
+
`Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
|
|
5360
6804
|
);
|
|
5361
6805
|
}
|
|
5362
6806
|
for (const table of tables) {
|
|
5363
6807
|
if (table.columns.length === 0) {
|
|
5364
|
-
throw new SdlGenerationError(`
|
|
6808
|
+
throw new SdlGenerationError(`Relation "${table.table}" has no columns \u2014 cannot generate an object type.`);
|
|
5365
6809
|
}
|
|
5366
6810
|
}
|
|
5367
6811
|
const namesByTable = new Map(tables.map((table) => [table.table, tableNames(table.table)]));
|
|
@@ -5455,8 +6899,8 @@ function renderEnum(name, values) {
|
|
|
5455
6899
|
${values.map((value) => ` ${value}`).join("\n")}
|
|
5456
6900
|
}`;
|
|
5457
6901
|
}
|
|
5458
|
-
function descriptionLines(
|
|
5459
|
-
const escaped =
|
|
6902
|
+
function descriptionLines(text4, indent) {
|
|
6903
|
+
const escaped = text4.replace(/"""/g, '\\"""');
|
|
5460
6904
|
return [`${indent}"""`, ...escaped.split("\n").map((line) => `${indent}${line}`), `${indent}"""`];
|
|
5461
6905
|
}
|
|
5462
6906
|
function renderComparisonExp(gqlTypeName) {
|
|
@@ -5541,10 +6985,20 @@ function renderTableBlocks(ctx) {
|
|
|
5541
6985
|
${fields}
|
|
5542
6986
|
}`);
|
|
5543
6987
|
}
|
|
5544
|
-
if (
|
|
6988
|
+
if (allowsInsert(table) || allowsUpdate(table)) {
|
|
6989
|
+
blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
|
|
6990
|
+
}
|
|
5545
6991
|
return blocks;
|
|
5546
6992
|
}
|
|
5547
|
-
function
|
|
6993
|
+
function allowsInsert(table) {
|
|
6994
|
+
return table.policies.write !== void 0 || table.policies.insert !== void 0;
|
|
6995
|
+
}
|
|
6996
|
+
function allowsUpdate(table) {
|
|
6997
|
+
return table.policies.write !== void 0 || table.policies.update !== void 0;
|
|
6998
|
+
}
|
|
6999
|
+
function allowsOnConflict(table) {
|
|
7000
|
+
if (!allowsInsert(table) || !allowsUpdate(table)) return false;
|
|
7001
|
+
if (table.policies.insert !== void 0 || table.policies.update !== void 0) return false;
|
|
5548
7002
|
return table.policies.write !== void 0;
|
|
5549
7003
|
}
|
|
5550
7004
|
function renderObjectType(ctx) {
|
|
@@ -5676,46 +7130,50 @@ function constraintValues(table) {
|
|
|
5676
7130
|
const values = /* @__PURE__ */ new Set();
|
|
5677
7131
|
if (table.primaryKey && table.primaryKey.length > 0) values.add(`${table.table}_pkey`);
|
|
5678
7132
|
for (const column of table.columns) {
|
|
5679
|
-
if (column.unique) values.add(`${table.table}_${column.columnName}
|
|
7133
|
+
if (column.unique) values.add(`${table.table}_${column.columnName}_unique`);
|
|
5680
7134
|
}
|
|
5681
7135
|
for (const index3 of table.indexes) {
|
|
5682
7136
|
if (index3.unique) values.add(index3.name ?? `${table.table}_${index3.columns.join("_")}_key`);
|
|
5683
7137
|
}
|
|
5684
7138
|
return [...values].sort();
|
|
5685
7139
|
}
|
|
5686
|
-
function renderWriteBlocks(ctx) {
|
|
7140
|
+
function renderWriteBlocks(ctx, access) {
|
|
5687
7141
|
const { table, names, pinned } = ctx;
|
|
5688
7142
|
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(", ")}].`
|
|
7143
|
+
if (access.insert) {
|
|
7144
|
+
const pinnedSet = new Set(pinned);
|
|
7145
|
+
const insertable = table.columns.filter(
|
|
7146
|
+
(column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
|
|
5696
7147
|
);
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
7148
|
+
if (insertable.length === 0) {
|
|
7149
|
+
throw new SdlGenerationError(
|
|
7150
|
+
`Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
|
|
7151
|
+
);
|
|
7152
|
+
}
|
|
7153
|
+
const insertLines = insertable.map(
|
|
7154
|
+
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7155
|
+
);
|
|
7156
|
+
blocks.push(`input ${names.insertInput} {
|
|
5702
7157
|
${insertLines.join("\n")}
|
|
5703
7158
|
}`);
|
|
5704
|
-
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
7159
|
+
}
|
|
7160
|
+
if (access.update) {
|
|
7161
|
+
const setLines = table.columns.map(
|
|
7162
|
+
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
7163
|
+
);
|
|
7164
|
+
blocks.push(`input ${names.setInput} {
|
|
5708
7165
|
${setLines.join("\n")}
|
|
5709
7166
|
}`);
|
|
5710
|
-
|
|
5711
|
-
|
|
5712
|
-
|
|
7167
|
+
}
|
|
7168
|
+
const jsonb4 = jsonbColumns(table);
|
|
7169
|
+
if (access.update && jsonb4.length > 0) {
|
|
7170
|
+
const appendLines = jsonb4.map((column) => ` ${fieldName(column.name)}: jsonb`);
|
|
5713
7171
|
blocks.push(`input ${names.appendInput} {
|
|
5714
7172
|
${appendLines.join("\n")}
|
|
5715
7173
|
}`);
|
|
5716
7174
|
}
|
|
5717
7175
|
const numericish = numericishColumns(table);
|
|
5718
|
-
if (numericish.length > 0) {
|
|
7176
|
+
if (access.update && numericish.length > 0) {
|
|
5719
7177
|
const incLines = numericish.map(
|
|
5720
7178
|
(column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
|
|
5721
7179
|
);
|
|
@@ -5724,7 +7182,7 @@ ${incLines.join("\n")}
|
|
|
5724
7182
|
}`);
|
|
5725
7183
|
}
|
|
5726
7184
|
const constraints = constraintValues(table);
|
|
5727
|
-
if (constraints.length > 0) {
|
|
7185
|
+
if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
|
|
5728
7186
|
for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
|
|
5729
7187
|
blocks.push(renderEnum(names.constraintEnum, constraints));
|
|
5730
7188
|
const updateColumns = table.columns.map((column) => fieldName(column.name));
|
|
@@ -5737,12 +7195,14 @@ ${incLines.join("\n")}
|
|
|
5737
7195
|
}`
|
|
5738
7196
|
);
|
|
5739
7197
|
}
|
|
5740
|
-
|
|
5741
|
-
|
|
7198
|
+
if (access.insert || access.update) {
|
|
7199
|
+
blocks.push(
|
|
7200
|
+
`type ${names.mutationResponse} {
|
|
5742
7201
|
affectedRows: Int!
|
|
5743
7202
|
returning: [${names.typename}!]!
|
|
5744
7203
|
}`
|
|
5745
|
-
|
|
7204
|
+
);
|
|
7205
|
+
}
|
|
5746
7206
|
return blocks;
|
|
5747
7207
|
}
|
|
5748
7208
|
function renderQueryRoot(tables, namesByTable) {
|
|
@@ -5767,35 +7227,64 @@ function columnGqlTypeOf(table, column) {
|
|
|
5767
7227
|
function renderMutationRoot(tables, namesByTable) {
|
|
5768
7228
|
const lines = [];
|
|
5769
7229
|
for (const table of tables) {
|
|
5770
|
-
|
|
7230
|
+
const canInsert = allowsInsert(table);
|
|
7231
|
+
const canUpdate = allowsUpdate(table);
|
|
7232
|
+
if (!canInsert && !canUpdate) continue;
|
|
5771
7233
|
const names = namesByTable.get(table.table);
|
|
5772
7234
|
if (!names) continue;
|
|
5773
7235
|
const constraints = constraintValues(table);
|
|
5774
|
-
const onConflictArg = constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
7236
|
+
const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
|
|
5775
7237
|
const updateArgs = [`_set: ${names.setInput}`];
|
|
5776
7238
|
if (jsonbColumns(table).length > 0) updateArgs.push(`_append: ${names.appendInput}`);
|
|
5777
7239
|
if (numericishColumns(table).length > 0) updateArgs.push(`_inc: ${names.incInput}`);
|
|
5778
7240
|
const updateArgList = updateArgs.join(", ");
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
if (
|
|
5784
|
-
lines.push(
|
|
5785
|
-
|
|
5786
|
-
)
|
|
5787
|
-
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
|
|
5791
|
-
lines.push(` ${names.
|
|
7241
|
+
if (canInsert) {
|
|
7242
|
+
lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
|
|
7243
|
+
lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
|
|
7244
|
+
}
|
|
7245
|
+
if (canUpdate) {
|
|
7246
|
+
lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
|
|
7247
|
+
const pk = primaryKeyColumns(table);
|
|
7248
|
+
if (pk.length > 0) {
|
|
7249
|
+
lines.push(
|
|
7250
|
+
` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
|
|
7251
|
+
);
|
|
7252
|
+
}
|
|
7253
|
+
lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
|
|
7254
|
+
if (pk.length > 0) {
|
|
7255
|
+
const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
|
|
7256
|
+
lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
|
|
7257
|
+
}
|
|
5792
7258
|
}
|
|
7259
|
+
lines.push(...renderCowMutationFields(table, names));
|
|
5793
7260
|
}
|
|
5794
7261
|
if (lines.length === 0) return null;
|
|
5795
7262
|
return `type Mutation {
|
|
5796
7263
|
${lines.join("\n")}
|
|
5797
7264
|
}`;
|
|
5798
7265
|
}
|
|
7266
|
+
function renderCowMutationFields(table, names) {
|
|
7267
|
+
const lines = [];
|
|
7268
|
+
const columnByName = new Map(table.columns.map((column) => [column.name, column]));
|
|
7269
|
+
for (const mutation of table.cowMutations ?? []) {
|
|
7270
|
+
const pkColumn = columnByName.get(mutation.pk);
|
|
7271
|
+
if (!pkColumn) continue;
|
|
7272
|
+
const pkArg = `${fieldName(mutation.pk)}: ${columnGqlTypeOf(table, pkColumn)}!`;
|
|
7273
|
+
if (mutation.kind === "revert") {
|
|
7274
|
+
lines.push(` ${fieldName(mutation.name)}(${pkArg}): ${names.typename}`);
|
|
7275
|
+
continue;
|
|
7276
|
+
}
|
|
7277
|
+
const setArgs = mutation.set.map((property) => {
|
|
7278
|
+
const column = columnByName.get(property);
|
|
7279
|
+
if (!column) return null;
|
|
7280
|
+
const bang = column.nullable === false ? "!" : "";
|
|
7281
|
+
return `${fieldName(property)}: ${columnGqlTypeOf(table, column)}${bang}`;
|
|
7282
|
+
});
|
|
7283
|
+
if (setArgs.some((arg) => arg === null)) continue;
|
|
7284
|
+
lines.push(` ${fieldName(mutation.name)}(${pkArg}, ${setArgs.join(", ")}): ${names.typename}`);
|
|
7285
|
+
}
|
|
7286
|
+
return lines;
|
|
7287
|
+
}
|
|
5799
7288
|
|
|
5800
7289
|
// src/cli.ts
|
|
5801
7290
|
var BASE_URL_ENV = "SEQUENCE_ORM_BASE_URL";
|
|
@@ -6033,7 +7522,7 @@ async function generateCommand(rest) {
|
|
|
6033
7522
|
await writeGenerateOutputs(dir, compiled.project);
|
|
6034
7523
|
const { loaded, manifest } = compiled.project;
|
|
6035
7524
|
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))`
|
|
7525
|
+
`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
7526
|
);
|
|
6038
7527
|
return runConsumerCodegen(dir);
|
|
6039
7528
|
}
|
|
@@ -6086,7 +7575,7 @@ async function validateV2Command(dir) {
|
|
|
6086
7575
|
if (!compiled.ok) return 1;
|
|
6087
7576
|
const { loaded, manifest } = compiled.project;
|
|
6088
7577
|
console.log(
|
|
6089
|
-
`namespace ${loaded.canonical.namespace}: ${loaded.canonical.tables.length} table(s), ${Object.keys(manifest.operations).length} operation(s) valid`
|
|
7578
|
+
`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
7579
|
);
|
|
6091
7580
|
console.log(`content hash ${loaded.hash}`);
|
|
6092
7581
|
const workspace = await readMigrationWorkspace(dir, loaded.canonical.namespace);
|
|
@@ -6216,8 +7705,8 @@ async function checkWorkspaceV2({
|
|
|
6216
7705
|
let previousId3;
|
|
6217
7706
|
let previousHash = null;
|
|
6218
7707
|
for (const entry of workspace.journal.entries) {
|
|
6219
|
-
const
|
|
6220
|
-
if (checksumOf(
|
|
7708
|
+
const sql4 = await readMigrationSql(dir, entry);
|
|
7709
|
+
if (checksumOf(sql4) !== entry.checksum) {
|
|
6221
7710
|
log(`${MIGRATIONS_DIRNAME}/${entry.name}.sql does not match its journal checksum \u2014 committed migrations must not be edited`);
|
|
6222
7711
|
return 1;
|
|
6223
7712
|
}
|
|
@@ -6404,9 +7893,15 @@ async function migrateFromYamlCommand(rest) {
|
|
|
6404
7893
|
}
|
|
6405
7894
|
const v1 = await loadNamespaceDefinition(dir);
|
|
6406
7895
|
const canonical = convertV1DefinitionToCanonical(v1.definition);
|
|
7896
|
+
const droppedActions = droppedV1Actions(v1.definition);
|
|
6407
7897
|
await writeConverted(path.join(dir, NAMESPACE_CONFIG_FILE), emitSequenceConfig(canonical));
|
|
6408
7898
|
await promises.mkdir(path.join(dir, SCHEMA_DIR), { recursive: true });
|
|
6409
|
-
await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical));
|
|
7899
|
+
await writeConverted(path.join(dir, SCHEMA_DIR, "tables.ts"), emitSchemaTables(canonical, droppedActions));
|
|
7900
|
+
for (const [table, names] of droppedActions) {
|
|
7901
|
+
console.log(
|
|
7902
|
+
`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)`
|
|
7903
|
+
);
|
|
7904
|
+
}
|
|
6410
7905
|
const reloaded = await loadV2(dir);
|
|
6411
7906
|
if (JSON.stringify(sortKeys(reloaded.canonical)) !== JSON.stringify(sortKeys(canonical))) {
|
|
6412
7907
|
console.error(
|
|
@@ -6470,14 +7965,15 @@ function emitSequenceConfig(canonical) {
|
|
|
6470
7965
|
lines.push("})", "");
|
|
6471
7966
|
return lines.join("\n");
|
|
6472
7967
|
}
|
|
6473
|
-
function emitSchemaTables(canonical) {
|
|
7968
|
+
function emitSchemaTables(canonical, droppedActions) {
|
|
6474
7969
|
const columnTables2 = canonical.tables.filter((table) => table.source === void 0);
|
|
6475
7970
|
const syncedTables = canonical.tables.filter((table) => table.source !== void 0);
|
|
6476
7971
|
const hasLinks = canonical.tables.some((table) => table.links.length > 0);
|
|
7972
|
+
const syncedHasColumns = syncedTables.some((table) => table.columns.length > 0);
|
|
6477
7973
|
const imports = [
|
|
6478
7974
|
...columnTables2.length > 0 ? ["defineTable"] : [],
|
|
6479
7975
|
...syncedTables.length > 0 ? ["defineSyncedTable"] : [],
|
|
6480
|
-
...columnTables2.length > 0 ? ["t"] : [],
|
|
7976
|
+
...columnTables2.length > 0 || syncedHasColumns ? ["t"] : [],
|
|
6481
7977
|
...hasLinks ? ["toOne"] : []
|
|
6482
7978
|
].sort((a, b) => a.localeCompare(b));
|
|
6483
7979
|
const parts = [
|
|
@@ -6491,18 +7987,62 @@ function emitSchemaTables(canonical) {
|
|
|
6491
7987
|
""
|
|
6492
7988
|
];
|
|
6493
7989
|
for (const table of canonical.tables) {
|
|
7990
|
+
const dropped = droppedActions?.get(table.table) ?? [];
|
|
7991
|
+
if (dropped.length > 0) {
|
|
7992
|
+
parts.push(
|
|
7993
|
+
`// TODO: v1 actions dropped by the conversion \u2014 v2 has no action surface (writes`,
|
|
7994
|
+
`// are persisted mutations). Re-author for '${table.table}': ${dropped.join(", ")}`
|
|
7995
|
+
);
|
|
7996
|
+
}
|
|
6494
7997
|
parts.push(table.source !== void 0 ? emitSyncedTable(table) : emitTable(table));
|
|
6495
7998
|
}
|
|
6496
7999
|
return parts.join("\n");
|
|
6497
8000
|
}
|
|
6498
8001
|
function emitSyncedTable(table) {
|
|
8002
|
+
const singlePrimaryKey = table.primaryKey?.length === 1 ? table.primaryKey[0] : void 0;
|
|
8003
|
+
if (table.columns.length === 0) {
|
|
8004
|
+
const lines2 = [
|
|
8005
|
+
`export const ${tableVarName(table.table)} = defineSyncedTable(${quote(table.table)}, {`,
|
|
8006
|
+
` source: ${quote(table.source ?? "")},`
|
|
8007
|
+
];
|
|
8008
|
+
if (table.description !== void 0) lines2.push(` description: ${quote(table.description)},`);
|
|
8009
|
+
if (table.policies.read !== void 0) {
|
|
8010
|
+
lines2.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
|
|
8011
|
+
}
|
|
8012
|
+
lines2.push("})", "");
|
|
8013
|
+
return lines2.join("\n");
|
|
8014
|
+
}
|
|
6499
8015
|
const lines = [
|
|
6500
|
-
`export const ${tableVarName(table.table)} = defineSyncedTable(
|
|
6501
|
-
|
|
6502
|
-
|
|
8016
|
+
`export const ${tableVarName(table.table)} = defineSyncedTable(`,
|
|
8017
|
+
` ${quote(table.table)},`,
|
|
8018
|
+
" {"
|
|
6503
8019
|
];
|
|
6504
|
-
|
|
6505
|
-
lines.push("}
|
|
8020
|
+
for (const column of table.columns) lines.push(` ${columnProperty(table.table, column, singlePrimaryKey)}`);
|
|
8021
|
+
lines.push(" },", " {");
|
|
8022
|
+
lines.push(` source: ${quote(table.source ?? "")},`);
|
|
8023
|
+
if (table.description !== void 0) lines.push(` description: ${quote(table.description)},`);
|
|
8024
|
+
if (singlePrimaryKey === void 0 && table.primaryKey && table.primaryKey.length > 0) {
|
|
8025
|
+
lines.push(` primaryKey: ${jsonLiteral(table.primaryKey, 4)},`);
|
|
8026
|
+
}
|
|
8027
|
+
if (table.links.length > 0) {
|
|
8028
|
+
lines.push(" links: {");
|
|
8029
|
+
for (const link of table.links) {
|
|
8030
|
+
if (link.kind !== "toOne" || link.target === void 0) {
|
|
8031
|
+
throw new OrmDefinitionError(
|
|
8032
|
+
`link '${link.name}' on synced '${table.table}' is not a toOne link \u2014 not a v1 conversion output`
|
|
8033
|
+
);
|
|
8034
|
+
}
|
|
8035
|
+
const options = [`on: ${quote(link.on)}`];
|
|
8036
|
+
if (link.references !== void 0) options.push(`references: ${quote(link.references)}`);
|
|
8037
|
+
if (link.description !== void 0) options.push(`description: ${quote(link.description)}`);
|
|
8038
|
+
lines.push(` ${objectKey(link.name)}: toOne(() => ${tableVarName(link.target)}, { ${options.join(", ")} }),`);
|
|
8039
|
+
}
|
|
8040
|
+
lines.push(" },");
|
|
8041
|
+
}
|
|
8042
|
+
if (table.policies.read !== void 0) {
|
|
8043
|
+
lines.push(" policies: {", ` read: ${policyLiteral(table.policies.read)},`, " },");
|
|
8044
|
+
}
|
|
8045
|
+
lines.push(" },", ")", "");
|
|
6506
8046
|
return lines.join("\n");
|
|
6507
8047
|
}
|
|
6508
8048
|
function emitTable(table) {
|
|
@@ -6572,7 +8112,6 @@ function emitTableConfig(table, singlePrimaryKey) {
|
|
|
6572
8112
|
if (table.policies.write !== void 0) lines.push(` write: ${policyLiteral(table.policies.write)},`);
|
|
6573
8113
|
lines.push(" },");
|
|
6574
8114
|
}
|
|
6575
|
-
if (table.actions.length > 0) lines.push(` actions: ${jsonLiteral(table.actions, 4)},`);
|
|
6576
8115
|
return lines;
|
|
6577
8116
|
}
|
|
6578
8117
|
function policyLiteral(policy) {
|
|
@@ -6873,7 +8412,7 @@ function positional(rest) {
|
|
|
6873
8412
|
if (envIndex !== -1) args.splice(envIndex, 2);
|
|
6874
8413
|
return args.find((arg) => !arg.startsWith("-"));
|
|
6875
8414
|
}
|
|
6876
|
-
var firstLine2 = (
|
|
8415
|
+
var firstLine2 = (sql4) => sql4.split("\n")[0] ?? sql4;
|
|
6877
8416
|
|
|
6878
8417
|
exports.BASE_URL_ENV = BASE_URL_ENV;
|
|
6879
8418
|
exports.ORM_MODULE_PATH_ENV = ORM_MODULE_PATH_ENV;
|