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/README.md
CHANGED
|
@@ -1,164 +1,51 @@
|
|
|
1
1
|
# bunderstack
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
-
|
|
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:
|
|
83
|
-
|
|
84
|
-
```ts
|
|
85
|
-
import { app } from './bunderstack'
|
|
86
|
-
|
|
87
|
-
await app.runWorker()
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Most applications need none of this: background work runs in-process by
|
|
91
|
-
default. Set `BUNDERSTACK_ROLE` to `web` or `worker` to split it across
|
|
92
|
-
processes without changing code.
|
|
93
|
-
|
|
94
|
-
If a queue handler calls `ctx.realtime.publish()`, the web and worker processes
|
|
95
|
-
must share a realtime transport. Configure `REDIS_URL` (or
|
|
96
|
-
`realtime: { redis: "redis://..." }`). `realtime: true` without Redis uses a
|
|
97
|
-
process-local memory broker and is suitable only when the worker is embedded
|
|
98
|
-
with `app.startWorker()`.
|
|
99
|
-
|
|
100
|
-
Since 0.9.0, `app.runWorker()` rejects that unsafe combination by default. If
|
|
101
|
-
queue handlers never publish realtime events, acknowledge the process-local
|
|
102
|
-
behavior with `app.runWorker({ allowProcessLocalRealtime: true })`.
|
|
103
|
-
|
|
104
|
-
Inspect the active runtime with `app.realtime.transport` (`'disabled'`,
|
|
105
|
-
`'memory'`, or `'redis'`). The generated blueprint declares only whether
|
|
106
|
-
realtime is required; the host chooses its shared transport and injects its
|
|
107
|
-
runtime configuration.
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
const app = await createBunderstack({
|
|
111
|
-
// ...
|
|
112
|
-
realtime: { redis: process.env.REDIS_URL! },
|
|
113
|
-
})
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
```bash
|
|
117
|
-
REDIS_URL=redis://localhost:6379 bun src/server.ts
|
|
118
|
-
REDIS_URL=redis://localhost:6379 bun src/worker.ts
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Cron tasks (`j.cron()`) are materialized as job rows keyed by their slot, so
|
|
122
|
-
they run through the same loop, retries, and timeouts as queue jobs. There is
|
|
123
|
-
no separate cron process and no signed dispatch endpoint.
|
|
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.
|
|
36
|
+
Generated CRUD writes publish automatically. After a custom write commits,
|
|
37
|
+
publish its complete returned row with:
|
|
130
38
|
|
|
131
39
|
```ts
|
|
132
|
-
|
|
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)
|
|
40
|
+
await context.realtime.publish(schema.posts, 'update', post)
|
|
139
41
|
```
|
|
140
42
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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.
|
|
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`.
|
|
150
46
|
|
|
151
|
-
|
|
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.
|
|
4
|
-
"description": "Batteries-included backend framework for Bun:
|
|
3
|
+
"version": "0.17.0-beta.2",
|
|
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
|
-
"
|
|
12
|
+
"orpc",
|
|
13
13
|
"realtime",
|
|
14
|
-
"
|
|
14
|
+
"typesafe"
|
|
15
15
|
],
|
|
16
16
|
"homepage": "https://github.com/kirill-dev-pro/bunderstack#readme",
|
|
17
17
|
"license": "MIT",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"files": [
|
|
27
27
|
"src",
|
|
28
28
|
"!src/**/*.test.ts",
|
|
29
|
+
"!src/**/*.types.ts",
|
|
29
30
|
"README.md",
|
|
30
31
|
"LICENSE"
|
|
31
32
|
],
|
|
@@ -47,9 +48,9 @@
|
|
|
47
48
|
"./typeid/pg": "./src/typeid-pg.ts",
|
|
48
49
|
"./env": "./src/env.ts",
|
|
49
50
|
"./blueprint": "./src/blueprint.ts",
|
|
50
|
-
"./trpc": "./src/trpc.ts",
|
|
51
51
|
"./cron": "./src/cron.ts",
|
|
52
|
-
"./email/smtp": "./src/email/smtp.ts"
|
|
52
|
+
"./email/smtp": "./src/email/smtp.ts",
|
|
53
|
+
"./api": "./src/api/types.ts"
|
|
53
54
|
},
|
|
54
55
|
"scripts": {
|
|
55
56
|
"test": "bun test",
|
|
@@ -59,32 +60,41 @@
|
|
|
59
60
|
"db:migrate": "drizzle-kit migrate"
|
|
60
61
|
},
|
|
61
62
|
"dependencies": {
|
|
62
|
-
"
|
|
63
|
+
"@standard-schema/spec": "1.1.0",
|
|
64
|
+
"valibot": "1.4.2",
|
|
63
65
|
"yaml": "^2.9.0"
|
|
64
66
|
},
|
|
65
67
|
"devDependencies": {
|
|
66
68
|
"@electric-sql/pglite": ">=0.3.0",
|
|
67
69
|
"@libsql/client": ">=0.14.0",
|
|
68
|
-
"@
|
|
70
|
+
"@orpc/bun": "2.0.0-beta.26",
|
|
71
|
+
"@orpc/json-schema": "2.0.0-beta.26",
|
|
72
|
+
"@orpc/openapi": "2.0.0-beta.26",
|
|
73
|
+
"@orpc/publisher": "2.0.0-beta.26",
|
|
74
|
+
"@orpc/server": "2.0.0-beta.26",
|
|
75
|
+
"@orpc/valibot": "2.0.0-beta.26",
|
|
69
76
|
"@types/nodemailer": "^6",
|
|
70
77
|
"better-auth": "^1.0.0",
|
|
71
78
|
"drizzle-kit": "^0.30.0",
|
|
72
79
|
"drizzle-orm": "^0.45.0",
|
|
73
|
-
"
|
|
74
|
-
"zod": "^4.4.3"
|
|
80
|
+
"drizzle-valibot": "0.4.2"
|
|
75
81
|
},
|
|
76
82
|
"peerDependencies": {
|
|
77
83
|
"@electric-sql/pglite": ">=0.3.0",
|
|
78
84
|
"@libsql/client": ">=0.14.0",
|
|
79
|
-
"@
|
|
85
|
+
"@orpc/bun": "2.0.0-beta.26",
|
|
86
|
+
"@orpc/json-schema": "2.0.0-beta.26",
|
|
87
|
+
"@orpc/openapi": "2.0.0-beta.26",
|
|
88
|
+
"@orpc/publisher": "2.0.0-beta.26",
|
|
89
|
+
"@orpc/server": "2.0.0-beta.26",
|
|
90
|
+
"@orpc/valibot": "2.0.0-beta.26",
|
|
80
91
|
"better-auth": "^1.0.0",
|
|
81
92
|
"drizzle-kit": "^0.30.0",
|
|
82
93
|
"drizzle-orm": "^0.45.0",
|
|
83
|
-
"
|
|
94
|
+
"drizzle-valibot": "0.4.2",
|
|
84
95
|
"nodemailer": ">=6 <10",
|
|
85
96
|
"postgres": ">=3.4.0",
|
|
86
|
-
"typescript": ">=5"
|
|
87
|
-
"zod": "^4.4.3"
|
|
97
|
+
"typescript": ">=5"
|
|
88
98
|
},
|
|
89
99
|
"peerDependenciesMeta": {
|
|
90
100
|
"@electric-sql/pglite": {
|
package/src/access.ts
CHANGED
|
@@ -38,16 +38,6 @@ export type ScopeResolver = (ctx: AccessContext) => ScopeMap
|
|
|
38
38
|
|
|
39
39
|
export type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete'
|
|
40
40
|
|
|
41
|
-
const RESERVED_LIST_PARAMS = new Set([
|
|
42
|
-
'limit',
|
|
43
|
-
'offset',
|
|
44
|
-
'sort',
|
|
45
|
-
'order',
|
|
46
|
-
'q',
|
|
47
|
-
'cursor',
|
|
48
|
-
'count',
|
|
49
|
-
])
|
|
50
|
-
|
|
51
41
|
export type SortOrder = 'asc' | 'desc'
|
|
52
42
|
|
|
53
43
|
export type DefaultSort = {
|
|
@@ -65,6 +55,8 @@ export type TableAccessInput = {
|
|
|
65
55
|
create?: OperationRule
|
|
66
56
|
update?: OperationRule
|
|
67
57
|
delete?: OperationRule
|
|
58
|
+
/** Explicit write allowlist. Entries override matching system-readonly
|
|
59
|
+
* defaults such as `updatedAt`; `id` remains immutable on update. */
|
|
68
60
|
writableColumns?: string[]
|
|
69
61
|
readonlyColumns?: string[]
|
|
70
62
|
/** Columns matched by `?q=` on list — opt-in; omitted columns are never searched. */
|
|
@@ -174,11 +166,6 @@ function resolveListAccess(
|
|
|
174
166
|
|
|
175
167
|
const filterableColumns = input.filterableColumns ?? []
|
|
176
168
|
for (const col of filterableColumns) {
|
|
177
|
-
if (RESERVED_LIST_PARAMS.has(col)) {
|
|
178
|
-
throw new Error(
|
|
179
|
-
`[bunderstack] filterableColumns cannot include reserved query param "${col}"`,
|
|
180
|
-
)
|
|
181
|
-
}
|
|
182
169
|
if (!columns.includes(col)) {
|
|
183
170
|
throw new Error(
|
|
184
171
|
`[bunderstack] filterableColumns references unknown column "${col}"`,
|
|
@@ -187,11 +174,6 @@ function resolveListAccess(
|
|
|
187
174
|
}
|
|
188
175
|
|
|
189
176
|
for (const col of sortableColumns) {
|
|
190
|
-
if (RESERVED_LIST_PARAMS.has(col)) {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`[bunderstack] sortableColumns cannot include reserved query param "${col}"`,
|
|
193
|
-
)
|
|
194
|
-
}
|
|
195
177
|
if (!columns.includes(col)) {
|
|
196
178
|
throw new Error(
|
|
197
179
|
`[bunderstack] sortableColumns references unknown column "${col}"`,
|
|
@@ -208,6 +190,7 @@ function resolveDefaults(
|
|
|
208
190
|
columns: string[],
|
|
209
191
|
): Omit<ResolvedTableAccess, 'tableKey' | 'tableName' | 'enabled'> {
|
|
210
192
|
const listAccess = resolveListAccess(input, columns)
|
|
193
|
+
const explicitlyWritable = new Set(input.writableColumns ?? [])
|
|
211
194
|
return {
|
|
212
195
|
ownerColumn,
|
|
213
196
|
list: input.list ?? 'public',
|
|
@@ -217,7 +200,9 @@ function resolveDefaults(
|
|
|
217
200
|
delete: input.delete ?? (ownerColumn ? 'owner' : 'deny'),
|
|
218
201
|
writableColumns: input.writableColumns,
|
|
219
202
|
readonlyColumns: [
|
|
220
|
-
...DEFAULT_READONLY
|
|
203
|
+
...DEFAULT_READONLY.filter(
|
|
204
|
+
(column) => column === 'id' || !explicitlyWritable.has(column),
|
|
205
|
+
),
|
|
221
206
|
...(input.readonlyColumns ?? []),
|
|
222
207
|
...(ownerColumn ? [ownerColumn] : []),
|
|
223
208
|
],
|
|
@@ -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
|
+
}
|