on-zero 0.6.12 → 0.6.13

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 (131) hide show
  1. package/dist/cjs/createZeroSQLiteServer.cjs +319 -0
  2. package/dist/cjs/createZeroSQLiteServer.native.js +440 -0
  3. package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -0
  4. package/dist/cjs/createZeroSQLiteServer.test.cjs +429 -0
  5. package/dist/cjs/createZeroSQLiteServer.test.native.js +466 -0
  6. package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -0
  7. package/dist/cjs/createZeroServer.cjs +20 -234
  8. package/dist/cjs/createZeroServer.native.js +24 -250
  9. package/dist/cjs/createZeroServer.native.js.map +1 -1
  10. package/dist/cjs/createZeroServer.test.cjs +35 -1
  11. package/dist/cjs/createZeroServer.test.native.js +38 -1
  12. package/dist/cjs/createZeroServer.test.native.js.map +1 -1
  13. package/dist/cjs/createZeroServerCore.cjs +362 -0
  14. package/dist/cjs/createZeroServerCore.native.js +389 -0
  15. package/dist/cjs/createZeroServerCore.native.js.map +1 -0
  16. package/dist/cjs/generate-lite.cjs +1 -0
  17. package/dist/cjs/generate-lite.native.js +1 -0
  18. package/dist/cjs/generate-lite.native.js.map +1 -1
  19. package/dist/cjs/generate-lite.test.cjs +5 -1
  20. package/dist/cjs/generate-lite.test.native.js +5 -1
  21. package/dist/cjs/generate-lite.test.native.js.map +1 -1
  22. package/dist/cjs/generate.cjs +20 -11
  23. package/dist/cjs/generate.native.js +29 -21
  24. package/dist/cjs/generate.native.js.map +1 -1
  25. package/dist/cjs/generate.test.cjs +110 -0
  26. package/dist/cjs/generate.test.native.js +110 -0
  27. package/dist/cjs/generate.test.native.js.map +1 -1
  28. package/dist/cjs/httpPull/transport.test.cjs +5 -4
  29. package/dist/cjs/httpPull/transport.test.native.js +6 -5
  30. package/dist/cjs/httpPull/transport.test.native.js.map +1 -1
  31. package/dist/cjs/httpPullTransport.cjs +2 -1
  32. package/dist/cjs/httpPullTransport.native.js +3 -2
  33. package/dist/cjs/httpPullTransport.native.js.map +1 -1
  34. package/dist/cjs/server.cjs +2 -1
  35. package/dist/cjs/syncHost.cjs +164 -0
  36. package/dist/cjs/syncHost.native.js +371 -0
  37. package/dist/cjs/syncHost.native.js.map +1 -0
  38. package/dist/cjs/syncHost.test.cjs +229 -0
  39. package/dist/cjs/syncHost.test.native.js +278 -0
  40. package/dist/cjs/syncHost.test.native.js.map +1 -0
  41. package/dist/esm/createZeroSQLiteServer.mjs +293 -0
  42. package/dist/esm/createZeroSQLiteServer.mjs.map +1 -0
  43. package/dist/esm/createZeroSQLiteServer.native.js +411 -0
  44. package/dist/esm/createZeroSQLiteServer.native.js.map +1 -0
  45. package/dist/esm/createZeroSQLiteServer.test.mjs +430 -0
  46. package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -0
  47. package/dist/esm/createZeroSQLiteServer.test.native.js +464 -0
  48. package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -0
  49. package/dist/esm/createZeroServer.mjs +17 -231
  50. package/dist/esm/createZeroServer.mjs.map +1 -1
  51. package/dist/esm/createZeroServer.native.js +21 -247
  52. package/dist/esm/createZeroServer.native.js.map +1 -1
  53. package/dist/esm/createZeroServer.test.mjs +35 -1
  54. package/dist/esm/createZeroServer.test.mjs.map +1 -1
  55. package/dist/esm/createZeroServer.test.native.js +38 -1
  56. package/dist/esm/createZeroServer.test.native.js.map +1 -1
  57. package/dist/esm/createZeroServerCore.mjs +336 -0
  58. package/dist/esm/createZeroServerCore.mjs.map +1 -0
  59. package/dist/esm/createZeroServerCore.native.js +360 -0
  60. package/dist/esm/createZeroServerCore.native.js.map +1 -0
  61. package/dist/esm/generate-lite.mjs +1 -0
  62. package/dist/esm/generate-lite.mjs.map +1 -1
  63. package/dist/esm/generate-lite.native.js +1 -0
  64. package/dist/esm/generate-lite.native.js.map +1 -1
  65. package/dist/esm/generate-lite.test.mjs +5 -1
  66. package/dist/esm/generate-lite.test.mjs.map +1 -1
  67. package/dist/esm/generate-lite.test.native.js +5 -1
  68. package/dist/esm/generate-lite.test.native.js.map +1 -1
  69. package/dist/esm/generate.mjs +20 -11
  70. package/dist/esm/generate.mjs.map +1 -1
  71. package/dist/esm/generate.native.js +29 -21
  72. package/dist/esm/generate.native.js.map +1 -1
  73. package/dist/esm/generate.test.mjs +87 -1
  74. package/dist/esm/generate.test.mjs.map +1 -1
  75. package/dist/esm/generate.test.native.js +87 -1
  76. package/dist/esm/generate.test.native.js.map +1 -1
  77. package/dist/esm/httpPull/transport.test.mjs +5 -4
  78. package/dist/esm/httpPull/transport.test.mjs.map +1 -1
  79. package/dist/esm/httpPull/transport.test.native.js +6 -5
  80. package/dist/esm/httpPull/transport.test.native.js.map +1 -1
  81. package/dist/esm/httpPullTransport.mjs +2 -1
  82. package/dist/esm/httpPullTransport.mjs.map +1 -1
  83. package/dist/esm/httpPullTransport.native.js +3 -2
  84. package/dist/esm/httpPullTransport.native.js.map +1 -1
  85. package/dist/esm/server.mjs +1 -0
  86. package/dist/esm/server.mjs.map +1 -1
  87. package/dist/esm/syncHost.mjs +138 -0
  88. package/dist/esm/syncHost.mjs.map +1 -0
  89. package/dist/esm/syncHost.native.js +342 -0
  90. package/dist/esm/syncHost.native.js.map +1 -0
  91. package/dist/esm/syncHost.test.mjs +206 -0
  92. package/dist/esm/syncHost.test.mjs.map +1 -0
  93. package/dist/esm/syncHost.test.native.js +252 -0
  94. package/dist/esm/syncHost.test.native.js.map +1 -0
  95. package/package.json +19 -3
  96. package/readme.md +26 -0
  97. package/src/createZeroSQLiteServer.test.ts +461 -0
  98. package/src/createZeroSQLiteServer.ts +523 -0
  99. package/src/createZeroServer.test.ts +39 -1
  100. package/src/createZeroServer.ts +35 -428
  101. package/src/createZeroServerCore.ts +557 -0
  102. package/src/generate-lite.test.ts +4 -0
  103. package/src/generate-lite.ts +1 -0
  104. package/src/generate.test.ts +87 -1
  105. package/src/generate.ts +29 -20
  106. package/src/httpPull/transport.test.ts +6 -5
  107. package/src/httpPullTransport.ts +9 -4
  108. package/src/server.ts +1 -0
  109. package/src/syncHost.test.ts +216 -0
  110. package/src/syncHost.ts +255 -0
  111. package/test-fixtures/server-return-types.ts +50 -0
  112. package/test-fixtures/tsconfig.json +13 -0
  113. package/types/createZeroSQLiteServer.d.ts +37 -0
  114. package/types/createZeroSQLiteServer.d.ts.map +1 -0
  115. package/types/createZeroSQLiteServer.test.d.ts +2 -0
  116. package/types/createZeroSQLiteServer.test.d.ts.map +1 -0
  117. package/types/createZeroServer.d.ts +11 -293
  118. package/types/createZeroServer.d.ts.map +1 -1
  119. package/types/createZeroServerCore.d.ts +310 -0
  120. package/types/createZeroServerCore.d.ts.map +1 -0
  121. package/types/generate-lite.d.ts.map +1 -1
  122. package/types/generate.d.ts +2 -1
  123. package/types/generate.d.ts.map +1 -1
  124. package/types/httpPullTransport.d.ts +1 -0
  125. package/types/httpPullTransport.d.ts.map +1 -1
  126. package/types/server.d.ts +1 -0
  127. package/types/server.d.ts.map +1 -1
  128. package/types/syncHost.d.ts +51 -0
  129. package/types/syncHost.d.ts.map +1 -0
  130. package/types/syncHost.test.d.ts +2 -0
  131. package/types/syncHost.test.d.ts.map +1 -0
@@ -1,475 +1,82 @@
1
- import { mustGetQuery } from '@rocicorp/zero'
2
- import { PushProcessor } from '@rocicorp/zero/pg'
3
- import { handleQueryRequest as zeroHandleQueryRequest } from '@rocicorp/zero/server'
4
- import { zeroNodePg, type NodePgTransaction } from '@rocicorp/zero/server/adapters/pg'
1
+ import { zeroNodePg } from '@rocicorp/zero/server/adapters/pg'
5
2
  import { assertString } from '@take-out/helpers'
6
3
  import { Pool } from 'pg'
7
4
 
8
- import { createPermissions } from './createPermissions'
9
- import { createMutators } from './helpers/createMutators'
10
- import {
11
- getScopedAuthData,
12
- isInZeroMutation,
13
- mutatorContext,
14
- runWithAuthScope,
15
- } from './helpers/mutatorContext'
16
- import { runWithQueryContext } from './helpers/queryContext'
17
- import { getMutationsPermissions } from './modelRegistry'
18
- import { setCustomQueries } from './run'
19
- import { getZQL, setEnvironment, setSchema } from './state'
20
- import { setEvaluatingPermission } from './where'
21
- import { setRunner } from './zeroRunner'
5
+ import { createZeroServerCore } from './createZeroServerCore'
22
6
 
23
7
  import type {
24
- AdminRoleMode,
25
- AsyncAction,
26
- AuthData,
27
- GenericModels,
28
- MutatorContext,
29
- QueryBuilder,
30
- Transaction,
31
- } from './types'
32
- import type {
33
- AnyQueryRegistry,
34
- HumanReadable,
35
- Query,
36
- Schema as ZeroSchema,
37
- } from '@rocicorp/zero'
38
- import type { TransactionProviderInput } from '@rocicorp/zero/pg'
39
-
40
- type MutateAuthData = Pick<AuthData, 'email' | 'id'> & Partial<AuthData>
41
-
42
- type MutateOptions = {
43
- authData?: MutateAuthData
44
- awaitEffects?: boolean
45
- }
46
-
47
- type AlreadyProcessedMutation = {
48
- id: {
49
- clientID: string
50
- id: number
51
- }
52
- result: {
53
- error: 'alreadyProcessed'
54
- details?: unknown
55
- }
56
- }
57
-
58
- type PushRequestBody = {
59
- clientGroupID?: unknown
60
- }
61
-
62
- type ServerMutate<Models extends GenericModels> = {
63
- [Key in keyof Models]: {
64
- [K in keyof Models[Key]['mutate']]: Models[Key]['mutate'][K] extends (
65
- ctx: MutatorContext,
66
- arg: infer Arg,
67
- ) => any
68
- ? (arg: Arg, options?: MutateOptions) => Promise<void>
69
- : (options?: MutateOptions) => Promise<void>
70
- }
71
- }
72
-
73
- export type ValidateQueryArgs = {
74
- authData: AuthData | null
75
- queryName: string
76
- params: unknown
77
- }
78
-
79
- export type ValidateMutationArgs = {
80
- authData: AuthData | null
81
- mutatorName: string
82
- tableName: string
83
- args: unknown
84
- }
85
-
86
- export type ValidateQueryFn = (args: ValidateQueryArgs) => void
87
- export type ValidateMutationFn = (args: ValidateMutationArgs) => void | Promise<void>
8
+ PersistAlreadyProcessedArgs,
9
+ ZeroServer,
10
+ ZeroServerCommonOptions,
11
+ ZeroServerDatabase,
12
+ } from './createZeroServerCore'
13
+ import type { GenericModels } from './types'
14
+ import type { Schema as ZeroSchema } from '@rocicorp/zero'
15
+ import type { NodePgTransaction } from '@rocicorp/zero/server/adapters/pg'
16
+
17
+ export type {
18
+ ValidateMutationArgs,
19
+ ValidateMutationFn,
20
+ ValidateQueryArgs,
21
+ ValidateQueryFn,
22
+ } from './createZeroServerCore'
88
23
 
89
24
  export function createZeroServer<
90
25
  Schema extends ZeroSchema,
91
26
  Models extends GenericModels,
92
27
  ServerActions extends Record<string, unknown>,
93
28
  >({
94
- createServerActions,
95
29
  database,
96
30
  pool: externalPool,
97
- schema,
98
- models,
99
- queries,
100
- mutations: mutationValidators,
101
- validateQuery,
102
- validateMutation,
103
- defaultAllowAdminRole = 'all',
104
- defaultMutateAuthData = {} as MutateAuthData,
105
- }: {
31
+ ...options
32
+ }: ZeroServerCommonOptions<Schema, Models, ServerActions> & {
106
33
  /**
107
- * Optional pre-built Pool-like object (e.g. PGlite wrapper for Cloudflare Workers).
108
- * When provided, `database` is not required.
34
+ * Optional pre-built Pool-like object, such as a PGlite wrapper.
35
+ * When provided, database is not required.
109
36
  */
110
37
  pool?: NodePgTransaction
111
38
  /**
112
- * The DB connection string, same as ZERO_UPSTREAM_DB.
113
- * Not required when `pool` is provided.
39
+ * The PostgreSQL connection string.
40
+ * Not required when pool is provided.
114
41
  */
115
42
  database?: string
116
- schema: Schema
117
- models: Models
118
- createServerActions: () => ServerActions
119
- queries?: AnyQueryRegistry
120
- /**
121
- * Generated valibot validators for mutation args, keyed by model.mutationName.
122
- * Pass the `mutationValidators` export from generated syncedMutations.ts.
123
- * Args are auto-validated before running the mutation.
124
- */
125
- mutations?: Record<string, Record<string, any>>
126
- /**
127
- * Hook to validate queries before execution. Throw to reject.
128
- * Must be synchronous.
129
- */
130
- validateQuery?: ValidateQueryFn
131
- /**
132
- * Hook to validate mutations before execution. Throw to reject.
133
- */
134
- validateMutation?: ValidateMutationFn
135
- /**
136
- * Admin role bypass for permissions:
137
- * - 'all': admin bypasses both query and mutation permissions (default)
138
- * - 'queries': admin bypasses only query permissions
139
- * - 'mutations': admin bypasses only mutation permissions
140
- * - 'off': admin has no special bypass
141
- */
142
- defaultAllowAdminRole?: AdminRoleMode
143
- /**
144
- * Default authData used by zeroServer.mutate when no authData is provided
145
- * and none is available from mutation context or auth scope.
146
- * Defaults to {}.
147
- */
148
- defaultMutateAuthData?: MutateAuthData
149
- }) {
150
- setSchema(schema)
151
- setEnvironment('server')
152
-
43
+ }): ZeroServer<Schema, Models, ServerActions> {
153
44
  let pool: NodePgTransaction
154
45
  if (externalPool) {
155
46
  pool = externalPool
156
47
  } else {
157
- const dbString = assertString(database, `createZeroServer "database"`)
48
+ const dbString = assertString(database, 'createZeroServer "database"')
158
49
  const pgPool = new Pool({
159
50
  connectionString: dbString,
160
- // handle self-signed certificates in production
161
51
  ssl: dbString.includes('sslmode=require')
162
52
  ? { rejectUnauthorized: false }
163
53
  : undefined,
164
54
  })
165
55
  pgPool.on('error', (error) => {
166
- console.error(`[on-zero] pool error`, error.message)
56
+ console.error('[on-zero] pool error', error.message)
167
57
  })
168
58
  pgPool.on('connect', (client) => {
169
59
  client.on('error', (error) => {
170
- console.error(`[on-zero] client error`, error.message)
60
+ console.error('[on-zero] client error', error.message)
171
61
  })
172
62
  })
173
63
  pool = pgPool
174
64
  }
175
65
 
176
- const zeroDb = zeroNodePg(schema, pool)
177
-
178
- const permissions = createPermissions<Schema>({
179
- environment: 'server',
180
- schema,
181
- adminRoleMode: defaultAllowAdminRole,
182
- })
183
-
184
- const processor = new PushProcessor(zeroDb)
185
-
186
- const handleMutationRequest = async ({
187
- authData,
188
- request,
189
- skipAsyncTasks,
190
- }: {
191
- authData: AuthData | null
192
- request: Request
193
- skipAsyncTasks?: boolean
194
- }) => {
195
- // since mutations do DB work in transaction, avoid any async tasks during
196
- const asyncTasks: AsyncAction[] = []
197
- const recoveryRequest = request.clone()
198
-
199
- const mutators = createMutators({
200
- asyncTasks,
201
- can: permissions.can,
202
- createServerActions,
203
- environment: 'server',
204
- models,
205
- authData,
206
- validateMutation,
207
- mutationValidators,
208
- })
209
-
210
- // @ts-expect-error type is ok but config in monorepo
211
- const response = await processor.process(mutators, request)
212
- await persistAlreadyProcessedMutationResults(pool, recoveryRequest, response)
213
-
214
- // now finish
215
- if (!skipAsyncTasks && asyncTasks.length) {
216
- Promise.all(asyncTasks.map((task) => runWithAuthScope(authData, task))).catch(
217
- (err: unknown) => {
218
- console.error(`[push] async tasks failed`, err)
219
- },
220
- )
221
- }
222
-
223
- return {
224
- response,
225
- asyncTasks,
226
- }
227
- }
228
-
229
- const handleQueryRequest = async ({
230
- authData,
231
- request,
232
- }: {
233
- authData: AuthData | null
234
- request: Request
235
- }) => {
236
- if (!queries) {
237
- throw new Error(
238
- 'No queries registered with createZeroServer. ' +
239
- 'Pass the syncedQueries registry to createZeroServer via the queries option.',
240
- )
241
- }
242
-
243
- const handler = (name: string, args: unknown) => {
244
- // per-model permission queries registered by on-zero at runtime
245
- if (name.startsWith('permission.')) {
246
- const table = name.slice('permission.'.length)
247
- const { objOrId } = args as {
248
- objOrId: string | Record<string, any>
249
- }
250
- const perm = getMutationsPermissions(table)
251
- if (!perm) {
252
- throw new Error(`[permission] no permission defined for table: ${table}`)
253
- }
254
- // wrap with setEvaluatingPermission so serverWhere evaluates
255
- // even when environment is 'client' (SSR hydration)
256
- setEvaluatingPermission(true)
257
- try {
258
- return (getZQL() as any)[table]
259
- .where((eb: any) => {
260
- return permissions.buildPermissionQuery(authData, eb, perm, objOrId, table)
261
- })
262
- .one()
263
- } finally {
264
- setEvaluatingPermission(false)
265
- }
266
- }
267
-
268
- // run validation hook if provided (must be sync - throw to reject)
269
- if (validateQuery) {
270
- validateQuery({ authData, queryName: name, params: args })
271
- }
272
-
273
- const query = (mustGetQuery as any)(queries, name)
274
- return query.fn({ args, ctx: authData })
275
- }
276
-
277
- // when there is an authenticated user, pin the connection to its id so zero
278
- // server-validates it (a client cannot claim another user's id). when logged
279
- // out there is no server identity to validate against, so use zero's
280
- // client-fallback path (the legacy call signature) which trusts whatever id
281
- // the client declared — "anon", "guest", a per-device id, etc. on-zero stays
282
- // un-prescriptive about the anon convention; permissions still gate access by
283
- // authData. passing userID:null to the object form instead would make zero
284
- // server-validate against null and reject every anon client, since a string
285
- // connection userID can never equal null.
286
- const userID = typeof authData?.id === 'string' ? authData.id : undefined
287
-
288
- const response = await runWithQueryContext(
289
- { authData: authData || ({} as AuthData) },
290
- () =>
291
- userID === undefined
292
- ? zeroHandleQueryRequest(handler, schema, request)
293
- : zeroHandleQueryRequest({ handler, schema, request, userID }),
294
- )
295
-
296
- return {
297
- response,
298
- }
299
- }
300
-
301
- async function runMutate(
302
- modelName: string,
303
- mutatorName: string,
304
- mutatorArg: unknown,
305
- options?: MutateOptions,
306
- ) {
307
- let authData = options?.authData
308
-
309
- // auto-resolve authData from mutation context or auth scope
310
- if (!authData) {
311
- const scoped = getScopedAuthData()
312
- if (scoped) {
313
- authData = scoped as MutateAuthData
314
- }
315
- }
66
+ const databaseProvider = zeroNodePg(options.schema, pool)
316
67
 
317
- const asyncTasks: Array<() => Promise<void>> = []
318
-
319
- const mutators = createMutators({
320
- models,
321
- environment: 'server',
322
- asyncTasks,
323
- authData: {
324
- ...defaultMutateAuthData,
325
- ...authData,
326
- },
327
- createServerActions,
328
- can: permissions.can,
329
- validateMutation,
330
- mutationValidators,
331
- })
332
-
333
- const modelMutators = mutators[modelName as keyof typeof mutators] as Record<
334
- string,
335
- (tx: Transaction, arg?: unknown) => Promise<void>
336
- >
337
- const mutator = modelMutators[mutatorName]
338
-
339
- await transaction(async (tx) => {
340
- await mutator(tx, mutatorArg)
341
- })
342
-
343
- if (asyncTasks.length) {
344
- const resolvedAuth = authData ?? null
345
- const promise = Promise.all(
346
- asyncTasks.map((t) => runWithAuthScope(resolvedAuth, t)),
347
- )
348
- if (options?.awaitEffects) {
349
- await promise
350
- } else {
351
- promise.catch((err: unknown) => {
352
- console.error(`[mutate] async tasks failed`, err)
353
- })
354
- }
355
- }
356
- }
357
-
358
- // zeroServer.mutate.user.insert(user)
359
- const mutate = new Proxy({} as ServerMutate<Models>, {
360
- get(_, modelName: string) {
361
- return new Proxy(
362
- {},
363
- {
364
- get(_, mutatorName: string) {
365
- return (arg: unknown, options?: MutateOptions) =>
366
- runMutate(modelName, mutatorName, arg, options)
367
- },
368
- },
369
- )
370
- },
371
- })
372
-
373
- async function transaction<
374
- CB extends (tx: Transaction) => Promise<any>,
375
- Returns extends CB extends (tx: Transaction) => Promise<infer X> ? X : never,
376
- >(query: CB): Promise<Returns> {
377
- try {
378
- if (isInZeroMutation()) {
379
- const { tx } = mutatorContext()
380
- return await query(tx)
381
- }
382
- // @ts-expect-error type
383
- const output = await zeroDb.transaction(query, dummyTransactionInput)
384
- return output
385
- } catch (err) {
386
- if ((err as any)?.name !== 'PermissionError') {
387
- console.error(`Error running transaction(): ${err}`)
388
- }
389
- throw err
390
- }
391
- }
392
-
393
- function query<R>(
394
- cb: (q: QueryBuilder) => Query<any, Schema, R>,
395
- authData?: AuthData | null,
396
- ): Promise<HumanReadable<R>> {
397
- const run = () =>
398
- transaction(async (tx) => {
399
- return tx.run(cb(getZQL()))
400
- }) as any
401
-
402
- if (authData !== undefined) {
403
- return runWithQueryContext({ authData }, run)
404
- }
405
- return run()
406
- }
407
-
408
- // register for global run() helper
409
- if (queries) {
410
- setCustomQueries(queries)
411
- }
412
-
413
- // server uses transaction-based execution
414
- setRunner((queryObj) => {
415
- return transaction(async (tx) => {
416
- return tx.run(queryObj)
417
- })
418
- })
419
-
420
- // This is needed temporarily and will be cleaned up in the future.
421
- const dummyTransactionInput: TransactionProviderInput = {
422
- clientGroupID: 'unused',
423
- clientID: 'unused',
424
- mutationID: 42,
425
- upstreamSchema: 'unused',
426
- }
427
-
428
- return {
429
- handleMutationRequest,
430
- handleQueryRequest,
431
- transaction,
432
- mutate,
433
- query,
434
- }
435
- }
436
-
437
- function alreadyProcessedMutations(response: unknown): AlreadyProcessedMutation[] {
438
- if (!response || typeof response !== 'object') return []
439
- const mutations = (response as { mutations?: unknown }).mutations
440
- if (!Array.isArray(mutations)) return []
441
-
442
- return mutations.filter((mutation): mutation is AlreadyProcessedMutation => {
443
- if (!mutation || typeof mutation !== 'object') return false
444
- const id = (mutation as { id?: unknown }).id
445
- const result = (mutation as { result?: unknown }).result
446
- return (
447
- !!id &&
448
- typeof id === 'object' &&
449
- typeof (id as { clientID?: unknown }).clientID === 'string' &&
450
- typeof (id as { id?: unknown }).id === 'number' &&
451
- !!result &&
452
- typeof result === 'object' &&
453
- (result as { error?: unknown }).error === 'alreadyProcessed'
454
- )
68
+ return createZeroServerCore({
69
+ ...options,
70
+ createDatabase: () => databaseProvider as unknown as ZeroServerDatabase,
71
+ persistAlreadyProcessed: (args) => persistAlreadyProcessedMutationResults(pool, args),
455
72
  })
456
73
  }
457
74
 
458
75
  async function persistAlreadyProcessedMutationResults(
459
76
  pool: NodePgTransaction,
460
- request: Request,
461
- response: unknown,
77
+ { clientGroupID, mutations, upstreamSchema }: PersistAlreadyProcessedArgs,
462
78
  ) {
463
- const mutations = alreadyProcessedMutations(response)
464
- if (mutations.length === 0) return
465
-
466
- const schema = new URL(request.url).searchParams.get('schema')
467
- if (!schema) return
468
-
469
- const body = (await request.json()) as PushRequestBody
470
- if (typeof body.clientGroupID !== 'string') return
471
-
472
- const table = `${quotePgIdentifier(schema)}."mutations"`
79
+ const table = `${quotePgIdentifier(upstreamSchema)}."mutations"`
473
80
  const values = mutations
474
81
  .map((_, index) => {
475
82
  const offset = index * 4
@@ -477,7 +84,7 @@ async function persistAlreadyProcessedMutationResults(
477
84
  })
478
85
  .join(', ')
479
86
  const params = mutations.flatMap((mutation) => [
480
- body.clientGroupID,
87
+ clientGroupID,
481
88
  mutation.id.clientID,
482
89
  mutation.id.id,
483
90
  JSON.stringify(mutation.result),