on-zero 0.9.7 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/generate-helpers.test.cjs +0 -38
- package/dist/cjs/generate-helpers.test.native.js +0 -38
- package/dist/cjs/generate-helpers.test.native.js.map +1 -1
- package/dist/cjs/generate-lite.test.cjs +0 -522
- package/dist/cjs/generate-lite.test.native.js +0 -525
- package/dist/cjs/generate-lite.test.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +0 -691
- package/dist/cjs/generate.test.native.js +0 -691
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/mutations.test.cjs +0 -11
- package/dist/cjs/mutations.test.native.js +0 -18
- package/dist/cjs/mutations.test.native.js.map +1 -1
- package/dist/cjs/server.test.cjs +0 -93
- package/dist/cjs/server.test.native.js +0 -101
- package/dist/cjs/server.test.native.js.map +1 -1
- package/dist/cjs/serverWhere.test.cjs +0 -45
- package/dist/cjs/serverWhere.test.native.js +0 -51
- package/dist/cjs/serverWhere.test.native.js.map +1 -1
- package/dist/esm/generate-helpers.test.mjs +1 -39
- package/dist/esm/generate-helpers.test.mjs.map +1 -1
- package/dist/esm/generate-helpers.test.native.js +1 -39
- package/dist/esm/generate-helpers.test.native.js.map +1 -1
- package/dist/esm/generate-lite.test.mjs +0 -522
- package/dist/esm/generate-lite.test.mjs.map +1 -1
- package/dist/esm/generate-lite.test.native.js +0 -525
- package/dist/esm/generate-lite.test.native.js.map +1 -1
- package/dist/esm/generate.test.mjs +2 -668
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +2 -668
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/mutations.test.mjs +0 -11
- package/dist/esm/mutations.test.mjs.map +1 -1
- package/dist/esm/mutations.test.native.js +0 -18
- package/dist/esm/mutations.test.native.js.map +1 -1
- package/dist/esm/server.test.mjs +0 -93
- package/dist/esm/server.test.mjs.map +1 -1
- package/dist/esm/server.test.native.js +0 -101
- package/dist/esm/server.test.native.js.map +1 -1
- package/dist/esm/serverWhere.test.mjs +0 -45
- package/dist/esm/serverWhere.test.mjs.map +1 -1
- package/dist/esm/serverWhere.test.native.js +0 -51
- package/dist/esm/serverWhere.test.native.js.map +1 -1
- package/package.json +1 -1
- package/src/generate-helpers.test.ts +0 -40
- package/src/generate-lite.test.ts +0 -586
- package/src/generate.test.ts +0 -844
- package/src/mutations.test.ts +0 -21
- package/src/server.test.ts +0 -75
- package/src/serverWhere.test.ts +0 -42
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync as writeFile } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { runInNewContext } from "node:vm";
|
|
5
|
-
import * as zero from "@rocicorp/zero";
|
|
6
5
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
7
|
-
import { deriveDataMembership, generate,
|
|
6
|
+
import { deriveDataMembership, generate, generateDrizzleSchemaInputFile } from "./generate.mjs";
|
|
8
7
|
const testDir = join(tmpdir(), "on-zero-test-" + Date.now());
|
|
9
8
|
function writeFileSync(path, content) {
|
|
10
9
|
mkdirSync(dirname(path), {
|
|
@@ -25,105 +24,6 @@ afterEach(() => {
|
|
|
25
24
|
});
|
|
26
25
|
});
|
|
27
26
|
describe("generate", () => {
|
|
28
|
-
test("generates models.ts, types.ts, tables.ts from model files", async () => {
|
|
29
|
-
writeFileSync(join(testDir, "post/mutations.ts"), `
|
|
30
|
-
import { table, string, boolean } from 'on-zero'
|
|
31
|
-
|
|
32
|
-
export const schema = table('post', {
|
|
33
|
-
id: string(),
|
|
34
|
-
title: string(),
|
|
35
|
-
published: boolean(),
|
|
36
|
-
})
|
|
37
|
-
`);
|
|
38
|
-
writeFileSync(join(testDir, "comment/mutations.ts"), `
|
|
39
|
-
import { table, string } from 'on-zero'
|
|
40
|
-
|
|
41
|
-
export const schema = table('comment', {
|
|
42
|
-
id: string(),
|
|
43
|
-
postId: string(),
|
|
44
|
-
body: string(),
|
|
45
|
-
})
|
|
46
|
-
`);
|
|
47
|
-
writeFileSync(join(testDir, "post/post.test.ts"), `throw new Error('test files must not be generated as models')`);
|
|
48
|
-
writeFileSync(join(testDir, "comment/comment.spec.ts"), `throw new Error('spec files must not be generated as queries')`);
|
|
49
|
-
const result = await generate({
|
|
50
|
-
dir: testDir,
|
|
51
|
-
silent: true
|
|
52
|
-
});
|
|
53
|
-
expect(result.modelCount).toBe(2);
|
|
54
|
-
expect(result.schemaCount).toBe(2);
|
|
55
|
-
expect(result.filesChanged).toBeGreaterThan(0);
|
|
56
|
-
expect(existsSync(join(testDir, "generated/models.ts"))).toBe(true);
|
|
57
|
-
expect(existsSync(join(testDir, "generated/types.ts"))).toBe(true);
|
|
58
|
-
expect(existsSync(join(testDir, "generated/tables.ts"))).toBe(true);
|
|
59
|
-
const modelsContent = readFileSync(join(testDir, "generated/models.ts"), "utf-8");
|
|
60
|
-
expect(modelsContent).toContain("import * as comment from '../comment/mutations'");
|
|
61
|
-
expect(modelsContent).toContain("import * as post from '../post/mutations'");
|
|
62
|
-
expect(modelsContent).not.toContain("post.test");
|
|
63
|
-
expect(modelsContent).toContain("export const models = {");
|
|
64
|
-
const typesContent = readFileSync(join(testDir, "generated/types.ts"), "utf-8");
|
|
65
|
-
expect(typesContent).toContain("export type Post = TableInsertRow<typeof schema.post>");
|
|
66
|
-
expect(typesContent).toContain("export type Comment = TableInsertRow<typeof schema.comment>");
|
|
67
|
-
const tablesContent = readFileSync(join(testDir, "generated/tables.ts"), "utf-8");
|
|
68
|
-
expect(tablesContent).toContain("export { schema as post } from '../post/mutations'");
|
|
69
|
-
expect(tablesContent).toContain("export { schema as comment } from '../comment/mutations'");
|
|
70
|
-
});
|
|
71
|
-
test("generates query validators from query files", async () => {
|
|
72
|
-
writeFileSync(join(testDir, "post/mutations.ts"), `export const schema = table('post', { id: string() })`);
|
|
73
|
-
writeFileSync(join(testDir, "post/queries.ts"), `
|
|
74
|
-
import { zero } from '../zero'
|
|
75
|
-
|
|
76
|
-
export const allPosts = () => zero.query.post
|
|
77
|
-
|
|
78
|
-
export const postById = ({ id }: { id: string }) => zero.query.post.where('id', id)
|
|
79
|
-
|
|
80
|
-
export const postsByAuthor = ({ authorId, limit }: { authorId: string; limit?: number }) =>
|
|
81
|
-
zero.query.post.where('authorId', authorId).limit(limit ?? 10)
|
|
82
|
-
`);
|
|
83
|
-
const result = await generate({
|
|
84
|
-
dir: testDir,
|
|
85
|
-
silent: true
|
|
86
|
-
});
|
|
87
|
-
expect(result.queryCount).toBe(3);
|
|
88
|
-
expect(existsSync(join(testDir, "generated/groupedQueries.ts"))).toBe(true);
|
|
89
|
-
expect(existsSync(join(testDir, "generated/syncedQueries.ts"))).toBe(true);
|
|
90
|
-
const groupedContent = readFileSync(join(testDir, "generated/groupedQueries.ts"), "utf-8");
|
|
91
|
-
expect(groupedContent).toContain("import * as postSource from '../post/queries'");
|
|
92
|
-
expect(groupedContent).toContain("postById: postSource.postById");
|
|
93
|
-
const syncedContent = readFileSync(join(testDir, "generated/syncedQueries.ts"), "utf-8");
|
|
94
|
-
expect(syncedContent).toContain("allPosts: defineQuery");
|
|
95
|
-
expect(syncedContent).toContain("postById: defineQuery");
|
|
96
|
-
expect(syncedContent).toContain("postsByAuthor: defineQuery");
|
|
97
|
-
expect(syncedContent).toContain("v.object");
|
|
98
|
-
});
|
|
99
|
-
test("skips permission exports in queries", async () => {
|
|
100
|
-
writeFileSync(join(testDir, "post/mutations.ts"), `export const schema = table('post', { id: string() })`);
|
|
101
|
-
writeFileSync(join(testDir, "post/queries.ts"), `
|
|
102
|
-
export const permission = () => ({ canRead: true })
|
|
103
|
-
export const allPosts = () => zero.query.post
|
|
104
|
-
`);
|
|
105
|
-
const result = await generate({
|
|
106
|
-
dir: testDir,
|
|
107
|
-
silent: true
|
|
108
|
-
});
|
|
109
|
-
expect(result.queryCount).toBe(1);
|
|
110
|
-
const syncedContent = readFileSync(join(testDir, "generated/syncedQueries.ts"), "utf-8");
|
|
111
|
-
expect(syncedContent).toContain("allPosts");
|
|
112
|
-
expect(syncedContent).not.toContain("permission:");
|
|
113
|
-
});
|
|
114
|
-
test("aliases user import without changing the model key", async () => {
|
|
115
|
-
writeFileSync(join(testDir, "user/mutations.ts"), `export const schema = table('user', { id: string(), name: string() })`);
|
|
116
|
-
await generate({
|
|
117
|
-
dir: testDir,
|
|
118
|
-
silent: true
|
|
119
|
-
});
|
|
120
|
-
const modelsContent = readFileSync(join(testDir, "generated/models.ts"), "utf-8");
|
|
121
|
-
expect(modelsContent).toContain("import * as userPublic from '../user/mutations'");
|
|
122
|
-
expect(modelsContent).toContain("user: userPublic,");
|
|
123
|
-
expect(modelsContent).not.toContain("\n userPublic,");
|
|
124
|
-
const typesContent = readFileSync(join(testDir, "generated/types.ts"), "utf-8");
|
|
125
|
-
expect(typesContent).toContain("typeof schema.userPublic");
|
|
126
|
-
});
|
|
127
27
|
test("runs after command when files change", async () => {
|
|
128
28
|
writeFileSync(join(testDir, "post/mutations.ts"), `export const schema = table('post', { id: string() })`);
|
|
129
29
|
const markerFile = join(testDir, "after-ran");
|
|
@@ -167,51 +67,6 @@ export const allPosts = () => zero.query.post
|
|
|
167
67
|
});
|
|
168
68
|
describe("instance layout", () => {
|
|
169
69
|
const dataDir = () => join(testDir, "src/data");
|
|
170
|
-
test("discovers file and folder namespaces and emits related sync closure", async () => {
|
|
171
|
-
writeFileSync(join(dataDir(), "control/reaction.ts"), `export const allReactions = () => zql.reaction`);
|
|
172
|
-
writeFileSync(join(dataDir(), "on-zero.config.ts"), `export default defineConfig({ instances: { control: {}, project: { scope: 'projectId' } } })`);
|
|
173
|
-
writeFileSync(join(dataDir(), "project/message/queries.ts"), `
|
|
174
|
-
const unused = () => zql.message.related('notARealRelation')
|
|
175
|
-
const withComments = () => zql.message.related('comments', (q) => q.related('author'))
|
|
176
|
-
export const messages = () => withComments()
|
|
177
|
-
`);
|
|
178
|
-
writeFileSync(join(dataDir(), "project/message/mutations.ts"), `export const schema = table('message').columns({ id: string(), projectId: string() })`);
|
|
179
|
-
writeFileSync(join(dataDir(), "project/message/helpers.ts"), `export const privateHelper = () => 'ignored'`);
|
|
180
|
-
writeFileSync(join(testDir, "src/database/relations.ts"), `
|
|
181
|
-
export const relations = defineRelations(schema, (r) => ({
|
|
182
|
-
message: { comments: r.many.comment({}) },
|
|
183
|
-
comment: { author: r.one.userPublic({}) },
|
|
184
|
-
}))
|
|
185
|
-
`);
|
|
186
|
-
writeFileSync(join(testDir, "src/database/schema.ts"), `
|
|
187
|
-
export const comment = sqliteTable('comment', { id: text(), projectId: text() })
|
|
188
|
-
export const userPublic = sqliteTable('user_public', { id: text(), projectId: text() })
|
|
189
|
-
`);
|
|
190
|
-
const membership = await deriveDataMembership({
|
|
191
|
-
dir: dataDir()
|
|
192
|
-
});
|
|
193
|
-
expect(membership.allTables).toEqual(["comment", "message", "reaction", "userPublic"]);
|
|
194
|
-
expect(membership.instances.project).toEqual({
|
|
195
|
-
tables: ["message"],
|
|
196
|
-
syncTables: ["comment", "message", "userPublic"],
|
|
197
|
-
supportTables: [],
|
|
198
|
-
scope: "projectId"
|
|
199
|
-
});
|
|
200
|
-
await generate({
|
|
201
|
-
dir: dataDir(),
|
|
202
|
-
silent: true
|
|
203
|
-
});
|
|
204
|
-
const grouped = readFileSync(join(dataDir(), "generated/groupedQueries.ts"), "utf8");
|
|
205
|
-
expect(grouped).toContain("from '../control/reaction'");
|
|
206
|
-
expect(grouped).toContain("from '../project/message/queries'");
|
|
207
|
-
expect(grouped).not.toContain("privateHelper");
|
|
208
|
-
const manifest = readFileSync(join(dataDir(), "generated/instances.ts"), "utf8");
|
|
209
|
-
expect(manifest).toContain("control: {");
|
|
210
|
-
expect(manifest).toContain("project: {");
|
|
211
|
-
expect(manifest).toContain('tables: ["message"]');
|
|
212
|
-
expect(manifest).toContain('syncTables: ["comment","message","userPublic"]');
|
|
213
|
-
expect(manifest).toContain(`defaultVisibility: (value: string) => ({ column: "projectId", value })`);
|
|
214
|
-
});
|
|
215
70
|
test("derives query membership from the returned query root", async () => {
|
|
216
71
|
writeFileSync(join(dataDir(), "category/mutations.ts"), `export const schema = table('category').columns({ id: string() })`);
|
|
217
72
|
writeFileSync(join(dataDir(), "dashboard/queries.ts"), `export const monthSummary = () => zql.category.related('expenses')`);
|
|
@@ -420,42 +275,6 @@ export const messages = () => zql.message.related('author')
|
|
|
420
275
|
silent: true
|
|
421
276
|
})).rejects.toThrow(/namespace 'message'.*instances 'control' and 'project'/);
|
|
422
277
|
});
|
|
423
|
-
test("resolves configured instance directories relative to the config file", async () => {
|
|
424
|
-
writeFileSync(join(dataDir(), "on-zero.config.ts"), `export default defineConfig({ instances: { control: { dir: './planes/control-data', supportTables: ['audit'] }, project: { dir: '../project-data', scope: 'projectId' } } })`);
|
|
425
|
-
writeFileSync(join(dataDir(), "planes/control-data/account.ts"), `export const accounts = () => zql.account`);
|
|
426
|
-
writeFileSync(join(testDir, "src/project-data/message.ts"), `export const schema = table('message').columns({ id: string(), projectId: string() })`);
|
|
427
|
-
await expect(deriveDataMembership({
|
|
428
|
-
dir: dataDir(),
|
|
429
|
-
config: join(dataDir(), "on-zero.config.ts")
|
|
430
|
-
})).resolves.toEqual({
|
|
431
|
-
instances: {
|
|
432
|
-
control: {
|
|
433
|
-
tables: ["account"],
|
|
434
|
-
syncTables: ["account"],
|
|
435
|
-
supportTables: ["audit"],
|
|
436
|
-
scope: null
|
|
437
|
-
},
|
|
438
|
-
project: {
|
|
439
|
-
tables: ["message"],
|
|
440
|
-
syncTables: ["message"],
|
|
441
|
-
supportTables: [],
|
|
442
|
-
scope: "projectId"
|
|
443
|
-
}
|
|
444
|
-
},
|
|
445
|
-
allTables: ["account", "audit", "message"]
|
|
446
|
-
});
|
|
447
|
-
await expect(generate({
|
|
448
|
-
dir: dataDir(),
|
|
449
|
-
config: join(dataDir(), "on-zero.config.ts"),
|
|
450
|
-
silent: true
|
|
451
|
-
})).resolves.toMatchObject({
|
|
452
|
-
modelCount: 1,
|
|
453
|
-
schemaCount: 1
|
|
454
|
-
});
|
|
455
|
-
const models = readFileSync(join(dataDir(), "generated/models.ts"), "utf8");
|
|
456
|
-
expect(models).toContain("from '../../project-data/message'");
|
|
457
|
-
expect(models).not.toContain("account");
|
|
458
|
-
});
|
|
459
278
|
test("rejects missing configured instance directories", async () => {
|
|
460
279
|
writeFileSync(join(dataDir(), "on-zero.config.ts"), `export default defineConfig({ instances: { project: { dir: './missing' } } })`);
|
|
461
280
|
await expect(deriveDataMembership({
|
|
@@ -500,253 +319,6 @@ export const messages = () => zql.message.related('author')
|
|
|
500
319
|
});
|
|
501
320
|
});
|
|
502
321
|
describe("mutations", () => {
|
|
503
|
-
test("generates validators for inline mutation param types", async () => {
|
|
504
|
-
writeFileSync(join(testDir, "post/mutations.ts"), `
|
|
505
|
-
import { table, string } from 'on-zero'
|
|
506
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
507
|
-
|
|
508
|
-
export const schema = table('post').columns({
|
|
509
|
-
id: string(),
|
|
510
|
-
title: string(),
|
|
511
|
-
}).primaryKey('id')
|
|
512
|
-
|
|
513
|
-
const perm = serverWhere('post', () => true)
|
|
514
|
-
|
|
515
|
-
export const mutate = mutations(schema, perm, {
|
|
516
|
-
archive: async ({ tx }, { id, reason }: { id: string; reason: string }) => {
|
|
517
|
-
await tx.mutate.post.update({ id, archived: true })
|
|
518
|
-
},
|
|
519
|
-
})
|
|
520
|
-
`);
|
|
521
|
-
const result = await generate({
|
|
522
|
-
dir: testDir,
|
|
523
|
-
silent: true
|
|
524
|
-
});
|
|
525
|
-
expect(result.mutationCount).toBeGreaterThan(0);
|
|
526
|
-
expect(existsSync(join(testDir, "generated/syncedMutations.ts"))).toBe(true);
|
|
527
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
528
|
-
expect(content).toContain("archive");
|
|
529
|
-
expect(content).toContain("v.object");
|
|
530
|
-
expect(content).toContain("v.string()");
|
|
531
|
-
});
|
|
532
|
-
test("generates CRUD validators from schema columns", async () => {
|
|
533
|
-
writeFileSync(join(testDir, "task/mutations.ts"), `
|
|
534
|
-
import { table, string, number, boolean } from 'on-zero'
|
|
535
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
536
|
-
|
|
537
|
-
export const schema = table('task').columns({
|
|
538
|
-
id: string(),
|
|
539
|
-
title: string(),
|
|
540
|
-
priority: number(),
|
|
541
|
-
done: boolean(),
|
|
542
|
-
note: string().optional(),
|
|
543
|
-
}).primaryKey('id')
|
|
544
|
-
|
|
545
|
-
const perm = serverWhere('task', () => true)
|
|
546
|
-
|
|
547
|
-
export const mutate = mutations(schema, perm)
|
|
548
|
-
`);
|
|
549
|
-
const result = await generate({
|
|
550
|
-
dir: testDir,
|
|
551
|
-
silent: true
|
|
552
|
-
});
|
|
553
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
554
|
-
expect(content).toContain("insert:");
|
|
555
|
-
expect(content).toMatch(/insert:.*v\.object/);
|
|
556
|
-
expect(content).toContain("update:");
|
|
557
|
-
expect(content).toContain("delete:");
|
|
558
|
-
});
|
|
559
|
-
test("treats models without export const mutate as empty mutations", async () => {
|
|
560
|
-
writeFileSync(join(testDir, "readonly/mutations.ts"), `
|
|
561
|
-
import { table, string } from 'on-zero'
|
|
562
|
-
|
|
563
|
-
export const schema = table('readonly').columns({
|
|
564
|
-
id: string(),
|
|
565
|
-
name: string(),
|
|
566
|
-
}).primaryKey('id')
|
|
567
|
-
`);
|
|
568
|
-
const result = await generate({
|
|
569
|
-
dir: testDir,
|
|
570
|
-
silent: true
|
|
571
|
-
});
|
|
572
|
-
expect(result.mutationCount).toBe(0);
|
|
573
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
574
|
-
expect(content).toContain("readonly: {");
|
|
575
|
-
});
|
|
576
|
-
test("extracts custom mutations from bare mutations({})", async () => {
|
|
577
|
-
writeFileSync(join(testDir, "admin/mutations.ts"), `
|
|
578
|
-
import { mutations } from 'on-zero'
|
|
579
|
-
|
|
580
|
-
export const mutate = mutations({
|
|
581
|
-
reset: async ({ tx }, { targetId }: { targetId: string }) => {
|
|
582
|
-
await tx.mutate.admin.delete({ id: targetId })
|
|
583
|
-
},
|
|
584
|
-
})
|
|
585
|
-
`);
|
|
586
|
-
const result = await generate({
|
|
587
|
-
dir: testDir,
|
|
588
|
-
silent: true
|
|
589
|
-
});
|
|
590
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
591
|
-
expect(content).toContain("reset");
|
|
592
|
-
expect(content).toContain("v.string()");
|
|
593
|
-
});
|
|
594
|
-
test("generates validators for string-model multiline mutation params", async () => {
|
|
595
|
-
writeFileSync(join(testDir, "agentEvent/mutations.ts"), `
|
|
596
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
597
|
-
|
|
598
|
-
const perm = serverWhere('agentEvent', () => true)
|
|
599
|
-
|
|
600
|
-
export const mutate = mutations('agentEvent', perm, {
|
|
601
|
-
claimReviewLease: async (
|
|
602
|
-
{ tx },
|
|
603
|
-
props: {
|
|
604
|
-
id: string
|
|
605
|
-
projectId: string
|
|
606
|
-
agentId: string
|
|
607
|
-
userId: string
|
|
608
|
-
taskId: string
|
|
609
|
-
reviewKey: string
|
|
610
|
-
runnerId: string
|
|
611
|
-
createdAt: number
|
|
612
|
-
},
|
|
613
|
-
) => {
|
|
614
|
-
await tx.mutate.agentEvent.insert(props)
|
|
615
|
-
},
|
|
616
|
-
})
|
|
617
|
-
`);
|
|
618
|
-
await generate({
|
|
619
|
-
dir: testDir,
|
|
620
|
-
silent: true
|
|
621
|
-
});
|
|
622
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
623
|
-
expect(content).toContain("claimReviewLease");
|
|
624
|
-
expect(content).toContain("projectId: v.string()");
|
|
625
|
-
expect(content).toContain("createdAt: v.number()");
|
|
626
|
-
expect(content).not.toContain("claimReviewLease: v.object({})");
|
|
627
|
-
});
|
|
628
|
-
test("generates validators for object intersections", async () => {
|
|
629
|
-
writeFileSync(join(testDir, "agent/mutations.ts"), `
|
|
630
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
631
|
-
|
|
632
|
-
const perm = serverWhere('agent', () => true)
|
|
633
|
-
|
|
634
|
-
export const mutate = mutations('agent', perm, {
|
|
635
|
-
update: async (
|
|
636
|
-
{ tx },
|
|
637
|
-
props: {
|
|
638
|
-
id: string
|
|
639
|
-
} & {
|
|
640
|
-
status?: string
|
|
641
|
-
currentTaskId?: string
|
|
642
|
-
[key: string]: unknown
|
|
643
|
-
},
|
|
644
|
-
) => {
|
|
645
|
-
await tx.mutate.agent.update(props)
|
|
646
|
-
},
|
|
647
|
-
})
|
|
648
|
-
`);
|
|
649
|
-
await generate({
|
|
650
|
-
dir: testDir,
|
|
651
|
-
silent: true
|
|
652
|
-
});
|
|
653
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
654
|
-
expect(content).toContain("update");
|
|
655
|
-
expect(content).toContain("id: v.string()");
|
|
656
|
-
expect(content).toContain("status: v.optional(v.string())");
|
|
657
|
-
expect(content).toContain("currentTaskId: v.optional(v.string())");
|
|
658
|
-
expect(content).not.toContain("update: v.object({})");
|
|
659
|
-
expect(content).not.toContain("[key");
|
|
660
|
-
});
|
|
661
|
-
test("uses v.unknown for untyped mutation payloads", async () => {
|
|
662
|
-
writeFileSync(join(testDir, "project/mutations.ts"), `
|
|
663
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
664
|
-
|
|
665
|
-
const perm = serverWhere('project', () => true)
|
|
666
|
-
|
|
667
|
-
export const mutate = mutations('project', perm, {
|
|
668
|
-
insert: async ({ tx }, project) => {
|
|
669
|
-
await tx.mutate.project.insert(project)
|
|
670
|
-
},
|
|
671
|
-
})
|
|
672
|
-
`);
|
|
673
|
-
await generate({
|
|
674
|
-
dir: testDir,
|
|
675
|
-
silent: true
|
|
676
|
-
});
|
|
677
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
678
|
-
expect(content).toContain("insert: v.unknown()");
|
|
679
|
-
expect(content).not.toContain("insert: v.void_()");
|
|
680
|
-
});
|
|
681
|
-
test("handles mutations with only context param (void)", async () => {
|
|
682
|
-
writeFileSync(join(testDir, "user/mutations.ts"), `
|
|
683
|
-
import { table, string } from 'on-zero'
|
|
684
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
685
|
-
|
|
686
|
-
export const schema = table('user').columns({
|
|
687
|
-
id: string(),
|
|
688
|
-
name: string(),
|
|
689
|
-
}).primaryKey('id')
|
|
690
|
-
|
|
691
|
-
const perm = serverWhere('user', () => true)
|
|
692
|
-
|
|
693
|
-
export const mutate = mutations(schema, perm, {
|
|
694
|
-
finishOnboarding: async ({ tx }) => {
|
|
695
|
-
// no second param
|
|
696
|
-
},
|
|
697
|
-
})
|
|
698
|
-
`);
|
|
699
|
-
const result = await generate({
|
|
700
|
-
dir: testDir,
|
|
701
|
-
silent: true
|
|
702
|
-
});
|
|
703
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
704
|
-
expect(content).toContain("finishOnboarding");
|
|
705
|
-
});
|
|
706
|
-
test("handles primitive param type", async () => {
|
|
707
|
-
writeFileSync(join(testDir, "user/mutations.ts"), `
|
|
708
|
-
import { table, string } from 'on-zero'
|
|
709
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
710
|
-
|
|
711
|
-
export const schema = table('user').columns({
|
|
712
|
-
id: string(),
|
|
713
|
-
name: string(),
|
|
714
|
-
}).primaryKey('id')
|
|
715
|
-
|
|
716
|
-
const perm = serverWhere('user', () => true)
|
|
717
|
-
|
|
718
|
-
export const mutate = mutations(schema, perm, {
|
|
719
|
-
completeSignup: async ({ tx }, userId: string) => {
|
|
720
|
-
await tx.mutate.user.update({ id: userId })
|
|
721
|
-
},
|
|
722
|
-
})
|
|
723
|
-
`);
|
|
724
|
-
const result = await generate({
|
|
725
|
-
dir: testDir,
|
|
726
|
-
silent: true
|
|
727
|
-
});
|
|
728
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
729
|
-
expect(content).toContain("completeSignup");
|
|
730
|
-
expect(content).toContain("v.string()");
|
|
731
|
-
});
|
|
732
|
-
test("handles array param type", async () => {
|
|
733
|
-
writeFileSync(join(testDir, "batch/mutations.ts"), `
|
|
734
|
-
import { mutations } from 'on-zero'
|
|
735
|
-
|
|
736
|
-
export const mutate = mutations({
|
|
737
|
-
bulkDelete: async ({ tx }, ids: Array<{ id: string }>) => {
|
|
738
|
-
for (const { id } of ids) await tx.mutate.batch.delete({ id })
|
|
739
|
-
},
|
|
740
|
-
})
|
|
741
|
-
`);
|
|
742
|
-
const result = await generate({
|
|
743
|
-
dir: testDir,
|
|
744
|
-
silent: true
|
|
745
|
-
});
|
|
746
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
747
|
-
expect(content).toContain("bulkDelete");
|
|
748
|
-
expect(content).toContain("v.array");
|
|
749
|
-
});
|
|
750
322
|
test("populates mutationCount and caching works", async () => {
|
|
751
323
|
writeFileSync(join(testDir, "item/mutations.ts"), `
|
|
752
324
|
import { table, string } from 'on-zero'
|
|
@@ -778,242 +350,4 @@ export const mutate = mutations(schema, perm, {
|
|
|
778
350
|
expect(second.mutationCount).toBe(first.mutationCount);
|
|
779
351
|
});
|
|
780
352
|
});
|
|
781
|
-
describe("type resolution", () => {
|
|
782
|
-
test("resolves imported type references for mutations", async () => {
|
|
783
|
-
writeFileSync(join(testDir, "post/types.ts"), `
|
|
784
|
-
export type ArchiveParams = {
|
|
785
|
-
id: string
|
|
786
|
-
reason: string
|
|
787
|
-
archived: boolean
|
|
788
|
-
}
|
|
789
|
-
`);
|
|
790
|
-
writeFileSync(join(testDir, "post/mutations.ts"), `
|
|
791
|
-
import { table, string, boolean } from 'on-zero'
|
|
792
|
-
import { mutations } from 'on-zero'
|
|
793
|
-
import type { ArchiveParams } from './types'
|
|
794
|
-
|
|
795
|
-
export const mutate = mutations({
|
|
796
|
-
archive: async ({ tx }, params: ArchiveParams) => {
|
|
797
|
-
await tx.mutate.post.update(params)
|
|
798
|
-
},
|
|
799
|
-
})
|
|
800
|
-
`);
|
|
801
|
-
const result = await generate({
|
|
802
|
-
dir: testDir,
|
|
803
|
-
silent: true
|
|
804
|
-
});
|
|
805
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
806
|
-
expect(content).toContain("v.string()");
|
|
807
|
-
expect(content).toContain("v.boolean()");
|
|
808
|
-
expect(content).toContain("reason");
|
|
809
|
-
expect(content).toContain("archived");
|
|
810
|
-
});
|
|
811
|
-
test("resolves utility types like Pick", async () => {
|
|
812
|
-
writeFileSync(join(testDir, "item/types.ts"), `
|
|
813
|
-
export type Item = {
|
|
814
|
-
id: string
|
|
815
|
-
name: string
|
|
816
|
-
description: string
|
|
817
|
-
count: number
|
|
818
|
-
}
|
|
819
|
-
`);
|
|
820
|
-
writeFileSync(join(testDir, "item/mutations.ts"), `
|
|
821
|
-
import { table, string, number } from 'on-zero'
|
|
822
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
823
|
-
import type { Item } from './types'
|
|
824
|
-
|
|
825
|
-
export const schema = table('item').columns({
|
|
826
|
-
id: string(),
|
|
827
|
-
name: string(),
|
|
828
|
-
description: string(),
|
|
829
|
-
count: number(),
|
|
830
|
-
}).primaryKey('id')
|
|
831
|
-
|
|
832
|
-
const perm = serverWhere('item', () => true)
|
|
833
|
-
|
|
834
|
-
export const mutate = mutations(schema, perm, {
|
|
835
|
-
rename: async ({ tx }, updates: Pick<Item, 'id' | 'name'>) => {
|
|
836
|
-
await tx.mutate.item.update(updates)
|
|
837
|
-
},
|
|
838
|
-
})
|
|
839
|
-
`);
|
|
840
|
-
const result = await generate({
|
|
841
|
-
dir: testDir,
|
|
842
|
-
silent: true
|
|
843
|
-
});
|
|
844
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
845
|
-
expect(content).toContain("id");
|
|
846
|
-
expect(content).toContain("name");
|
|
847
|
-
expect(content).not.toMatch(/rename:[\s\S]*description/);
|
|
848
|
-
expect(content).not.toMatch(/rename:[\s\S]*count/);
|
|
849
|
-
});
|
|
850
|
-
test("resolves instantiated Zero row fields through Partial and Omit", async () => {
|
|
851
|
-
symlinkSync(join(import.meta.dirname, "../node_modules"), join(testDir, "node_modules"), "dir");
|
|
852
|
-
writeFileSync(join(testDir, "item/types.ts"), `
|
|
853
|
-
import type { Row } from '@rocicorp/zero'
|
|
854
|
-
import { drizzleZeroConfig } from 'drizzle-zero-sqlite'
|
|
855
|
-
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
856
|
-
|
|
857
|
-
const item = sqliteTable('item', {
|
|
858
|
-
id: text().primaryKey(),
|
|
859
|
-
name: text().notNull(),
|
|
860
|
-
description: text().notNull(),
|
|
861
|
-
})
|
|
862
|
-
const schema = drizzleZeroConfig({ item })
|
|
863
|
-
|
|
864
|
-
export type Item = Row<(typeof schema)['tables']['item']>
|
|
865
|
-
`);
|
|
866
|
-
writeFileSync(join(testDir, "item/mutations.ts"), `
|
|
867
|
-
import { mutations } from 'on-zero'
|
|
868
|
-
import type { Item } from './types'
|
|
869
|
-
|
|
870
|
-
type ItemUpdate = { id: string } & Partial<Omit<Item, 'id'>>
|
|
871
|
-
|
|
872
|
-
export const mutate = mutations('item', {
|
|
873
|
-
update: async ({ tx }, props: ItemUpdate) => {
|
|
874
|
-
await tx.mutate.item.update(props)
|
|
875
|
-
},
|
|
876
|
-
})
|
|
877
|
-
`);
|
|
878
|
-
await generate({
|
|
879
|
-
dir: testDir,
|
|
880
|
-
silent: true
|
|
881
|
-
});
|
|
882
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
883
|
-
expect(content).toContain("name: v.optional(v.string())");
|
|
884
|
-
expect(content).toContain("description: v.optional(v.string())");
|
|
885
|
-
expect(content).not.toContain("name: v.optional(v.unknown())");
|
|
886
|
-
expect(content).not.toContain("description: v.optional(v.unknown())");
|
|
887
|
-
});
|
|
888
|
-
test("skips symbol-keyed properties when resolving imported mutation param types", async () => {
|
|
889
|
-
writeFileSync(join(testDir, "item/types.ts"), `
|
|
890
|
-
export type WeirdParams = {
|
|
891
|
-
id: string
|
|
892
|
-
[Symbol.iterator]?: () => Iterator<string>
|
|
893
|
-
}
|
|
894
|
-
`);
|
|
895
|
-
writeFileSync(join(testDir, "item/mutations.ts"), `
|
|
896
|
-
import { mutations } from 'on-zero'
|
|
897
|
-
import type { WeirdParams } from './types'
|
|
898
|
-
|
|
899
|
-
export const mutate = mutations({
|
|
900
|
-
run: async ({ tx }, params: WeirdParams) => {
|
|
901
|
-
await tx.mutate.item.delete({ id: params.id })
|
|
902
|
-
},
|
|
903
|
-
})
|
|
904
|
-
`);
|
|
905
|
-
await generate({
|
|
906
|
-
dir: testDir,
|
|
907
|
-
silent: true
|
|
908
|
-
});
|
|
909
|
-
const content = readFileSync(join(testDir, "generated/syncedMutations.ts"), "utf-8");
|
|
910
|
-
expect(content).toContain("run");
|
|
911
|
-
expect(content).toContain("id: v.string()");
|
|
912
|
-
expect(content).not.toContain("__@iterator");
|
|
913
|
-
});
|
|
914
|
-
test("resolves imported types in query params", async () => {
|
|
915
|
-
writeFileSync(join(testDir, "post/mutations.ts"), `export const schema = table('post', { id: string() })`);
|
|
916
|
-
writeFileSync(join(testDir, "post/types.ts"), `
|
|
917
|
-
export type PostFilter = {
|
|
918
|
-
authorId: string
|
|
919
|
-
published: boolean
|
|
920
|
-
}
|
|
921
|
-
`);
|
|
922
|
-
writeFileSync(join(testDir, "post/queries.ts"), `
|
|
923
|
-
import type { PostFilter } from './types'
|
|
924
|
-
|
|
925
|
-
export const filteredPosts = (filter: PostFilter) => zero.query.post
|
|
926
|
-
`);
|
|
927
|
-
const result = await generate({
|
|
928
|
-
dir: testDir,
|
|
929
|
-
silent: true
|
|
930
|
-
});
|
|
931
|
-
const content = readFileSync(join(testDir, "generated/syncedQueries.ts"), "utf-8");
|
|
932
|
-
expect(content).toContain("filteredPosts");
|
|
933
|
-
expect(content).toContain("v.object");
|
|
934
|
-
expect(content).toContain("authorId");
|
|
935
|
-
expect(content).toContain("v.boolean()");
|
|
936
|
-
});
|
|
937
|
-
});
|
|
938
|
-
describe("generateDrizzleSchemaFile", () => {
|
|
939
|
-
test("preserves table server names and executable two-hop relationships", () => {
|
|
940
|
-
const schema = {
|
|
941
|
-
tables: {
|
|
942
|
-
users: {
|
|
943
|
-
name: "users",
|
|
944
|
-
serverName: "user_records",
|
|
945
|
-
primaryKey: ["id"],
|
|
946
|
-
columns: {
|
|
947
|
-
id: {
|
|
948
|
-
type: "string",
|
|
949
|
-
optional: false,
|
|
950
|
-
customType: null
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
},
|
|
954
|
-
groups: {
|
|
955
|
-
name: "groups",
|
|
956
|
-
primaryKey: ["id"],
|
|
957
|
-
columns: {
|
|
958
|
-
id: {
|
|
959
|
-
type: "string",
|
|
960
|
-
optional: false,
|
|
961
|
-
customType: null
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
},
|
|
965
|
-
memberships: {
|
|
966
|
-
name: "memberships",
|
|
967
|
-
primaryKey: ["userId", "groupId"],
|
|
968
|
-
columns: {
|
|
969
|
-
userId: {
|
|
970
|
-
type: "string",
|
|
971
|
-
optional: false,
|
|
972
|
-
customType: null
|
|
973
|
-
},
|
|
974
|
-
groupId: {
|
|
975
|
-
type: "string",
|
|
976
|
-
optional: false,
|
|
977
|
-
customType: null
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
},
|
|
982
|
-
relationships: {
|
|
983
|
-
users: {
|
|
984
|
-
groups: [{
|
|
985
|
-
sourceField: ["id"],
|
|
986
|
-
destField: ["userId"],
|
|
987
|
-
destSchema: "memberships",
|
|
988
|
-
cardinality: "many"
|
|
989
|
-
}, {
|
|
990
|
-
sourceField: ["groupId"],
|
|
991
|
-
destField: ["id"],
|
|
992
|
-
destSchema: "groups",
|
|
993
|
-
cardinality: "many"
|
|
994
|
-
}]
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
|
-
const source = generateDrizzleSchemaFile(schema);
|
|
999
|
-
const executableSource = source.replace("import { boolean, createSchema, json, number, relationships, string, table } from '@rocicorp/zero'", "").replace("export const schema =", "globalThis.generatedSchema =");
|
|
1000
|
-
const context = {
|
|
1001
|
-
...zero
|
|
1002
|
-
};
|
|
1003
|
-
runInNewContext(executableSource, context);
|
|
1004
|
-
expect(source).toContain('const usersTable = table("users").from("user_records")');
|
|
1005
|
-
expect(context.generatedSchema).toMatchObject({
|
|
1006
|
-
tables: {
|
|
1007
|
-
users: {
|
|
1008
|
-
serverName: "user_records"
|
|
1009
|
-
}
|
|
1010
|
-
},
|
|
1011
|
-
relationships: {
|
|
1012
|
-
users: {
|
|
1013
|
-
groups: schema.relationships.users.groups
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
});
|
|
1017
|
-
});
|
|
1018
|
-
});
|
|
1019
353
|
//# sourceMappingURL=generate.test.mjs.map
|