on-zero 0.6.11 → 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.
- package/dist/cjs/createZeroSQLiteServer.cjs +319 -0
- package/dist/cjs/createZeroSQLiteServer.native.js +440 -0
- package/dist/cjs/createZeroSQLiteServer.native.js.map +1 -0
- package/dist/cjs/createZeroSQLiteServer.test.cjs +429 -0
- package/dist/cjs/createZeroSQLiteServer.test.native.js +466 -0
- package/dist/cjs/createZeroSQLiteServer.test.native.js.map +1 -0
- package/dist/cjs/createZeroServer.cjs +20 -234
- package/dist/cjs/createZeroServer.native.js +24 -250
- package/dist/cjs/createZeroServer.native.js.map +1 -1
- package/dist/cjs/createZeroServer.test.cjs +35 -1
- package/dist/cjs/createZeroServer.test.native.js +38 -1
- package/dist/cjs/createZeroServer.test.native.js.map +1 -1
- package/dist/cjs/createZeroServerCore.cjs +362 -0
- package/dist/cjs/createZeroServerCore.native.js +389 -0
- package/dist/cjs/createZeroServerCore.native.js.map +1 -0
- package/dist/cjs/generate-lite.cjs +1 -0
- package/dist/cjs/generate-lite.native.js +1 -0
- package/dist/cjs/generate-lite.native.js.map +1 -1
- package/dist/cjs/generate-lite.test.cjs +5 -1
- package/dist/cjs/generate-lite.test.native.js +5 -1
- package/dist/cjs/generate-lite.test.native.js.map +1 -1
- package/dist/cjs/generate.cjs +20 -11
- package/dist/cjs/generate.native.js +29 -21
- package/dist/cjs/generate.native.js.map +1 -1
- package/dist/cjs/generate.test.cjs +110 -0
- package/dist/cjs/generate.test.native.js +110 -0
- package/dist/cjs/generate.test.native.js.map +1 -1
- package/dist/cjs/httpPull/transport.test.cjs +44 -0
- package/dist/cjs/httpPull/transport.test.native.js +53 -0
- package/dist/cjs/httpPull/transport.test.native.js.map +1 -1
- package/dist/cjs/httpPullTransport.cjs +4 -1
- package/dist/cjs/httpPullTransport.native.js +5 -2
- package/dist/cjs/httpPullTransport.native.js.map +1 -1
- package/dist/cjs/server.cjs +2 -1
- package/dist/cjs/syncHost.cjs +164 -0
- package/dist/cjs/syncHost.native.js +371 -0
- package/dist/cjs/syncHost.native.js.map +1 -0
- package/dist/cjs/syncHost.test.cjs +229 -0
- package/dist/cjs/syncHost.test.native.js +278 -0
- package/dist/cjs/syncHost.test.native.js.map +1 -0
- package/dist/esm/createZeroSQLiteServer.mjs +293 -0
- package/dist/esm/createZeroSQLiteServer.mjs.map +1 -0
- package/dist/esm/createZeroSQLiteServer.native.js +411 -0
- package/dist/esm/createZeroSQLiteServer.native.js.map +1 -0
- package/dist/esm/createZeroSQLiteServer.test.mjs +430 -0
- package/dist/esm/createZeroSQLiteServer.test.mjs.map +1 -0
- package/dist/esm/createZeroSQLiteServer.test.native.js +464 -0
- package/dist/esm/createZeroSQLiteServer.test.native.js.map +1 -0
- package/dist/esm/createZeroServer.mjs +17 -231
- package/dist/esm/createZeroServer.mjs.map +1 -1
- package/dist/esm/createZeroServer.native.js +21 -247
- package/dist/esm/createZeroServer.native.js.map +1 -1
- package/dist/esm/createZeroServer.test.mjs +35 -1
- package/dist/esm/createZeroServer.test.mjs.map +1 -1
- package/dist/esm/createZeroServer.test.native.js +38 -1
- package/dist/esm/createZeroServer.test.native.js.map +1 -1
- package/dist/esm/createZeroServerCore.mjs +336 -0
- package/dist/esm/createZeroServerCore.mjs.map +1 -0
- package/dist/esm/createZeroServerCore.native.js +360 -0
- package/dist/esm/createZeroServerCore.native.js.map +1 -0
- package/dist/esm/generate-lite.mjs +1 -0
- package/dist/esm/generate-lite.mjs.map +1 -1
- package/dist/esm/generate-lite.native.js +1 -0
- package/dist/esm/generate-lite.native.js.map +1 -1
- package/dist/esm/generate-lite.test.mjs +5 -1
- package/dist/esm/generate-lite.test.mjs.map +1 -1
- package/dist/esm/generate-lite.test.native.js +5 -1
- package/dist/esm/generate-lite.test.native.js.map +1 -1
- package/dist/esm/generate.mjs +20 -11
- package/dist/esm/generate.mjs.map +1 -1
- package/dist/esm/generate.native.js +29 -21
- package/dist/esm/generate.native.js.map +1 -1
- package/dist/esm/generate.test.mjs +87 -1
- package/dist/esm/generate.test.mjs.map +1 -1
- package/dist/esm/generate.test.native.js +87 -1
- package/dist/esm/generate.test.native.js.map +1 -1
- package/dist/esm/httpPull/transport.test.mjs +44 -0
- package/dist/esm/httpPull/transport.test.mjs.map +1 -1
- package/dist/esm/httpPull/transport.test.native.js +53 -0
- package/dist/esm/httpPull/transport.test.native.js.map +1 -1
- package/dist/esm/httpPullTransport.mjs +4 -1
- package/dist/esm/httpPullTransport.mjs.map +1 -1
- package/dist/esm/httpPullTransport.native.js +5 -2
- package/dist/esm/httpPullTransport.native.js.map +1 -1
- package/dist/esm/server.mjs +1 -0
- package/dist/esm/server.mjs.map +1 -1
- package/dist/esm/syncHost.mjs +138 -0
- package/dist/esm/syncHost.mjs.map +1 -0
- package/dist/esm/syncHost.native.js +342 -0
- package/dist/esm/syncHost.native.js.map +1 -0
- package/dist/esm/syncHost.test.mjs +206 -0
- package/dist/esm/syncHost.test.mjs.map +1 -0
- package/dist/esm/syncHost.test.native.js +252 -0
- package/dist/esm/syncHost.test.native.js.map +1 -0
- package/package.json +19 -3
- package/readme.md +26 -0
- package/src/createZeroSQLiteServer.test.ts +461 -0
- package/src/createZeroSQLiteServer.ts +523 -0
- package/src/createZeroServer.test.ts +39 -1
- package/src/createZeroServer.ts +35 -428
- package/src/createZeroServerCore.ts +557 -0
- package/src/generate-lite.test.ts +4 -0
- package/src/generate-lite.ts +1 -0
- package/src/generate.test.ts +87 -1
- package/src/generate.ts +29 -20
- package/src/httpPull/transport.test.ts +47 -0
- package/src/httpPullTransport.ts +15 -1
- package/src/server.ts +1 -0
- package/src/syncHost.test.ts +216 -0
- package/src/syncHost.ts +255 -0
- package/test-fixtures/server-return-types.ts +50 -0
- package/test-fixtures/tsconfig.json +13 -0
- package/types/createZeroSQLiteServer.d.ts +37 -0
- package/types/createZeroSQLiteServer.d.ts.map +1 -0
- package/types/createZeroSQLiteServer.test.d.ts +2 -0
- package/types/createZeroSQLiteServer.test.d.ts.map +1 -0
- package/types/createZeroServer.d.ts +11 -293
- package/types/createZeroServer.d.ts.map +1 -1
- package/types/createZeroServerCore.d.ts +310 -0
- package/types/createZeroServerCore.d.ts.map +1 -0
- package/types/generate-lite.d.ts.map +1 -1
- package/types/generate.d.ts +2 -1
- package/types/generate.d.ts.map +1 -1
- package/types/httpPullTransport.d.ts +2 -0
- package/types/httpPullTransport.d.ts.map +1 -1
- package/types/server.d.ts +1 -0
- package/types/server.d.ts.map +1 -1
- package/types/syncHost.d.ts +51 -0
- package/types/syncHost.d.ts.map +1 -0
- package/types/syncHost.test.d.ts +2 -0
- package/types/syncHost.test.d.ts.map +1 -0
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import { mustGetQuery } from '@rocicorp/zero'
|
|
2
|
+
import {
|
|
3
|
+
PushProcessor,
|
|
4
|
+
handleQueryRequest as zeroHandleQueryRequest,
|
|
5
|
+
} from '@rocicorp/zero/server'
|
|
6
|
+
|
|
7
|
+
import { createPermissions } from './createPermissions'
|
|
8
|
+
import { createMutators } from './helpers/createMutators'
|
|
9
|
+
import {
|
|
10
|
+
getScopedAuthData,
|
|
11
|
+
isInZeroMutation,
|
|
12
|
+
mutatorContext,
|
|
13
|
+
runWithAuthScope,
|
|
14
|
+
} from './helpers/mutatorContext'
|
|
15
|
+
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
|
+
|
|
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
|
+
}
|
|
45
|
+
|
|
46
|
+
type AlreadyProcessedMutation = {
|
|
47
|
+
id: {
|
|
48
|
+
clientID: string
|
|
49
|
+
id: number
|
|
50
|
+
}
|
|
51
|
+
result: {
|
|
52
|
+
error: 'alreadyProcessed'
|
|
53
|
+
details?: unknown
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type PushRequestBody = {
|
|
58
|
+
clientGroupID?: unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
export type PersistAlreadyProcessedArgs = {
|
|
151
|
+
clientGroupID: string
|
|
152
|
+
mutations: AlreadyProcessedMutation[]
|
|
153
|
+
upstreamSchema: string
|
|
154
|
+
}
|
|
155
|
+
|
|
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
|
+
type ZeroServerCoreOptions<
|
|
178
|
+
Schema extends ZeroSchema,
|
|
179
|
+
Models extends GenericModels,
|
|
180
|
+
ServerActions extends Record<string, unknown>,
|
|
181
|
+
> = ZeroServerCommonOptions<Schema, Models, ServerActions> & {
|
|
182
|
+
createDatabase: () => ZeroServerDatabase
|
|
183
|
+
persistAlreadyProcessed?: (args: PersistAlreadyProcessedArgs) => Promise<void>
|
|
184
|
+
}
|
|
185
|
+
|
|
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
|
+
export function createZeroServerCore<
|
|
241
|
+
Schema extends ZeroSchema,
|
|
242
|
+
Models extends GenericModels,
|
|
243
|
+
ServerActions extends Record<string, unknown>,
|
|
244
|
+
>({
|
|
245
|
+
createDatabase,
|
|
246
|
+
createServerActions,
|
|
247
|
+
defaultAllowAdminRole = 'all',
|
|
248
|
+
defaultMutateAuthData = {} as MutateAuthData,
|
|
249
|
+
models,
|
|
250
|
+
mutations: mutationValidators,
|
|
251
|
+
persistAlreadyProcessed,
|
|
252
|
+
queries,
|
|
253
|
+
schema,
|
|
254
|
+
validateMutation,
|
|
255
|
+
validateQuery,
|
|
256
|
+
}: ZeroServerCoreOptions<Schema, Models, ServerActions>) {
|
|
257
|
+
setSchema(schema)
|
|
258
|
+
setEnvironment('server')
|
|
259
|
+
|
|
260
|
+
const permissions = createPermissions<Schema>({
|
|
261
|
+
environment: 'server',
|
|
262
|
+
schema,
|
|
263
|
+
adminRoleMode: defaultAllowAdminRole,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const handleMutationRequest = async ({
|
|
267
|
+
authData,
|
|
268
|
+
request,
|
|
269
|
+
skipAsyncTasks,
|
|
270
|
+
}: {
|
|
271
|
+
authData: AuthData | null
|
|
272
|
+
request: Request
|
|
273
|
+
skipAsyncTasks?: boolean
|
|
274
|
+
}) => {
|
|
275
|
+
const tasks = createAsyncTaskCollector()
|
|
276
|
+
const recoveryRequest = request.clone()
|
|
277
|
+
const mutators = createMutators({
|
|
278
|
+
asyncTasks: tasks.queue,
|
|
279
|
+
can: permissions.can,
|
|
280
|
+
createServerActions,
|
|
281
|
+
environment: 'server',
|
|
282
|
+
models,
|
|
283
|
+
authData,
|
|
284
|
+
validateMutation,
|
|
285
|
+
mutationValidators,
|
|
286
|
+
})
|
|
287
|
+
const processor = new PushProcessor(
|
|
288
|
+
withTransactionLifecycle(createDatabase(), tasks.lifecycle),
|
|
289
|
+
)
|
|
290
|
+
const response = await processor.process(mutators, request)
|
|
291
|
+
|
|
292
|
+
if (persistAlreadyProcessed) {
|
|
293
|
+
const parsed = await parseAlreadyProcessed(recoveryRequest, response)
|
|
294
|
+
if (parsed) await persistAlreadyProcessed(parsed)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
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
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
response,
|
|
307
|
+
asyncTasks: tasks.committed,
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const handleQueryRequest = async ({
|
|
312
|
+
authData,
|
|
313
|
+
request,
|
|
314
|
+
}: {
|
|
315
|
+
authData: AuthData | null
|
|
316
|
+
request: Request
|
|
317
|
+
}) => {
|
|
318
|
+
if (!queries) {
|
|
319
|
+
throw new Error(
|
|
320
|
+
'No queries registered with createZeroServer. ' +
|
|
321
|
+
'Pass the syncedQueries registry to createZeroServer via the queries option.',
|
|
322
|
+
)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const handler = (name: string, args: unknown) =>
|
|
326
|
+
resolveServerQuery({
|
|
327
|
+
authData,
|
|
328
|
+
name,
|
|
329
|
+
args,
|
|
330
|
+
queries,
|
|
331
|
+
permissions,
|
|
332
|
+
validateQuery,
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
const userID = typeof authData?.id === 'string' ? authData.id : undefined
|
|
336
|
+
const response = await runWithQueryContext(
|
|
337
|
+
{ authData: authData || ({} as AuthData) },
|
|
338
|
+
() =>
|
|
339
|
+
// the legacy overload omits userID when auth does not identify a user.
|
|
340
|
+
userID === undefined
|
|
341
|
+
? zeroHandleQueryRequest(handler, schema, request)
|
|
342
|
+
: zeroHandleQueryRequest({ handler, schema, request, userID }),
|
|
343
|
+
)
|
|
344
|
+
|
|
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()
|
|
456
|
+
}
|
|
457
|
+
|
|
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
|
+
return {
|
|
469
|
+
handleMutationRequest,
|
|
470
|
+
handleQueryRequest,
|
|
471
|
+
transaction,
|
|
472
|
+
mutate,
|
|
473
|
+
query,
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export type ZeroServer<
|
|
478
|
+
Schema extends ZeroSchema,
|
|
479
|
+
Models extends GenericModels,
|
|
480
|
+
ServerActions extends Record<string, unknown>,
|
|
481
|
+
> = ReturnType<typeof createZeroServerCore<Schema, Models, ServerActions>>
|
|
482
|
+
|
|
483
|
+
function withTransactionLifecycle(
|
|
484
|
+
database: ZeroServerDatabase,
|
|
485
|
+
lifecycle: TransactionAttemptLifecycle,
|
|
486
|
+
): ZeroServerDatabase {
|
|
487
|
+
return {
|
|
488
|
+
async transaction(callback, input) {
|
|
489
|
+
try {
|
|
490
|
+
const value = await database.transaction(async (tx, hooks) => {
|
|
491
|
+
lifecycle.beginAttempt()
|
|
492
|
+
try {
|
|
493
|
+
const result = await callback(tx, hooks)
|
|
494
|
+
lifecycle.completeAttempt()
|
|
495
|
+
return result
|
|
496
|
+
} catch (error) {
|
|
497
|
+
lifecycle.rollbackAttempt()
|
|
498
|
+
throw error
|
|
499
|
+
}
|
|
500
|
+
}, input)
|
|
501
|
+
lifecycle.commitTransaction()
|
|
502
|
+
return value
|
|
503
|
+
} catch (error) {
|
|
504
|
+
lifecycle.rollbackTransaction()
|
|
505
|
+
throw error
|
|
506
|
+
}
|
|
507
|
+
},
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function parseAlreadyProcessed(
|
|
512
|
+
request: Request,
|
|
513
|
+
response: unknown,
|
|
514
|
+
): Promise<PersistAlreadyProcessedArgs | undefined> {
|
|
515
|
+
const mutations = alreadyProcessedMutations(response)
|
|
516
|
+
if (mutations.length === 0) return
|
|
517
|
+
|
|
518
|
+
const upstreamSchema = new URL(request.url).searchParams.get('schema')
|
|
519
|
+
if (!upstreamSchema) return
|
|
520
|
+
|
|
521
|
+
const body = (await request.json()) as PushRequestBody
|
|
522
|
+
if (typeof body.clientGroupID !== 'string') return
|
|
523
|
+
|
|
524
|
+
return {
|
|
525
|
+
clientGroupID: body.clientGroupID,
|
|
526
|
+
mutations,
|
|
527
|
+
upstreamSchema,
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function alreadyProcessedMutations(response: unknown): AlreadyProcessedMutation[] {
|
|
532
|
+
if (!response || typeof response !== 'object') return []
|
|
533
|
+
const mutations = (response as { mutations?: unknown }).mutations
|
|
534
|
+
if (!Array.isArray(mutations)) return []
|
|
535
|
+
|
|
536
|
+
return mutations.filter((mutation): mutation is AlreadyProcessedMutation => {
|
|
537
|
+
if (!mutation || typeof mutation !== 'object') return false
|
|
538
|
+
const id = (mutation as { id?: unknown }).id
|
|
539
|
+
const result = (mutation as { result?: unknown }).result
|
|
540
|
+
return (
|
|
541
|
+
!!id &&
|
|
542
|
+
typeof id === 'object' &&
|
|
543
|
+
typeof (id as { clientID?: unknown }).clientID === 'string' &&
|
|
544
|
+
typeof (id as { id?: unknown }).id === 'number' &&
|
|
545
|
+
!!result &&
|
|
546
|
+
typeof result === 'object' &&
|
|
547
|
+
(result as { error?: unknown }).error === 'alreadyProcessed'
|
|
548
|
+
)
|
|
549
|
+
})
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const dummyTransactionInput: TransactionProviderInput = {
|
|
553
|
+
clientGroupID: 'unused',
|
|
554
|
+
clientID: 'unused',
|
|
555
|
+
mutationID: 42,
|
|
556
|
+
upstreamSchema: 'unused',
|
|
557
|
+
}
|
|
@@ -402,6 +402,8 @@ describe('generateLite', () => {
|
|
|
402
402
|
[`${DIR}/models/README.md`]: 'not a model',
|
|
403
403
|
[`${DIR}/models/helpers/util.ts`]: 'nested should be ignored',
|
|
404
404
|
[`${DIR}/models/post.d.ts`]: 'declaration file, ignored',
|
|
405
|
+
[`${DIR}/models/post.test.ts`]: 'test file, ignored',
|
|
406
|
+
[`${DIR}/models/post.spec.ts`]: 'spec file, ignored',
|
|
405
407
|
}
|
|
406
408
|
|
|
407
409
|
const fixtures: Record<string, LiteParsedFile> = {
|
|
@@ -422,6 +424,8 @@ describe('generateLite', () => {
|
|
|
422
424
|
expect(models).toContain("import * as post from '../models/post'")
|
|
423
425
|
expect(models).not.toContain('util')
|
|
424
426
|
expect(models).not.toContain('README')
|
|
427
|
+
expect(models).not.toContain('post.test')
|
|
428
|
+
expect(models).not.toContain('post.spec')
|
|
425
429
|
})
|
|
426
430
|
|
|
427
431
|
test('infers mutations/ directory when present', () => {
|
package/src/generate-lite.ts
CHANGED
|
@@ -136,6 +136,7 @@ function listDirectTsFiles(files: Record<string, string>, dirPrefix: string): st
|
|
|
136
136
|
if (rest.includes('/')) continue
|
|
137
137
|
if (!rest.endsWith('.ts')) continue
|
|
138
138
|
if (rest.endsWith('.d.ts')) continue
|
|
139
|
+
if (rest.endsWith('.test.ts') || rest.endsWith('.spec.ts')) continue
|
|
139
140
|
out.push(path)
|
|
140
141
|
}
|
|
141
142
|
return out.sort()
|
package/src/generate.test.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
2
|
import { tmpdir } from 'node:os'
|
|
3
3
|
import { join } from 'node:path'
|
|
4
|
+
import { runInNewContext } from 'node:vm'
|
|
4
5
|
|
|
6
|
+
import * as zero from '@rocicorp/zero'
|
|
5
7
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest'
|
|
6
8
|
|
|
7
|
-
import { generate } from './generate'
|
|
9
|
+
import { generate, generateDrizzleSchemaFile } from './generate'
|
|
8
10
|
|
|
9
11
|
const testDir = join(tmpdir(), 'on-zero-test-' + Date.now())
|
|
10
12
|
|
|
@@ -45,6 +47,15 @@ export const schema = table('comment', {
|
|
|
45
47
|
`,
|
|
46
48
|
)
|
|
47
49
|
|
|
50
|
+
writeFileSync(
|
|
51
|
+
join(testDir, 'models/post.test.ts'),
|
|
52
|
+
`throw new Error('test files must not be generated as models')`,
|
|
53
|
+
)
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(testDir, 'queries/comment.spec.ts'),
|
|
56
|
+
`throw new Error('spec files must not be generated as queries')`,
|
|
57
|
+
)
|
|
58
|
+
|
|
48
59
|
const result = await generate({ dir: testDir, silent: true })
|
|
49
60
|
|
|
50
61
|
expect(result.modelCount).toBe(2)
|
|
@@ -60,6 +71,7 @@ export const schema = table('comment', {
|
|
|
60
71
|
const modelsContent = readFileSync(join(testDir, 'generated/models.ts'), 'utf-8')
|
|
61
72
|
expect(modelsContent).toContain("import * as comment from '../models/comment'")
|
|
62
73
|
expect(modelsContent).toContain("import * as post from '../models/post'")
|
|
74
|
+
expect(modelsContent).not.toContain('post.test')
|
|
63
75
|
expect(modelsContent).toContain('export const models = {')
|
|
64
76
|
|
|
65
77
|
// check types.ts content
|
|
@@ -672,3 +684,77 @@ export const filteredPosts = (filter: PostFilter) => zero.query.post
|
|
|
672
684
|
expect(content).toContain('v.boolean()')
|
|
673
685
|
})
|
|
674
686
|
})
|
|
687
|
+
|
|
688
|
+
describe('generateDrizzleSchemaFile', () => {
|
|
689
|
+
test('preserves table server names and executable two-hop relationships', () => {
|
|
690
|
+
const schema = {
|
|
691
|
+
tables: {
|
|
692
|
+
users: {
|
|
693
|
+
name: 'users',
|
|
694
|
+
serverName: 'user_records',
|
|
695
|
+
primaryKey: ['id'],
|
|
696
|
+
columns: {
|
|
697
|
+
id: { type: 'string', optional: false, customType: null },
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
groups: {
|
|
701
|
+
name: 'groups',
|
|
702
|
+
primaryKey: ['id'],
|
|
703
|
+
columns: {
|
|
704
|
+
id: { type: 'string', optional: false, customType: null },
|
|
705
|
+
},
|
|
706
|
+
},
|
|
707
|
+
memberships: {
|
|
708
|
+
name: 'memberships',
|
|
709
|
+
primaryKey: ['userId', 'groupId'],
|
|
710
|
+
columns: {
|
|
711
|
+
userId: { type: 'string', optional: false, customType: null },
|
|
712
|
+
groupId: { type: 'string', optional: false, customType: null },
|
|
713
|
+
},
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
relationships: {
|
|
717
|
+
users: {
|
|
718
|
+
groups: [
|
|
719
|
+
{
|
|
720
|
+
sourceField: ['id'],
|
|
721
|
+
destField: ['userId'],
|
|
722
|
+
destSchema: 'memberships',
|
|
723
|
+
cardinality: 'many' as const,
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
sourceField: ['groupId'],
|
|
727
|
+
destField: ['id'],
|
|
728
|
+
destSchema: 'groups',
|
|
729
|
+
cardinality: 'many' as const,
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
}
|
|
735
|
+
const source = generateDrizzleSchemaFile(schema)
|
|
736
|
+
const executableSource = source
|
|
737
|
+
.replace(
|
|
738
|
+
"import { boolean, createSchema, json, number, relationships, string, table } from '@rocicorp/zero'",
|
|
739
|
+
'',
|
|
740
|
+
)
|
|
741
|
+
.replace('export const schema =', 'globalThis.generatedSchema =')
|
|
742
|
+
const context: Record<string, unknown> = { ...zero }
|
|
743
|
+
|
|
744
|
+
runInNewContext(executableSource, context)
|
|
745
|
+
|
|
746
|
+
expect(source).toContain('const usersTable = table("users").from("user_records")')
|
|
747
|
+
expect(context.generatedSchema).toMatchObject({
|
|
748
|
+
tables: {
|
|
749
|
+
users: {
|
|
750
|
+
serverName: 'user_records',
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
relationships: {
|
|
754
|
+
users: {
|
|
755
|
+
groups: schema.relationships.users.groups,
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
})
|
|
759
|
+
})
|
|
760
|
+
})
|