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