over-zero 0.0.36 → 0.0.37

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 (141) hide show
  1. package/dist/cjs/cli.cjs +16 -10
  2. package/dist/cjs/cli.js +15 -8
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/cli.native.js +18 -10
  5. package/dist/cjs/cli.native.js.map +1 -1
  6. package/dist/cjs/createPermissions.cjs +7 -6
  7. package/dist/cjs/createPermissions.js +6 -6
  8. package/dist/cjs/createPermissions.js.map +1 -1
  9. package/dist/cjs/createPermissions.native.js +8 -6
  10. package/dist/cjs/createPermissions.native.js.map +1 -1
  11. package/dist/cjs/createUseQuery.cjs +18 -27
  12. package/dist/cjs/createUseQuery.js +18 -19
  13. package/dist/cjs/createUseQuery.js.map +1 -1
  14. package/dist/cjs/createUseQuery.native.js +19 -32
  15. package/dist/cjs/createUseQuery.native.js.map +1 -1
  16. package/dist/cjs/createZeroClient.cjs +38 -16
  17. package/dist/cjs/createZeroClient.js +40 -28
  18. package/dist/cjs/createZeroClient.js.map +2 -2
  19. package/dist/cjs/createZeroClient.native.js +45 -21
  20. package/dist/cjs/createZeroClient.native.js.map +1 -1
  21. package/dist/cjs/createZeroServer.cjs +16 -16
  22. package/dist/cjs/createZeroServer.js +15 -19
  23. package/dist/cjs/createZeroServer.js.map +2 -2
  24. package/dist/cjs/createZeroServer.native.js +19 -50
  25. package/dist/cjs/createZeroServer.native.js.map +1 -1
  26. package/dist/cjs/helpers/batchQuery.cjs +1 -1
  27. package/dist/cjs/helpers/batchQuery.js +1 -1
  28. package/dist/cjs/helpers/batchQuery.native.js +1 -1
  29. package/dist/cjs/helpers/batchQuery.native.js.map +1 -1
  30. package/dist/cjs/helpers/createMutators.cjs +1 -1
  31. package/dist/cjs/helpers/createMutators.js +1 -1
  32. package/dist/cjs/helpers/createMutators.js.map +1 -1
  33. package/dist/cjs/helpers/createMutators.native.js +1 -1
  34. package/dist/cjs/helpers/didRunPermissionCheck.cjs +1 -1
  35. package/dist/cjs/helpers/didRunPermissionCheck.js +1 -1
  36. package/dist/cjs/helpers/didRunPermissionCheck.native.js +1 -1
  37. package/dist/cjs/helpers/didRunPermissionCheck.native.js.map +1 -1
  38. package/dist/cjs/helpers/ensureLoggedIn.cjs +1 -1
  39. package/dist/cjs/helpers/ensureLoggedIn.js +1 -1
  40. package/dist/cjs/helpers/ensureLoggedIn.js.map +1 -1
  41. package/dist/cjs/helpers/ensureLoggedIn.native.js +1 -1
  42. package/dist/cjs/helpers/mutatorContext.cjs +1 -1
  43. package/dist/cjs/helpers/mutatorContext.js +1 -1
  44. package/dist/cjs/helpers/mutatorContext.native.js +1 -1
  45. package/dist/cjs/helpers/mutatorContext.native.js.map +1 -1
  46. package/dist/cjs/helpers/prettyFormatZeroQuery.cjs +1 -1
  47. package/dist/cjs/helpers/prettyFormatZeroQuery.js +1 -1
  48. package/dist/cjs/helpers/prettyFormatZeroQuery.native.js +1 -1
  49. package/dist/cjs/helpers/prettyFormatZeroQuery.native.js.map +1 -1
  50. package/dist/cjs/helpers/useZeroDebug.cjs +3 -3
  51. package/dist/cjs/helpers/useZeroDebug.js +2 -2
  52. package/dist/cjs/helpers/useZeroDebug.js.map +1 -1
  53. package/dist/cjs/helpers/useZeroDebug.native.js +5 -3
  54. package/dist/cjs/helpers/useZeroDebug.native.js.map +1 -1
  55. package/dist/cjs/where.cjs +1 -1
  56. package/dist/cjs/where.js +1 -1
  57. package/dist/cjs/where.js.map +1 -1
  58. package/dist/cjs/where.native.js +1 -1
  59. package/dist/cjs/where.native.js.map +1 -1
  60. package/dist/esm/cli.js +15 -8
  61. package/dist/esm/cli.js.map +1 -1
  62. package/dist/esm/cli.mjs +16 -10
  63. package/dist/esm/cli.mjs.map +1 -1
  64. package/dist/esm/cli.native.js +18 -10
  65. package/dist/esm/cli.native.js.map +1 -1
  66. package/dist/esm/createPermissions.js +5 -4
  67. package/dist/esm/createPermissions.js.map +1 -1
  68. package/dist/esm/createPermissions.mjs +5 -4
  69. package/dist/esm/createPermissions.mjs.map +1 -1
  70. package/dist/esm/createPermissions.native.js +6 -4
  71. package/dist/esm/createPermissions.native.js.map +1 -1
  72. package/dist/esm/createUseQuery.js +17 -19
  73. package/dist/esm/createUseQuery.js.map +1 -1
  74. package/dist/esm/createUseQuery.mjs +17 -26
  75. package/dist/esm/createUseQuery.mjs.map +1 -1
  76. package/dist/esm/createUseQuery.native.js +18 -31
  77. package/dist/esm/createUseQuery.native.js.map +1 -1
  78. package/dist/esm/createZeroClient.js +55 -30
  79. package/dist/esm/createZeroClient.js.map +2 -2
  80. package/dist/esm/createZeroClient.mjs +39 -17
  81. package/dist/esm/createZeroClient.mjs.map +1 -1
  82. package/dist/esm/createZeroClient.native.js +47 -23
  83. package/dist/esm/createZeroClient.native.js.map +1 -1
  84. package/dist/esm/createZeroServer.js +18 -20
  85. package/dist/esm/createZeroServer.js.map +1 -1
  86. package/dist/esm/createZeroServer.mjs +16 -16
  87. package/dist/esm/createZeroServer.mjs.map +1 -1
  88. package/dist/esm/createZeroServer.native.js +19 -50
  89. package/dist/esm/createZeroServer.native.js.map +1 -1
  90. package/dist/esm/helpers/batchQuery.js +1 -1
  91. package/dist/esm/helpers/batchQuery.mjs +1 -1
  92. package/dist/esm/helpers/batchQuery.native.js +1 -1
  93. package/dist/esm/helpers/createMutators.js +1 -1
  94. package/dist/esm/helpers/createMutators.mjs +1 -1
  95. package/dist/esm/helpers/createMutators.native.js +1 -1
  96. package/dist/esm/helpers/didRunPermissionCheck.js +1 -1
  97. package/dist/esm/helpers/didRunPermissionCheck.mjs +1 -1
  98. package/dist/esm/helpers/didRunPermissionCheck.native.js +1 -1
  99. package/dist/esm/helpers/ensureLoggedIn.js +1 -1
  100. package/dist/esm/helpers/ensureLoggedIn.mjs +1 -1
  101. package/dist/esm/helpers/ensureLoggedIn.native.js +1 -1
  102. package/dist/esm/helpers/mutatorContext.js +1 -1
  103. package/dist/esm/helpers/mutatorContext.mjs +1 -1
  104. package/dist/esm/helpers/mutatorContext.native.js +1 -1
  105. package/dist/esm/helpers/prettyFormatZeroQuery.js +1 -1
  106. package/dist/esm/helpers/prettyFormatZeroQuery.mjs +1 -1
  107. package/dist/esm/helpers/prettyFormatZeroQuery.native.js +1 -1
  108. package/dist/esm/helpers/useZeroDebug.js +2 -2
  109. package/dist/esm/helpers/useZeroDebug.js.map +1 -1
  110. package/dist/esm/helpers/useZeroDebug.mjs +3 -3
  111. package/dist/esm/helpers/useZeroDebug.mjs.map +1 -1
  112. package/dist/esm/helpers/useZeroDebug.native.js +5 -3
  113. package/dist/esm/helpers/useZeroDebug.native.js.map +1 -1
  114. package/dist/esm/where.js +1 -1
  115. package/dist/esm/where.mjs +1 -1
  116. package/dist/esm/where.native.js +1 -1
  117. package/package.json +2 -2
  118. package/readme.md +20 -6
  119. package/src/cli.ts +23 -22
  120. package/src/createPermissions.ts +6 -4
  121. package/src/createUseQuery.tsx +34 -69
  122. package/src/createZeroClient.tsx +81 -33
  123. package/src/createZeroServer.ts +27 -35
  124. package/src/helpers/batchQuery.ts +1 -1
  125. package/src/helpers/createMutators.ts +1 -1
  126. package/src/helpers/didRunPermissionCheck.ts +1 -1
  127. package/src/helpers/ensureLoggedIn.ts +1 -1
  128. package/src/helpers/mutatorContext.ts +1 -1
  129. package/src/helpers/prettyFormatZeroQuery.ts +1 -1
  130. package/src/helpers/useZeroDebug.ts +3 -3
  131. package/src/types.ts +2 -2
  132. package/src/where.ts +1 -1
  133. package/types/createPermissions.d.ts.map +1 -1
  134. package/types/createUseQuery.d.ts +7 -9
  135. package/types/createUseQuery.d.ts.map +1 -1
  136. package/types/createZeroClient.d.ts +5 -6
  137. package/types/createZeroClient.d.ts.map +1 -1
  138. package/types/createZeroServer.d.ts +69 -18
  139. package/types/createZeroServer.d.ts.map +1 -1
  140. package/types/types.d.ts +2 -2
  141. package/types/types.d.ts.map +1 -1
package/readme.md CHANGED
@@ -212,7 +212,7 @@ generates all files needed to connect your models and queries:
212
212
  - `models.ts` - aggregates all model files into a single import
213
213
  - `types.ts` - generates TypeScript types from table schemas
214
214
  - `tables.ts` - exports table schemas (separate to avoid circular types)
215
- - `queries.ts` - generates synced query definitions with valibot validators
215
+ - `syncedQueries.ts` - generates synced query definitions with valibot validators
216
216
 
217
217
  **options:**
218
218
 
@@ -283,7 +283,7 @@ export { schema as channel } from '~/data/models/channel'
283
283
  export { schema as message } from '~/data/models/message'
284
284
  ```
285
285
 
286
- **queries.ts:**
286
+ **syncedQueries.ts:**
287
287
 
288
288
  ```ts
289
289
  import * as v from 'valibot'
@@ -329,12 +329,14 @@ client:
329
329
 
330
330
  ```tsx
331
331
  import { createZeroClient } from 'over-zero'
332
- import { schema } from './data/schema'
333
- import { models } from './data/models'
332
+ import { schema } from '~/data/schema'
333
+ import { models } from '~/data/generated/models'
334
+ import * as groupedQueries from '~/data/generated/groupedQueries'
334
335
 
335
- export const { ProvideZero, useQuery, zero } = createZeroClient({
336
+ export const { ProvideZero, useQuery, zero, usePermission } = createZeroClient({
336
337
  schema,
337
338
  models,
339
+ groupedQueries,
338
340
  })
339
341
 
340
342
  // in your app root
@@ -352,17 +354,19 @@ server:
352
354
 
353
355
  ```ts
354
356
  import { createZeroServer } from 'over-zero/server'
357
+ import { syncedQueries } from '~/data/generated/syncedQueries'
355
358
 
356
359
  export const zeroServer = createZeroServer({
357
360
  schema,
358
361
  models,
359
362
  database: process.env.DATABASE_URL,
363
+ queries: syncedQueries, // required for synced queries / pull endpoint
360
364
  createServerActions: () => ({
361
365
  sendEmail: async (to, subject, body) => { ... }
362
366
  })
363
367
  })
364
368
 
365
- // in your api endpoint
369
+ // push endpoint for mutations
366
370
  app.post('/api/zero/push', async (req) => {
367
371
  const authData = await getAuthFromRequest(req)
368
372
  const { response } = await zeroServer.handleMutationRequest({
@@ -371,6 +375,16 @@ app.post('/api/zero/push', async (req) => {
371
375
  })
372
376
  return response
373
377
  })
378
+
379
+ // pull endpoint for synced queries
380
+ app.post('/api/zero/pull', async (req) => {
381
+ const authData = await getAuthFromRequest(req)
382
+ const { response } = await zeroServer.handleQueryRequest({
383
+ authData,
384
+ request: req
385
+ })
386
+ return response
387
+ })
374
388
  ```
375
389
 
376
390
  type augmentation:
package/src/cli.ts CHANGED
@@ -453,14 +453,14 @@ function generateSyncedQueriesFile(
453
453
  const sortedFiles = Array.from(queryByFile.keys()).sort()
454
454
 
455
455
  const imports = `// auto-generated by: over-zero generate
456
- // server-side syncedQuery wrappers with validators
457
- import { syncedQuery } from '@rocicorp/zero'
456
+ // server-side query definitions with validators
457
+ import { defineQuery, defineQueries } from '@rocicorp/zero'
458
458
  import * as v from 'valibot'
459
459
  import * as Queries from './groupedQueries'
460
460
  `
461
461
 
462
- // generate grouped exports by namespace
463
- const namespaceExports = sortedFiles
462
+ // generate grouped definitions by namespace
463
+ const namespaceDefs = sortedFiles
464
464
  .map((file) => {
465
465
  const fileQueries = queryByFile
466
466
  .get(file)!
@@ -474,7 +474,7 @@ import * as Queries from './groupedQueries'
474
474
  l.startsWith('export const QueryParams')
475
475
  )
476
476
 
477
- let validatorDef = 'v.void()'
477
+ let validatorDef = ''
478
478
  if (schemaLineIndex !== -1) {
479
479
  const schemaLines: string[] = []
480
480
  let openBraces = 0
@@ -500,31 +500,32 @@ import * as Queries from './groupedQueries'
500
500
  validatorDef = schemaLines.join('\n')
501
501
  }
502
502
 
503
- // wrap validator in v.parser(v.tuple([...]))
504
- const wrappedValidator =
505
- validatorDef === 'v.void()'
506
- ? 'v.parser(v.tuple([]))'
507
- : `v.parser(v.tuple([${validatorDef}]))`
508
-
509
- // namespaced query name: file.queryName
510
- const namespacedName = `${q.sourceFile}.${q.name}`
511
-
512
- // for void queries, no arg parameter
513
- const queryFn =
514
- validatorDef === 'v.void()'
515
- ? `() => Queries.${file}.${q.name}()`
516
- : `(arg) => Queries.${file}.${q.name}(arg)`
503
+ // for void queries, no validator and no args
504
+ if (!validatorDef) {
505
+ return ` ${q.name}: defineQuery(() => Queries.${file}.${q.name}()),`
506
+ }
517
507
 
518
- return ` ${q.name}: syncedQuery('${namespacedName}', ${wrappedValidator}, ${queryFn}),`
508
+ // defineQuery with validator and args
509
+ return ` ${q.name}: defineQuery(
510
+ ${validatorDef},
511
+ ({ args }) => Queries.${file}.${q.name}(args),
512
+ ),`
519
513
  })
520
514
  .join('\n')
521
515
 
522
- return `export const ${file} = {\n${queryDefs}\n}`
516
+ return `const ${file} = {\n${queryDefs}\n}`
523
517
  })
524
518
  .join('\n\n')
525
519
 
520
+ // build the defineQueries call with all namespaces
521
+ const queriesObject = sortedFiles.map((file) => ` ${file},`).join('\n')
522
+
526
523
  return `${imports}
527
- ${namespaceExports}
524
+ ${namespaceDefs}
525
+
526
+ export const queries = defineQueries({
527
+ ${queriesObject}
528
+ })
528
529
  `
529
530
  }
530
531
 
@@ -1,8 +1,9 @@
1
- import { ensure, EnsureError } from '@vxrn/helpers'
1
+ import { ensure, EnsureError } from '@take-out/helpers'
2
2
 
3
3
  import { setDidRunPermissionCheck } from './helpers/didRunPermissionCheck'
4
4
  import { mutatorContext } from './helpers/mutatorContext'
5
5
  import { prettyFormatZeroQuery } from './helpers/prettyFormatZeroQuery'
6
+ import { getZQL } from './state'
6
7
  import { getWhereTableName } from './where'
7
8
 
8
9
  import type { AuthData, Can, TableName, Transaction, Where } from './types'
@@ -96,7 +97,8 @@ export function createPermissions<Schema extends ZeroSchema>({
96
97
  return
97
98
  }
98
99
 
99
- const queryBase = tx.query[tableName] as Query<any, any>
100
+ const zqlBuilder = getZQL() as any
101
+ const queryBase = zqlBuilder[tableName] as Query<any, any>
100
102
  let query: Query<any, any, any> | null = null
101
103
 
102
104
  try {
@@ -106,9 +108,9 @@ export function createPermissions<Schema extends ZeroSchema>({
106
108
  })
107
109
  .one()
108
110
 
109
- ensure(await query)
111
+ ensure(await tx.run(query))
110
112
  } catch (err) {
111
- const errorTitle = `${name} with auth id: ${authData?.id}`
113
+ const errorTitle = `${tableName} with auth id: ${authData?.id}`
112
114
 
113
115
  if (err instanceof EnsureError) {
114
116
  let msg = `[permission] 🚫 Not Allowed: ${errorTitle}`
@@ -1,4 +1,3 @@
1
- import { syncedQuery } from '@rocicorp/zero'
2
1
  import { useQuery as zeroUseQuery } from '@rocicorp/zero/react'
3
2
  import { use, useMemo, type Context } from 'react'
4
3
 
@@ -6,10 +5,9 @@ import { useZeroDebug } from './helpers/useZeroDebug'
6
5
  import { getQueryName } from './queryRegistry'
7
6
 
8
7
  import type {
8
+ AnyQueryRegistry,
9
9
  HumanReadable,
10
10
  Query,
11
- ReadonlyJSONValue,
12
- SyncedQuery,
13
11
  Schema as ZeroSchema,
14
12
  } from '@rocicorp/zero'
15
13
 
@@ -26,77 +24,43 @@ export type PlainQueryFn<
26
24
  TReturn extends Query<any, any, any> = Query<any, any, any>,
27
25
  > = (args: TArg) => TReturn
28
26
 
29
- // inline query type - only available when DisableInline is false
30
- type InlineQuery<
31
- Schema extends ZeroSchema,
32
- TTable extends keyof Schema['tables'] & string,
33
- TReturn,
34
- > =
35
- | Query<Schema, TTable, TReturn>
36
- | SyncedQuery<any, any, any, any, Query<Schema, TTable, TReturn>>
37
-
38
- // the useQuery hook type with conditional inline overload
39
- export type UseQueryHook<
40
- Schema extends ZeroSchema,
41
- DisableInline extends boolean = false,
42
- > = {
43
- // overload 1: inline query (only when DisableInline is false)
44
- <TTable extends keyof Schema['tables'] & string, TReturn>(
45
- query: DisableInline extends true ? never : InlineQuery<Schema, TTable, TReturn>,
46
- options?: UseQueryOptions | boolean
47
- ): QueryResult<TReturn>;
48
-
49
- // overload 2: plain function with params
27
+ export type UseQueryHook<Schema extends ZeroSchema> = {
28
+ // overload 1: plain function with params
50
29
  <TArg, TTable extends keyof Schema['tables'] & string, TReturn>(
51
- fn: PlainQueryFn<TArg, Query<Schema, TTable, TReturn>>,
30
+ fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>,
52
31
  params: TArg,
53
32
  options?: UseQueryOptions | boolean
54
33
  ): QueryResult<TReturn>;
55
34
 
56
- // overload 3: plain function with no params
35
+ // overload 2: plain function with no params
57
36
  <TTable extends keyof Schema['tables'] & string, TReturn>(
58
- fn: PlainQueryFn<void, Query<Schema, TTable, TReturn>>,
37
+ fn: PlainQueryFn<void, Query<TTable, Schema, TReturn>>,
59
38
  options?: UseQueryOptions | boolean
60
39
  ): QueryResult<TReturn>
61
40
  }
62
41
 
63
- export function createUseQuery<
64
- Schema extends ZeroSchema,
65
- const DisableInline extends boolean = false,
66
- >({
42
+ export function createUseQuery<Schema extends ZeroSchema>({
67
43
  DisabledContext,
68
- disableInlineQueries = false as DisableInline,
44
+ customQueries,
69
45
  }: {
70
46
  DisabledContext: Context<boolean>
71
- disableInlineQueries?: DisableInline
72
- }): UseQueryHook<Schema, DisableInline> {
73
- const queryCache = new Map<string, SyncedQuery<any, any, any, any, any>>()
74
- const parseAny = (x: unknown[]): [ReadonlyJSONValue] => [x[0] as ReadonlyJSONValue]
75
-
47
+ customQueries: AnyQueryRegistry
48
+ }): UseQueryHook<Schema> {
76
49
  function useQuery(...args: any[]): any {
77
50
  const disabled = use(DisabledContext)
78
- const [queryOrFn, paramsOrOptions, optionsArg] = args
79
-
80
- // detect which calling pattern is being used
81
- const isPlainFunction = typeof queryOrFn === 'function' && !('queryName' in queryOrFn)
82
-
83
- const { actualQuery, options } = useMemo(() => {
84
- if (!isPlainFunction) {
85
- // pattern 1: original api - useQuery(query, options)
86
- return {
87
- actualQuery: queryOrFn,
88
- options: paramsOrOptions,
89
- }
90
- }
51
+ const [fn, paramsOrOptions, optionsArg] = args
91
52
 
92
- const fn = queryOrFn
53
+ const { queryRequest, options } = useMemo(() => {
93
54
  const queryName = getQueryName(fn)
94
-
95
55
  if (!queryName) {
96
- throw new Error(`No query name?`)
56
+ const fnName = fn?.name || 'anonymous'
57
+ throw new Error(
58
+ `Query function '${fnName}' not registered. ` +
59
+ `Ensure it is exported from a queries file and passed to createZeroClient via groupedQueries.`
60
+ )
97
61
  }
98
62
 
99
- // determine if this is pattern 2 (with params) or pattern 3 (no params)
63
+ // determine if this is with params or no params
100
64
  const hasParams =
101
65
  optionsArg !== undefined ||
102
66
  (paramsOrOptions &&
@@ -107,27 +71,28 @@ export function createUseQuery<
107
71
  const params = hasParams ? paramsOrOptions : undefined
108
72
  const opts = hasParams ? optionsArg : paramsOrOptions
109
73
 
110
- let synced = queryCache.get(queryName)
111
- if (!synced) {
112
- synced = syncedQuery(queryName, parseAny, (arg: ReadonlyJSONValue) => {
113
- return fn(arg)
114
- })
115
- queryCache.set(queryName, synced)
116
- }
74
+ // look up the CustomQuery from the shared registry
75
+ // queryName is "namespace.name" format, e.g., "user.userById"
76
+ const [namespace, name] = queryName.split('.', 2)
77
+ const customQuery = (customQueries as any)[namespace]?.[name]
117
78
 
118
- // call the SyncedQuery with params if provided
119
- const query = params !== undefined ? (synced as any)(params) : synced
79
+ if (!customQuery) {
80
+ throw new Error(
81
+ `CustomQuery '${queryName}' not found. ` +
82
+ `Check that the query is exported and the namespace/name matches.`
83
+ )
84
+ }
120
85
 
121
- return { actualQuery: query, options: opts }
122
- }, [queryOrFn, paramsOrOptions, optionsArg, isPlainFunction])
86
+ const queryRequest = params !== undefined ? customQuery(params) : customQuery()
123
87
 
124
- console.info('running', { actualQuery, options, queryOrFn })
88
+ return { queryRequest, options: opts }
89
+ }, [fn, paramsOrOptions, optionsArg])
125
90
 
126
- const out = zeroUseQuery(actualQuery, options)
91
+ const out = zeroUseQuery(queryRequest as any, options)
127
92
 
128
93
  if (process.env.NODE_ENV === 'development') {
129
94
  // eslint-disable-next-line react-hooks/rules-of-hooks
130
- useZeroDebug(actualQuery, options, out)
95
+ useZeroDebug(queryRequest as any, options, out)
131
96
  }
132
97
 
133
98
  if (disabled) {
@@ -137,5 +102,5 @@ export function createUseQuery<
137
102
  return out
138
103
  }
139
104
 
140
- return useQuery as UseQueryHook<Schema, DisableInline>
105
+ return useQuery as UseQueryHook<Schema>
141
106
  }
@@ -1,6 +1,20 @@
1
- import { useZero, ZeroProvider } from '@rocicorp/zero/react'
2
- import { createEmitter, mapObject } from '@vxrn/helpers'
3
- import { createContext, use, useMemo, type ReactNode } from 'react'
1
+ import { defineQueries, defineQuery } from '@rocicorp/zero'
2
+ import {
3
+ useConnectionState,
4
+ useZero,
5
+ ZeroProvider,
6
+ useQuery as zeroUseQuery,
7
+ } from '@rocicorp/zero/react'
8
+ import { createEmitter, mapObject } from '@take-out/helpers'
9
+ import {
10
+ createContext,
11
+ memo,
12
+ use,
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ type ReactNode,
17
+ } from 'react'
4
18
 
5
19
  import { createPermissions } from './createPermissions'
6
20
  import { createUseQuery } from './createUseQuery'
@@ -9,7 +23,7 @@ import { prettyFormatZeroQuery } from './helpers/prettyFormatZeroQuery'
9
23
  import { registerQuery } from './queryRegistry'
10
24
  import { setAuthData, setSchema } from './state'
11
25
 
12
- import type { AuthData, GenericModels, GetZeroMutators, ZeroEvent } from './types'
26
+ import type { AuthData, GenericModels, GetZeroMutators, Where, ZeroEvent } from './types'
13
27
  import type { Row, Zero, ZeroOptions, Schema as ZeroSchema } from '@rocicorp/zero'
14
28
 
15
29
  export type GroupedQueries = Record<string, Record<string, (...args: any[]) => any>>
@@ -17,17 +31,14 @@ export type GroupedQueries = Record<string, Record<string, (...args: any[]) => a
17
31
  export function createZeroClient<
18
32
  Schema extends ZeroSchema,
19
33
  Models extends GenericModels,
20
- const DisableInlineQueries extends boolean = false,
21
34
  >({
22
35
  schema,
23
36
  models,
24
37
  groupedQueries,
25
- disableInlineQueries = false as DisableInlineQueries,
26
38
  }: {
27
39
  schema: Schema
28
40
  models: Models
29
41
  groupedQueries: GroupedQueries
30
- disableInlineQueries?: DisableInlineQueries
31
42
  }) {
32
43
  type ZeroMutators = GetZeroMutators<Models>
33
44
  type ZeroInstance = Zero<Schema, ZeroMutators>
@@ -36,25 +47,45 @@ export function createZeroClient<
36
47
  setSchema(schema)
37
48
 
38
49
  // build query registry from grouped queries
50
+ // this creates ONE shared defineQueries registry that matches the server's structure
51
+ const wrappedNamespaces: Record<
52
+ string,
53
+ Record<string, ReturnType<typeof defineQuery>>
54
+ > = {}
55
+
39
56
  for (const [namespace, queries] of Object.entries(groupedQueries)) {
57
+ wrappedNamespaces[namespace] = {}
40
58
  for (const [name, fn] of Object.entries(queries)) {
41
59
  registerQuery(fn, `${namespace}.${name}`)
60
+ // wrap each plain function in defineQuery
61
+ wrappedNamespaces[namespace][name] = defineQuery(({ args }: { args: any }) =>
62
+ fn(args)
63
+ )
42
64
  }
43
65
  }
44
66
 
67
+ // create the single shared CustomQuery registry
68
+ const customQueries = defineQueries(wrappedNamespaces)
69
+
45
70
  const DisabledContext = createContext(false)
46
71
 
47
- const modelWritePermissions = mapObject(models, (val) => val.permissions) as {
48
- [K in TableName]: K extends keyof Models ? Models[K]['permissions'] : never
49
- }
72
+ const modelWritePermissions = mapObject(models, (val) => val.permissions) as Record<
73
+ TableName,
74
+ Where<any, any> | undefined
75
+ >
50
76
 
51
77
  let latestZeroInstance: ZeroInstance | null = null
52
78
 
53
- // when we log in this is swapped out and later the exported zero is just a proxy
54
- // may cause some issues, ideally zero itself supports this
79
+ // Proxy allows swapping the Zero instance on login without breaking existing references.
80
+ // Ideally rocicorp/zero would support .setAuth() natively, but for now we swap instances.
55
81
  const zero: ZeroInstance = new Proxy({} as never, {
56
82
  get(_, key) {
57
- return Reflect.get(latestZeroInstance!, key, latestZeroInstance)
83
+ if (latestZeroInstance === null) {
84
+ throw new Error(
85
+ `Zero instance not initialized. Ensure ZeroProvider is mounted before accessing 'zero'.`
86
+ )
87
+ }
88
+ return Reflect.get(latestZeroInstance, key, latestZeroInstance)
58
89
  },
59
90
  })
60
91
 
@@ -72,9 +103,9 @@ export function createZeroClient<
72
103
  const AuthDataContext = createContext<AuthData>({} as AuthData)
73
104
  const useAuthData = () => use(AuthDataContext)
74
105
 
75
- const useQuery = createUseQuery<Schema, DisableInlineQueries>({
106
+ const useQuery = createUseQuery<Schema>({
76
107
  DisabledContext,
77
- disableInlineQueries,
108
+ customQueries,
78
109
  })
79
110
 
80
111
  // we don't want flickers as you move around and these queries are re-run
@@ -83,7 +114,7 @@ export function createZeroClient<
83
114
  // always update once the query resolves
84
115
  function usePermission<K extends TableName>(
85
116
  table: K,
86
- objOrId: string | Partial<Row<Schema['tables'][K]>> | undefined,
117
+ objOrId: string | Partial<Row<any>> | undefined,
87
118
  enabled = typeof objOrId !== 'undefined',
88
119
  debug = false
89
120
  ): boolean | null {
@@ -93,7 +124,7 @@ export function createZeroClient<
93
124
  const permission = modelWritePermissions[table]
94
125
 
95
126
  const query = (() => {
96
- let baseQuery = zero.query[table].one()
127
+ let baseQuery = (zero.query as any)[table].one()
97
128
 
98
129
  if (disabled || !enabled || !permission) {
99
130
  return baseQuery
@@ -109,9 +140,8 @@ export function createZeroClient<
109
140
  })
110
141
  })()
111
142
 
112
- // note: usePermission is internal and uses inline queries intentionally
113
- // if disableInlineQueries is enabled, this will error and need a cast
114
- const [data, status] = useQuery(query as any, {
143
+ // usePermission is internal and uses inline queries directly via zeroUseQuery
144
+ const [data, status] = zeroUseQuery(query, {
115
145
  enabled: Boolean(enabled && permission && authData && objOrId),
116
146
  })
117
147
 
@@ -164,20 +194,9 @@ export function createZeroClient<
164
194
 
165
195
  return (
166
196
  <AuthDataContext.Provider value={authData}>
167
- <ZeroProvider
168
- schema={schema}
169
- kvStore="mem"
170
- onError={(error) => {
171
- console.error(`Zero Error:`, error)
172
- zeroEvents.emit({
173
- type: 'error',
174
- message: error,
175
- })
176
- }}
177
- mutators={mutators}
178
- {...props}
179
- >
197
+ <ZeroProvider schema={schema} kvStore="mem" mutators={mutators as any} {...props}>
180
198
  <SetZeroInstance />
199
+ <ConnectionMonitor zeroEvents={zeroEvents} />
181
200
  {children}
182
201
  </ZeroProvider>
183
202
  </AuthDataContext.Provider>
@@ -200,6 +219,35 @@ export function createZeroClient<
200
219
  return null
201
220
  }
202
221
 
222
+ // monitors connection state and emits events (replaces onError callback removed in 0.25)
223
+ const ConnectionMonitor = memo(
224
+ ({
225
+ zeroEvents,
226
+ }: {
227
+ zeroEvents: ReturnType<typeof createEmitter<ZeroEvent | null>>
228
+ }) => {
229
+ const state = useConnectionState()
230
+ const prevState = useRef(state.name)
231
+
232
+ useEffect(() => {
233
+ if (state.name !== prevState.current) {
234
+ const reason = 'reason' in state ? state.reason : ''
235
+ prevState.current = state.name
236
+
237
+ if (state.name === 'error' || state.name === 'needs-auth') {
238
+ const message = typeof reason === 'string' ? reason : state.name
239
+ zeroEvents.emit({
240
+ type: 'error',
241
+ message,
242
+ })
243
+ }
244
+ }
245
+ }, [state, zeroEvents])
246
+
247
+ return null
248
+ }
249
+ )
250
+
203
251
  return {
204
252
  zeroEvents,
205
253
  ProvideZero,
@@ -1,6 +1,8 @@
1
- import { handleGetQueriesRequest, PushProcessor } from '@rocicorp/zero/pg'
1
+ import { mustGetQuery } from '@rocicorp/zero'
2
+ import { PushProcessor } from '@rocicorp/zero/pg'
3
+ import { handleQueryRequest as zeroHandleQueryRequest } from '@rocicorp/zero/server'
2
4
  import { zeroNodePg } from '@rocicorp/zero/server/adapters/pg'
3
- import { assertString, randomId } from '@vxrn/helpers'
5
+ import { assertString, randomId } from '@take-out/helpers'
4
6
  import { Pool } from 'pg'
5
7
 
6
8
  import { createPermissions } from './createPermissions'
@@ -16,31 +18,23 @@ import type {
16
18
  Transaction,
17
19
  } from './types'
18
20
  import type {
21
+ AnyQueryRegistry,
19
22
  HumanReadable,
20
23
  Query,
21
- ReadonlyJSONValue,
22
- SyncedQuery,
23
24
  Schema as ZeroSchema,
24
25
  } from '@rocicorp/zero'
25
26
  import type { TransactionProviderInput } from '@rocicorp/zero/pg'
26
27
 
27
- // grouped synced queries: { namespace: { queryName: SyncedQuery } }
28
- export type SyncedQueries = Record<
29
- string,
30
- Record<string, SyncedQuery<any, any, any, any, any>>
31
- >
32
-
33
28
  export function createZeroServer<
34
29
  Schema extends ZeroSchema,
35
30
  Models extends GenericModels,
36
31
  ServerActions extends Record<string, unknown>,
37
- Queries extends SyncedQueries = Record<string, never>,
38
32
  >({
39
33
  createServerActions,
40
34
  database,
41
35
  schema,
42
36
  models,
43
- syncedQueries,
37
+ queries,
44
38
  }: {
45
39
  /**
46
40
  * The DB connection string, same as ZERO_UPSTREAM_DB
@@ -49,7 +43,7 @@ export function createZeroServer<
49
43
  schema: Schema
50
44
  models: Models
51
45
  createServerActions: () => ServerActions
52
- syncedQueries?: Queries
46
+ queries?: AnyQueryRegistry
53
47
  }) {
54
48
  setSchema(schema)
55
49
 
@@ -59,6 +53,10 @@ export function createZeroServer<
59
53
  schema,
60
54
  new Pool({
61
55
  connectionString: dbString,
56
+ // handle self-signed certificates in production
57
+ ssl: dbString.includes('sslmode=require')
58
+ ? { rejectUnauthorized: false }
59
+ : undefined,
62
60
  })
63
61
  )
64
62
 
@@ -112,16 +110,6 @@ export function createZeroServer<
112
110
  }
113
111
  }
114
112
 
115
- // build flat lookup map from grouped syncedQueries: "namespace.queryName" => SyncedQuery
116
- const queryLookup = new Map<string, SyncedQuery<any, any, any, any, any>>()
117
- if (syncedQueries) {
118
- for (const [namespace, queries] of Object.entries(syncedQueries)) {
119
- for (const [queryName, syncedQuery] of Object.entries(queries)) {
120
- queryLookup.set(`${namespace}.${queryName}`, syncedQuery)
121
- }
122
- }
123
- }
124
-
125
113
  const handleQueryRequest = async ({
126
114
  authData,
127
115
  request,
@@ -129,20 +117,24 @@ export function createZeroServer<
129
117
  authData: AuthData | null
130
118
  request: Request
131
119
  }) => {
132
- function getQuery(name: string, args: readonly ReadonlyJSONValue[]) {
133
- const q = queryLookup.get(name)
134
- if (!q) {
135
- throw new Error(`No such query: ${name}`)
136
- }
137
-
138
- return {
139
- // @ts-expect-error zero bug atm
140
- query: q(...args),
141
- }
120
+ if (!queries) {
121
+ throw new Error(
122
+ 'No queries registered with createZeroServer. ' +
123
+ 'Pass the syncedQueries registry to createZeroServer via the queries option.'
124
+ )
142
125
  }
143
126
 
127
+ // set authData globally for permission checks in query functions
144
128
  setAuthData(authData || {})
145
- const response = await handleGetQueriesRequest(getQuery, schema, request)
129
+
130
+ const response = await zeroHandleQueryRequest(
131
+ (name, args) => {
132
+ const query = (mustGetQuery as any)(queries, name)
133
+ return query.fn({ args, ctx: authData })
134
+ },
135
+ schema,
136
+ request
137
+ )
146
138
 
147
139
  return {
148
140
  response,
@@ -195,7 +187,7 @@ export function createZeroServer<
195
187
  }
196
188
 
197
189
  function query<R>(
198
- cb: (q: Transaction['query']) => Query<Schema, any, R>
190
+ cb: (q: Transaction['query']) => Query<any, Schema, R>
199
191
  ): Promise<HumanReadable<R>> {
200
192
  return transaction(async (tx) => {
201
193
  return cb(tx.query)
@@ -1,4 +1,4 @@
1
- import { sleep } from '@vxrn/helpers'
1
+ import { sleep } from '@take-out/helpers'
2
2
 
3
3
  import type { Query, Row } from '@rocicorp/zero'
4
4