@sequenceholdings/orm 0.1.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -24
- package/dist/authoring.cjs +1072 -60
- package/dist/authoring.cjs.map +1 -1
- package/dist/authoring.d.cts +291 -26
- package/dist/authoring.d.ts +291 -26
- package/dist/authoring.js +1062 -61
- package/dist/authoring.js.map +1 -1
- package/dist/canonical.cjs +415 -9
- package/dist/canonical.cjs.map +1 -1
- package/dist/canonical.d.cts +235 -1
- package/dist/canonical.d.ts +235 -1
- package/dist/canonical.js +411 -10
- package/dist/canonical.js.map +1 -1
- package/dist/cli.cjs +1860 -228
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1861 -229
- package/dist/cli.js.map +1 -1
- package/dist/client.cjs.map +1 -1
- package/dist/client.d.cts +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js.map +1 -1
- package/dist/compiler.cjs +512 -54
- package/dist/compiler.cjs.map +1 -1
- package/dist/compiler.d.cts +39 -231
- package/dist/compiler.d.ts +39 -231
- package/dist/compiler.js +510 -55
- package/dist/compiler.js.map +1 -1
- package/dist/drizzle.cjs +14 -0
- package/dist/drizzle.cjs.map +1 -0
- package/dist/drizzle.d.cts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.js +3 -0
- package/dist/drizzle.js.map +1 -0
- package/dist/dynamic-input.cjs +391 -0
- package/dist/dynamic-input.cjs.map +1 -0
- package/dist/dynamic-input.d.cts +111 -0
- package/dist/dynamic-input.d.ts +111 -0
- package/dist/dynamic-input.js +380 -0
- package/dist/dynamic-input.js.map +1 -0
- package/dist/index.cjs +2229 -220
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +125 -9
- package/dist/index.d.ts +125 -9
- package/dist/index.js +2205 -221
- package/dist/index.js.map +1 -1
- package/dist/ir.cjs +249 -14
- package/dist/ir.cjs.map +1 -1
- package/dist/ir.d.cts +20 -80
- package/dist/ir.d.ts +20 -80
- package/dist/ir.js +249 -14
- package/dist/ir.js.map +1 -1
- package/dist/predicate-v2.cjs +1099 -0
- package/dist/predicate-v2.cjs.map +1 -0
- package/dist/predicate-v2.d.cts +142 -0
- package/dist/predicate-v2.d.ts +142 -0
- package/dist/predicate-v2.js +1089 -0
- package/dist/predicate-v2.js.map +1 -0
- package/dist/sdl.cjs +344 -55
- package/dist/sdl.cjs.map +1 -1
- package/dist/sdl.d.cts +1 -3
- package/dist/sdl.d.ts +1 -3
- package/dist/sdl.js +344 -55
- package/dist/sdl.js.map +1 -1
- package/package.json +20 -3
- package/src/codegen-gql/preset.cjs +64 -0
- package/src/codegen-gql/server-fns.cjs +11 -6
- package/templates/orm-namespace/CLAUDE.md +6 -5
- package/templates/orm-namespace/codegen.preset.cjs +17 -0
- package/templates/orm-namespace/codegen.ts +1 -1
- package/templates/orm-namespace/package.json +3 -0
- package/templates/orm-namespace/schema/tables.ts +1 -1
- package/templates/orm-namespace/sequence.config.ts +1 -1
package/dist/compiler.cjs
CHANGED
|
@@ -90,8 +90,33 @@ var policyExpressionSchema = zod.z.union([
|
|
|
90
90
|
]);
|
|
91
91
|
var canonicalPoliciesSchema = zod.z.object({
|
|
92
92
|
read: policyExpressionSchema.optional(),
|
|
93
|
-
write: policyExpressionSchema.optional()
|
|
93
|
+
write: policyExpressionSchema.optional(),
|
|
94
|
+
insert: policyExpressionSchema.optional(),
|
|
95
|
+
update: policyExpressionSchema.optional()
|
|
94
96
|
});
|
|
97
|
+
function isSingleViewQuerySql(value) {
|
|
98
|
+
const query = value.trim();
|
|
99
|
+
if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
|
|
100
|
+
let state = "normal";
|
|
101
|
+
for (let index = 0; index < query.length; index += 1) {
|
|
102
|
+
const char = query[index];
|
|
103
|
+
const next = query[index + 1];
|
|
104
|
+
if (state === "single") {
|
|
105
|
+
if (char === "'" && next === "'") index += 1;
|
|
106
|
+
else if (char === "'") state = "normal";
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (state === "double") {
|
|
110
|
+
if (char === '"' && next === '"') index += 1;
|
|
111
|
+
else if (char === '"') state = "normal";
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (char === "'") state = "single";
|
|
115
|
+
else if (char === '"') state = "double";
|
|
116
|
+
else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
|
|
117
|
+
}
|
|
118
|
+
return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
|
|
119
|
+
}
|
|
95
120
|
var canonicalActionSchema = zod.z.object({
|
|
96
121
|
name: identifier,
|
|
97
122
|
requires: zod.z.string().max(1e3).optional(),
|
|
@@ -103,6 +128,37 @@ var canonicalActionSchema = zod.z.object({
|
|
|
103
128
|
returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
|
|
104
129
|
audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
|
|
105
130
|
});
|
|
131
|
+
var canonicalCowMutationSchema = zod.z.object({
|
|
132
|
+
kind: zod.z.enum(["override", "revert"]),
|
|
133
|
+
name: identifier,
|
|
134
|
+
/** Synced table (same namespace) the overlay copies from / falls back to. */
|
|
135
|
+
synced: identifier,
|
|
136
|
+
/** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
|
|
137
|
+
pk: zod.z.string().min(1).max(63),
|
|
138
|
+
/** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
|
|
139
|
+
copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
|
|
140
|
+
/** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
|
|
141
|
+
set: zod.z.array(zod.z.string().min(1).max(63)).default([])
|
|
142
|
+
}).superRefine((mutation, ctx) => {
|
|
143
|
+
if (mutation.kind === "override") {
|
|
144
|
+
if (mutation.set.length === 0) {
|
|
145
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
|
|
146
|
+
}
|
|
147
|
+
if (!mutation.copy.includes(mutation.pk)) {
|
|
148
|
+
ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
|
|
149
|
+
}
|
|
150
|
+
} else if (mutation.set.length > 0 || mutation.copy.length > 0) {
|
|
151
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
|
|
152
|
+
}
|
|
153
|
+
if (mutation.set.includes(mutation.pk)) {
|
|
154
|
+
ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
|
|
155
|
+
}
|
|
156
|
+
for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
|
|
157
|
+
if (new Set(values).size !== values.length) {
|
|
158
|
+
ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
});
|
|
106
162
|
var canonicalTableSchema = zod.z.object({
|
|
107
163
|
table: identifier,
|
|
108
164
|
description: zod.z.string().max(2e3).optional(),
|
|
@@ -115,7 +171,45 @@ var canonicalTableSchema = zod.z.object({
|
|
|
115
171
|
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
116
172
|
indexes: zod.z.array(canonicalIndexSchema).default([]),
|
|
117
173
|
policies: canonicalPoliciesSchema.default({}),
|
|
118
|
-
|
|
174
|
+
/**
|
|
175
|
+
* v1-conversion carry-over only. v2 has no action surface — the runtime
|
|
176
|
+
* neither compiles nor executes these; writes are mutations. Kept parseable
|
|
177
|
+
* so stored definitions and their content hashes stay stable.
|
|
178
|
+
*/
|
|
179
|
+
actions: zod.z.array(canonicalActionSchema).default([]),
|
|
180
|
+
/** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
|
|
181
|
+
cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
|
|
182
|
+
});
|
|
183
|
+
var canonicalViewSchema = zod.z.object({
|
|
184
|
+
view: identifier,
|
|
185
|
+
description: zod.z.string().max(2e3).optional(),
|
|
186
|
+
columns: zod.z.array(canonicalColumnSchema).min(1),
|
|
187
|
+
/** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
|
|
188
|
+
primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
|
|
189
|
+
links: zod.z.array(canonicalLinkSchema).default([]),
|
|
190
|
+
policies: zod.z.object({ read: policyExpressionSchema }).strict(),
|
|
191
|
+
query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
|
|
192
|
+
message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
|
|
193
|
+
}),
|
|
194
|
+
/** Namespace-local logical relation names, sorted for deterministic hashing. */
|
|
195
|
+
dependencies: zod.z.array(identifier).max(500)
|
|
196
|
+
}).superRefine((view, ctx) => {
|
|
197
|
+
for (const [index, column] of view.columns.entries()) {
|
|
198
|
+
if (column.default !== void 0 || column.defaultFn !== void 0) {
|
|
199
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
|
|
200
|
+
}
|
|
201
|
+
if (column.unique) {
|
|
202
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
|
|
203
|
+
}
|
|
204
|
+
if (column.renamedFrom !== void 0 || column.cast !== void 0) {
|
|
205
|
+
ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const [index, link] of view.links.entries()) {
|
|
209
|
+
if (link.enforce) {
|
|
210
|
+
ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
119
213
|
});
|
|
120
214
|
var canonicalDefinitionSchema = zod.z.object({
|
|
121
215
|
definition_format: zod.z.literal(CANONICAL_FORMAT),
|
|
@@ -123,24 +217,174 @@ var canonicalDefinitionSchema = zod.z.object({
|
|
|
123
217
|
description: zod.z.string().max(2e3).optional(),
|
|
124
218
|
enums: zod.z.array(canonicalEnumSchema).default([]),
|
|
125
219
|
tables: zod.z.array(canonicalTableSchema).min(0).max(500),
|
|
220
|
+
/** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
|
|
221
|
+
views: zod.z.array(canonicalViewSchema).max(500).optional(),
|
|
126
222
|
/** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
|
|
127
223
|
capabilities: zod.z.object({
|
|
128
224
|
roles: zod.z.unknown().optional(),
|
|
129
225
|
uses: zod.z.array(zod.z.string()).default([])
|
|
130
226
|
}).default({ uses: [] })
|
|
227
|
+
}).superRefine((definition, ctx) => {
|
|
228
|
+
const seen = /* @__PURE__ */ new Set();
|
|
229
|
+
for (const [index, table] of definition.tables.entries()) {
|
|
230
|
+
if (seen.has(table.table)) {
|
|
231
|
+
ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
|
|
232
|
+
}
|
|
233
|
+
seen.add(table.table);
|
|
234
|
+
}
|
|
235
|
+
const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
|
|
236
|
+
for (const [tableIndex, table] of definition.tables.entries()) {
|
|
237
|
+
const cowMutations = table.cowMutations ?? [];
|
|
238
|
+
if (cowMutations.length === 0) continue;
|
|
239
|
+
const path = (index, ...rest) => ["tables", tableIndex, "cowMutations", index, ...rest];
|
|
240
|
+
if (table.source !== void 0) {
|
|
241
|
+
ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
for (const [index, mutation] of cowMutations.entries()) {
|
|
245
|
+
const synced = tableByName.get(mutation.synced);
|
|
246
|
+
if (!synced || synced.source === void 0) {
|
|
247
|
+
ctx.addIssue({
|
|
248
|
+
code: "custom",
|
|
249
|
+
path: path(index, "synced"),
|
|
250
|
+
message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
|
|
251
|
+
});
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
|
|
255
|
+
const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
|
|
256
|
+
let columnsExist = true;
|
|
257
|
+
for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
|
|
258
|
+
if (!overlayColumnsByName.has(property)) {
|
|
259
|
+
columnsExist = false;
|
|
260
|
+
ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
264
|
+
if (!syncedColumnsByName.has(property)) {
|
|
265
|
+
columnsExist = false;
|
|
266
|
+
ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!columnsExist) continue;
|
|
270
|
+
const overlayPk = table.primaryKey ?? [];
|
|
271
|
+
const syncedPk = synced.primaryKey ?? [];
|
|
272
|
+
if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
|
|
273
|
+
ctx.addIssue({
|
|
274
|
+
code: "custom",
|
|
275
|
+
path: path(index, "pk"),
|
|
276
|
+
message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
for (const property of [mutation.pk, ...mutation.copy]) {
|
|
280
|
+
const overlayColumn = overlayColumnsByName.get(property);
|
|
281
|
+
const syncedColumn = syncedColumnsByName.get(property);
|
|
282
|
+
if (!overlayColumn || !syncedColumn) continue;
|
|
283
|
+
if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
|
|
284
|
+
ctx.addIssue({
|
|
285
|
+
code: "custom",
|
|
286
|
+
path: path(index),
|
|
287
|
+
message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (overlayColumn.columnName !== syncedColumn.columnName) {
|
|
291
|
+
ctx.addIssue({
|
|
292
|
+
code: "custom",
|
|
293
|
+
path: path(index),
|
|
294
|
+
message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
|
|
298
|
+
ctx.addIssue({
|
|
299
|
+
code: "custom",
|
|
300
|
+
path: path(index),
|
|
301
|
+
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`
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
for (const [index, view] of (definition.views ?? []).entries()) {
|
|
308
|
+
if (seen.has(view.view)) {
|
|
309
|
+
ctx.addIssue({ code: "custom", path: ["views", index, "view"], message: `duplicate relation '${view.view}'` });
|
|
310
|
+
}
|
|
311
|
+
seen.add(view.view);
|
|
312
|
+
}
|
|
313
|
+
const views = definition.views ?? [];
|
|
314
|
+
const viewByName = new Map(views.map((view) => [view.view, view]));
|
|
315
|
+
for (const [viewIndex, view] of views.entries()) {
|
|
316
|
+
for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
|
|
317
|
+
if (!seen.has(dependency)) {
|
|
318
|
+
ctx.addIssue({
|
|
319
|
+
code: "custom",
|
|
320
|
+
path: ["views", viewIndex, "dependencies", dependencyIndex],
|
|
321
|
+
message: `view '${view.view}' depends on unknown relation '${dependency}'`
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
327
|
+
const visited = /* @__PURE__ */ new Set();
|
|
328
|
+
let cycleReported = false;
|
|
329
|
+
const visit = (name, path) => {
|
|
330
|
+
if (visited.has(name) || cycleReported) return;
|
|
331
|
+
if (visiting.has(name)) {
|
|
332
|
+
cycleReported = true;
|
|
333
|
+
ctx.addIssue({
|
|
334
|
+
code: "custom",
|
|
335
|
+
path: ["views"],
|
|
336
|
+
message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
|
|
337
|
+
});
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
visiting.add(name);
|
|
341
|
+
const view = viewByName.get(name);
|
|
342
|
+
for (const dependency of view?.dependencies ?? []) {
|
|
343
|
+
if (viewByName.has(dependency)) visit(dependency, [...path, name]);
|
|
344
|
+
}
|
|
345
|
+
visiting.delete(name);
|
|
346
|
+
visited.add(name);
|
|
347
|
+
};
|
|
348
|
+
for (const view of views) visit(view.view, []);
|
|
131
349
|
});
|
|
350
|
+
function canonicalRelations(definition) {
|
|
351
|
+
const views = (definition.views ?? []).map((view) => ({
|
|
352
|
+
table: view.view,
|
|
353
|
+
...view.description !== void 0 ? { description: view.description } : {},
|
|
354
|
+
columns: view.columns,
|
|
355
|
+
...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
|
|
356
|
+
links: view.links,
|
|
357
|
+
indexes: [],
|
|
358
|
+
policies: { read: view.policies.read },
|
|
359
|
+
actions: []
|
|
360
|
+
}));
|
|
361
|
+
return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
|
|
362
|
+
}
|
|
132
363
|
function normalizeCanonical(definition) {
|
|
133
364
|
const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
-
|
|
365
|
+
const normalized = {
|
|
135
366
|
...definition,
|
|
136
367
|
enums: sortByName(definition.enums),
|
|
137
|
-
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) =>
|
|
138
|
-
...t
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
368
|
+
tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
|
|
369
|
+
const { cowMutations, ...rest } = t;
|
|
370
|
+
return {
|
|
371
|
+
...rest,
|
|
372
|
+
links: sortByName(t.links),
|
|
373
|
+
indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
|
|
374
|
+
actions: sortByName(t.actions),
|
|
375
|
+
// Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
|
|
376
|
+
...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
|
|
377
|
+
};
|
|
378
|
+
})
|
|
143
379
|
};
|
|
380
|
+
if (definition.views !== void 0) {
|
|
381
|
+
normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
|
|
382
|
+
...view,
|
|
383
|
+
links: sortByName(view.links),
|
|
384
|
+
dependencies: [...view.dependencies].sort()
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
return normalized;
|
|
144
388
|
}
|
|
145
389
|
|
|
146
390
|
// src/ir/index.ts
|
|
@@ -281,9 +525,23 @@ var onConflictIrSchema = zod.z.union([
|
|
|
281
525
|
]);
|
|
282
526
|
var mutationRootSchema = zod.z.object({
|
|
283
527
|
kind: zod.z.literal("mutation"),
|
|
284
|
-
|
|
528
|
+
/**
|
|
529
|
+
* CRUD verbs plus the structured copy-on-write ops. `cowOverride` copies the
|
|
530
|
+
* synced row into the overlay (INSERT…SELECT, ON CONFLICT DO NOTHING) then
|
|
531
|
+
* applies `set` — both statements generated by the executor from this
|
|
532
|
+
* metadata, never authored SQL. `cowRevert` deletes the override so the
|
|
533
|
+
* coalesce view falls back to synced. Both are gated on a matching synced
|
|
534
|
+
* row and carry the write policy in `where` like update/delete.
|
|
535
|
+
*/
|
|
536
|
+
op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk", "cowOverride", "cowRevert"]),
|
|
285
537
|
alias: zod.z.string(),
|
|
286
538
|
table: zod.z.string(),
|
|
539
|
+
/** CoW ops: logical name of the synced twin (read through its policy-filtered view). */
|
|
540
|
+
cowSynced: zod.z.string().optional(),
|
|
541
|
+
/** cowOverride: logical properties copied INSERT…SELECT from synced (pk included). */
|
|
542
|
+
cowCopyColumns: zod.z.array(zod.z.string()).optional(),
|
|
543
|
+
/** cowOverride: insert-policy WITH CHECK evaluated over the copied rows. */
|
|
544
|
+
cowCopyCheck: predIrSchema.optional(),
|
|
287
545
|
/** insert/insertOne: rows come from a slot ($object/$objects) or literals. */
|
|
288
546
|
objects: zod.z.union([
|
|
289
547
|
zod.z.object({ slot: zod.z.string() }),
|
|
@@ -304,19 +562,11 @@ var mutationRootSchema = zod.z.object({
|
|
|
304
562
|
affectedRows: zod.z.boolean().default(false),
|
|
305
563
|
includeIf: includeIfSchema.optional()
|
|
306
564
|
});
|
|
307
|
-
var actionRootSchema = zod.z.object({
|
|
308
|
-
kind: zod.z.literal("action"),
|
|
309
|
-
alias: zod.z.string(),
|
|
310
|
-
name: zod.z.string(),
|
|
311
|
-
params: zod.z.array(zod.z.object({ name: zod.z.string(), value: valueIrSchema })),
|
|
312
|
-
returns: zod.z.enum(["rows", "rowsAffected", "scalar"])
|
|
313
|
-
});
|
|
314
565
|
var rootIrSchema = zod.z.discriminatedUnion("kind", [
|
|
315
566
|
selectRootSchema,
|
|
316
567
|
byPkRootSchema,
|
|
317
568
|
aggregateRootSchema,
|
|
318
|
-
mutationRootSchema
|
|
319
|
-
actionRootSchema
|
|
569
|
+
mutationRootSchema
|
|
320
570
|
]);
|
|
321
571
|
var INPUT_REF_KINDS = [
|
|
322
572
|
"boolExp",
|
|
@@ -348,7 +598,7 @@ var compiledOperationSchema = zod.z.object({
|
|
|
348
598
|
irVersion: zod.z.literal(IR_VERSION),
|
|
349
599
|
name: zod.z.string(),
|
|
350
600
|
opHash: zod.z.string().regex(/^sha256:[0-9a-f]{64}$/),
|
|
351
|
-
kind: zod.z.enum(["query", "mutation"
|
|
601
|
+
kind: zod.z.enum(["query", "mutation"]),
|
|
352
602
|
requires: zod.z.object({
|
|
353
603
|
capability: zod.z.string().optional(),
|
|
354
604
|
role: zod.z.string().optional()
|
|
@@ -362,13 +612,24 @@ var tableMetaIrSchema = zod.z.object({
|
|
|
362
612
|
table: zod.z.string(),
|
|
363
613
|
schema: zod.z.string(),
|
|
364
614
|
typename: zod.z.string(),
|
|
615
|
+
/**
|
|
616
|
+
* True when statements over this relation evaluate a claims-gated synced
|
|
617
|
+
* view: synced twins (`source: gold.*`) materialize as views whose baked
|
|
618
|
+
* policy reads `current_setting('request.jwt.claims')`, and namespace views
|
|
619
|
+
* inherit the gate transitively through their declared dependencies. The
|
|
620
|
+
* executor must stamp that GUC (transaction-scoped) before any statement
|
|
621
|
+
* touching such a relation, or the synced arm yields zero rows regardless
|
|
622
|
+
* of the compiled predicates.
|
|
623
|
+
*/
|
|
624
|
+
claimsGated: zod.z.boolean().default(false),
|
|
365
625
|
columns: zod.z.array(zod.z.object({
|
|
366
626
|
name: zod.z.string(),
|
|
367
627
|
columnName: zod.z.string(),
|
|
368
628
|
type: zod.z.union([zod.z.enum(COLUMN_TYPES), zod.z.object({ enum: zod.z.string(), values: zod.z.array(zod.z.string()) })]),
|
|
369
629
|
nullable: zod.z.boolean()
|
|
370
630
|
})),
|
|
371
|
-
|
|
631
|
+
/** Empty for keyless read-only views; keyed tables and views list logical columns. */
|
|
632
|
+
primaryKey: zod.z.array(zod.z.string()),
|
|
372
633
|
links: zod.z.array(zod.z.object({
|
|
373
634
|
name: zod.z.string(),
|
|
374
635
|
kind: zod.z.enum(["toOne", "toMany", "toPlatformUser"]),
|
|
@@ -554,6 +815,10 @@ var Parser = class {
|
|
|
554
815
|
this.expect("rparen", "')'");
|
|
555
816
|
return inner;
|
|
556
817
|
}
|
|
818
|
+
if (this.peekKeyword("true") || this.peekKeyword("false")) {
|
|
819
|
+
const token = this.advance();
|
|
820
|
+
return { kind: "literal", value: token.value === "true" };
|
|
821
|
+
}
|
|
557
822
|
if (this.peekKeyword("capability") || this.peekKeyword("role")) {
|
|
558
823
|
const fn = this.peek().value;
|
|
559
824
|
const node = this.parseBooleanCall(fn);
|
|
@@ -737,6 +1002,8 @@ var PolicyCompiler = class {
|
|
|
737
1002
|
}
|
|
738
1003
|
compile(node, table, depth) {
|
|
739
1004
|
switch (node.kind) {
|
|
1005
|
+
case "literal":
|
|
1006
|
+
return { kind: "const", value: node.value };
|
|
740
1007
|
case "and":
|
|
741
1008
|
case "or":
|
|
742
1009
|
return { kind: node.kind, operands: node.operands.map((operand) => this.compile(operand, table, depth)) };
|
|
@@ -907,6 +1174,7 @@ var NamespaceCompileError = class extends Error {
|
|
|
907
1174
|
|
|
908
1175
|
// src/compiler-gql/pred-utils.ts
|
|
909
1176
|
var CONST_TRUE = { kind: "const", value: true };
|
|
1177
|
+
var CONST_FALSE = { kind: "const", value: false };
|
|
910
1178
|
function andPred(...preds) {
|
|
911
1179
|
const operands = preds.filter(
|
|
912
1180
|
(pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
|
|
@@ -1000,6 +1268,12 @@ var PolicyInjector = class {
|
|
|
1000
1268
|
write(table) {
|
|
1001
1269
|
return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
|
|
1002
1270
|
}
|
|
1271
|
+
insert(table) {
|
|
1272
|
+
return this.ctx.insertPolicies.get(table) ?? this.write(table);
|
|
1273
|
+
}
|
|
1274
|
+
update(table) {
|
|
1275
|
+
return this.ctx.updatePolicies.get(table) ?? this.write(table);
|
|
1276
|
+
}
|
|
1003
1277
|
linkTarget(table, link) {
|
|
1004
1278
|
const meta = this.ctx.tables[table];
|
|
1005
1279
|
const resolved = meta?.links.find((l) => l.name === link);
|
|
@@ -1074,31 +1348,50 @@ var PolicyInjector = class {
|
|
|
1074
1348
|
};
|
|
1075
1349
|
case "mutation":
|
|
1076
1350
|
return this.injectMutation(root);
|
|
1077
|
-
case "action":
|
|
1078
|
-
return root;
|
|
1079
1351
|
}
|
|
1080
1352
|
}
|
|
1081
1353
|
injectMutation(root) {
|
|
1082
1354
|
const table = root.table;
|
|
1083
|
-
const writePolicy = this.write(table);
|
|
1084
1355
|
const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
|
|
1085
1356
|
if (root.op === "insert" || root.op === "insertOne") {
|
|
1086
|
-
|
|
1357
|
+
const insertPolicy = this.insert(table);
|
|
1358
|
+
if (predContainsExists(insertPolicy)) {
|
|
1087
1359
|
throw new DocumentCompileError(
|
|
1088
|
-
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its
|
|
1360
|
+
`table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
|
|
1089
1361
|
"insert-check-unrepresentable"
|
|
1090
1362
|
);
|
|
1091
1363
|
}
|
|
1092
1364
|
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
1093
1365
|
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
1094
1366
|
);
|
|
1095
|
-
|
|
1367
|
+
if (root.onConflict && this.ctx.splitWritePolicyTables.has(table)) {
|
|
1368
|
+
throw new DocumentCompileError(
|
|
1369
|
+
`table '${table}' cannot use onConflict while insert and update policies are split \u2014 use separate insert and update mutations`,
|
|
1370
|
+
"on-conflict-split-policies"
|
|
1371
|
+
);
|
|
1372
|
+
}
|
|
1373
|
+
return { ...root, pinnedColumns: pins, insertCheck: insertPolicy, returning };
|
|
1096
1374
|
}
|
|
1097
|
-
const
|
|
1375
|
+
const updatePolicy = this.update(table);
|
|
1376
|
+
const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
|
|
1098
1377
|
if (root.op === "update" || root.op === "updateByPk") {
|
|
1099
|
-
const check = predContainsExists(
|
|
1378
|
+
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
1100
1379
|
return { ...root, where, ...check, returning };
|
|
1101
1380
|
}
|
|
1381
|
+
if (root.op === "cowOverride") {
|
|
1382
|
+
const insertPolicy = this.insert(table);
|
|
1383
|
+
if (predContainsExists(insertPolicy)) {
|
|
1384
|
+
throw new DocumentCompileError(
|
|
1385
|
+
`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)`,
|
|
1386
|
+
"insert-check-unrepresentable"
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const pins = (this.ctx.pins.get(table) ?? []).map(
|
|
1390
|
+
(pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
|
|
1391
|
+
);
|
|
1392
|
+
const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
|
|
1393
|
+
return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
|
|
1394
|
+
}
|
|
1102
1395
|
return { ...root, where, returning };
|
|
1103
1396
|
}
|
|
1104
1397
|
};
|
|
@@ -1144,7 +1437,6 @@ function collectTableSet(roots, ctx) {
|
|
|
1144
1437
|
}
|
|
1145
1438
|
};
|
|
1146
1439
|
for (const root of roots) {
|
|
1147
|
-
if (root.kind === "action") continue;
|
|
1148
1440
|
tables.add(root.table);
|
|
1149
1441
|
if (root.kind === "select" || root.kind === "byPk") {
|
|
1150
1442
|
visitPred(root.where, root.table);
|
|
@@ -1155,8 +1447,10 @@ function collectTableSet(roots, ctx) {
|
|
|
1155
1447
|
if (root.nodes) visitSelection(root.nodes, root.table);
|
|
1156
1448
|
}
|
|
1157
1449
|
if (root.kind === "mutation") {
|
|
1450
|
+
if (root.cowSynced !== void 0) tables.add(root.cowSynced);
|
|
1158
1451
|
if (root.where) visitPred(root.where, root.table);
|
|
1159
1452
|
if (root.insertCheck) visitPred(root.insertCheck, root.table);
|
|
1453
|
+
if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
|
|
1160
1454
|
if (root.returning) visitSelection(root.returning, root.table);
|
|
1161
1455
|
}
|
|
1162
1456
|
}
|
|
@@ -1327,6 +1621,7 @@ function platformUserMeta() {
|
|
|
1327
1621
|
table: "users_directory",
|
|
1328
1622
|
schema: "platform",
|
|
1329
1623
|
typename: PLATFORM_USER_TYPENAME,
|
|
1624
|
+
claimsGated: false,
|
|
1330
1625
|
columns: columns.map((c) => ({ ...c })),
|
|
1331
1626
|
primaryKey: ["sub"],
|
|
1332
1627
|
links: [],
|
|
@@ -1369,15 +1664,30 @@ function buildCompileContext(rawCanonical) {
|
|
|
1369
1664
|
const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
|
|
1370
1665
|
const diagnostics = [];
|
|
1371
1666
|
const enums = new Map(canonical.enums.map((e) => [e.name, e]));
|
|
1667
|
+
const relations = canonicalRelations(canonical);
|
|
1668
|
+
const viewNames = new Set((canonical.views ?? []).map((view) => view.view));
|
|
1372
1669
|
const columnEntries = /* @__PURE__ */ new Map();
|
|
1373
1670
|
const tables = {};
|
|
1374
|
-
const usesPlatformUsers =
|
|
1671
|
+
const usesPlatformUsers = relations.some((t) => t.links.some((l) => l.kind === "toPlatformUser"));
|
|
1375
1672
|
const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
|
|
1376
1673
|
if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
|
|
1377
|
-
|
|
1674
|
+
const syncedTableNames = new Set(canonical.tables.filter((t) => t.source !== void 0).map((t) => t.table));
|
|
1675
|
+
const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
|
|
1676
|
+
const claimsGatedMemo = /* @__PURE__ */ new Map();
|
|
1677
|
+
const relationIsClaimsGated = (name) => {
|
|
1678
|
+
if (syncedTableNames.has(name)) return true;
|
|
1679
|
+
const memo = claimsGatedMemo.get(name);
|
|
1680
|
+
if (memo !== void 0) return memo;
|
|
1681
|
+
claimsGatedMemo.set(name, false);
|
|
1682
|
+
const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
|
|
1683
|
+
claimsGatedMemo.set(name, gated);
|
|
1684
|
+
return gated;
|
|
1685
|
+
};
|
|
1686
|
+
for (const table of relations) {
|
|
1687
|
+
const isView = viewNames.has(table.table);
|
|
1378
1688
|
const entries = resolveColumnEntries(table, enums, diagnostics);
|
|
1379
1689
|
columnEntries.set(table.table, entries);
|
|
1380
|
-
const pkRefs = table.primaryKey ?? ["id"];
|
|
1690
|
+
const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
|
|
1381
1691
|
const primaryKey = [];
|
|
1382
1692
|
for (const ref of pkRefs) {
|
|
1383
1693
|
const resolved = logicalColumn(entries, ref);
|
|
@@ -1398,7 +1708,7 @@ function buildCompileContext(rawCanonical) {
|
|
|
1398
1708
|
for (const column of table.columns) {
|
|
1399
1709
|
if (column.unique) {
|
|
1400
1710
|
const logical = logicalColumn(entries, column.columnName);
|
|
1401
|
-
constraints.push({ name: `${table.table}_${column.columnName}
|
|
1711
|
+
constraints.push({ name: `${table.table}_${column.columnName}_unique`, columns: [logical] });
|
|
1402
1712
|
}
|
|
1403
1713
|
}
|
|
1404
1714
|
for (const index of table.indexes) {
|
|
@@ -1410,20 +1720,23 @@ function buildCompileContext(rawCanonical) {
|
|
|
1410
1720
|
table: table.table,
|
|
1411
1721
|
schema: ormSchemaName(canonical.namespace),
|
|
1412
1722
|
typename: typeNameForTable(table.table),
|
|
1723
|
+
// Synced twins (and views reading them, resolved transitively below)
|
|
1724
|
+
// are claims-gated — see tableMetaIrSchema.claimsGated.
|
|
1725
|
+
claimsGated: relationIsClaimsGated(table.table),
|
|
1413
1726
|
columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
|
|
1414
|
-
primaryKey: primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1727
|
+
primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
|
|
1415
1728
|
links: [],
|
|
1416
1729
|
filterable: entries.map((c) => c.name),
|
|
1417
1730
|
orderable: entries.filter((c) => c.type !== "jsonb").map((c) => c.name),
|
|
1418
|
-
insertable: active.map((c) => c.name),
|
|
1419
|
-
settable: active.map((c) => c.name),
|
|
1420
|
-
appendable: active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1421
|
-
constraints,
|
|
1731
|
+
insertable: isView ? [] : active.map((c) => c.name),
|
|
1732
|
+
settable: isView ? [] : active.map((c) => c.name),
|
|
1733
|
+
appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
|
|
1734
|
+
constraints: isView ? [] : constraints,
|
|
1422
1735
|
dynamicFilterMaxDepth: 6,
|
|
1423
1736
|
dynamicFilterMaxNodes: 64
|
|
1424
1737
|
};
|
|
1425
1738
|
}
|
|
1426
|
-
for (const table of
|
|
1739
|
+
for (const table of relations) {
|
|
1427
1740
|
const meta = tables[table.table];
|
|
1428
1741
|
const entries = columnEntries.get(table.table);
|
|
1429
1742
|
for (const link of table.links) {
|
|
@@ -1458,17 +1771,17 @@ function buildCompileContext(rawCanonical) {
|
|
|
1458
1771
|
}
|
|
1459
1772
|
const readPolicies = /* @__PURE__ */ new Map();
|
|
1460
1773
|
const writePolicies = /* @__PURE__ */ new Map();
|
|
1774
|
+
const insertPolicies = /* @__PURE__ */ new Map();
|
|
1775
|
+
const updatePolicies = /* @__PURE__ */ new Map();
|
|
1461
1776
|
const pins = /* @__PURE__ */ new Map();
|
|
1777
|
+
const splitWritePolicyTables = /* @__PURE__ */ new Set();
|
|
1462
1778
|
const resolver = { tables };
|
|
1463
|
-
for (const table of
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
["write", writePolicies]
|
|
1467
|
-
]) {
|
|
1779
|
+
for (const table of relations) {
|
|
1780
|
+
const isView = viewNames.has(table.table);
|
|
1781
|
+
const compilePolicyFor = (access, store) => {
|
|
1468
1782
|
const expression = table.policies[access];
|
|
1469
1783
|
if (expression === void 0) {
|
|
1470
|
-
|
|
1471
|
-
continue;
|
|
1784
|
+
return;
|
|
1472
1785
|
}
|
|
1473
1786
|
try {
|
|
1474
1787
|
store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
|
|
@@ -1481,37 +1794,117 @@ function buildCompileContext(rawCanonical) {
|
|
|
1481
1794
|
table: table.table
|
|
1482
1795
|
});
|
|
1483
1796
|
store.set(table.table, CONST_TRUE);
|
|
1484
|
-
|
|
1797
|
+
return;
|
|
1485
1798
|
}
|
|
1486
1799
|
throw error;
|
|
1487
1800
|
}
|
|
1801
|
+
};
|
|
1802
|
+
compilePolicyFor("read", readPolicies);
|
|
1803
|
+
compilePolicyFor("write", writePolicies);
|
|
1804
|
+
compilePolicyFor("insert", insertPolicies);
|
|
1805
|
+
compilePolicyFor("update", updatePolicies);
|
|
1806
|
+
if (isView) {
|
|
1807
|
+
writePolicies.set(table.table, CONST_FALSE);
|
|
1808
|
+
insertPolicies.set(table.table, CONST_FALSE);
|
|
1809
|
+
updatePolicies.set(table.table, CONST_FALSE);
|
|
1810
|
+
pins.set(table.table, []);
|
|
1811
|
+
continue;
|
|
1812
|
+
}
|
|
1813
|
+
if (table.policies.insert !== void 0 || table.policies.update !== void 0) {
|
|
1814
|
+
splitWritePolicyTables.add(table.table);
|
|
1488
1815
|
}
|
|
1489
|
-
const
|
|
1816
|
+
const fallbackWrite = writePolicies.get(table.table);
|
|
1817
|
+
const hasSplitPolicy = table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1818
|
+
if (!insertPolicies.has(table.table)) {
|
|
1819
|
+
if (fallbackWrite !== void 0) insertPolicies.set(table.table, fallbackWrite);
|
|
1820
|
+
else if (hasSplitPolicy) insertPolicies.set(table.table, CONST_FALSE);
|
|
1821
|
+
else insertPolicies.set(table.table, CONST_TRUE);
|
|
1822
|
+
}
|
|
1823
|
+
if (!updatePolicies.has(table.table)) {
|
|
1824
|
+
if (fallbackWrite !== void 0) updatePolicies.set(table.table, fallbackWrite);
|
|
1825
|
+
else if (hasSplitPolicy) updatePolicies.set(table.table, CONST_FALSE);
|
|
1826
|
+
else updatePolicies.set(table.table, CONST_TRUE);
|
|
1827
|
+
}
|
|
1828
|
+
const tablePins = collectPinnedColumns(insertPolicies.get(table.table));
|
|
1490
1829
|
pins.set(table.table, tablePins);
|
|
1491
1830
|
const pinned = new Set(tablePins.map((pin) => pin.column));
|
|
1492
1831
|
const meta = tables[table.table];
|
|
1493
1832
|
meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
|
|
1494
|
-
meta.settable = meta.settable.filter((column) => !pinned.has(column));
|
|
1495
1833
|
}
|
|
1496
1834
|
if (platformMeta) {
|
|
1497
1835
|
readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
|
|
1498
1836
|
writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
1837
|
+
insertPolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
1838
|
+
updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
|
|
1499
1839
|
pins.set(PLATFORM_USER_TABLE, []);
|
|
1500
1840
|
}
|
|
1501
1841
|
const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
|
|
1502
1842
|
const queryRoots = /* @__PURE__ */ new Map();
|
|
1503
1843
|
const mutationRoots = /* @__PURE__ */ new Map();
|
|
1504
|
-
for (const table of
|
|
1844
|
+
for (const table of relations) {
|
|
1505
1845
|
const roots = queryRootNames(table.table);
|
|
1506
1846
|
queryRoots.set(roots.select, { kind: "select", table: table.table });
|
|
1507
|
-
|
|
1847
|
+
if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
|
|
1848
|
+
queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
|
|
1849
|
+
}
|
|
1508
1850
|
queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
|
|
1509
|
-
|
|
1851
|
+
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1852
|
+
if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
|
|
1510
1853
|
for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
|
|
1511
1854
|
mutationRoots.set(field, { op, table: table.table });
|
|
1512
1855
|
}
|
|
1513
1856
|
}
|
|
1514
1857
|
}
|
|
1858
|
+
for (const table of relations) {
|
|
1859
|
+
for (const mutation of table.cowMutations ?? []) {
|
|
1860
|
+
const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
|
|
1861
|
+
if (!hasWritePolicy) {
|
|
1862
|
+
diagnostics.push({
|
|
1863
|
+
severity: "error",
|
|
1864
|
+
code: "cow-mutation-unpoliced",
|
|
1865
|
+
message: `cow mutation '${mutation.name}' on '${table.table}' requires a write (or insert/update) policy on the overlay table`,
|
|
1866
|
+
table: table.table
|
|
1867
|
+
});
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const field = camelCase(mutation.name);
|
|
1871
|
+
if (mutationRoots.has(field) || queryRoots.has(field)) {
|
|
1872
|
+
diagnostics.push({
|
|
1873
|
+
severity: "error",
|
|
1874
|
+
code: "cow-mutation-name-collision",
|
|
1875
|
+
message: `cow mutation '${mutation.name}' on '${table.table}' collides with the generated root '${field}'`,
|
|
1876
|
+
table: table.table
|
|
1877
|
+
});
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
if (mutation.kind === "override") {
|
|
1881
|
+
const pinned = new Set((pins.get(table.table) ?? []).map((pin) => pin.column));
|
|
1882
|
+
const covered = /* @__PURE__ */ new Set([mutation.pk, ...mutation.copy, ...pinned]);
|
|
1883
|
+
const gaps = table.columns.filter((column) => {
|
|
1884
|
+
if (covered.has(column.name)) return false;
|
|
1885
|
+
const hasDefault = column.default !== void 0 || column.defaultFn !== void 0;
|
|
1886
|
+
return column.nullable === false && !hasDefault;
|
|
1887
|
+
});
|
|
1888
|
+
if (gaps.length > 0) {
|
|
1889
|
+
diagnostics.push({
|
|
1890
|
+
severity: "error",
|
|
1891
|
+
code: "cow-copy-uncovered-column",
|
|
1892
|
+
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`,
|
|
1893
|
+
table: table.table
|
|
1894
|
+
});
|
|
1895
|
+
continue;
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
mutationRoots.set(field, {
|
|
1899
|
+
op: mutation.kind === "override" ? "cowOverride" : "cowRevert",
|
|
1900
|
+
table: table.table,
|
|
1901
|
+
synced: mutation.synced,
|
|
1902
|
+
pk: mutation.pk,
|
|
1903
|
+
set: mutation.set,
|
|
1904
|
+
copyColumns: mutation.copy
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1515
1908
|
const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
|
|
1516
1909
|
return {
|
|
1517
1910
|
ctx: {
|
|
@@ -1523,7 +1916,10 @@ function buildCompileContext(rawCanonical) {
|
|
|
1523
1916
|
enumsByTypeName,
|
|
1524
1917
|
readPolicies,
|
|
1525
1918
|
writePolicies,
|
|
1526
|
-
|
|
1919
|
+
insertPolicies,
|
|
1920
|
+
updatePolicies,
|
|
1921
|
+
pins,
|
|
1922
|
+
splitWritePolicyTables
|
|
1527
1923
|
},
|
|
1528
1924
|
diagnostics
|
|
1529
1925
|
};
|
|
@@ -2075,6 +2471,9 @@ var OperationLowerer = class {
|
|
|
2075
2471
|
const alias = node.alias?.value ?? name;
|
|
2076
2472
|
const rootInfo = this.ctx.mutationRoots.get(name);
|
|
2077
2473
|
if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
|
|
2474
|
+
if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
|
|
2475
|
+
return this.lowerCowRoot(node, rootInfo);
|
|
2476
|
+
}
|
|
2078
2477
|
const { op, table } = rootInfo;
|
|
2079
2478
|
const args = argsByName(node);
|
|
2080
2479
|
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
@@ -2144,6 +2543,55 @@ var OperationLowerer = class {
|
|
|
2144
2543
|
}
|
|
2145
2544
|
}
|
|
2146
2545
|
}
|
|
2546
|
+
/**
|
|
2547
|
+
* Copy-on-write roots: flat args — the pk plus (override only) every
|
|
2548
|
+
* declared set column. Overrides are full snapshots of the set columns, so
|
|
2549
|
+
* every set argument must be supplied explicitly (pass null to clear a
|
|
2550
|
+
* nullable column); the executor generates the copy/update/delete SQL from
|
|
2551
|
+
* the structured fields, never from authored statements.
|
|
2552
|
+
*/
|
|
2553
|
+
lowerCowRoot(node, rootInfo) {
|
|
2554
|
+
const name = node.name.value;
|
|
2555
|
+
const alias = node.alias?.value ?? name;
|
|
2556
|
+
const { table } = rootInfo;
|
|
2557
|
+
if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
|
|
2558
|
+
const meta = this.meta(table);
|
|
2559
|
+
const args = argsByName(node);
|
|
2560
|
+
const pkArg = args.get(rootInfo.pk);
|
|
2561
|
+
if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
|
|
2562
|
+
const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
|
|
2563
|
+
const base = {
|
|
2564
|
+
kind: "mutation",
|
|
2565
|
+
op: rootInfo.op,
|
|
2566
|
+
alias,
|
|
2567
|
+
table,
|
|
2568
|
+
pinnedColumns: [],
|
|
2569
|
+
affectedRows: false,
|
|
2570
|
+
pk,
|
|
2571
|
+
cowSynced: rootInfo.synced,
|
|
2572
|
+
returning: this.lowerSelection(node.selectionSet, table)
|
|
2573
|
+
};
|
|
2574
|
+
if (rootInfo.op === "cowRevert") {
|
|
2575
|
+
assertKnownArgs(node, [rootInfo.pk]);
|
|
2576
|
+
return base;
|
|
2577
|
+
}
|
|
2578
|
+
assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
|
|
2579
|
+
const entries = rootInfo.set.map((column) => {
|
|
2580
|
+
const arg = args.get(column);
|
|
2581
|
+
if (!arg) {
|
|
2582
|
+
fail(
|
|
2583
|
+
`'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
|
|
2584
|
+
"missing-argument",
|
|
2585
|
+
[...rootInfo.set]
|
|
2586
|
+
);
|
|
2587
|
+
}
|
|
2588
|
+
if (!meta.settable.includes(column)) {
|
|
2589
|
+
fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
|
|
2590
|
+
}
|
|
2591
|
+
return { column, value: this.lowerValue(arg.value) };
|
|
2592
|
+
});
|
|
2593
|
+
return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
|
|
2594
|
+
}
|
|
2147
2595
|
lowerMutationResponse(selectionSet, table) {
|
|
2148
2596
|
let affectedRows = false;
|
|
2149
2597
|
let returning = null;
|
|
@@ -2619,6 +3067,13 @@ function allowedOps(type) {
|
|
|
2619
3067
|
return ["eq", "neq", "contains", "hasKey", "isNull"];
|
|
2620
3068
|
}
|
|
2621
3069
|
}
|
|
3070
|
+
var CMP_OP_TO_BOOLEXP_KEY = Object.fromEntries(
|
|
3071
|
+
Object.entries(BOOLEXP_OPS2).map(([key, op]) => [op, key])
|
|
3072
|
+
);
|
|
3073
|
+
function allowedBoolExpOperators(type) {
|
|
3074
|
+
return allowedOps(type).map((op) => CMP_OP_TO_BOOLEXP_KEY[op] ?? `_${op}`);
|
|
3075
|
+
}
|
|
3076
|
+
var ORDER_BY_DIRECTIONS = Object.keys(ORDER_DIRECTIONS2);
|
|
2622
3077
|
function isPlainObject(value) {
|
|
2623
3078
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2624
3079
|
}
|
|
@@ -2950,6 +3405,7 @@ exports.DEFAULT_LIMIT = DEFAULT_LIMIT;
|
|
|
2950
3405
|
exports.DocumentCompileError = DocumentCompileError;
|
|
2951
3406
|
exports.MAX_LIMIT = MAX_LIMIT;
|
|
2952
3407
|
exports.NamespaceCompileError = NamespaceCompileError;
|
|
3408
|
+
exports.ORDER_BY_DIRECTIONS = ORDER_BY_DIRECTIONS;
|
|
2953
3409
|
exports.OperationLowerer = OperationLowerer;
|
|
2954
3410
|
exports.PLATFORM_USER_TABLE = PLATFORM_USER_TABLE;
|
|
2955
3411
|
exports.PLATFORM_USER_TYPENAME = PLATFORM_USER_TYPENAME;
|
|
@@ -2957,6 +3413,8 @@ exports.POLICY_COMPARE_OPS = POLICY_COMPARE_OPS;
|
|
|
2957
3413
|
exports.POLICY_MAX_LINK_DEPTH = POLICY_MAX_LINK_DEPTH;
|
|
2958
3414
|
exports.PolicyCompileError = PolicyCompileError;
|
|
2959
3415
|
exports.PolicyParseError = PolicyParseError;
|
|
3416
|
+
exports.allowedBoolExpOperators = allowedBoolExpOperators;
|
|
3417
|
+
exports.allowedOps = allowedOps;
|
|
2960
3418
|
exports.andPred = andPred;
|
|
2961
3419
|
exports.assertClaimsOnlyWhen = assertClaimsOnlyWhen;
|
|
2962
3420
|
exports.buildCompileContext = buildCompileContext;
|