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.
Files changed (47) hide show
  1. package/README.md +25 -138
  2. package/package.json +22 -14
  3. package/src/access.ts +24 -1
  4. package/src/api/api-types.types.ts +106 -0
  5. package/src/api/builder.ts +52 -0
  6. package/src/api/context.ts +83 -0
  7. package/src/api/crud-router.ts +321 -0
  8. package/src/api/openapi.ts +184 -0
  9. package/src/api/realtime-router.ts +75 -0
  10. package/src/api/registry.ts +338 -0
  11. package/src/api/router.ts +34 -0
  12. package/src/api/storage-router.ts +224 -0
  13. package/src/api/types.ts +84 -0
  14. package/src/auth.ts +5 -0
  15. package/src/blueprint.ts +88 -105
  16. package/src/config.ts +73 -77
  17. package/src/cron.ts +2 -1
  18. package/src/crud-operations.ts +488 -0
  19. package/src/dialect.ts +1 -1
  20. package/src/env.ts +28 -21
  21. package/src/errors.ts +90 -23
  22. package/src/handler.ts +16 -44
  23. package/src/index.ts +283 -294
  24. package/src/internal-tables-pg.ts +1 -17
  25. package/src/internal-tables.ts +0 -31
  26. package/src/jobs/define.ts +75 -21
  27. package/src/jobs/index.ts +3 -9
  28. package/src/jobs/queue.ts +14 -6
  29. package/src/jobs/slots.ts +52 -0
  30. package/src/jobs/worker.ts +142 -42
  31. package/src/manifest.ts +84 -93
  32. package/src/realtime/facade.ts +16 -13
  33. package/src/realtime/filter.ts +77 -0
  34. package/src/realtime/heartbeat.ts +80 -0
  35. package/src/realtime/publisher.ts +46 -0
  36. package/src/standard-schema.ts +59 -0
  37. package/src/storage/index.ts +8 -0
  38. package/src/storage/operations.ts +398 -0
  39. package/src/crud.ts +0 -408
  40. package/src/jobs/cron-auth.ts +0 -28
  41. package/src/jobs/cron-router.ts +0 -135
  42. package/src/jobs/cron-runner.ts +0 -224
  43. package/src/jobs/local-cron.ts +0 -78
  44. package/src/realtime/index.ts +0 -250
  45. package/src/realtime/redis.ts +0 -228
  46. package/src/storage/router.ts +0 -531
  47. package/src/trpc.ts +0 -57
package/README.md CHANGED
@@ -1,164 +1,51 @@
1
1
  # bunderstack
2
2
 
3
- A batteries-included backend framework for Bun. Point it at a Drizzle schema
4
- and get CRUD APIs, auth, file storage, realtime, typed custom endpoints
5
- (tRPC), email, and validated env — all from a single config object and a
6
- single `Request → Response` handler.
3
+ The server package for Bunderstack's unified, type-safe oRPC backend.
7
4
 
8
5
  ```sh
9
- bun add bunderstack better-auth drizzle-orm hono zod @libsql/client
6
+ bun add bunderstack better-auth drizzle-orm valibot @libsql/client
10
7
  ```
11
8
 
12
9
  ```ts
13
10
  import { createBunderstack } from 'bunderstack'
14
11
  import { libsql } from 'bunderstack/database/libsql'
12
+ import * as v from 'valibot'
15
13
  import * as schema from './schema'
16
14
 
17
- const app = await createBunderstack({
15
+ export const app = await createBunderstack({
18
16
  schema,
19
- database: {
20
- adapter: libsql(),
21
- url: 'file:./data.db',
22
- },
23
- auth: { emailAndPassword: { enabled: true } },
24
- access: {
25
- posts: { ownerColumn: 'userId', list: 'public', create: 'authenticated' },
26
- },
17
+ database: { adapter: libsql(), url: 'file:./data.db' },
18
+ access: { posts: { crud: true } },
19
+ realtime: true,
20
+ api: (o) => ({
21
+ ping: o.public
22
+ .route({ method: 'GET', path: '/api/ping' })
23
+ .input(v.optional(v.object({})))
24
+ .handler(() => ({ ok: true })),
25
+ }),
27
26
  })
28
27
 
29
28
  Bun.serve({ fetch: app.handler })
29
+ export type App = typeof app
30
30
  ```
31
31
 
32
- Full documentation and examples:
33
- [github.com/kirill-dev-pro/bunderstack](https://github.com/kirill-dev-pro/bunderstack)
32
+ CRUD, custom procedures, webhooks, file buckets, health, and the Publisher
33
+ event iterator form one router. Validation accepts Standard Schema; internal
34
+ generated schemas use Valibot. OpenAPI is opt-in with `openapi: true`.
34
35
 
35
- ## Platform deployment contract
36
-
37
- Deployment platforms (like Bunderhost) integrate with any bunderstack app
38
- through env vars alone — no code changes required.
39
-
40
- ### Overrides (beat code-level config)
41
-
42
- | Var | Effect |
43
- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
44
- | `BUNDERSTACK_DATABASE_URL` | Database URL; wins over `database.url` in code |
45
- | `BUNDERSTACK_DATABASE_AUTH_TOKEN` | Auth token for the database |
46
- | `BUNDERSTACK_S3_ENDPOINT` | Forces ALL buckets onto this S3 backend (code-level `local`/per-bucket `s3` blocks are ignored) |
47
- | `BUNDERSTACK_S3_BUCKET` | Physical bucket name (logical buckets become key prefixes) |
48
- | `BUNDERSTACK_S3_ACCESS_KEY_ID` / `BUNDERSTACK_S3_SECRET_ACCESS_KEY` | Credentials |
49
- | `BUNDERSTACK_S3_REGION` | Region (default `auto`) |
50
- | `BUNDERSTACK_S3_PUBLIC_URL` | Public base URL for `visibility: 'public'` buckets |
51
-
52
- Plain `DATABASE_URL` / `S3_*` vars keep their usual role: fallbacks that
53
- code-level config wins over.
54
-
55
- ### Committed deployment blueprint
56
-
57
- Generate a deterministic, provider-neutral declaration that a host can read
58
- without importing the application at deploy time. Version 1 supports TanStack
59
- Start apps and records database tables and migration mode, storage buckets,
60
- environment requirements, realtime, jobs, cron, and maintenance schedules.
61
-
62
- ```sh
63
- bunx bunderstack blueprint
64
- bunx bunderstack blueprint --check
65
- ```
66
-
67
- The command imports `src/bunderstack.ts` by default (or
68
- `package.json#bunderstack.entry`) with `BUNDERSTACK_INTROSPECT=1`, so it never
69
- opens external database, storage, or realtime connections. Commit the generated
70
- `bunderstack.blueprint.yaml`; CI should run the `--check` form. The public
71
- `bunderstack/blueprint` module exposes the strict parser and conversion helpers
72
- for hosts and other tooling.
73
-
74
- Real database clients belong to the app. Call `await app.close()` when a
75
- standalone process or test is finished; it closes the real libSQL, PGlite,
76
- postgres.js, or Bun SQL client selected by `database.adapter`. Introspection
77
- mocks own no client, so there is nothing to close for that database path.
78
-
79
- ### Background runtime
80
-
81
- Declaring jobs does not start a worker. Queue jobs (`j.job()`) are processed by
82
- an explicit worker process:
36
+ Generated CRUD writes publish automatically. After a custom write commits,
37
+ publish its complete returned row with:
83
38
 
84
39
  ```ts
85
- import { app } from './bunderstack'
86
-
87
- await app.runWorker()
40
+ await context.realtime.publish(schema.posts, 'update', post)
88
41
  ```
89
42
 
90
- If a queue handler calls `ctx.realtime.publish()`, the web and worker processes
91
- must share a realtime transport. Configure `REDIS_URL` (or
92
- `realtime: { redis: "redis://..." }`). `realtime: true` without Redis uses a
93
- process-local memory broker and is suitable only when the worker is embedded
94
- with `app.startWorker()`.
43
+ Use the in-memory Publisher for one process or configure
44
+ `realtime: { redis: process.env.REDIS_URL! }` for multi-process delivery and
45
+ replay. Deployment metadata is generated with `bunx bunderstack blueprint`.
95
46
 
96
- Since 0.9.0, `app.runWorker()` rejects that unsafe combination by default. If
97
- queue handlers never publish realtime events, acknowledge the process-local
98
- behavior with `app.runWorker({ allowProcessLocalRealtime: true })`.
99
-
100
- Inspect the active runtime with `app.realtime.transport` (`'disabled'`,
101
- `'memory'`, or `'redis'`). The generated blueprint declares only whether
102
- realtime is required; the host chooses its shared transport and injects its
103
- runtime configuration.
104
-
105
- ```ts
106
- const app = await createBunderstack({
107
- // ...
108
- realtime: { redis: process.env.REDIS_URL! },
109
- })
110
- ```
111
-
112
- ```bash
113
- REDIS_URL=redis://localhost:6379 bun src/server.ts
114
- REDIS_URL=redis://localhost:6379 bun src/worker.ts
115
- ```
116
-
117
- Cron tasks (`j.cron()`) are delivered by the host to
118
- `POST /api/_bunderstack/cron/:name`; storage maintenance uses
119
- `POST /api/_bunderstack/maintenance/storage-sweep`. Production requires the
120
- injected `BUNDERSTACK_CRON_SECRET`. Use `await app.startCronScheduler()` only
121
- for local standalone development. `app.manifest.background` tells Bunderhost
122
- whether to deploy an always-on worker (queue jobs) or only HTTP-delivered cron
123
- (cron-only).
124
-
125
- ### Publishing custom writes to realtime
126
-
127
- Generated CRUD publishes automatically. Writes made directly through `app.db`
128
- or `ctx.db` are explicit: publish the complete row returned by Drizzle after the
129
- write commits.
130
-
131
- ```ts
132
- const [avatar] = await ctx.db
133
- .update(schema.avatars)
134
- .set({ status: 'completed' })
135
- .where(eq(schema.avatars.id, avatarId))
136
- .returning()
137
-
138
- await ctx.realtime.publish(schema.avatars, 'update', avatar)
139
- ```
140
-
141
- The same typed facade is available as `app.realtime`, in tRPC context, and in
142
- queue-job and cron context. Passing the Drizzle table makes a table-name typo a
143
- type error and constrains the record to that table's select model.
144
-
145
- Publish after an enclosing transaction resolves, not from inside it. The full
146
- row is required because realtime access filtering may inspect its `id`, owner,
147
- or read-scope columns. Subscriber access checks, Redis fan-out, and replay are
148
- applied automatically by the existing broker. When server realtime is not
149
- configured, `realtime.enabled` is `false` and `publish()` is a no-op.
150
-
151
- ## Shipping TypeScript source
152
-
153
- This package publishes raw TypeScript (`exports` point at `.ts` files). Bun
154
- consumes it natively. If a Node-based bundler or SSR server processes it,
155
- make sure the package is bundled rather than externalized — e.g. in Vite:
156
-
157
- ```ts
158
- ssr: {
159
- noExternal: [/^bunderstack/]
160
- }
161
- ```
47
+ See the [workspace documentation](../../README.md) for webhooks, clients,
48
+ storage, collections, lifecycle, and complete examples.
162
49
 
163
50
  ## License
164
51
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bunderstack",
3
- "version": "0.15.2",
4
- "description": "Batteries-included backend framework for Bun: CRUD APIs, auth, file storage, realtime, tRPC, email, and validated env from a single Drizzle schema and config object.",
3
+ "version": "0.17.0-beta.0",
4
+ "description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, email, and validated env from one config.",
5
5
  "keywords": [
6
6
  "backend",
7
7
  "better-auth",
@@ -9,9 +9,9 @@
9
9
  "crud",
10
10
  "drizzle",
11
11
  "framework",
12
- "hono",
12
+ "orpc",
13
13
  "realtime",
14
- "trpc"
14
+ "typesafe"
15
15
  ],
16
16
  "homepage": "https://github.com/kirill-dev-pro/bunderstack#readme",
17
17
  "license": "MIT",
@@ -47,43 +47,51 @@
47
47
  "./typeid/pg": "./src/typeid-pg.ts",
48
48
  "./env": "./src/env.ts",
49
49
  "./blueprint": "./src/blueprint.ts",
50
- "./trpc": "./src/trpc.ts",
51
50
  "./cron": "./src/cron.ts",
52
- "./email/smtp": "./src/email/smtp.ts"
51
+ "./email/smtp": "./src/email/smtp.ts",
52
+ "./api": "./src/api/types.ts"
53
53
  },
54
54
  "scripts": {
55
55
  "test": "bun test",
56
+ "typecheck": "tsc --noEmit",
56
57
  "dev": "bun --hot ../../examples/standalone/server.ts",
57
58
  "db:push": "drizzle-kit push",
58
59
  "db:migrate": "drizzle-kit migrate"
59
60
  },
60
61
  "dependencies": {
61
- "superjson": "^2.2.0",
62
+ "@standard-schema/spec": "1.1.0",
63
+ "valibot": "1.4.2",
62
64
  "yaml": "^2.9.0"
63
65
  },
64
66
  "devDependencies": {
65
67
  "@electric-sql/pglite": ">=0.3.0",
66
68
  "@libsql/client": ">=0.14.0",
67
- "@trpc/server": "^11.0.0",
69
+ "@orpc/openapi": "2.0.0-beta.26",
70
+ "@orpc/bun": "2.0.0-beta.26",
71
+ "@orpc/publisher": "2.0.0-beta.26",
72
+ "@orpc/server": "2.0.0-beta.26",
73
+ "@orpc/valibot": "2.0.0-beta.26",
68
74
  "@types/nodemailer": "^6",
69
75
  "better-auth": "^1.0.0",
70
76
  "drizzle-kit": "^0.30.0",
71
77
  "drizzle-orm": "^0.45.0",
72
- "hono": "^4.0.0",
73
- "zod": "^4.4.3"
78
+ "drizzle-valibot": "0.4.2"
74
79
  },
75
80
  "peerDependencies": {
76
81
  "@electric-sql/pglite": ">=0.3.0",
77
82
  "@libsql/client": ">=0.14.0",
78
- "@trpc/server": "^11.0.0",
83
+ "@orpc/openapi": "2.0.0-beta.26",
84
+ "@orpc/bun": "2.0.0-beta.26",
85
+ "@orpc/publisher": "2.0.0-beta.26",
86
+ "@orpc/server": "2.0.0-beta.26",
87
+ "@orpc/valibot": "2.0.0-beta.26",
79
88
  "better-auth": "^1.0.0",
80
89
  "drizzle-kit": "^0.30.0",
81
90
  "drizzle-orm": "^0.45.0",
82
- "hono": "^4.0.0",
91
+ "drizzle-valibot": "0.4.2",
83
92
  "nodemailer": ">=6 <10",
84
93
  "postgres": ">=3.4.0",
85
- "typescript": ">=5",
86
- "zod": "^4.4.3"
94
+ "typescript": ">=5"
87
95
  },
88
96
  "peerDependenciesMeta": {
89
97
  "@electric-sql/pglite": {
package/src/access.ts CHANGED
@@ -65,6 +65,8 @@ export type TableAccessInput = {
65
65
  create?: OperationRule
66
66
  update?: OperationRule
67
67
  delete?: OperationRule
68
+ /** Explicit write allowlist. Entries override matching system-readonly
69
+ * defaults such as `updatedAt`; `id` remains immutable on update. */
68
70
  writableColumns?: string[]
69
71
  readonlyColumns?: string[]
70
72
  /** Columns matched by `?q=` on list — opt-in; omitted columns are never searched. */
@@ -103,6 +105,24 @@ export type ResolvedTableAccess = {
103
105
 
104
106
  export type ResolvedAccess = Map<string, ResolvedTableAccess>
105
107
 
108
+ /**
109
+ * Look up a table's resolved access by its physical table name.
110
+ *
111
+ * `ResolvedAccess` is keyed by schema export name, not table name, so every
112
+ * consumer that starts from a physical name needs this scan. It lives here so
113
+ * CRUD, realtime, and route validation cannot drift apart on which tables they
114
+ * consider enabled.
115
+ */
116
+ export function tableEntryForName(
117
+ access: ResolvedAccess,
118
+ tableName: string,
119
+ ): ResolvedTableAccess | undefined {
120
+ for (const entry of access.values()) {
121
+ if (entry.tableName === tableName) return entry
122
+ }
123
+ return undefined
124
+ }
125
+
106
126
  const DEFAULT_READONLY = [
107
127
  'id',
108
128
  'createdAt',
@@ -190,6 +210,7 @@ function resolveDefaults(
190
210
  columns: string[],
191
211
  ): Omit<ResolvedTableAccess, 'tableKey' | 'tableName' | 'enabled'> {
192
212
  const listAccess = resolveListAccess(input, columns)
213
+ const explicitlyWritable = new Set(input.writableColumns ?? [])
193
214
  return {
194
215
  ownerColumn,
195
216
  list: input.list ?? 'public',
@@ -199,7 +220,9 @@ function resolveDefaults(
199
220
  delete: input.delete ?? (ownerColumn ? 'owner' : 'deny'),
200
221
  writableColumns: input.writableColumns,
201
222
  readonlyColumns: [
202
- ...DEFAULT_READONLY,
223
+ ...DEFAULT_READONLY.filter(
224
+ (column) => column === 'id' || !explicitlyWritable.has(column),
225
+ ),
203
226
  ...(input.readonlyColumns ?? []),
204
227
  ...(ownerColumn ? [ownerColumn] : []),
205
228
  ],
@@ -0,0 +1,106 @@
1
+ import type { InferRouterInputs, InferRouterOutputs } from '@orpc/server'
2
+
3
+ import { pgTable, text } from 'drizzle-orm/pg-core'
4
+ import * as v from 'valibot'
5
+
6
+ import type { ExposedApiTables } from './types'
7
+
8
+ import { pglite } from '../database/pglite'
9
+ import { createBunderstack } from '../index'
10
+
11
+ type Equal<A, B> =
12
+ (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
13
+ ? true
14
+ : false
15
+ type Expect<T extends true> = T
16
+
17
+ const posts = pgTable('posts', {
18
+ id: text('id').primaryKey(),
19
+ title: text('title').notNull(),
20
+ })
21
+
22
+ const privateNotes = pgTable('private_notes', {
23
+ id: text('id').primaryKey(),
24
+ content: text('content').notNull(),
25
+ })
26
+
27
+ const ownedPosts = pgTable('owned_posts', {
28
+ id: text('id').primaryKey(),
29
+ userId: text('user_id').notNull(),
30
+ })
31
+
32
+ type ImplicitTables = ExposedApiTables<
33
+ { posts: typeof posts; ownedPosts: typeof ownedPosts },
34
+ undefined
35
+ >
36
+ type _ImplicitAccessHidesUnownedTable = Expect<
37
+ Equal<'posts' extends ImplicitTables ? true : false, false>
38
+ >
39
+ type _ImplicitAccessIncludesConventionTable = Expect<
40
+ Equal<'ownedPosts' extends ImplicitTables ? true : false, true>
41
+ >
42
+
43
+ const typedApp = await createBunderstack({
44
+ schema: { posts, privateNotes },
45
+ database: { adapter: pglite() },
46
+ processEnv: {
47
+ DATABASE_URL: 'memory://',
48
+ BUNDERSTACK_ROLE: 'web',
49
+ },
50
+ access: {
51
+ posts: { crud: true, list: 'public', create: 'public' },
52
+ privateNotes: { crud: false },
53
+ },
54
+ api: (o) => ({
55
+ stats: o.protected
56
+ .input(v.object({ period: v.picklist(['day', 'week']) }))
57
+ .output(
58
+ v.object({ period: v.picklist(['day', 'week']), userId: v.string() }),
59
+ )
60
+ .handler(async ({ input, context }) => {
61
+ const _userId: string = context.user.id
62
+ const _db = context.db
63
+ const _env = context.env
64
+ return {
65
+ period: input.period,
66
+ userId: context.user.id,
67
+ }
68
+ }),
69
+ }),
70
+ })
71
+
72
+ type Api = NonNullable<typeof typedApp.$inferClient>['api']
73
+
74
+ type _HasPosts = Expect<Equal<'posts' extends keyof Api ? true : false, true>>
75
+ type _HidesPrivateNotes = Expect<
76
+ Equal<'privateNotes' extends keyof Api ? true : false, false>
77
+ >
78
+ type _HasStats = Expect<Equal<'stats' extends keyof Api ? true : false, true>>
79
+
80
+ type PostsInputs = InferRouterInputs<Api>['posts']
81
+ type PostsOutputs = InferRouterOutputs<Api>['posts']
82
+ type IsAny<T> = 0 extends 1 & T ? true : false
83
+ type ExpectedUpdateInput = {
84
+ params: { id: string }
85
+ query?: Record<string, unknown>
86
+ headers?: Record<string, unknown>
87
+ body: { title?: string }
88
+ }
89
+
90
+ type _CreateInput = Expect<
91
+ Equal<
92
+ PostsInputs['create'],
93
+ Partial<typeof posts.$inferInsert>
94
+ >
95
+ >
96
+ type _GetInput = Expect<Equal<PostsInputs['get'], { id: string }>>
97
+ type _UpdateInputToExpected = Expect<
98
+ PostsInputs['update'] extends ExpectedUpdateInput ? true : false
99
+ >
100
+ type _ExpectedToUpdateInput = Expect<
101
+ ExpectedUpdateInput extends PostsInputs['update'] ? true : false
102
+ >
103
+ type _ListItems = Expect<
104
+ Equal<PostsOutputs['list']['items'], Array<{ id: string; title: string }>>
105
+ >
106
+ type _GetOutputIsTyped = Expect<Equal<IsAny<PostsOutputs['get']>, false>>
@@ -0,0 +1,52 @@
1
+ import { os, type AnyRouter } from '@orpc/server'
2
+
3
+ import type { ApiContext } from './context'
4
+
5
+ import {
6
+ BUNDERSTACK_ERRORS,
7
+ BunderstackError,
8
+ mapBunderstackErrors,
9
+ } from '../errors'
10
+ export type { ProtectedContextAdditions } from './types'
11
+
12
+ export function createApiBuilder<
13
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
14
+ TEnv = Record<string, unknown>,
15
+ >() {
16
+ const base = os
17
+ .$context<ApiContext<TSchema, TEnv>>()
18
+ .errors(BUNDERSTACK_ERRORS)
19
+ .use(mapBunderstackErrors)
20
+
21
+ const protectedProc = base.use(async ({ context, next }) => {
22
+ const session = await context.getSession()
23
+ if (!session.user) {
24
+ throw new BunderstackError('UNAUTHORIZED', 'Authentication required')
25
+ }
26
+ return next({
27
+ context: {
28
+ user: session.user,
29
+ session: {
30
+ activeOrganizationId: session.activeOrganizationId,
31
+ },
32
+ },
33
+ })
34
+ })
35
+
36
+ return {
37
+ public: base,
38
+ protected: protectedProc,
39
+ webhook: base,
40
+ }
41
+ }
42
+
43
+ export type BunderstackApiBuilder<
44
+ TSchema extends Record<string, unknown>,
45
+ TEnv = Record<string, unknown>,
46
+ > = ReturnType<typeof createApiBuilder<TSchema, TEnv>>
47
+
48
+ export type ApiFactory<
49
+ TSchema extends Record<string, unknown>,
50
+ TEnv,
51
+ TCustomApiRouter extends AnyRouter,
52
+ > = (builder: BunderstackApiBuilder<TSchema, TEnv>) => TCustomApiRouter
@@ -0,0 +1,83 @@
1
+ import type { AccessUser, AuthSessionResolver } from '../access'
2
+ import type { DbFor } from '../db'
3
+ import type { EmailFacade } from '../email'
4
+ import type { AuthInstance, StorageFacade } from '../index'
5
+ import type { JobsRuntimeFacade } from '../jobs/define'
6
+ import type { RealtimeFacade } from '../realtime/facade'
7
+
8
+ import { resolveSession } from '../access'
9
+
10
+ export interface ApiContextDeps<
11
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
12
+ TEnv = Record<string, unknown>,
13
+ > {
14
+ db: DbFor<TSchema>
15
+ env: TEnv
16
+ storage: StorageFacade
17
+ email: EmailFacade
18
+ jobs: JobsRuntimeFacade
19
+ realtime: RealtimeFacade<TSchema>
20
+ auth: AuthInstance
21
+ authResolver?: AuthSessionResolver
22
+ }
23
+
24
+ export interface ApiContext<
25
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
26
+ TEnv = Record<string, unknown>,
27
+ > {
28
+ db: DbFor<TSchema>
29
+ env: TEnv
30
+ storage: StorageFacade
31
+ email: EmailFacade
32
+ jobs: JobsRuntimeFacade
33
+ realtime: RealtimeFacade<TSchema>
34
+ auth: AuthInstance
35
+ request: Request
36
+ resHeaders: Headers
37
+ getRawBody: () => Promise<string>
38
+ getSession: () => Promise<{
39
+ user: AccessUser | null
40
+ activeOrganizationId: string | null
41
+ }>
42
+ }
43
+
44
+ export function createApiContext<
45
+ TSchema extends Record<string, unknown> = Record<string, unknown>,
46
+ TEnv = Record<string, unknown>,
47
+ >(
48
+ deps: ApiContextDeps<TSchema, TEnv>,
49
+ request: Request,
50
+ ): ApiContext<TSchema, TEnv> {
51
+ // Reserve the body stream before a transport codec consumes `request`.
52
+ const rawBodyRequest = request.clone()
53
+ let rawBodyPromise: Promise<string> | undefined
54
+ let sessionPromise:
55
+ | Promise<{ user: AccessUser | null; activeOrganizationId: string | null }>
56
+ | undefined
57
+
58
+ const getSession = () => {
59
+ if (!sessionPromise) {
60
+ sessionPromise = resolveSession(deps.authResolver, request.headers)
61
+ }
62
+ return sessionPromise
63
+ }
64
+
65
+ const getRawBody = () => {
66
+ if (!rawBodyPromise) rawBodyPromise = rawBodyRequest.text()
67
+ return rawBodyPromise
68
+ }
69
+
70
+ return {
71
+ db: deps.db,
72
+ env: deps.env,
73
+ storage: deps.storage,
74
+ email: deps.email,
75
+ jobs: deps.jobs,
76
+ realtime: deps.realtime,
77
+ auth: deps.auth,
78
+ request,
79
+ resHeaders: new Headers(),
80
+ getRawBody,
81
+ getSession,
82
+ }
83
+ }