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.
Files changed (49) hide show
  1. package/dist/cjs/generate-helpers.test.cjs +0 -38
  2. package/dist/cjs/generate-helpers.test.native.js +0 -38
  3. package/dist/cjs/generate-helpers.test.native.js.map +1 -1
  4. package/dist/cjs/generate-lite.test.cjs +0 -522
  5. package/dist/cjs/generate-lite.test.native.js +0 -525
  6. package/dist/cjs/generate-lite.test.native.js.map +1 -1
  7. package/dist/cjs/generate.test.cjs +0 -691
  8. package/dist/cjs/generate.test.native.js +0 -691
  9. package/dist/cjs/generate.test.native.js.map +1 -1
  10. package/dist/cjs/mutations.test.cjs +0 -11
  11. package/dist/cjs/mutations.test.native.js +0 -18
  12. package/dist/cjs/mutations.test.native.js.map +1 -1
  13. package/dist/cjs/server.test.cjs +0 -93
  14. package/dist/cjs/server.test.native.js +0 -101
  15. package/dist/cjs/server.test.native.js.map +1 -1
  16. package/dist/cjs/serverWhere.test.cjs +0 -45
  17. package/dist/cjs/serverWhere.test.native.js +0 -51
  18. package/dist/cjs/serverWhere.test.native.js.map +1 -1
  19. package/dist/esm/generate-helpers.test.mjs +1 -39
  20. package/dist/esm/generate-helpers.test.mjs.map +1 -1
  21. package/dist/esm/generate-helpers.test.native.js +1 -39
  22. package/dist/esm/generate-helpers.test.native.js.map +1 -1
  23. package/dist/esm/generate-lite.test.mjs +0 -522
  24. package/dist/esm/generate-lite.test.mjs.map +1 -1
  25. package/dist/esm/generate-lite.test.native.js +0 -525
  26. package/dist/esm/generate-lite.test.native.js.map +1 -1
  27. package/dist/esm/generate.test.mjs +2 -668
  28. package/dist/esm/generate.test.mjs.map +1 -1
  29. package/dist/esm/generate.test.native.js +2 -668
  30. package/dist/esm/generate.test.native.js.map +1 -1
  31. package/dist/esm/mutations.test.mjs +0 -11
  32. package/dist/esm/mutations.test.mjs.map +1 -1
  33. package/dist/esm/mutations.test.native.js +0 -18
  34. package/dist/esm/mutations.test.native.js.map +1 -1
  35. package/dist/esm/server.test.mjs +0 -93
  36. package/dist/esm/server.test.mjs.map +1 -1
  37. package/dist/esm/server.test.native.js +0 -101
  38. package/dist/esm/server.test.native.js.map +1 -1
  39. package/dist/esm/serverWhere.test.mjs +0 -45
  40. package/dist/esm/serverWhere.test.mjs.map +1 -1
  41. package/dist/esm/serverWhere.test.native.js +0 -51
  42. package/dist/esm/serverWhere.test.native.js.map +1 -1
  43. package/package.json +1 -1
  44. package/src/generate-helpers.test.ts +0 -40
  45. package/src/generate-lite.test.ts +0 -586
  46. package/src/generate.test.ts +0 -844
  47. package/src/mutations.test.ts +0 -21
  48. package/src/server.test.ts +0 -75
  49. package/src/serverWhere.test.ts +0 -42
@@ -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
- })