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
package/src/generate.test.ts
CHANGED
|
@@ -37,168 +37,6 @@ afterEach(() => {
|
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
describe('generate', () => {
|
|
40
|
-
test('generates models.ts, types.ts, tables.ts from model files', async () => {
|
|
41
|
-
writeFileSync(
|
|
42
|
-
join(testDir, 'post/mutations.ts'),
|
|
43
|
-
`
|
|
44
|
-
import { table, string, boolean } from 'on-zero'
|
|
45
|
-
|
|
46
|
-
export const schema = table('post', {
|
|
47
|
-
id: string(),
|
|
48
|
-
title: string(),
|
|
49
|
-
published: boolean(),
|
|
50
|
-
})
|
|
51
|
-
`
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
writeFileSync(
|
|
55
|
-
join(testDir, 'comment/mutations.ts'),
|
|
56
|
-
`
|
|
57
|
-
import { table, string } from 'on-zero'
|
|
58
|
-
|
|
59
|
-
export const schema = table('comment', {
|
|
60
|
-
id: string(),
|
|
61
|
-
postId: string(),
|
|
62
|
-
body: string(),
|
|
63
|
-
})
|
|
64
|
-
`
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
writeFileSync(
|
|
68
|
-
join(testDir, 'post/post.test.ts'),
|
|
69
|
-
`throw new Error('test files must not be generated as models')`
|
|
70
|
-
)
|
|
71
|
-
writeFileSync(
|
|
72
|
-
join(testDir, 'comment/comment.spec.ts'),
|
|
73
|
-
`throw new Error('spec files must not be generated as queries')`
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
77
|
-
|
|
78
|
-
expect(result.modelCount).toBe(2)
|
|
79
|
-
expect(result.schemaCount).toBe(2)
|
|
80
|
-
expect(result.filesChanged).toBeGreaterThan(0)
|
|
81
|
-
|
|
82
|
-
// check generated files exist
|
|
83
|
-
expect(existsSync(join(testDir, 'generated/models.ts'))).toBe(true)
|
|
84
|
-
expect(existsSync(join(testDir, 'generated/types.ts'))).toBe(true)
|
|
85
|
-
expect(existsSync(join(testDir, 'generated/tables.ts'))).toBe(true)
|
|
86
|
-
|
|
87
|
-
// check models.ts content
|
|
88
|
-
const modelsContent = readFileSync(join(testDir, 'generated/models.ts'), 'utf-8')
|
|
89
|
-
expect(modelsContent).toContain("import * as comment from '../comment/mutations'")
|
|
90
|
-
expect(modelsContent).toContain("import * as post from '../post/mutations'")
|
|
91
|
-
expect(modelsContent).not.toContain('post.test')
|
|
92
|
-
expect(modelsContent).toContain('export const models = {')
|
|
93
|
-
|
|
94
|
-
// check types.ts content
|
|
95
|
-
const typesContent = readFileSync(join(testDir, 'generated/types.ts'), 'utf-8')
|
|
96
|
-
expect(typesContent).toContain(
|
|
97
|
-
'export type Post = TableInsertRow<typeof schema.post>'
|
|
98
|
-
)
|
|
99
|
-
expect(typesContent).toContain(
|
|
100
|
-
'export type Comment = TableInsertRow<typeof schema.comment>'
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
// check tables.ts content
|
|
104
|
-
const tablesContent = readFileSync(join(testDir, 'generated/tables.ts'), 'utf-8')
|
|
105
|
-
expect(tablesContent).toContain("export { schema as post } from '../post/mutations'")
|
|
106
|
-
expect(tablesContent).toContain(
|
|
107
|
-
"export { schema as comment } from '../comment/mutations'"
|
|
108
|
-
)
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
test('generates query validators from query files', async () => {
|
|
112
|
-
// need at least one model
|
|
113
|
-
writeFileSync(
|
|
114
|
-
join(testDir, 'post/mutations.ts'),
|
|
115
|
-
`export const schema = table('post', { id: string() })`
|
|
116
|
-
)
|
|
117
|
-
|
|
118
|
-
writeFileSync(
|
|
119
|
-
join(testDir, 'post/queries.ts'),
|
|
120
|
-
`
|
|
121
|
-
import { zero } from '../zero'
|
|
122
|
-
|
|
123
|
-
export const allPosts = () => zero.query.post
|
|
124
|
-
|
|
125
|
-
export const postById = ({ id }: { id: string }) => zero.query.post.where('id', id)
|
|
126
|
-
|
|
127
|
-
export const postsByAuthor = ({ authorId, limit }: { authorId: string; limit?: number }) =>
|
|
128
|
-
zero.query.post.where('authorId', authorId).limit(limit ?? 10)
|
|
129
|
-
`
|
|
130
|
-
)
|
|
131
|
-
|
|
132
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
133
|
-
|
|
134
|
-
expect(result.queryCount).toBe(3)
|
|
135
|
-
|
|
136
|
-
// check query files exist
|
|
137
|
-
expect(existsSync(join(testDir, 'generated/groupedQueries.ts'))).toBe(true)
|
|
138
|
-
expect(existsSync(join(testDir, 'generated/syncedQueries.ts'))).toBe(true)
|
|
139
|
-
|
|
140
|
-
// check groupedQueries.ts
|
|
141
|
-
const groupedContent = readFileSync(
|
|
142
|
-
join(testDir, 'generated/groupedQueries.ts'),
|
|
143
|
-
'utf-8'
|
|
144
|
-
)
|
|
145
|
-
expect(groupedContent).toContain("import * as postSource from '../post/queries'")
|
|
146
|
-
expect(groupedContent).toContain('postById: postSource.postById')
|
|
147
|
-
|
|
148
|
-
// check syncedQueries.ts has validators
|
|
149
|
-
const syncedContent = readFileSync(
|
|
150
|
-
join(testDir, 'generated/syncedQueries.ts'),
|
|
151
|
-
'utf-8'
|
|
152
|
-
)
|
|
153
|
-
expect(syncedContent).toContain('allPosts: defineQuery')
|
|
154
|
-
expect(syncedContent).toContain('postById: defineQuery')
|
|
155
|
-
expect(syncedContent).toContain('postsByAuthor: defineQuery')
|
|
156
|
-
expect(syncedContent).toContain('v.object')
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
test('skips permission exports in queries', async () => {
|
|
160
|
-
writeFileSync(
|
|
161
|
-
join(testDir, 'post/mutations.ts'),
|
|
162
|
-
`export const schema = table('post', { id: string() })`
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
writeFileSync(
|
|
166
|
-
join(testDir, 'post/queries.ts'),
|
|
167
|
-
`
|
|
168
|
-
export const permission = () => ({ canRead: true })
|
|
169
|
-
export const allPosts = () => zero.query.post
|
|
170
|
-
`
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
174
|
-
|
|
175
|
-
expect(result.queryCount).toBe(1)
|
|
176
|
-
|
|
177
|
-
const syncedContent = readFileSync(
|
|
178
|
-
join(testDir, 'generated/syncedQueries.ts'),
|
|
179
|
-
'utf-8'
|
|
180
|
-
)
|
|
181
|
-
expect(syncedContent).toContain('allPosts')
|
|
182
|
-
expect(syncedContent).not.toContain('permission:')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
test('aliases user import without changing the model key', async () => {
|
|
186
|
-
writeFileSync(
|
|
187
|
-
join(testDir, 'user/mutations.ts'),
|
|
188
|
-
`export const schema = table('user', { id: string(), name: string() })`
|
|
189
|
-
)
|
|
190
|
-
|
|
191
|
-
await generate({ dir: testDir, silent: true })
|
|
192
|
-
|
|
193
|
-
const modelsContent = readFileSync(join(testDir, 'generated/models.ts'), 'utf-8')
|
|
194
|
-
expect(modelsContent).toContain("import * as userPublic from '../user/mutations'")
|
|
195
|
-
expect(modelsContent).toContain('user: userPublic,')
|
|
196
|
-
expect(modelsContent).not.toContain('\n userPublic,')
|
|
197
|
-
|
|
198
|
-
const typesContent = readFileSync(join(testDir, 'generated/types.ts'), 'utf-8')
|
|
199
|
-
expect(typesContent).toContain('typeof schema.userPublic')
|
|
200
|
-
})
|
|
201
|
-
|
|
202
40
|
test('runs after command when files change', async () => {
|
|
203
41
|
writeFileSync(
|
|
204
42
|
join(testDir, 'post/mutations.ts'),
|
|
@@ -252,74 +90,6 @@ export const allPosts = () => zero.query.post
|
|
|
252
90
|
describe('instance layout', () => {
|
|
253
91
|
const dataDir = () => join(testDir, 'src/data')
|
|
254
92
|
|
|
255
|
-
test('discovers file and folder namespaces and emits related sync closure', async () => {
|
|
256
|
-
writeFileSync(
|
|
257
|
-
join(dataDir(), 'control/reaction.ts'),
|
|
258
|
-
`export const allReactions = () => zql.reaction`
|
|
259
|
-
)
|
|
260
|
-
writeFileSync(
|
|
261
|
-
join(dataDir(), 'on-zero.config.ts'),
|
|
262
|
-
`export default defineConfig({ instances: { control: {}, project: { scope: 'projectId' } } })`
|
|
263
|
-
)
|
|
264
|
-
writeFileSync(
|
|
265
|
-
join(dataDir(), 'project/message/queries.ts'),
|
|
266
|
-
`
|
|
267
|
-
const unused = () => zql.message.related('notARealRelation')
|
|
268
|
-
const withComments = () => zql.message.related('comments', (q) => q.related('author'))
|
|
269
|
-
export const messages = () => withComments()
|
|
270
|
-
`
|
|
271
|
-
)
|
|
272
|
-
writeFileSync(
|
|
273
|
-
join(dataDir(), 'project/message/mutations.ts'),
|
|
274
|
-
`export const schema = table('message').columns({ id: string(), projectId: string() })`
|
|
275
|
-
)
|
|
276
|
-
writeFileSync(
|
|
277
|
-
join(dataDir(), 'project/message/helpers.ts'),
|
|
278
|
-
`export const privateHelper = () => 'ignored'`
|
|
279
|
-
)
|
|
280
|
-
writeFileSync(
|
|
281
|
-
join(testDir, 'src/database/relations.ts'),
|
|
282
|
-
`
|
|
283
|
-
export const relations = defineRelations(schema, (r) => ({
|
|
284
|
-
message: { comments: r.many.comment({}) },
|
|
285
|
-
comment: { author: r.one.userPublic({}) },
|
|
286
|
-
}))
|
|
287
|
-
`
|
|
288
|
-
)
|
|
289
|
-
writeFileSync(
|
|
290
|
-
join(testDir, 'src/database/schema.ts'),
|
|
291
|
-
`
|
|
292
|
-
export const comment = sqliteTable('comment', { id: text(), projectId: text() })
|
|
293
|
-
export const userPublic = sqliteTable('user_public', { id: text(), projectId: text() })
|
|
294
|
-
`
|
|
295
|
-
)
|
|
296
|
-
|
|
297
|
-
const membership = await deriveDataMembership({ dir: dataDir() })
|
|
298
|
-
expect(membership.allTables).toEqual(['comment', 'message', 'reaction', 'userPublic'])
|
|
299
|
-
expect(membership.instances.project).toEqual({
|
|
300
|
-
tables: ['message'],
|
|
301
|
-
syncTables: ['comment', 'message', 'userPublic'],
|
|
302
|
-
supportTables: [],
|
|
303
|
-
scope: 'projectId',
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
await generate({ dir: dataDir(), silent: true })
|
|
307
|
-
|
|
308
|
-
const grouped = readFileSync(join(dataDir(), 'generated/groupedQueries.ts'), 'utf8')
|
|
309
|
-
expect(grouped).toContain("from '../control/reaction'")
|
|
310
|
-
expect(grouped).toContain("from '../project/message/queries'")
|
|
311
|
-
expect(grouped).not.toContain('privateHelper')
|
|
312
|
-
|
|
313
|
-
const manifest = readFileSync(join(dataDir(), 'generated/instances.ts'), 'utf8')
|
|
314
|
-
expect(manifest).toContain('control: {')
|
|
315
|
-
expect(manifest).toContain('project: {')
|
|
316
|
-
expect(manifest).toContain('tables: ["message"]')
|
|
317
|
-
expect(manifest).toContain('syncTables: ["comment","message","userPublic"]')
|
|
318
|
-
expect(manifest).toContain(
|
|
319
|
-
`defaultVisibility: (value: string) => ({ column: "projectId", value })`
|
|
320
|
-
)
|
|
321
|
-
})
|
|
322
|
-
|
|
323
93
|
test('derives query membership from the returned query root', async () => {
|
|
324
94
|
writeFileSync(
|
|
325
95
|
join(dataDir(), 'category/mutations.ts'),
|
|
@@ -603,57 +373,6 @@ export const messages = () => zql.message.related('author')
|
|
|
603
373
|
)
|
|
604
374
|
})
|
|
605
375
|
|
|
606
|
-
test('resolves configured instance directories relative to the config file', async () => {
|
|
607
|
-
writeFileSync(
|
|
608
|
-
join(dataDir(), 'on-zero.config.ts'),
|
|
609
|
-
`export default defineConfig({ instances: { control: { dir: './planes/control-data', supportTables: ['audit'] }, project: { dir: '../project-data', scope: 'projectId' } } })`
|
|
610
|
-
)
|
|
611
|
-
writeFileSync(
|
|
612
|
-
join(dataDir(), 'planes/control-data/account.ts'),
|
|
613
|
-
`export const accounts = () => zql.account`
|
|
614
|
-
)
|
|
615
|
-
writeFileSync(
|
|
616
|
-
join(testDir, 'src/project-data/message.ts'),
|
|
617
|
-
`export const schema = table('message').columns({ id: string(), projectId: string() })`
|
|
618
|
-
)
|
|
619
|
-
|
|
620
|
-
await expect(
|
|
621
|
-
deriveDataMembership({
|
|
622
|
-
dir: dataDir(),
|
|
623
|
-
config: join(dataDir(), 'on-zero.config.ts'),
|
|
624
|
-
})
|
|
625
|
-
).resolves.toEqual({
|
|
626
|
-
instances: {
|
|
627
|
-
control: {
|
|
628
|
-
tables: ['account'],
|
|
629
|
-
syncTables: ['account'],
|
|
630
|
-
supportTables: ['audit'],
|
|
631
|
-
scope: null,
|
|
632
|
-
},
|
|
633
|
-
project: {
|
|
634
|
-
tables: ['message'],
|
|
635
|
-
syncTables: ['message'],
|
|
636
|
-
supportTables: [],
|
|
637
|
-
scope: 'projectId',
|
|
638
|
-
},
|
|
639
|
-
},
|
|
640
|
-
allTables: ['account', 'audit', 'message'],
|
|
641
|
-
})
|
|
642
|
-
|
|
643
|
-
// only message.ts is a model: account.ts exports a query and no mutate,
|
|
644
|
-
// where, or table declaration, so it must not enter models.ts
|
|
645
|
-
await expect(
|
|
646
|
-
generate({
|
|
647
|
-
dir: dataDir(),
|
|
648
|
-
config: join(dataDir(), 'on-zero.config.ts'),
|
|
649
|
-
silent: true,
|
|
650
|
-
})
|
|
651
|
-
).resolves.toMatchObject({ modelCount: 1, schemaCount: 1 })
|
|
652
|
-
const models = readFileSync(join(dataDir(), 'generated/models.ts'), 'utf8')
|
|
653
|
-
expect(models).toContain("from '../../project-data/message'")
|
|
654
|
-
expect(models).not.toContain('account')
|
|
655
|
-
})
|
|
656
|
-
|
|
657
376
|
test('rejects missing configured instance directories', async () => {
|
|
658
377
|
writeFileSync(
|
|
659
378
|
join(dataDir(), 'on-zero.config.ts'),
|
|
@@ -732,291 +451,6 @@ export const messages = () => zql.message.related('author')
|
|
|
732
451
|
})
|
|
733
452
|
|
|
734
453
|
describe('mutations', () => {
|
|
735
|
-
test('generates validators for inline mutation param types', async () => {
|
|
736
|
-
writeFileSync(
|
|
737
|
-
join(testDir, 'post/mutations.ts'),
|
|
738
|
-
`
|
|
739
|
-
import { table, string } from 'on-zero'
|
|
740
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
741
|
-
|
|
742
|
-
export const schema = table('post').columns({
|
|
743
|
-
id: string(),
|
|
744
|
-
title: string(),
|
|
745
|
-
}).primaryKey('id')
|
|
746
|
-
|
|
747
|
-
const perm = serverWhere('post', () => true)
|
|
748
|
-
|
|
749
|
-
export const mutate = mutations(schema, perm, {
|
|
750
|
-
archive: async ({ tx }, { id, reason }: { id: string; reason: string }) => {
|
|
751
|
-
await tx.mutate.post.update({ id, archived: true })
|
|
752
|
-
},
|
|
753
|
-
})
|
|
754
|
-
`
|
|
755
|
-
)
|
|
756
|
-
|
|
757
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
758
|
-
|
|
759
|
-
expect(result.mutationCount).toBeGreaterThan(0)
|
|
760
|
-
expect(existsSync(join(testDir, 'generated/syncedMutations.ts'))).toBe(true)
|
|
761
|
-
|
|
762
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
763
|
-
expect(content).toContain('archive')
|
|
764
|
-
expect(content).toContain('v.object')
|
|
765
|
-
expect(content).toContain('v.string()')
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
test('generates CRUD validators from schema columns', async () => {
|
|
769
|
-
writeFileSync(
|
|
770
|
-
join(testDir, 'task/mutations.ts'),
|
|
771
|
-
`
|
|
772
|
-
import { table, string, number, boolean } from 'on-zero'
|
|
773
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
774
|
-
|
|
775
|
-
export const schema = table('task').columns({
|
|
776
|
-
id: string(),
|
|
777
|
-
title: string(),
|
|
778
|
-
priority: number(),
|
|
779
|
-
done: boolean(),
|
|
780
|
-
note: string().optional(),
|
|
781
|
-
}).primaryKey('id')
|
|
782
|
-
|
|
783
|
-
const perm = serverWhere('task', () => true)
|
|
784
|
-
|
|
785
|
-
export const mutate = mutations(schema, perm)
|
|
786
|
-
`
|
|
787
|
-
)
|
|
788
|
-
|
|
789
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
790
|
-
|
|
791
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
792
|
-
|
|
793
|
-
// insert: all columns present
|
|
794
|
-
expect(content).toContain('insert:')
|
|
795
|
-
expect(content).toMatch(/insert:.*v\.object/)
|
|
796
|
-
|
|
797
|
-
// update: id required, rest optional
|
|
798
|
-
expect(content).toContain('update:')
|
|
799
|
-
|
|
800
|
-
// delete: only PK
|
|
801
|
-
expect(content).toContain('delete:')
|
|
802
|
-
})
|
|
803
|
-
|
|
804
|
-
test('treats models without export const mutate as empty mutations', async () => {
|
|
805
|
-
writeFileSync(
|
|
806
|
-
join(testDir, 'readonly/mutations.ts'),
|
|
807
|
-
`
|
|
808
|
-
import { table, string } from 'on-zero'
|
|
809
|
-
|
|
810
|
-
export const schema = table('readonly').columns({
|
|
811
|
-
id: string(),
|
|
812
|
-
name: string(),
|
|
813
|
-
}).primaryKey('id')
|
|
814
|
-
`
|
|
815
|
-
)
|
|
816
|
-
|
|
817
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
818
|
-
expect(result.mutationCount).toBe(0)
|
|
819
|
-
|
|
820
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
821
|
-
expect(content).toContain('readonly: {')
|
|
822
|
-
})
|
|
823
|
-
|
|
824
|
-
test('extracts custom mutations from bare mutations({})', async () => {
|
|
825
|
-
writeFileSync(
|
|
826
|
-
join(testDir, 'admin/mutations.ts'),
|
|
827
|
-
`
|
|
828
|
-
import { mutations } from 'on-zero'
|
|
829
|
-
|
|
830
|
-
export const mutate = mutations({
|
|
831
|
-
reset: async ({ tx }, { targetId }: { targetId: string }) => {
|
|
832
|
-
await tx.mutate.admin.delete({ id: targetId })
|
|
833
|
-
},
|
|
834
|
-
})
|
|
835
|
-
`
|
|
836
|
-
)
|
|
837
|
-
|
|
838
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
839
|
-
|
|
840
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
841
|
-
expect(content).toContain('reset')
|
|
842
|
-
expect(content).toContain('v.string()')
|
|
843
|
-
})
|
|
844
|
-
|
|
845
|
-
test('generates validators for string-model multiline mutation params', async () => {
|
|
846
|
-
writeFileSync(
|
|
847
|
-
join(testDir, 'agentEvent/mutations.ts'),
|
|
848
|
-
`
|
|
849
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
850
|
-
|
|
851
|
-
const perm = serverWhere('agentEvent', () => true)
|
|
852
|
-
|
|
853
|
-
export const mutate = mutations('agentEvent', perm, {
|
|
854
|
-
claimReviewLease: async (
|
|
855
|
-
{ tx },
|
|
856
|
-
props: {
|
|
857
|
-
id: string
|
|
858
|
-
projectId: string
|
|
859
|
-
agentId: string
|
|
860
|
-
userId: string
|
|
861
|
-
taskId: string
|
|
862
|
-
reviewKey: string
|
|
863
|
-
runnerId: string
|
|
864
|
-
createdAt: number
|
|
865
|
-
},
|
|
866
|
-
) => {
|
|
867
|
-
await tx.mutate.agentEvent.insert(props)
|
|
868
|
-
},
|
|
869
|
-
})
|
|
870
|
-
`
|
|
871
|
-
)
|
|
872
|
-
|
|
873
|
-
await generate({ dir: testDir, silent: true })
|
|
874
|
-
|
|
875
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
876
|
-
expect(content).toContain('claimReviewLease')
|
|
877
|
-
expect(content).toContain('projectId: v.string()')
|
|
878
|
-
expect(content).toContain('createdAt: v.number()')
|
|
879
|
-
expect(content).not.toContain('claimReviewLease: v.object({})')
|
|
880
|
-
})
|
|
881
|
-
|
|
882
|
-
test('generates validators for object intersections', async () => {
|
|
883
|
-
writeFileSync(
|
|
884
|
-
join(testDir, 'agent/mutations.ts'),
|
|
885
|
-
`
|
|
886
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
887
|
-
|
|
888
|
-
const perm = serverWhere('agent', () => true)
|
|
889
|
-
|
|
890
|
-
export const mutate = mutations('agent', perm, {
|
|
891
|
-
update: async (
|
|
892
|
-
{ tx },
|
|
893
|
-
props: {
|
|
894
|
-
id: string
|
|
895
|
-
} & {
|
|
896
|
-
status?: string
|
|
897
|
-
currentTaskId?: string
|
|
898
|
-
[key: string]: unknown
|
|
899
|
-
},
|
|
900
|
-
) => {
|
|
901
|
-
await tx.mutate.agent.update(props)
|
|
902
|
-
},
|
|
903
|
-
})
|
|
904
|
-
`
|
|
905
|
-
)
|
|
906
|
-
|
|
907
|
-
await generate({ dir: testDir, silent: true })
|
|
908
|
-
|
|
909
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
910
|
-
expect(content).toContain('update')
|
|
911
|
-
expect(content).toContain('id: v.string()')
|
|
912
|
-
expect(content).toContain('status: v.optional(v.string())')
|
|
913
|
-
expect(content).toContain('currentTaskId: v.optional(v.string())')
|
|
914
|
-
expect(content).not.toContain('update: v.object({})')
|
|
915
|
-
expect(content).not.toContain('[key')
|
|
916
|
-
})
|
|
917
|
-
|
|
918
|
-
test('uses v.unknown for untyped mutation payloads', async () => {
|
|
919
|
-
writeFileSync(
|
|
920
|
-
join(testDir, 'project/mutations.ts'),
|
|
921
|
-
`
|
|
922
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
923
|
-
|
|
924
|
-
const perm = serverWhere('project', () => true)
|
|
925
|
-
|
|
926
|
-
export const mutate = mutations('project', perm, {
|
|
927
|
-
insert: async ({ tx }, project) => {
|
|
928
|
-
await tx.mutate.project.insert(project)
|
|
929
|
-
},
|
|
930
|
-
})
|
|
931
|
-
`
|
|
932
|
-
)
|
|
933
|
-
|
|
934
|
-
await generate({ dir: testDir, silent: true })
|
|
935
|
-
|
|
936
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
937
|
-
expect(content).toContain('insert: v.unknown()')
|
|
938
|
-
expect(content).not.toContain('insert: v.void_()')
|
|
939
|
-
})
|
|
940
|
-
|
|
941
|
-
test('handles mutations with only context param (void)', async () => {
|
|
942
|
-
writeFileSync(
|
|
943
|
-
join(testDir, 'user/mutations.ts'),
|
|
944
|
-
`
|
|
945
|
-
import { table, string } from 'on-zero'
|
|
946
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
947
|
-
|
|
948
|
-
export const schema = table('user').columns({
|
|
949
|
-
id: string(),
|
|
950
|
-
name: string(),
|
|
951
|
-
}).primaryKey('id')
|
|
952
|
-
|
|
953
|
-
const perm = serverWhere('user', () => true)
|
|
954
|
-
|
|
955
|
-
export const mutate = mutations(schema, perm, {
|
|
956
|
-
finishOnboarding: async ({ tx }) => {
|
|
957
|
-
// no second param
|
|
958
|
-
},
|
|
959
|
-
})
|
|
960
|
-
`
|
|
961
|
-
)
|
|
962
|
-
|
|
963
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
964
|
-
|
|
965
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
966
|
-
// should not crash, void mutations get no validator
|
|
967
|
-
expect(content).toContain('finishOnboarding')
|
|
968
|
-
})
|
|
969
|
-
|
|
970
|
-
test('handles primitive param type', async () => {
|
|
971
|
-
writeFileSync(
|
|
972
|
-
join(testDir, 'user/mutations.ts'),
|
|
973
|
-
`
|
|
974
|
-
import { table, string } from 'on-zero'
|
|
975
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
976
|
-
|
|
977
|
-
export const schema = table('user').columns({
|
|
978
|
-
id: string(),
|
|
979
|
-
name: string(),
|
|
980
|
-
}).primaryKey('id')
|
|
981
|
-
|
|
982
|
-
const perm = serverWhere('user', () => true)
|
|
983
|
-
|
|
984
|
-
export const mutate = mutations(schema, perm, {
|
|
985
|
-
completeSignup: async ({ tx }, userId: string) => {
|
|
986
|
-
await tx.mutate.user.update({ id: userId })
|
|
987
|
-
},
|
|
988
|
-
})
|
|
989
|
-
`
|
|
990
|
-
)
|
|
991
|
-
|
|
992
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
993
|
-
|
|
994
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
995
|
-
expect(content).toContain('completeSignup')
|
|
996
|
-
expect(content).toContain('v.string()')
|
|
997
|
-
})
|
|
998
|
-
|
|
999
|
-
test('handles array param type', async () => {
|
|
1000
|
-
writeFileSync(
|
|
1001
|
-
join(testDir, 'batch/mutations.ts'),
|
|
1002
|
-
`
|
|
1003
|
-
import { mutations } from 'on-zero'
|
|
1004
|
-
|
|
1005
|
-
export const mutate = mutations({
|
|
1006
|
-
bulkDelete: async ({ tx }, ids: Array<{ id: string }>) => {
|
|
1007
|
-
for (const { id } of ids) await tx.mutate.batch.delete({ id })
|
|
1008
|
-
},
|
|
1009
|
-
})
|
|
1010
|
-
`
|
|
1011
|
-
)
|
|
1012
|
-
|
|
1013
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
1014
|
-
|
|
1015
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
1016
|
-
expect(content).toContain('bulkDelete')
|
|
1017
|
-
expect(content).toContain('v.array')
|
|
1018
|
-
})
|
|
1019
|
-
|
|
1020
454
|
test('populates mutationCount and caching works', async () => {
|
|
1021
455
|
writeFileSync(
|
|
1022
456
|
join(testDir, 'item/mutations.ts'),
|
|
@@ -1047,281 +481,3 @@ export const mutate = mutations(schema, perm, {
|
|
|
1047
481
|
expect(second.mutationCount).toBe(first.mutationCount)
|
|
1048
482
|
})
|
|
1049
483
|
})
|
|
1050
|
-
|
|
1051
|
-
describe('type resolution', () => {
|
|
1052
|
-
test('resolves imported type references for mutations', async () => {
|
|
1053
|
-
// types file that the model imports from
|
|
1054
|
-
writeFileSync(
|
|
1055
|
-
join(testDir, 'post/types.ts'),
|
|
1056
|
-
`
|
|
1057
|
-
export type ArchiveParams = {
|
|
1058
|
-
id: string
|
|
1059
|
-
reason: string
|
|
1060
|
-
archived: boolean
|
|
1061
|
-
}
|
|
1062
|
-
`
|
|
1063
|
-
)
|
|
1064
|
-
|
|
1065
|
-
writeFileSync(
|
|
1066
|
-
join(testDir, 'post/mutations.ts'),
|
|
1067
|
-
`
|
|
1068
|
-
import { table, string, boolean } from 'on-zero'
|
|
1069
|
-
import { mutations } from 'on-zero'
|
|
1070
|
-
import type { ArchiveParams } from './types'
|
|
1071
|
-
|
|
1072
|
-
export const mutate = mutations({
|
|
1073
|
-
archive: async ({ tx }, params: ArchiveParams) => {
|
|
1074
|
-
await tx.mutate.post.update(params)
|
|
1075
|
-
},
|
|
1076
|
-
})
|
|
1077
|
-
`
|
|
1078
|
-
)
|
|
1079
|
-
|
|
1080
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
1081
|
-
|
|
1082
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
1083
|
-
// the imported ArchiveParams type should be resolved to its fields
|
|
1084
|
-
expect(content).toContain('v.string()')
|
|
1085
|
-
expect(content).toContain('v.boolean()')
|
|
1086
|
-
expect(content).toContain('reason')
|
|
1087
|
-
expect(content).toContain('archived')
|
|
1088
|
-
})
|
|
1089
|
-
|
|
1090
|
-
test('resolves utility types like Pick', async () => {
|
|
1091
|
-
writeFileSync(
|
|
1092
|
-
join(testDir, 'item/types.ts'),
|
|
1093
|
-
`
|
|
1094
|
-
export type Item = {
|
|
1095
|
-
id: string
|
|
1096
|
-
name: string
|
|
1097
|
-
description: string
|
|
1098
|
-
count: number
|
|
1099
|
-
}
|
|
1100
|
-
`
|
|
1101
|
-
)
|
|
1102
|
-
|
|
1103
|
-
writeFileSync(
|
|
1104
|
-
join(testDir, 'item/mutations.ts'),
|
|
1105
|
-
`
|
|
1106
|
-
import { table, string, number } from 'on-zero'
|
|
1107
|
-
import { mutations, serverWhere } from 'on-zero'
|
|
1108
|
-
import type { Item } from './types'
|
|
1109
|
-
|
|
1110
|
-
export const schema = table('item').columns({
|
|
1111
|
-
id: string(),
|
|
1112
|
-
name: string(),
|
|
1113
|
-
description: string(),
|
|
1114
|
-
count: number(),
|
|
1115
|
-
}).primaryKey('id')
|
|
1116
|
-
|
|
1117
|
-
const perm = serverWhere('item', () => true)
|
|
1118
|
-
|
|
1119
|
-
export const mutate = mutations(schema, perm, {
|
|
1120
|
-
rename: async ({ tx }, updates: Pick<Item, 'id' | 'name'>) => {
|
|
1121
|
-
await tx.mutate.item.update(updates)
|
|
1122
|
-
},
|
|
1123
|
-
})
|
|
1124
|
-
`
|
|
1125
|
-
)
|
|
1126
|
-
|
|
1127
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
1128
|
-
|
|
1129
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
1130
|
-
// Pick should resolve to only id and name
|
|
1131
|
-
expect(content).toContain('id')
|
|
1132
|
-
expect(content).toContain('name')
|
|
1133
|
-
// description and count should NOT be in the rename validator
|
|
1134
|
-
expect(content).not.toMatch(/rename:[\s\S]*description/)
|
|
1135
|
-
expect(content).not.toMatch(/rename:[\s\S]*count/)
|
|
1136
|
-
})
|
|
1137
|
-
|
|
1138
|
-
test('resolves instantiated Zero row fields through Partial and Omit', async () => {
|
|
1139
|
-
symlinkSync(
|
|
1140
|
-
join(import.meta.dirname, '../node_modules'),
|
|
1141
|
-
join(testDir, 'node_modules'),
|
|
1142
|
-
'dir'
|
|
1143
|
-
)
|
|
1144
|
-
writeFileSync(
|
|
1145
|
-
join(testDir, 'item/types.ts'),
|
|
1146
|
-
`
|
|
1147
|
-
import type { Row } from '@rocicorp/zero'
|
|
1148
|
-
import { drizzleZeroConfig } from 'drizzle-zero-sqlite'
|
|
1149
|
-
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
1150
|
-
|
|
1151
|
-
const item = sqliteTable('item', {
|
|
1152
|
-
id: text().primaryKey(),
|
|
1153
|
-
name: text().notNull(),
|
|
1154
|
-
description: text().notNull(),
|
|
1155
|
-
})
|
|
1156
|
-
const schema = drizzleZeroConfig({ item })
|
|
1157
|
-
|
|
1158
|
-
export type Item = Row<(typeof schema)['tables']['item']>
|
|
1159
|
-
`
|
|
1160
|
-
)
|
|
1161
|
-
|
|
1162
|
-
writeFileSync(
|
|
1163
|
-
join(testDir, 'item/mutations.ts'),
|
|
1164
|
-
`
|
|
1165
|
-
import { mutations } from 'on-zero'
|
|
1166
|
-
import type { Item } from './types'
|
|
1167
|
-
|
|
1168
|
-
type ItemUpdate = { id: string } & Partial<Omit<Item, 'id'>>
|
|
1169
|
-
|
|
1170
|
-
export const mutate = mutations('item', {
|
|
1171
|
-
update: async ({ tx }, props: ItemUpdate) => {
|
|
1172
|
-
await tx.mutate.item.update(props)
|
|
1173
|
-
},
|
|
1174
|
-
})
|
|
1175
|
-
`
|
|
1176
|
-
)
|
|
1177
|
-
|
|
1178
|
-
await generate({ dir: testDir, silent: true })
|
|
1179
|
-
|
|
1180
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
1181
|
-
expect(content).toContain('name: v.optional(v.string())')
|
|
1182
|
-
expect(content).toContain('description: v.optional(v.string())')
|
|
1183
|
-
expect(content).not.toContain('name: v.optional(v.unknown())')
|
|
1184
|
-
expect(content).not.toContain('description: v.optional(v.unknown())')
|
|
1185
|
-
})
|
|
1186
|
-
|
|
1187
|
-
test('skips symbol-keyed properties when resolving imported mutation param types', async () => {
|
|
1188
|
-
writeFileSync(
|
|
1189
|
-
join(testDir, 'item/types.ts'),
|
|
1190
|
-
`
|
|
1191
|
-
export type WeirdParams = {
|
|
1192
|
-
id: string
|
|
1193
|
-
[Symbol.iterator]?: () => Iterator<string>
|
|
1194
|
-
}
|
|
1195
|
-
`
|
|
1196
|
-
)
|
|
1197
|
-
|
|
1198
|
-
writeFileSync(
|
|
1199
|
-
join(testDir, 'item/mutations.ts'),
|
|
1200
|
-
`
|
|
1201
|
-
import { mutations } from 'on-zero'
|
|
1202
|
-
import type { WeirdParams } from './types'
|
|
1203
|
-
|
|
1204
|
-
export const mutate = mutations({
|
|
1205
|
-
run: async ({ tx }, params: WeirdParams) => {
|
|
1206
|
-
await tx.mutate.item.delete({ id: params.id })
|
|
1207
|
-
},
|
|
1208
|
-
})
|
|
1209
|
-
`
|
|
1210
|
-
)
|
|
1211
|
-
|
|
1212
|
-
await generate({ dir: testDir, silent: true })
|
|
1213
|
-
|
|
1214
|
-
const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
|
|
1215
|
-
expect(content).toContain('run')
|
|
1216
|
-
expect(content).toContain('id: v.string()')
|
|
1217
|
-
expect(content).not.toContain('__@iterator')
|
|
1218
|
-
})
|
|
1219
|
-
|
|
1220
|
-
test('resolves imported types in query params', async () => {
|
|
1221
|
-
writeFileSync(
|
|
1222
|
-
join(testDir, 'post/mutations.ts'),
|
|
1223
|
-
`export const schema = table('post', { id: string() })`
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
writeFileSync(
|
|
1227
|
-
join(testDir, 'post/types.ts'),
|
|
1228
|
-
`
|
|
1229
|
-
export type PostFilter = {
|
|
1230
|
-
authorId: string
|
|
1231
|
-
published: boolean
|
|
1232
|
-
}
|
|
1233
|
-
`
|
|
1234
|
-
)
|
|
1235
|
-
|
|
1236
|
-
writeFileSync(
|
|
1237
|
-
join(testDir, 'post/queries.ts'),
|
|
1238
|
-
`
|
|
1239
|
-
import type { PostFilter } from './types'
|
|
1240
|
-
|
|
1241
|
-
export const filteredPosts = (filter: PostFilter) => zero.query.post
|
|
1242
|
-
`
|
|
1243
|
-
)
|
|
1244
|
-
|
|
1245
|
-
const result = await generate({ dir: testDir, silent: true })
|
|
1246
|
-
|
|
1247
|
-
const content = readFileSync(join(testDir, 'generated/syncedQueries.ts'), 'utf-8')
|
|
1248
|
-
expect(content).toContain('filteredPosts')
|
|
1249
|
-
expect(content).toContain('v.object')
|
|
1250
|
-
expect(content).toContain('authorId')
|
|
1251
|
-
expect(content).toContain('v.boolean()')
|
|
1252
|
-
})
|
|
1253
|
-
})
|
|
1254
|
-
|
|
1255
|
-
describe('generateDrizzleSchemaFile', () => {
|
|
1256
|
-
test('preserves table server names and executable two-hop relationships', () => {
|
|
1257
|
-
const schema = {
|
|
1258
|
-
tables: {
|
|
1259
|
-
users: {
|
|
1260
|
-
name: 'users',
|
|
1261
|
-
serverName: 'user_records',
|
|
1262
|
-
primaryKey: ['id'],
|
|
1263
|
-
columns: {
|
|
1264
|
-
id: { type: 'string', optional: false, customType: null },
|
|
1265
|
-
},
|
|
1266
|
-
},
|
|
1267
|
-
groups: {
|
|
1268
|
-
name: 'groups',
|
|
1269
|
-
primaryKey: ['id'],
|
|
1270
|
-
columns: {
|
|
1271
|
-
id: { type: 'string', optional: false, customType: null },
|
|
1272
|
-
},
|
|
1273
|
-
},
|
|
1274
|
-
memberships: {
|
|
1275
|
-
name: 'memberships',
|
|
1276
|
-
primaryKey: ['userId', 'groupId'],
|
|
1277
|
-
columns: {
|
|
1278
|
-
userId: { type: 'string', optional: false, customType: null },
|
|
1279
|
-
groupId: { type: 'string', optional: false, customType: null },
|
|
1280
|
-
},
|
|
1281
|
-
},
|
|
1282
|
-
},
|
|
1283
|
-
relationships: {
|
|
1284
|
-
users: {
|
|
1285
|
-
groups: [
|
|
1286
|
-
{
|
|
1287
|
-
sourceField: ['id'],
|
|
1288
|
-
destField: ['userId'],
|
|
1289
|
-
destSchema: 'memberships',
|
|
1290
|
-
cardinality: 'many' as const,
|
|
1291
|
-
},
|
|
1292
|
-
{
|
|
1293
|
-
sourceField: ['groupId'],
|
|
1294
|
-
destField: ['id'],
|
|
1295
|
-
destSchema: 'groups',
|
|
1296
|
-
cardinality: 'many' as const,
|
|
1297
|
-
},
|
|
1298
|
-
],
|
|
1299
|
-
},
|
|
1300
|
-
},
|
|
1301
|
-
}
|
|
1302
|
-
const source = generateDrizzleSchemaFile(schema)
|
|
1303
|
-
const executableSource = source
|
|
1304
|
-
.replace(
|
|
1305
|
-
"import { boolean, createSchema, json, number, relationships, string, table } from '@rocicorp/zero'",
|
|
1306
|
-
''
|
|
1307
|
-
)
|
|
1308
|
-
.replace('export const schema =', 'globalThis.generatedSchema =')
|
|
1309
|
-
const context: Record<string, unknown> = { ...zero }
|
|
1310
|
-
|
|
1311
|
-
runInNewContext(executableSource, context)
|
|
1312
|
-
|
|
1313
|
-
expect(source).toContain('const usersTable = table("users").from("user_records")')
|
|
1314
|
-
expect(context.generatedSchema).toMatchObject({
|
|
1315
|
-
tables: {
|
|
1316
|
-
users: {
|
|
1317
|
-
serverName: 'user_records',
|
|
1318
|
-
},
|
|
1319
|
-
},
|
|
1320
|
-
relationships: {
|
|
1321
|
-
users: {
|
|
1322
|
-
groups: schema.relationships.users.groups,
|
|
1323
|
-
},
|
|
1324
|
-
},
|
|
1325
|
-
})
|
|
1326
|
-
})
|
|
1327
|
-
})
|