bunderstack 0.17.0-beta.2 → 0.17.0-beta.4
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/CHANGELOG.md +233 -0
- package/README.md +11 -1
- package/dist/access.d.ts +116 -0
- package/dist/access.d.ts.map +1 -0
- package/dist/access.js +325 -0
- package/dist/access.js.map +1 -0
- package/dist/api/builder.d.ts +145 -0
- package/dist/api/builder.d.ts.map +1 -0
- package/dist/api/builder.js +28 -0
- package/dist/api/builder.js.map +1 -0
- package/dist/api/context.d.ts +34 -0
- package/dist/api/context.d.ts.map +1 -0
- package/dist/api/context.js +32 -0
- package/dist/api/context.js.map +1 -0
- package/dist/api/crud-router.d.ts +290 -0
- package/dist/api/crud-router.d.ts.map +1 -0
- package/dist/api/crud-router.js +249 -0
- package/dist/api/crud-router.js.map +1 -0
- package/dist/api/openapi.d.ts +6 -0
- package/dist/api/openapi.d.ts.map +1 -0
- package/dist/api/openapi.js +146 -0
- package/dist/api/openapi.js.map +1 -0
- package/dist/api/realtime-router.d.ts +76 -0
- package/dist/api/realtime-router.d.ts.map +1 -0
- package/dist/api/realtime-router.js +41 -0
- package/dist/api/realtime-router.js.map +1 -0
- package/dist/api/registry.d.ts +30 -0
- package/dist/api/registry.d.ts.map +1 -0
- package/dist/api/registry.js +239 -0
- package/dist/api/registry.js.map +1 -0
- package/dist/api/router.d.ts +9 -0
- package/dist/api/router.d.ts.map +1 -0
- package/dist/api/router.js +19 -0
- package/dist/api/router.js.map +1 -0
- package/dist/api/storage-router.d.ts +279 -0
- package/dist/api/storage-router.d.ts.map +1 -0
- package/dist/api/storage-router.js +155 -0
- package/dist/api/storage-router.js.map +1 -0
- package/dist/api/types.d.ts +55 -0
- package/dist/api/types.d.ts.map +1 -0
- package/dist/api/types.js +2 -0
- package/dist/api/types.js.map +1 -0
- package/dist/auth.d.ts +31 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +102 -0
- package/dist/auth.js.map +1 -0
- package/dist/blueprint-generator.d.ts +18 -0
- package/dist/blueprint-generator.d.ts.map +1 -0
- package/dist/blueprint-generator.js +125 -0
- package/dist/blueprint-generator.js.map +1 -0
- package/dist/blueprint.d.ts +46 -0
- package/dist/blueprint.d.ts.map +1 -0
- package/dist/blueprint.js +162 -0
- package/dist/blueprint.js.map +1 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +68 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +108 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +90 -0
- package/dist/config.js.map +1 -0
- package/dist/cron.d.ts +4 -0
- package/dist/cron.d.ts.map +1 -0
- package/dist/cron.js +3 -0
- package/dist/cron.js.map +1 -0
- package/dist/crud-operations.d.ts +43 -0
- package/dist/crud-operations.d.ts.map +1 -0
- package/dist/crud-operations.js +238 -0
- package/dist/crud-operations.js.map +1 -0
- package/dist/database/adapter.d.ts +20 -0
- package/dist/database/adapter.d.ts.map +1 -0
- package/dist/database/adapter.js +2 -0
- package/dist/database/adapter.js.map +1 -0
- package/dist/database/bun-sql.d.ts +3 -0
- package/dist/database/bun-sql.d.ts.map +1 -0
- package/dist/database/bun-sql.js +21 -0
- package/dist/database/bun-sql.js.map +1 -0
- package/dist/database/libsql.d.ts +3 -0
- package/dist/database/libsql.d.ts.map +1 -0
- package/dist/database/libsql.js +22 -0
- package/dist/database/libsql.js.map +1 -0
- package/dist/database/pglite.d.ts +3 -0
- package/dist/database/pglite.d.ts.map +1 -0
- package/dist/database/pglite.js +27 -0
- package/dist/database/pglite.js.map +1 -0
- package/dist/database/postgres-js.d.ts +3 -0
- package/dist/database/postgres-js.d.ts.map +1 -0
- package/dist/database/postgres-js.js +21 -0
- package/dist/database/postgres-js.js.map +1 -0
- package/dist/db.d.ts +18 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +26 -0
- package/dist/db.js.map +1 -0
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.d.ts.map +1 -0
- package/dist/dialect.js +22 -0
- package/dist/dialect.js.map +1 -0
- package/dist/email/smtp.d.ts +14 -0
- package/dist/email/smtp.d.ts.map +1 -0
- package/dist/email/smtp.js +27 -0
- package/dist/email/smtp.js.map +1 -0
- package/dist/email.d.ts +39 -0
- package/dist/email.d.ts.map +1 -0
- package/dist/email.js +97 -0
- package/dist/email.js.map +1 -0
- package/dist/env.d.ts +47 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +99 -0
- package/dist/env.js.map +1 -0
- package/dist/errors.d.ts +125 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +92 -0
- package/dist/errors.js.map +1 -0
- package/dist/handler.d.ts +9 -0
- package/dist/handler.d.ts.map +1 -0
- package/dist/handler.js +22 -0
- package/dist/handler.js.map +1 -0
- package/dist/idempotency.d.ts +17 -0
- package/dist/idempotency.d.ts.map +1 -0
- package/dist/idempotency.js +65 -0
- package/dist/idempotency.js.map +1 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +480 -0
- package/dist/index.js.map +1 -0
- package/dist/internal-tables-pg.d.ts +481 -0
- package/dist/internal-tables-pg.d.ts.map +1 -0
- package/{src/internal-tables-pg.ts → dist/internal-tables-pg.js} +10 -34
- package/dist/internal-tables-pg.js.map +1 -0
- package/dist/internal-tables.d.ts +2509 -0
- package/dist/internal-tables.d.ts.map +1 -0
- package/dist/internal-tables.js +108 -0
- package/dist/internal-tables.js.map +1 -0
- package/dist/jobs/cron.d.ts +15 -0
- package/dist/jobs/cron.d.ts.map +1 -0
- package/dist/jobs/cron.js +74 -0
- package/dist/jobs/cron.js.map +1 -0
- package/dist/jobs/define.d.ts +133 -0
- package/dist/jobs/define.d.ts.map +1 -0
- package/dist/jobs/define.js +74 -0
- package/dist/jobs/define.js.map +1 -0
- package/dist/jobs/index.d.ts +11 -0
- package/dist/jobs/index.d.ts.map +1 -0
- package/dist/jobs/index.js +8 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/queue.d.ts +6 -0
- package/dist/jobs/queue.d.ts.map +1 -0
- package/dist/jobs/queue.js +55 -0
- package/dist/jobs/queue.js.map +1 -0
- package/dist/jobs/runtime.d.ts +13 -0
- package/dist/jobs/runtime.d.ts.map +1 -0
- package/dist/jobs/runtime.js +46 -0
- package/dist/jobs/runtime.js.map +1 -0
- package/dist/jobs/slots.d.ts +25 -0
- package/dist/jobs/slots.d.ts.map +1 -0
- package/dist/jobs/slots.js +41 -0
- package/dist/jobs/slots.js.map +1 -0
- package/dist/jobs/worker.d.ts +12 -0
- package/dist/jobs/worker.d.ts.map +1 -0
- package/dist/jobs/worker.js +311 -0
- package/dist/jobs/worker.js.map +1 -0
- package/dist/lifecycle.d.ts +10 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +37 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/list-query.d.ts +62 -0
- package/dist/list-query.d.ts.map +1 -0
- package/dist/list-query.js +194 -0
- package/dist/list-query.js.map +1 -0
- package/dist/manifest.d.ts +59 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +167 -0
- package/dist/manifest.js.map +1 -0
- package/dist/provision-internals.d.ts +24 -0
- package/dist/provision-internals.d.ts.map +1 -0
- package/dist/provision-internals.js +7 -0
- package/dist/provision-internals.js.map +1 -0
- package/dist/provision.d.ts +23 -0
- package/dist/provision.d.ts.map +1 -0
- package/dist/provision.js +96 -0
- package/dist/provision.js.map +1 -0
- package/dist/rate-limit.d.ts +7 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +49 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/realtime/facade.d.ts +18 -0
- package/dist/realtime/facade.d.ts.map +1 -0
- package/dist/realtime/facade.js +36 -0
- package/dist/realtime/facade.js.map +1 -0
- package/dist/realtime/filter.d.ts +13 -0
- package/dist/realtime/filter.d.ts.map +1 -0
- package/dist/realtime/filter.js +48 -0
- package/dist/realtime/filter.js.map +1 -0
- package/dist/realtime/heartbeat.d.ts +14 -0
- package/dist/realtime/heartbeat.d.ts.map +1 -0
- package/dist/realtime/heartbeat.js +64 -0
- package/dist/realtime/heartbeat.js.map +1 -0
- package/dist/realtime/publisher.d.ts +21 -0
- package/dist/realtime/publisher.d.ts.map +1 -0
- package/dist/realtime/publisher.js +17 -0
- package/dist/realtime/publisher.js.map +1 -0
- package/dist/schema-export-pg.d.ts +2 -0
- package/dist/schema-export-pg.d.ts.map +1 -0
- package/dist/schema-export-pg.js +4 -0
- package/dist/schema-export-pg.js.map +1 -0
- package/dist/schema-export.d.ts +2 -0
- package/dist/schema-export.d.ts.map +1 -0
- package/dist/schema-export.js +2 -0
- package/dist/schema-export.js.map +1 -0
- package/dist/scope.d.ts +4 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +13 -0
- package/dist/scope.js.map +1 -0
- package/dist/standard-schema.d.ts +13 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/standard-schema.js +34 -0
- package/dist/standard-schema.js.map +1 -0
- package/dist/storage/buckets.d.ts +80 -0
- package/dist/storage/buckets.d.ts.map +1 -0
- package/dist/storage/buckets.js +178 -0
- package/dist/storage/buckets.js.map +1 -0
- package/dist/storage/delete.d.ts +11 -0
- package/dist/storage/delete.d.ts.map +1 -0
- package/dist/storage/delete.js +18 -0
- package/dist/storage/delete.js.map +1 -0
- package/dist/storage/file-meta.d.ts +37 -0
- package/dist/storage/file-meta.d.ts.map +1 -0
- package/dist/storage/file-meta.js +166 -0
- package/dist/storage/file-meta.js.map +1 -0
- package/dist/storage/index.d.ts +28 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/local.d.ts +15 -0
- package/dist/storage/local.d.ts.map +1 -0
- package/dist/storage/local.js +56 -0
- package/dist/storage/local.js.map +1 -0
- package/dist/storage/operations.d.ts +54 -0
- package/dist/storage/operations.d.ts.map +1 -0
- package/dist/storage/operations.js +250 -0
- package/dist/storage/operations.js.map +1 -0
- package/dist/storage/registry.d.ts +10 -0
- package/dist/storage/registry.d.ts.map +1 -0
- package/dist/storage/registry.js +23 -0
- package/dist/storage/registry.js.map +1 -0
- package/dist/storage/s3.d.ts +28 -0
- package/dist/storage/s3.d.ts.map +1 -0
- package/dist/storage/s3.js +71 -0
- package/dist/storage/s3.js.map +1 -0
- package/dist/storage/sweep.d.ts +9 -0
- package/dist/storage/sweep.d.ts.map +1 -0
- package/dist/storage/sweep.js +17 -0
- package/dist/storage/sweep.js.map +1 -0
- package/dist/storage/thumbnails.d.ts +11 -0
- package/dist/storage/thumbnails.d.ts.map +1 -0
- package/dist/storage/thumbnails.js +60 -0
- package/dist/storage/thumbnails.js.map +1 -0
- package/dist/testing.d.ts +27 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +11 -0
- package/dist/testing.js.map +1 -0
- package/dist/typeid-pg.d.ts +19 -0
- package/dist/typeid-pg.d.ts.map +1 -0
- package/{src/typeid-pg.ts → dist/typeid-pg.js} +10 -13
- package/dist/typeid-pg.js.map +1 -0
- package/dist/typeid.d.ts +45 -0
- package/dist/typeid.d.ts.map +1 -0
- package/dist/typeid.js +100 -0
- package/dist/typeid.js.map +1 -0
- package/package.json +77 -28
- package/src/access.ts +0 -532
- package/src/api/builder.ts +0 -52
- package/src/api/context.ts +0 -83
- package/src/api/crud-router.ts +0 -386
- package/src/api/openapi.ts +0 -184
- package/src/api/realtime-router.ts +0 -75
- package/src/api/registry.ts +0 -338
- package/src/api/router.ts +0 -34
- package/src/api/storage-router.ts +0 -224
- package/src/api/types.ts +0 -105
- package/src/auth.ts +0 -117
- package/src/blueprint-generator.ts +0 -203
- package/src/blueprint.ts +0 -264
- package/src/cli.ts +0 -90
- package/src/config.ts +0 -215
- package/src/cron.ts +0 -3
- package/src/crud-operations.ts +0 -478
- package/src/database/adapter.ts +0 -26
- package/src/database/bun-sql.ts +0 -23
- package/src/database/libsql.ts +0 -28
- package/src/database/pglite.ts +0 -33
- package/src/database/postgres-js.ts +0 -33
- package/src/db.ts +0 -61
- package/src/dialect.ts +0 -38
- package/src/email/smtp.ts +0 -45
- package/src/email.ts +0 -154
- package/src/env.ts +0 -180
- package/src/errors.ts +0 -123
- package/src/handler.ts +0 -30
- package/src/idempotency.ts +0 -97
- package/src/index.ts +0 -834
- package/src/internal-tables.ts +0 -149
- package/src/jobs/cron.ts +0 -87
- package/src/jobs/define.ts +0 -283
- package/src/jobs/index.ts +0 -37
- package/src/jobs/queue.ts +0 -67
- package/src/jobs/runtime.ts +0 -58
- package/src/jobs/slots.ts +0 -52
- package/src/jobs/worker.ts +0 -369
- package/src/lifecycle.ts +0 -49
- package/src/list-query.ts +0 -333
- package/src/manifest.ts +0 -297
- package/src/provision-internals.ts +0 -29
- package/src/provision.ts +0 -128
- package/src/rate-limit.ts +0 -71
- package/src/realtime/facade.ts +0 -74
- package/src/realtime/filter.ts +0 -81
- package/src/realtime/heartbeat.ts +0 -80
- package/src/realtime/publisher.ts +0 -46
- package/src/schema-export-pg.ts +0 -7
- package/src/schema-export.ts +0 -5
- package/src/scope.ts +0 -17
- package/src/standard-schema.ts +0 -59
- package/src/storage/buckets.ts +0 -308
- package/src/storage/delete.ts +0 -26
- package/src/storage/file-meta.ts +0 -222
- package/src/storage/index.ts +0 -39
- package/src/storage/local.ts +0 -69
- package/src/storage/operations.ts +0 -398
- package/src/storage/registry.ts +0 -40
- package/src/storage/s3.ts +0 -99
- package/src/storage/sweep.ts +0 -25
- package/src/storage/thumbnails.ts +0 -65
- package/src/testing.ts +0 -31
- package/src/typeid.ts +0 -116
package/src/provision.ts
DELETED
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
// src/provision.ts
|
|
2
|
-
import { access, mkdir } from 'node:fs/promises'
|
|
3
|
-
import { dirname, join } from 'node:path'
|
|
4
|
-
|
|
5
|
-
import type { AnyDb, Dialect } from './dialect'
|
|
6
|
-
|
|
7
|
-
import { detectDialect } from './dialect'
|
|
8
|
-
import {
|
|
9
|
-
PROVISION_INTERNALS,
|
|
10
|
-
type WithProvisionInternals,
|
|
11
|
-
} from './provision-internals'
|
|
12
|
-
|
|
13
|
-
/** Create the local backing directory for file-based urls, per dialect. */
|
|
14
|
-
async function ensureLocalDataDir(
|
|
15
|
-
url: string,
|
|
16
|
-
dialect: Dialect,
|
|
17
|
-
): Promise<void> {
|
|
18
|
-
if (dialect === 'pg') {
|
|
19
|
-
// PGlite data dir: `file:<dir>` or a bare path; server/memory urls need nothing.
|
|
20
|
-
if (/^postgres(ql)?:\/\//.test(url)) return
|
|
21
|
-
const raw = url.startsWith('file:') ? url.slice('file:'.length) : url
|
|
22
|
-
if (raw === ':memory:' || raw.startsWith('memory://')) return
|
|
23
|
-
await mkdir(raw, { recursive: true })
|
|
24
|
-
return
|
|
25
|
-
}
|
|
26
|
-
const match = /^file:(.+)$/.exec(url)
|
|
27
|
-
if (!match) return
|
|
28
|
-
const filePath = match[1]!
|
|
29
|
-
if (filePath === ':memory:') return
|
|
30
|
-
await mkdir(dirname(filePath), { recursive: true })
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function exists(path: string): Promise<boolean> {
|
|
34
|
-
try {
|
|
35
|
-
await access(path)
|
|
36
|
-
return true
|
|
37
|
-
} catch {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const DRIZZLE_KIT_HINT =
|
|
43
|
-
'[bunderstack] Schema push requires drizzle-kit, which is not installed.\n' +
|
|
44
|
-
' Development: run `bun add -d drizzle-kit` — provision() will push schema changes to the database on startup.\n' +
|
|
45
|
-
' Production: generate migrations locally with `bunx drizzle-kit generate` and commit the folder — provision() applies them without drizzle-kit.'
|
|
46
|
-
|
|
47
|
-
/** Push the merged schema to the database via drizzle-kit/api. */
|
|
48
|
-
export async function provisionSchema<TSchema extends Record<string, unknown>>(
|
|
49
|
-
db: AnyDb,
|
|
50
|
-
schema: TSchema,
|
|
51
|
-
options?: { force?: boolean; databaseUrl?: string },
|
|
52
|
-
): Promise<void> {
|
|
53
|
-
const dialect = detectDialect(schema)
|
|
54
|
-
if (options?.databaseUrl) {
|
|
55
|
-
await ensureLocalDataDir(options.databaseUrl, dialect)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let kit: typeof import('drizzle-kit/api')
|
|
59
|
-
try {
|
|
60
|
-
kit = await import('drizzle-kit/api')
|
|
61
|
-
} catch (cause) {
|
|
62
|
-
throw new Error(DRIZZLE_KIT_HINT, { cause })
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const result =
|
|
66
|
-
dialect === 'pg'
|
|
67
|
-
? await kit.pushSchema(schema, db as never)
|
|
68
|
-
: await kit.pushSQLiteSchema(schema, db as never)
|
|
69
|
-
|
|
70
|
-
if (result.hasDataLoss && !options?.force) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
'[bunderstack] Schema push would cause data loss. Run `bunx drizzle-kit push` or call provision(app, { force: true }).',
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (result.warnings.length > 0) {
|
|
77
|
-
for (const warning of result.warnings) {
|
|
78
|
-
console.warn(`[bunderstack] ${warning}`)
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (result.statementsToExecute.length === 0) return
|
|
83
|
-
|
|
84
|
-
await result.apply()
|
|
85
|
-
|
|
86
|
-
console.log(
|
|
87
|
-
`[bunderstack] provisioned ${result.statementsToExecute.length} schema change(s)`,
|
|
88
|
-
)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Provision the database for a Bunderstack app.
|
|
93
|
-
*
|
|
94
|
-
* The migrations folder is the mode switch:
|
|
95
|
-
* - `<migrations>/meta/_journal.json` exists → apply committed migrations via
|
|
96
|
-
* drizzle-orm's migrator. Pure runtime; drizzle-kit is never imported, so a
|
|
97
|
-
* fresh clone deploys with `bun install --production`.
|
|
98
|
-
* - No migrations → development: push the schema straight to the database via
|
|
99
|
-
* drizzle-kit (install it with `bun add -d drizzle-kit`).
|
|
100
|
-
*
|
|
101
|
-
* Once you run `bunx drizzle-kit generate` for the first time, provision stops
|
|
102
|
-
* pushing and every schema change goes through an explicit `generate`.
|
|
103
|
-
*/
|
|
104
|
-
export async function provision(
|
|
105
|
-
app: object,
|
|
106
|
-
options?: { force?: boolean },
|
|
107
|
-
): Promise<void> {
|
|
108
|
-
if (process.env.BUNDERSTACK_INTROSPECT === '1') return
|
|
109
|
-
|
|
110
|
-
const internals = (app as WithProvisionInternals)[PROVISION_INTERNALS]
|
|
111
|
-
if (!internals) {
|
|
112
|
-
throw new Error(
|
|
113
|
-
'[bunderstack] provision() expects the app returned by createBunderstack().',
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const { db, schema, databaseUrl, migrationsFolder, dialect, adapter } =
|
|
118
|
-
internals
|
|
119
|
-
const journal = join(migrationsFolder, 'meta', '_journal.json')
|
|
120
|
-
|
|
121
|
-
if (await exists(journal)) {
|
|
122
|
-
await ensureLocalDataDir(databaseUrl, dialect)
|
|
123
|
-
await adapter.migrate(db as never, migrationsFolder)
|
|
124
|
-
return
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
await provisionSchema(db, schema, { force: options?.force, databaseUrl })
|
|
128
|
-
}
|
package/src/rate-limit.ts
DELETED
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
export type RateLimitConfig = {
|
|
2
|
-
windowMs?: number
|
|
3
|
-
max?: number
|
|
4
|
-
skip?: (req: Request) => boolean
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
type Bucket = {
|
|
8
|
-
count: number
|
|
9
|
-
resetAt: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const buckets = new Map<string, Bucket>()
|
|
13
|
-
|
|
14
|
-
function resolveConfig(
|
|
15
|
-
config: boolean | RateLimitConfig | undefined,
|
|
16
|
-
): RateLimitConfig | null {
|
|
17
|
-
if (!config) return null
|
|
18
|
-
if (config === true) return {}
|
|
19
|
-
return config
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function clientKey(req: Request): string {
|
|
23
|
-
const forwarded = req.headers.get('x-forwarded-for')
|
|
24
|
-
if (forwarded) return forwarded.split(',')[0]!.trim()
|
|
25
|
-
return req.headers.get('x-real-ip') ?? 'local'
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function createRateLimiter(
|
|
29
|
-
config: boolean | RateLimitConfig | undefined,
|
|
30
|
-
): (req: Request) => Promise<Response | null> {
|
|
31
|
-
const resolved = resolveConfig(config)
|
|
32
|
-
if (!resolved) {
|
|
33
|
-
return async (_req: Request) => null
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const windowMs = resolved.windowMs ?? 60_000
|
|
37
|
-
const max = resolved.max ?? 100
|
|
38
|
-
|
|
39
|
-
return async (req: Request): Promise<Response | null> => {
|
|
40
|
-
if (resolved.skip?.(req)) return null
|
|
41
|
-
|
|
42
|
-
const key = `${clientKey(req)}:${new URL(req.url).pathname}`
|
|
43
|
-
const now = Date.now()
|
|
44
|
-
let bucket = buckets.get(key)
|
|
45
|
-
|
|
46
|
-
if (!bucket || bucket.resetAt <= now) {
|
|
47
|
-
bucket = { count: 0, resetAt: now + windowMs }
|
|
48
|
-
buckets.set(key, bucket)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
bucket.count += 1
|
|
52
|
-
if (bucket.count > max) {
|
|
53
|
-
const retryAfter = Math.ceil((bucket.resetAt - now) / 1000)
|
|
54
|
-
return new Response(
|
|
55
|
-
JSON.stringify({
|
|
56
|
-
error: 'Too many requests',
|
|
57
|
-
code: 'TOO_MANY_REQUESTS',
|
|
58
|
-
}),
|
|
59
|
-
{
|
|
60
|
-
status: 429,
|
|
61
|
-
headers: {
|
|
62
|
-
'Content-Type': 'application/json',
|
|
63
|
-
'Retry-After': String(retryAfter),
|
|
64
|
-
},
|
|
65
|
-
},
|
|
66
|
-
)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return null
|
|
70
|
-
}
|
|
71
|
-
}
|
package/src/realtime/facade.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getTableName,
|
|
3
|
-
isTable,
|
|
4
|
-
type InferSelectModel,
|
|
5
|
-
type Table,
|
|
6
|
-
} from 'drizzle-orm'
|
|
7
|
-
|
|
8
|
-
import type {
|
|
9
|
-
RealtimeAction,
|
|
10
|
-
RealtimePublisher,
|
|
11
|
-
} from './publisher'
|
|
12
|
-
|
|
13
|
-
export type RealtimeTransport = 'disabled' | 'memory' | 'redis'
|
|
14
|
-
|
|
15
|
-
export type SchemaTable<TSchema extends Record<string, unknown>> = Extract<
|
|
16
|
-
TSchema[keyof TSchema],
|
|
17
|
-
Table
|
|
18
|
-
>
|
|
19
|
-
|
|
20
|
-
export interface RealtimeFacade<
|
|
21
|
-
TSchema extends Record<string, unknown> = Record<string, unknown>,
|
|
22
|
-
> {
|
|
23
|
-
readonly enabled: boolean
|
|
24
|
-
readonly transport: RealtimeTransport
|
|
25
|
-
|
|
26
|
-
publish<TTable extends SchemaTable<TSchema>>(
|
|
27
|
-
table: TTable,
|
|
28
|
-
action: RealtimeAction,
|
|
29
|
-
record: InferSelectModel<TTable>,
|
|
30
|
-
): Promise<void>
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* One name for a table everywhere: events, subscriptions, and the CRUD router
|
|
35
|
-
* all use the schema key. Pass the schema so a key like `creditBalances` is not
|
|
36
|
-
* published as its SQL name `credit_balances` — clients subscribe with the key
|
|
37
|
-
* they call procedures with. Without a schema, the SQL name is the only name
|
|
38
|
-
* available and is used as-is.
|
|
39
|
-
*/
|
|
40
|
-
export function createRealtimeFacade<TSchema extends Record<string, unknown>>(
|
|
41
|
-
publisher?: RealtimePublisher,
|
|
42
|
-
transport: RealtimeTransport = publisher ? 'memory' : 'disabled',
|
|
43
|
-
schema?: TSchema,
|
|
44
|
-
): RealtimeFacade<TSchema> {
|
|
45
|
-
if (!publisher && transport !== 'disabled') {
|
|
46
|
-
throw new Error(
|
|
47
|
-
'[bunderstack] an enabled realtime transport requires a publisher',
|
|
48
|
-
)
|
|
49
|
-
}
|
|
50
|
-
if (publisher && transport === 'disabled') {
|
|
51
|
-
throw new Error(
|
|
52
|
-
'[bunderstack] a realtime publisher cannot use the disabled transport',
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const keyByTableName = new Map<string, string>()
|
|
57
|
-
for (const [key, value] of Object.entries(schema ?? {})) {
|
|
58
|
-
if (isTable(value)) keyByTableName.set(getTableName(value), key)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
enabled: publisher !== undefined,
|
|
63
|
-
transport,
|
|
64
|
-
async publish(table, action, record) {
|
|
65
|
-
if (!publisher) return
|
|
66
|
-
const tableName = getTableName(table)
|
|
67
|
-
await publisher.publish('change', {
|
|
68
|
-
table: keyByTableName.get(tableName) ?? tableName,
|
|
69
|
-
action,
|
|
70
|
-
record: record as unknown as Record<string, unknown>,
|
|
71
|
-
})
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
}
|
package/src/realtime/filter.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { getEventMeta, withEventMeta } from '@orpc/server'
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
AccessUser,
|
|
5
|
-
ResolvedAccess,
|
|
6
|
-
} from '../access'
|
|
7
|
-
import type { RealtimeChange } from './publisher'
|
|
8
|
-
|
|
9
|
-
import {
|
|
10
|
-
checkAccess,
|
|
11
|
-
rowMatchesScope,
|
|
12
|
-
tableEntryForName,
|
|
13
|
-
} from '../access'
|
|
14
|
-
|
|
15
|
-
export interface FilterRealtimeChangesOptions {
|
|
16
|
-
subscriptions: readonly string[]
|
|
17
|
-
access: ResolvedAccess
|
|
18
|
-
request: Request
|
|
19
|
-
getSession: () => Promise<{
|
|
20
|
-
user: AccessUser | null
|
|
21
|
-
activeOrganizationId: string | null
|
|
22
|
-
}>
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export async function* filterRealtimeChanges(
|
|
26
|
-
source: AsyncIterable<RealtimeChange>,
|
|
27
|
-
options: FilterRealtimeChangesOptions,
|
|
28
|
-
): AsyncGenerator<RealtimeChange, void, void> {
|
|
29
|
-
const subscriptions = new Set(options.subscriptions)
|
|
30
|
-
let sessionPromise:
|
|
31
|
-
| ReturnType<FilterRealtimeChangesOptions['getSession']>
|
|
32
|
-
| undefined
|
|
33
|
-
const getSession = () => (sessionPromise ??= options.getSession())
|
|
34
|
-
|
|
35
|
-
for await (const change of source) {
|
|
36
|
-
// Events name tables by schema key; the SQL-name lookup stays as a fallback
|
|
37
|
-
// for publishers outside the CRUD path that only know the physical name.
|
|
38
|
-
const entry =
|
|
39
|
-
options.access.get(change.table) ??
|
|
40
|
-
tableEntryForName(options.access, change.table)
|
|
41
|
-
if (!entry?.enabled) continue
|
|
42
|
-
|
|
43
|
-
const recordId = change.record.id
|
|
44
|
-
if (
|
|
45
|
-
!subscriptions.has(change.table) &&
|
|
46
|
-
(recordId == null ||
|
|
47
|
-
!subscriptions.has(`${change.table}/${String(recordId)}`))
|
|
48
|
-
) {
|
|
49
|
-
continue
|
|
50
|
-
}
|
|
51
|
-
if (entry.get === 'deny') continue
|
|
52
|
-
|
|
53
|
-
const needsSession = entry.get !== 'public' || entry.readScope !== undefined
|
|
54
|
-
const session = needsSession
|
|
55
|
-
? await getSession()
|
|
56
|
-
: { user: null, activeOrganizationId: null }
|
|
57
|
-
const context = {
|
|
58
|
-
request: options.request,
|
|
59
|
-
user: session.user,
|
|
60
|
-
row: change.record,
|
|
61
|
-
session: { activeOrganizationId: session.activeOrganizationId },
|
|
62
|
-
}
|
|
63
|
-
if (!(await checkAccess(entry.get, context, entry.ownerColumn)).allowed) {
|
|
64
|
-
continue
|
|
65
|
-
}
|
|
66
|
-
if (
|
|
67
|
-
entry.readScope &&
|
|
68
|
-
!rowMatchesScope(change.record, entry.readScope(context))
|
|
69
|
-
) {
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const projected: RealtimeChange = {
|
|
74
|
-
table: change.table,
|
|
75
|
-
action: change.action,
|
|
76
|
-
record: change.record,
|
|
77
|
-
}
|
|
78
|
-
const meta = getEventMeta(change)
|
|
79
|
-
yield meta ? withEventMeta(projected, meta) : projected
|
|
80
|
-
}
|
|
81
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
export const REALTIME_HEARTBEAT_INTERVAL_MS = 5_000
|
|
2
|
-
|
|
3
|
-
export type RealtimeHeartbeat = { type: 'heartbeat' }
|
|
4
|
-
|
|
5
|
-
type SourceState<T> =
|
|
6
|
-
| { status: 'pending' }
|
|
7
|
-
| { status: 'ready'; result: IteratorResult<T> }
|
|
8
|
-
| { status: 'error'; error: unknown }
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Emits a transport-only event whenever the source has been idle for an
|
|
12
|
-
* interval. Heartbeats are deliberately not published, persisted, or assigned
|
|
13
|
-
* event IDs, so they do not affect replay and resume semantics.
|
|
14
|
-
*/
|
|
15
|
-
export async function* withRealtimeHeartbeat<T>(
|
|
16
|
-
source: AsyncIterable<T>,
|
|
17
|
-
options: { intervalMs?: number; signal?: AbortSignal },
|
|
18
|
-
): AsyncGenerator<T | RealtimeHeartbeat, void, void> {
|
|
19
|
-
const intervalMs = Math.max(
|
|
20
|
-
1,
|
|
21
|
-
options.intervalMs ?? REALTIME_HEARTBEAT_INTERVAL_MS,
|
|
22
|
-
)
|
|
23
|
-
const iterator = source[Symbol.asyncIterator]()
|
|
24
|
-
let state: SourceState<T> = { status: 'pending' }
|
|
25
|
-
let wake: (() => void) | undefined
|
|
26
|
-
const getState = (): SourceState<T> => state
|
|
27
|
-
|
|
28
|
-
const requestNext = () => {
|
|
29
|
-
state = { status: 'pending' }
|
|
30
|
-
void iterator.next().then(
|
|
31
|
-
(result) => {
|
|
32
|
-
state = { status: 'ready', result }
|
|
33
|
-
wake?.()
|
|
34
|
-
},
|
|
35
|
-
(error: unknown) => {
|
|
36
|
-
state = { status: 'error', error }
|
|
37
|
-
wake?.()
|
|
38
|
-
},
|
|
39
|
-
)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
requestNext()
|
|
43
|
-
try {
|
|
44
|
-
while (!options.signal?.aborted) {
|
|
45
|
-
let current = getState()
|
|
46
|
-
if (current.status === 'pending') {
|
|
47
|
-
await new Promise<void>((resolve) => {
|
|
48
|
-
let settled = false
|
|
49
|
-
const finish = () => {
|
|
50
|
-
if (settled) return
|
|
51
|
-
settled = true
|
|
52
|
-
clearTimeout(timer)
|
|
53
|
-
options.signal?.removeEventListener('abort', finish)
|
|
54
|
-
resolve()
|
|
55
|
-
}
|
|
56
|
-
const timer = setTimeout(finish, intervalMs)
|
|
57
|
-
options.signal?.addEventListener('abort', finish, { once: true })
|
|
58
|
-
wake = finish
|
|
59
|
-
})
|
|
60
|
-
wake = undefined
|
|
61
|
-
|
|
62
|
-
if (options.signal?.aborted) break
|
|
63
|
-
current = getState()
|
|
64
|
-
if (current.status === 'pending') {
|
|
65
|
-
yield { type: 'heartbeat' }
|
|
66
|
-
continue
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (current.status === 'error') throw current.error
|
|
71
|
-
if (current.result.done) break
|
|
72
|
-
|
|
73
|
-
yield current.result.value
|
|
74
|
-
requestNext()
|
|
75
|
-
}
|
|
76
|
-
} finally {
|
|
77
|
-
wake = undefined
|
|
78
|
-
await iterator.return?.()
|
|
79
|
-
}
|
|
80
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { Publisher } from '@orpc/publisher'
|
|
2
|
-
import type { RedisClient } from 'bun'
|
|
3
|
-
|
|
4
|
-
import { BunRedisPublisher } from '@orpc/bun'
|
|
5
|
-
import { MemoryPublisher } from '@orpc/publisher/memory'
|
|
6
|
-
|
|
7
|
-
export type RealtimeAction = 'create' | 'update' | 'delete'
|
|
8
|
-
|
|
9
|
-
export interface RealtimeChange {
|
|
10
|
-
table: string
|
|
11
|
-
action: RealtimeAction
|
|
12
|
-
record: Record<string, unknown>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface RealtimeEvents extends Record<string, object> {
|
|
16
|
-
change: RealtimeChange
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type RealtimePublisher = Publisher<RealtimeEvents>
|
|
20
|
-
|
|
21
|
-
export interface RealtimePublisherOptions {
|
|
22
|
-
maxBufferedEvents?: number
|
|
23
|
-
resumeSeconds?: number
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function createMemoryRealtimePublisher(
|
|
27
|
-
options: RealtimePublisherOptions = {},
|
|
28
|
-
): RealtimePublisher {
|
|
29
|
-
return new MemoryPublisher<RealtimeEvents>({
|
|
30
|
-
maxBufferedEvents: options.maxBufferedEvents,
|
|
31
|
-
resume: { enabled: true, seconds: options.resumeSeconds ?? 300 },
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function createRedisRealtimePublisher(
|
|
36
|
-
redis: RedisClient,
|
|
37
|
-
subscriber: RedisClient | Promise<RedisClient>,
|
|
38
|
-
options: RealtimePublisherOptions & { prefix?: string } = {},
|
|
39
|
-
): RealtimePublisher {
|
|
40
|
-
return new BunRedisPublisher<RealtimeEvents>(redis, {
|
|
41
|
-
subscriber,
|
|
42
|
-
prefix: options.prefix ?? 'bunderstack:',
|
|
43
|
-
maxBufferedEvents: options.maxBufferedEvents,
|
|
44
|
-
resume: { enabled: true, seconds: options.resumeSeconds ?? 300 },
|
|
45
|
-
})
|
|
46
|
-
}
|
package/src/schema-export-pg.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
// src/schema-export-pg.ts — pg twins under the same names bunderstack/schema
|
|
2
|
-
// uses, so `export * from 'bunderstack/schema/pg'` mirrors the sqlite setup.
|
|
3
|
-
export {
|
|
4
|
-
bunderstackFilesPg as bunderstackFiles,
|
|
5
|
-
bunderstackIdempotencyPg as bunderstackIdempotency,
|
|
6
|
-
bunderstackJobsPg as bunderstackJobs,
|
|
7
|
-
} from './internal-tables-pg'
|
package/src/schema-export.ts
DELETED
package/src/scope.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { and, eq, getTableColumns, inArray, type SQL } from 'drizzle-orm'
|
|
2
|
-
|
|
3
|
-
import type { ScopeMap } from './access'
|
|
4
|
-
|
|
5
|
-
export function buildScopeWhere(
|
|
6
|
-
table: Parameters<typeof getTableColumns>[0],
|
|
7
|
-
scope: ScopeMap,
|
|
8
|
-
): SQL | undefined {
|
|
9
|
-
const columns = getTableColumns(table)
|
|
10
|
-
const conditions: SQL[] = []
|
|
11
|
-
for (const [name, value] of Object.entries(scope)) {
|
|
12
|
-
const col = columns[name]
|
|
13
|
-
if (!col) continue
|
|
14
|
-
conditions.push(Array.isArray(value) ? inArray(col, value) : eq(col, value))
|
|
15
|
-
}
|
|
16
|
-
return conditions.length ? and(...conditions) : undefined
|
|
17
|
-
}
|
package/src/standard-schema.ts
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
2
|
-
|
|
3
|
-
export type StandardSchema = StandardSchemaV1
|
|
4
|
-
export type InferStandardOutput<TSchema extends StandardSchemaV1> =
|
|
5
|
-
StandardSchemaV1.InferOutput<TSchema>
|
|
6
|
-
|
|
7
|
-
export type StandardSchemaIssue = {
|
|
8
|
-
path: PropertyKey[]
|
|
9
|
-
message: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export class StandardSchemaValidationError extends Error {
|
|
13
|
-
readonly issues: StandardSchemaIssue[]
|
|
14
|
-
|
|
15
|
-
constructor(label: string, issues: readonly StandardSchemaV1.Issue[]) {
|
|
16
|
-
const normalized = issues.map((issue) => ({
|
|
17
|
-
path: [...(issue.path ?? [])].map((segment) =>
|
|
18
|
-
typeof segment === 'object' ? segment.key : segment,
|
|
19
|
-
),
|
|
20
|
-
message: issue.message,
|
|
21
|
-
}))
|
|
22
|
-
super(
|
|
23
|
-
normalized
|
|
24
|
-
.map((issue) => {
|
|
25
|
-
const path = issue.path.map(String).join('.')
|
|
26
|
-
return `${label}${path ? `.${path}` : ''}: ${issue.message}`
|
|
27
|
-
})
|
|
28
|
-
.join('\n'),
|
|
29
|
-
)
|
|
30
|
-
this.name = 'StandardSchemaValidationError'
|
|
31
|
-
this.issues = normalized
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
36
|
-
return (
|
|
37
|
-
typeof value === 'object' &&
|
|
38
|
-
value !== null &&
|
|
39
|
-
'then' in value &&
|
|
40
|
-
typeof value.then === 'function'
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function validateStandardSchema<TSchema extends StandardSchemaV1>(
|
|
45
|
-
schema: TSchema,
|
|
46
|
-
value: unknown,
|
|
47
|
-
label: string,
|
|
48
|
-
): StandardSchemaV1.InferOutput<TSchema> {
|
|
49
|
-
const result = schema['~standard'].validate(value)
|
|
50
|
-
if (isPromiseLike(result)) {
|
|
51
|
-
throw new Error(
|
|
52
|
-
`[bunderstack] ${label} schema validation must be synchronous`,
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
if (result.issues) {
|
|
56
|
-
throw new StandardSchemaValidationError(label, result.issues)
|
|
57
|
-
}
|
|
58
|
-
return result.value
|
|
59
|
-
}
|