@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/sdl.cjs CHANGED
@@ -76,8 +76,33 @@ var policyExpressionSchema = zod.z.union([
76
76
  ]);
77
77
  var canonicalPoliciesSchema = zod.z.object({
78
78
  read: policyExpressionSchema.optional(),
79
- write: policyExpressionSchema.optional()
79
+ write: policyExpressionSchema.optional(),
80
+ insert: policyExpressionSchema.optional(),
81
+ update: policyExpressionSchema.optional()
80
82
  });
83
+ function isSingleViewQuerySql(value) {
84
+ const query = value.trim();
85
+ if (!/^\(*(?:select|with)\b/i.test(query) || query.includes("\0")) return false;
86
+ let state = "normal";
87
+ for (let index = 0; index < query.length; index += 1) {
88
+ const char = query[index];
89
+ const next = query[index + 1];
90
+ if (state === "single") {
91
+ if (char === "'" && next === "'") index += 1;
92
+ else if (char === "'") state = "normal";
93
+ continue;
94
+ }
95
+ if (state === "double") {
96
+ if (char === '"' && next === '"') index += 1;
97
+ else if (char === '"') state = "normal";
98
+ continue;
99
+ }
100
+ if (char === "'") state = "single";
101
+ else if (char === '"') state = "double";
102
+ else if (char === ";" || char === "-" && next === "-" || char === "/" && next === "*") return false;
103
+ }
104
+ return state === "normal" && !/\$[a-z_0-9]*\$/i.test(query);
105
+ }
81
106
  var canonicalActionSchema = zod.z.object({
82
107
  name: identifier,
83
108
  requires: zod.z.string().max(1e3).optional(),
@@ -89,6 +114,37 @@ var canonicalActionSchema = zod.z.object({
89
114
  returns: zod.z.enum(["rows", "rowsAffected", "scalar"]).default("rowsAffected"),
90
115
  audit: zod.z.object({ action: zod.z.string(), resource: zod.z.string() }).optional()
91
116
  });
117
+ var canonicalCowMutationSchema = zod.z.object({
118
+ kind: zod.z.enum(["override", "revert"]),
119
+ name: identifier,
120
+ /** Synced table (same namespace) the overlay copies from / falls back to. */
121
+ synced: identifier,
122
+ /** Single-column primary key PROPERTY (TS camelCase, like column `name`) shared by overlay and synced. */
123
+ pk: zod.z.string().min(1).max(63),
124
+ /** Override: contract properties copied INSERT…SELECT from synced (pk included). Empty for revert. */
125
+ copy: zod.z.array(zod.z.string().min(1).max(63)).default([]),
126
+ /** Override: overlay properties written by the UPDATE (never the pk). Empty for revert. */
127
+ set: zod.z.array(zod.z.string().min(1).max(63)).default([])
128
+ }).superRefine((mutation, ctx) => {
129
+ if (mutation.kind === "override") {
130
+ if (mutation.set.length === 0) {
131
+ ctx.addIssue({ code: "custom", path: ["set"], message: "override mutations must set at least one column" });
132
+ }
133
+ if (!mutation.copy.includes(mutation.pk)) {
134
+ ctx.addIssue({ code: "custom", path: ["copy"], message: "override copy list must include the pk" });
135
+ }
136
+ } else if (mutation.set.length > 0 || mutation.copy.length > 0) {
137
+ ctx.addIssue({ code: "custom", path: ["set"], message: "revert mutations take no copy/set columns" });
138
+ }
139
+ if (mutation.set.includes(mutation.pk)) {
140
+ ctx.addIssue({ code: "custom", path: ["set"], message: "set must not include the pk \u2014 identity columns are pinned" });
141
+ }
142
+ for (const [field, values] of [["copy", mutation.copy], ["set", mutation.set]]) {
143
+ if (new Set(values).size !== values.length) {
144
+ ctx.addIssue({ code: "custom", path: [field], message: `${field} must not contain duplicate columns` });
145
+ }
146
+ }
147
+ });
92
148
  var canonicalTableSchema = zod.z.object({
93
149
  table: identifier,
94
150
  description: zod.z.string().max(2e3).optional(),
@@ -101,7 +157,45 @@ var canonicalTableSchema = zod.z.object({
101
157
  links: zod.z.array(canonicalLinkSchema).default([]),
102
158
  indexes: zod.z.array(canonicalIndexSchema).default([]),
103
159
  policies: canonicalPoliciesSchema.default({}),
104
- actions: zod.z.array(canonicalActionSchema).default([])
160
+ /**
161
+ * v1-conversion carry-over only. v2 has no action surface — the runtime
162
+ * neither compiles nor executes these; writes are mutations. Kept parseable
163
+ * so stored definitions and their content hashes stay stable.
164
+ */
165
+ actions: zod.z.array(canonicalActionSchema).default([]),
166
+ /** Optional (not defaulted) so pre-CoW canonical documents keep byte-identical hashes. */
167
+ cowMutations: zod.z.array(canonicalCowMutationSchema).optional()
168
+ });
169
+ var canonicalViewSchema = zod.z.object({
170
+ view: identifier,
171
+ description: zod.z.string().max(2e3).optional(),
172
+ columns: zod.z.array(canonicalColumnSchema).min(1),
173
+ /** Logical identity for GraphQL only; PostgreSQL views have no PK constraint. */
174
+ primaryKey: zod.z.array(identifier).min(1).max(8).optional(),
175
+ links: zod.z.array(canonicalLinkSchema).default([]),
176
+ policies: zod.z.object({ read: policyExpressionSchema }).strict(),
177
+ query: zod.z.string().min(1).max(1e5).refine(isSingleViewQuerySql, {
178
+ message: "view query must be one SELECT/WITH statement without comments or dollar-quoted bodies"
179
+ }),
180
+ /** Namespace-local logical relation names, sorted for deterministic hashing. */
181
+ dependencies: zod.z.array(identifier).max(500)
182
+ }).superRefine((view, ctx) => {
183
+ for (const [index, column] of view.columns.entries()) {
184
+ if (column.default !== void 0 || column.defaultFn !== void 0) {
185
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare a default` });
186
+ }
187
+ if (column.unique) {
188
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare unique` });
189
+ }
190
+ if (column.renamedFrom !== void 0 || column.cast !== void 0) {
191
+ ctx.addIssue({ code: "custom", path: ["columns", index], message: `view column '${column.name}' cannot declare migration hints` });
192
+ }
193
+ }
194
+ for (const [index, link] of view.links.entries()) {
195
+ if (link.enforce) {
196
+ ctx.addIssue({ code: "custom", path: ["links", index, "enforce"], message: `view link '${link.name}' cannot enforce a foreign key` });
197
+ }
198
+ }
105
199
  });
106
200
  var canonicalDefinitionSchema = zod.z.object({
107
201
  definition_format: zod.z.literal(CANONICAL_FORMAT),
@@ -109,24 +203,174 @@ var canonicalDefinitionSchema = zod.z.object({
109
203
  description: zod.z.string().max(2e3).optional(),
110
204
  enums: zod.z.array(canonicalEnumSchema).default([]),
111
205
  tables: zod.z.array(canonicalTableSchema).min(0).max(500),
206
+ /** Optional to preserve byte-identical hashes for pre-view canonical v2 documents. */
207
+ views: zod.z.array(canonicalViewSchema).max(500).optional(),
112
208
  /** Verbatim custom-roles grammar (@sequenceholdings/custom-roles-manifest). */
113
209
  capabilities: zod.z.object({
114
210
  roles: zod.z.unknown().optional(),
115
211
  uses: zod.z.array(zod.z.string()).default([])
116
212
  }).default({ uses: [] })
213
+ }).superRefine((definition, ctx) => {
214
+ const seen = /* @__PURE__ */ new Set();
215
+ for (const [index, table] of definition.tables.entries()) {
216
+ if (seen.has(table.table)) {
217
+ ctx.addIssue({ code: "custom", path: ["tables", index, "table"], message: `duplicate relation '${table.table}'` });
218
+ }
219
+ seen.add(table.table);
220
+ }
221
+ const tableByName = new Map(definition.tables.map((table) => [table.table, table]));
222
+ for (const [tableIndex, table] of definition.tables.entries()) {
223
+ const cowMutations = table.cowMutations ?? [];
224
+ if (cowMutations.length === 0) continue;
225
+ const path = (index, ...rest) => ["tables", tableIndex, "cowMutations", index, ...rest];
226
+ if (table.source !== void 0) {
227
+ ctx.addIssue({ code: "custom", path: path(0), message: `synced table '${table.table}' cannot declare cow mutations \u2014 it is read-only` });
228
+ continue;
229
+ }
230
+ for (const [index, mutation] of cowMutations.entries()) {
231
+ const synced = tableByName.get(mutation.synced);
232
+ if (!synced || synced.source === void 0) {
233
+ ctx.addIssue({
234
+ code: "custom",
235
+ path: path(index, "synced"),
236
+ message: `cow mutation '${mutation.name}' references '${mutation.synced}', which is not a synced table in this namespace`
237
+ });
238
+ continue;
239
+ }
240
+ const overlayColumnsByName = new Map(table.columns.map((column) => [column.name, column]));
241
+ const syncedColumnsByName = new Map(synced.columns.map((column) => [column.name, column]));
242
+ let columnsExist = true;
243
+ for (const property of [mutation.pk, ...mutation.copy, ...mutation.set]) {
244
+ if (!overlayColumnsByName.has(property)) {
245
+ columnsExist = false;
246
+ ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on overlay '${table.table}'` });
247
+ }
248
+ }
249
+ for (const property of [mutation.pk, ...mutation.copy]) {
250
+ if (!syncedColumnsByName.has(property)) {
251
+ columnsExist = false;
252
+ ctx.addIssue({ code: "custom", path: path(index), message: `cow mutation '${mutation.name}': column '${property}' does not exist on synced '${mutation.synced}'` });
253
+ }
254
+ }
255
+ if (!columnsExist) continue;
256
+ const overlayPk = table.primaryKey ?? [];
257
+ const syncedPk = synced.primaryKey ?? [];
258
+ if (overlayPk.length !== 1 || overlayPk[0] !== mutation.pk || syncedPk.length !== 1 || syncedPk[0] !== mutation.pk) {
259
+ ctx.addIssue({
260
+ code: "custom",
261
+ path: path(index, "pk"),
262
+ message: `cow mutation '${mutation.name}': pk '${mutation.pk}' must be the single-column primary key on both '${table.table}' and '${mutation.synced}'`
263
+ });
264
+ }
265
+ for (const property of [mutation.pk, ...mutation.copy]) {
266
+ const overlayColumn = overlayColumnsByName.get(property);
267
+ const syncedColumn = syncedColumnsByName.get(property);
268
+ if (!overlayColumn || !syncedColumn) continue;
269
+ if (JSON.stringify(overlayColumn.type) !== JSON.stringify(syncedColumn.type)) {
270
+ ctx.addIssue({
271
+ code: "custom",
272
+ path: path(index),
273
+ message: `cow mutation '${mutation.name}': column '${property}' must have the same type on overlay and synced`
274
+ });
275
+ }
276
+ if (overlayColumn.columnName !== syncedColumn.columnName) {
277
+ ctx.addIssue({
278
+ code: "custom",
279
+ path: path(index),
280
+ message: `cow mutation '${mutation.name}': column '${property}' maps to different Postgres names on overlay ('${overlayColumn.columnName}') and synced ('${syncedColumn.columnName}')`
281
+ });
282
+ }
283
+ if (syncedColumn.nullable !== false && overlayColumn.nullable === false) {
284
+ ctx.addIssue({
285
+ code: "custom",
286
+ path: path(index),
287
+ 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`
288
+ });
289
+ }
290
+ }
291
+ }
292
+ }
293
+ for (const [index, view] of (definition.views ?? []).entries()) {
294
+ if (seen.has(view.view)) {
295
+ ctx.addIssue({ code: "custom", path: ["views", index, "view"], message: `duplicate relation '${view.view}'` });
296
+ }
297
+ seen.add(view.view);
298
+ }
299
+ const views = definition.views ?? [];
300
+ const viewByName = new Map(views.map((view) => [view.view, view]));
301
+ for (const [viewIndex, view] of views.entries()) {
302
+ for (const [dependencyIndex, dependency] of view.dependencies.entries()) {
303
+ if (!seen.has(dependency)) {
304
+ ctx.addIssue({
305
+ code: "custom",
306
+ path: ["views", viewIndex, "dependencies", dependencyIndex],
307
+ message: `view '${view.view}' depends on unknown relation '${dependency}'`
308
+ });
309
+ }
310
+ }
311
+ }
312
+ const visiting = /* @__PURE__ */ new Set();
313
+ const visited = /* @__PURE__ */ new Set();
314
+ let cycleReported = false;
315
+ const visit = (name, path) => {
316
+ if (visited.has(name) || cycleReported) return;
317
+ if (visiting.has(name)) {
318
+ cycleReported = true;
319
+ ctx.addIssue({
320
+ code: "custom",
321
+ path: ["views"],
322
+ message: `view dependencies form a cycle: ${[...path, name].join(" -> ")}`
323
+ });
324
+ return;
325
+ }
326
+ visiting.add(name);
327
+ const view = viewByName.get(name);
328
+ for (const dependency of view?.dependencies ?? []) {
329
+ if (viewByName.has(dependency)) visit(dependency, [...path, name]);
330
+ }
331
+ visiting.delete(name);
332
+ visited.add(name);
333
+ };
334
+ for (const view of views) visit(view.view, []);
117
335
  });
336
+ function canonicalRelations(definition) {
337
+ const views = (definition.views ?? []).map((view) => ({
338
+ table: view.view,
339
+ ...view.description !== void 0 ? { description: view.description } : {},
340
+ columns: view.columns,
341
+ ...view.primaryKey !== void 0 ? { primaryKey: view.primaryKey } : {},
342
+ links: view.links,
343
+ indexes: [],
344
+ policies: { read: view.policies.read },
345
+ actions: []
346
+ }));
347
+ return [...definition.tables, ...views].sort((a, b) => a.table.localeCompare(b.table));
348
+ }
118
349
  function normalizeCanonical(definition) {
119
350
  const sortByName = (items) => [...items].sort((a, b) => a.name.localeCompare(b.name));
120
- return {
351
+ const normalized = {
121
352
  ...definition,
122
353
  enums: sortByName(definition.enums),
123
- tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => ({
124
- ...t,
125
- links: sortByName(t.links),
126
- indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
127
- actions: sortByName(t.actions)
128
- }))
354
+ tables: [...definition.tables].sort((a, b) => a.table.localeCompare(b.table)).map((t) => {
355
+ const { cowMutations, ...rest } = t;
356
+ return {
357
+ ...rest,
358
+ links: sortByName(t.links),
359
+ indexes: [...t.indexes].sort((a, b) => (a.name ?? a.columns.join("_")).localeCompare(b.name ?? b.columns.join("_"))),
360
+ actions: sortByName(t.actions),
361
+ // Omitted entirely when absent/empty so pre-CoW documents hash unchanged.
362
+ ...cowMutations !== void 0 && cowMutations.length > 0 ? { cowMutations: sortByName(cowMutations) } : {}
363
+ };
364
+ })
129
365
  };
366
+ if (definition.views !== void 0) {
367
+ normalized.views = [...definition.views].sort((a, b) => a.view.localeCompare(b.view)).map((view) => ({
368
+ ...view,
369
+ links: sortByName(view.links),
370
+ dependencies: [...view.dependencies].sort()
371
+ }));
372
+ }
373
+ return normalized;
130
374
  }
131
375
 
132
376
  // src/sdl/naming.ts
@@ -259,15 +503,15 @@ var PLATFORM_USER_FIELDS = [
259
503
  ];
260
504
  function generateSdl(canonical, options = {}) {
261
505
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
262
- const tables = def.tables.filter((table) => table.source === void 0);
506
+ const tables = canonicalRelations(def).filter((table) => table.source === void 0);
263
507
  if (tables.length === 0) {
264
508
  throw new SdlGenerationError(
265
- `Namespace "${def.namespace}" exposes no tables (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
509
+ `Namespace "${def.namespace}" exposes no relations (synced tables are excluded from SDL in v2.0) \u2014 nothing to generate.`
266
510
  );
267
511
  }
268
512
  for (const table of tables) {
269
513
  if (table.columns.length === 0) {
270
- throw new SdlGenerationError(`Table "${table.table}" has no columns \u2014 cannot generate an object type.`);
514
+ throw new SdlGenerationError(`Relation "${table.table}" has no columns \u2014 cannot generate an object type.`);
271
515
  }
272
516
  }
273
517
  const namesByTable = new Map(tables.map((table) => [table.table, tableNames(table.table)]));
@@ -447,10 +691,20 @@ function renderTableBlocks(ctx) {
447
691
  ${fields}
448
692
  }`);
449
693
  }
450
- if (isWritable(table)) blocks.push(...renderWriteBlocks(ctx));
694
+ if (allowsInsert(table) || allowsUpdate(table)) {
695
+ blocks.push(...renderWriteBlocks(ctx, { insert: allowsInsert(table), update: allowsUpdate(table) }));
696
+ }
451
697
  return blocks;
452
698
  }
453
- function isWritable(table) {
699
+ function allowsInsert(table) {
700
+ return table.policies.write !== void 0 || table.policies.insert !== void 0;
701
+ }
702
+ function allowsUpdate(table) {
703
+ return table.policies.write !== void 0 || table.policies.update !== void 0;
704
+ }
705
+ function allowsOnConflict(table) {
706
+ if (!allowsInsert(table) || !allowsUpdate(table)) return false;
707
+ if (table.policies.insert !== void 0 || table.policies.update !== void 0) return false;
454
708
  return table.policies.write !== void 0;
455
709
  }
456
710
  function renderObjectType(ctx) {
@@ -582,46 +836,50 @@ function constraintValues(table) {
582
836
  const values = /* @__PURE__ */ new Set();
583
837
  if (table.primaryKey && table.primaryKey.length > 0) values.add(`${table.table}_pkey`);
584
838
  for (const column of table.columns) {
585
- if (column.unique) values.add(`${table.table}_${column.columnName}_key`);
839
+ if (column.unique) values.add(`${table.table}_${column.columnName}_unique`);
586
840
  }
587
841
  for (const index of table.indexes) {
588
842
  if (index.unique) values.add(index.name ?? `${table.table}_${index.columns.join("_")}_key`);
589
843
  }
590
844
  return [...values].sort();
591
845
  }
592
- function renderWriteBlocks(ctx) {
846
+ function renderWriteBlocks(ctx, access) {
593
847
  const { table, names, pinned } = ctx;
594
848
  const blocks = [];
595
- const pinnedSet = new Set(pinned);
596
- const insertable = table.columns.filter(
597
- (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
598
- );
599
- if (insertable.length === 0) {
600
- throw new SdlGenerationError(
601
- `Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
849
+ if (access.insert) {
850
+ const pinnedSet = new Set(pinned);
851
+ const insertable = table.columns.filter(
852
+ (column) => !pinnedSet.has(column.name) && !pinnedSet.has(column.columnName)
602
853
  );
603
- }
604
- const insertLines = insertable.map(
605
- (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
606
- );
607
- blocks.push(`input ${names.insertInput} {
854
+ if (insertable.length === 0) {
855
+ throw new SdlGenerationError(
856
+ `Table "${table.table}" has no insertable columns after excluding pinned columns [${pinned.join(", ")}].`
857
+ );
858
+ }
859
+ const insertLines = insertable.map(
860
+ (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
861
+ );
862
+ blocks.push(`input ${names.insertInput} {
608
863
  ${insertLines.join("\n")}
609
864
  }`);
610
- const setLines = table.columns.map(
611
- (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
612
- );
613
- blocks.push(`input ${names.setInput} {
865
+ }
866
+ if (access.update) {
867
+ const setLines = table.columns.map(
868
+ (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
869
+ );
870
+ blocks.push(`input ${names.setInput} {
614
871
  ${setLines.join("\n")}
615
872
  }`);
873
+ }
616
874
  const jsonb = jsonbColumns(table);
617
- if (jsonb.length > 0) {
875
+ if (access.update && jsonb.length > 0) {
618
876
  const appendLines = jsonb.map((column) => ` ${fieldName(column.name)}: jsonb`);
619
877
  blocks.push(`input ${names.appendInput} {
620
878
  ${appendLines.join("\n")}
621
879
  }`);
622
880
  }
623
881
  const numericish = numericishColumns(table);
624
- if (numericish.length > 0) {
882
+ if (access.update && numericish.length > 0) {
625
883
  const incLines = numericish.map(
626
884
  (column) => ` ${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}`
627
885
  );
@@ -630,7 +888,7 @@ ${incLines.join("\n")}
630
888
  }`);
631
889
  }
632
890
  const constraints = constraintValues(table);
633
- if (constraints.length > 0) {
891
+ if (access.insert && allowsOnConflict(table) && constraints.length > 0) {
634
892
  for (const value of constraints) assertEnumValue(value, `constraint enum for table "${table.table}"`);
635
893
  blocks.push(renderEnum(names.constraintEnum, constraints));
636
894
  const updateColumns = table.columns.map((column) => fieldName(column.name));
@@ -643,12 +901,14 @@ ${incLines.join("\n")}
643
901
  }`
644
902
  );
645
903
  }
646
- blocks.push(
647
- `type ${names.mutationResponse} {
904
+ if (access.insert || access.update) {
905
+ blocks.push(
906
+ `type ${names.mutationResponse} {
648
907
  affectedRows: Int!
649
908
  returning: [${names.typename}!]!
650
909
  }`
651
- );
910
+ );
911
+ }
652
912
  return blocks;
653
913
  }
654
914
  function renderQueryRoot(tables, namesByTable) {
@@ -673,35 +933,64 @@ function columnGqlTypeOf(table, column) {
673
933
  function renderMutationRoot(tables, namesByTable) {
674
934
  const lines = [];
675
935
  for (const table of tables) {
676
- if (!isWritable(table)) continue;
936
+ const canInsert = allowsInsert(table);
937
+ const canUpdate = allowsUpdate(table);
938
+ if (!canInsert && !canUpdate) continue;
677
939
  const names = namesByTable.get(table.table);
678
940
  if (!names) continue;
679
941
  const constraints = constraintValues(table);
680
- const onConflictArg = constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
942
+ const onConflictArg = canInsert && allowsOnConflict(table) && constraints.length > 0 ? `, onConflict: ${names.onConflict}` : "";
681
943
  const updateArgs = [`_set: ${names.setInput}`];
682
944
  if (jsonbColumns(table).length > 0) updateArgs.push(`_append: ${names.appendInput}`);
683
945
  if (numericishColumns(table).length > 0) updateArgs.push(`_inc: ${names.incInput}`);
684
946
  const updateArgList = updateArgs.join(", ");
685
- lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
686
- lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
687
- lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
688
- const pk = primaryKeyColumns(table);
689
- if (pk.length > 0) {
690
- lines.push(
691
- ` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
692
- );
947
+ if (canInsert) {
948
+ lines.push(` ${names.insertRoot}(objects: [${names.insertInput}!]!${onConflictArg}): ${names.mutationResponse}`);
949
+ lines.push(` ${names.insertOneRoot}(object: ${names.insertInput}!${onConflictArg}): ${names.typename}`);
693
950
  }
694
- lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
695
- if (pk.length > 0) {
696
- const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
697
- lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
951
+ if (canUpdate) {
952
+ lines.push(` ${names.updateRoot}(where: ${names.boolExp}!, ${updateArgList}): ${names.mutationResponse}`);
953
+ const pk = primaryKeyColumns(table);
954
+ if (pk.length > 0) {
955
+ lines.push(
956
+ ` ${names.updateByPkRoot}(pkColumns: ${names.pkColumnsInput}!, ${updateArgList}): ${names.typename}`
957
+ );
958
+ }
959
+ lines.push(` ${names.deleteRoot}(where: ${names.boolExp}!): ${names.mutationResponse}`);
960
+ if (pk.length > 0) {
961
+ const args = pk.map((column) => `${fieldName(column.name)}: ${columnGqlTypeOf(table, column)}!`).join(", ");
962
+ lines.push(` ${names.deleteByPkRoot}(${args}): ${names.typename}`);
963
+ }
698
964
  }
965
+ lines.push(...renderCowMutationFields(table, names));
699
966
  }
700
967
  if (lines.length === 0) return null;
701
968
  return `type Mutation {
702
969
  ${lines.join("\n")}
703
970
  }`;
704
971
  }
972
+ function renderCowMutationFields(table, names) {
973
+ const lines = [];
974
+ const columnByName = new Map(table.columns.map((column) => [column.name, column]));
975
+ for (const mutation of table.cowMutations ?? []) {
976
+ const pkColumn = columnByName.get(mutation.pk);
977
+ if (!pkColumn) continue;
978
+ const pkArg = `${fieldName(mutation.pk)}: ${columnGqlTypeOf(table, pkColumn)}!`;
979
+ if (mutation.kind === "revert") {
980
+ lines.push(` ${fieldName(mutation.name)}(${pkArg}): ${names.typename}`);
981
+ continue;
982
+ }
983
+ const setArgs = mutation.set.map((property) => {
984
+ const column = columnByName.get(property);
985
+ if (!column) return null;
986
+ const bang = column.nullable === false ? "!" : "";
987
+ return `${fieldName(property)}: ${columnGqlTypeOf(table, column)}${bang}`;
988
+ });
989
+ if (setArgs.some((arg) => arg === null)) continue;
990
+ lines.push(` ${fieldName(mutation.name)}(${pkArg}, ${setArgs.join(", ")}): ${names.typename}`);
991
+ }
992
+ return lines;
993
+ }
705
994
  function stableJson(value) {
706
995
  if (value === null || typeof value !== "object") return JSON.stringify(value);
707
996
  if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
@@ -727,7 +1016,7 @@ var manifestRequiresSchema = zod.z.object({
727
1016
  }).nullable();
728
1017
  var manifestOperationSchema = zod.z.object({
729
1018
  hash: zod.z.string().regex(OP_HASH_RE),
730
- kind: zod.z.enum(["query", "mutation", "action"]),
1019
+ kind: zod.z.enum(["query", "mutation"]),
731
1020
  requires: manifestRequiresSchema
732
1021
  });
733
1022
  var operationsManifestSchema = zod.z.object({
@@ -781,14 +1070,14 @@ function generateTypePolicies(canonical) {
781
1070
  const def = normalizeCanonical(canonicalDefinitionSchema.parse(canonical));
782
1071
  const entries = [];
783
1072
  let hasPlatformUser = false;
784
- for (const table of def.tables) {
1073
+ for (const table of canonicalRelations(def)) {
785
1074
  if (table.source !== void 0) continue;
786
1075
  if (table.links.some((link) => link.kind === "toPlatformUser")) hasPlatformUser = true;
787
1076
  if (!table.primaryKey || table.primaryKey.length === 0) continue;
788
1077
  const keyFields = table.primaryKey.map((identifier2) => {
789
1078
  const column = table.columns.find((c) => c.columnName === identifier2 || c.name === identifier2);
790
1079
  if (!column) {
791
- throw new CodegenGqlError(`Primary key column "${identifier2}" not found on table "${table.table}".`);
1080
+ throw new CodegenGqlError(`Primary key column "${identifier2}" not found on relation "${table.table}".`);
792
1081
  }
793
1082
  return fieldName(column.name);
794
1083
  });