on-zero 0.6.14 → 0.6.16

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 (163) hide show
  1. package/dist/cjs/asyncTasks.cjs +51 -0
  2. package/dist/cjs/asyncTasks.native.js +84 -0
  3. package/dist/cjs/asyncTasks.native.js.map +1 -0
  4. package/dist/cjs/cli.cjs +8 -1
  5. package/dist/cjs/cli.native.js +8 -1
  6. package/dist/cjs/cli.native.js.map +1 -1
  7. package/dist/cjs/createSchemaFromDrizzle.cjs +4 -4
  8. package/dist/cjs/createSchemaFromDrizzle.native.js +4 -4
  9. package/dist/cjs/createSchemaFromDrizzle.native.js.map +1 -1
  10. package/dist/cjs/createZeroDirectServer.cjs +269 -0
  11. package/dist/cjs/createZeroDirectServer.native.js +287 -0
  12. package/dist/cjs/createZeroDirectServer.native.js.map +1 -0
  13. package/dist/cjs/createZeroSQLiteServer.cjs +8 -277
  14. package/dist/cjs/createZeroSQLiteServer.native.js +8 -390
  15. package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -1
  16. package/dist/cjs/createZeroSQLiteServer.test.cjs +36 -9
  17. package/dist/cjs/createZeroSQLiteServer.test.native.js +37 -9
  18. package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -1
  19. package/dist/cjs/createZeroServer.test.cjs +79 -1
  20. package/dist/cjs/createZeroServer.test.native.js +85 -1
  21. package/dist/cjs/createZeroServer.test.native.js.map +1 -1
  22. package/dist/cjs/createZeroServerCore.cjs +28 -208
  23. package/dist/cjs/createZeroServerCore.native.js +33 -218
  24. package/dist/cjs/createZeroServerCore.native.js.map +1 -1
  25. package/dist/cjs/generate.cjs +15 -5
  26. package/dist/cjs/generate.native.js +15 -5
  27. package/dist/cjs/generate.native.js.map +1 -1
  28. package/dist/cjs/generate.test.cjs +55 -0
  29. package/dist/cjs/generate.test.native.js +55 -0
  30. package/dist/cjs/generate.test.native.js.map +1 -1
  31. package/dist/cjs/helpers/createMutators.cjs +2 -2
  32. package/dist/cjs/helpers/createMutators.native.js +2 -2
  33. package/dist/cjs/helpers/createMutators.native.js.map +1 -1
  34. package/dist/cjs/index.cjs +1 -0
  35. package/dist/cjs/index.native.js +1 -0
  36. package/dist/cjs/index.native.js.map +1 -1
  37. package/dist/cjs/server.cjs +2 -1
  38. package/dist/cjs/sqliteRuntime.cjs +318 -0
  39. package/dist/cjs/sqliteRuntime.native.js +438 -0
  40. package/dist/cjs/sqliteRuntime.native.js.map +1 -0
  41. package/dist/cjs/syncHost.cjs +26 -12
  42. package/dist/cjs/syncHost.native.js +32 -16
  43. package/dist/cjs/syncHost.native.js.map +1 -1
  44. package/dist/cjs/syncHost.test.cjs +15 -4
  45. package/dist/cjs/syncHost.test.native.js +16 -4
  46. package/dist/cjs/syncHost.test.native.js.map +1 -1
  47. package/dist/cjs/vite-plugin.cjs +2 -1
  48. package/dist/cjs/vite-plugin.native.js +2 -1
  49. package/dist/cjs/vite-plugin.native.js.map +1 -1
  50. package/dist/esm/asyncTasks.mjs +26 -0
  51. package/dist/esm/asyncTasks.mjs.map +1 -0
  52. package/dist/esm/asyncTasks.native.js +56 -0
  53. package/dist/esm/asyncTasks.native.js.map +1 -0
  54. package/dist/esm/cli.mjs +8 -1
  55. package/dist/esm/cli.mjs.map +1 -1
  56. package/dist/esm/cli.native.js +8 -1
  57. package/dist/esm/cli.native.js.map +1 -1
  58. package/dist/esm/createSchemaFromDrizzle.mjs +2 -2
  59. package/dist/esm/createSchemaFromDrizzle.native.js +2 -2
  60. package/dist/esm/createZeroDirectServer.mjs +241 -0
  61. package/dist/esm/createZeroDirectServer.mjs.map +1 -0
  62. package/dist/esm/createZeroDirectServer.native.js +256 -0
  63. package/dist/esm/createZeroDirectServer.native.js.map +1 -0
  64. package/dist/esm/createZeroSQLiteServer.mjs +3 -272
  65. package/dist/esm/createZeroSQLiteServer.mjs.map +1 -1
  66. package/dist/esm/createZeroSQLiteServer.native.js +3 -385
  67. package/dist/esm/createZeroSQLiteServer.native.js.map +1 -1
  68. package/dist/esm/createZeroSQLiteServer.test.mjs +36 -9
  69. package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -1
  70. package/dist/esm/createZeroSQLiteServer.test.native.js +37 -9
  71. package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -1
  72. package/dist/esm/createZeroServer.test.mjs +80 -2
  73. package/dist/esm/createZeroServer.test.mjs.map +1 -1
  74. package/dist/esm/createZeroServer.test.native.js +86 -2
  75. package/dist/esm/createZeroServer.test.native.js.map +1 -1
  76. package/dist/esm/createZeroServerCore.mjs +26 -205
  77. package/dist/esm/createZeroServerCore.mjs.map +1 -1
  78. package/dist/esm/createZeroServerCore.native.js +32 -216
  79. package/dist/esm/createZeroServerCore.native.js.map +1 -1
  80. package/dist/esm/generate.mjs +15 -5
  81. package/dist/esm/generate.mjs.map +1 -1
  82. package/dist/esm/generate.native.js +15 -5
  83. package/dist/esm/generate.native.js.map +1 -1
  84. package/dist/esm/generate.test.mjs +55 -1
  85. package/dist/esm/generate.test.mjs.map +1 -1
  86. package/dist/esm/generate.test.native.js +55 -1
  87. package/dist/esm/generate.test.native.js.map +1 -1
  88. package/dist/esm/helpers/createMutators.mjs +2 -2
  89. package/dist/esm/helpers/createMutators.mjs.map +1 -1
  90. package/dist/esm/helpers/createMutators.native.js +2 -2
  91. package/dist/esm/helpers/createMutators.native.js.map +1 -1
  92. package/dist/esm/index.js +1 -0
  93. package/dist/esm/index.js.map +1 -1
  94. package/dist/esm/index.mjs +1 -0
  95. package/dist/esm/index.mjs.map +1 -1
  96. package/dist/esm/index.native.js +1 -0
  97. package/dist/esm/index.native.js.map +1 -1
  98. package/dist/esm/server.mjs +1 -0
  99. package/dist/esm/server.mjs.map +1 -1
  100. package/dist/esm/sqliteRuntime.mjs +289 -0
  101. package/dist/esm/sqliteRuntime.mjs.map +1 -0
  102. package/dist/esm/sqliteRuntime.native.js +406 -0
  103. package/dist/esm/sqliteRuntime.native.js.map +1 -0
  104. package/dist/esm/syncHost.mjs +24 -10
  105. package/dist/esm/syncHost.mjs.map +1 -1
  106. package/dist/esm/syncHost.native.js +30 -14
  107. package/dist/esm/syncHost.native.js.map +1 -1
  108. package/dist/esm/syncHost.test.mjs +15 -4
  109. package/dist/esm/syncHost.test.mjs.map +1 -1
  110. package/dist/esm/syncHost.test.native.js +16 -4
  111. package/dist/esm/syncHost.test.native.js.map +1 -1
  112. package/dist/esm/vite-plugin.mjs +2 -1
  113. package/dist/esm/vite-plugin.mjs.map +1 -1
  114. package/dist/esm/vite-plugin.native.js +2 -1
  115. package/dist/esm/vite-plugin.native.js.map +1 -1
  116. package/lint/ssr-guards.js +8 -7
  117. package/package.json +5 -6
  118. package/readme.md +13 -4
  119. package/src/asyncTasks.ts +33 -0
  120. package/src/cli.ts +7 -1
  121. package/src/createSchemaFromDrizzle.ts +2 -2
  122. package/src/createZeroDirectServer.ts +411 -0
  123. package/src/createZeroSQLiteServer.test.ts +28 -12
  124. package/src/createZeroSQLiteServer.ts +14 -481
  125. package/src/createZeroServer.test.ts +86 -2
  126. package/src/createZeroServerCore.ts +39 -343
  127. package/src/generate.test.ts +75 -1
  128. package/src/generate.ts +30 -4
  129. package/src/helpers/createMutators.ts +3 -3
  130. package/src/helpers/mutatorContext.ts +1 -1
  131. package/src/index.ts +1 -0
  132. package/src/server.ts +2 -0
  133. package/src/sqliteRuntime.ts +512 -0
  134. package/src/syncHost.test.ts +12 -5
  135. package/src/syncHost.ts +55 -16
  136. package/src/types.ts +21 -3
  137. package/src/vite-plugin.ts +1 -0
  138. package/test-fixtures/server-return-types.ts +20 -2
  139. package/types/asyncTasks.d.ts +7 -0
  140. package/types/asyncTasks.d.ts.map +1 -0
  141. package/types/createSchemaFromDrizzle.d.ts +2 -2
  142. package/types/createSchemaFromDrizzle.d.ts.map +1 -1
  143. package/types/createZeroDirectServer.d.ts +80 -0
  144. package/types/createZeroDirectServer.d.ts.map +1 -0
  145. package/types/createZeroSQLiteServer.d.ts +5 -30
  146. package/types/createZeroSQLiteServer.d.ts.map +1 -1
  147. package/types/createZeroServerCore.d.ts +30 -87
  148. package/types/createZeroServerCore.d.ts.map +1 -1
  149. package/types/generate.d.ts +2 -0
  150. package/types/generate.d.ts.map +1 -1
  151. package/types/helpers/createMutators.d.ts +3 -3
  152. package/types/helpers/createMutators.d.ts.map +1 -1
  153. package/types/index.d.ts +1 -0
  154. package/types/index.d.ts.map +1 -1
  155. package/types/server.d.ts +2 -0
  156. package/types/server.d.ts.map +1 -1
  157. package/types/sqliteRuntime.d.ts +71 -0
  158. package/types/sqliteRuntime.d.ts.map +1 -0
  159. package/types/syncHost.d.ts +10 -19
  160. package/types/syncHost.d.ts.map +1 -1
  161. package/types/types.d.ts +18 -2
  162. package/types/types.d.ts.map +1 -1
  163. package/types/vite-plugin.d.ts.map +1 -1
@@ -1,47 +1,23 @@
1
- import { mustGetQuery } from '@rocicorp/zero'
2
1
  import {
3
2
  PushProcessor,
4
3
  handleQueryRequest as zeroHandleQueryRequest,
5
4
  } from '@rocicorp/zero/server'
6
5
 
7
- import { createPermissions } from './createPermissions'
8
- import { createMutators } from './helpers/createMutators'
6
+ import { runAsyncTaskEntries } from './asyncTasks'
9
7
  import {
10
- getScopedAuthData,
11
- isInZeroMutation,
12
- mutatorContext,
13
- runWithAuthScope,
14
- } from './helpers/mutatorContext'
8
+ createAsyncTaskCollector,
9
+ createZeroDirectServerRuntime,
10
+ resolveServerQuery,
11
+ type TransactionAttemptLifecycle,
12
+ type ZeroServerCommonOptions,
13
+ type ZeroServerDatabase,
14
+ } from './createZeroDirectServer'
15
+ import { createMutators } from './helpers/createMutators'
16
+ import { runWithAuthScope } from './helpers/mutatorContext'
15
17
  import { runWithQueryContext } from './helpers/queryContext'
16
- import { getMutationsPermissions } from './modelRegistry'
17
- import { setCustomQueries } from './run'
18
- import { getZQL, setEnvironment, setSchema } from './state'
19
- import { setEvaluatingPermission } from './where'
20
- import { setRunner } from './zeroRunner'
21
18
 
22
- import type {
23
- AdminRoleMode,
24
- AsyncAction,
25
- AuthData,
26
- GenericModels,
27
- MutatorContext,
28
- QueryBuilder,
29
- Transaction,
30
- } from './types'
31
- import type {
32
- AnyQueryRegistry,
33
- HumanReadable,
34
- Query,
35
- Schema as ZeroSchema,
36
- } from '@rocicorp/zero'
37
- import type { Database, TransactionProviderInput } from '@rocicorp/zero/server'
38
-
39
- type MutateAuthData = Pick<AuthData, 'email' | 'id'> & Partial<AuthData>
40
-
41
- type MutateOptions = {
42
- authData?: MutateAuthData
43
- awaitEffects?: boolean
44
- }
19
+ import type { AuthData, GenericModels } from './types'
20
+ import type { Schema as ZeroSchema } from '@rocicorp/zero'
45
21
 
46
22
  type AlreadyProcessedMutation = {
47
23
  id: {
@@ -58,122 +34,12 @@ type PushRequestBody = {
58
34
  clientGroupID?: unknown
59
35
  }
60
36
 
61
- type ServerMutate<Models extends GenericModels> = {
62
- [Key in keyof Models]: {
63
- [K in keyof Models[Key]['mutate']]: Models[Key]['mutate'][K] extends (
64
- ctx: MutatorContext,
65
- arg: infer Arg,
66
- ) => any
67
- ? (arg: Arg, options?: MutateOptions) => Promise<void>
68
- : (options?: MutateOptions) => Promise<void>
69
- }
70
- }
71
-
72
- export type ValidateQueryArgs = {
73
- authData: AuthData | null
74
- queryName: string
75
- params: unknown
76
- }
77
-
78
- export type ValidateMutationArgs = {
79
- authData: AuthData | null
80
- mutatorName: string
81
- tableName: string
82
- args: unknown
83
- }
84
-
85
- export type ValidateQueryFn = (args: ValidateQueryArgs) => void
86
- export type ValidateMutationFn = (args: ValidateMutationArgs) => void | Promise<void>
87
-
88
- export type ServerQueryResolverOptions = {
89
- authData: AuthData | null
90
- name: string
91
- args: unknown
92
- queries: AnyQueryRegistry
93
- permissions: ReturnType<typeof createPermissions>
94
- validateQuery?: ValidateQueryFn
95
- }
96
-
97
- export function resolveServerQuery({
98
- authData,
99
- name,
100
- args,
101
- queries,
102
- permissions,
103
- validateQuery,
104
- }: ServerQueryResolverOptions) {
105
- if (name.startsWith('permission.')) {
106
- const table = name.slice('permission.'.length)
107
- const { objOrId } = args as {
108
- objOrId: string | Record<string, any>
109
- }
110
- const permission = getMutationsPermissions(table)
111
- if (!permission) {
112
- throw new Error(`[permission] no permission defined for table: ${table}`)
113
- }
114
- setEvaluatingPermission(true)
115
- try {
116
- return (getZQL() as any)[table]
117
- .where((eb: any) => {
118
- return permissions.buildPermissionQuery(
119
- authData,
120
- eb,
121
- permission,
122
- objOrId,
123
- table,
124
- )
125
- })
126
- .one()
127
- } finally {
128
- setEvaluatingPermission(false)
129
- }
130
- }
131
-
132
- if (validateQuery) {
133
- validateQuery({ authData, queryName: name, params: args })
134
- }
135
-
136
- const query = (mustGetQuery as any)(queries, name)
137
- return query.fn({ args, ctx: authData })
138
- }
139
-
140
- export type TransactionAttemptLifecycle = {
141
- beginAttempt(): void
142
- completeAttempt(): void
143
- rollbackAttempt(): void
144
- commitTransaction(): void
145
- rollbackTransaction(): void
146
- }
147
-
148
- export type ZeroServerDatabase = Database<Transaction>
149
-
150
37
  export type PersistAlreadyProcessedArgs = {
151
38
  clientGroupID: string
152
39
  mutations: AlreadyProcessedMutation[]
153
40
  upstreamSchema: string
154
41
  }
155
42
 
156
- export type ZeroServerCommonOptions<
157
- Schema extends ZeroSchema,
158
- Models extends GenericModels,
159
- ServerActions extends Record<string, unknown>,
160
- > = {
161
- schema: Schema
162
- models: Models
163
- createServerActions: () => ServerActions
164
- queries?: AnyQueryRegistry
165
- /** generated validators keyed by model and mutation name */
166
- mutations?: Record<string, Record<string, any>>
167
- /** synchronous query authorization hook; throw to reject */
168
- validateQuery?: ValidateQueryFn
169
- /** mutation authorization hook; throw to reject */
170
- validateMutation?: ValidateMutationFn
171
- /** controls which operations the admin role bypasses */
172
- defaultAllowAdminRole?: AdminRoleMode
173
- /** auth data used by direct server mutations when no scope provides it */
174
- defaultMutateAuthData?: MutateAuthData
175
- }
176
-
177
43
  type ZeroServerCoreOptions<
178
44
  Schema extends ZeroSchema,
179
45
  Models extends GenericModels,
@@ -183,60 +49,6 @@ type ZeroServerCoreOptions<
183
49
  persistAlreadyProcessed?: (args: PersistAlreadyProcessedArgs) => Promise<void>
184
50
  }
185
51
 
186
- function createAsyncTaskCollector() {
187
- const committed: AsyncAction[] = []
188
- let attempt: AsyncAction[] | undefined
189
- let completedAttempt: AsyncAction[] | undefined
190
- const current = () => attempt ?? committed
191
- const queue = new Proxy([] as AsyncAction[], {
192
- deleteProperty(_target, property) {
193
- return Reflect.deleteProperty(current(), property)
194
- },
195
- get(_target, property) {
196
- const target = current()
197
- const value = Reflect.get(target, property, target)
198
- return typeof value === 'function' ? value.bind(target) : value
199
- },
200
- getOwnPropertyDescriptor(_target, property) {
201
- return Reflect.getOwnPropertyDescriptor(current(), property)
202
- },
203
- has(_target, property) {
204
- return Reflect.has(current(), property)
205
- },
206
- ownKeys() {
207
- return Reflect.ownKeys(current())
208
- },
209
- set(_target, property, value) {
210
- return Reflect.set(current(), property, value)
211
- },
212
- })
213
-
214
- const lifecycle: TransactionAttemptLifecycle = {
215
- beginAttempt() {
216
- attempt = []
217
- completedAttempt = undefined
218
- },
219
- completeAttempt() {
220
- completedAttempt = attempt
221
- attempt = undefined
222
- },
223
- rollbackAttempt() {
224
- attempt = undefined
225
- completedAttempt = undefined
226
- },
227
- commitTransaction() {
228
- if (completedAttempt) committed.push(...completedAttempt)
229
- completedAttempt = undefined
230
- },
231
- rollbackTransaction() {
232
- attempt = undefined
233
- completedAttempt = undefined
234
- },
235
- }
236
-
237
- return { committed, lifecycle, queue }
238
- }
239
-
240
52
  export function createZeroServerCore<
241
53
  Schema extends ZeroSchema,
242
54
  Models extends GenericModels,
@@ -245,7 +57,7 @@ export function createZeroServerCore<
245
57
  createDatabase,
246
58
  createServerActions,
247
59
  defaultAllowAdminRole = 'all',
248
- defaultMutateAuthData = {} as MutateAuthData,
60
+ defaultMutateAuthData,
249
61
  models,
250
62
  mutations: mutationValidators,
251
63
  persistAlreadyProcessed,
@@ -254,13 +66,17 @@ export function createZeroServerCore<
254
66
  validateMutation,
255
67
  validateQuery,
256
68
  }: ZeroServerCoreOptions<Schema, Models, ServerActions>) {
257
- setSchema(schema)
258
- setEnvironment('server')
259
-
260
- const permissions = createPermissions<Schema>({
261
- environment: 'server',
69
+ const { permissions, server } = createZeroDirectServerRuntime({
70
+ createDatabase,
71
+ createServerActions,
72
+ defaultAllowAdminRole,
73
+ defaultMutateAuthData,
74
+ models,
75
+ mutations: mutationValidators,
76
+ queries,
262
77
  schema,
263
- adminRoleMode: defaultAllowAdminRole,
78
+ validateMutation,
79
+ validateQuery,
264
80
  })
265
81
 
266
82
  const handleMutationRequest = async ({
@@ -275,7 +91,7 @@ export function createZeroServerCore<
275
91
  const tasks = createAsyncTaskCollector()
276
92
  const recoveryRequest = request.clone()
277
93
  const mutators = createMutators({
278
- asyncTasks: tasks.queue,
94
+ enqueueTask: tasks.enqueueTask,
279
95
  can: permissions.can,
280
96
  createServerActions,
281
97
  environment: 'server',
@@ -295,11 +111,11 @@ export function createZeroServerCore<
295
111
  }
296
112
 
297
113
  if (!skipAsyncTasks && tasks.committed.length) {
298
- Promise.all(tasks.committed.map((task) => runWithAuthScope(authData, task))).catch(
299
- (err: unknown) => {
300
- console.error('[push] async tasks failed', err)
301
- },
302
- )
114
+ const entries = tasks.committed.map((entry) => ({
115
+ ...entry,
116
+ task: () => runWithAuthScope(authData, entry.task),
117
+ }))
118
+ await runAsyncTaskEntries(entries, { background: () => undefined })
303
119
  }
304
120
 
305
121
  return {
@@ -342,135 +158,13 @@ export function createZeroServerCore<
342
158
  : zeroHandleQueryRequest({ handler, schema, request, userID }),
343
159
  )
344
160
 
345
- return {
346
- response,
347
- }
348
- }
349
-
350
- async function runTransaction<
351
- CB extends (tx: Transaction) => Promise<any>,
352
- Returns extends CB extends (tx: Transaction) => Promise<infer X> ? X : never,
353
- >(database: ZeroServerDatabase, query: CB): Promise<Returns> {
354
- try {
355
- if (isInZeroMutation()) {
356
- const { tx } = mutatorContext()
357
- return await query(tx)
358
- }
359
- return (await database.transaction(query, dummyTransactionInput)) as Returns
360
- } catch (err) {
361
- if ((err as any)?.name !== 'PermissionError') {
362
- console.error(`Error running transaction(): ${err}`)
363
- }
364
- throw err
365
- }
366
- }
367
-
368
- async function transaction<
369
- CB extends (tx: Transaction) => Promise<any>,
370
- Returns extends CB extends (tx: Transaction) => Promise<infer X> ? X : never,
371
- >(query: CB): Promise<Returns> {
372
- return runTransaction(createDatabase(), query)
373
- }
374
-
375
- async function runMutate(
376
- modelName: string,
377
- mutatorName: string,
378
- mutatorArg: unknown,
379
- options?: MutateOptions,
380
- ) {
381
- let authData = options?.authData
382
- if (!authData) {
383
- const scoped = getScopedAuthData()
384
- if (scoped) authData = scoped as MutateAuthData
385
- }
386
-
387
- const tasks = createAsyncTaskCollector()
388
- const mutators = createMutators({
389
- models,
390
- environment: 'server',
391
- asyncTasks: tasks.queue,
392
- authData: {
393
- ...defaultMutateAuthData,
394
- ...authData,
395
- },
396
- createServerActions,
397
- can: permissions.can,
398
- validateMutation,
399
- mutationValidators,
400
- })
401
- const modelMutators = mutators[modelName as keyof typeof mutators] as Record<
402
- string,
403
- (tx: Transaction, arg?: unknown) => Promise<void>
404
- >
405
- const mutator = modelMutators[mutatorName]
406
-
407
- await runTransaction(
408
- withTransactionLifecycle(createDatabase(), tasks.lifecycle),
409
- async (tx) => {
410
- await mutator(tx, mutatorArg)
411
- },
412
- )
413
-
414
- if (tasks.committed.length) {
415
- const resolvedAuth = authData ?? null
416
- const promise = Promise.all(
417
- tasks.committed.map((task) => runWithAuthScope(resolvedAuth, task)),
418
- )
419
- if (options?.awaitEffects) {
420
- await promise
421
- } else {
422
- promise.catch((err: unknown) => {
423
- console.error('[mutate] async tasks failed', err)
424
- })
425
- }
426
- }
427
- }
428
-
429
- const mutate = new Proxy({} as ServerMutate<Models>, {
430
- get(_, modelName: string) {
431
- return new Proxy(
432
- {},
433
- {
434
- get(_, mutatorName: string) {
435
- return (arg: unknown, options?: MutateOptions) =>
436
- runMutate(modelName, mutatorName, arg, options)
437
- },
438
- },
439
- )
440
- },
441
- })
442
-
443
- function query<R>(
444
- cb: (q: QueryBuilder) => Query<any, Schema, R>,
445
- authData?: AuthData | null,
446
- ): Promise<HumanReadable<R>> {
447
- const run = () =>
448
- transaction(async (tx) => {
449
- return tx.run(cb(getZQL()))
450
- }) as any
451
-
452
- if (authData !== undefined) {
453
- return runWithQueryContext({ authData }, run)
454
- }
455
- return run()
161
+ return { response }
456
162
  }
457
163
 
458
- if (queries) {
459
- setCustomQueries(queries)
460
- }
461
-
462
- setRunner((queryObj) => {
463
- return transaction(async (tx) => {
464
- return tx.run(queryObj)
465
- })
466
- })
467
-
468
164
  return {
469
165
  handleMutationRequest,
470
166
  handleQueryRequest,
471
- transaction,
472
- mutate,
473
- query,
167
+ ...server,
474
168
  }
475
169
  }
476
170
 
@@ -549,9 +243,11 @@ function alreadyProcessedMutations(response: unknown): AlreadyProcessedMutation[
549
243
  })
550
244
  }
551
245
 
552
- const dummyTransactionInput: TransactionProviderInput = {
553
- clientGroupID: 'unused',
554
- clientID: 'unused',
555
- mutationID: 42,
556
- upstreamSchema: 'unused',
557
- }
246
+ export type {
247
+ ValidateMutationArgs,
248
+ ValidateMutationFn,
249
+ ValidateQueryArgs,
250
+ ValidateQueryFn,
251
+ ZeroServerCommonOptions,
252
+ ZeroServerDatabase,
253
+ } from './createZeroDirectServer'
@@ -1,4 +1,11 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ symlinkSync,
7
+ writeFileSync,
8
+ } from 'node:fs'
2
9
  import { tmpdir } from 'node:os'
3
10
  import { join } from 'node:path'
4
11
  import { runInNewContext } from 'node:vm'
@@ -211,6 +218,24 @@ export const allPosts = () => zero.query.post
211
218
  const second = await generate({ dir: testDir, silent: true })
212
219
  expect(second.filesChanged).toBe(0)
213
220
  })
221
+
222
+ test('force regenerates without source changes', async () => {
223
+ writeFileSync(
224
+ join(testDir, 'models/post.ts'),
225
+ `export const schema = table('post', { id: string() })`,
226
+ )
227
+ writeFileSync(
228
+ join(testDir, 'queries/post.ts'),
229
+ `export const allPosts = () => zero.query.post`,
230
+ )
231
+ await generate({ dir: testDir, silent: true })
232
+ const syncedQueriesPath = join(testDir, 'generated/syncedQueries.ts')
233
+ rmSync(syncedQueriesPath)
234
+
235
+ await generate({ dir: testDir, silent: true, force: true })
236
+
237
+ expect(existsSync(syncedQueriesPath)).toBe(true)
238
+ })
214
239
  })
215
240
 
216
241
  describe('mutations', () => {
@@ -617,6 +642,55 @@ export const mutate = mutations(schema, perm, {
617
642
  expect(content).not.toMatch(/rename:[\s\S]*count/)
618
643
  })
619
644
 
645
+ test('resolves instantiated Zero row fields through Partial and Omit', async () => {
646
+ symlinkSync(
647
+ join(import.meta.dirname, '../../../node_modules'),
648
+ join(testDir, 'node_modules'),
649
+ 'dir',
650
+ )
651
+ writeFileSync(
652
+ join(testDir, 'models/types.ts'),
653
+ `
654
+ import type { Row } from '@rocicorp/zero'
655
+ import { drizzleZeroConfig } from 'drizzle-zero-sqlite'
656
+ import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
657
+
658
+ const item = sqliteTable('item', {
659
+ id: text().primaryKey(),
660
+ name: text().notNull(),
661
+ description: text().notNull(),
662
+ })
663
+ const schema = drizzleZeroConfig({ item })
664
+
665
+ export type Item = Row<(typeof schema)['tables']['item']>
666
+ `,
667
+ )
668
+
669
+ writeFileSync(
670
+ join(testDir, 'models/item.ts'),
671
+ `
672
+ import { mutations } from 'on-zero'
673
+ import type { Item } from './types'
674
+
675
+ type ItemUpdate = { id: string } & Partial<Omit<Item, 'id'>>
676
+
677
+ export const mutate = mutations('item', {
678
+ update: async ({ tx }, props: ItemUpdate) => {
679
+ await tx.mutate.item.update(props)
680
+ },
681
+ })
682
+ `,
683
+ )
684
+
685
+ await generate({ dir: testDir, silent: true })
686
+
687
+ const content = readFileSync(join(testDir, 'generated/syncedMutations.ts'), 'utf-8')
688
+ expect(content).toContain('name: v.optional(v.string())')
689
+ expect(content).toContain('description: v.optional(v.string())')
690
+ expect(content).not.toContain('name: v.optional(v.unknown())')
691
+ expect(content).not.toContain('description: v.optional(v.unknown())')
692
+ })
693
+
620
694
  test('skips symbol-keyed properties when resolving imported mutation param types', async () => {
621
695
  writeFileSync(
622
696
  join(testDir, 'models/types.ts'),
package/src/generate.ts CHANGED
@@ -17,6 +17,7 @@ import {
17
17
  import type { ExtractedMutation, ModelMutations, SchemaColumn } from './generate-helpers'
18
18
 
19
19
  const hash = (s: string) => createHash('sha256').update(s).digest('hex')
20
+ const GENERATOR_CACHE_VERSION = '1'
20
21
 
21
22
  const isGeneratorSourceFile = (name: string) =>
22
23
  name.endsWith('.ts') &&
@@ -442,6 +443,19 @@ function extractSchemaColumns(
442
443
  visit(sourceFile)
443
444
  }
444
445
 
446
+ function getInstantiatedPropertyType(
447
+ checker: import('typescript').TypeChecker,
448
+ parent: import('typescript').Type,
449
+ name: string,
450
+ ) {
451
+ const resolveProperty: unknown = Reflect.get(checker, 'getTypeOfPropertyOfType')
452
+ if (typeof resolveProperty !== 'function') return undefined
453
+ // typescript exposes this runtime method but omits it from TypeChecker.
454
+ return Reflect.apply(resolveProperty, checker, [parent, name]) as
455
+ | import('typescript').Type
456
+ | undefined
457
+ }
458
+
445
459
  // convert a ts.Type to valibot code by walking the type checker AST
446
460
  function tsTypeToValibot(
447
461
  ts: typeof import('typescript'),
@@ -528,7 +542,14 @@ function tsTypeToValibot(
528
542
  }
529
543
 
530
544
  // resolve symbol property type with fallbacks
531
- const resolveSymbolType = (prop: import('typescript').Symbol) => {
545
+ const resolveSymbolType = (
546
+ parent: import('typescript').Type,
547
+ prop: import('typescript').Symbol,
548
+ ) => {
549
+ // mapped and conditional types can expose transient symbols with no
550
+ // declaration, so resolve each property against its concrete parent.
551
+ const instantiated = getInstantiatedPropertyType(checker, parent, prop.getName())
552
+ if (instantiated) return instantiated
532
553
  if (prop.valueDeclaration)
533
554
  return checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration)
534
555
  if ((prop as any).declarations?.[0])
@@ -544,7 +565,7 @@ function tsTypeToValibot(
544
565
  for (const prop of props) {
545
566
  const name = prop.getName()
546
567
  if (shouldSkipObjectKey(name)) continue
547
- const propType = resolveSymbolType(prop)
568
+ const propType = resolveSymbolType(type, prop)
548
569
  const isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional)
549
570
  let val = recurse(propType)
550
571
  if (isOptional && !val.startsWith('v.optional(')) {
@@ -589,7 +610,7 @@ function tsTypeToValibot(
589
610
  for (const prop of props) {
590
611
  const name = prop.getName()
591
612
  if (shouldSkipObjectKey(name)) continue
592
- const propType = resolveSymbolType(prop)
613
+ const propType = resolveSymbolType(type, prop)
593
614
  const isOptional = !!(prop.getFlags() & ts.SymbolFlags.Optional)
594
615
  let val = recurse(propType)
595
616
  if (isOptional && !val.startsWith('v.optional(')) {
@@ -773,6 +794,8 @@ export interface GenerateOptions {
773
794
  after?: string
774
795
  /** suppress output */
775
796
  silent?: boolean
797
+ /** ignore the generation cache */
798
+ force?: boolean
776
799
  }
777
800
 
778
801
  export interface WatchOptions extends GenerateOptions {
@@ -789,7 +812,7 @@ export interface GenerateResult {
789
812
  }
790
813
 
791
814
  export async function generate(options: GenerateOptions): Promise<GenerateResult> {
792
- const { dir, after, silent } = options
815
+ const { dir, after, silent, force } = options
793
816
  const baseDir = resolve(dir)
794
817
  // support both mutations/ (new) and models/ (legacy) directories
795
818
  const mutationsDir = resolve(baseDir, 'mutations')
@@ -812,6 +835,8 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
812
835
  // still re-runs generate on real model/query edits.
813
836
  const inputHash = hashInputTree(baseDir, generatedDir)
814
837
  if (
838
+ !force &&
839
+ generateCache.__generatorVersion === GENERATOR_CACHE_VERSION &&
815
840
  generateCache.__inputHash === inputHash &&
816
841
  existsSync(resolve(generatedDir, 'models.ts'))
817
842
  ) {
@@ -1134,6 +1159,7 @@ export async function generate(options: GenerateOptions): Promise<GenerateResult
1134
1159
  }
1135
1160
 
1136
1161
  // record the input hash + counts so the next boot can skip a no-op regen.
1162
+ generateCache.__generatorVersion = GENERATOR_CACHE_VERSION
1137
1163
  generateCache.__inputHash = inputHash
1138
1164
  generateCache.__counts = JSON.stringify({
1139
1165
  modelCount: allModelFiles.length,
@@ -25,7 +25,7 @@ export function createMutators<Models extends GenericModels>({
25
25
  environment,
26
26
  authData,
27
27
  createServerActions,
28
- asyncTasks = [],
28
+ enqueueTask = () => {},
29
29
  can,
30
30
  models,
31
31
  validateMutation,
@@ -35,7 +35,7 @@ export function createMutators<Models extends GenericModels>({
35
35
  authData: AuthData | null
36
36
  can: Can
37
37
  models: Models
38
- asyncTasks?: Array<() => Promise<void>>
38
+ enqueueTask?: NonNullable<MutatorContext['server']>['enqueueTask']
39
39
  createServerActions?: () => Record<string, any>
40
40
  validateMutation?: ValidateMutationFn
41
41
  /** valibot schemas keyed by model.mutationName, auto-validates args before running */
@@ -61,7 +61,7 @@ export function createMutators<Models extends GenericModels>({
61
61
  environment === 'server'
62
62
  ? ({
63
63
  actions: serverActions || {},
64
- asyncTasks: asyncTasks || {},
64
+ enqueueTask,
65
65
  } as MutatorContext['server'])
66
66
  : undefined,
67
67
  }
@@ -13,7 +13,7 @@ const asyncContext =
13
13
  contextStore.__onZeroMutatorContext__ ??
14
14
  (contextStore.__onZeroMutatorContext__ = createAsyncContext<MutatorContext>())
15
15
 
16
- // lightweight auth-only scope for asyncTasks (where mutation context is gone but authData is needed)
16
+ // lightweight auth-only scope for async tasks (where mutation context is gone but authData is needed)
17
17
  const authScopeContext =
18
18
  contextStore.__onZeroAuthScopeContext__ ??
19
19
  (contextStore.__onZeroAuthScopeContext__ = createAsyncContext<AuthData | null>())
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from './createPermissions'
2
+ export * from './asyncTasks'
2
3
  export * from './queryRegistry'
3
4
  export * from './helpers/batchQuery'
4
5
  export * from './helpers/createMutators'
package/src/server.ts CHANGED
@@ -1,2 +1,4 @@
1
1
  export * from './createZeroServer'
2
2
  export * from './createZeroSQLiteServer'
3
+ export * from './asyncTasks'
4
+ export type { AsyncTask, AsyncTaskEntry, AsyncTaskOptions } from './types'