[READ-ONLY] Mirror of https://github.com/danielroe/zero-vue. Vue bindings for Zero
sync
vue
zero
1import type { TTL } from '@rocicorp/zero'
2import {
3 createBuilder,
4 createSchema,
5 defineMutatorsWithType,
6 defineMutatorWithType,
7 defineQueriesWithType,
8 defineQuery,
9 number,
10 string,
11 table,
12 Zero,
13} from '@rocicorp/zero'
14import { describe, expect, it, onTestFinished, vi } from 'vitest'
15import { nextTick, ref, watchEffect } from 'vue'
16import z from 'zod'
17import { createZeroComposables } from './create-zero-composables'
18import { useQuery } from './query'
19import { VueView, vueViewFactory } from './view'
20
21const schema = createSchema({
22 tables: [
23 table('table')
24 .columns({
25 a: number(),
26 b: string(),
27 })
28 .primaryKey('a'),
29 ],
30})
31
32async function setupTestEnvironment() {
33 const userID = ref('asdf')
34
35 const defineMutators = defineMutatorsWithType<typeof schema>()
36 const defineMutator = defineMutatorWithType<typeof schema>()
37 const mutators = defineMutators({
38 table: {
39 insert: defineMutator(
40 z.object({ a: z.number(), b: z.string() }),
41 async ({ tx, args: { a, b } }) => {
42 return tx.mutate.table.insert({ a, b })
43 },
44 ),
45 update: defineMutator(
46 z.object({ a: z.number(), b: z.string() }),
47 async ({ tx, args: { a, b } }) => {
48 return tx.mutate.table.update({ a, b })
49 },
50 ),
51 },
52 })
53
54 const { useZero, useQuery } = createZeroComposables(() => ({
55 userID: userID.value,
56 server: null,
57 schema,
58 mutators,
59 kvStore: 'mem',
60 }))
61
62 const zero = useZero()
63 await zero.value.mutate(mutators.table.insert({ a: 1, b: 'a' })).client
64 await zero.value.mutate(mutators.table.insert({ a: 2, b: 'b' })).client
65
66 const zql = createBuilder(schema)
67 const defineQueries = defineQueriesWithType<typeof schema>()
68 const queries = defineQueries({
69 byId: defineQuery(
70 z.number(),
71 ({ args: a }) => zql.table.where('a', a),
72 ),
73 table: defineQuery(
74 () => zql.table,
75 ),
76 })
77
78 onTestFinished(async () => {
79 await zero.value.close()
80 })
81
82 return { zero, queries, useQuery, mutators, userID }
83}
84
85describe('useQuery', () => {
86 it('useQuery', async () => {
87 const { zero, mutators, queries, useQuery } = await setupTestEnvironment()
88 const { data: rows, status } = useQuery(() => queries.table())
89 expect(rows.value).toMatchInlineSnapshot(`[
90 {
91 "a": 1,
92 "b": "a",
93 Symbol(rc): 1,
94 },
95 {
96 "a": 2,
97 "b": "b",
98 Symbol(rc): 1,
99 },
100]`)
101 expect(status.value).toEqual('unknown')
102
103 await zero.value.mutate(mutators.table.insert({ a: 3, b: 'c' })).client
104 await nextTick()
105
106 expect(rows.value).toMatchInlineSnapshot(`[
107 {
108 "a": 1,
109 "b": "a",
110 Symbol(rc): 1,
111 },
112 {
113 "a": 2,
114 "b": "b",
115 Symbol(rc): 1,
116 },
117 {
118 "a": 3,
119 "b": "c",
120 Symbol(rc): 1,
121 },
122]`)
123
124 // TODO: this is not working at the moment, possibly because we don't have a server connection in test
125 // expect(resultType.value).toEqual("complete");
126 })
127
128 it('useQuery with ttl', async () => {
129 const { zero, queries, useQuery } = await setupTestEnvironment()
130
131 const ttl = ref<TTL>('1m')
132
133 const materializeSpy = vi.spyOn(zero.value, 'materialize')
134 const queryGetter = vi.fn(() => queries.table())
135
136 useQuery(queryGetter, () => ({ ttl: ttl.value }))
137 expect(queryGetter).toHaveBeenCalledTimes(1)
138
139 expect(materializeSpy).toHaveLastReturnedWith(expect.any(VueView))
140 expect(materializeSpy).toHaveBeenCalledExactlyOnceWith(
141 expect.any(Object),
142 vueViewFactory,
143 { ttl: '1m' },
144 )
145 expect(materializeSpy.mock.calls[0]![0]).toMatchInlineSnapshot(`
146 QueryImpl {
147 "customQueryID": {
148 "args": [],
149 "name": "table",
150 },
151 "format": {
152 "relationships": {},
153 "singular": false,
154 },
155 "limit": [Function],
156 "one": [Function],
157 "orderBy": [Function],
158 "related": [Function],
159 "start": [Function],
160 "where": [Function],
161 "whereExists": [Function],
162 Symbol(): true,
163 }
164 `)
165
166 const view: VueView = materializeSpy.mock.results[0]!.value
167 const updateTTLSpy = vi.spyOn(view, 'updateTTL')
168
169 materializeSpy.mockClear()
170
171 ttl.value = '10m'
172 await nextTick()
173
174 expect(materializeSpy).toHaveBeenCalledTimes(0)
175 expect(updateTTLSpy).toHaveBeenCalledExactlyOnceWith('10m')
176 })
177
178 it('useQuery supports disabled maybe queries', async () => {
179 const { zero, queries, useQuery } = await setupTestEnvironment()
180 const enabled = ref(false)
181 const materializeSpy = vi.spyOn(zero.value, 'materialize')
182
183 const { data: rows, status } = useQuery(() => enabled.value && queries.table())
184
185 expect(rows.value).toBeUndefined()
186 expect(status.value).toBe('disabled')
187 expect(materializeSpy).not.toHaveBeenCalled()
188
189 enabled.value = true
190 await nextTick()
191
192 expect(materializeSpy).toHaveBeenCalledTimes(1)
193 expect(rows.value).toMatchInlineSnapshot(`
194[
195 {
196 "a": 1,
197 "b": "a",
198 Symbol(rc): 1,
199 },
200 {
201 "a": 2,
202 "b": "b",
203 Symbol(rc): 1,
204 },
205]
206`)
207 expect(status.value).toEqual('unknown')
208
209 const view: VueView = materializeSpy.mock.results[0]!.value
210 const destroySpy = vi.spyOn(view, 'destroy')
211
212 enabled.value = false
213 await nextTick()
214
215 expect(destroySpy).toHaveBeenCalledTimes(1)
216 expect(rows.value).toBeUndefined()
217 expect(status.value).toBe('disabled')
218 })
219
220 it('useQuery deps change', async () => {
221 const { queries, useQuery } = await setupTestEnvironment()
222
223 const a = ref(1)
224
225 const { data: rows, status } = useQuery(() =>
226 queries.byId(a.value),
227 )
228
229 const rowLog: unknown[] = []
230 const resultDetailsLog: unknown[] = []
231 const resetLogs = () => {
232 rowLog.length = 0
233 resultDetailsLog.length = 0
234 }
235
236 watchEffect(() => {
237 rowLog.push(rows.value)
238 })
239
240 watchEffect(() => {
241 resultDetailsLog.push(status.value)
242 })
243
244 expect(rowLog).toMatchInlineSnapshot(`[
245 [
246 {
247 "a": 1,
248 "b": "a",
249 Symbol(rc): 1,
250 },
251 ],
252]`)
253 // expect(resultDetailsLog).toEqual(['unknown'])
254 resetLogs()
255
256 expect(rowLog).toEqual([])
257 // expect(resultDetailsLog).toEqual(['complete'])
258 resetLogs()
259
260 a.value = 2
261 await nextTick()
262
263 expect(rowLog).toMatchInlineSnapshot(`[
264 [
265 {
266 "a": 2,
267 "b": "b",
268 Symbol(rc): 1,
269 },
270 ],
271]`)
272 // expect(resultDetailsLog).toEqual(["unknown"]);
273 resetLogs()
274
275 expect(rowLog).toEqual([])
276 // expect(resultDetailsLog).toEqual(["complete"]);
277 })
278
279 it('useQuery deps change watchEffect', async () => {
280 const { zero, queries, mutators, useQuery } = await setupTestEnvironment()
281 const a = ref(1)
282 const { data: rows } = useQuery(() => queries.byId(a.value))
283
284 let run = 0
285
286 await new Promise((resolve) => {
287 watchEffect(() => {
288 if (run === 0) {
289 expect(rows.value).toMatchInlineSnapshot(
290 `[
291 {
292 "a": 1,
293 "b": "a",
294 Symbol(rc): 1,
295 },
296]`,
297 )
298 zero.value.mutate(mutators.table.update({ a: 1, b: 'a2' }))
299 }
300 else if (run === 1) {
301 expect(rows.value).toMatchInlineSnapshot(
302 `[
303 {
304 "a": 1,
305 "b": "a2",
306 Symbol(rc): 1,
307 },
308]`,
309 )
310 a.value = 2
311 }
312 else if (run === 2) {
313 expect(rows.value).toMatchInlineSnapshot(
314 `[
315 {
316 "a": 2,
317 "b": "b",
318 Symbol(rc): 1,
319 },
320]`,
321 )
322 resolve(true)
323 }
324 run++
325 })
326 })
327 })
328
329 it('can still be used without createZero', async () => {
330 const defineMutators = defineMutatorsWithType<typeof schema>()
331 const defineMutator = defineMutatorWithType<typeof schema>()
332 const mutators = defineMutators({
333 table: {
334 insert: defineMutator(
335 z.object({ a: z.number(), b: z.string() }),
336 async ({ tx, args: { a, b } }) => {
337 return tx.mutate.table.insert({ a, b })
338 },
339 ),
340 },
341 })
342 const zero = new Zero({
343 userID: 'test-user',
344 server: null,
345 schema,
346 mutators,
347 kvStore: 'mem' as const,
348 })
349 await zero.mutate(mutators.table.insert({ a: 1, b: 'a' })).client
350 await zero.mutate(mutators.table.insert({ a: 2, b: 'b' })).client
351
352 const zql = createBuilder(schema)
353 const defineQueries = defineQueriesWithType<typeof schema>()
354 const queries = defineQueries({
355 table: defineQuery(
356 () => zql.table,
357 ),
358 })
359
360 const { data: rows, status } = useQuery(zero, () => queries.table())
361 expect(rows.value).toMatchInlineSnapshot(`[
362 {
363 "a": 1,
364 "b": "a",
365 Symbol(rc): 1,
366 },
367 {
368 "a": 2,
369 "b": "b",
370 Symbol(rc): 1,
371 },
372]`)
373 expect(status.value).toEqual('unknown')
374 })
375
376 it('still works with legacy queries', async () => {
377 const schema = createSchema({
378 tables: [
379 table('table')
380 .columns({
381 a: number(),
382 b: string(),
383 })
384 .primaryKey('a'),
385 ],
386 enableLegacyQueries: true,
387 enableLegacyMutators: true,
388 })
389
390 const zero = new Zero({
391 userID: 'test-user',
392 server: null,
393 schema,
394 kvStore: 'mem' as const,
395 })
396
397 const { data } = useQuery(zero, zero.query.table)
398 expect(data.value).toEqual([])
399
400 await zero.mutate.table.insert({ a: 1, b: 'a2' })
401
402 expect(data.value).toMatchInlineSnapshot(`
403 [
404 {
405 "a": 1,
406 "b": "a2",
407 Symbol(rc): 1,
408 },
409 ]
410 `)
411 })
412})