bunderstack 0.17.0-beta.2 → 0.17.0-beta.3

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 +227 -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 +76 -0
  60. package/dist/config.d.ts.map +1 -0
  61. package/dist/config.js +81 -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 +477 -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/config.ts DELETED
@@ -1,215 +0,0 @@
1
- // src/config.ts
2
- import { betterAuth } from 'better-auth'
3
- import * as v from 'valibot'
4
-
5
- import type { AnyRouter } from '@orpc/server'
6
- import type { AuthSessionResolver, TableAccessInput } from './access'
7
- import type { BunderstackApiBuilder } from './api/builder'
8
- import type { DatabaseAdapter } from './database/adapter'
9
- import type { EmailConfigInput } from './email'
10
- import type { IdempotencyConfig } from './idempotency'
11
- import type { RateLimitConfig } from './rate-limit'
12
-
13
- import { validateEnv, type BaseEnv, type EnvConfigInput, type ValidatedEnv } from './env'
14
- import {
15
- resolveBuckets,
16
- type ResolvedStorageBuckets,
17
- type StorageConfigInput,
18
- } from './storage/buckets'
19
-
20
- export type BetterAuthConfig = Omit<
21
- NonNullable<Parameters<typeof betterAuth>[0]>,
22
- 'database'
23
- >
24
-
25
- // Only the union-shaped options need runtime validation: they are the ones a
26
- // JavaScript caller can plausibly get wrong in a way that fails confusingly
27
- // downstream. Everything else is either typed-only or read raw from `options`.
28
- const RuntimeOptionsSchema = v.object({
29
- rateLimit: v.optional(
30
- v.union([
31
- v.boolean(),
32
- v.object({
33
- windowMs: v.optional(v.number()),
34
- max: v.optional(v.number()),
35
- }),
36
- ]),
37
- ),
38
- idempotency: v.optional(
39
- v.union([v.boolean(), v.object({ ttlMs: v.optional(v.number()) })]),
40
- ),
41
- realtime: v.optional(
42
- v.union([
43
- v.boolean(),
44
- v.object({
45
- bufferSize: v.optional(v.number()),
46
- resumeSeconds: v.optional(v.number()),
47
- redis: v.optional(
48
- v.union([
49
- v.string(),
50
- v.object({ url: v.string(), token: v.optional(v.string()) }),
51
- ]),
52
- ),
53
- }),
54
- ]),
55
- ),
56
- openapi: v.optional(v.boolean()),
57
- })
58
-
59
- export type BunderstackConfig<
60
- TSchema extends Record<string, unknown>,
61
- TAccess extends Record<string, TableAccessInput> | undefined =
62
- | Record<string, TableAccessInput>
63
- | undefined,
64
- TStorage extends StorageConfigInput | undefined =
65
- | StorageConfigInput
66
- | undefined,
67
- TEnv extends EnvConfigInput | undefined = EnvConfigInput | undefined,
68
- TCustomApiRouter extends AnyRouter | undefined = AnyRouter | undefined,
69
- > = {
70
- schema: TSchema
71
- access?: TAccess
72
- database: {
73
- adapter: DatabaseAdapter
74
- url?: string
75
- authToken?: string
76
- migrations?: string
77
- }
78
- auth?: BetterAuthConfig
79
- /**
80
- * Reuse an application-owned session reader for the unified API while
81
- * keeping Bunderstack's auth handler available.
82
- */
83
- authResolver?: AuthSessionResolver
84
- storage?: TStorage
85
- env?: TEnv
86
- /**
87
- * Stand-in for `process.env`. Feeds both env validation and platform
88
- * overrides, so tests and embedders have one injection point instead of
89
- * three.
90
- */
91
- processEnv?: Record<string, string | undefined>
92
- background?: { autoStart?: boolean }
93
- email?: EmailConfigInput
94
- /**
95
- * Unified oRPC API builder callback.
96
- */
97
- api?: (
98
- builder: BunderstackApiBuilder<TSchema, ValidatedEnv<TEnv>>,
99
- ) => TCustomApiRouter
100
- rateLimit?: boolean | RateLimitConfig
101
- idempotency?: boolean | IdempotencyConfig
102
- /** Generate and serve `/api/openapi.json`. Disabled by default. */
103
- openapi?: boolean
104
- realtime?:
105
- | boolean
106
- | {
107
- bufferSize?: number
108
- resumeSeconds?: number
109
- redis?: string | { url: string; token?: string }
110
- }
111
- }
112
-
113
- export type ResolvedConfig = {
114
- database: {
115
- adapter: DatabaseAdapter
116
- url: string
117
- authToken?: string
118
- migrations: string
119
- }
120
- auth: BetterAuthConfig
121
- storage: ResolvedStorageBuckets
122
- realtime?:
123
- | boolean
124
- | {
125
- bufferSize?: number
126
- resumeSeconds?: number
127
- redis?: string | { url: string; token?: string }
128
- }
129
- }
130
-
131
- export function resolveConfig<
132
- TSchema extends Record<string, unknown>,
133
- TAccess extends Record<string, TableAccessInput> | undefined = undefined,
134
- TStorage extends StorageConfigInput | undefined = undefined,
135
- TEnv extends EnvConfigInput | undefined = undefined,
136
- TCustomApiRouter extends AnyRouter | undefined = undefined,
137
- >(
138
- options: BunderstackConfig<
139
- TSchema,
140
- TAccess,
141
- TStorage,
142
- TEnv,
143
- TCustomApiRouter
144
- >,
145
- env?: BaseEnv,
146
- // Platform-injected overrides (Bunderhost & co.) beat code-level config so
147
- // apps with hardcoded local urls deploy unchanged.
148
- platformSource: Record<string, string | undefined> = process.env as Record<
149
- string,
150
- string | undefined
151
- >,
152
- ): ResolvedConfig {
153
- const parsed = v.parse(RuntimeOptionsSchema, options)
154
- // Self-validate when the caller didn't pass a pre-validated env, so
155
- // resolveConfig stays usable standalone.
156
- const resolvedEnv =
157
- env ?? validateEnv(options.env as EnvConfigInput | undefined)
158
-
159
- const adapter = options.database?.adapter
160
- if (!adapter) {
161
- throw new Error('[bunderstack] database.adapter is required')
162
- }
163
-
164
- const defaultUrl =
165
- adapter.dialect === 'sqlite' ? 'file:./data.db' : 'file:./data.pglite'
166
-
167
- return {
168
- database: {
169
- adapter,
170
- url:
171
- platformSource['BUNDERSTACK_DATABASE_URL'] ??
172
- options.database?.url ??
173
- resolvedEnv.DATABASE_URL ??
174
- defaultUrl,
175
- authToken:
176
- platformSource['BUNDERSTACK_DATABASE_AUTH_TOKEN'] ??
177
- options.database?.authToken ??
178
- resolvedEnv.DATABASE_AUTH_TOKEN,
179
- migrations: options.database?.migrations ?? './migrations',
180
- },
181
- auth: (() => {
182
- const authInput = options.auth ?? {}
183
- return {
184
- ...authInput,
185
- secret: authInput.secret ?? resolvedEnv.AUTH_SECRET,
186
- }
187
- })(),
188
- storage: resolveBuckets(options.storage, platformSource),
189
- realtime: parsed.realtime,
190
- }
191
- }
192
-
193
- export function resolveRealtimeRedisUrl(
194
- realtime: ResolvedConfig['realtime'],
195
- env?: BaseEnv,
196
- platformSource: Record<string, string | undefined> = process.env as Record<
197
- string,
198
- string | undefined
199
- >,
200
- ): string | undefined {
201
- const platformRedis = platformSource['REDIS_URL']
202
- if (platformRedis) return platformRedis
203
-
204
- const envRedis = env?.REDIS_URL
205
- if (envRedis) return envRedis
206
-
207
- const fromConfig =
208
- typeof realtime === 'object' && realtime.redis
209
- ? typeof realtime.redis === 'string'
210
- ? realtime.redis
211
- : realtime.redis.url
212
- : undefined
213
-
214
- return fromConfig ?? undefined
215
- }
package/src/cron.ts DELETED
@@ -1,3 +0,0 @@
1
- export { cronMatches, parseCron } from './jobs/cron'
2
- export { floorSlot, slotsDue, CRON_PREFIX, SLOT_MS } from './jobs/slots'
3
- export type { CatchUp } from './jobs/slots'
@@ -1,478 +0,0 @@
1
- import {
2
- eq,
3
- getTableColumns,
4
- getTableName,
5
- isTable,
6
- type Table,
7
- } from 'drizzle-orm'
8
-
9
- import type { AnyDb } from './dialect'
10
- import type { RealtimeFacade } from './realtime/facade'
11
-
12
- import {
13
- checkAccess,
14
- rowMatchesScope,
15
- sanitizeWriteBody,
16
- stampScope,
17
- tableEntryForName,
18
- type AccessUser,
19
- type ResolvedAccess,
20
- type ScopeMap,
21
- type ScopeResolver,
22
- } from './access'
23
- import {
24
- BunderstackError,
25
- ErrorCode,
26
- ListQueryError,
27
- type BunderstackErrorCode,
28
- type ErrorCodeValue,
29
- } from './errors'
30
- import {
31
- lookupIdempotency,
32
- resolveIdempotencyConfig,
33
- storeIdempotency,
34
- type IdempotencyConfig,
35
- } from './idempotency'
36
- import {
37
- executeList,
38
- resolveListParams,
39
- type ListParamsInput,
40
- type ListResult,
41
- } from './list-query'
42
- import { buildScopeWhere } from './scope'
43
-
44
- export interface CrudExecutionContext {
45
- request: Request
46
- user: AccessUser | null
47
- session: { activeOrganizationId: string | null }
48
- }
49
-
50
- function errorCodeForStatus(status: number): BunderstackErrorCode {
51
- if (status === 401) return 'UNAUTHORIZED'
52
- if (status === 403) return 'FORBIDDEN'
53
- if (status === 404) return 'NOT_FOUND'
54
- if (status === 409) return 'CONFLICT'
55
- if (status === 413) return 'PAYLOAD_TOO_LARGE'
56
- if (status === 429) return 'TOO_MANY_REQUESTS'
57
- return 'BAD_REQUEST'
58
- }
59
-
60
- export class CrudOperationError extends BunderstackError {
61
- constructor(
62
- status: number,
63
- readonly legacyCode: ErrorCodeValue,
64
- message: string,
65
- details?: unknown,
66
- ) {
67
- const code = errorCodeForStatus(status)
68
- super(
69
- code,
70
- message,
71
- legacyCode === code
72
- ? details
73
- : {
74
- code: legacyCode,
75
- ...(details === undefined ? {} : { details }),
76
- },
77
- )
78
- this.name = 'CrudOperationError'
79
- }
80
- }
81
-
82
- export type CrudOperationsDeps<
83
- TSchema extends Record<string, unknown> = Record<string, unknown>,
84
- > = {
85
- schema: TSchema
86
- db: AnyDb
87
- access: ResolvedAccess
88
- idempotency?: boolean | IdempotencyConfig
89
- realtime?: RealtimeFacade<TSchema>
90
- }
91
-
92
- export type CreateResult =
93
- | { type: 'created'; status: 201; record: Record<string, unknown> }
94
- | {
95
- type: 'replay'
96
- status: number
97
- body: string
98
- record: Record<string, unknown>
99
- }
100
-
101
- function isRecord(value: unknown): value is Record<string, unknown> {
102
- return value !== null && typeof value === 'object' && !Array.isArray(value)
103
- }
104
-
105
- function isUniqueConstraintError(error: unknown): boolean {
106
- const seen = new Set<unknown>()
107
- let current = error
108
-
109
- while (isRecord(current) && !seen.has(current)) {
110
- seen.add(current)
111
- const code = current['code']
112
- if (
113
- code === '23505' ||
114
- code === 'SQLITE_CONSTRAINT_UNIQUE' ||
115
- code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
116
- ) {
117
- return true
118
- }
119
-
120
- const message = current['message']
121
- if (
122
- typeof message === 'string' &&
123
- (/duplicate key value violates unique constraint/i.test(message) ||
124
- /unique constraint failed/i.test(message))
125
- ) {
126
- return true
127
- }
128
-
129
- current = current['cause']
130
- }
131
-
132
- return false
133
- }
134
-
135
- function coerceId(rawId: string | number): string | number {
136
- if (typeof rawId === 'number') return rawId
137
- return isNaN(Number(rawId)) ? rawId : Number(rawId)
138
- }
139
-
140
- export function createCrudOperations<
141
- TSchema extends Record<string, unknown> = Record<string, unknown>,
142
- >(deps: CrudOperationsDeps<TSchema>) {
143
- const { schema, db, access, realtime } = deps
144
- const idempotency = resolveIdempotencyConfig(deps.idempotency)
145
-
146
- const scopeFor = (
147
- resolver: ScopeResolver | undefined,
148
- ctx: {
149
- user: AccessUser | null
150
- session: { activeOrganizationId: string | null }
151
- request: Request
152
- row?: Record<string, unknown>
153
- body?: Record<string, unknown>
154
- },
155
- ): ScopeMap | undefined => (resolver ? resolver(ctx) : undefined)
156
-
157
- function resolveTable(tableName: string) {
158
- const table = Object.values(schema).find(
159
- (t) => isTable(t) && getTableName(t) === tableName,
160
- ) as Table | undefined
161
- if (!table) {
162
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
163
- }
164
- const tableAccess = tableEntryForName(access, tableName)
165
- if (!tableAccess || !tableAccess.enabled) {
166
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
167
- }
168
- const idCol = getTableColumns(table)['id']
169
- if (!idCol) {
170
- throw new CrudOperationError(
171
- 400,
172
- ErrorCode.BAD_REQUEST,
173
- `Table ${tableName} has no id column`,
174
- )
175
- }
176
- return { table, tableAccess, idCol }
177
- }
178
-
179
- return {
180
- async list(
181
- tableName: string,
182
- params: ListParamsInput | undefined,
183
- ctx: CrudExecutionContext,
184
- ): Promise<ListResult<Record<string, unknown>>> {
185
- const { table, tableAccess, idCol } = resolveTable(tableName)
186
-
187
- const denied = await checkAccess(
188
- tableAccess.list,
189
- ctx,
190
- tableAccess.ownerColumn,
191
- )
192
- if (!denied.allowed) {
193
- throw new CrudOperationError(
194
- denied.status === 401 ? 401 : 403,
195
- ErrorCode.FORBIDDEN,
196
- 'Forbidden',
197
- )
198
- }
199
-
200
- try {
201
- const resolved = resolveListParams(params ?? {}, tableAccess)
202
- const scope = scopeFor(tableAccess.readScope, ctx)
203
- const scopeWhere = scope ? buildScopeWhere(table, scope) : undefined
204
- return await executeList(
205
- db,
206
- table,
207
- tableAccess,
208
- resolved,
209
- idCol,
210
- scopeWhere,
211
- )
212
- } catch (err) {
213
- if (err instanceof ListQueryError) {
214
- throw new CrudOperationError(400, err.code, err.message, err.details)
215
- }
216
- throw err
217
- }
218
- },
219
-
220
- async get(
221
- tableName: string,
222
- rawId: string | number,
223
- ctx: CrudExecutionContext,
224
- ): Promise<Record<string, unknown>> {
225
- const { table, tableAccess, idCol } = resolveTable(tableName)
226
- const id = coerceId(rawId)
227
-
228
- const rows = await db
229
- .select()
230
- .from(table)
231
- .where(eq(idCol, id))
232
- if (!rows[0]) {
233
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
234
- }
235
- const row = rows[0] as Record<string, unknown>
236
-
237
- const denied = await checkAccess(
238
- tableAccess.get,
239
- { ...ctx, row },
240
- tableAccess.ownerColumn,
241
- )
242
- if (!denied.allowed) {
243
- throw new CrudOperationError(
244
- denied.status === 401 ? 401 : 403,
245
- ErrorCode.FORBIDDEN,
246
- 'Forbidden',
247
- )
248
- }
249
-
250
- const scope = scopeFor(tableAccess.readScope, ctx)
251
- if (scope && !rowMatchesScope(row, scope)) {
252
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
253
- }
254
-
255
- return row
256
- },
257
-
258
- async create(
259
- tableName: string,
260
- body: unknown,
261
- rawBody: string | undefined,
262
- idempotencyKey: string | undefined,
263
- ctx: CrudExecutionContext,
264
- ): Promise<CreateResult> {
265
- const { table, tableAccess } = resolveTable(tableName)
266
-
267
- const denied = await checkAccess(
268
- tableAccess.create,
269
- ctx,
270
- tableAccess.ownerColumn,
271
- )
272
- if (!denied.allowed) {
273
- throw new CrudOperationError(
274
- denied.status === 401 ? 401 : 403,
275
- ErrorCode.FORBIDDEN,
276
- 'Forbidden',
277
- )
278
- }
279
-
280
- if (!isRecord(body)) {
281
- throw new CrudOperationError(
282
- 400,
283
- ErrorCode.BAD_REQUEST,
284
- 'Invalid JSON body',
285
- )
286
- }
287
-
288
- const trimmedKey = idempotencyKey?.trim()
289
- const effectiveRawBody = rawBody ?? JSON.stringify(body)
290
-
291
- if (idempotency && trimmedKey) {
292
- const lookup = await lookupIdempotency(
293
- db,
294
- tableName,
295
- trimmedKey,
296
- effectiveRawBody,
297
- idempotency,
298
- )
299
- if (lookup.type === 'conflict') {
300
- throw new CrudOperationError(
301
- 409,
302
- ErrorCode.IDEMPOTENCY_CONFLICT,
303
- 'Idempotency key reused with different body',
304
- )
305
- }
306
- if (lookup.type === 'replay') {
307
- return {
308
- type: 'replay',
309
- status: lookup.status,
310
- body: lookup.response,
311
- record: JSON.parse(lookup.response) as Record<string, unknown>,
312
- }
313
- }
314
- }
315
-
316
- const values = sanitizeWriteBody(
317
- body,
318
- tableAccess,
319
- 'create',
320
- ctx.user?.id ?? null,
321
- )
322
-
323
- const scope = scopeFor(tableAccess.writeScope, { ...ctx, body })
324
- const stamped = scope ? stampScope(values, scope) : values
325
-
326
- let rows: Record<string, unknown>[]
327
- try {
328
- rows = await db.insert(table).values(stamped).returning()
329
- } catch (error) {
330
- if (isUniqueConstraintError(error)) {
331
- throw new CrudOperationError(
332
- 409,
333
- ErrorCode.CONFLICT,
334
- 'Record already exists',
335
- )
336
- }
337
- throw error
338
- }
339
- const created = rows[0] as Record<string, unknown>
340
- void realtime?.publish(table as never, 'create', created as never)
341
-
342
- if (idempotency && trimmedKey) {
343
- await storeIdempotency(
344
- db,
345
- tableName,
346
- trimmedKey,
347
- effectiveRawBody,
348
- 201,
349
- created,
350
- idempotency,
351
- )
352
- }
353
-
354
- return {
355
- type: 'created',
356
- status: 201,
357
- record: created,
358
- }
359
- },
360
-
361
- async update(
362
- tableName: string,
363
- rawId: string | number,
364
- body: unknown,
365
- ctx: CrudExecutionContext,
366
- ): Promise<Record<string, unknown>> {
367
- const { table, tableAccess, idCol } = resolveTable(tableName)
368
- const id = coerceId(rawId)
369
-
370
- const existing = await db
371
- .select()
372
- .from(table)
373
- .where(eq(idCol, id))
374
- if (!existing[0]) {
375
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
376
- }
377
- const existingRow = existing[0] as Record<string, unknown>
378
-
379
- const readScope = scopeFor(tableAccess.readScope, ctx)
380
- if (readScope && !rowMatchesScope(existingRow, readScope)) {
381
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
382
- }
383
-
384
- const denied = await checkAccess(
385
- tableAccess.update,
386
- { ...ctx, row: existingRow },
387
- tableAccess.ownerColumn,
388
- )
389
- if (!denied.allowed) {
390
- throw new CrudOperationError(
391
- denied.status === 401 ? 401 : 403,
392
- ErrorCode.FORBIDDEN,
393
- 'Forbidden',
394
- )
395
- }
396
-
397
- if (!isRecord(body)) {
398
- throw new CrudOperationError(
399
- 400,
400
- ErrorCode.BAD_REQUEST,
401
- 'Invalid JSON body',
402
- )
403
- }
404
-
405
- const values = sanitizeWriteBody(
406
- body,
407
- tableAccess,
408
- 'update',
409
- ctx.user?.id ?? null,
410
- )
411
-
412
- if (Object.keys(values).length === 0) {
413
- throw new CrudOperationError(
414
- 400,
415
- ErrorCode.BAD_REQUEST,
416
- 'No writable fields to update',
417
- )
418
- }
419
-
420
- const writeScope = scopeFor(tableAccess.writeScope, { ...ctx, body })
421
- const stamped = writeScope ? stampScope(values, writeScope) : values
422
-
423
- const rows = await db
424
- .update(table)
425
- .set(stamped)
426
- .where(eq(idCol, id))
427
- .returning()
428
-
429
- if (!rows[0]) {
430
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
431
- }
432
- const updated = rows[0] as Record<string, unknown>
433
- void realtime?.publish(table as never, 'update', updated as never)
434
- return updated
435
- },
436
-
437
- async delete(
438
- tableName: string,
439
- rawId: string | number,
440
- ctx: CrudExecutionContext,
441
- ): Promise<void> {
442
- const { table, tableAccess, idCol } = resolveTable(tableName)
443
- const id = coerceId(rawId)
444
-
445
- const existing = await db
446
- .select()
447
- .from(table)
448
- .where(eq(idCol, id))
449
- if (!existing[0]) {
450
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
451
- }
452
- const existingRow = existing[0] as Record<string, unknown>
453
-
454
- const readScope = scopeFor(tableAccess.readScope, ctx)
455
- if (readScope && !rowMatchesScope(existingRow, readScope)) {
456
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
457
- }
458
-
459
- const denied = await checkAccess(
460
- tableAccess.delete,
461
- { ...ctx, row: existingRow },
462
- tableAccess.ownerColumn,
463
- )
464
- if (!denied.allowed) {
465
- throw new CrudOperationError(
466
- denied.status === 401 ? 401 : 403,
467
- ErrorCode.FORBIDDEN,
468
- 'Forbidden',
469
- )
470
- }
471
-
472
- await db.delete(table).where(eq(idCol, id))
473
- void realtime?.publish(table as never, 'delete', existingRow as never)
474
- },
475
- }
476
- }
477
-
478
- export type CrudOperations = ReturnType<typeof createCrudOperations>