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