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.
- package/dist/cjs/createZeroClient.httpAuth.test.cjs +5 -3
- package/dist/cjs/createZeroClient.httpAuth.test.native.js +3 -3
- package/dist/cjs/createZeroClient.httpAuth.test.native.js.map +1 -1
- package/dist/cjs/generate-helpers.cjs +21 -14
- package/dist/cjs/generate-helpers.native.js +50 -27
- package/dist/cjs/generate-helpers.native.js.map +1 -1
- package/dist/cjs/generate-helpers.test.cjs +146 -0
- package/dist/cjs/generate-helpers.test.native.js +149 -0
- package/dist/cjs/generate-helpers.test.native.js.map +1 -1
- package/dist/cjs/generate-lite.cjs +3 -3
- package/dist/cjs/generate-lite.native.js +5 -3
- package/dist/cjs/generate-lite.native.js.map +1 -1
- package/dist/cjs/generate-lite.test.cjs +56 -0
- package/dist/cjs/generate-lite.test.native.js +60 -0
- package/dist/cjs/generate-lite.test.native.js.map +1 -1
- package/dist/cjs/generate.cjs +37 -26
- package/dist/cjs/generate.native.js +58 -26
- package/dist/cjs/generate.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +109 -0
- package/dist/cjs/generate.test.native.js +115 -0
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/mutations.cjs +22 -11
- package/dist/cjs/mutations.native.js +42 -10
- package/dist/cjs/mutations.native.js.map +1 -1
- package/dist/cjs/mutations.test.cjs +228 -0
- package/dist/cjs/mutations.test.native.js +259 -0
- package/dist/cjs/mutations.test.native.js.map +1 -1
- package/dist/esm/createZeroClient.httpAuth.test.mjs +5 -3
- package/dist/esm/createZeroClient.httpAuth.test.mjs.map +1 -1
- package/dist/esm/createZeroClient.httpAuth.test.native.js +3 -3
- package/dist/esm/createZeroClient.httpAuth.test.native.js.map +1 -1
- package/dist/esm/generate-helpers.mjs +21 -15
- package/dist/esm/generate-helpers.mjs.map +1 -1
- package/dist/esm/generate-helpers.native.js +50 -28
- package/dist/esm/generate-helpers.native.js.map +1 -1
- package/dist/esm/generate-helpers.test.mjs +122 -0
- package/dist/esm/generate-helpers.test.mjs.map +1 -1
- package/dist/esm/generate-helpers.test.native.js +125 -0
- package/dist/esm/generate-helpers.test.native.js.map +1 -1
- package/dist/esm/generate-lite.mjs +4 -4
- package/dist/esm/generate-lite.mjs.map +1 -1
- package/dist/esm/generate-lite.native.js +6 -4
- package/dist/esm/generate-lite.native.js.map +1 -1
- package/dist/esm/generate-lite.test.mjs +56 -0
- package/dist/esm/generate-lite.test.mjs.map +1 -1
- package/dist/esm/generate-lite.test.native.js +60 -0
- package/dist/esm/generate-lite.test.native.js.map +1 -1
- package/dist/esm/generate.mjs +38 -27
- package/dist/esm/generate.mjs.map +1 -1
- package/dist/esm/generate.native.js +59 -27
- package/dist/esm/generate.native.js.map +1 -1
- package/dist/esm/generate.test.mjs +86 -1
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +92 -1
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/index.native.js.map +1 -1
- package/dist/esm/mutations.mjs +22 -11
- package/dist/esm/mutations.mjs.map +1 -1
- package/dist/esm/mutations.native.js +42 -10
- package/dist/esm/mutations.native.js.map +1 -1
- package/dist/esm/mutations.test.mjs +229 -1
- package/dist/esm/mutations.test.mjs.map +1 -1
- package/dist/esm/mutations.test.native.js +260 -1
- package/dist/esm/mutations.test.native.js.map +1 -1
- package/package.json +2 -2
- package/src/createZeroClient.httpAuth.test.ts +4 -5
- package/src/generate-helpers.test.ts +124 -0
- package/src/generate-helpers.ts +43 -27
- package/src/generate-lite.test.ts +53 -0
- package/src/generate-lite.ts +16 -9
- package/src/generate.test.ts +92 -0
- package/src/generate.ts +60 -33
- package/src/mutations.test.ts +216 -1
- package/src/mutations.ts +39 -39
- package/types/generate-helpers.d.ts +3 -1
- package/types/generate-helpers.d.ts.map +1 -1
- package/types/generate-lite.d.ts +1 -0
- package/types/generate-lite.d.ts.map +1 -1
- package/types/generate.d.ts.map +1 -1
- package/types/mutations.d.ts +5 -2
- package/types/mutations.d.ts.map +1 -1
|
@@ -1,7 +1,67 @@
|
|
|
1
|
-
import {
|
|
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","
|
|
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.
|
|
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.
|
|
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
|
})
|
package/src/generate-helpers.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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 (
|
|
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
|
})
|