bunderstack 0.15.1 → 0.16.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 +7 -7
- package/package.json +2 -1
- package/src/access.ts +18 -0
- package/src/blueprint-generator.ts +63 -17
- package/src/blueprint.ts +123 -30
- package/src/cli.ts +10 -2
- package/src/config.ts +22 -44
- package/src/cron.ts +2 -1
- package/src/crud.ts +28 -14
- package/src/env.ts +17 -9
- package/src/handler.ts +5 -5
- package/src/index.ts +86 -86
- package/src/internal-tables-pg.ts +1 -17
- package/src/internal-tables.ts +0 -28
- package/src/jobs/define.ts +68 -12
- package/src/jobs/index.ts +7 -9
- package/src/jobs/queue.ts +10 -6
- package/src/jobs/slots.ts +52 -0
- package/src/jobs/worker.ts +145 -45
- package/src/list-query.ts +2 -4
- package/src/manifest.ts +65 -21
- package/src/realtime/index.ts +3 -11
- package/src/realtime/redis.ts +3 -12
- package/src/routes.ts +137 -0
- package/src/storage/buckets.ts +2 -1
- package/src/storage/file-meta.ts +1 -1
- package/src/storage/router.ts +2 -6
- package/src/storage/s3.ts +9 -3
- package/src/trpc.ts +1 -1
- package/src/jobs/cron-auth.ts +0 -28
- package/src/jobs/cron-router.ts +0 -131
- package/src/jobs/cron-runner.ts +0 -224
- package/src/jobs/local-cron.ts +0 -78
package/src/crud.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
type AuthSessionResolver,
|
|
15
15
|
type CrudOperation,
|
|
16
16
|
type ResolvedAccess,
|
|
17
|
+
tableEntryForName,
|
|
17
18
|
type ResolvedTableAccess,
|
|
18
19
|
type ScopeMap,
|
|
19
20
|
type ScopeResolver,
|
|
@@ -38,15 +39,6 @@ export type CrudRouterOptions<
|
|
|
38
39
|
realtime?: RealtimeFacade<TSchema>
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
function tableEntryForName(
|
|
42
|
-
access: ResolvedAccess,
|
|
43
|
-
tableName: string,
|
|
44
|
-
): ResolvedTableAccess | undefined {
|
|
45
|
-
for (const entry of access.values()) {
|
|
46
|
-
if (entry.tableName === tableName) return entry
|
|
47
|
-
}
|
|
48
|
-
return undefined
|
|
49
|
-
}
|
|
50
42
|
|
|
51
43
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
52
44
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -163,7 +155,11 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
|
|
|
163
155
|
)
|
|
164
156
|
}
|
|
165
157
|
|
|
166
|
-
const scope = scopeFor(tableAccess.readScope, {
|
|
158
|
+
const scope = scopeFor(tableAccess.readScope, {
|
|
159
|
+
user,
|
|
160
|
+
session,
|
|
161
|
+
request: c.req.raw,
|
|
162
|
+
})
|
|
167
163
|
if (
|
|
168
164
|
scope &&
|
|
169
165
|
!rowMatchesScope(rows[0] as Record<string, unknown>, scope)
|
|
@@ -240,7 +236,12 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
|
|
|
240
236
|
user?.id ?? null,
|
|
241
237
|
)
|
|
242
238
|
|
|
243
|
-
const scope = scopeFor(tableAccess.writeScope, {
|
|
239
|
+
const scope = scopeFor(tableAccess.writeScope, {
|
|
240
|
+
user,
|
|
241
|
+
session,
|
|
242
|
+
request: c.req.raw,
|
|
243
|
+
body: body as Record<string, unknown>,
|
|
244
|
+
})
|
|
244
245
|
const stamped = scope ? stampScope(values, scope) : values
|
|
245
246
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
246
247
|
const rows = await (db as any).insert(table).values(stamped).returning()
|
|
@@ -279,7 +280,11 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
|
|
|
279
280
|
return apiError(c, ErrorCode.NOT_FOUND, 'Not found', 404)
|
|
280
281
|
}
|
|
281
282
|
|
|
282
|
-
const readScope = scopeFor(tableAccess.readScope, {
|
|
283
|
+
const readScope = scopeFor(tableAccess.readScope, {
|
|
284
|
+
user,
|
|
285
|
+
session,
|
|
286
|
+
request: c.req.raw,
|
|
287
|
+
})
|
|
283
288
|
if (
|
|
284
289
|
readScope &&
|
|
285
290
|
!rowMatchesScope(existing[0] as Record<string, unknown>, readScope)
|
|
@@ -319,7 +324,12 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
|
|
|
319
324
|
user?.id ?? null,
|
|
320
325
|
)
|
|
321
326
|
|
|
322
|
-
const writeScope = scopeFor(tableAccess.writeScope, {
|
|
327
|
+
const writeScope = scopeFor(tableAccess.writeScope, {
|
|
328
|
+
user,
|
|
329
|
+
session,
|
|
330
|
+
request: c.req.raw,
|
|
331
|
+
body: body as Record<string, unknown>,
|
|
332
|
+
})
|
|
323
333
|
const stamped = writeScope ? stampScope(values, writeScope) : values
|
|
324
334
|
|
|
325
335
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -352,7 +362,11 @@ export function buildCrudRouter<TSchema extends Record<string, unknown>>(
|
|
|
352
362
|
return apiError(c, ErrorCode.NOT_FOUND, 'Not found', 404)
|
|
353
363
|
}
|
|
354
364
|
|
|
355
|
-
const scope = scopeFor(tableAccess.readScope, {
|
|
365
|
+
const scope = scopeFor(tableAccess.readScope, {
|
|
366
|
+
user,
|
|
367
|
+
session,
|
|
368
|
+
request: c.req.raw,
|
|
369
|
+
})
|
|
356
370
|
if (
|
|
357
371
|
scope &&
|
|
358
372
|
!rowMatchesScope(existing[0] as Record<string, unknown>, scope)
|
package/src/env.ts
CHANGED
|
@@ -10,6 +10,10 @@ export type EnvConfigInput = {
|
|
|
10
10
|
runtimeEnv?: Record<string, unknown>
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
export type BunderstackRole = 'all' | 'web' | 'worker'
|
|
14
|
+
|
|
15
|
+
const ROLES: readonly BunderstackRole[] = ['all', 'web', 'worker']
|
|
16
|
+
|
|
13
17
|
/** Vars bunderstack itself consumes, always validated. */
|
|
14
18
|
export type BaseEnv = {
|
|
15
19
|
NODE_ENV?: string
|
|
@@ -19,12 +23,13 @@ export type BaseEnv = {
|
|
|
19
23
|
REDIS_URL?: string
|
|
20
24
|
RESEND_API_KEY?: string
|
|
21
25
|
SMTP_URL?: string
|
|
22
|
-
|
|
26
|
+
BUNDERSTACK_ROLE: BunderstackRole
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
type InferVars<T> =
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
type InferVars<T> =
|
|
30
|
+
T extends Record<string, ZodType>
|
|
31
|
+
? { [K in keyof T]: z.output<T[K]> }
|
|
32
|
+
: unknown
|
|
28
33
|
|
|
29
34
|
// Non-distributive so `ValidatedEnv<undefined>` is BaseEnv, not `never`.
|
|
30
35
|
export type ValidatedEnv<TEnv extends EnvConfigInput | undefined> = [
|
|
@@ -52,8 +57,6 @@ export type ValidateEnvOptions = {
|
|
|
52
57
|
source?: Record<string, string | undefined>
|
|
53
58
|
/** Dialect-aware DATABASE_URL fallback; createBunderstack passes it. */
|
|
54
59
|
defaultDatabaseUrl?: string
|
|
55
|
-
/** Require the platform schedule secret for an application with cron work. */
|
|
56
|
-
cronConfigured?: boolean
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
const DEV_AUTH_SECRET = 'dev-secret-change-in-prod'
|
|
@@ -108,13 +111,18 @@ export function validateEnv<TEnv extends EnvConfigInput | undefined>(
|
|
|
108
111
|
REDIS_URL: source.REDIS_URL,
|
|
109
112
|
RESEND_API_KEY: source.RESEND_API_KEY,
|
|
110
113
|
SMTP_URL: source.SMTP_URL,
|
|
111
|
-
|
|
114
|
+
BUNDERSTACK_ROLE: (source.BUNDERSTACK_ROLE ?? 'all') as BunderstackRole,
|
|
112
115
|
}
|
|
113
116
|
if (isProduction && !source.AUTH_SECRET) {
|
|
114
117
|
issues.push('AUTH_SECRET: required in production')
|
|
115
118
|
}
|
|
116
|
-
if (
|
|
117
|
-
|
|
119
|
+
if (
|
|
120
|
+
source.BUNDERSTACK_ROLE !== undefined &&
|
|
121
|
+
!ROLES.includes(source.BUNDERSTACK_ROLE as BunderstackRole)
|
|
122
|
+
) {
|
|
123
|
+
issues.push(
|
|
124
|
+
`BUNDERSTACK_ROLE: must be one of ${ROLES.join(', ')} (got "${String(source.BUNDERSTACK_ROLE)}")`,
|
|
125
|
+
)
|
|
118
126
|
}
|
|
119
127
|
if (options.emailProvider === 'resend' && !source.RESEND_API_KEY) {
|
|
120
128
|
issues.push("RESEND_API_KEY: required when email provider is 'resend'")
|
package/src/handler.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { Hono } from 'hono'
|
|
|
4
4
|
import { createRateLimiter, type RateLimitConfig } from './rate-limit'
|
|
5
5
|
|
|
6
6
|
interface HandlerParts {
|
|
7
|
+
customRouter?: Hono
|
|
7
8
|
crudRouter: Hono
|
|
8
9
|
authHandler?: (req: Request) => Promise<Response>
|
|
9
10
|
storageRouter?: Hono
|
|
10
11
|
realtimeRouter?: Hono
|
|
11
|
-
cronRouter?: Hono
|
|
12
12
|
trpcHandler?: (req: Request) => Promise<Response>
|
|
13
13
|
rateLimit?: boolean | RateLimitConfig
|
|
14
14
|
}
|
|
@@ -20,16 +20,16 @@ export function buildHandler(parts: HandlerParts): {
|
|
|
20
20
|
const app = new Hono()
|
|
21
21
|
const checkRateLimit = createRateLimiter(parts.rateLimit)
|
|
22
22
|
|
|
23
|
+
// Registered ahead of everything so custom routes can sit in front of the
|
|
24
|
+
// core app. Collisions are rejected at construction, not silently shadowed.
|
|
25
|
+
if (parts.customRouter) app.route('/', parts.customRouter)
|
|
26
|
+
|
|
23
27
|
const health = (c: { json: (data: unknown) => Response }) =>
|
|
24
28
|
c.json({ status: 'ok' })
|
|
25
29
|
app.get('/health', health)
|
|
26
30
|
app.get('/api/health', health)
|
|
27
31
|
app.route('/api', parts.crudRouter)
|
|
28
32
|
|
|
29
|
-
if (parts.cronRouter) {
|
|
30
|
-
app.route('/api/_bunderstack', parts.cronRouter)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
33
|
if (parts.authHandler) {
|
|
34
34
|
app.all('/api/auth/*', (c) => parts.authHandler!(c.req.raw))
|
|
35
35
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,8 @@ import type { Hono as HonoType } from 'hono'
|
|
|
4
4
|
|
|
5
5
|
import { fetchRequestHandler } from '@trpc/server/adapters/fetch'
|
|
6
6
|
|
|
7
|
+
import { getTableName, isTable } from 'drizzle-orm'
|
|
8
|
+
|
|
7
9
|
import type { TableAccessInput } from './access'
|
|
8
10
|
import type { DbFor } from './db'
|
|
9
11
|
import type {
|
|
@@ -11,15 +13,17 @@ import type {
|
|
|
11
13
|
EnqueueOptions,
|
|
12
14
|
JobsDefs,
|
|
13
15
|
JobsFacade,
|
|
14
|
-
LocalCronScheduler,
|
|
15
|
-
LocalCronSchedulerOptions,
|
|
16
16
|
StartWorkerOptions,
|
|
17
17
|
WorkerHandle,
|
|
18
18
|
} from './jobs/index'
|
|
19
19
|
import type { StorageConfigInput } from './storage/buckets'
|
|
20
20
|
import type { StorageAdapter } from './storage/index'
|
|
21
21
|
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
resolveAccessUser,
|
|
24
|
+
tableEntryForName,
|
|
25
|
+
validateAndResolveAccess,
|
|
26
|
+
} from './access'
|
|
23
27
|
import {
|
|
24
28
|
createAuth,
|
|
25
29
|
toAuthSessionResolver,
|
|
@@ -37,10 +41,7 @@ import { withInternalTables } from './internal-tables'
|
|
|
37
41
|
import {
|
|
38
42
|
createJobsBuilder,
|
|
39
43
|
createJobRunner,
|
|
40
|
-
buildCronRouter,
|
|
41
44
|
enqueueJob,
|
|
42
|
-
runCronSlot,
|
|
43
|
-
startLocalCronScheduler,
|
|
44
45
|
startJobWorker,
|
|
45
46
|
validateJobsDefs,
|
|
46
47
|
} from './jobs/index'
|
|
@@ -57,6 +58,7 @@ import {
|
|
|
57
58
|
} from './realtime/facade'
|
|
58
59
|
import { createRealtimeBroker, buildRealtimeRouter } from './realtime/index'
|
|
59
60
|
import { createRedisRealtimeBroker } from './realtime/redis'
|
|
61
|
+
import { createRouteContext, validateCustomRoutes } from './routes'
|
|
60
62
|
import { deleteFileWithDerivatives } from './storage/delete'
|
|
61
63
|
import { deleteFileMetaRow, insertReadyFile } from './storage/file-meta'
|
|
62
64
|
import { createBucketStorages } from './storage/registry'
|
|
@@ -64,7 +66,8 @@ import { buildBucketStorageRouter } from './storage/router'
|
|
|
64
66
|
import { sweepOrphans } from './storage/sweep'
|
|
65
67
|
import { createTRPC, type BunderstackTRPC } from './trpc'
|
|
66
68
|
|
|
67
|
-
type AuthInstance = ReturnType<typeof createAuth>
|
|
69
|
+
export type AuthInstance = ReturnType<typeof createAuth>
|
|
70
|
+
|
|
68
71
|
|
|
69
72
|
function waitForWorkerShutdown(
|
|
70
73
|
signal: AbortSignal,
|
|
@@ -138,11 +141,6 @@ export type AppRunWorkerOptions = AppStartWorkerOptions & {
|
|
|
138
141
|
*/
|
|
139
142
|
allowProcessLocalRealtime?: boolean
|
|
140
143
|
}
|
|
141
|
-
export type AppStartCronSchedulerOptions = Pick<
|
|
142
|
-
LocalCronSchedulerOptions,
|
|
143
|
-
'onError'
|
|
144
|
-
>
|
|
145
|
-
|
|
146
144
|
/** Bucket names declared in a storage config; `string` when unknowable. */
|
|
147
145
|
export type BucketNamesOf<TStorage> = TStorage extends {
|
|
148
146
|
buckets: infer B extends Record<string, unknown>
|
|
@@ -178,11 +176,9 @@ export type BunderstackApp<
|
|
|
178
176
|
startWorker(options?: AppStartWorkerOptions): Promise<WorkerHandle>
|
|
179
177
|
/** Run a queue worker until aborted, then close the application. */
|
|
180
178
|
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
179
|
close(): Promise<void>
|
|
180
|
+
/** True when this process is running the background tick loop. */
|
|
181
|
+
readonly backgroundRunning: boolean
|
|
186
182
|
readonly status: LifecycleStatus
|
|
187
183
|
readonly signal: AbortSignal
|
|
188
184
|
/** Deploy-time introspection: what this app needs provisioned. */
|
|
@@ -345,11 +341,9 @@ export async function createBunderstack<
|
|
|
345
341
|
emailProvider: emailProviderTag(options.email),
|
|
346
342
|
defaultDatabaseUrl:
|
|
347
343
|
dialect === 'pg' ? 'file:./data.pglite' : 'file:./data.db',
|
|
348
|
-
|
|
349
|
-
// production app has scheduled delivery even without user-defined cron.
|
|
350
|
-
cronConfigured: true,
|
|
344
|
+
source: options.processEnv,
|
|
351
345
|
})
|
|
352
|
-
const config = resolveConfig(options, env)
|
|
346
|
+
const config = resolveConfig(options, env, options.processEnv)
|
|
353
347
|
// Adapters use Drizzle mocks during deployment introspection, so the database
|
|
354
348
|
// and Redis below never touch external services.
|
|
355
349
|
const introspect = process.env.BUNDERSTACK_INTROSPECT === '1'
|
|
@@ -518,25 +512,41 @@ export async function createBunderstack<
|
|
|
518
512
|
})
|
|
519
513
|
},
|
|
520
514
|
}
|
|
521
|
-
const
|
|
515
|
+
const storageConfigured = Boolean(options.storage)
|
|
516
|
+
// The storage sweep used to be a hardcoded maintenance route. It is an
|
|
517
|
+
// ordinary cron now, so it inherits retries, timeout and onFailed.
|
|
518
|
+
const resolvedDefs: JobsDefs | undefined = storageConfigured
|
|
519
|
+
? {
|
|
520
|
+
...(jobsDefs ?? {}),
|
|
521
|
+
'bunderstack:storage-sweep': {
|
|
522
|
+
kind: 'cron',
|
|
523
|
+
schedule: '0 4 * * *',
|
|
524
|
+
handler: async () => {
|
|
525
|
+
await storage.sweep()
|
|
526
|
+
},
|
|
527
|
+
},
|
|
528
|
+
}
|
|
529
|
+
: jobsDefs
|
|
530
|
+
|
|
531
|
+
const jobRunner = resolvedDefs
|
|
522
532
|
? createJobRunner({
|
|
523
533
|
db,
|
|
524
|
-
defs:
|
|
534
|
+
defs: resolvedDefs,
|
|
525
535
|
ctx: { db: userDb, env, email, storage, realtime },
|
|
526
536
|
})
|
|
527
537
|
: undefined
|
|
528
538
|
const jobs = {
|
|
529
539
|
async enqueue(name: string, input?: unknown, opts?: EnqueueOptions) {
|
|
530
|
-
if (!
|
|
540
|
+
if (!resolvedDefs) {
|
|
531
541
|
throw new Error(
|
|
532
542
|
'[bunderstack] no jobs configured — add a `jobs` key to createBunderstack',
|
|
533
543
|
)
|
|
534
544
|
}
|
|
535
|
-
const result = await enqueueJob(db,
|
|
545
|
+
const result = await enqueueJob(db, resolvedDefs, name, input, opts)
|
|
536
546
|
return result
|
|
537
547
|
},
|
|
538
548
|
tick(now?: number) {
|
|
539
|
-
return jobRunner ? jobRunner.tick(now) : Promise.resolve()
|
|
549
|
+
return jobRunner ? jobRunner.tick(now) : Promise.resolve({ claimed: 0, ran: 0, failed: 0 })
|
|
540
550
|
},
|
|
541
551
|
}
|
|
542
552
|
if (jobRunner) jobRunner.setJobsFacade(jobs)
|
|
@@ -558,54 +568,16 @@ export async function createBunderstack<
|
|
|
558
568
|
const handle = startJobWorker({
|
|
559
569
|
...options,
|
|
560
570
|
signal,
|
|
561
|
-
tick
|
|
571
|
+
// The runtime loop only cares that a tick completed; TickResult is for
|
|
572
|
+
// callers that invoke tick() directly.
|
|
573
|
+
tick: async (now) => {
|
|
574
|
+
await jobRunner.tick(now)
|
|
575
|
+
},
|
|
562
576
|
})
|
|
563
577
|
const unregister = lifecycle.add(() => handle.close())
|
|
564
578
|
void handle.closed.finally(unregister)
|
|
565
579
|
return handle
|
|
566
580
|
}
|
|
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
581
|
const runWorker = async (
|
|
610
582
|
options: AppRunWorkerOptions = {},
|
|
611
583
|
): Promise<void> => {
|
|
@@ -655,25 +627,52 @@ export async function createBunderstack<
|
|
|
655
627
|
}),
|
|
656
628
|
})
|
|
657
629
|
: undefined
|
|
658
|
-
const
|
|
659
|
-
?
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
630
|
+
const customRouter = options.routes
|
|
631
|
+
? (() => {
|
|
632
|
+
const routeCtx = createRouteContext({
|
|
633
|
+
db: userDb,
|
|
634
|
+
env,
|
|
635
|
+
storage,
|
|
636
|
+
email,
|
|
637
|
+
jobs,
|
|
638
|
+
realtime,
|
|
639
|
+
auth,
|
|
640
|
+
authResolver,
|
|
641
|
+
})
|
|
642
|
+
const built = (
|
|
643
|
+
options.routes as (ctx: unknown) => import('hono').Hono
|
|
644
|
+
)(routeCtx)
|
|
645
|
+
const enabledTables = Object.values(options.schema)
|
|
646
|
+
.filter((table) => isTable(table))
|
|
647
|
+
.map((table) => getTableName(table))
|
|
648
|
+
.filter((name) => tableEntryForName(resolvedAccess, name)?.enabled)
|
|
649
|
+
validateCustomRoutes(built.routes, enabledTables)
|
|
650
|
+
return built
|
|
651
|
+
})()
|
|
666
652
|
: undefined
|
|
667
653
|
const { handler, router } = buildHandler({
|
|
654
|
+
customRouter,
|
|
668
655
|
crudRouter,
|
|
669
656
|
authHandler: (req) => auth.handler(req),
|
|
670
657
|
storageRouter,
|
|
671
658
|
realtimeRouter,
|
|
672
659
|
trpcHandler,
|
|
673
|
-
cronRouter,
|
|
674
660
|
rateLimit: options.rateLimit,
|
|
675
661
|
})
|
|
676
662
|
|
|
663
|
+
// Topology is a deployment concern: the role decides whether this process
|
|
664
|
+
// runs background work, so application code never has to.
|
|
665
|
+
const roleWantsWorker =
|
|
666
|
+
env.BUNDERSTACK_ROLE === 'all' || env.BUNDERSTACK_ROLE === 'worker'
|
|
667
|
+
const autoStart =
|
|
668
|
+
options.background?.autoStart ??
|
|
669
|
+
(roleWantsWorker && !introspect && resolvedDefs !== undefined)
|
|
670
|
+
let backgroundRunning = false
|
|
671
|
+
if (autoStart) {
|
|
672
|
+
await startWorker()
|
|
673
|
+
backgroundRunning = true
|
|
674
|
+
}
|
|
675
|
+
|
|
677
676
|
const app: BunderstackApp<
|
|
678
677
|
TSchema,
|
|
679
678
|
TAccess,
|
|
@@ -697,8 +696,8 @@ export async function createBunderstack<
|
|
|
697
696
|
jobs: jobs as never,
|
|
698
697
|
startWorker,
|
|
699
698
|
runWorker,
|
|
700
|
-
startCronScheduler,
|
|
701
699
|
close: () => lifecycle.close(),
|
|
700
|
+
backgroundRunning,
|
|
702
701
|
get status() {
|
|
703
702
|
return lifecycle.status
|
|
704
703
|
},
|
|
@@ -712,7 +711,7 @@ export async function createBunderstack<
|
|
|
712
711
|
envConfig: options.env as EnvConfigInput | undefined,
|
|
713
712
|
emailProvider: emailProviderTag(options.email),
|
|
714
713
|
realtime: Boolean(config.realtime),
|
|
715
|
-
jobs:
|
|
714
|
+
jobs: resolvedDefs,
|
|
716
715
|
}),
|
|
717
716
|
}
|
|
718
717
|
|
|
@@ -763,11 +762,7 @@ export type {
|
|
|
763
762
|
} from './email'
|
|
764
763
|
export { createTRPC } from './trpc'
|
|
765
764
|
export type { BunderstackTRPC, TRPCContext } from './trpc'
|
|
766
|
-
export {
|
|
767
|
-
createJobsBuilder,
|
|
768
|
-
signScheduleRequest,
|
|
769
|
-
verifyScheduleRequest,
|
|
770
|
-
} from './jobs/index'
|
|
765
|
+
export { createJobsBuilder } from './jobs/index'
|
|
771
766
|
export type {
|
|
772
767
|
BunderstackJobContext,
|
|
773
768
|
BunderstackJobsBuilder,
|
|
@@ -783,8 +778,6 @@ export type {
|
|
|
783
778
|
JobsRuntimeFacade,
|
|
784
779
|
QueueJobDefinition,
|
|
785
780
|
QueueJobKeys,
|
|
786
|
-
LocalCronScheduler,
|
|
787
|
-
LocalCronSchedulerOptions,
|
|
788
781
|
RunWorkerOptions,
|
|
789
782
|
StartWorkerOptions,
|
|
790
783
|
WorkerHandle,
|
|
@@ -831,3 +824,10 @@ export type {
|
|
|
831
824
|
RealtimeTransport,
|
|
832
825
|
SchemaTable,
|
|
833
826
|
} from './realtime/facade'
|
|
827
|
+
|
|
828
|
+
export type {
|
|
829
|
+
BunderstackRouteContext,
|
|
830
|
+
RouteContext,
|
|
831
|
+
RoutesBuilder,
|
|
832
|
+
} from './routes'
|
|
833
|
+
|
|
@@ -67,20 +67,4 @@ export const bunderstackJobsPg = pgTable(
|
|
|
67
67
|
],
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
'_bunderstack_cron_runs',
|
|
72
|
-
{
|
|
73
|
-
taskId: text('task_id').notNull(),
|
|
74
|
-
scheduledAt: bigint('scheduled_at', { mode: 'number' }).notNull(),
|
|
75
|
-
status: text('status').notNull(),
|
|
76
|
-
attempts: integer('attempts').notNull().default(0),
|
|
77
|
-
lockedUntil: bigint('locked_until', { mode: 'number' }),
|
|
78
|
-
lastError: text('last_error'),
|
|
79
|
-
startedAt: bigint('started_at', { mode: 'number' }),
|
|
80
|
-
finishedAt: bigint('finished_at', { mode: 'number' }),
|
|
81
|
-
},
|
|
82
|
-
(t) => [
|
|
83
|
-
primaryKey({ columns: [t.taskId, t.scheduledAt] }),
|
|
84
|
-
index('bcr_claim').on(t.status, t.lockedUntil),
|
|
85
|
-
],
|
|
86
|
-
)
|
|
70
|
+
|
package/src/internal-tables.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
import { detectDialect } from './dialect'
|
|
13
13
|
import {
|
|
14
14
|
bunderstackFilesPg,
|
|
15
|
-
bunderstackCronRunsPg,
|
|
16
15
|
bunderstackIdempotencyPg,
|
|
17
16
|
bunderstackJobsPg,
|
|
18
17
|
} from './internal-tables-pg'
|
|
@@ -74,43 +73,22 @@ export const bunderstackJobs = sqliteTable(
|
|
|
74
73
|
],
|
|
75
74
|
)
|
|
76
75
|
|
|
77
|
-
export const bunderstackCronRuns = sqliteTable(
|
|
78
|
-
'_bunderstack_cron_runs',
|
|
79
|
-
{
|
|
80
|
-
taskId: text('task_id').notNull(),
|
|
81
|
-
scheduledAt: integer('scheduled_at').notNull(),
|
|
82
|
-
status: text('status').notNull(),
|
|
83
|
-
attempts: integer('attempts').notNull().default(0),
|
|
84
|
-
lockedUntil: integer('locked_until'),
|
|
85
|
-
lastError: text('last_error'),
|
|
86
|
-
startedAt: integer('started_at'),
|
|
87
|
-
finishedAt: integer('finished_at'),
|
|
88
|
-
},
|
|
89
|
-
(t) => [
|
|
90
|
-
primaryKey({ columns: [t.taskId, t.scheduledAt] }),
|
|
91
|
-
index('bcr_claim').on(t.status, t.lockedUntil),
|
|
92
|
-
],
|
|
93
|
-
)
|
|
94
|
-
|
|
95
76
|
export const INTERNAL_TABLES = {
|
|
96
77
|
bunderstackFiles,
|
|
97
78
|
bunderstackIdempotency,
|
|
98
79
|
bunderstackJobs,
|
|
99
|
-
bunderstackCronRuns,
|
|
100
80
|
} as const
|
|
101
81
|
|
|
102
82
|
export const INTERNAL_TABLE_NAMES: ReadonlySet<string> = new Set([
|
|
103
83
|
'bunderstack_file_meta',
|
|
104
84
|
'_bunderstack_idempotency',
|
|
105
85
|
'_bunderstack_jobs',
|
|
106
|
-
'_bunderstack_cron_runs',
|
|
107
86
|
])
|
|
108
87
|
|
|
109
88
|
export const INTERNAL_TABLES_PG = {
|
|
110
89
|
bunderstackFiles: bunderstackFilesPg,
|
|
111
90
|
bunderstackIdempotency: bunderstackIdempotencyPg,
|
|
112
91
|
bunderstackJobs: bunderstackJobsPg,
|
|
113
|
-
bunderstackCronRuns: bunderstackCronRunsPg,
|
|
114
92
|
} as const
|
|
115
93
|
|
|
116
94
|
// Both dialect twins count as "ours" for the re-export identity check.
|
|
@@ -121,7 +99,6 @@ const INTERNAL_TABLE_CANDIDATES = new Map<string, readonly unknown[]>([
|
|
|
121
99
|
[bunderstackIdempotency, bunderstackIdempotencyPg],
|
|
122
100
|
],
|
|
123
101
|
[getTableName(bunderstackJobs), [bunderstackJobs, bunderstackJobsPg]],
|
|
124
|
-
[getTableName(bunderstackCronRuns), [bunderstackCronRuns, bunderstackCronRunsPg]],
|
|
125
102
|
])
|
|
126
103
|
|
|
127
104
|
/** Internal file-meta table matching the db's dialect. */
|
|
@@ -139,11 +116,6 @@ export function jobsTableFor(db: unknown) {
|
|
|
139
116
|
return is(db, PgDatabase) ? bunderstackJobsPg : bunderstackJobs
|
|
140
117
|
}
|
|
141
118
|
|
|
142
|
-
/** Internal cron-run table matching the db's dialect. */
|
|
143
|
-
export function cronRunsTableFor(db: unknown) {
|
|
144
|
-
return is(db, PgDatabase) ? bunderstackCronRunsPg : bunderstackCronRuns
|
|
145
|
-
}
|
|
146
|
-
|
|
147
119
|
export function withInternalTables<TSchema extends Record<string, unknown>>(
|
|
148
120
|
schema: TSchema,
|
|
149
121
|
): TSchema & typeof INTERNAL_TABLES {
|