@sequenceholdings/orm 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/README.md +11 -1
  2. package/dist/authoring.cjs +1066 -59
  3. package/dist/authoring.cjs.map +1 -1
  4. package/dist/authoring.d.cts +289 -24
  5. package/dist/authoring.d.ts +289 -24
  6. package/dist/authoring.js +1056 -60
  7. package/dist/authoring.js.map +1 -1
  8. package/dist/canonical.cjs +415 -9
  9. package/dist/canonical.cjs.map +1 -1
  10. package/dist/canonical.d.cts +235 -1
  11. package/dist/canonical.d.ts +235 -1
  12. package/dist/canonical.js +411 -10
  13. package/dist/canonical.js.map +1 -1
  14. package/dist/cli.cjs +1764 -225
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.js +1765 -226
  17. package/dist/cli.js.map +1 -1
  18. package/dist/client.cjs.map +1 -1
  19. package/dist/client.d.cts +1 -1
  20. package/dist/client.d.ts +1 -1
  21. package/dist/client.js.map +1 -1
  22. package/dist/compiler.cjs +506 -54
  23. package/dist/compiler.cjs.map +1 -1
  24. package/dist/compiler.d.cts +39 -231
  25. package/dist/compiler.d.ts +39 -231
  26. package/dist/compiler.js +504 -55
  27. package/dist/compiler.js.map +1 -1
  28. package/dist/drizzle.cjs +14 -0
  29. package/dist/drizzle.cjs.map +1 -0
  30. package/dist/drizzle.d.cts +1 -0
  31. package/dist/drizzle.d.ts +1 -0
  32. package/dist/drizzle.js +3 -0
  33. package/dist/drizzle.js.map +1 -0
  34. package/dist/dynamic-input.cjs +391 -0
  35. package/dist/dynamic-input.cjs.map +1 -0
  36. package/dist/dynamic-input.d.cts +111 -0
  37. package/dist/dynamic-input.d.ts +111 -0
  38. package/dist/dynamic-input.js +380 -0
  39. package/dist/dynamic-input.js.map +1 -0
  40. package/dist/index.cjs +2223 -219
  41. package/dist/index.cjs.map +1 -1
  42. package/dist/index.d.cts +125 -9
  43. package/dist/index.d.ts +125 -9
  44. package/dist/index.js +2199 -220
  45. package/dist/index.js.map +1 -1
  46. package/dist/ir.cjs +249 -14
  47. package/dist/ir.cjs.map +1 -1
  48. package/dist/ir.d.cts +20 -80
  49. package/dist/ir.d.ts +20 -80
  50. package/dist/ir.js +249 -14
  51. package/dist/ir.js.map +1 -1
  52. package/dist/predicate-v2.cjs +1093 -0
  53. package/dist/predicate-v2.cjs.map +1 -0
  54. package/dist/predicate-v2.d.cts +139 -0
  55. package/dist/predicate-v2.d.ts +139 -0
  56. package/dist/predicate-v2.js +1083 -0
  57. package/dist/predicate-v2.js.map +1 -0
  58. package/dist/sdl.cjs +344 -55
  59. package/dist/sdl.cjs.map +1 -1
  60. package/dist/sdl.d.cts +1 -3
  61. package/dist/sdl.d.ts +1 -3
  62. package/dist/sdl.js +344 -55
  63. package/dist/sdl.js.map +1 -1
  64. package/package.json +16 -1
  65. package/templates/orm-namespace/CLAUDE.md +6 -5
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
- actions: zod.z.array(canonicalActionSchema).default([])
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
- return {
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
- links: sortByName(t.links),
140
- indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
141
- actions: sortByName(t.actions)
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
- op: zod.z.enum(["insert", "insertOne", "update", "updateByPk", "delete", "deleteByPk"]),
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", "action"]),
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
- primaryKey: zod.z.array(zod.z.string()).min(1),
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"]),
@@ -907,6 +1168,7 @@ var NamespaceCompileError = class extends Error {
907
1168
 
908
1169
  // src/compiler-gql/pred-utils.ts
909
1170
  var CONST_TRUE = { kind: "const", value: true };
1171
+ var CONST_FALSE = { kind: "const", value: false };
910
1172
  function andPred(...preds) {
911
1173
  const operands = preds.filter(
912
1174
  (pred) => pred !== void 0 && !(pred.kind === "const" && pred.value)
@@ -1000,6 +1262,12 @@ var PolicyInjector = class {
1000
1262
  write(table) {
1001
1263
  return this.ctx.writePolicies.get(table) ?? { kind: "const", value: true };
1002
1264
  }
1265
+ insert(table) {
1266
+ return this.ctx.insertPolicies.get(table) ?? this.write(table);
1267
+ }
1268
+ update(table) {
1269
+ return this.ctx.updatePolicies.get(table) ?? this.write(table);
1270
+ }
1003
1271
  linkTarget(table, link) {
1004
1272
  const meta = this.ctx.tables[table];
1005
1273
  const resolved = meta?.links.find((l) => l.name === link);
@@ -1074,31 +1342,50 @@ var PolicyInjector = class {
1074
1342
  };
1075
1343
  case "mutation":
1076
1344
  return this.injectMutation(root);
1077
- case "action":
1078
- return root;
1079
1345
  }
1080
1346
  }
1081
1347
  injectMutation(root) {
1082
1348
  const table = root.table;
1083
- const writePolicy = this.write(table);
1084
1349
  const returning = root.returning ? this.injectSelection(root.returning, table) : root.returning;
1085
1350
  if (root.op === "insert" || root.op === "insertOne") {
1086
- if (predContainsExists(writePolicy)) {
1351
+ const insertPolicy = this.insert(table);
1352
+ if (predContainsExists(insertPolicy)) {
1087
1353
  throw new DocumentCompileError(
1088
- `table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its write policy traverses a link (exists)`,
1354
+ `table '${table}' cannot be inserted into: v2.0 insert checks are claims + inserted-values only, but its insert policy traverses a link (exists)`,
1089
1355
  "insert-check-unrepresentable"
1090
1356
  );
1091
1357
  }
1092
1358
  const pins = (this.ctx.pins.get(table) ?? []).map(
1093
1359
  (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
1094
1360
  );
1095
- return { ...root, pinnedColumns: pins, insertCheck: writePolicy, returning };
1361
+ if (root.onConflict && this.ctx.splitWritePolicyTables.has(table)) {
1362
+ throw new DocumentCompileError(
1363
+ `table '${table}' cannot use onConflict while insert and update policies are split \u2014 use separate insert and update mutations`,
1364
+ "on-conflict-split-policies"
1365
+ );
1366
+ }
1367
+ return { ...root, pinnedColumns: pins, insertCheck: insertPolicy, returning };
1096
1368
  }
1097
- const where = andPred(writePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
1369
+ const updatePolicy = this.update(table);
1370
+ const where = andPred(updatePolicy, root.where ? this.injectDocPred(root.where, table) : void 0);
1098
1371
  if (root.op === "update" || root.op === "updateByPk") {
1099
- const check = predContainsExists(writePolicy) ? {} : { insertCheck: writePolicy };
1372
+ const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
1100
1373
  return { ...root, where, ...check, returning };
1101
1374
  }
1375
+ if (root.op === "cowOverride") {
1376
+ const insertPolicy = this.insert(table);
1377
+ if (predContainsExists(insertPolicy)) {
1378
+ throw new DocumentCompileError(
1379
+ `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)`,
1380
+ "insert-check-unrepresentable"
1381
+ );
1382
+ }
1383
+ const pins = (this.ctx.pins.get(table) ?? []).map(
1384
+ (pin) => ({ column: pin.column, value: { kind: "claim", key: pin.claim } })
1385
+ );
1386
+ const check = predContainsExists(updatePolicy) ? {} : { insertCheck: updatePolicy };
1387
+ return { ...root, pinnedColumns: pins, cowCopyCheck: insertPolicy, where, ...check, returning };
1388
+ }
1102
1389
  return { ...root, where, returning };
1103
1390
  }
1104
1391
  };
@@ -1144,7 +1431,6 @@ function collectTableSet(roots, ctx) {
1144
1431
  }
1145
1432
  };
1146
1433
  for (const root of roots) {
1147
- if (root.kind === "action") continue;
1148
1434
  tables.add(root.table);
1149
1435
  if (root.kind === "select" || root.kind === "byPk") {
1150
1436
  visitPred(root.where, root.table);
@@ -1155,8 +1441,10 @@ function collectTableSet(roots, ctx) {
1155
1441
  if (root.nodes) visitSelection(root.nodes, root.table);
1156
1442
  }
1157
1443
  if (root.kind === "mutation") {
1444
+ if (root.cowSynced !== void 0) tables.add(root.cowSynced);
1158
1445
  if (root.where) visitPred(root.where, root.table);
1159
1446
  if (root.insertCheck) visitPred(root.insertCheck, root.table);
1447
+ if (root.cowCopyCheck) visitPred(root.cowCopyCheck, root.table);
1160
1448
  if (root.returning) visitSelection(root.returning, root.table);
1161
1449
  }
1162
1450
  }
@@ -1327,6 +1615,7 @@ function platformUserMeta() {
1327
1615
  table: "users_directory",
1328
1616
  schema: "platform",
1329
1617
  typename: PLATFORM_USER_TYPENAME,
1618
+ claimsGated: false,
1330
1619
  columns: columns.map((c) => ({ ...c })),
1331
1620
  primaryKey: ["sub"],
1332
1621
  links: [],
@@ -1369,15 +1658,30 @@ function buildCompileContext(rawCanonical) {
1369
1658
  const canonical = normalizeCanonical(canonicalDefinitionSchema.parse(rawCanonical));
1370
1659
  const diagnostics = [];
1371
1660
  const enums = new Map(canonical.enums.map((e) => [e.name, e]));
1661
+ const relations = canonicalRelations(canonical);
1662
+ const viewNames = new Set((canonical.views ?? []).map((view) => view.view));
1372
1663
  const columnEntries = /* @__PURE__ */ new Map();
1373
1664
  const tables = {};
1374
- const usesPlatformUsers = canonical.tables.some((t) => t.links.some((l) => l.kind === "toPlatformUser"));
1665
+ const usesPlatformUsers = relations.some((t) => t.links.some((l) => l.kind === "toPlatformUser"));
1375
1666
  const platformMeta = usesPlatformUsers ? platformUserMeta() : void 0;
1376
1667
  if (platformMeta) tables[PLATFORM_USER_TABLE] = platformMeta;
1377
- for (const table of canonical.tables) {
1668
+ const syncedTableNames = new Set(canonical.tables.filter((t) => t.source !== void 0).map((t) => t.table));
1669
+ const viewDependencies = new Map((canonical.views ?? []).map((view) => [view.view, view.dependencies]));
1670
+ const claimsGatedMemo = /* @__PURE__ */ new Map();
1671
+ const relationIsClaimsGated = (name) => {
1672
+ if (syncedTableNames.has(name)) return true;
1673
+ const memo = claimsGatedMemo.get(name);
1674
+ if (memo !== void 0) return memo;
1675
+ claimsGatedMemo.set(name, false);
1676
+ const gated = (viewDependencies.get(name) ?? []).some(relationIsClaimsGated);
1677
+ claimsGatedMemo.set(name, gated);
1678
+ return gated;
1679
+ };
1680
+ for (const table of relations) {
1681
+ const isView = viewNames.has(table.table);
1378
1682
  const entries = resolveColumnEntries(table, enums, diagnostics);
1379
1683
  columnEntries.set(table.table, entries);
1380
- const pkRefs = table.primaryKey ?? ["id"];
1684
+ const pkRefs = table.primaryKey ?? (isView ? [] : ["id"]);
1381
1685
  const primaryKey = [];
1382
1686
  for (const ref of pkRefs) {
1383
1687
  const resolved = logicalColumn(entries, ref);
@@ -1398,7 +1702,7 @@ function buildCompileContext(rawCanonical) {
1398
1702
  for (const column of table.columns) {
1399
1703
  if (column.unique) {
1400
1704
  const logical = logicalColumn(entries, column.columnName);
1401
- constraints.push({ name: `${table.table}_${column.columnName}_key`, columns: [logical] });
1705
+ constraints.push({ name: `${table.table}_${column.columnName}_unique`, columns: [logical] });
1402
1706
  }
1403
1707
  }
1404
1708
  for (const index of table.indexes) {
@@ -1410,20 +1714,23 @@ function buildCompileContext(rawCanonical) {
1410
1714
  table: table.table,
1411
1715
  schema: ormSchemaName(canonical.namespace),
1412
1716
  typename: typeNameForTable(table.table),
1717
+ // Synced twins (and views reading them, resolved transitively below)
1718
+ // are claims-gated — see tableMetaIrSchema.claimsGated.
1719
+ claimsGated: relationIsClaimsGated(table.table),
1413
1720
  columns: entries.map(({ deprecated: _deprecated, ...column }) => column),
1414
- primaryKey: primaryKey.length > 0 ? primaryKey : ["id"],
1721
+ primaryKey: isView ? primaryKey : primaryKey.length > 0 ? primaryKey : ["id"],
1415
1722
  links: [],
1416
1723
  filterable: entries.map((c) => c.name),
1417
1724
  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,
1725
+ insertable: isView ? [] : active.map((c) => c.name),
1726
+ settable: isView ? [] : active.map((c) => c.name),
1727
+ appendable: isView ? [] : active.filter((c) => c.type === "jsonb").map((c) => c.name),
1728
+ constraints: isView ? [] : constraints,
1422
1729
  dynamicFilterMaxDepth: 6,
1423
1730
  dynamicFilterMaxNodes: 64
1424
1731
  };
1425
1732
  }
1426
- for (const table of canonical.tables) {
1733
+ for (const table of relations) {
1427
1734
  const meta = tables[table.table];
1428
1735
  const entries = columnEntries.get(table.table);
1429
1736
  for (const link of table.links) {
@@ -1458,17 +1765,17 @@ function buildCompileContext(rawCanonical) {
1458
1765
  }
1459
1766
  const readPolicies = /* @__PURE__ */ new Map();
1460
1767
  const writePolicies = /* @__PURE__ */ new Map();
1768
+ const insertPolicies = /* @__PURE__ */ new Map();
1769
+ const updatePolicies = /* @__PURE__ */ new Map();
1461
1770
  const pins = /* @__PURE__ */ new Map();
1771
+ const splitWritePolicyTables = /* @__PURE__ */ new Set();
1462
1772
  const resolver = { tables };
1463
- for (const table of canonical.tables) {
1464
- for (const [access, store] of [
1465
- ["read", readPolicies],
1466
- ["write", writePolicies]
1467
- ]) {
1773
+ for (const table of relations) {
1774
+ const isView = viewNames.has(table.table);
1775
+ const compilePolicyFor = (access, store) => {
1468
1776
  const expression = table.policies[access];
1469
1777
  if (expression === void 0) {
1470
- store.set(table.table, CONST_TRUE);
1471
- continue;
1778
+ return;
1472
1779
  }
1473
1780
  try {
1474
1781
  store.set(table.table, compilePolicy({ expression, table: table.table, resolver }));
@@ -1481,37 +1788,117 @@ function buildCompileContext(rawCanonical) {
1481
1788
  table: table.table
1482
1789
  });
1483
1790
  store.set(table.table, CONST_TRUE);
1484
- continue;
1791
+ return;
1485
1792
  }
1486
1793
  throw error;
1487
1794
  }
1795
+ };
1796
+ compilePolicyFor("read", readPolicies);
1797
+ compilePolicyFor("write", writePolicies);
1798
+ compilePolicyFor("insert", insertPolicies);
1799
+ compilePolicyFor("update", updatePolicies);
1800
+ if (isView) {
1801
+ writePolicies.set(table.table, CONST_FALSE);
1802
+ insertPolicies.set(table.table, CONST_FALSE);
1803
+ updatePolicies.set(table.table, CONST_FALSE);
1804
+ pins.set(table.table, []);
1805
+ continue;
1806
+ }
1807
+ if (table.policies.insert !== void 0 || table.policies.update !== void 0) {
1808
+ splitWritePolicyTables.add(table.table);
1809
+ }
1810
+ const fallbackWrite = writePolicies.get(table.table);
1811
+ const hasSplitPolicy = table.policies.insert !== void 0 || table.policies.update !== void 0;
1812
+ if (!insertPolicies.has(table.table)) {
1813
+ if (fallbackWrite !== void 0) insertPolicies.set(table.table, fallbackWrite);
1814
+ else if (hasSplitPolicy) insertPolicies.set(table.table, CONST_FALSE);
1815
+ else insertPolicies.set(table.table, CONST_TRUE);
1488
1816
  }
1489
- const tablePins = collectPinnedColumns(writePolicies.get(table.table));
1817
+ if (!updatePolicies.has(table.table)) {
1818
+ if (fallbackWrite !== void 0) updatePolicies.set(table.table, fallbackWrite);
1819
+ else if (hasSplitPolicy) updatePolicies.set(table.table, CONST_FALSE);
1820
+ else updatePolicies.set(table.table, CONST_TRUE);
1821
+ }
1822
+ const tablePins = collectPinnedColumns(insertPolicies.get(table.table));
1490
1823
  pins.set(table.table, tablePins);
1491
1824
  const pinned = new Set(tablePins.map((pin) => pin.column));
1492
1825
  const meta = tables[table.table];
1493
1826
  meta.insertable = meta.insertable.filter((column) => !pinned.has(column));
1494
- meta.settable = meta.settable.filter((column) => !pinned.has(column));
1495
1827
  }
1496
1828
  if (platformMeta) {
1497
1829
  readPolicies.set(PLATFORM_USER_TABLE, CONST_TRUE);
1498
1830
  writePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
1831
+ insertPolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
1832
+ updatePolicies.set(PLATFORM_USER_TABLE, { kind: "const", value: false });
1499
1833
  pins.set(PLATFORM_USER_TABLE, []);
1500
1834
  }
1501
1835
  const tableByTypename = new Map(Object.entries(tables).map(([key, meta]) => [meta.typename, key]));
1502
1836
  const queryRoots = /* @__PURE__ */ new Map();
1503
1837
  const mutationRoots = /* @__PURE__ */ new Map();
1504
- for (const table of canonical.tables) {
1838
+ for (const table of relations) {
1505
1839
  const roots = queryRootNames(table.table);
1506
1840
  queryRoots.set(roots.select, { kind: "select", table: table.table });
1507
- queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
1841
+ if ((tables[table.table]?.primaryKey.length ?? 0) > 0) {
1842
+ queryRoots.set(roots.byPk, { kind: "byPk", table: table.table });
1843
+ }
1508
1844
  queryRoots.set(roots.aggregate, { kind: "aggregate", table: table.table });
1509
- if (table.source === void 0) {
1845
+ const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
1846
+ if (!viewNames.has(table.table) && table.source === void 0 && hasWritePolicy) {
1510
1847
  for (const [op, field] of Object.entries(mutationRootNames(table.table))) {
1511
1848
  mutationRoots.set(field, { op, table: table.table });
1512
1849
  }
1513
1850
  }
1514
1851
  }
1852
+ for (const table of relations) {
1853
+ for (const mutation of table.cowMutations ?? []) {
1854
+ const hasWritePolicy = table.policies.write !== void 0 || table.policies.insert !== void 0 || table.policies.update !== void 0;
1855
+ if (!hasWritePolicy) {
1856
+ diagnostics.push({
1857
+ severity: "error",
1858
+ code: "cow-mutation-unpoliced",
1859
+ message: `cow mutation '${mutation.name}' on '${table.table}' requires a write (or insert/update) policy on the overlay table`,
1860
+ table: table.table
1861
+ });
1862
+ continue;
1863
+ }
1864
+ const field = camelCase(mutation.name);
1865
+ if (mutationRoots.has(field) || queryRoots.has(field)) {
1866
+ diagnostics.push({
1867
+ severity: "error",
1868
+ code: "cow-mutation-name-collision",
1869
+ message: `cow mutation '${mutation.name}' on '${table.table}' collides with the generated root '${field}'`,
1870
+ table: table.table
1871
+ });
1872
+ continue;
1873
+ }
1874
+ if (mutation.kind === "override") {
1875
+ const pinned = new Set((pins.get(table.table) ?? []).map((pin) => pin.column));
1876
+ const covered = /* @__PURE__ */ new Set([mutation.pk, ...mutation.copy, ...pinned]);
1877
+ const gaps = table.columns.filter((column) => {
1878
+ if (covered.has(column.name)) return false;
1879
+ const hasDefault = column.default !== void 0 || column.defaultFn !== void 0;
1880
+ return column.nullable === false && !hasDefault;
1881
+ });
1882
+ if (gaps.length > 0) {
1883
+ diagnostics.push({
1884
+ severity: "error",
1885
+ code: "cow-copy-uncovered-column",
1886
+ 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`,
1887
+ table: table.table
1888
+ });
1889
+ continue;
1890
+ }
1891
+ }
1892
+ mutationRoots.set(field, {
1893
+ op: mutation.kind === "override" ? "cowOverride" : "cowRevert",
1894
+ table: table.table,
1895
+ synced: mutation.synced,
1896
+ pk: mutation.pk,
1897
+ set: mutation.set,
1898
+ copyColumns: mutation.copy
1899
+ });
1900
+ }
1901
+ }
1515
1902
  const enumsByTypeName = new Map(canonical.enums.map((e) => [enumTypeName(e.name), e]));
1516
1903
  return {
1517
1904
  ctx: {
@@ -1523,7 +1910,10 @@ function buildCompileContext(rawCanonical) {
1523
1910
  enumsByTypeName,
1524
1911
  readPolicies,
1525
1912
  writePolicies,
1526
- pins
1913
+ insertPolicies,
1914
+ updatePolicies,
1915
+ pins,
1916
+ splitWritePolicyTables
1527
1917
  },
1528
1918
  diagnostics
1529
1919
  };
@@ -2075,6 +2465,9 @@ var OperationLowerer = class {
2075
2465
  const alias = node.alias?.value ?? name;
2076
2466
  const rootInfo = this.ctx.mutationRoots.get(name);
2077
2467
  if (!rootInfo) fail(`unknown mutation root '${name}'`, "unknown-root", [...this.ctx.mutationRoots.keys()]);
2468
+ if (rootInfo.op === "cowOverride" || rootInfo.op === "cowRevert") {
2469
+ return this.lowerCowRoot(node, rootInfo);
2470
+ }
2078
2471
  const { op, table } = rootInfo;
2079
2472
  const args = argsByName(node);
2080
2473
  if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
@@ -2144,6 +2537,55 @@ var OperationLowerer = class {
2144
2537
  }
2145
2538
  }
2146
2539
  }
2540
+ /**
2541
+ * Copy-on-write roots: flat args — the pk plus (override only) every
2542
+ * declared set column. Overrides are full snapshots of the set columns, so
2543
+ * every set argument must be supplied explicitly (pass null to clear a
2544
+ * nullable column); the executor generates the copy/update/delete SQL from
2545
+ * the structured fields, never from authored statements.
2546
+ */
2547
+ lowerCowRoot(node, rootInfo) {
2548
+ const name = node.name.value;
2549
+ const alias = node.alias?.value ?? name;
2550
+ const { table } = rootInfo;
2551
+ if (!node.selectionSet) fail(`'${name}' needs a selection set`, "invalid-selection");
2552
+ const meta = this.meta(table);
2553
+ const args = argsByName(node);
2554
+ const pkArg = args.get(rootInfo.pk);
2555
+ if (!pkArg) fail(`'${name}' is missing primary-key argument '${rootInfo.pk}'`, "missing-pk-argument", [rootInfo.pk]);
2556
+ const pk = [{ column: rootInfo.pk, value: this.lowerValue(pkArg.value) }];
2557
+ const base = {
2558
+ kind: "mutation",
2559
+ op: rootInfo.op,
2560
+ alias,
2561
+ table,
2562
+ pinnedColumns: [],
2563
+ affectedRows: false,
2564
+ pk,
2565
+ cowSynced: rootInfo.synced,
2566
+ returning: this.lowerSelection(node.selectionSet, table)
2567
+ };
2568
+ if (rootInfo.op === "cowRevert") {
2569
+ assertKnownArgs(node, [rootInfo.pk]);
2570
+ return base;
2571
+ }
2572
+ assertKnownArgs(node, [rootInfo.pk, ...rootInfo.set]);
2573
+ const entries = rootInfo.set.map((column) => {
2574
+ const arg = args.get(column);
2575
+ if (!arg) {
2576
+ fail(
2577
+ `'${name}' is missing set argument '${column}' \u2014 a cow override writes every declared set column (pass null to clear a nullable one)`,
2578
+ "missing-argument",
2579
+ [...rootInfo.set]
2580
+ );
2581
+ }
2582
+ if (!meta.settable.includes(column)) {
2583
+ fail(`column '${table}.${column}' is not updatable (pinned by the write policy or deprecated)`, "column-not-settable", meta.settable);
2584
+ }
2585
+ return { column, value: this.lowerValue(arg.value) };
2586
+ });
2587
+ return { ...base, set: { entries }, cowCopyColumns: [...rootInfo.copyColumns] };
2588
+ }
2147
2589
  lowerMutationResponse(selectionSet, table) {
2148
2590
  let affectedRows = false;
2149
2591
  let returning = null;
@@ -2619,6 +3061,13 @@ function allowedOps(type) {
2619
3061
  return ["eq", "neq", "contains", "hasKey", "isNull"];
2620
3062
  }
2621
3063
  }
3064
+ var CMP_OP_TO_BOOLEXP_KEY = Object.fromEntries(
3065
+ Object.entries(BOOLEXP_OPS2).map(([key, op]) => [op, key])
3066
+ );
3067
+ function allowedBoolExpOperators(type) {
3068
+ return allowedOps(type).map((op) => CMP_OP_TO_BOOLEXP_KEY[op] ?? `_${op}`);
3069
+ }
3070
+ var ORDER_BY_DIRECTIONS = Object.keys(ORDER_DIRECTIONS2);
2622
3071
  function isPlainObject(value) {
2623
3072
  return typeof value === "object" && value !== null && !Array.isArray(value);
2624
3073
  }
@@ -2950,6 +3399,7 @@ exports.DEFAULT_LIMIT = DEFAULT_LIMIT;
2950
3399
  exports.DocumentCompileError = DocumentCompileError;
2951
3400
  exports.MAX_LIMIT = MAX_LIMIT;
2952
3401
  exports.NamespaceCompileError = NamespaceCompileError;
3402
+ exports.ORDER_BY_DIRECTIONS = ORDER_BY_DIRECTIONS;
2953
3403
  exports.OperationLowerer = OperationLowerer;
2954
3404
  exports.PLATFORM_USER_TABLE = PLATFORM_USER_TABLE;
2955
3405
  exports.PLATFORM_USER_TYPENAME = PLATFORM_USER_TYPENAME;
@@ -2957,6 +3407,8 @@ exports.POLICY_COMPARE_OPS = POLICY_COMPARE_OPS;
2957
3407
  exports.POLICY_MAX_LINK_DEPTH = POLICY_MAX_LINK_DEPTH;
2958
3408
  exports.PolicyCompileError = PolicyCompileError;
2959
3409
  exports.PolicyParseError = PolicyParseError;
3410
+ exports.allowedBoolExpOperators = allowedBoolExpOperators;
3411
+ exports.allowedOps = allowedOps;
2960
3412
  exports.andPred = andPred;
2961
3413
  exports.assertClaimsOnlyWhen = assertClaimsOnlyWhen;
2962
3414
  exports.buildCompileContext = buildCompileContext;