bunderstack 0.15.2 → 0.17.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -138
- package/package.json +22 -14
- package/src/access.ts +24 -1
- package/src/api/api-types.types.ts +106 -0
- package/src/api/builder.ts +52 -0
- package/src/api/context.ts +83 -0
- package/src/api/crud-router.ts +321 -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 +84 -0
- package/src/auth.ts +5 -0
- package/src/blueprint.ts +88 -105
- package/src/config.ts +73 -77
- package/src/cron.ts +2 -1
- package/src/crud-operations.ts +488 -0
- package/src/dialect.ts +1 -1
- package/src/env.ts +28 -21
- package/src/errors.ts +90 -23
- package/src/handler.ts +16 -44
- package/src/index.ts +283 -294
- package/src/internal-tables-pg.ts +1 -17
- package/src/internal-tables.ts +0 -31
- package/src/jobs/define.ts +75 -21
- package/src/jobs/index.ts +3 -9
- package/src/jobs/queue.ts +14 -6
- package/src/jobs/slots.ts +52 -0
- package/src/jobs/worker.ts +142 -42
- package/src/manifest.ts +84 -93
- package/src/realtime/facade.ts +16 -13
- package/src/realtime/filter.ts +77 -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 -408
- package/src/jobs/cron-auth.ts +0 -28
- package/src/jobs/cron-router.ts +0 -135
- package/src/jobs/cron-runner.ts +0 -224
- package/src/jobs/local-cron.ts +0 -78
- package/src/realtime/index.ts +0 -250
- package/src/realtime/redis.ts +0 -228
- package/src/storage/router.ts +0 -531
- package/src/trpc.ts +0 -57
package/src/index.ts
CHANGED
|
@@ -1,25 +1,45 @@
|
|
|
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 {
|
|
4
|
+
import { OpenAPIGenerator, OpenAPIGeneratorError } from '@orpc/openapi'
|
|
5
|
+
import { OpenAPIHandler } from '@orpc/openapi/fetch'
|
|
6
|
+
import { RPCHandler } from '@orpc/server/fetch'
|
|
7
|
+
import { ValibotToJsonSchemaConverter } from '@orpc/valibot'
|
|
6
8
|
|
|
7
9
|
import type { TableAccessInput } from './access'
|
|
10
|
+
import type {
|
|
11
|
+
CrudApiRouterFor,
|
|
12
|
+
ExposedApiTables,
|
|
13
|
+
MergeApiRouterTypes,
|
|
14
|
+
UnifiedApiRouter,
|
|
15
|
+
} from './api/types'
|
|
16
|
+
import type { RealtimeApiRouter } from './api/realtime-router'
|
|
8
17
|
import type { DbFor } from './db'
|
|
9
18
|
import type {
|
|
10
19
|
BunderstackJobsBuilder,
|
|
11
20
|
EnqueueOptions,
|
|
12
21
|
JobsDefs,
|
|
13
22
|
JobsFacade,
|
|
14
|
-
LocalCronScheduler,
|
|
15
|
-
LocalCronSchedulerOptions,
|
|
16
23
|
StartWorkerOptions,
|
|
17
24
|
WorkerHandle,
|
|
18
25
|
} from './jobs/index'
|
|
19
26
|
import type { StorageConfigInput } from './storage/buckets'
|
|
20
27
|
import type { StorageAdapter } from './storage/index'
|
|
21
28
|
|
|
22
|
-
import {
|
|
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'
|
|
37
|
+
import {
|
|
38
|
+
buildApiRegistry,
|
|
39
|
+
mergeApiRoutersStrict,
|
|
40
|
+
normalizeApiPath,
|
|
41
|
+
normalizeForeignOpenAPISpec,
|
|
42
|
+
} from './api/registry'
|
|
23
43
|
import {
|
|
24
44
|
createAuth,
|
|
25
45
|
toAuthSessionResolver,
|
|
@@ -27,20 +47,17 @@ import {
|
|
|
27
47
|
} from './auth'
|
|
28
48
|
import { resolveConfig, type BunderstackConfig } from './config'
|
|
29
49
|
import { resolveRealtimeRedisUrl } from './config'
|
|
30
|
-
import { buildCrudRouter } from './crud'
|
|
31
50
|
import { createDb } from './db'
|
|
32
51
|
import { detectDialect } from './dialect'
|
|
33
52
|
import { createEmail, emailProviderTag, type EmailFacade } from './email'
|
|
34
53
|
import { validateEnv, type EnvConfigInput, type ValidatedEnv } from './env'
|
|
54
|
+
import { BUNDERSTACK_ERROR_STATUS_MAP } from './errors'
|
|
35
55
|
import { buildHandler } from './handler'
|
|
36
56
|
import { withInternalTables } from './internal-tables'
|
|
37
57
|
import {
|
|
38
58
|
createJobsBuilder,
|
|
39
59
|
createJobRunner,
|
|
40
|
-
buildCronRouter,
|
|
41
60
|
enqueueJob,
|
|
42
|
-
runCronSlot,
|
|
43
|
-
startLocalCronScheduler,
|
|
44
61
|
startJobWorker,
|
|
45
62
|
validateJobsDefs,
|
|
46
63
|
} from './jobs/index'
|
|
@@ -55,16 +72,17 @@ import {
|
|
|
55
72
|
type RealtimeFacade,
|
|
56
73
|
type RealtimeTransport,
|
|
57
74
|
} from './realtime/facade'
|
|
58
|
-
import {
|
|
59
|
-
|
|
75
|
+
import {
|
|
76
|
+
createMemoryRealtimePublisher,
|
|
77
|
+
createRedisRealtimePublisher,
|
|
78
|
+
} from './realtime/publisher'
|
|
60
79
|
import { deleteFileWithDerivatives } from './storage/delete'
|
|
61
80
|
import { deleteFileMetaRow, insertReadyFile } from './storage/file-meta'
|
|
62
81
|
import { createBucketStorages } from './storage/registry'
|
|
63
|
-
import {
|
|
82
|
+
import { createStorageOperations } from './storage/operations'
|
|
64
83
|
import { sweepOrphans } from './storage/sweep'
|
|
65
|
-
import { createTRPC, type BunderstackTRPC } from './trpc'
|
|
66
84
|
|
|
67
|
-
type AuthInstance = ReturnType<typeof createAuth>
|
|
85
|
+
export type AuthInstance = ReturnType<typeof createAuth>
|
|
68
86
|
|
|
69
87
|
function waitForWorkerShutdown(
|
|
70
88
|
signal: AbortSignal,
|
|
@@ -138,11 +156,6 @@ export type AppRunWorkerOptions = AppStartWorkerOptions & {
|
|
|
138
156
|
*/
|
|
139
157
|
allowProcessLocalRealtime?: boolean
|
|
140
158
|
}
|
|
141
|
-
export type AppStartCronSchedulerOptions = Pick<
|
|
142
|
-
LocalCronSchedulerOptions,
|
|
143
|
-
'onError'
|
|
144
|
-
>
|
|
145
|
-
|
|
146
159
|
/** Bucket names declared in a storage config; `string` when unknowable. */
|
|
147
160
|
export type BucketNamesOf<TStorage> = TStorage extends {
|
|
148
161
|
buckets: infer B extends Record<string, unknown>
|
|
@@ -155,16 +168,13 @@ export type BunderstackApp<
|
|
|
155
168
|
TAccess extends Record<string, TableAccessInput> | undefined = undefined,
|
|
156
169
|
TBuckets extends string = string,
|
|
157
170
|
TEnv extends EnvConfigInput | undefined = undefined,
|
|
158
|
-
TRouter = undefined,
|
|
159
171
|
TJobsDefs extends JobsDefs | undefined = undefined,
|
|
172
|
+
TCustomApiRouter extends AnyORPCRouter | undefined = undefined,
|
|
160
173
|
> = {
|
|
161
174
|
handler: (req: Request) => Promise<Response>
|
|
162
175
|
db: DbFor<TSchema>
|
|
163
176
|
auth: AuthInstance
|
|
164
177
|
storage: StorageFacade
|
|
165
|
-
router: HonoType
|
|
166
|
-
/** Raw tRPC router when the config declared one — escape hatch. */
|
|
167
|
-
trpcRouter?: AnyRouter
|
|
168
178
|
/** Validated env: bunderstack's base vars plus the config's `env` extension. */
|
|
169
179
|
env: ValidatedEnv<TEnv>
|
|
170
180
|
/** Email facade; always present — send() throws when email isn't configured. */
|
|
@@ -178,11 +188,9 @@ export type BunderstackApp<
|
|
|
178
188
|
startWorker(options?: AppStartWorkerOptions): Promise<WorkerHandle>
|
|
179
189
|
/** Run a queue worker until aborted, then close the application. */
|
|
180
190
|
runWorker(options?: AppRunWorkerOptions): Promise<void>
|
|
181
|
-
/** Start local delivery for declared cron tasks (for development only). */
|
|
182
|
-
startCronScheduler(
|
|
183
|
-
options?: AppStartCronSchedulerOptions,
|
|
184
|
-
): Promise<LocalCronScheduler>
|
|
185
191
|
close(): Promise<void>
|
|
192
|
+
/** True when this process is running the background tick loop. */
|
|
193
|
+
readonly backgroundRunning: boolean
|
|
186
194
|
readonly status: LifecycleStatus
|
|
187
195
|
readonly signal: AbortSignal
|
|
188
196
|
/** Deploy-time introspection: what this app needs provisioned. */
|
|
@@ -195,129 +203,40 @@ export type BunderstackApp<
|
|
|
195
203
|
schema: TSchema
|
|
196
204
|
access: TAccess
|
|
197
205
|
buckets: TBuckets
|
|
198
|
-
|
|
206
|
+
api: MergeApiRouterTypes<
|
|
207
|
+
UnifiedApiRouter<CrudApiRouterFor<TSchema, TAccess>, TCustomApiRouter>,
|
|
208
|
+
RealtimeApiRouter
|
|
209
|
+
>
|
|
199
210
|
}
|
|
200
211
|
}
|
|
201
212
|
|
|
202
|
-
// Overloads: the builder-callback form and the prebuilt-router/none form are
|
|
203
|
-
// separate signatures so the callback's `t` parameter gets contextual typing
|
|
204
|
-
// and the router type lands on `$inferClient` without conditional-type
|
|
205
|
-
// inference (which breaks under contextual return types). `jobs` needs the
|
|
206
|
-
// same split against BOTH trpc forms — a union parameter type (`TJobsDefs |
|
|
207
|
-
// (callback => TJobsDefs)`) defeats inference (TS widens TJobsDefs to its
|
|
208
|
-
// constraint when a function literal could match either union arm) — hence
|
|
209
|
-
// four overloads covering the trpc × jobs option cross product.
|
|
210
213
|
export function createBunderstack<
|
|
211
214
|
TSchema extends Record<string, unknown>,
|
|
212
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
213
|
-
undefined,
|
|
215
|
+
const TAccess extends Record<string, TableAccessInput> | undefined = undefined,
|
|
214
216
|
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
215
217
|
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
216
|
-
TRouter extends AnyRouter = AnyRouter,
|
|
217
218
|
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
219
|
+
TCustomApiRouter extends AnyORPCRouter | undefined = undefined,
|
|
218
220
|
>(
|
|
219
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
220
|
-
|
|
221
|
-
trpc: (t: BunderstackTRPC<TSchema, ValidatedEnv<TEnv>>) => TRouter
|
|
222
|
-
/** Builder callback receiving the pre-wired `j` instance. */
|
|
223
|
-
jobs: (j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => TJobsDefs
|
|
221
|
+
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv, TCustomApiRouter> & {
|
|
222
|
+
jobs?: TJobsDefs | ((j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => TJobsDefs)
|
|
224
223
|
},
|
|
225
|
-
): Promise<
|
|
226
|
-
|
|
227
|
-
TSchema,
|
|
228
|
-
TAccess,
|
|
229
|
-
BucketNamesOf<TStorage>,
|
|
230
|
-
TEnv,
|
|
231
|
-
TRouter,
|
|
232
|
-
TJobsDefs
|
|
233
|
-
>
|
|
234
|
-
>
|
|
235
|
-
export function createBunderstack<
|
|
236
|
-
TSchema extends Record<string, unknown>,
|
|
237
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
238
|
-
undefined,
|
|
239
|
-
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
240
|
-
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
241
|
-
TRouter extends AnyRouter = AnyRouter,
|
|
242
|
-
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
243
|
-
>(
|
|
244
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
245
|
-
/** Builder callback receiving the pre-wired `t` instance. */
|
|
246
|
-
trpc: (t: BunderstackTRPC<TSchema, ValidatedEnv<TEnv>>) => TRouter
|
|
247
|
-
/** Prebuilt job definitions (escape hatch for multi-file setups). */
|
|
248
|
-
jobs?: TJobsDefs
|
|
249
|
-
},
|
|
250
|
-
): Promise<
|
|
251
|
-
BunderstackApp<
|
|
252
|
-
TSchema,
|
|
253
|
-
TAccess,
|
|
254
|
-
BucketNamesOf<TStorage>,
|
|
255
|
-
TEnv,
|
|
256
|
-
TRouter,
|
|
257
|
-
TJobsDefs
|
|
258
|
-
>
|
|
259
|
-
>
|
|
260
|
-
export function createBunderstack<
|
|
261
|
-
TSchema extends Record<string, unknown>,
|
|
262
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
263
|
-
undefined,
|
|
264
|
-
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
265
|
-
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
266
|
-
TRouter extends AnyRouter | undefined = undefined,
|
|
267
|
-
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
268
|
-
>(
|
|
269
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
270
|
-
/** Prebuilt tRPC router (escape hatch for multi-file setups). */
|
|
271
|
-
trpc?: TRouter
|
|
272
|
-
/** Builder callback receiving the pre-wired `j` instance. */
|
|
273
|
-
jobs: (j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => TJobsDefs
|
|
274
|
-
},
|
|
275
|
-
): Promise<
|
|
276
|
-
BunderstackApp<
|
|
277
|
-
TSchema,
|
|
278
|
-
TAccess,
|
|
279
|
-
BucketNamesOf<TStorage>,
|
|
280
|
-
TEnv,
|
|
281
|
-
TRouter,
|
|
282
|
-
TJobsDefs
|
|
283
|
-
>
|
|
284
|
-
>
|
|
285
|
-
export function createBunderstack<
|
|
224
|
+
): Promise<BunderstackApp<TSchema, TAccess, BucketNamesOf<TStorage>, TEnv, TJobsDefs, TCustomApiRouter>>
|
|
225
|
+
export async function createBunderstack<
|
|
286
226
|
TSchema extends Record<string, unknown>,
|
|
287
227
|
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
288
228
|
undefined,
|
|
289
229
|
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
290
230
|
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
291
|
-
|
|
292
|
-
const TJobsDefs extends JobsDefs | undefined = undefined,
|
|
231
|
+
TCustomApiRouter extends AnyORPCRouter | undefined = undefined,
|
|
293
232
|
>(
|
|
294
|
-
options: BunderstackConfig<
|
|
295
|
-
/** Prebuilt tRPC router (escape hatch for multi-file setups). */
|
|
296
|
-
trpc?: TRouter
|
|
297
|
-
/** Prebuilt job definitions (escape hatch for multi-file setups). */
|
|
298
|
-
jobs?: TJobsDefs
|
|
299
|
-
},
|
|
300
|
-
): Promise<
|
|
301
|
-
BunderstackApp<
|
|
233
|
+
options: BunderstackConfig<
|
|
302
234
|
TSchema,
|
|
303
235
|
TAccess,
|
|
304
|
-
|
|
236
|
+
TStorage,
|
|
305
237
|
TEnv,
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
>
|
|
309
|
-
>
|
|
310
|
-
export async function createBunderstack<
|
|
311
|
-
TSchema extends Record<string, unknown>,
|
|
312
|
-
const TAccess extends Record<string, TableAccessInput> | undefined =
|
|
313
|
-
undefined,
|
|
314
|
-
const TStorage extends StorageConfigInput | undefined = undefined,
|
|
315
|
-
const TEnv extends EnvConfigInput | undefined = undefined,
|
|
316
|
-
>(
|
|
317
|
-
options: BunderstackConfig<TSchema, TAccess, TStorage, TEnv> & {
|
|
318
|
-
trpc?:
|
|
319
|
-
| AnyRouter
|
|
320
|
-
| ((t: BunderstackTRPC<TSchema, ValidatedEnv<TEnv>>) => AnyRouter)
|
|
238
|
+
TCustomApiRouter
|
|
239
|
+
> & {
|
|
321
240
|
jobs?:
|
|
322
241
|
| JobsDefs
|
|
323
242
|
| ((j: BunderstackJobsBuilder<TSchema, ValidatedEnv<TEnv>>) => JobsDefs)
|
|
@@ -328,8 +247,8 @@ export async function createBunderstack<
|
|
|
328
247
|
TAccess,
|
|
329
248
|
BucketNamesOf<TStorage>,
|
|
330
249
|
TEnv,
|
|
331
|
-
|
|
332
|
-
|
|
250
|
+
JobsDefs | undefined,
|
|
251
|
+
TCustomApiRouter
|
|
333
252
|
>
|
|
334
253
|
> {
|
|
335
254
|
const dialect = detectDialect(options.schema)
|
|
@@ -340,16 +259,14 @@ export async function createBunderstack<
|
|
|
340
259
|
: undefined
|
|
341
260
|
if (jobsDefs) validateJobsDefs(jobsDefs)
|
|
342
261
|
// Env is validated FIRST: the app refuses to boot on missing/invalid vars,
|
|
343
|
-
// and everything downstream
|
|
262
|
+
// and everything downstream consumes the result.
|
|
344
263
|
const env = validateEnv(options.env, {
|
|
345
264
|
emailProvider: emailProviderTag(options.email),
|
|
346
265
|
defaultDatabaseUrl:
|
|
347
266
|
dialect === 'pg' ? 'file:./data.pglite' : 'file:./data.db',
|
|
348
|
-
|
|
349
|
-
// production app has scheduled delivery even without user-defined cron.
|
|
350
|
-
cronConfigured: true,
|
|
267
|
+
source: options.processEnv,
|
|
351
268
|
})
|
|
352
|
-
const config = resolveConfig(options, env)
|
|
269
|
+
const config = resolveConfig(options, env, options.processEnv)
|
|
353
270
|
// Adapters use Drizzle mocks during deployment introspection, so the database
|
|
354
271
|
// and Redis below never touch external services.
|
|
355
272
|
const introspect = process.env.BUNDERSTACK_INTROSPECT === '1'
|
|
@@ -394,81 +311,51 @@ export async function createBunderstack<
|
|
|
394
311
|
typeof config.realtime === 'object'
|
|
395
312
|
? config.realtime.bufferSize
|
|
396
313
|
: undefined
|
|
314
|
+
const realtimeResumeSeconds =
|
|
315
|
+
typeof config.realtime === 'object'
|
|
316
|
+
? config.realtime.resumeSeconds
|
|
317
|
+
: undefined
|
|
397
318
|
const configuredRedisUrl = config.realtime
|
|
398
319
|
? resolveRealtimeRedisUrl(config.realtime, env)
|
|
399
320
|
: undefined
|
|
400
|
-
const configuredRealtimeTransport: RealtimeTransport = !config.realtime
|
|
401
|
-
? 'disabled'
|
|
402
|
-
: configuredRedisUrl
|
|
403
|
-
? 'redis'
|
|
404
|
-
: 'memory'
|
|
405
321
|
const redisUrl = introspect ? undefined : configuredRedisUrl
|
|
406
|
-
const
|
|
322
|
+
const publisher = config.realtime
|
|
407
323
|
? redisUrl
|
|
408
|
-
?
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
cmdClient.ltrim(key, start, stop),
|
|
426
|
-
lrange: (key: string, start: number, stop: number) =>
|
|
427
|
-
cmdClient.lrange(key, start, stop),
|
|
428
|
-
close: () => {
|
|
429
|
-
cmdClient.close()
|
|
430
|
-
subClient.close()
|
|
431
|
-
},
|
|
432
|
-
}
|
|
433
|
-
},
|
|
434
|
-
bufferSize: realtimeBufferSize,
|
|
435
|
-
})
|
|
436
|
-
: createRealtimeBroker({
|
|
437
|
-
access: resolvedAccess,
|
|
438
|
-
bufferSize: realtimeBufferSize,
|
|
324
|
+
? (() => {
|
|
325
|
+
const redis = new Bun.RedisClient(redisUrl)
|
|
326
|
+
const subscriber = redis.duplicate()
|
|
327
|
+
lifecycle.add(async () => {
|
|
328
|
+
redis.close()
|
|
329
|
+
;(await subscriber).close()
|
|
330
|
+
})
|
|
331
|
+
return createRedisRealtimePublisher(redis, subscriber, {
|
|
332
|
+
prefix:
|
|
333
|
+
process.env.BUNDERSTACK_REALTIME_PREFIX ?? 'bunderstack:',
|
|
334
|
+
maxBufferedEvents: realtimeBufferSize,
|
|
335
|
+
resumeSeconds: realtimeResumeSeconds,
|
|
336
|
+
})
|
|
337
|
+
})()
|
|
338
|
+
: createMemoryRealtimePublisher({
|
|
339
|
+
maxBufferedEvents: realtimeBufferSize,
|
|
340
|
+
resumeSeconds: realtimeResumeSeconds,
|
|
439
341
|
})
|
|
440
342
|
: undefined
|
|
441
|
-
const runtimeRealtimeTransport: RealtimeTransport = !
|
|
343
|
+
const runtimeRealtimeTransport: RealtimeTransport = !publisher
|
|
442
344
|
? 'disabled'
|
|
443
345
|
: redisUrl
|
|
444
346
|
? 'redis'
|
|
445
347
|
: 'memory'
|
|
446
348
|
const realtime = createRealtimeFacade<TSchema>(
|
|
447
|
-
|
|
349
|
+
publisher,
|
|
448
350
|
runtimeRealtimeTransport,
|
|
449
351
|
)
|
|
450
|
-
const crudRouter = buildCrudRouter(options.schema, userDb, {
|
|
451
|
-
auth: authResolver,
|
|
452
|
-
access: resolvedAccess,
|
|
453
|
-
idempotency: options.idempotency,
|
|
454
|
-
realtime,
|
|
455
|
-
})
|
|
456
|
-
const realtimeRouter = broker
|
|
457
|
-
? buildRealtimeRouter(broker, {
|
|
458
|
-
auth: authResolver,
|
|
459
|
-
keepaliveMs:
|
|
460
|
-
typeof config.realtime === 'object'
|
|
461
|
-
? config.realtime.keepaliveMs
|
|
462
|
-
: undefined,
|
|
463
|
-
})
|
|
464
|
-
: undefined
|
|
465
352
|
const registry = createBucketStorages(config.storage)
|
|
466
|
-
|
|
467
|
-
const storageRouter = buildBucketStorageRouter({
|
|
353
|
+
const storageOperations = createStorageOperations({
|
|
468
354
|
registry,
|
|
469
355
|
db,
|
|
470
|
-
auth: authResolver,
|
|
471
356
|
})
|
|
357
|
+
const storageApiRouter = buildStorageApiRouter(registry, storageOperations)
|
|
358
|
+
const realtimeApiRouter = buildRealtimeApiRouter(publisher, resolvedAccess)
|
|
472
359
|
const storage: StorageFacade = {
|
|
473
360
|
async delete(fileId) {
|
|
474
361
|
const bucketName = fileId.split('/')[0] ?? ''
|
|
@@ -518,25 +405,43 @@ export async function createBunderstack<
|
|
|
518
405
|
})
|
|
519
406
|
},
|
|
520
407
|
}
|
|
521
|
-
const
|
|
408
|
+
const storageConfigured = Boolean(options.storage)
|
|
409
|
+
// The storage sweep used to be a hardcoded maintenance route. It is an
|
|
410
|
+
// ordinary cron now, so it inherits retries, timeout and onFailed.
|
|
411
|
+
const resolvedDefs: JobsDefs | undefined = storageConfigured
|
|
412
|
+
? {
|
|
413
|
+
...(jobsDefs ?? {}),
|
|
414
|
+
'bunderstack:storage-sweep': {
|
|
415
|
+
kind: 'cron',
|
|
416
|
+
schedule: '0 4 * * *',
|
|
417
|
+
handler: async () => {
|
|
418
|
+
await storage.sweep()
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
}
|
|
422
|
+
: jobsDefs
|
|
423
|
+
|
|
424
|
+
const jobRunner = resolvedDefs
|
|
522
425
|
? createJobRunner({
|
|
523
426
|
db,
|
|
524
|
-
defs:
|
|
427
|
+
defs: resolvedDefs,
|
|
525
428
|
ctx: { db: userDb, env, email, storage, realtime },
|
|
526
429
|
})
|
|
527
430
|
: undefined
|
|
528
431
|
const jobs = {
|
|
529
432
|
async enqueue(name: string, input?: unknown, opts?: EnqueueOptions) {
|
|
530
|
-
if (!
|
|
433
|
+
if (!resolvedDefs) {
|
|
531
434
|
throw new Error(
|
|
532
435
|
'[bunderstack] no jobs configured — add a `jobs` key to createBunderstack',
|
|
533
436
|
)
|
|
534
437
|
}
|
|
535
|
-
const result = await enqueueJob(db,
|
|
438
|
+
const result = await enqueueJob(db, resolvedDefs, name, input, opts)
|
|
536
439
|
return result
|
|
537
440
|
},
|
|
538
441
|
tick(now?: number) {
|
|
539
|
-
return jobRunner
|
|
442
|
+
return jobRunner
|
|
443
|
+
? jobRunner.tick(now)
|
|
444
|
+
: Promise.resolve({ claimed: 0, ran: 0, failed: 0 })
|
|
540
445
|
},
|
|
541
446
|
}
|
|
542
447
|
if (jobRunner) jobRunner.setJobsFacade(jobs)
|
|
@@ -558,54 +463,16 @@ export async function createBunderstack<
|
|
|
558
463
|
const handle = startJobWorker({
|
|
559
464
|
...options,
|
|
560
465
|
signal,
|
|
561
|
-
tick
|
|
466
|
+
// The runtime loop only cares that a tick completed; TickResult is for
|
|
467
|
+
// callers that invoke tick() directly.
|
|
468
|
+
tick: async (now) => {
|
|
469
|
+
await jobRunner.tick(now)
|
|
470
|
+
},
|
|
562
471
|
})
|
|
563
472
|
const unregister = lifecycle.add(() => handle.close())
|
|
564
473
|
void handle.closed.finally(unregister)
|
|
565
474
|
return handle
|
|
566
475
|
}
|
|
567
|
-
const startCronScheduler = async (
|
|
568
|
-
options: AppStartCronSchedulerOptions = {},
|
|
569
|
-
): Promise<LocalCronScheduler> => {
|
|
570
|
-
if (introspect) {
|
|
571
|
-
return { tick: async () => {}, close: async () => {} }
|
|
572
|
-
}
|
|
573
|
-
const cron = Object.entries(jobsDefs ?? {}).flatMap(
|
|
574
|
-
([name, definition]) =>
|
|
575
|
-
definition.kind === 'cron'
|
|
576
|
-
? [{ name, schedule: definition.schedule }]
|
|
577
|
-
: [],
|
|
578
|
-
)
|
|
579
|
-
if (cron.length === 0) {
|
|
580
|
-
throw new Error('[bunderstack] no cron tasks configured')
|
|
581
|
-
}
|
|
582
|
-
if (lifecycle.status !== 'ready') {
|
|
583
|
-
throw new Error('[bunderstack] application lifecycle is closed')
|
|
584
|
-
}
|
|
585
|
-
const scheduler = startLocalCronScheduler({
|
|
586
|
-
cron,
|
|
587
|
-
onError: options.onError,
|
|
588
|
-
runSlot: async (name, slot) => {
|
|
589
|
-
await runCronSlot({
|
|
590
|
-
db,
|
|
591
|
-
defs: jobsDefs!,
|
|
592
|
-
ctx: { db: userDb, env, email, storage, realtime },
|
|
593
|
-
name,
|
|
594
|
-
slot,
|
|
595
|
-
now: Date.now(),
|
|
596
|
-
})
|
|
597
|
-
},
|
|
598
|
-
})
|
|
599
|
-
const unregister = lifecycle.add(() => scheduler.close())
|
|
600
|
-
try {
|
|
601
|
-
await scheduler.tick()
|
|
602
|
-
} catch (error) {
|
|
603
|
-
unregister()
|
|
604
|
-
await scheduler.close()
|
|
605
|
-
throw error
|
|
606
|
-
}
|
|
607
|
-
return scheduler
|
|
608
|
-
}
|
|
609
476
|
const runWorker = async (
|
|
610
477
|
options: AppRunWorkerOptions = {},
|
|
611
478
|
): Promise<void> => {
|
|
@@ -633,61 +500,173 @@ export async function createBunderstack<
|
|
|
633
500
|
await lifecycle.close()
|
|
634
501
|
}
|
|
635
502
|
}
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
req,
|
|
645
|
-
router: trpcRouter,
|
|
646
|
-
createContext: async () => ({
|
|
647
|
-
db: userDb,
|
|
648
|
-
user: await resolveAccessUser(authResolver, req.headers),
|
|
649
|
-
env,
|
|
650
|
-
email,
|
|
651
|
-
jobs,
|
|
652
|
-
realtime,
|
|
653
|
-
storage,
|
|
654
|
-
req,
|
|
655
|
-
}),
|
|
656
|
-
})
|
|
503
|
+
const crudApiRouter = buildCrudApiRouter(options.schema, userDb, {
|
|
504
|
+
access: resolvedAccess,
|
|
505
|
+
idempotency: options.idempotency,
|
|
506
|
+
realtime,
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const customApiRouter = options.api
|
|
510
|
+
? options.api(createApiBuilder<TSchema, ValidatedEnv<TEnv>>())
|
|
657
511
|
: undefined
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
512
|
+
|
|
513
|
+
const nativeRouter = buildApiRouter({
|
|
514
|
+
crud: crudApiRouter as Record<string, unknown>,
|
|
515
|
+
storage: storageApiRouter as Record<string, unknown>,
|
|
516
|
+
realtime: realtimeApiRouter as Record<string, unknown> | undefined,
|
|
517
|
+
custom: customApiRouter as Record<string, unknown> | undefined,
|
|
518
|
+
}) as any
|
|
519
|
+
|
|
520
|
+
const authOpenAPISpecRaw =
|
|
521
|
+
options.openapi &&
|
|
522
|
+
auth.api &&
|
|
523
|
+
'generateOpenAPISchema' in auth.api &&
|
|
524
|
+
typeof auth.api.generateOpenAPISchema === 'function'
|
|
525
|
+
? await auth.api.generateOpenAPISchema()
|
|
526
|
+
: undefined
|
|
527
|
+
|
|
528
|
+
const authOpenAPISpec = authOpenAPISpecRaw
|
|
529
|
+
? normalizeForeignOpenAPISpec(authOpenAPISpecRaw, {
|
|
530
|
+
prefix: '/api/auth',
|
|
531
|
+
source: 'auth',
|
|
665
532
|
})
|
|
666
533
|
: undefined
|
|
667
|
-
|
|
668
|
-
|
|
534
|
+
|
|
535
|
+
await buildApiRegistry({
|
|
536
|
+
nativeRouter,
|
|
537
|
+
foreignSpecs: authOpenAPISpec ? [authOpenAPISpec] : [],
|
|
538
|
+
reservedCoreHandles: new Set([
|
|
539
|
+
'health',
|
|
540
|
+
...(publisher ? ['realtime.changes'] : []),
|
|
541
|
+
...[...registry.keys()].flatMap((name) =>
|
|
542
|
+
['prepareUpload', 'upload', 'confirmUpload', 'download', 'delete'].map(
|
|
543
|
+
(operation) => `files.${name}.${operation}`,
|
|
544
|
+
),
|
|
545
|
+
),
|
|
546
|
+
]),
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
const combinedOpenAPISpec = options.openapi
|
|
550
|
+
? mergeOpenAPISpecs({
|
|
551
|
+
nativeSpec: await new OpenAPIGenerator({
|
|
552
|
+
converters: [
|
|
553
|
+
new ValibotToJsonSchemaConverter(),
|
|
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
|
+
})
|
|
571
|
+
: undefined
|
|
572
|
+
|
|
573
|
+
const openapiHandler = new OpenAPIHandler(nativeRouter, {
|
|
574
|
+
errorStatusMap: BUNDERSTACK_ERROR_STATUS_MAP,
|
|
575
|
+
customErrorResponseBodyEncoder: (error: any) => ({
|
|
576
|
+
error: error.message,
|
|
577
|
+
code: error.data?.code ?? error.code,
|
|
578
|
+
...(error.data?.details !== undefined
|
|
579
|
+
? { details: error.data.details }
|
|
580
|
+
: {}),
|
|
581
|
+
}),
|
|
582
|
+
fetchInterceptors: [
|
|
583
|
+
async (options) => {
|
|
584
|
+
const res = await options.next()
|
|
585
|
+
if (res.matched && options.context.resHeaders) {
|
|
586
|
+
options.context.resHeaders.forEach((v: string, k: string) =>
|
|
587
|
+
res.response.headers.set(k, v),
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
return res
|
|
591
|
+
},
|
|
592
|
+
],
|
|
593
|
+
})
|
|
594
|
+
const rpcHandler = new RPCHandler(nativeRouter)
|
|
595
|
+
|
|
596
|
+
const apiHandler = async (req: Request): Promise<Response | null> => {
|
|
597
|
+
const urlString = typeof req === 'string' ? (req as string) : req.url
|
|
598
|
+
if (!urlString) return null
|
|
599
|
+
const url = new URL(urlString, 'http://localhost')
|
|
600
|
+
if (
|
|
601
|
+
combinedOpenAPISpec &&
|
|
602
|
+
url.pathname === '/api/openapi.json' &&
|
|
603
|
+
req.method === 'GET'
|
|
604
|
+
) {
|
|
605
|
+
return new Response(JSON.stringify(combinedOpenAPISpec), {
|
|
606
|
+
headers: { 'Content-Type': 'application/json' },
|
|
607
|
+
})
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const apiCtx = createApiContext(
|
|
611
|
+
{
|
|
612
|
+
db: userDb,
|
|
613
|
+
env,
|
|
614
|
+
storage,
|
|
615
|
+
email,
|
|
616
|
+
jobs,
|
|
617
|
+
realtime,
|
|
618
|
+
auth,
|
|
619
|
+
authResolver,
|
|
620
|
+
},
|
|
621
|
+
req,
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
if (url.pathname.startsWith('/api/rpc')) {
|
|
625
|
+
const res = await rpcHandler.handle(req, {
|
|
626
|
+
prefix: '/api/rpc',
|
|
627
|
+
context: apiCtx,
|
|
628
|
+
})
|
|
629
|
+
if (res.matched) return res.response
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const openapiRes = await openapiHandler.handle(req, { context: apiCtx })
|
|
633
|
+
if (openapiRes.matched) return openapiRes.response
|
|
634
|
+
|
|
635
|
+
return null
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
const handler = buildHandler({
|
|
669
639
|
authHandler: (req) => auth.handler(req),
|
|
670
|
-
|
|
671
|
-
realtimeRouter,
|
|
672
|
-
trpcHandler,
|
|
673
|
-
cronRouter,
|
|
640
|
+
apiHandler,
|
|
674
641
|
rateLimit: options.rateLimit,
|
|
675
642
|
})
|
|
676
643
|
|
|
644
|
+
// Topology is a deployment concern: the role decides whether this process
|
|
645
|
+
// runs background work, so application code never has to.
|
|
646
|
+
const roleWantsWorker =
|
|
647
|
+
env.BUNDERSTACK_ROLE === 'all' || env.BUNDERSTACK_ROLE === 'worker'
|
|
648
|
+
const autoStart =
|
|
649
|
+
options.background?.autoStart ??
|
|
650
|
+
(roleWantsWorker && !introspect && resolvedDefs !== undefined)
|
|
651
|
+
let backgroundRunning = false
|
|
652
|
+
if (autoStart) {
|
|
653
|
+
await startWorker()
|
|
654
|
+
backgroundRunning = true
|
|
655
|
+
}
|
|
656
|
+
|
|
677
657
|
const app: BunderstackApp<
|
|
678
658
|
TSchema,
|
|
679
659
|
TAccess,
|
|
680
660
|
BucketNamesOf<TStorage>,
|
|
681
661
|
TEnv,
|
|
682
|
-
|
|
683
|
-
|
|
662
|
+
JobsDefs | undefined,
|
|
663
|
+
TCustomApiRouter
|
|
684
664
|
> = {
|
|
685
665
|
handler,
|
|
686
666
|
// Internal tables live on the runtime db but stay out of the public type.
|
|
687
667
|
db: userDb,
|
|
688
668
|
auth,
|
|
689
669
|
storage,
|
|
690
|
-
router,
|
|
691
670
|
env,
|
|
692
671
|
email,
|
|
693
672
|
realtime,
|
|
@@ -697,13 +676,12 @@ export async function createBunderstack<
|
|
|
697
676
|
jobs: jobs as never,
|
|
698
677
|
startWorker,
|
|
699
678
|
runWorker,
|
|
700
|
-
startCronScheduler,
|
|
701
679
|
close: () => lifecycle.close(),
|
|
680
|
+
backgroundRunning,
|
|
702
681
|
get status() {
|
|
703
682
|
return lifecycle.status
|
|
704
683
|
},
|
|
705
684
|
signal: lifecycle.signal,
|
|
706
|
-
trpcRouter,
|
|
707
685
|
manifest: buildManifest({
|
|
708
686
|
schema: options.schema,
|
|
709
687
|
dialect,
|
|
@@ -712,7 +690,7 @@ export async function createBunderstack<
|
|
|
712
690
|
envConfig: options.env as EnvConfigInput | undefined,
|
|
713
691
|
emailProvider: emailProviderTag(options.email),
|
|
714
692
|
realtime: Boolean(config.realtime),
|
|
715
|
-
jobs:
|
|
693
|
+
jobs: resolvedDefs,
|
|
716
694
|
}),
|
|
717
695
|
}
|
|
718
696
|
|
|
@@ -744,6 +722,8 @@ export async function createBunderstack<
|
|
|
744
722
|
}
|
|
745
723
|
|
|
746
724
|
export { MAX_LIST_LIMIT } from './list-query'
|
|
725
|
+
export { BunderstackError } from './errors'
|
|
726
|
+
export type { BunderstackErrorCode } from './errors'
|
|
747
727
|
export { resolveConfig } from './config'
|
|
748
728
|
export type {
|
|
749
729
|
BetterAuthConfig,
|
|
@@ -761,13 +741,7 @@ export type {
|
|
|
761
741
|
EmailConfigInput,
|
|
762
742
|
EmailFacade,
|
|
763
743
|
} from './email'
|
|
764
|
-
export {
|
|
765
|
-
export type { BunderstackTRPC, TRPCContext } from './trpc'
|
|
766
|
-
export {
|
|
767
|
-
createJobsBuilder,
|
|
768
|
-
signScheduleRequest,
|
|
769
|
-
verifyScheduleRequest,
|
|
770
|
-
} from './jobs/index'
|
|
744
|
+
export { createJobsBuilder } from './jobs/index'
|
|
771
745
|
export type {
|
|
772
746
|
BunderstackJobContext,
|
|
773
747
|
BunderstackJobsBuilder,
|
|
@@ -783,8 +757,6 @@ export type {
|
|
|
783
757
|
JobsRuntimeFacade,
|
|
784
758
|
QueueJobDefinition,
|
|
785
759
|
QueueJobKeys,
|
|
786
|
-
LocalCronScheduler,
|
|
787
|
-
LocalCronSchedulerOptions,
|
|
788
760
|
RunWorkerOptions,
|
|
789
761
|
StartWorkerOptions,
|
|
790
762
|
WorkerHandle,
|
|
@@ -824,10 +796,27 @@ export type {
|
|
|
824
796
|
export type { TransformSpec } from './storage/thumbnails'
|
|
825
797
|
export { mockAuthSession } from './testing'
|
|
826
798
|
|
|
827
|
-
export type { RealtimeAction } from './realtime/
|
|
799
|
+
export type { RealtimeAction } from './realtime/publisher'
|
|
828
800
|
export { createRealtimeFacade } from './realtime/facade'
|
|
829
801
|
export type {
|
|
830
802
|
RealtimeFacade,
|
|
831
803
|
RealtimeTransport,
|
|
832
804
|
SchemaTable,
|
|
833
805
|
} from './realtime/facade'
|
|
806
|
+
|
|
807
|
+
export { createApiBuilder } from './api/builder'
|
|
808
|
+
export type { BunderstackApiBuilder, ApiFactory } from './api/builder'
|
|
809
|
+
export type {
|
|
810
|
+
CrudApiRouterFor,
|
|
811
|
+
ExposedApiTables,
|
|
812
|
+
MergeApiRouterTypes,
|
|
813
|
+
UnifiedApiRouter,
|
|
814
|
+
} from './api/types'
|
|
815
|
+
export type { TableCrudProcedures } from './api/crud-router'
|
|
816
|
+
export {
|
|
817
|
+
buildApiRegistry,
|
|
818
|
+
mergeApiRoutersStrict,
|
|
819
|
+
normalizeApiPath,
|
|
820
|
+
normalizeForeignOpenAPISpec,
|
|
821
|
+
} from './api/registry'
|
|
822
|
+
export { mergeOpenAPISpecs } from './api/openapi'
|