bunderstack 0.16.0 → 0.17.0-beta.2
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/README.md +25 -138
- package/package.json +24 -14
- package/src/access.ts +6 -21
- package/src/api/builder.ts +52 -0
- package/src/api/context.ts +83 -0
- package/src/api/crud-router.ts +386 -0
- package/src/api/openapi.ts +184 -0
- package/src/api/realtime-router.ts +75 -0
- package/src/api/registry.ts +338 -0
- package/src/api/router.ts +34 -0
- package/src/api/storage-router.ts +224 -0
- package/src/api/types.ts +105 -0
- package/src/auth.ts +5 -0
- package/src/blueprint.ts +88 -105
- package/src/config.ts +57 -39
- package/src/crud-operations.ts +478 -0
- package/src/dialect.ts +1 -1
- package/src/email/smtp.ts +1 -1
- package/src/env.ts +19 -13
- package/src/errors.ts +98 -26
- package/src/handler.ts +16 -44
- package/src/index.ts +248 -247
- package/src/jobs/define.ts +8 -10
- package/src/jobs/queue.ts +6 -2
- package/src/jobs/worker.ts +4 -1
- package/src/list-query.ts +50 -133
- package/src/manifest.ts +84 -87
- package/src/rate-limit.ts +1 -1
- package/src/realtime/facade.ts +36 -14
- package/src/realtime/filter.ts +81 -0
- package/src/realtime/heartbeat.ts +80 -0
- package/src/realtime/publisher.ts +46 -0
- package/src/standard-schema.ts +59 -0
- package/src/storage/index.ts +8 -0
- package/src/storage/operations.ts +398 -0
- package/src/crud.ts +0 -400
- package/src/realtime/index.ts +0 -242
- package/src/realtime/redis.ts +0 -219
- package/src/routes.ts +0 -137
- package/src/storage/router.ts +0 -531
- package/src/trpc.ts +0 -57
package/src/index.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import type { AnyRouter as AnyORPCRouter } from '@orpc/server'
|
|
1
2
|
// src/index.ts
|
|
2
|
-
import type { AnyRouter } from '@trpc/server'
|
|
3
|
-
import type { Hono as HonoType } from 'hono'
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
import {
|
|
4
|
+
import { SmartCoercionHandlerPlugin } from '@orpc/json-schema'
|
|
5
|
+
import { OpenAPIGenerator, OpenAPIGeneratorError } from '@orpc/openapi'
|
|
6
|
+
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
|
7
|
+
import { RPCHandler } from '@orpc/server/fetch'
|
|
8
|
+
import { ValibotToJsonSchemaConverter } from '@orpc/valibot'
|
|
8
9
|
|
|
9
10
|
import type { TableAccessInput } from './access'
|
|
11
|
+
import type {
|
|
12
|
+
CrudApiRouterFor,
|
|
13
|
+
MergeApiRouterTypes,
|
|
14
|
+
UnifiedApiRouter,
|
|
15
|
+
} from './api/types'
|
|
16
|
+
import type { RealtimeApiRouter } from './api/realtime-router'
|
|
10
17
|
import type { DbFor } from './db'
|
|
11
18
|
import type {
|
|
12
19
|
BunderstackJobsBuilder,
|
|
@@ -19,11 +26,18 @@ import type {
|
|
|
19
26
|
import type { StorageConfigInput } from './storage/buckets'
|
|
20
27
|
import type { StorageAdapter } from './storage/index'
|
|
21
28
|
|
|
29
|
+
import { validateAndResolveAccess } from './access'
|
|
30
|
+
import { createApiBuilder } from './api/builder'
|
|
31
|
+
import { createApiContext } from './api/context'
|
|
32
|
+
import { buildCrudApiRouter } from './api/crud-router'
|
|
33
|
+
import { mergeOpenAPISpecs } from './api/openapi'
|
|
34
|
+
import { buildRealtimeApiRouter } from './api/realtime-router'
|
|
35
|
+
import { buildApiRouter } from './api/router'
|
|
36
|
+
import { buildStorageApiRouter } from './api/storage-router'
|
|
22
37
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
} from './access'
|
|
38
|
+
buildApiRegistry,
|
|
39
|
+
normalizeForeignOpenAPISpec,
|
|
40
|
+
} from './api/registry'
|
|
27
41
|
import {
|
|
28
42
|
createAuth,
|
|
29
43
|
toAuthSessionResolver,
|
|
@@ -31,7 +45,6 @@ import {
|
|
|
31
45
|
} from './auth'
|
|
32
46
|
import { resolveConfig, type BunderstackConfig } from './config'
|
|
33
47
|
import { resolveRealtimeRedisUrl } from './config'
|
|
34
|
-
import { buildCrudRouter } from './crud'
|
|
35
48
|
import { createDb } from './db'
|
|
36
49
|
import { detectDialect } from './dialect'
|
|
37
50
|
import { createEmail, emailProviderTag, type EmailFacade } from './email'
|
|
@@ -56,19 +69,18 @@ import {
|
|
|
56
69
|
type RealtimeFacade,
|
|
57
70
|
type RealtimeTransport,
|
|
58
71
|
} from './realtime/facade'
|
|
59
|
-
import {
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
import {
|
|
73
|
+
createMemoryRealtimePublisher,
|
|
74
|
+
createRedisRealtimePublisher,
|
|
75
|
+
} from './realtime/publisher'
|
|
62
76
|
import { deleteFileWithDerivatives } from './storage/delete'
|
|
63
77
|
import { deleteFileMetaRow, insertReadyFile } from './storage/file-meta'
|
|
64
78
|
import { createBucketStorages } from './storage/registry'
|
|
65
|
-
import {
|
|
79
|
+
import { createStorageOperations } from './storage/operations'
|
|
66
80
|
import { sweepOrphans } from './storage/sweep'
|
|
67
|
-
import { createTRPC, type BunderstackTRPC } from './trpc'
|
|
68
81
|
|
|
69
82
|
export type AuthInstance = ReturnType<typeof createAuth>
|
|
70
83
|
|
|
71
|
-
|
|
72
84
|
function waitForWorkerShutdown(
|
|
73
85
|
signal: AbortSignal,
|
|
74
86
|
installProcessListeners: boolean,
|
|
@@ -153,16 +165,13 @@ export type BunderstackApp<
|
|
|
153
165
|
TAccess extends Record<string, TableAccessInput> | undefined = undefined,
|
|
154
166
|
TBuckets extends string = string,
|
|
155
167
|
TEnv extends EnvConfigInput | undefined = undefined,
|
|
156
|
-
TRouter = undefined,
|
|
157
168
|
TJobsDefs extends JobsDefs | undefined = undefined,
|
|
169
|
+
TCustomApiRouter extends AnyORPCRouter | undefined = undefined,
|
|
158
170
|
> = {
|
|
159
171
|
handler: (req: Request) => Promise<Response>
|
|
160
172
|
db: DbFor<TSchema>
|
|
161
173
|
auth: AuthInstance
|
|
162
174
|
storage: StorageFacade
|
|
163
|
-
router: HonoType
|
|
164
|
-
/** Raw tRPC router when the config declared one — escape hatch. */
|
|
165
|
-
trpcRouter?: AnyRouter
|
|
166
175
|
/** Validated env: bunderstack's base vars plus the config's `env` extension. */
|
|
167
176
|
env: ValidatedEnv<TEnv>
|
|
168
177
|
/** Email facade; always present — send() throws when email isn't configured. */
|
|
@@ -191,129 +200,40 @@ export type BunderstackApp<
|
|
|
191
200
|
schema: TSchema
|
|
192
201
|
access: TAccess
|
|
193
202
|
buckets: TBuckets
|
|
194
|
-
|
|
203
|
+
api: MergeApiRouterTypes<
|
|
204
|
+
UnifiedApiRouter<CrudApiRouterFor<TSchema, TAccess>, TCustomApiRouter>,
|
|
205
|
+
RealtimeApiRouter
|
|
206
|
+
>
|
|
195
207
|
}
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
// Overloads: the builder-callback form and the prebuilt-router/none form are
|
|
199
|
-
// separate signatures so the callback's `t` parameter gets contextual typing
|
|
200
|
-
// and the router type lands on `$inferClient` without conditional-type
|
|
201
|
-
// inference (which breaks under contextual return types). `jobs` needs the
|
|
202
|
-
// same split against BOTH trpc forms — a union parameter type (`TJobsDefs |
|
|
203
|
-
// (callback => TJobsDefs)`) defeats inference (TS widens TJobsDefs to its
|
|
204
|
-
// constraint when a function literal could match either union arm) — hence
|
|
205
|
-
// four overloads covering the trpc × jobs option cross product.
|
|
206
|
-
export function createBunderstack<
|
|
207
|
-
TSchema extends Record<string, unknown>,
|
|
208
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
209
|
-
undefined,
|
|
210
|
-
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
211
|
-
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
212
|
-
TRouter extends AnyRouter = AnyRouter,
|
|
213
|
-
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
214
|
-
>(
|
|
215
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
216
|
-
/** Builder callback receiving the pre-wired `t` instance. */
|
|
217
|
-
trpc: (t: BunderstackTRPC<TSchema, ValidatedEnv<TEnv>>) => TRouter
|
|
218
|
-
/** Builder callback receiving the pre-wired `j` instance. */
|
|
219
|
-
jobs: (j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => TJobsDefs
|
|
220
|
-
},
|
|
221
|
-
): Promise<
|
|
222
|
-
BunderstackApp<
|
|
223
|
-
TSchema,
|
|
224
|
-
TAccess,
|
|
225
|
-
BucketNamesOf<TStorage>,
|
|
226
|
-
TEnv,
|
|
227
|
-
TRouter,
|
|
228
|
-
TJobsDefs
|
|
229
|
-
>
|
|
230
|
-
>
|
|
231
210
|
export function createBunderstack<
|
|
232
211
|
TSchema extends Record<string, unknown>,
|
|
233
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
234
|
-
undefined,
|
|
212
|
+
const TAccess extends Record<string, TableAccessInput> | undefined = undefined,
|
|
235
213
|
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
236
214
|
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
237
|
-
TRouter extends AnyRouter = AnyRouter,
|
|
238
215
|
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
216
|
+
TCustomApiRouter extends AnyORPCRouter | undefined = undefined,
|
|
239
217
|
>(
|
|
240
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
241
|
-
|
|
242
|
-
trpc: (t: BunderstackTRPC<TSchema, ValidatedEnv<TEnv>>) => TRouter
|
|
243
|
-
/** Prebuilt job definitions (escape hatch for multi-file setups). */
|
|
244
|
-
jobs?: TJobsDefs
|
|
218
|
+
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv, TCustomApiRouter> & {
|
|
219
|
+
jobs?: TJobsDefs | ((j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => TJobsDefs)
|
|
245
220
|
},
|
|
246
|
-
): Promise<
|
|
247
|
-
|
|
248
|
-
TSchema,
|
|
249
|
-
TAccess,
|
|
250
|
-
BucketNamesOf<TStorage>,
|
|
251
|
-
TEnv,
|
|
252
|
-
TRouter,
|
|
253
|
-
TJobsDefs
|
|
254
|
-
>
|
|
255
|
-
>
|
|
256
|
-
export function createBunderstack<
|
|
257
|
-
TSchema extends Record<string, unknown>,
|
|
258
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
259
|
-
undefined,
|
|
260
|
-
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
261
|
-
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
262
|
-
TRouter extends AnyRouter | undefined = undefined,
|
|
263
|
-
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
264
|
-
>(
|
|
265
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
266
|
-
/** Prebuilt tRPC router (escape hatch for multi-file setups). */
|
|
267
|
-
trpc?: TRouter
|
|
268
|
-
/** Builder callback receiving the pre-wired `j` instance. */
|
|
269
|
-
jobs: (j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => TJobsDefs
|
|
270
|
-
},
|
|
271
|
-
): Promise<
|
|
272
|
-
BunderstackApp<
|
|
273
|
-
TSchema,
|
|
274
|
-
TAccess,
|
|
275
|
-
BucketNamesOf<TStorage>,
|
|
276
|
-
TEnv,
|
|
277
|
-
TRouter,
|
|
278
|
-
TJobsDefs
|
|
279
|
-
>
|
|
280
|
-
>
|
|
281
|
-
export function createBunderstack<
|
|
221
|
+
): Promise<BunderstackApp<TSchema, TAccess, BucketNamesOf<TStorage>, TEnv, TJobsDefs, TCustomApiRouter>>
|
|
222
|
+
export async function createBunderstack<
|
|
282
223
|
TSchema extends Record<string, unknown>,
|
|
283
224
|
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
284
225
|
undefined,
|
|
285
226
|
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
286
227
|
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
287
|
-
|
|
288
|
-
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
228
|
+
TCustomApiRouter extends AnyORPCRouter | undefined = undefined,
|
|
289
229
|
>(
|
|
290
|
-
options: BunderstackConfig<
|
|
291
|
-
/** Prebuilt tRPC router (escape hatch for multi-file setups). */
|
|
292
|
-
trpc?: TRouter
|
|
293
|
-
/** Prebuilt job definitions (escape hatch for multi-file setups). */
|
|
294
|
-
jobs?: TJobsDefs
|
|
295
|
-
},
|
|
296
|
-
): Promise<
|
|
297
|
-
BunderstackApp<
|
|
230
|
+
options: BunderstackConfig<
|
|
298
231
|
TSchema,
|
|
299
232
|
TAccess,
|
|
300
|
-
|
|
233
|
+
TStorage,
|
|
301
234
|
TEnv,
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
>
|
|
305
|
-
>
|
|
306
|
-
export async function createBunderstack<
|
|
307
|
-
TSchema extends Record<string, unknown>,
|
|
308
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
309
|
-
undefined,
|
|
310
|
-
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
311
|
-
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
312
|
-
>(
|
|
313
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
314
|
-
trpc?:
|
|
315
|
-
| AnyRouter
|
|
316
|
-
| ((t: BunderstackTRPC<TSchema, ValidatedEnv<TEnv>>) => AnyRouter)
|
|
235
|
+
TCustomApiRouter
|
|
236
|
+
> & {
|
|
317
237
|
jobs?:
|
|
318
238
|
| JobsDefs
|
|
319
239
|
| ((j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => JobsDefs)
|
|
@@ -324,8 +244,8 @@ export async function createBunderstack<
|
|
|
324
244
|
TAccess,
|
|
325
245
|
BucketNamesOf<TStorage>,
|
|
326
246
|
TEnv,
|
|
327
|
-
|
|
328
|
-
|
|
247
|
+
JobsDefs | undefined,
|
|
248
|
+
TCustomApiRouter
|
|
329
249
|
>
|
|
330
250
|
> {
|
|
331
251
|
const dialect = detectDialect(options.schema)
|
|
@@ -336,7 +256,7 @@ export async function createBunderstack<
|
|
|
336
256
|
: undefined
|
|
337
257
|
if (jobsDefs) validateJobsDefs(jobsDefs)
|
|
338
258
|
// Env is validated FIRST: the app refuses to boot on missing/invalid vars,
|
|
339
|
-
// and everything downstream
|
|
259
|
+
// and everything downstream consumes the result.
|
|
340
260
|
const env = validateEnv(options.env, {
|
|
341
261
|
emailProvider: emailProviderTag(options.email),
|
|
342
262
|
defaultDatabaseUrl:
|
|
@@ -388,81 +308,52 @@ export async function createBunderstack<
|
|
|
388
308
|
typeof config.realtime === 'object'
|
|
389
309
|
? config.realtime.bufferSize
|
|
390
310
|
: undefined
|
|
311
|
+
const realtimeResumeSeconds =
|
|
312
|
+
typeof config.realtime === 'object'
|
|
313
|
+
? config.realtime.resumeSeconds
|
|
314
|
+
: undefined
|
|
391
315
|
const configuredRedisUrl = config.realtime
|
|
392
316
|
? resolveRealtimeRedisUrl(config.realtime, env)
|
|
393
317
|
: undefined
|
|
394
|
-
const configuredRealtimeTransport: RealtimeTransport = !config.realtime
|
|
395
|
-
? 'disabled'
|
|
396
|
-
: configuredRedisUrl
|
|
397
|
-
? 'redis'
|
|
398
|
-
: 'memory'
|
|
399
318
|
const redisUrl = introspect ? undefined : configuredRedisUrl
|
|
400
|
-
const
|
|
319
|
+
const publisher = config.realtime
|
|
401
320
|
? redisUrl
|
|
402
|
-
?
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
cmdClient.ltrim(key, start, stop),
|
|
420
|
-
lrange: (key: string, start: number, stop: number) =>
|
|
421
|
-
cmdClient.lrange(key, start, stop),
|
|
422
|
-
close: () => {
|
|
423
|
-
cmdClient.close()
|
|
424
|
-
subClient.close()
|
|
425
|
-
},
|
|
426
|
-
}
|
|
427
|
-
},
|
|
428
|
-
bufferSize: realtimeBufferSize,
|
|
429
|
-
})
|
|
430
|
-
: createRealtimeBroker({
|
|
431
|
-
access: resolvedAccess,
|
|
432
|
-
bufferSize: realtimeBufferSize,
|
|
321
|
+
? (() => {
|
|
322
|
+
const redis = new Bun.RedisClient(redisUrl)
|
|
323
|
+
const subscriber = redis.duplicate()
|
|
324
|
+
lifecycle.add(async () => {
|
|
325
|
+
redis.close()
|
|
326
|
+
;(await subscriber).close()
|
|
327
|
+
})
|
|
328
|
+
return createRedisRealtimePublisher(redis, subscriber, {
|
|
329
|
+
prefix:
|
|
330
|
+
process.env.BUNDERSTACK_REALTIME_PREFIX ?? 'bunderstack:',
|
|
331
|
+
maxBufferedEvents: realtimeBufferSize,
|
|
332
|
+
resumeSeconds: realtimeResumeSeconds,
|
|
333
|
+
})
|
|
334
|
+
})()
|
|
335
|
+
: createMemoryRealtimePublisher({
|
|
336
|
+
maxBufferedEvents: realtimeBufferSize,
|
|
337
|
+
resumeSeconds: realtimeResumeSeconds,
|
|
433
338
|
})
|
|
434
339
|
: undefined
|
|
435
|
-
const runtimeRealtimeTransport: RealtimeTransport = !
|
|
340
|
+
const runtimeRealtimeTransport: RealtimeTransport = !publisher
|
|
436
341
|
? 'disabled'
|
|
437
342
|
: redisUrl
|
|
438
343
|
? 'redis'
|
|
439
344
|
: 'memory'
|
|
440
345
|
const realtime = createRealtimeFacade<TSchema>(
|
|
441
|
-
|
|
346
|
+
publisher,
|
|
442
347
|
runtimeRealtimeTransport,
|
|
348
|
+
options.schema,
|
|
443
349
|
)
|
|
444
|
-
const crudRouter = buildCrudRouter(options.schema, userDb, {
|
|
445
|
-
auth: authResolver,
|
|
446
|
-
access: resolvedAccess,
|
|
447
|
-
idempotency: options.idempotency,
|
|
448
|
-
realtime,
|
|
449
|
-
})
|
|
450
|
-
const realtimeRouter = broker
|
|
451
|
-
? buildRealtimeRouter(broker, {
|
|
452
|
-
auth: authResolver,
|
|
453
|
-
keepaliveMs:
|
|
454
|
-
typeof config.realtime === 'object'
|
|
455
|
-
? config.realtime.keepaliveMs
|
|
456
|
-
: undefined,
|
|
457
|
-
})
|
|
458
|
-
: undefined
|
|
459
350
|
const registry = createBucketStorages(config.storage)
|
|
460
|
-
|
|
461
|
-
const storageRouter = buildBucketStorageRouter({
|
|
351
|
+
const storageOperations = createStorageOperations({
|
|
462
352
|
registry,
|
|
463
353
|
db,
|
|
464
|
-
auth: authResolver,
|
|
465
354
|
})
|
|
355
|
+
const storageApiRouter = buildStorageApiRouter(registry, storageOperations)
|
|
356
|
+
const realtimeApiRouter = buildRealtimeApiRouter(publisher, resolvedAccess)
|
|
466
357
|
const storage: StorageFacade = {
|
|
467
358
|
async delete(fileId) {
|
|
468
359
|
const bucketName = fileId.split('/')[0] ?? ''
|
|
@@ -517,7 +408,7 @@ export async function createBunderstack<
|
|
|
517
408
|
// ordinary cron now, so it inherits retries, timeout and onFailed.
|
|
518
409
|
const resolvedDefs: JobsDefs | undefined = storageConfigured
|
|
519
410
|
? {
|
|
520
|
-
...
|
|
411
|
+
...jobsDefs,
|
|
521
412
|
'bunderstack:storage-sweep': {
|
|
522
413
|
kind: 'cron',
|
|
523
414
|
schedule: '0 4 * * *',
|
|
@@ -546,7 +437,9 @@ export async function createBunderstack<
|
|
|
546
437
|
return result
|
|
547
438
|
},
|
|
548
439
|
tick(now?: number) {
|
|
549
|
-
return jobRunner
|
|
440
|
+
return jobRunner
|
|
441
|
+
? jobRunner.tick(now)
|
|
442
|
+
: Promise.resolve({ claimed: 0, ran: 0, failed: 0 })
|
|
550
443
|
},
|
|
551
444
|
}
|
|
552
445
|
if (jobRunner) jobRunner.setJobsFacade(jobs)
|
|
@@ -605,58 +498,155 @@ export async function createBunderstack<
|
|
|
605
498
|
await lifecycle.close()
|
|
606
499
|
}
|
|
607
500
|
}
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
req,
|
|
617
|
-
router: trpcRouter,
|
|
618
|
-
createContext: async () => ({
|
|
619
|
-
db: userDb,
|
|
620
|
-
user: await resolveAccessUser(authResolver, req.headers),
|
|
621
|
-
env,
|
|
622
|
-
email,
|
|
623
|
-
jobs,
|
|
624
|
-
realtime,
|
|
625
|
-
storage,
|
|
626
|
-
req,
|
|
627
|
-
}),
|
|
628
|
-
})
|
|
501
|
+
const crudApiRouter = buildCrudApiRouter(options.schema, userDb, {
|
|
502
|
+
access: resolvedAccess,
|
|
503
|
+
idempotency: options.idempotency,
|
|
504
|
+
realtime,
|
|
505
|
+
})
|
|
506
|
+
|
|
507
|
+
const customApiRouter = options.api
|
|
508
|
+
? options.api(createApiBuilder<TSchema, ValidatedEnv<TEnv>>())
|
|
629
509
|
: undefined
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
510
|
+
|
|
511
|
+
const nativeRouter = buildApiRouter({
|
|
512
|
+
crud: crudApiRouter as Record<string, unknown>,
|
|
513
|
+
storage: storageApiRouter as Record<string, unknown>,
|
|
514
|
+
realtime: realtimeApiRouter as Record<string, unknown> | undefined,
|
|
515
|
+
custom: customApiRouter as Record<string, unknown> | undefined,
|
|
516
|
+
}) as any
|
|
517
|
+
|
|
518
|
+
const authOpenAPISpecRaw =
|
|
519
|
+
options.openapi &&
|
|
520
|
+
auth.api &&
|
|
521
|
+
'generateOpenAPISchema' in auth.api &&
|
|
522
|
+
typeof auth.api.generateOpenAPISchema === 'function'
|
|
523
|
+
? await auth.api.generateOpenAPISchema()
|
|
524
|
+
: undefined
|
|
525
|
+
|
|
526
|
+
const authOpenAPISpec = authOpenAPISpecRaw
|
|
527
|
+
? normalizeForeignOpenAPISpec(authOpenAPISpecRaw, {
|
|
528
|
+
prefix: '/api/auth',
|
|
529
|
+
source: 'auth',
|
|
530
|
+
})
|
|
531
|
+
: undefined
|
|
532
|
+
|
|
533
|
+
await buildApiRegistry({
|
|
534
|
+
nativeRouter,
|
|
535
|
+
foreignSpecs: authOpenAPISpec ? [authOpenAPISpec] : [],
|
|
536
|
+
reservedCoreHandles: new Set([
|
|
537
|
+
'health',
|
|
538
|
+
...(publisher ? ['realtime.changes'] : []),
|
|
539
|
+
...[...registry.keys()].flatMap((name) =>
|
|
540
|
+
['prepareUpload', 'upload', 'confirmUpload', 'download', 'delete'].map(
|
|
541
|
+
(operation) => `files.${name}.${operation}`,
|
|
542
|
+
),
|
|
543
|
+
),
|
|
544
|
+
]),
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
const valibotConverter = new ValibotToJsonSchemaConverter()
|
|
548
|
+
|
|
549
|
+
const combinedOpenAPISpec = options.openapi
|
|
550
|
+
? mergeOpenAPISpecs({
|
|
551
|
+
nativeSpec: await new OpenAPIGenerator({
|
|
552
|
+
converters: [
|
|
553
|
+
valibotConverter,
|
|
554
|
+
{
|
|
555
|
+
condition: (schema: any) =>
|
|
556
|
+
Boolean(
|
|
557
|
+
schema?.['~standard'] &&
|
|
558
|
+
!schema['~standard'].jsonSchema,
|
|
559
|
+
),
|
|
560
|
+
convert: (schema: any) => {
|
|
561
|
+
const vendor = schema?.['~standard']?.vendor ?? 'unknown'
|
|
562
|
+
throw new OpenAPIGeneratorError(
|
|
563
|
+
`No JSON Schema converter is configured for Standard Schema vendor "${vendor}"`,
|
|
564
|
+
)
|
|
565
|
+
},
|
|
566
|
+
},
|
|
567
|
+
],
|
|
568
|
+
}).generate(nativeRouter),
|
|
569
|
+
authSpec: authOpenAPISpec,
|
|
570
|
+
})
|
|
652
571
|
: undefined
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
572
|
+
|
|
573
|
+
const openapiHandler = new OpenAPIHandler(nativeRouter, {
|
|
574
|
+
// Query strings and form bodies are strings; this coerces them to the
|
|
575
|
+
// types each procedure's input schema declares, so schemas stay honest
|
|
576
|
+
// (`v.number()`, not a string-union pipe) and REST matches RPC.
|
|
577
|
+
plugins: [
|
|
578
|
+
new SmartCoercionHandlerPlugin({ converters: [valibotConverter] }),
|
|
579
|
+
],
|
|
580
|
+
customErrorResponseBodyEncoder: (error: any) => ({
|
|
581
|
+
error: error.message,
|
|
582
|
+
code: error.data?.code ?? error.code,
|
|
583
|
+
// oRPC reports schema failures as `data.issues`; forwarding them tells
|
|
584
|
+
// the client which field was rejected instead of just "invalid".
|
|
585
|
+
...(error.data?.details !== undefined
|
|
586
|
+
? { details: error.data.details }
|
|
587
|
+
: error.data?.issues !== undefined
|
|
588
|
+
? { details: error.data.issues }
|
|
589
|
+
: {}),
|
|
590
|
+
}),
|
|
591
|
+
fetchInterceptors: [
|
|
592
|
+
async (options) => {
|
|
593
|
+
const res = await options.next()
|
|
594
|
+
if (res.matched && options.context.resHeaders) {
|
|
595
|
+
options.context.resHeaders.forEach((v: string, k: string) =>
|
|
596
|
+
res.response.headers.set(k, v),
|
|
597
|
+
)
|
|
598
|
+
}
|
|
599
|
+
return res
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
})
|
|
603
|
+
const rpcHandler = new RPCHandler(nativeRouter)
|
|
604
|
+
|
|
605
|
+
const apiHandler = async (req: Request): Promise<Response | null> => {
|
|
606
|
+
const urlString = typeof req === 'string' ? (req as string) : req.url
|
|
607
|
+
if (!urlString) return null
|
|
608
|
+
const url = new URL(urlString, 'http://localhost')
|
|
609
|
+
if (
|
|
610
|
+
combinedOpenAPISpec &&
|
|
611
|
+
url.pathname === '/api/openapi.json' &&
|
|
612
|
+
req.method === 'GET'
|
|
613
|
+
) {
|
|
614
|
+
return new Response(JSON.stringify(combinedOpenAPISpec), {
|
|
615
|
+
headers: { 'Content-Type': 'application/json' },
|
|
616
|
+
})
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const apiCtx = createApiContext(
|
|
620
|
+
{
|
|
621
|
+
db: userDb,
|
|
622
|
+
env,
|
|
623
|
+
storage,
|
|
624
|
+
email,
|
|
625
|
+
jobs,
|
|
626
|
+
realtime,
|
|
627
|
+
auth,
|
|
628
|
+
authResolver,
|
|
629
|
+
},
|
|
630
|
+
req,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
if (url.pathname.startsWith('/api/rpc')) {
|
|
634
|
+
const res = await rpcHandler.handle(req, {
|
|
635
|
+
prefix: '/api/rpc',
|
|
636
|
+
context: apiCtx,
|
|
637
|
+
})
|
|
638
|
+
if (res.matched) return res.response
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const openapiRes = await openapiHandler.handle(req, { context: apiCtx })
|
|
642
|
+
if (openapiRes.matched) return openapiRes.response
|
|
643
|
+
|
|
644
|
+
return null
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const handler = buildHandler({
|
|
656
648
|
authHandler: (req) => auth.handler(req),
|
|
657
|
-
|
|
658
|
-
realtimeRouter,
|
|
659
|
-
trpcHandler,
|
|
649
|
+
apiHandler,
|
|
660
650
|
rateLimit: options.rateLimit,
|
|
661
651
|
})
|
|
662
652
|
|
|
@@ -678,15 +668,14 @@ export async function createBunderstack<
|
|
|
678
668
|
TAccess,
|
|
679
669
|
BucketNamesOf<TStorage>,
|
|
680
670
|
TEnv,
|
|
681
|
-
|
|
682
|
-
|
|
671
|
+
JobsDefs | undefined,
|
|
672
|
+
TCustomApiRouter
|
|
683
673
|
> = {
|
|
684
674
|
handler,
|
|
685
675
|
// Internal tables live on the runtime db but stay out of the public type.
|
|
686
676
|
db: userDb,
|
|
687
677
|
auth,
|
|
688
678
|
storage,
|
|
689
|
-
router,
|
|
690
679
|
env,
|
|
691
680
|
email,
|
|
692
681
|
realtime,
|
|
@@ -702,7 +691,6 @@ export async function createBunderstack<
|
|
|
702
691
|
return lifecycle.status
|
|
703
692
|
},
|
|
704
693
|
signal: lifecycle.signal,
|
|
705
|
-
trpcRouter,
|
|
706
694
|
manifest: buildManifest({
|
|
707
695
|
schema: options.schema,
|
|
708
696
|
dialect,
|
|
@@ -743,6 +731,8 @@ export async function createBunderstack<
|
|
|
743
731
|
}
|
|
744
732
|
|
|
745
733
|
export { MAX_LIST_LIMIT } from './list-query'
|
|
734
|
+
export { BunderstackError } from './errors'
|
|
735
|
+
export type { BunderstackErrorCode } from './errors'
|
|
746
736
|
export { resolveConfig } from './config'
|
|
747
737
|
export type {
|
|
748
738
|
BetterAuthConfig,
|
|
@@ -760,8 +750,6 @@ export type {
|
|
|
760
750
|
EmailConfigInput,
|
|
761
751
|
EmailFacade,
|
|
762
752
|
} from './email'
|
|
763
|
-
export { createTRPC } from './trpc'
|
|
764
|
-
export type { BunderstackTRPC, TRPCContext } from './trpc'
|
|
765
753
|
export { createJobsBuilder } from './jobs/index'
|
|
766
754
|
export type {
|
|
767
755
|
BunderstackJobContext,
|
|
@@ -817,7 +805,7 @@ export type {
|
|
|
817
805
|
export type { TransformSpec } from './storage/thumbnails'
|
|
818
806
|
export { mockAuthSession } from './testing'
|
|
819
807
|
|
|
820
|
-
export type { RealtimeAction } from './realtime/
|
|
808
|
+
export type { RealtimeAction } from './realtime/publisher'
|
|
821
809
|
export { createRealtimeFacade } from './realtime/facade'
|
|
822
810
|
export type {
|
|
823
811
|
RealtimeFacade,
|
|
@@ -825,9 +813,22 @@ export type {
|
|
|
825
813
|
SchemaTable,
|
|
826
814
|
} from './realtime/facade'
|
|
827
815
|
|
|
816
|
+
export { createApiBuilder } from './api/builder'
|
|
817
|
+
export type { BunderstackApiBuilder, ApiFactory } from './api/builder'
|
|
818
|
+
// Needed to declare shared middleware over the app's context, e.g.
|
|
819
|
+
// `os.$context<ApiContext<typeof schema>>().middleware(...)`.
|
|
820
|
+
export type { ApiContext } from './api/context'
|
|
828
821
|
export type {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
822
|
+
CrudApiRouterFor,
|
|
823
|
+
ExposedApiTables,
|
|
824
|
+
MergeApiRouterTypes,
|
|
825
|
+
UnifiedApiRouter,
|
|
826
|
+
} from './api/types'
|
|
827
|
+
export type { TableCrudProcedures } from './api/crud-router'
|
|
828
|
+
export {
|
|
829
|
+
buildApiRegistry,
|
|
830
|
+
mergeApiRoutersStrict,
|
|
831
|
+
normalizeApiPath,
|
|
832
|
+
normalizeForeignOpenAPISpec,
|
|
833
|
+
} from './api/registry'
|
|
834
|
+
export { mergeOpenAPISpecs } from './api/openapi'
|