over-zero 0.0.33 → 0.0.35

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 (70) hide show
  1. package/dist/cjs/cli.cjs +38 -57
  2. package/dist/cjs/cli.js +38 -54
  3. package/dist/cjs/cli.js.map +1 -1
  4. package/dist/cjs/cli.native.js +42 -69
  5. package/dist/cjs/cli.native.js.map +1 -1
  6. package/dist/cjs/createUseQuery.cjs +72 -0
  7. package/dist/cjs/createUseQuery.js +46 -0
  8. package/dist/cjs/createUseQuery.js.map +6 -0
  9. package/dist/cjs/createUseQuery.native.js +88 -0
  10. package/dist/cjs/createUseQuery.native.js.map +1 -0
  11. package/dist/cjs/createZeroClient.cjs +10 -36
  12. package/dist/cjs/createZeroClient.js +8 -18
  13. package/dist/cjs/createZeroClient.js.map +2 -2
  14. package/dist/cjs/createZeroClient.native.js +9 -47
  15. package/dist/cjs/createZeroClient.native.js.map +1 -1
  16. package/dist/cjs/createZeroServer.cjs +5 -4
  17. package/dist/cjs/createZeroServer.js +8 -3
  18. package/dist/cjs/createZeroServer.js.map +1 -1
  19. package/dist/cjs/createZeroServer.native.js +39 -5
  20. package/dist/cjs/createZeroServer.native.js.map +1 -1
  21. package/dist/cjs/index.cjs +1 -0
  22. package/dist/cjs/index.js +1 -0
  23. package/dist/cjs/index.js.map +1 -1
  24. package/dist/cjs/index.native.js +1 -0
  25. package/dist/cjs/index.native.js.map +1 -1
  26. package/dist/esm/cli.js +38 -54
  27. package/dist/esm/cli.js.map +1 -1
  28. package/dist/esm/cli.mjs +38 -57
  29. package/dist/esm/cli.mjs.map +1 -1
  30. package/dist/esm/cli.native.js +42 -69
  31. package/dist/esm/cli.native.js.map +1 -1
  32. package/dist/esm/createUseQuery.js +34 -0
  33. package/dist/esm/createUseQuery.js.map +6 -0
  34. package/dist/esm/createUseQuery.mjs +49 -0
  35. package/dist/esm/createUseQuery.mjs.map +1 -0
  36. package/dist/esm/createUseQuery.native.js +62 -0
  37. package/dist/esm/createUseQuery.native.js.map +1 -0
  38. package/dist/esm/createZeroClient.js +10 -21
  39. package/dist/esm/createZeroClient.js.map +1 -1
  40. package/dist/esm/createZeroClient.mjs +11 -37
  41. package/dist/esm/createZeroClient.mjs.map +1 -1
  42. package/dist/esm/createZeroClient.native.js +11 -49
  43. package/dist/esm/createZeroClient.native.js.map +1 -1
  44. package/dist/esm/createZeroServer.js +8 -3
  45. package/dist/esm/createZeroServer.js.map +1 -1
  46. package/dist/esm/createZeroServer.mjs +5 -4
  47. package/dist/esm/createZeroServer.mjs.map +1 -1
  48. package/dist/esm/createZeroServer.native.js +39 -5
  49. package/dist/esm/createZeroServer.native.js.map +1 -1
  50. package/dist/esm/index.js +1 -0
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/index.mjs +1 -0
  53. package/dist/esm/index.mjs.map +1 -1
  54. package/dist/esm/index.native.js +1 -0
  55. package/dist/esm/index.native.js.map +1 -1
  56. package/package.json +1 -1
  57. package/readme.md +23 -0
  58. package/src/cli.ts +79 -102
  59. package/src/createUseQuery.tsx +141 -0
  60. package/src/createZeroClient.tsx +21 -119
  61. package/src/createZeroServer.ts +16 -7
  62. package/src/index.ts +1 -0
  63. package/types/createUseQuery.d.ts +22 -0
  64. package/types/createUseQuery.d.ts.map +1 -0
  65. package/types/createZeroClient.d.ts +6 -18
  66. package/types/createZeroClient.d.ts.map +1 -1
  67. package/types/createZeroServer.d.ts +3 -3
  68. package/types/createZeroServer.d.ts.map +1 -1
  69. package/types/index.d.ts +1 -0
  70. package/types/index.d.ts.map +1 -1
package/src/cli.ts CHANGED
@@ -274,23 +274,23 @@ const generate = defineCommand({
274
274
  )
275
275
 
276
276
  const allQueries = queryResults.flat()
277
- const clientQueriesOutput = generateClientQueriesFile(allQueries)
278
- const serverQueriesOutput = generateServerQueriesFile(allQueries)
277
+ const groupedQueriesOutput = generateGroupedQueriesFile(allQueries)
278
+ const syncedQueriesOutput = generateSyncedQueriesFile(allQueries)
279
279
 
280
- const clientChanged = writeFileIfChanged(
281
- resolve(generatedDir, 'queries.client.ts'),
282
- clientQueriesOutput
280
+ const groupedChanged = writeFileIfChanged(
281
+ resolve(generatedDir, 'groupedQueries.ts'),
282
+ groupedQueriesOutput
283
283
  )
284
- const serverChanged = writeFileIfChanged(
285
- resolve(generatedDir, 'queries.server.ts'),
286
- serverQueriesOutput
284
+ const syncedChanged = writeFileIfChanged(
285
+ resolve(generatedDir, 'syncedQueries.ts'),
286
+ syncedQueriesOutput
287
287
  )
288
288
 
289
- if (clientChanged) {
290
- console.info(` 📝 Updated queries.client.ts`)
289
+ if (groupedChanged) {
290
+ console.info(` 📝 Updated groupedQueries.ts`)
291
291
  }
292
- if (serverChanged) {
293
- console.info(` 📝 Updated queries.server.ts`)
292
+ if (syncedChanged) {
293
+ console.info(` 📝 Updated syncedQueries.ts`)
294
294
  }
295
295
 
296
296
  console.info(
@@ -405,7 +405,7 @@ function generateTablesFile(modelFiles: string[]) {
405
405
  return `// auto-generated by: over-zero generate\n// this is separate from models as otherwise you end up with circular types :/\n\n${exports}\n`
406
406
  }
407
407
 
408
- function generateClientQueriesFile(
408
+ function generateGroupedQueriesFile(
409
409
  queries: Array<{
410
410
  name: string
411
411
  params: string
@@ -416,30 +416,23 @@ function generateClientQueriesFile(
416
416
  // get unique source files sorted
417
417
  const sortedFiles = [...new Set(queries.map((q) => q.sourceFile))].sort()
418
418
 
419
- // generate imports
420
- const imports = sortedFiles
421
- .map((file) => `import * as ${file}Queries from '../queries/${file}'`)
419
+ // generate re-exports
420
+ const exports = sortedFiles
421
+ .map((file) => `export * as ${file} from '../queries/${file}'`)
422
422
  .join('\n')
423
423
 
424
- // generate namespaced export object
425
- const namespaces = sortedFiles.map((file) => ` ${file}: ${file}Queries,`).join('\n')
426
-
427
424
  return `/**
428
425
  * auto-generated by: over-zero generate
429
426
  *
430
- * client-side query references for minification-safe query identity.
427
+ * grouped query re-exports for minification-safe query identity.
431
428
  * this file re-exports all query modules - while this breaks tree-shaking,
432
429
  * queries are typically small and few in number even in larger apps.
433
430
  */
434
- ${imports}
435
-
436
- export const clientQueries = {
437
- ${namespaces}
438
- }
431
+ ${exports}
439
432
  `
440
433
  }
441
434
 
442
- function generateServerQueriesFile(
435
+ function generateSyncedQueriesFile(
443
436
  queries: Array<{
444
437
  name: string
445
438
  params: string
@@ -459,95 +452,79 @@ function generateServerQueriesFile(
459
452
  // sort file names for consistent output
460
453
  const sortedFiles = Array.from(queryByFile.keys()).sort()
461
454
 
462
- // generate imports
463
- const queryImports = sortedFiles
464
- .map((file) => `import * as ${file}Queries from '../queries/${file}'`)
465
- .join('\n')
466
-
467
455
  const imports = `// auto-generated by: over-zero generate
468
456
  // server-side syncedQuery wrappers with validators
469
457
  import { syncedQuery } from '@rocicorp/zero'
470
458
  import * as v from 'valibot'
471
- ${queryImports}
459
+ import * as Queries from './groupedQueries'
472
460
  `
473
461
 
474
- // generate synced queries grouped by namespace
475
- const sortedQueries = [...queries].sort((a, b) => a.name.localeCompare(b.name))
462
+ // generate grouped exports by namespace
463
+ const namespaceExports = sortedFiles
464
+ .map((file) => {
465
+ const fileQueries = queryByFile
466
+ .get(file)!
467
+ .sort((a, b) => a.name.localeCompare(b.name))
476
468
 
477
- const syncedQueryDefs = sortedQueries
478
- .map((q) => {
479
- // extract validator schema
480
- const lines = q.valibotCode.split('\n').filter((l) => l.trim())
481
- const schemaLineIndex = lines.findIndex((l) =>
482
- l.startsWith('export const QueryParams')
483
- )
469
+ const queryDefs = fileQueries
470
+ .map((q) => {
471
+ // extract validator schema
472
+ const lines = q.valibotCode.split('\n').filter((l) => l.trim())
473
+ const schemaLineIndex = lines.findIndex((l) =>
474
+ l.startsWith('export const QueryParams')
475
+ )
484
476
 
485
- let validatorDef = 'v.void()'
486
- if (schemaLineIndex !== -1) {
487
- const schemaLines: string[] = []
488
- let openBraces = 0
489
- let started = false
490
-
491
- for (let i = schemaLineIndex; i < lines.length; i++) {
492
- const line = lines[i]!
493
- const cleaned = started ? line : line.replace('export const QueryParams = ', '')
494
- schemaLines.push(cleaned)
495
- started = true
496
-
497
- openBraces += (cleaned.match(/\{/g) || []).length
498
- openBraces -= (cleaned.match(/\}/g) || []).length
499
- openBraces += (cleaned.match(/\(/g) || []).length
500
- openBraces -= (cleaned.match(/\)/g) || []).length
501
-
502
- if (openBraces === 0 && schemaLines.length > 0) {
503
- break
477
+ let validatorDef = 'v.void()'
478
+ if (schemaLineIndex !== -1) {
479
+ const schemaLines: string[] = []
480
+ let openBraces = 0
481
+ let started = false
482
+
483
+ for (let i = schemaLineIndex; i < lines.length; i++) {
484
+ const line = lines[i]!
485
+ const cleaned = started
486
+ ? line
487
+ : line.replace('export const QueryParams = ', '')
488
+ schemaLines.push(cleaned)
489
+ started = true
490
+
491
+ openBraces += (cleaned.match(/\{/g) || []).length
492
+ openBraces -= (cleaned.match(/\}/g) || []).length
493
+ openBraces += (cleaned.match(/\(/g) || []).length
494
+ openBraces -= (cleaned.match(/\)/g) || []).length
495
+
496
+ if (openBraces === 0 && schemaLines.length > 0) {
497
+ break
498
+ }
499
+ }
500
+ validatorDef = schemaLines.join('\n')
504
501
  }
505
- }
506
- validatorDef = schemaLines.join('\n')
507
- }
508
502
 
509
- // wrap validator in v.parser(v.tuple([...]))
510
- const wrappedValidator =
511
- validatorDef === 'v.void()'
512
- ? 'v.parser(v.tuple([]))'
513
- : `v.parser(v.tuple([${validatorDef}]))`
514
-
515
- // namespaced query name: file.queryName
516
- const namespacedName = `${q.sourceFile}.${q.name}`
517
-
518
- // for void queries, no arg parameter
519
- const queryFn =
520
- validatorDef === 'v.void()'
521
- ? `() => {
522
- return ${q.sourceFile}Queries.${q.name}()
523
- }`
524
- : `(arg) => {
525
- return ${q.sourceFile}Queries.${q.name}(arg)
526
- }`
527
-
528
- return `const ${q.name}Synced = syncedQuery('${namespacedName}', ${wrappedValidator}, ${queryFn})`
529
- })
530
- .join('\n\n')
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}]))`
531
508
 
532
- // generate namespaced export object
533
- const namespaces = sortedFiles
534
- .map((file) => {
535
- const fileQueries = queryByFile
536
- .get(file)!
537
- .sort((a, b) => a.name.localeCompare(b.name))
538
- const queryEntries = fileQueries
539
- .map((q) => ` ${q.name}: ${q.name}Synced,`)
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)`
517
+
518
+ return ` ${q.name}: syncedQuery('${namespacedName}', ${wrappedValidator}, ${queryFn}),`
519
+ })
540
520
  .join('\n')
541
- return ` ${file}: {\n${queryEntries}\n },`
521
+
522
+ return `export const ${file} = {\n${queryDefs}\n}`
542
523
  })
543
- .join('\n')
524
+ .join('\n\n')
544
525
 
545
526
  return `${imports}
546
- ${syncedQueryDefs}
547
-
548
- export const serverQueries = {
549
- ${namespaces}
550
- }
527
+ ${namespaceExports}
551
528
  `
552
529
  }
553
530
 
@@ -561,8 +538,8 @@ this folder is auto-generated by over-zero. do not edit files here directly.
561
538
  - \`models.ts\` - exports all models from ../models
562
539
  - \`types.ts\` - typescript types derived from table schemas
563
540
  - \`tables.ts\` - exports table schemas for type inference
564
- - \`queries.client.ts\` - namespaced query references for client setup
565
- - \`queries.server.ts\` - namespaced syncedQuery wrappers for server setup
541
+ - \`groupedQueries.ts\` - namespaced query re-exports for client setup
542
+ - \`syncedQueries.ts\` - namespaced syncedQuery wrappers for server setup
566
543
 
567
544
  ## usage guidelines
568
545
 
@@ -0,0 +1,141 @@
1
+ import { syncedQuery } from '@rocicorp/zero'
2
+ import { useQuery as zeroUseQuery } from '@rocicorp/zero/react'
3
+ import { use, useMemo, type Context } from 'react'
4
+
5
+ import { useZeroDebug } from './helpers/useZeroDebug'
6
+ import { getQueryName } from './queryRegistry'
7
+
8
+ import type {
9
+ HumanReadable,
10
+ Query,
11
+ ReadonlyJSONValue,
12
+ SyncedQuery,
13
+ Schema as ZeroSchema,
14
+ } from '@rocicorp/zero'
15
+
16
+ export type UseQueryOptions = {
17
+ enabled?: boolean | undefined
18
+ ttl?: 'always' | 'never' | number | undefined
19
+ }
20
+
21
+ type QueryResultDetails = ReturnType<typeof zeroUseQuery>[1]
22
+ export type QueryResult<TReturn> = readonly [HumanReadable<TReturn>, QueryResultDetails]
23
+
24
+ export type PlainQueryFn<
25
+ TArg = any,
26
+ TReturn extends Query<any, any, any> = Query<any, any, any>,
27
+ > = (args: TArg) => TReturn
28
+
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
50
+ <TArg, TTable extends keyof Schema['tables'] & string, TReturn>(
51
+ fn: PlainQueryFn<TArg, Query<Schema, TTable, TReturn>>,
52
+ params: TArg,
53
+ options?: UseQueryOptions | boolean
54
+ ): QueryResult<TReturn>;
55
+
56
+ // overload 3: plain function with no params
57
+ <TTable extends keyof Schema['tables'] & string, TReturn>(
58
+ fn: PlainQueryFn<void, Query<Schema, TTable, TReturn>>,
59
+ options?: UseQueryOptions | boolean
60
+ ): QueryResult<TReturn>
61
+ }
62
+
63
+ export function createUseQuery<
64
+ Schema extends ZeroSchema,
65
+ const DisableInline extends boolean = false,
66
+ >({
67
+ DisabledContext,
68
+ disableInlineQueries = false as DisableInline,
69
+ }: {
70
+ 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
+
76
+ function useQuery(...args: any[]): any {
77
+ 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
+ }
91
+
92
+ const fn = queryOrFn
93
+ const queryName = getQueryName(fn)
94
+
95
+ if (!queryName) {
96
+ throw new Error(`No query name?`)
97
+ }
98
+
99
+ // determine if this is pattern 2 (with params) or pattern 3 (no params)
100
+ const hasParams =
101
+ optionsArg !== undefined ||
102
+ (paramsOrOptions &&
103
+ typeof paramsOrOptions === 'object' &&
104
+ !('enabled' in paramsOrOptions) &&
105
+ !('ttl' in paramsOrOptions))
106
+
107
+ const params = hasParams ? paramsOrOptions : undefined
108
+ const opts = hasParams ? optionsArg : paramsOrOptions
109
+
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
+ }
117
+
118
+ // call the SyncedQuery with params if provided
119
+ const query = params !== undefined ? (synced as any)(params) : synced
120
+
121
+ return { actualQuery: query, options: opts }
122
+ }, [queryOrFn, paramsOrOptions, optionsArg, isPlainFunction])
123
+
124
+ console.info('running', { actualQuery, options, queryOrFn })
125
+
126
+ const out = zeroUseQuery(actualQuery, options)
127
+
128
+ if (process.env.NODE_ENV === 'development') {
129
+ // eslint-disable-next-line react-hooks/rules-of-hooks
130
+ useZeroDebug(actualQuery, options, out)
131
+ }
132
+
133
+ if (disabled) {
134
+ return [null, { type: 'unknown' }] as never
135
+ }
136
+
137
+ return out
138
+ }
139
+
140
+ return useQuery as UseQueryHook<Schema, DisableInline>
141
+ }
@@ -1,41 +1,33 @@
1
- import { syncedQuery } from '@rocicorp/zero'
2
- import { useZero, ZeroProvider, useQuery as zeroUseQuery } from '@rocicorp/zero/react'
1
+ import { useZero, ZeroProvider } from '@rocicorp/zero/react'
3
2
  import { createEmitter, mapObject } from '@vxrn/helpers'
4
3
  import { createContext, use, useMemo, type ReactNode } from 'react'
5
4
 
6
5
  import { createPermissions } from './createPermissions'
6
+ import { createUseQuery } from './createUseQuery'
7
7
  import { createMutators } from './helpers/createMutators'
8
8
  import { prettyFormatZeroQuery } from './helpers/prettyFormatZeroQuery'
9
- import { useZeroDebug } from './helpers/useZeroDebug'
10
- import { registerQuery, getQueryName } from './queryRegistry'
9
+ import { registerQuery } from './queryRegistry'
11
10
  import { setAuthData, setSchema } from './state'
12
11
 
13
12
  import type { AuthData, GenericModels, GetZeroMutators, ZeroEvent } from './types'
14
- import type {
15
- HumanReadable,
16
- Query,
17
- ReadonlyJSONValue,
18
- Row,
19
- SyncedQuery,
20
- Zero,
21
- ZeroOptions,
22
- Schema as ZeroSchema,
23
- } from '@rocicorp/zero'
24
-
25
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
26
- export type ClientQueries = Record<string, Record<string, (...args: any[]) => any>>
13
+ import type { Row, Zero, ZeroOptions, Schema as ZeroSchema } from '@rocicorp/zero'
14
+
15
+ export type GroupedQueries = Record<string, Record<string, (...args: any[]) => any>>
27
16
 
28
17
  export function createZeroClient<
29
18
  Schema extends ZeroSchema,
30
19
  Models extends GenericModels,
20
+ const DisableInlineQueries extends boolean = false,
31
21
  >({
32
22
  schema,
33
23
  models,
34
- clientQueries,
24
+ groupedQueries,
25
+ disableInlineQueries = false as DisableInlineQueries,
35
26
  }: {
36
27
  schema: Schema
37
28
  models: Models
38
- clientQueries: ClientQueries
29
+ groupedQueries: GroupedQueries
30
+ disableInlineQueries?: DisableInlineQueries
39
31
  }) {
40
32
  type ZeroMutators = GetZeroMutators<Models>
41
33
  type ZeroInstance = Zero<Schema, ZeroMutators>
@@ -43,8 +35,8 @@ export function createZeroClient<
43
35
 
44
36
  setSchema(schema)
45
37
 
46
- // build query registry from namespaced clientQueries
47
- for (const [namespace, queries] of Object.entries(clientQueries)) {
38
+ // build query registry from grouped queries
39
+ for (const [namespace, queries] of Object.entries(groupedQueries)) {
48
40
  for (const [name, fn] of Object.entries(queries)) {
49
41
  registerQuery(fn, `${namespace}.${name}`)
50
42
  }
@@ -80,6 +72,11 @@ export function createZeroClient<
80
72
  const AuthDataContext = createContext<AuthData>({} as AuthData)
81
73
  const useAuthData = () => use(AuthDataContext)
82
74
 
75
+ const useQuery = createUseQuery<Schema, DisableInlineQueries>({
76
+ DisabledContext,
77
+ disableInlineQueries,
78
+ })
79
+
83
80
  // we don't want flickers as you move around and these queries are re-run
84
81
  // and things generally aren't changing with permissions rapidly, so lets
85
82
  // cache the last results and use that when first rendering, they will
@@ -112,7 +109,9 @@ export function createZeroClient<
112
109
  })
113
110
  })()
114
111
 
115
- const [data, status] = useQuery(query, {
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, {
116
115
  enabled: Boolean(enabled && permission && authData && objOrId),
117
116
  })
118
117
 
@@ -135,103 +134,6 @@ export function createZeroClient<
135
134
  return allowed
136
135
  }
137
136
 
138
- type PlainQueryFn<
139
- TArg = any,
140
- TReturn extends Query<any, any, any> = Query<any, any, any>,
141
- > = (args: TArg) => TReturn
142
-
143
- type UseQueryOptions = {
144
- enabled?: boolean | undefined
145
- ttl?: 'always' | 'never' | number | undefined
146
- }
147
-
148
- type QueryResultDetails = ReturnType<typeof zeroUseQuery>[1]
149
- type QueryResult<TReturn> = readonly [HumanReadable<TReturn>, QueryResultDetails]
150
-
151
- const queryCache = new Map<string, SyncedQuery<any, any, any, any, any>>()
152
-
153
- const parseAny = (x: unknown[]): [ReadonlyJSONValue] => [x[0] as ReadonlyJSONValue]
154
-
155
- // Overload 1: Original API - Query or SyncedQuery with options
156
- function useQuery<TTable extends keyof Schema['tables'] & string, TReturn>(
157
- query:
158
- | Query<Schema, TTable, TReturn>
159
- | SyncedQuery<any, any, any, any, Query<Schema, TTable, TReturn>>,
160
- options?: UseQueryOptions | boolean
161
- ): QueryResult<TReturn>
162
-
163
- // Overload 2: Plain function with params
164
- function useQuery<TArg, TTable extends keyof Schema['tables'] & string, TReturn>(
165
- fn: PlainQueryFn<TArg, Query<Schema, TTable, TReturn>>,
166
- params: TArg,
167
- options?: UseQueryOptions | boolean
168
- ): QueryResult<TReturn>
169
-
170
- // Overload 3: Plain function with no params
171
- function useQuery<TTable extends keyof Schema['tables'] & string, TReturn>(
172
- fn: PlainQueryFn<void, Query<Schema, TTable, TReturn>>,
173
- options?: UseQueryOptions | boolean
174
- ): QueryResult<TReturn>
175
-
176
- // Implementation - keep it simple with any
177
- function useQuery(...args: any[]): any {
178
- const disabled = use(DisabledContext)
179
- const [queryOrFn, paramsOrOptions, optionsArg] = args
180
-
181
- // Detect which calling pattern is being used
182
- const isPlainFunction = typeof queryOrFn === 'function' && !('queryName' in queryOrFn)
183
-
184
- const { actualQuery, options } = useMemo(() => {
185
- if (!isPlainFunction) {
186
- // Pattern 1: Original API - useQuery(query, options)
187
- return {
188
- actualQuery: queryOrFn,
189
- options: paramsOrOptions,
190
- }
191
- }
192
-
193
- const fn = queryOrFn
194
- const queryName = getQueryName(fn) || fn.name || 'anonymousQuery'
195
-
196
- // Determine if this is Pattern 2 (with params) or Pattern 3 (no params)
197
- const hasParams =
198
- optionsArg !== undefined ||
199
- (paramsOrOptions &&
200
- typeof paramsOrOptions === 'object' &&
201
- !('enabled' in paramsOrOptions) &&
202
- !('ttl' in paramsOrOptions))
203
-
204
- const params = hasParams ? paramsOrOptions : undefined
205
- const opts = hasParams ? optionsArg : paramsOrOptions
206
-
207
- let synced = queryCache.get(queryName)
208
- if (!synced) {
209
- synced = syncedQuery(queryName, parseAny, (arg: ReadonlyJSONValue) => {
210
- return fn(arg)
211
- })
212
- queryCache.set(queryName, synced)
213
- }
214
-
215
- // Call the SyncedQuery with params if provided
216
- const query = params !== undefined ? (synced as any)(params) : synced
217
-
218
- return { actualQuery: query, options: opts }
219
- }, [queryOrFn, paramsOrOptions, optionsArg, isPlainFunction])
220
-
221
- const out = zeroUseQuery(actualQuery, options)
222
-
223
- if (process.env.NODE_ENV === 'development') {
224
- // eslint-disable-next-line react-hooks/rules-of-hooks
225
- useZeroDebug(actualQuery, options, out)
226
- }
227
-
228
- if (disabled) {
229
- return [null, { type: 'unknown' }] as never
230
- }
231
-
232
- return out
233
- }
234
-
235
137
  const ProvideZero = ({
236
138
  children,
237
139
  authData: authDataIn,
@@ -24,8 +24,8 @@ import type {
24
24
  } from '@rocicorp/zero'
25
25
  import type { TransactionProviderInput } from '@rocicorp/zero/pg'
26
26
 
27
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
- export type ServerQueries = Record<
27
+ // grouped synced queries: { namespace: { queryName: SyncedQuery } }
28
+ export type SyncedQueries = Record<
29
29
  string,
30
30
  Record<string, SyncedQuery<any, any, any, any, any>>
31
31
  >
@@ -34,12 +34,13 @@ export function createZeroServer<
34
34
  Schema extends ZeroSchema,
35
35
  Models extends GenericModels,
36
36
  ServerActions extends Record<string, unknown>,
37
+ Queries extends SyncedQueries = Record<string, never>,
37
38
  >({
38
39
  createServerActions,
39
40
  database,
40
41
  schema,
41
42
  models,
42
- serverQueries,
43
+ syncedQueries,
43
44
  }: {
44
45
  /**
45
46
  * The DB connection string, same as ZERO_UPSTREAM_DB
@@ -48,7 +49,7 @@ export function createZeroServer<
48
49
  schema: Schema
49
50
  models: Models
50
51
  createServerActions: () => ServerActions
51
- serverQueries?: ServerQueries
52
+ syncedQueries?: Queries
52
53
  }) {
53
54
  setSchema(schema)
54
55
 
@@ -111,6 +112,16 @@ export function createZeroServer<
111
112
  }
112
113
  }
113
114
 
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
+
114
125
  const handleQueryRequest = async ({
115
126
  authData,
116
127
  request,
@@ -119,9 +130,7 @@ export function createZeroServer<
119
130
  request: Request
120
131
  }) => {
121
132
  function getQuery(name: string, args: readonly ReadonlyJSONValue[]) {
122
- // name format: "namespace.queryName" (e.g. "post.allPosts")
123
- const [namespace, queryName] = name.split('.')
124
- const q = serverQueries?.[namespace!]?.[queryName!]
133
+ const q = queryLookup.get(name)
125
134
  if (!q) {
126
135
  throw new Error(`No such query: ${name}`)
127
136
  }
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export * from './helpers/ensureLoggedIn'
6
6
  export * from './helpers/mutatorContext'
7
7
 
8
8
  export * from './createZeroClient'
9
+ export * from './createUseQuery'
9
10
  export * from './mutations'
10
11
  export * from './where'
11
12
  export * from './serverWhere'
@@ -0,0 +1,22 @@
1
+ import { useQuery as zeroUseQuery } from '@rocicorp/zero/react';
2
+ import { type Context } from 'react';
3
+ import type { HumanReadable, Query, SyncedQuery, Schema as ZeroSchema } from '@rocicorp/zero';
4
+ export type UseQueryOptions = {
5
+ enabled?: boolean | undefined;
6
+ ttl?: 'always' | 'never' | number | undefined;
7
+ };
8
+ type QueryResultDetails = ReturnType<typeof zeroUseQuery>[1];
9
+ export type QueryResult<TReturn> = readonly [HumanReadable<TReturn>, QueryResultDetails];
10
+ export type PlainQueryFn<TArg = any, TReturn extends Query<any, any, any> = Query<any, any, any>> = (args: TArg) => TReturn;
11
+ type InlineQuery<Schema extends ZeroSchema, TTable extends keyof Schema['tables'] & string, TReturn> = Query<Schema, TTable, TReturn> | SyncedQuery<any, any, any, any, Query<Schema, TTable, TReturn>>;
12
+ export type UseQueryHook<Schema extends ZeroSchema, DisableInline extends boolean = false> = {
13
+ <TTable extends keyof Schema['tables'] & string, TReturn>(query: DisableInline extends true ? never : InlineQuery<Schema, TTable, TReturn>, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
14
+ <TArg, TTable extends keyof Schema['tables'] & string, TReturn>(fn: PlainQueryFn<TArg, Query<Schema, TTable, TReturn>>, params: TArg, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
15
+ <TTable extends keyof Schema['tables'] & string, TReturn>(fn: PlainQueryFn<void, Query<Schema, TTable, TReturn>>, options?: UseQueryOptions | boolean): QueryResult<TReturn>;
16
+ };
17
+ export declare function createUseQuery<Schema extends ZeroSchema, const DisableInline extends boolean = false>({ DisabledContext, disableInlineQueries, }: {
18
+ DisabledContext: Context<boolean>;
19
+ disableInlineQueries?: DisableInline;
20
+ }): UseQueryHook<Schema, DisableInline>;
21
+ export {};
22
+ //# sourceMappingURL=createUseQuery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createUseQuery.d.ts","sourceRoot":"","sources":["../src/createUseQuery.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAgB,KAAK,OAAO,EAAE,MAAM,OAAO,CAAA;AAKlD,OAAO,KAAK,EACV,aAAa,EACb,KAAK,EAEL,WAAW,EACX,MAAM,IAAI,UAAU,EACrB,MAAM,gBAAgB,CAAA;AAEvB,MAAM,MAAM,eAAe,GAAG;IAC5B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;IAC7B,GAAG,CAAC,EAAE,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,SAAS,CAAA;CAC9C,CAAA;AAED,KAAK,kBAAkB,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC,CAAC,CAAC,CAAA;AAC5D,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,EAAE,kBAAkB,CAAC,CAAA;AAExF,MAAM,MAAM,YAAY,CACtB,IAAI,GAAG,GAAG,EACV,OAAO,SAAS,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,IACzD,CAAC,IAAI,EAAE,IAAI,KAAK,OAAO,CAAA;AAG3B,KAAK,WAAW,CACd,MAAM,SAAS,UAAU,EACzB,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAC9C,OAAO,IAEL,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,GAC9B,WAAW,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAGnE,MAAM,MAAM,YAAY,CACtB,MAAM,SAAS,UAAU,EACzB,aAAa,SAAS,OAAO,GAAG,KAAK,IACnC;IAEF,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,KAAK,EAAE,aAAa,SAAS,IAAI,GAAG,KAAK,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAChF,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,IAAI,EAAE,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EAC5D,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,MAAM,EAAE,IAAI,EACZ,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAC;IAGxB,CAAC,MAAM,SAAS,MAAM,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,EAAE,OAAO,EACtD,EAAE,EAAE,YAAY,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC,EACtD,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,GAClC,WAAW,CAAC,OAAO,CAAC,CAAA;CACxB,CAAA;AAED,wBAAgB,cAAc,CAC5B,MAAM,SAAS,UAAU,EACzB,KAAK,CAAC,aAAa,SAAS,OAAO,GAAG,KAAK,EAC3C,EACA,eAAe,EACf,oBAA6C,GAC9C,EAAE;IACD,eAAe,EAAE,OAAO,CAAC,OAAO,CAAC,CAAA;IACjC,oBAAoB,CAAC,EAAE,aAAa,CAAA;CACrC,GAAG,YAAY,CAAC,MAAM,EAAE,aAAa,CAAC,CAqEtC"}