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