on-zero 0.16.16 → 0.16.17

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 (81) hide show
  1. package/dist/cjs/createZeroClient.httpAuth.test.cjs +5 -3
  2. package/dist/cjs/createZeroClient.httpAuth.test.native.js +3 -3
  3. package/dist/cjs/createZeroClient.httpAuth.test.native.js.map +1 -1
  4. package/dist/cjs/generate-helpers.cjs +21 -14
  5. package/dist/cjs/generate-helpers.native.js +50 -27
  6. package/dist/cjs/generate-helpers.native.js.map +1 -1
  7. package/dist/cjs/generate-helpers.test.cjs +146 -0
  8. package/dist/cjs/generate-helpers.test.native.js +149 -0
  9. package/dist/cjs/generate-helpers.test.native.js.map +1 -1
  10. package/dist/cjs/generate-lite.cjs +3 -3
  11. package/dist/cjs/generate-lite.native.js +5 -3
  12. package/dist/cjs/generate-lite.native.js.map +1 -1
  13. package/dist/cjs/generate-lite.test.cjs +56 -0
  14. package/dist/cjs/generate-lite.test.native.js +60 -0
  15. package/dist/cjs/generate-lite.test.native.js.map +1 -1
  16. package/dist/cjs/generate.cjs +37 -26
  17. package/dist/cjs/generate.native.js +58 -26
  18. package/dist/cjs/generate.native.js.map +1 -1
  19. package/dist/cjs/generate.test.cjs +109 -0
  20. package/dist/cjs/generate.test.native.js +115 -0
  21. package/dist/cjs/generate.test.native.js.map +1 -1
  22. package/dist/cjs/mutations.cjs +22 -11
  23. package/dist/cjs/mutations.native.js +42 -10
  24. package/dist/cjs/mutations.native.js.map +1 -1
  25. package/dist/cjs/mutations.test.cjs +228 -0
  26. package/dist/cjs/mutations.test.native.js +259 -0
  27. package/dist/cjs/mutations.test.native.js.map +1 -1
  28. package/dist/esm/createZeroClient.httpAuth.test.mjs +5 -3
  29. package/dist/esm/createZeroClient.httpAuth.test.mjs.map +1 -1
  30. package/dist/esm/createZeroClient.httpAuth.test.native.js +3 -3
  31. package/dist/esm/createZeroClient.httpAuth.test.native.js.map +1 -1
  32. package/dist/esm/generate-helpers.mjs +21 -15
  33. package/dist/esm/generate-helpers.mjs.map +1 -1
  34. package/dist/esm/generate-helpers.native.js +50 -28
  35. package/dist/esm/generate-helpers.native.js.map +1 -1
  36. package/dist/esm/generate-helpers.test.mjs +122 -0
  37. package/dist/esm/generate-helpers.test.mjs.map +1 -1
  38. package/dist/esm/generate-helpers.test.native.js +125 -0
  39. package/dist/esm/generate-helpers.test.native.js.map +1 -1
  40. package/dist/esm/generate-lite.mjs +4 -4
  41. package/dist/esm/generate-lite.mjs.map +1 -1
  42. package/dist/esm/generate-lite.native.js +6 -4
  43. package/dist/esm/generate-lite.native.js.map +1 -1
  44. package/dist/esm/generate-lite.test.mjs +56 -0
  45. package/dist/esm/generate-lite.test.mjs.map +1 -1
  46. package/dist/esm/generate-lite.test.native.js +60 -0
  47. package/dist/esm/generate-lite.test.native.js.map +1 -1
  48. package/dist/esm/generate.mjs +38 -27
  49. package/dist/esm/generate.mjs.map +1 -1
  50. package/dist/esm/generate.native.js +59 -27
  51. package/dist/esm/generate.native.js.map +1 -1
  52. package/dist/esm/generate.test.mjs +86 -1
  53. package/dist/esm/generate.test.mjs.map +1 -1
  54. package/dist/esm/generate.test.native.js +92 -1
  55. package/dist/esm/generate.test.native.js.map +1 -1
  56. package/dist/esm/index.native.js.map +1 -1
  57. package/dist/esm/mutations.mjs +22 -11
  58. package/dist/esm/mutations.mjs.map +1 -1
  59. package/dist/esm/mutations.native.js +42 -10
  60. package/dist/esm/mutations.native.js.map +1 -1
  61. package/dist/esm/mutations.test.mjs +229 -1
  62. package/dist/esm/mutations.test.mjs.map +1 -1
  63. package/dist/esm/mutations.test.native.js +260 -1
  64. package/dist/esm/mutations.test.native.js.map +1 -1
  65. package/package.json +2 -2
  66. package/src/createZeroClient.httpAuth.test.ts +4 -5
  67. package/src/generate-helpers.test.ts +124 -0
  68. package/src/generate-helpers.ts +43 -27
  69. package/src/generate-lite.test.ts +53 -0
  70. package/src/generate-lite.ts +16 -9
  71. package/src/generate.test.ts +92 -0
  72. package/src/generate.ts +60 -33
  73. package/src/mutations.test.ts +216 -1
  74. package/src/mutations.ts +39 -39
  75. package/types/generate-helpers.d.ts +3 -1
  76. package/types/generate-helpers.d.ts.map +1 -1
  77. package/types/generate-lite.d.ts +1 -0
  78. package/types/generate-lite.d.ts.map +1 -1
  79. package/types/generate.d.ts.map +1 -1
  80. package/types/mutations.d.ts +5 -2
  81. package/types/mutations.d.ts.map +1 -1
@@ -1,7 +1,67 @@
1
- import { describe, expect, test } from "vitest";
1
+ import { AsyncLocalStorage } from "async_hooks";
2
+ import { DatabaseSync } from "node:sqlite";
3
+ import { createSchema, string, table } from "@rocicorp/zero";
4
+ import { createSQLiteApplicationDatabase, createSyncExecutor } from "orez-lite";
5
+ import { createQueryCompiler } from "orez-lite/cloudflare/query-compiler";
6
+ import { executeTransactionQueryPlan } from "orez-lite/cloudflare/transaction-query";
7
+ import { afterAll, describe, expect, test, vi } from "vitest";
8
+ import { createZeroServerBindings, setupAsyncLocalStorage } from "./server.native.js";
9
+ import { getMutationsPermissions } from "./modelRegistry.native.js";
2
10
  import { mutations } from "./mutations.native.js";
3
11
  import { serverWhere } from "./serverWhere.native.js";
12
+ var previousEnvironment = vi.hoisted(function () {
13
+ var previous = process.env.VITE_ENVIRONMENT;
14
+ process.env.VITE_ENVIRONMENT = "client";
15
+ return previous;
16
+ });
17
+ afterAll(function () {
18
+ if (previousEnvironment === void 0) delete process.env.VITE_ENVIRONMENT;else process.env.VITE_ENVIRONMENT = previousEnvironment;
19
+ });
4
20
  describe("mutations registry", function () {
21
+ test("explicit opt-out removes generated CRUD from existing proxies and retains permissions", async function () {
22
+ var record = table("closedRecord").columns({
23
+ id: string()
24
+ }).primaryKey("id");
25
+ var permission = serverWhere("closedRecord", function () {
26
+ return true;
27
+ });
28
+ var first = mutations(record, permission);
29
+ expect(Object.keys(first).sort()).toEqual(["delete", "insert", "update", "upsert"]);
30
+ var custom = vi.fn(async function () {});
31
+ var closed = mutations(record, permission, {
32
+ custom
33
+ }, {
34
+ crud: false
35
+ });
36
+ expect(closed).toBe(first);
37
+ expect(Object.keys(closed)).toEqual(["custom"]);
38
+ expect("insert" in closed).toBe(false);
39
+ expect(Reflect.get(closed, "upsert")).toBeUndefined();
40
+ expect(getMutationsPermissions("closedRecord")).toBe(permission);
41
+ await closed.custom();
42
+ expect(custom).toHaveBeenCalledOnce();
43
+ var reopened = mutations(record, permission, {}, {
44
+ crud: true
45
+ });
46
+ expect(Object.keys(reopened).sort()).toEqual(["custom", "delete", "insert", "update", "upsert"]);
47
+ });
48
+ test("opt-out preserves explicitly declared CRUD handlers", async function () {
49
+ var record = table("customRecord").columns({
50
+ id: string()
51
+ }).primaryKey("id");
52
+ var permission = serverWhere("customRecord", function () {
53
+ return true;
54
+ });
55
+ var insert = vi.fn(async function () {});
56
+ var custom = mutations(record, permission, {
57
+ insert
58
+ }, {
59
+ crud: false
60
+ });
61
+ expect(Object.keys(custom)).toEqual(["insert"]);
62
+ await custom.insert();
63
+ expect(insert).toHaveBeenCalledOnce();
64
+ });
5
65
  test("re-registering a handler replaces it per key (HMR)", function () {
6
66
  var permissions = serverWhere("post", function () {
7
67
  return true;
@@ -17,4 +77,203 @@ describe("mutations registry", function () {
17
77
  expect(proxy.custom).toBe(v2);
18
78
  });
19
79
  });
80
+ describe("generated CRUD authorization", function () {
81
+ test("authorizes both sides of composite-key writes through the server executor", async function () {
82
+ setupAsyncLocalStorage(AsyncLocalStorage);
83
+ var document = table("document").columns({
84
+ workspace: string(),
85
+ id: string(),
86
+ ownerId: string(),
87
+ title: string()
88
+ }).primaryKey("workspace", "id");
89
+ var schema = createSchema({
90
+ tables: [document],
91
+ relationships: [],
92
+ enableLegacyQueries: true
93
+ });
94
+ var sqlite = new DatabaseSync(":memory:");
95
+ sqlite.exec("CREATE TABLE document (workspace TEXT, id TEXT, ownerId TEXT, title TEXT, PRIMARY KEY (workspace, id))");
96
+ var compile = createQueryCompiler(schema);
97
+ var query = async function query2(sql) {
98
+ var params = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : [];
99
+ return sqlite.prepare(sql).all(...params.map(function (value) {
100
+ return value;
101
+ }));
102
+ };
103
+ var tx = {
104
+ query,
105
+ exec: async function exec(sql) {
106
+ var params = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : [];
107
+ return {
108
+ changes: Number(sqlite.prepare(sql).run(...params.map(function (value) {
109
+ return value;
110
+ })).changes)
111
+ };
112
+ },
113
+ queryAst: async function queryAst(ast, format, queryName) {
114
+ return executeTransactionQueryPlan(compile(ast, format), function (sql, params) {
115
+ return sqlite.prepare(sql).all(...params.map(function (value) {
116
+ return value;
117
+ }));
118
+ }, {
119
+ queryName
120
+ });
121
+ }
122
+ };
123
+ var database = createSQLiteApplicationDatabase({
124
+ query,
125
+ transaction: async function transaction(work) {
126
+ sqlite.exec("BEGIN");
127
+ try {
128
+ var result = await work(tx);
129
+ sqlite.exec("COMMIT");
130
+ return result;
131
+ } catch (error) {
132
+ sqlite.exec("ROLLBACK");
133
+ throw error;
134
+ }
135
+ }
136
+ });
137
+ var permission = serverWhere("document", function (eb, auth2) {
138
+ return eb.cmp("ownerId", auth2 && "id" in auth2 && typeof auth2.id === "string" ? auth2.id : "");
139
+ });
140
+ var bindings = createZeroServerBindings({
141
+ schema,
142
+ models: {
143
+ document: {
144
+ mutate: mutations(document, permission)
145
+ }
146
+ },
147
+ createServerActions: function createServerActions() {
148
+ return {};
149
+ }
150
+ });
151
+ var executor = createSyncExecutor({
152
+ database,
153
+ schema,
154
+ mutators: bindings.mutators,
155
+ effects: {
156
+ runBackground: function runBackground(promise) {
157
+ return promise;
158
+ },
159
+ report: function report(error) {
160
+ throw error;
161
+ }
162
+ }
163
+ });
164
+ var server = bindings.server(executor);
165
+ var auth = {
166
+ authData: {
167
+ id: "alice"
168
+ }
169
+ };
170
+ var own = {
171
+ workspace: "a",
172
+ id: "same",
173
+ ownerId: "alice",
174
+ title: "own"
175
+ };
176
+ var foreign = {
177
+ workspace: "b",
178
+ id: "same",
179
+ ownerId: "bob",
180
+ title: "foreign"
181
+ };
182
+ try {
183
+ await server.mutate.document.insert(own, auth);
184
+ await server.mutate.document.insert(foreign, {
185
+ authData: {
186
+ id: "bob"
187
+ }
188
+ });
189
+ await expect(server.mutate.document.insert({
190
+ ...foreign,
191
+ id: "forbidden"
192
+ }, auth)).rejects.toThrow();
193
+ await expect(server.mutate.document.upsert({
194
+ ...foreign,
195
+ ownerId: "alice"
196
+ }, auth)).rejects.toThrow();
197
+ await expect(server.mutate.document.update({
198
+ ...own,
199
+ ownerId: "bob"
200
+ }, auth)).rejects.toThrow();
201
+ await expect(server.mutate.document.upsert({
202
+ ...own,
203
+ ownerId: "bob"
204
+ }, auth)).rejects.toThrow();
205
+ await expect(server.mutate.document.upsert({
206
+ ...foreign,
207
+ id: "missing"
208
+ }, auth)).rejects.toThrow();
209
+ await server.mutate.document.upsert({
210
+ ...own,
211
+ title: "updated"
212
+ }, auth);
213
+ await server.mutate.document.upsert({
214
+ ...own,
215
+ title: "updated"
216
+ }, auth);
217
+ await server.mutate.document.upsert({
218
+ ...own,
219
+ id: "new"
220
+ }, auth);
221
+ expect(sqlite.prepare("SELECT * FROM document ORDER BY workspace, id").all()).toEqual([{
222
+ ...own,
223
+ id: "new"
224
+ }, {
225
+ ...own,
226
+ title: "updated"
227
+ }, foreign]);
228
+ var closedBindings = createZeroServerBindings({
229
+ schema,
230
+ models: {
231
+ document: {
232
+ mutate: mutations(document, permission, {
233
+ rename: async function rename(ctx, input) {
234
+ await ctx.can(permission, input);
235
+ await ctx.tx.mutate.document.update(input);
236
+ }
237
+ }, {
238
+ crud: false
239
+ })
240
+ }
241
+ },
242
+ createServerActions: function createServerActions() {
243
+ return {};
244
+ }
245
+ });
246
+ expect(Object.keys(closedBindings.mutators)).toEqual(["document|rename"]);
247
+ var closedExecutor = createSyncExecutor({
248
+ database,
249
+ schema,
250
+ mutators: closedBindings.mutators,
251
+ effects: {
252
+ runBackground: function runBackground(promise) {
253
+ return promise;
254
+ },
255
+ report: function report(error) {
256
+ throw error;
257
+ }
258
+ }
259
+ });
260
+ for (var _i = 0, _iter = ["insert", "update", "delete", "upsert"]; _i < _iter.length; _i++) {
261
+ var action = _iter[_i];
262
+ await expect(closedExecutor.execute(`document|${action}`, own, {
263
+ userID: "alice"
264
+ })).rejects.toThrow(`unknown mutator: document|${action}`);
265
+ }
266
+ await closedBindings.server(closedExecutor).mutate.document.rename({
267
+ workspace: "a",
268
+ id: "same",
269
+ title: "renamed"
270
+ }, auth);
271
+ expect(sqlite.prepare("SELECT title FROM document WHERE workspace = ? AND id = ?").get("a", "same")).toEqual({
272
+ title: "renamed"
273
+ });
274
+ } finally {
275
+ sqlite.close();
276
+ }
277
+ });
278
+ });
20
279
  //# sourceMappingURL=mutations.test.native.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["describe","expect","test","mutations","serverWhere","permissions","v1","v12","v2","v22","custom"],"sources":["../../src/mutations.test.ts"],"sourcesContent":[null],"mappings":"AAAA,SAASA,QAAA,EAAUC,MAAA,EAAQC,IAAA,QAAY;AAEvC,SAASC,SAAA,QAAiB;AAC1B,SAASC,WAAA,QAAmB;AAE5BJ,QAAA,CAAS,sBAAsB,YAAM;EACnCE,IAAA,CAAK,sDAAsD,YAAM;IAC/D,IAAAG,WAAM,GAAAD,WAAc,OAAY,cAAc;MAC9C,OAAM,IAAK;IAAa;IACxB,IAAAE,EAAM,kBAAiBC,IAAA,GAAC;IACxB,IAAAC,EAAA,kBAAkBC,IAAA,GAClB;IAGAN,SAAO,OAAM,EAAAE,WAAe;MAC7BK,MAAA,EAAAJ;IACF","ignoreList":[]}
1
+ {"version":3,"names":["AsyncLocalStorage","DatabaseSync","createSchema","string","table","createSQLiteApplicationDatabase","createSyncExecutor","createQueryCompiler","executeTransactionQueryPlan","afterAll","describe","expect","test","vi","createZeroServerBindings","setupAsyncLocalStorage","getMutationsPermissions","mutations","serverWhere","previousEnvironment","hoisted","previous","process","env","VITE_ENVIRONMENT","record","columns","id","primaryKey","permission","first","Object","keys","sort","toEqual","custom","fn","closed","crud","toBe","Reflect","get","toBeUndefined","toHaveBeenCalledOnce","reopened","insert","permissions","v1","v12","v2","v22","proxy","document","workspace","ownerId","title","schema","tables","relationships","enableLegacyQueries","sqlite","exec","compile","query","query2","sql","params","arguments","length","prepare","all","map","value","tx","changes","Number","run","queryAst","ast","format","queryName","database","transaction","work","result","error","eb","auth2","cmp","bindings","models","mutate","createServerActions","executor","mutators","effects","runBackground","promise","report","server","auth","authData","own","foreign"],"sources":["../../src/mutations.test.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "on-zero",
3
- "version": "0.16.16",
3
+ "version": "0.16.17",
4
4
  "description": "A typed layer over @rocicorp/zero with queries, mutations, and permissions",
5
5
  "sideEffects": false,
6
6
  "files": [
@@ -105,7 +105,7 @@
105
105
  "chokidar": "^4.0.3",
106
106
  "citty": "^0.1.6",
107
107
  "dequal": "^2.0.3",
108
- "orez-lite": "0.16.16",
108
+ "orez-lite": "0.16.17",
109
109
  "valibot": "^1.1.0"
110
110
  },
111
111
  "peerDependencies": {
@@ -92,12 +92,11 @@ test('warm headless client refreshes an expired bearer and reconnects', async ()
92
92
  })
93
93
 
94
94
  try {
95
- await eventually(() =>
95
+ await eventually(() => {
96
+ expect(refreshAuth).toHaveBeenCalledTimes(1)
97
+ expect(authorizations).toEqual(['Bearer expired-token', 'Bearer fresh-token'])
96
98
  expect(connection.zero.connection.state.current.name).toBe('connected')
97
- )
98
-
99
- expect(refreshAuth).toHaveBeenCalledTimes(1)
100
- expect(authorizations).toEqual(['Bearer expired-token', 'Bearer fresh-token'])
99
+ })
101
100
  } finally {
102
101
  await connection.close()
103
102
  }
@@ -1,3 +1,6 @@
1
+ import { runInNewContext } from 'node:vm'
2
+
3
+ import * as v from 'valibot'
1
4
  import { describe, expect, test } from 'vitest'
2
5
 
3
6
  import {
@@ -73,4 +76,125 @@ describe('generated mutation validators', () => {
73
76
  id: v.string(),
74
77
  }),`)
75
78
  })
79
+
80
+ test('emits an upsert validator alongside the other generated crud slots', () => {
81
+ const source = generateSyncedMutationsFile([
82
+ {
83
+ modelName: 'post',
84
+ hasCRUD: true,
85
+ columns: {
86
+ id: { type: 'string', optional: false, customType: undefined },
87
+ title: { type: 'string', optional: true, customType: undefined },
88
+ },
89
+ primaryKeys: ['id'],
90
+ custom: [],
91
+ },
92
+ ])
93
+
94
+ // upsert takes the insert payload shape: every column, optionality as declared
95
+ expect(source).toContain(` upsert: v.object({
96
+ id: v.string(),
97
+ title: v.optional(v.nullable(v.string())),
98
+ }),`)
99
+ expect(source).toContain(' insert: v.object({')
100
+ expect(source).toContain(' update: v.object({')
101
+ expect(source).toContain(' delete: v.object({')
102
+ })
103
+
104
+ test('keeps a custom upsert override instead of dropping it', () => {
105
+ const source = generateSyncedMutationsFile([
106
+ {
107
+ modelName: 'post',
108
+ hasCRUD: true,
109
+ columns: {
110
+ id: { type: 'string', optional: false, customType: undefined },
111
+ },
112
+ primaryKeys: ['id'],
113
+ custom: [
114
+ {
115
+ name: 'upsert',
116
+ paramType: '{ id: string; draft: boolean }',
117
+ valibotCode: 'v.object({\n id: v.string(),\n draft: v.boolean(),\n })',
118
+ },
119
+ ],
120
+ },
121
+ ])
122
+
123
+ expect(source).toContain(` upsert: v.object({
124
+ id: v.string(),
125
+ draft: v.boolean(),
126
+ }),`)
127
+ // the override replaces the generated slot rather than appearing twice
128
+ expect(source.match(/upsert:/g)).toHaveLength(1)
129
+ })
130
+
131
+ test('honors a custom unknown payload even when table columns are available', () => {
132
+ const source = generateSyncedMutationsFile([
133
+ {
134
+ modelName: 'post',
135
+ hasCRUD: true,
136
+ columns: { id: { type: 'string', optional: false, customType: undefined } },
137
+ primaryKeys: ['id'],
138
+ custom: [{ name: 'upsert', paramType: 'unknown', valibotCode: '' }],
139
+ },
140
+ ])
141
+ const validators = runInNewContext(
142
+ source
143
+ .replace("import * as v from 'valibot'", '')
144
+ .replace('export const mutationValidators', 'const mutationValidators') +
145
+ '\nmutationValidators',
146
+ { v }
147
+ )
148
+ expect(v.parse(validators.post.upsert, { customInput: true })).toEqual({
149
+ customInput: true,
150
+ })
151
+ expect(() => v.parse(validators.post.insert, { customInput: true })).toThrow()
152
+ })
153
+
154
+ test('emits only authored handlers when the model opted out of crud', () => {
155
+ const source = generateSyncedMutationsFile([
156
+ {
157
+ modelName: 'appNotification',
158
+ hasCRUD: false,
159
+ columns: {
160
+ id: { type: 'string', optional: false, customType: undefined },
161
+ },
162
+ primaryKeys: ['id'],
163
+ custom: [
164
+ {
165
+ name: 'markRead',
166
+ paramType: '{ id: string }',
167
+ valibotCode: 'v.object({\n id: v.string(),\n })',
168
+ },
169
+ ],
170
+ },
171
+ ])
172
+
173
+ expect(source).toContain(' markRead: v.object({')
174
+ for (const slot of ['insert', 'update', 'delete', 'upsert']) {
175
+ expect(source).not.toContain(` ${slot}:`)
176
+ }
177
+ })
178
+
179
+ test('keeps an authored crud-named handler when there are no schema columns', () => {
180
+ const source = generateSyncedMutationsFile([
181
+ {
182
+ modelName: 'post',
183
+ hasCRUD: true,
184
+ columns: {},
185
+ primaryKeys: [],
186
+ custom: [
187
+ {
188
+ name: 'insert',
189
+ paramType: '{ id: string }',
190
+ valibotCode: 'v.object({\n id: v.string(),\n })',
191
+ },
192
+ ],
193
+ },
194
+ ])
195
+
196
+ expect(source).toContain(` insert: v.object({
197
+ id: v.string(),
198
+ }),`)
199
+ })
76
200
  })
@@ -23,6 +23,8 @@ export type ExtractedMutation = {
23
23
 
24
24
  export type ModelMutations = {
25
25
  modelName: string
26
+ // true when the mutate call registers a table (any 2+ arg form) and did not
27
+ // opt out with `mutations(table, permissions, handlers, { crud: false })`
26
28
  hasCRUD: boolean
27
29
  // populated when hasCRUD
28
30
  columns: Record<string, SchemaColumn>
@@ -671,10 +673,16 @@ export function columnTypeToValibot(col: SchemaColumn): string {
671
673
  return col.optional ? `v.optional(v.nullable(${base}))` : base
672
674
  }
673
675
 
676
+ // the four generated crud slots, in the order they are emitted. `upsert`
677
+ // takes the same payload shape as `insert` (see CRUDMutations in mutations.ts).
678
+ export const CRUD_MUTATION_NAMES = ['insert', 'update', 'delete', 'upsert'] as const
679
+
680
+ export type CRUDMutationName = (typeof CRUD_MUTATION_NAMES)[number]
681
+
674
682
  export function schemaColumnsToValibot(
675
683
  columns: Record<string, SchemaColumn>,
676
684
  primaryKeys: string[],
677
- mode: 'insert' | 'update' | 'delete'
685
+ mode: CRUDMutationName
678
686
  ): string {
679
687
  const entries: string[] = []
680
688
 
@@ -702,12 +710,16 @@ export function schemaColumnsToValibot(
702
710
  }
703
711
  }
704
712
  } else {
705
- // insert: all columns as-is
713
+ // insert and upsert: all columns as-is
706
714
  for (const [name, col] of Object.entries(columns)) {
707
715
  entries.push(`${formatObjectKey(name)}: ${columnTypeToValibot(col)}`)
708
716
  }
709
717
  }
710
718
 
719
+ // a schema with no declared primary key yields no `delete` entries; emitting
720
+ // the entry list unguarded produced `v.object({ , })`, which does not parse
721
+ if (entries.length === 0) return 'v.object({})'
722
+
711
723
  return `v.object({\n ${entries.join(',\n ')},\n })`
712
724
  }
713
725
 
@@ -748,36 +760,40 @@ export function generateSyncedMutationsFile(modelMutations: ModelMutations[]): s
748
760
  .map((model) => {
749
761
  const entries: string[] = []
750
762
 
751
- // crud validators from schema
752
- if (model.hasCRUD && Object.keys(model.columns).length > 0) {
753
- for (const mode of ['insert', 'update', 'delete'] as const) {
754
- // skip if custom mutation overrides this crud op
755
- const hasCustomOverride = model.custom.some((m) => m.name === mode)
756
- if (hasCustomOverride) {
757
- // use the custom version's validator instead
758
- const customMut = model.custom.find((m) => m.name === mode)!
759
- if (customMut.valibotCode) {
760
- entries.push(
761
- ` ${mode}: ${indentContinuationLines(extractValibotExpression(customMut.valibotCode), 2)},`
762
- )
763
- } else {
764
- // fall back to schema-derived
765
- entries.push(
766
- ` ${mode}: ${schemaColumnsToValibot(model.columns, model.primaryKeys, mode)},`
767
- )
768
- }
769
- } else {
770
- entries.push(
771
- ` ${mode}: ${schemaColumnsToValibot(model.columns, model.primaryKeys, mode)},`
772
- )
773
- }
763
+ // crud validators from schema. a model that opted out with
764
+ // `{ crud: false }`, or that has no inline schema columns, emits nothing
765
+ // here and leaves any authored insert/update/delete/upsert to the custom
766
+ // loop below.
767
+ const emitted = new Set<string>()
768
+ const canDeriveCRUD = model.hasCRUD && Object.keys(model.columns).length > 0
769
+
770
+ for (const mode of CRUD_MUTATION_NAMES) {
771
+ // a custom handler with the same name replaces the generated op, so its
772
+ // own payload type wins over the schema-derived shape
773
+ const customMut = model.custom.find((m) => m.name === mode)
774
+ if (customMut?.valibotCode) {
775
+ entries.push(
776
+ ` ${mode}: ${indentContinuationLines(extractValibotExpression(customMut.valibotCode), 2)},`
777
+ )
778
+ emitted.add(mode)
779
+ continue
780
+ }
781
+ if (customMut?.paramType === 'void') {
782
+ entries.push(` ${mode}: v.void_(),`)
783
+ emitted.add(mode)
784
+ continue
785
+ }
786
+ if (canDeriveCRUD && !customMut) {
787
+ entries.push(
788
+ ` ${mode}: ${schemaColumnsToValibot(model.columns, model.primaryKeys, mode)},`
789
+ )
790
+ emitted.add(mode)
774
791
  }
775
792
  }
776
793
 
777
794
  // custom mutations (excluding crud overrides already handled)
778
795
  for (const mut of model.custom) {
779
- if (model.hasCRUD && ['insert', 'update', 'delete', 'upsert'].includes(mut.name))
780
- continue
796
+ if (emitted.has(mut.name)) continue
781
797
  if (mut.paramType === 'void') {
782
798
  entries.push(` ${mut.name}: v.void_(),`)
783
799
  continue
@@ -457,4 +457,57 @@ describe('generateLite', () => {
457
457
  })
458
458
  ).toThrow(/uses removed instance\.ts configuration/)
459
459
  })
460
+ test('generates crud validators for a schema model and drops them on crud false', () => {
461
+ const empty = { mutations: [], queries: [] }
462
+ const schema = {
463
+ tableName: 'appNotification',
464
+ primaryKeys: ['id'],
465
+ columns: [
466
+ { name: 'id', builderText: 'string()' },
467
+ { name: 'readAt', builderText: 'number().optional()' },
468
+ ],
469
+ }
470
+ const handlers = [{ name: 'markRead', paramTypeText: '{ id: string }' }]
471
+
472
+ const run = (crud?: boolean) =>
473
+ generateLite({
474
+ files: { [`${DIR}/appNotification.ts`]: '// namespace' },
475
+ dir: DIR,
476
+ parse: makeParse({
477
+ [`${DIR}/appNotification.ts`]: {
478
+ ...empty,
479
+ mutations: [
480
+ {
481
+ modelName: 'appNotification',
482
+ handlers,
483
+ schema,
484
+ ...(crud === undefined ? {} : { crud }),
485
+ },
486
+ ],
487
+ },
488
+ }),
489
+ })
490
+
491
+ const withCRUD = run()
492
+ const validators = withCRUD.files['syncedMutations.ts']!
493
+ for (const slot of ['insert', 'update', 'delete', 'upsert']) {
494
+ expect(validators).toContain(` ${slot}: v.object({`)
495
+ }
496
+ expect(validators).toContain(' markRead: v.object({')
497
+ // one count per generated slot plus the authored handler
498
+ expect(withCRUD.mutationCount).toBe(5)
499
+
500
+ const optedOut = run(false)
501
+ const optedOutValidators = optedOut.files['syncedMutations.ts']!
502
+ for (const slot of ['insert', 'update', 'delete', 'upsert']) {
503
+ expect(optedOutValidators).not.toContain(` ${slot}:`)
504
+ }
505
+ expect(optedOutValidators).toContain(' markRead: v.object({')
506
+ expect(optedOut.mutationCount).toBe(1)
507
+ // the schema itself is still registered, so permissions/tables are unaffected
508
+ expect(optedOut.schemaCount).toBe(1)
509
+
510
+ // an explicit `{ crud: true }` is the same as omitting the options object
511
+ expect(run(true).files['syncedMutations.ts']).toBe(validators)
512
+ })
460
513
  })