on-zero 0.0.99 → 0.1.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 (81) hide show
  1. package/dist/cjs/createZeroClient.cjs +2 -2
  2. package/dist/cjs/createZeroClient.js +2 -2
  3. package/dist/cjs/createZeroClient.js.map +1 -1
  4. package/dist/cjs/createZeroClient.native.js +2 -2
  5. package/dist/cjs/createZeroClient.native.js.map +1 -1
  6. package/dist/cjs/createZeroServer.cjs +2 -2
  7. package/dist/cjs/createZeroServer.js +2 -2
  8. package/dist/cjs/createZeroServer.js.map +1 -1
  9. package/dist/cjs/createZeroServer.native.js +2 -2
  10. package/dist/cjs/createZeroServer.native.js.map +1 -1
  11. package/dist/cjs/index.cjs +2 -3
  12. package/dist/cjs/index.js +2 -2
  13. package/dist/cjs/index.js.map +1 -1
  14. package/dist/cjs/index.native.js +2 -3
  15. package/dist/cjs/index.native.js.map +1 -1
  16. package/dist/cjs/query.cjs +3 -5
  17. package/dist/cjs/query.js +2 -2
  18. package/dist/cjs/query.js.map +1 -1
  19. package/dist/cjs/query.native.js +4 -5
  20. package/dist/cjs/query.native.js.map +1 -1
  21. package/dist/cjs/run.cjs +12 -7
  22. package/dist/cjs/run.js +8 -3
  23. package/dist/cjs/run.js.map +1 -1
  24. package/dist/cjs/run.native.js +13 -8
  25. package/dist/cjs/run.native.js.map +1 -1
  26. package/dist/cjs/zeroRunner.cjs +2 -0
  27. package/dist/cjs/zeroRunner.js +3 -0
  28. package/dist/cjs/zeroRunner.js.map +1 -1
  29. package/dist/cjs/zeroRunner.native.js +3 -1
  30. package/dist/cjs/zeroRunner.native.js.map +1 -1
  31. package/dist/esm/createZeroClient.js +1 -1
  32. package/dist/esm/createZeroClient.js.map +1 -1
  33. package/dist/esm/createZeroClient.mjs +1 -1
  34. package/dist/esm/createZeroClient.mjs.map +1 -1
  35. package/dist/esm/createZeroClient.native.js +1 -1
  36. package/dist/esm/createZeroClient.native.js.map +1 -1
  37. package/dist/esm/createZeroServer.js +1 -1
  38. package/dist/esm/createZeroServer.mjs +1 -1
  39. package/dist/esm/createZeroServer.native.js +1 -1
  40. package/dist/esm/index.js +1 -2
  41. package/dist/esm/index.js.map +1 -1
  42. package/dist/esm/index.mjs +2 -2
  43. package/dist/esm/index.mjs.map +1 -1
  44. package/dist/esm/index.native.js +2 -2
  45. package/dist/esm/index.native.js.map +1 -1
  46. package/dist/esm/query.js +2 -2
  47. package/dist/esm/query.js.map +1 -1
  48. package/dist/esm/query.mjs +3 -5
  49. package/dist/esm/query.mjs.map +1 -1
  50. package/dist/esm/query.native.js +4 -5
  51. package/dist/esm/query.native.js.map +1 -1
  52. package/dist/esm/run.js +8 -3
  53. package/dist/esm/run.js.map +1 -1
  54. package/dist/esm/run.mjs +12 -7
  55. package/dist/esm/run.mjs.map +1 -1
  56. package/dist/esm/run.native.js +13 -8
  57. package/dist/esm/run.native.js.map +1 -1
  58. package/dist/esm/zeroRunner.js +3 -0
  59. package/dist/esm/zeroRunner.js.map +1 -1
  60. package/dist/esm/zeroRunner.mjs +2 -0
  61. package/dist/esm/zeroRunner.mjs.map +1 -1
  62. package/dist/esm/zeroRunner.native.js +2 -0
  63. package/dist/esm/zeroRunner.native.js.map +1 -1
  64. package/package.json +2 -2
  65. package/readme.md +601 -0
  66. package/src/createZeroClient.tsx +1 -1
  67. package/src/createZeroServer.ts +1 -1
  68. package/src/index.ts +1 -1
  69. package/src/modelRegistry.ts +1 -1
  70. package/src/{query.ts → run.ts} +34 -14
  71. package/src/zeroRunner.ts +7 -0
  72. package/types/createZeroClient.d.ts.map +1 -1
  73. package/types/index.d.ts +1 -1
  74. package/types/index.d.ts.map +1 -1
  75. package/types/modelRegistry.d.ts +1 -1
  76. package/types/modelRegistry.d.ts.map +1 -1
  77. package/types/run.d.ts +3 -3
  78. package/types/run.d.ts.map +1 -1
  79. package/types/zeroRunner.d.ts.map +1 -1
  80. package/types/query.d.ts +0 -6
  81. package/types/query.d.ts.map +0 -1
package/readme.md ADDED
@@ -0,0 +1,601 @@
1
+ # on-zero
2
+
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="./on-zero-dark.svg">
5
+ <source media="(prefers-color-scheme: light)" srcset="./on-zero.svg">
6
+ <img src="./on-zero.svg" width="120" alt="on-zero">
7
+ </picture>
8
+
9
+ makes [zero](https://zero.rocicorp.dev) really simple to use.
10
+
11
+ it's what we use for our [takeout stack](https://takeout.tamagui.dev).
12
+
13
+ ## what it does
14
+
15
+ on-zero tries to bring Rails-like structure and DRY code to Zero + React.
16
+
17
+ it provides a few things:
18
+
19
+ - **generation** - cli with watch and generate commands
20
+ - **queries** - convert plain TS query functions into validated synced queries
21
+ - **mutations** - simply create CRUD queries with permissions
22
+ - **models** - standardized co-locating schema/permissions/mutations
23
+ - **permissions** - `serverWhere` for simple query-based permissions
24
+
25
+ plus various hooks and helpers for react integration.
26
+
27
+ models live alongside their permissions and mutations. queries are just
28
+ functions that use a global `zql` builder.
29
+
30
+ ## queries
31
+
32
+ write plain functions. they become synced queries automatically.
33
+
34
+ ```ts
35
+ // src/data/queries/notification.ts
36
+ import { zql, serverWhere } from 'on-zero'
37
+
38
+ const permission = serverWhere('notification', (q, auth) => {
39
+ return q.cmp('userId', auth?.id || '')
40
+ })
41
+
42
+ export const latestNotifications = (props: {
43
+ userId: string
44
+ serverId: string
45
+ }) => {
46
+ return zql.notification
47
+ .where(permission)
48
+ .where('userId', props.userId)
49
+ .where('serverId', props.serverId)
50
+ .orderBy('createdAt', 'desc')
51
+ .limit(20)
52
+ }
53
+ ```
54
+
55
+ zql is just the normal Zero query builder based on your typed schema.
56
+
57
+ use them:
58
+
59
+ ```tsx
60
+ const [data, state] = useQuery(latestNotifications, { userId, serverId })
61
+ ```
62
+
63
+ the function name becomes the query name. `useQuery` detects plain functions,
64
+ creates a cached `SyncedQuery` per function, and calls it with your params.
65
+
66
+ ### query permissions
67
+
68
+ define permissions inline using `serverWhere()`:
69
+
70
+ ```ts
71
+ const permission = serverWhere('channel', (q, auth) => {
72
+ if (auth?.role === 'admin') return true
73
+
74
+ return q.and(
75
+ q.cmp('deleted', '!=', true),
76
+ q.or(
77
+ q.cmp('private', false),
78
+ q.exists('role', (r) =>
79
+ r.whereExists('member', (m) => m.where('id', auth?.id)),
80
+ ),
81
+ ),
82
+ )
83
+ })
84
+ ```
85
+
86
+ then use in queries:
87
+
88
+ ```ts
89
+ export const channelById = (props: { channelId: string }) => {
90
+ return zql.channel.where(permission).where('id', props.channelId).one()
91
+ }
92
+ ```
93
+
94
+ permissions execute server-side only. on the client they automatically pass. the
95
+ `serverWhere()` helper automatically accesses auth data from `queryContext()` or
96
+ `mutatorContext()` so you don't need to pass it manually.
97
+
98
+ ## models
99
+
100
+ models co-locate schema, permissions, and mutations in one file:
101
+
102
+ ```ts
103
+ // src/data/models/message.ts
104
+ import { number, string, table } from '@rocicorp/zero'
105
+ import { mutations, serverWhere } from 'on-zero'
106
+
107
+ export const schema = table('message')
108
+ .columns({
109
+ id: string(),
110
+ content: string(),
111
+ authorId: string(),
112
+ channelId: string(),
113
+ createdAt: number(),
114
+ })
115
+ .primaryKey('id')
116
+
117
+ export const permissions = serverWhere('message', (q, auth) => {
118
+ return q.cmp('authorId', auth?.id || '')
119
+ })
120
+
121
+ // CRUD mutations with permissions by passing schema + permissions:
122
+ export const mutate = mutations(schema, permissions, {
123
+ async send(ctx, props: { content: string; channelId: string }) {
124
+ await ctx.can(permissions, props)
125
+
126
+ await ctx.tx.mutate.message.insert({
127
+ id: randomId(),
128
+ content: props.content,
129
+ channelId: props.channelId,
130
+ authorId: ctx.authData!.id,
131
+ createdAt: Date.now(),
132
+ })
133
+
134
+ if (ctx.server) {
135
+ ctx.server.asyncTasks.push(async () => {
136
+ await ctx.server.actions.sendNotification(props)
137
+ })
138
+ }
139
+ },
140
+ })
141
+ ```
142
+
143
+ call mutations from react:
144
+
145
+ ```tsx
146
+ await zero.mutate.message.send({ content: 'hello', channelId: 'ch-1' })
147
+ ```
148
+
149
+ the second argument (`permissions`) enables auto-generated crud that checks
150
+ permissions:
151
+
152
+ ```tsx
153
+ zero.mutate.message.insert(message)
154
+ zero.mutate.message.update(message)
155
+ zero.mutate.message.delete(message)
156
+ zero.mutate.message.upsert(message)
157
+ ```
158
+
159
+ ## permissions
160
+
161
+ on-zero's permissions system is optional - you can implement your own
162
+ permission logic however you like. `serverWhere()` is a light helper for
163
+ RLS-style permissions that automatically integrate with queries and mutations.
164
+
165
+ permissions use the `serverWhere()` helper to create Zero `ExpressionBuilder`
166
+ conditions:
167
+
168
+ ```ts
169
+ export const permissions = serverWhere('channel', (q, auth) => {
170
+ if (auth?.role === 'admin') return true
171
+
172
+ return q.or(
173
+ q.cmp('public', true),
174
+ q.exists('members', (m) => m.where('userId', auth?.id)),
175
+ )
176
+ })
177
+ ```
178
+
179
+ the `serverWhere()` helper automatically gets auth data from `queryContext()` or
180
+ `mutatorContext()`, so you don't manually pass it. permissions only execute
181
+ server-side - on the client they automatically pass.
182
+
183
+ **for queries:** define permissions inline as a constant in query files:
184
+
185
+ ```ts
186
+ // src/data/queries/channel.ts
187
+ const permission = serverWhere('channel', (q, auth) => {
188
+ return q.cmp('userId', auth?.id || '')
189
+ })
190
+
191
+ export const myChannels = () => {
192
+ return zql.channel.where(permission)
193
+ }
194
+ ```
195
+
196
+ **for mutations:** define permissions in model files for CRUD operations:
197
+
198
+ ```ts
199
+ // src/data/models/message.ts
200
+ export const permissions = serverWhere('message', (q, auth) => {
201
+ return q.cmp('authorId', auth?.id || '')
202
+ })
203
+ ```
204
+
205
+ CRUD mutations automatically apply them, but for custom mutations use `can()`:
206
+
207
+ ```ts
208
+ await ctx.can(permissions, messageId)
209
+ ```
210
+
211
+ check permissions in React with `usePermission()`:
212
+
213
+ ```tsx
214
+ const canEdit = usePermission('message', messageId)
215
+ ```
216
+
217
+ ### composable query partials
218
+
219
+ for complex or reusable query logic, create partials in a `where/` directory.
220
+ use `serverWhere` without a table name to create partials that work across
221
+ multiple tables:
222
+
223
+ ```ts
224
+ // src/data/where/server.ts
225
+ import { serverWhere } from 'on-zero'
226
+
227
+ type RelatedToServer = 'role' | 'channel' | 'message'
228
+
229
+ export const hasServerAdminPermission = serverWhere<RelatedToServer>((_, auth) =>
230
+ _.exists('server', (q) =>
231
+ q.whereExists('role', (r) =>
232
+ r.where('canAdmin', true)
233
+ .whereExists('member', (m) => m.where('id', auth?.id || ''))
234
+ )
235
+ )
236
+ )
237
+
238
+ export const hasServerReadPermission = serverWhere<RelatedToServer>((_, auth) =>
239
+ _.exists('server', (q) =>
240
+ q.where((_) =>
241
+ _.or(
242
+ _.cmp('private', false),
243
+ _.exists('member', (m) => m.where('id', auth?.id || ''))
244
+ )
245
+ )
246
+ )
247
+ )
248
+ ```
249
+
250
+ then compose them in other permissions:
251
+
252
+ ```ts
253
+ // src/data/where/channel.ts
254
+ import { serverWhere } from 'on-zero'
255
+ import { hasServerAdminPermission, hasServerReadPermission } from './server'
256
+
257
+ type RelatedToChannel = 'message' | 'pin' | 'channelTopic'
258
+
259
+ const hasChannelRole = serverWhere<RelatedToChannel>((_, auth) =>
260
+ _.exists('channel', (q) =>
261
+ q.whereExists('role', (r) =>
262
+ r.whereExists('member', (m) => m.where('id', auth?.id || ''))
263
+ )
264
+ )
265
+ )
266
+
267
+ export const hasChannelReadPermission = serverWhere<RelatedToChannel>((_, auth) => {
268
+ const isServerMember = hasServerReadPermission(_, auth)
269
+ const isChannelMember = hasChannelRole(_, auth)
270
+ const isAdmin = hasServerAdminPermission(_, auth)
271
+
272
+ return _.or(isServerMember, isChannelMember, isAdmin)
273
+ })
274
+ ```
275
+
276
+ use in queries:
277
+
278
+ ```ts
279
+ import { hasChannelReadPermission } from '../where/channel'
280
+
281
+ export const channelMessages = (props: { channelId: string }) => {
282
+ return zql.message
283
+ .where(hasChannelReadPermission)
284
+ .where('channelId', props.channelId)
285
+ }
286
+ ```
287
+
288
+ ## generation
289
+
290
+ `on-zero` has a CLI that auto-generates glue files that wire up your models,
291
+ queries, and types.
292
+
293
+ ### cli commands
294
+
295
+ **`on-zero generate [dir]`**
296
+
297
+ generates all files needed to connect your models and queries:
298
+
299
+ - `models.ts` - aggregates all model files into a single import
300
+ - `types.ts` - generates TypeScript types from table schemas
301
+ - `tables.ts` - exports table schemas (separate to avoid circular types)
302
+ - `syncedQueries.ts` - generates synced query definitions with valibot
303
+ validators
304
+
305
+ **options:**
306
+
307
+ - `dir` - base directory containing `models/` and `queries/` folders (default:
308
+ `src/data`)
309
+ - `--watch` - watch for changes and regenerate automatically
310
+ - `--after` - command to run after generation completes
311
+
312
+ **examples:**
313
+
314
+ ```bash
315
+ # generate once
316
+ bun on-zero generate
317
+
318
+ # generate and watch
319
+ bun on-zero generate --watch
320
+
321
+ # custom directory
322
+ bun on-zero generate ./app/data
323
+
324
+ # run linter after generation
325
+ bun on-zero generate --after "bun lint:fix"
326
+ ```
327
+
328
+ **`on-zero generate-queries <dir>`**
329
+
330
+ generates query validators from TypeScript query functions. this is included in
331
+ `generate` but can be run standalone.
332
+
333
+ - parses exported arrow functions from `.ts` files in the queries directory
334
+ - extracts parameter types using TypeScript compiler API
335
+ - generates valibot schemas using typebox-codegen
336
+
337
+ **example:**
338
+
339
+ ```bash
340
+ bun on-zero generate-queries src/data/queries
341
+ ```
342
+
343
+ ### what gets generated
344
+
345
+ **models.ts:**
346
+
347
+ ```ts
348
+ import * as channel from '~/data/models/channel'
349
+ import * as message from '~/data/models/message'
350
+
351
+ export const models = {
352
+ channel,
353
+ message,
354
+ }
355
+ ```
356
+
357
+ **types.ts:**
358
+
359
+ ```ts
360
+ import type { TableInsertRow, TableUpdateRow } from 'on-zero'
361
+ import type * as schema from './tables'
362
+
363
+ export type Channel = TableInsertRow<typeof schema.channel>
364
+ export type ChannelUpdate = TableUpdateRow<typeof schema.channel>
365
+ ```
366
+
367
+ **tables.ts:**
368
+
369
+ ```ts
370
+ export { schema as channel } from '~/data/models/channel'
371
+ export { schema as message } from '~/data/models/message'
372
+ ```
373
+
374
+ **syncedQueries.ts:**
375
+
376
+ ```ts
377
+ import * as v from 'valibot'
378
+ import { syncedQuery } from '@rocicorp/zero'
379
+ import * as messageQueries from '../queries/message'
380
+
381
+ export const latestMessages = syncedQuery(
382
+ 'latestMessages',
383
+ v.parser(
384
+ v.tuple([
385
+ v.object({
386
+ channelId: v.string(),
387
+ limit: v.optional(v.number()),
388
+ }),
389
+ ]),
390
+ ),
391
+ (arg) => {
392
+ return messageQueries.latestMessages(arg)
393
+ },
394
+ )
395
+ ```
396
+
397
+ ### how it works
398
+
399
+ the generator:
400
+
401
+ 1. scans `models/` for files with `export const schema = table(...)`
402
+ 2. scans `queries/` for exported arrow functions
403
+ 3. parses TypeScript AST to extract parameter types
404
+ 4. converts types to valibot schemas using typebox-codegen
405
+ 5. wraps query functions in `syncedQuery()` with validators
406
+ 6. handles special cases (void params, user → userPublic mapping)
407
+ 7. groups query imports by source file
408
+
409
+ queries with no parameters get wrapped in `v.parser(v.tuple([]))` while queries
410
+ with params get validators like `v.parser(v.tuple([v.object({ ... })]))`.
411
+
412
+ exports named `permission` are automatically skipped during query generation.
413
+
414
+ ## setup
415
+
416
+ client:
417
+
418
+ ```tsx
419
+ import { createZeroClient } from 'on-zero'
420
+ import { schema } from '~/data/schema'
421
+ import { models } from '~/data/generated/models'
422
+ import * as groupedQueries from '~/data/generated/groupedQueries'
423
+
424
+ export const { ProvideZero, useQuery, zero, usePermission } = createZeroClient({
425
+ schema,
426
+ models,
427
+ groupedQueries,
428
+ })
429
+
430
+ // in your app root
431
+ <ProvideZero
432
+ server="http://localhost:4848"
433
+ userID={user.id}
434
+ auth={jwtToken}
435
+ authData={{ id: user.id, email: user.email, role: user.role }}
436
+ >
437
+ <App />
438
+ </ProvideZero>
439
+ ```
440
+
441
+ server:
442
+
443
+ ```ts
444
+ import { createZeroServer } from 'on-zero/server'
445
+ import { syncedQueries } from '~/data/generated/syncedQueries'
446
+
447
+ export const zeroServer = createZeroServer({
448
+ schema,
449
+ models,
450
+ database: process.env.DATABASE_URL,
451
+ queries: syncedQueries, // required for synced queries / pull endpoint
452
+ createServerActions: () => ({
453
+ sendEmail: async (to, subject, body) => { ... }
454
+ })
455
+ })
456
+
457
+ // push endpoint for mutations
458
+ app.post('/api/zero/push', async (req) => {
459
+ const authData = await getAuthFromRequest(req)
460
+ const { response } = await zeroServer.handleMutationRequest({
461
+ authData,
462
+ request: req
463
+ })
464
+ return response
465
+ })
466
+
467
+ // pull endpoint for synced queries
468
+ app.post('/api/zero/pull', async (req) => {
469
+ const authData = await getAuthFromRequest(req)
470
+ const { response } = await zeroServer.handleQueryRequest({
471
+ authData,
472
+ request: req
473
+ })
474
+ return response
475
+ })
476
+ ```
477
+
478
+ type augmentation:
479
+
480
+ ```ts
481
+ // src/zero/types.ts
482
+ import type { schema } from '~/data/schema'
483
+ import type { AuthData } from './auth'
484
+
485
+ declare module 'on-zero' {
486
+ interface Config {
487
+ schema: typeof schema
488
+ authData: AuthData
489
+ }
490
+ }
491
+ ```
492
+
493
+ ## mutation context
494
+
495
+ every mutation receives `MutatorContext` as first argument:
496
+
497
+ ```ts
498
+ type MutatorContext = {
499
+ tx: Transaction // database transaction
500
+ authData: AuthData | null // current user
501
+ environment: 'server' | 'client' // where executing
502
+ can: (where, obj) => Promise<void> // permission checker
503
+ server?: {
504
+ actions: ServerActions // async server functions
505
+ asyncTasks: AsyncAction[] // run after transaction
506
+ }
507
+ }
508
+ ```
509
+
510
+ use it:
511
+
512
+ ```ts
513
+ export const mutate = mutations(schema, permissions, {
514
+ async archive(ctx, { messageId }) {
515
+ await ctx.can(permissions, messageId)
516
+ await ctx.tx.mutate.message.update({ id: messageId, archived: true })
517
+
518
+ ctx.server?.asyncTasks.push(async () => {
519
+ await ctx.server.actions.indexForSearch(messageId)
520
+ })
521
+ },
522
+ })
523
+ ```
524
+
525
+ ## patterns
526
+
527
+ **server-only mutations:**
528
+
529
+ ```ts
530
+ await zeroServer.mutate(async (tx, mutators) => {
531
+ await mutators.user.insert(tx, user)
532
+ })
533
+ ```
534
+
535
+ **one-off queries with `run()`:**
536
+
537
+ run a query once without subscribing. works on both client and server:
538
+
539
+ ```ts
540
+ import { run } from 'on-zero'
541
+ import { userById } from '~/data/queries/user'
542
+
543
+ // with params
544
+ const user = await run(userById, { id: userId })
545
+
546
+ // without params
547
+ const allUsers = await run(allUsers)
548
+
549
+ // with options (client only)
550
+ const cached = await run(userById, { id: userId }, { type: 'unknown' })
551
+ ```
552
+
553
+ on-zero run is smart:
554
+
555
+ - on client, uses client `zero.run()`
556
+ - on server, uses server `zero.run()`
557
+ - in a mutation, uses `tx.run()`
558
+
559
+ this gives you a nice api.
560
+
561
+ **preloading data (client only):**
562
+
563
+ preload query results into cache without subscribing:
564
+
565
+ ```ts
566
+ import { preload } from '~/zero/client'
567
+ import { userNotifications } from '~/data/queries/notification'
568
+
569
+ // preload after login
570
+ const { complete, cleanup } = preload(userNotifications, { userId, limit: 100 })
571
+ await complete
572
+
573
+ // cleanup if needed
574
+ cleanup()
575
+ ```
576
+
577
+ useful for prefetching data before navigation to avoid loading states.
578
+
579
+ **server-only queries:**
580
+
581
+ for ad-hoc queries that don't use query functions:
582
+
583
+ ```ts
584
+ const user = await zeroServer.query((q) => q.user.where('id', userId).one())
585
+ ```
586
+
587
+ **batch processing:**
588
+
589
+ ```ts
590
+ import { batchQuery } from 'on-zero'
591
+
592
+ await batchQuery(
593
+ zql.message.where('processed', false),
594
+ async (messages) => {
595
+ for (const msg of messages) {
596
+ await processMessage(msg)
597
+ }
598
+ },
599
+ { chunk: 100, pause: 50 },
600
+ )
601
+ ```
@@ -16,9 +16,9 @@ import { createUseQuery } from './createUseQuery'
16
16
  import { createMutators } from './helpers/createMutators'
17
17
  import { getQueryOrMutatorAuthData } from './helpers/getQueryOrMutatorAuthData'
18
18
  import { getAllMutationsPermissions, getMutationsPermissions } from './modelRegistry'
19
- import { setCustomQueries } from './query'
20
19
  import { registerQuery } from './queryRegistry'
21
20
  import { resolveQuery, type PlainQueryFn } from './resolveQuery'
21
+ import { setCustomQueries } from './run'
22
22
  import { setAuthData, setSchema } from './state'
23
23
  import { getRawWhere, setEvaluatingPermission } from './where'
24
24
  import { setRunner } from './zeroRunner'
@@ -9,7 +9,7 @@ import { createPermissions } from './createPermissions'
9
9
  import { createMutators } from './helpers/createMutators'
10
10
  import { isInZeroMutation, mutatorContext } from './helpers/mutatorContext'
11
11
  import { getMutationsPermissions } from './modelRegistry'
12
- import { setCustomQueries } from './query'
12
+ import { setCustomQueries } from './run'
13
13
  import { getZQL, setAuthData, setSchema } from './state'
14
14
  import { setRunner } from './zeroRunner'
15
15
 
package/src/index.ts CHANGED
@@ -8,7 +8,7 @@ export * from './helpers/mutatorContext'
8
8
  export * from './createZeroClient'
9
9
  export * from './createUseQuery'
10
10
  export * from './resolveQuery'
11
- export { query } from './query'
11
+ export * from './run'
12
12
  export { setRunner, type ZeroRunner } from './zeroRunner'
13
13
  export * from './mutations'
14
14
  export * from './where'
@@ -1,4 +1,4 @@
1
- import { Where } from './types'
1
+ import type { Where } from './types'
2
2
 
3
3
  const mutationsToPermissionsRegistry = new Map<string, Where>()
4
4
 
@@ -24,8 +24,17 @@ function getCustomQueries(): AnyQueryRegistry {
24
24
  }
25
25
 
26
26
  // execute a query once (non-reactive counterpart to useQuery)
27
- // defaults to 'complete' (fetches from server), pass 'cached' for local-only
28
- export function query<
27
+ // defaults to 'unknown', pass 'complete' to have client fetch from server
28
+ export function run<
29
+ Schema extends ZeroSchema,
30
+ TTable extends keyof Schema['tables'] & string,
31
+ TReturn,
32
+ >(
33
+ query: Query<TTable, Schema, TReturn>,
34
+ mode?: 'complete'
35
+ ): Promise<HumanReadable<TReturn>>
36
+
37
+ export function run<
29
38
  Schema extends ZeroSchema,
30
39
  TArg,
31
40
  TTable extends keyof Schema['tables'] & string,
@@ -33,32 +42,43 @@ export function query<
33
42
  >(
34
43
  fn: PlainQueryFn<TArg, Query<TTable, Schema, TReturn>>,
35
44
  params: TArg,
36
- mode?: 'cached'
45
+ mode?: 'complete'
37
46
  ): Promise<HumanReadable<TReturn>>
38
47
 
39
- export function query<
48
+ export function run<
40
49
  Schema extends ZeroSchema,
41
50
  TTable extends keyof Schema['tables'] & string,
42
51
  TReturn,
43
52
  >(
44
53
  fn: PlainQueryFn<void, Query<TTable, Schema, TReturn>>,
45
- mode?: 'cached'
54
+ mode?: 'complete'
46
55
  ): Promise<HumanReadable<TReturn>>
47
56
 
48
- export function query(fnArg: any, paramsOrMode?: any, modeArg?: 'cached'): Promise<any> {
49
- const hasParams = modeArg !== undefined || (paramsOrMode && paramsOrMode !== 'cached')
57
+ export function run(
58
+ queryOrFn: any,
59
+ paramsOrMode?: any,
60
+ modeArg?: 'complete'
61
+ ): Promise<any> {
62
+ const hasParams = modeArg !== undefined || (paramsOrMode && paramsOrMode !== 'complete')
50
63
  const params = hasParams ? paramsOrMode : undefined
51
64
  const mode = hasParams ? modeArg : paramsOrMode
52
-
53
- const customQueries = getCustomQueries()
54
- const queryRequest = resolveQuery({ customQueries, fn: fnArg, params })
55
65
  const runner = getRunner()
66
+ const options =
67
+ mode === 'complete'
68
+ ? ({
69
+ type: 'complete',
70
+ } as const)
71
+ : undefined
56
72
 
57
- const out = runner(queryRequest as any, {
58
- type: mode === 'cached' ? 'unknown' : 'complete',
59
- })
73
+ if (queryOrFn && queryOrFn['ast']) {
74
+ // inline zql - on client it only resolves against cache, on server fully
75
+ return runner(queryOrFn, options)
76
+ }
77
+
78
+ const customQueries = getCustomQueries()
79
+ const queryRequest = resolveQuery({ customQueries, fn: queryOrFn, params })
60
80
 
61
- console.log('wtf', runner, out, queryRequest, customQueries, fnArg)
81
+ const out = runner(queryRequest as any, options)
62
82
 
63
83
  return out
64
84
  }