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.
Files changed (336) hide show
  1. package/CHANGELOG.md +233 -0
  2. package/README.md +11 -1
  3. package/dist/access.d.ts +116 -0
  4. package/dist/access.d.ts.map +1 -0
  5. package/dist/access.js +325 -0
  6. package/dist/access.js.map +1 -0
  7. package/dist/api/builder.d.ts +145 -0
  8. package/dist/api/builder.d.ts.map +1 -0
  9. package/dist/api/builder.js +28 -0
  10. package/dist/api/builder.js.map +1 -0
  11. package/dist/api/context.d.ts +34 -0
  12. package/dist/api/context.d.ts.map +1 -0
  13. package/dist/api/context.js +32 -0
  14. package/dist/api/context.js.map +1 -0
  15. package/dist/api/crud-router.d.ts +290 -0
  16. package/dist/api/crud-router.d.ts.map +1 -0
  17. package/dist/api/crud-router.js +249 -0
  18. package/dist/api/crud-router.js.map +1 -0
  19. package/dist/api/openapi.d.ts +6 -0
  20. package/dist/api/openapi.d.ts.map +1 -0
  21. package/dist/api/openapi.js +146 -0
  22. package/dist/api/openapi.js.map +1 -0
  23. package/dist/api/realtime-router.d.ts +76 -0
  24. package/dist/api/realtime-router.d.ts.map +1 -0
  25. package/dist/api/realtime-router.js +41 -0
  26. package/dist/api/realtime-router.js.map +1 -0
  27. package/dist/api/registry.d.ts +30 -0
  28. package/dist/api/registry.d.ts.map +1 -0
  29. package/dist/api/registry.js +239 -0
  30. package/dist/api/registry.js.map +1 -0
  31. package/dist/api/router.d.ts +9 -0
  32. package/dist/api/router.d.ts.map +1 -0
  33. package/dist/api/router.js +19 -0
  34. package/dist/api/router.js.map +1 -0
  35. package/dist/api/storage-router.d.ts +279 -0
  36. package/dist/api/storage-router.d.ts.map +1 -0
  37. package/dist/api/storage-router.js +155 -0
  38. package/dist/api/storage-router.js.map +1 -0
  39. package/dist/api/types.d.ts +55 -0
  40. package/dist/api/types.d.ts.map +1 -0
  41. package/dist/api/types.js +2 -0
  42. package/dist/api/types.js.map +1 -0
  43. package/dist/auth.d.ts +31 -0
  44. package/dist/auth.d.ts.map +1 -0
  45. package/dist/auth.js +102 -0
  46. package/dist/auth.js.map +1 -0
  47. package/dist/blueprint-generator.d.ts +18 -0
  48. package/dist/blueprint-generator.d.ts.map +1 -0
  49. package/dist/blueprint-generator.js +125 -0
  50. package/dist/blueprint-generator.js.map +1 -0
  51. package/dist/blueprint.d.ts +46 -0
  52. package/dist/blueprint.d.ts.map +1 -0
  53. package/dist/blueprint.js +162 -0
  54. package/dist/blueprint.js.map +1 -0
  55. package/dist/cli.d.ts +8 -0
  56. package/dist/cli.d.ts.map +1 -0
  57. package/dist/cli.js +68 -0
  58. package/dist/cli.js.map +1 -0
  59. package/dist/config.d.ts +108 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +90 -0
  62. package/dist/config.js.map +1 -0
  63. package/dist/cron.d.ts +4 -0
  64. package/dist/cron.d.ts.map +1 -0
  65. package/dist/cron.js +3 -0
  66. package/dist/cron.js.map +1 -0
  67. package/dist/crud-operations.d.ts +43 -0
  68. package/dist/crud-operations.d.ts.map +1 -0
  69. package/dist/crud-operations.js +238 -0
  70. package/dist/crud-operations.js.map +1 -0
  71. package/dist/database/adapter.d.ts +20 -0
  72. package/dist/database/adapter.d.ts.map +1 -0
  73. package/dist/database/adapter.js +2 -0
  74. package/dist/database/adapter.js.map +1 -0
  75. package/dist/database/bun-sql.d.ts +3 -0
  76. package/dist/database/bun-sql.d.ts.map +1 -0
  77. package/dist/database/bun-sql.js +21 -0
  78. package/dist/database/bun-sql.js.map +1 -0
  79. package/dist/database/libsql.d.ts +3 -0
  80. package/dist/database/libsql.d.ts.map +1 -0
  81. package/dist/database/libsql.js +22 -0
  82. package/dist/database/libsql.js.map +1 -0
  83. package/dist/database/pglite.d.ts +3 -0
  84. package/dist/database/pglite.d.ts.map +1 -0
  85. package/dist/database/pglite.js +27 -0
  86. package/dist/database/pglite.js.map +1 -0
  87. package/dist/database/postgres-js.d.ts +3 -0
  88. package/dist/database/postgres-js.d.ts.map +1 -0
  89. package/dist/database/postgres-js.js +21 -0
  90. package/dist/database/postgres-js.js.map +1 -0
  91. package/dist/db.d.ts +18 -0
  92. package/dist/db.d.ts.map +1 -0
  93. package/dist/db.js +26 -0
  94. package/dist/db.js.map +1 -0
  95. package/dist/dialect.d.ts +17 -0
  96. package/dist/dialect.d.ts.map +1 -0
  97. package/dist/dialect.js +22 -0
  98. package/dist/dialect.js.map +1 -0
  99. package/dist/email/smtp.d.ts +14 -0
  100. package/dist/email/smtp.d.ts.map +1 -0
  101. package/dist/email/smtp.js +27 -0
  102. package/dist/email/smtp.js.map +1 -0
  103. package/dist/email.d.ts +39 -0
  104. package/dist/email.d.ts.map +1 -0
  105. package/dist/email.js +97 -0
  106. package/dist/email.js.map +1 -0
  107. package/dist/env.d.ts +47 -0
  108. package/dist/env.d.ts.map +1 -0
  109. package/dist/env.js +99 -0
  110. package/dist/env.js.map +1 -0
  111. package/dist/errors.d.ts +125 -0
  112. package/dist/errors.d.ts.map +1 -0
  113. package/dist/errors.js +92 -0
  114. package/dist/errors.js.map +1 -0
  115. package/dist/handler.d.ts +9 -0
  116. package/dist/handler.d.ts.map +1 -0
  117. package/dist/handler.js +22 -0
  118. package/dist/handler.js.map +1 -0
  119. package/dist/idempotency.d.ts +17 -0
  120. package/dist/idempotency.d.ts.map +1 -0
  121. package/dist/idempotency.js +65 -0
  122. package/dist/idempotency.js.map +1 -0
  123. package/dist/index.d.ts +134 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +480 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/internal-tables-pg.d.ts +481 -0
  128. package/dist/internal-tables-pg.d.ts.map +1 -0
  129. package/{src/internal-tables-pg.ts → dist/internal-tables-pg.js} +10 -34
  130. package/dist/internal-tables-pg.js.map +1 -0
  131. package/dist/internal-tables.d.ts +2509 -0
  132. package/dist/internal-tables.d.ts.map +1 -0
  133. package/dist/internal-tables.js +108 -0
  134. package/dist/internal-tables.js.map +1 -0
  135. package/dist/jobs/cron.d.ts +15 -0
  136. package/dist/jobs/cron.d.ts.map +1 -0
  137. package/dist/jobs/cron.js +74 -0
  138. package/dist/jobs/cron.js.map +1 -0
  139. package/dist/jobs/define.d.ts +133 -0
  140. package/dist/jobs/define.d.ts.map +1 -0
  141. package/dist/jobs/define.js +74 -0
  142. package/dist/jobs/define.js.map +1 -0
  143. package/dist/jobs/index.d.ts +11 -0
  144. package/dist/jobs/index.d.ts.map +1 -0
  145. package/dist/jobs/index.js +8 -0
  146. package/dist/jobs/index.js.map +1 -0
  147. package/dist/jobs/queue.d.ts +6 -0
  148. package/dist/jobs/queue.d.ts.map +1 -0
  149. package/dist/jobs/queue.js +55 -0
  150. package/dist/jobs/queue.js.map +1 -0
  151. package/dist/jobs/runtime.d.ts +13 -0
  152. package/dist/jobs/runtime.d.ts.map +1 -0
  153. package/dist/jobs/runtime.js +46 -0
  154. package/dist/jobs/runtime.js.map +1 -0
  155. package/dist/jobs/slots.d.ts +25 -0
  156. package/dist/jobs/slots.d.ts.map +1 -0
  157. package/dist/jobs/slots.js +41 -0
  158. package/dist/jobs/slots.js.map +1 -0
  159. package/dist/jobs/worker.d.ts +12 -0
  160. package/dist/jobs/worker.d.ts.map +1 -0
  161. package/dist/jobs/worker.js +311 -0
  162. package/dist/jobs/worker.js.map +1 -0
  163. package/dist/lifecycle.d.ts +10 -0
  164. package/dist/lifecycle.d.ts.map +1 -0
  165. package/dist/lifecycle.js +37 -0
  166. package/dist/lifecycle.js.map +1 -0
  167. package/dist/list-query.d.ts +62 -0
  168. package/dist/list-query.d.ts.map +1 -0
  169. package/dist/list-query.js +194 -0
  170. package/dist/list-query.js.map +1 -0
  171. package/dist/manifest.d.ts +59 -0
  172. package/dist/manifest.d.ts.map +1 -0
  173. package/dist/manifest.js +167 -0
  174. package/dist/manifest.js.map +1 -0
  175. package/dist/provision-internals.d.ts +24 -0
  176. package/dist/provision-internals.d.ts.map +1 -0
  177. package/dist/provision-internals.js +7 -0
  178. package/dist/provision-internals.js.map +1 -0
  179. package/dist/provision.d.ts +23 -0
  180. package/dist/provision.d.ts.map +1 -0
  181. package/dist/provision.js +96 -0
  182. package/dist/provision.js.map +1 -0
  183. package/dist/rate-limit.d.ts +7 -0
  184. package/dist/rate-limit.d.ts.map +1 -0
  185. package/dist/rate-limit.js +49 -0
  186. package/dist/rate-limit.js.map +1 -0
  187. package/dist/realtime/facade.d.ts +18 -0
  188. package/dist/realtime/facade.d.ts.map +1 -0
  189. package/dist/realtime/facade.js +36 -0
  190. package/dist/realtime/facade.js.map +1 -0
  191. package/dist/realtime/filter.d.ts +13 -0
  192. package/dist/realtime/filter.d.ts.map +1 -0
  193. package/dist/realtime/filter.js +48 -0
  194. package/dist/realtime/filter.js.map +1 -0
  195. package/dist/realtime/heartbeat.d.ts +14 -0
  196. package/dist/realtime/heartbeat.d.ts.map +1 -0
  197. package/dist/realtime/heartbeat.js +64 -0
  198. package/dist/realtime/heartbeat.js.map +1 -0
  199. package/dist/realtime/publisher.d.ts +21 -0
  200. package/dist/realtime/publisher.d.ts.map +1 -0
  201. package/dist/realtime/publisher.js +17 -0
  202. package/dist/realtime/publisher.js.map +1 -0
  203. package/dist/schema-export-pg.d.ts +2 -0
  204. package/dist/schema-export-pg.d.ts.map +1 -0
  205. package/dist/schema-export-pg.js +4 -0
  206. package/dist/schema-export-pg.js.map +1 -0
  207. package/dist/schema-export.d.ts +2 -0
  208. package/dist/schema-export.d.ts.map +1 -0
  209. package/dist/schema-export.js +2 -0
  210. package/dist/schema-export.js.map +1 -0
  211. package/dist/scope.d.ts +4 -0
  212. package/dist/scope.d.ts.map +1 -0
  213. package/dist/scope.js +13 -0
  214. package/dist/scope.js.map +1 -0
  215. package/dist/standard-schema.d.ts +13 -0
  216. package/dist/standard-schema.d.ts.map +1 -0
  217. package/dist/standard-schema.js +34 -0
  218. package/dist/standard-schema.js.map +1 -0
  219. package/dist/storage/buckets.d.ts +80 -0
  220. package/dist/storage/buckets.d.ts.map +1 -0
  221. package/dist/storage/buckets.js +178 -0
  222. package/dist/storage/buckets.js.map +1 -0
  223. package/dist/storage/delete.d.ts +11 -0
  224. package/dist/storage/delete.d.ts.map +1 -0
  225. package/dist/storage/delete.js +18 -0
  226. package/dist/storage/delete.js.map +1 -0
  227. package/dist/storage/file-meta.d.ts +37 -0
  228. package/dist/storage/file-meta.d.ts.map +1 -0
  229. package/dist/storage/file-meta.js +166 -0
  230. package/dist/storage/file-meta.js.map +1 -0
  231. package/dist/storage/index.d.ts +28 -0
  232. package/dist/storage/index.d.ts.map +1 -0
  233. package/dist/storage/index.js +5 -0
  234. package/dist/storage/index.js.map +1 -0
  235. package/dist/storage/local.d.ts +15 -0
  236. package/dist/storage/local.d.ts.map +1 -0
  237. package/dist/storage/local.js +56 -0
  238. package/dist/storage/local.js.map +1 -0
  239. package/dist/storage/operations.d.ts +54 -0
  240. package/dist/storage/operations.d.ts.map +1 -0
  241. package/dist/storage/operations.js +250 -0
  242. package/dist/storage/operations.js.map +1 -0
  243. package/dist/storage/registry.d.ts +10 -0
  244. package/dist/storage/registry.d.ts.map +1 -0
  245. package/dist/storage/registry.js +23 -0
  246. package/dist/storage/registry.js.map +1 -0
  247. package/dist/storage/s3.d.ts +28 -0
  248. package/dist/storage/s3.d.ts.map +1 -0
  249. package/dist/storage/s3.js +71 -0
  250. package/dist/storage/s3.js.map +1 -0
  251. package/dist/storage/sweep.d.ts +9 -0
  252. package/dist/storage/sweep.d.ts.map +1 -0
  253. package/dist/storage/sweep.js +17 -0
  254. package/dist/storage/sweep.js.map +1 -0
  255. package/dist/storage/thumbnails.d.ts +11 -0
  256. package/dist/storage/thumbnails.d.ts.map +1 -0
  257. package/dist/storage/thumbnails.js +60 -0
  258. package/dist/storage/thumbnails.js.map +1 -0
  259. package/dist/testing.d.ts +27 -0
  260. package/dist/testing.d.ts.map +1 -0
  261. package/dist/testing.js +11 -0
  262. package/dist/testing.js.map +1 -0
  263. package/dist/typeid-pg.d.ts +19 -0
  264. package/dist/typeid-pg.d.ts.map +1 -0
  265. package/{src/typeid-pg.ts → dist/typeid-pg.js} +10 -13
  266. package/dist/typeid-pg.js.map +1 -0
  267. package/dist/typeid.d.ts +45 -0
  268. package/dist/typeid.d.ts.map +1 -0
  269. package/dist/typeid.js +100 -0
  270. package/dist/typeid.js.map +1 -0
  271. package/package.json +77 -28
  272. package/src/access.ts +0 -532
  273. package/src/api/builder.ts +0 -52
  274. package/src/api/context.ts +0 -83
  275. package/src/api/crud-router.ts +0 -386
  276. package/src/api/openapi.ts +0 -184
  277. package/src/api/realtime-router.ts +0 -75
  278. package/src/api/registry.ts +0 -338
  279. package/src/api/router.ts +0 -34
  280. package/src/api/storage-router.ts +0 -224
  281. package/src/api/types.ts +0 -105
  282. package/src/auth.ts +0 -117
  283. package/src/blueprint-generator.ts +0 -203
  284. package/src/blueprint.ts +0 -264
  285. package/src/cli.ts +0 -90
  286. package/src/config.ts +0 -215
  287. package/src/cron.ts +0 -3
  288. package/src/crud-operations.ts +0 -478
  289. package/src/database/adapter.ts +0 -26
  290. package/src/database/bun-sql.ts +0 -23
  291. package/src/database/libsql.ts +0 -28
  292. package/src/database/pglite.ts +0 -33
  293. package/src/database/postgres-js.ts +0 -33
  294. package/src/db.ts +0 -61
  295. package/src/dialect.ts +0 -38
  296. package/src/email/smtp.ts +0 -45
  297. package/src/email.ts +0 -154
  298. package/src/env.ts +0 -180
  299. package/src/errors.ts +0 -123
  300. package/src/handler.ts +0 -30
  301. package/src/idempotency.ts +0 -97
  302. package/src/index.ts +0 -834
  303. package/src/internal-tables.ts +0 -149
  304. package/src/jobs/cron.ts +0 -87
  305. package/src/jobs/define.ts +0 -283
  306. package/src/jobs/index.ts +0 -37
  307. package/src/jobs/queue.ts +0 -67
  308. package/src/jobs/runtime.ts +0 -58
  309. package/src/jobs/slots.ts +0 -52
  310. package/src/jobs/worker.ts +0 -369
  311. package/src/lifecycle.ts +0 -49
  312. package/src/list-query.ts +0 -333
  313. package/src/manifest.ts +0 -297
  314. package/src/provision-internals.ts +0 -29
  315. package/src/provision.ts +0 -128
  316. package/src/rate-limit.ts +0 -71
  317. package/src/realtime/facade.ts +0 -74
  318. package/src/realtime/filter.ts +0 -81
  319. package/src/realtime/heartbeat.ts +0 -80
  320. package/src/realtime/publisher.ts +0 -46
  321. package/src/schema-export-pg.ts +0 -7
  322. package/src/schema-export.ts +0 -5
  323. package/src/scope.ts +0 -17
  324. package/src/standard-schema.ts +0 -59
  325. package/src/storage/buckets.ts +0 -308
  326. package/src/storage/delete.ts +0 -26
  327. package/src/storage/file-meta.ts +0 -222
  328. package/src/storage/index.ts +0 -39
  329. package/src/storage/local.ts +0 -69
  330. package/src/storage/operations.ts +0 -398
  331. package/src/storage/registry.ts +0 -40
  332. package/src/storage/s3.ts +0 -99
  333. package/src/storage/sweep.ts +0 -25
  334. package/src/storage/thumbnails.ts +0 -65
  335. package/src/testing.ts +0 -31
  336. 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
- }
@@ -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
- }
@@ -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
- }
@@ -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'
@@ -1,5 +0,0 @@
1
- export {
2
- bunderstackFiles,
3
- bunderstackIdempotency,
4
- bunderstackJobs,
5
- } from './internal-tables'
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
- }
@@ -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
- }