bunderstack 0.17.0-beta.0 → 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 (337) 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 +81 -29
  272. package/src/access.ts +0 -552
  273. package/src/api/api-types.types.ts +0 -106
  274. package/src/api/builder.ts +0 -52
  275. package/src/api/context.ts +0 -83
  276. package/src/api/crud-router.ts +0 -321
  277. package/src/api/openapi.ts +0 -184
  278. package/src/api/realtime-router.ts +0 -75
  279. package/src/api/registry.ts +0 -338
  280. package/src/api/router.ts +0 -34
  281. package/src/api/storage-router.ts +0 -224
  282. package/src/api/types.ts +0 -84
  283. package/src/auth.ts +0 -117
  284. package/src/blueprint-generator.ts +0 -203
  285. package/src/blueprint.ts +0 -264
  286. package/src/cli.ts +0 -90
  287. package/src/config.ts +0 -215
  288. package/src/cron.ts +0 -3
  289. package/src/crud-operations.ts +0 -488
  290. package/src/database/adapter.ts +0 -26
  291. package/src/database/bun-sql.ts +0 -23
  292. package/src/database/libsql.ts +0 -28
  293. package/src/database/pglite.ts +0 -33
  294. package/src/database/postgres-js.ts +0 -33
  295. package/src/db.ts +0 -61
  296. package/src/dialect.ts +0 -38
  297. package/src/email/smtp.ts +0 -45
  298. package/src/email.ts +0 -154
  299. package/src/env.ts +0 -180
  300. package/src/errors.ts +0 -118
  301. package/src/handler.ts +0 -30
  302. package/src/idempotency.ts +0 -97
  303. package/src/index.ts +0 -822
  304. package/src/internal-tables.ts +0 -149
  305. package/src/jobs/cron.ts +0 -87
  306. package/src/jobs/define.ts +0 -283
  307. package/src/jobs/index.ts +0 -37
  308. package/src/jobs/queue.ts +0 -67
  309. package/src/jobs/runtime.ts +0 -58
  310. package/src/jobs/slots.ts +0 -52
  311. package/src/jobs/worker.ts +0 -369
  312. package/src/lifecycle.ts +0 -49
  313. package/src/list-query.ts +0 -416
  314. package/src/manifest.ts +0 -297
  315. package/src/provision-internals.ts +0 -29
  316. package/src/provision.ts +0 -128
  317. package/src/rate-limit.ts +0 -71
  318. package/src/realtime/facade.ts +0 -55
  319. package/src/realtime/filter.ts +0 -77
  320. package/src/realtime/heartbeat.ts +0 -80
  321. package/src/realtime/publisher.ts +0 -46
  322. package/src/schema-export-pg.ts +0 -7
  323. package/src/schema-export.ts +0 -5
  324. package/src/scope.ts +0 -17
  325. package/src/standard-schema.ts +0 -59
  326. package/src/storage/buckets.ts +0 -308
  327. package/src/storage/delete.ts +0 -26
  328. package/src/storage/file-meta.ts +0 -222
  329. package/src/storage/index.ts +0 -39
  330. package/src/storage/local.ts +0 -69
  331. package/src/storage/operations.ts +0 -398
  332. package/src/storage/registry.ts +0 -40
  333. package/src/storage/s3.ts +0 -99
  334. package/src/storage/sweep.ts +0 -25
  335. package/src/storage/thumbnails.ts +0 -65
  336. package/src/testing.ts +0 -31
  337. 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,488 +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 ResolvedTableAccess,
21
- type ScopeMap,
22
- type ScopeResolver,
23
- } from './access'
24
- import {
25
- BunderstackError,
26
- ErrorCode,
27
- ListQueryError,
28
- type BunderstackErrorCode,
29
- type ErrorCodeValue,
30
- } from './errors'
31
- import {
32
- lookupIdempotency,
33
- resolveIdempotencyConfig,
34
- storeIdempotency,
35
- type IdempotencyConfig,
36
- } from './idempotency'
37
- import { executeList, parseListParams, type ListResult } from './list-query'
38
- import { buildScopeWhere } from './scope'
39
-
40
- export interface CrudExecutionContext {
41
- request: Request
42
- user: AccessUser | null
43
- session: { activeOrganizationId: string | null }
44
- }
45
-
46
- function errorCodeForStatus(status: number): BunderstackErrorCode {
47
- if (status === 401) return 'UNAUTHORIZED'
48
- if (status === 403) return 'FORBIDDEN'
49
- if (status === 404) return 'NOT_FOUND'
50
- if (status === 409) return 'CONFLICT'
51
- if (status === 413) return 'PAYLOAD_TOO_LARGE'
52
- if (status === 429) return 'RATE_LIMITED'
53
- return 'VALIDATION_ERROR'
54
- }
55
-
56
- export class CrudOperationError extends BunderstackError {
57
- constructor(
58
- status: number,
59
- readonly legacyCode: ErrorCodeValue,
60
- message: string,
61
- details?: unknown,
62
- ) {
63
- const code = errorCodeForStatus(status)
64
- super(
65
- code,
66
- message,
67
- legacyCode === code
68
- ? details
69
- : {
70
- code: legacyCode,
71
- ...(details === undefined ? {} : { details }),
72
- },
73
- )
74
- this.name = 'CrudOperationError'
75
- }
76
- }
77
-
78
- export type CrudOperationsDeps<
79
- TSchema extends Record<string, unknown> = Record<string, unknown>,
80
- > = {
81
- schema: TSchema
82
- db: AnyDb
83
- access: ResolvedAccess
84
- idempotency?: boolean | IdempotencyConfig
85
- realtime?: RealtimeFacade<TSchema>
86
- }
87
-
88
- export type CreateResult =
89
- | { type: 'created'; status: 201; record: Record<string, unknown> }
90
- | {
91
- type: 'replay'
92
- status: number
93
- body: string
94
- record: Record<string, unknown>
95
- }
96
-
97
- function isRecord(value: unknown): value is Record<string, unknown> {
98
- return value !== null && typeof value === 'object' && !Array.isArray(value)
99
- }
100
-
101
- function isUniqueConstraintError(error: unknown): boolean {
102
- const seen = new Set<unknown>()
103
- let current = error
104
-
105
- while (isRecord(current) && !seen.has(current)) {
106
- seen.add(current)
107
- const code = current['code']
108
- if (
109
- code === '23505' ||
110
- code === 'SQLITE_CONSTRAINT_UNIQUE' ||
111
- code === 'SQLITE_CONSTRAINT_PRIMARYKEY'
112
- ) {
113
- return true
114
- }
115
-
116
- const message = current['message']
117
- if (
118
- typeof message === 'string' &&
119
- (/duplicate key value violates unique constraint/i.test(message) ||
120
- /unique constraint failed/i.test(message))
121
- ) {
122
- return true
123
- }
124
-
125
- current = current['cause']
126
- }
127
-
128
- return false
129
- }
130
-
131
- function coerceId(rawId: string | number): string | number {
132
- if (typeof rawId === 'number') return rawId
133
- return isNaN(Number(rawId)) ? rawId : Number(rawId)
134
- }
135
-
136
- export function createCrudOperations<
137
- TSchema extends Record<string, unknown> = Record<string, unknown>,
138
- >(deps: CrudOperationsDeps<TSchema>) {
139
- const { schema, db, access, realtime } = deps
140
- const idempotency = resolveIdempotencyConfig(deps.idempotency)
141
-
142
- const scopeFor = (
143
- resolver: ScopeResolver | undefined,
144
- ctx: {
145
- user: AccessUser | null
146
- session: { activeOrganizationId: string | null }
147
- request: Request
148
- row?: Record<string, unknown>
149
- body?: Record<string, unknown>
150
- },
151
- ): ScopeMap | undefined => (resolver ? resolver(ctx) : undefined)
152
-
153
- function resolveTable(tableName: string) {
154
- const table = Object.values(schema).find(
155
- (t) => isTable(t) && getTableName(t) === tableName,
156
- ) as Table | undefined
157
- if (!table) {
158
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
159
- }
160
- const tableAccess = tableEntryForName(access, tableName)
161
- if (!tableAccess || !tableAccess.enabled) {
162
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
163
- }
164
- const idCol = getTableColumns(table)['id']
165
- if (!idCol) {
166
- throw new CrudOperationError(
167
- 400,
168
- ErrorCode.VALIDATION_ERROR,
169
- `Table ${tableName} has no id column`,
170
- )
171
- }
172
- return { table, tableAccess, idCol }
173
- }
174
-
175
- return {
176
- async list(
177
- tableName: string,
178
- paramsInput: URL | Record<string, unknown> | undefined,
179
- ctx: CrudExecutionContext,
180
- ): Promise<ListResult<Record<string, unknown>>> {
181
- const { table, tableAccess, idCol } = resolveTable(tableName)
182
-
183
- const denied = await checkAccess(
184
- tableAccess.list,
185
- ctx,
186
- tableAccess.ownerColumn,
187
- )
188
- if (!denied.allowed) {
189
- throw new CrudOperationError(
190
- denied.status === 401 ? 401 : 403,
191
- ErrorCode.FORBIDDEN,
192
- 'Forbidden',
193
- )
194
- }
195
-
196
- let urlObj: URL
197
- if (paramsInput instanceof URL) {
198
- urlObj = paramsInput
199
- } else {
200
- urlObj = new URL(ctx.request.url || 'http://localhost')
201
- if (paramsInput) {
202
- for (const [k, v] of Object.entries(paramsInput)) {
203
- if (v !== undefined && v !== null) {
204
- urlObj.searchParams.set(k, String(v))
205
- }
206
- }
207
- }
208
- }
209
-
210
- try {
211
- const params = parseListParams(urlObj, tableAccess)
212
- const scope = scopeFor(tableAccess.readScope, ctx)
213
- const scopeWhere = scope ? buildScopeWhere(table, scope) : undefined
214
- return await executeList(
215
- db,
216
- table,
217
- tableAccess,
218
- params,
219
- idCol,
220
- scopeWhere,
221
- )
222
- } catch (err) {
223
- if (err instanceof ListQueryError) {
224
- throw new CrudOperationError(400, err.code, err.message, err.details)
225
- }
226
- throw err
227
- }
228
- },
229
-
230
- async get(
231
- tableName: string,
232
- rawId: string | number,
233
- ctx: CrudExecutionContext,
234
- ): Promise<Record<string, unknown>> {
235
- const { table, tableAccess, idCol } = resolveTable(tableName)
236
- const id = coerceId(rawId)
237
-
238
- const rows = await db
239
- .select()
240
- .from(table)
241
- .where(eq(idCol, id))
242
- if (!rows[0]) {
243
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
244
- }
245
- const row = rows[0] as Record<string, unknown>
246
-
247
- const denied = await checkAccess(
248
- tableAccess.get,
249
- { ...ctx, row },
250
- tableAccess.ownerColumn,
251
- )
252
- if (!denied.allowed) {
253
- throw new CrudOperationError(
254
- denied.status === 401 ? 401 : 403,
255
- ErrorCode.FORBIDDEN,
256
- 'Forbidden',
257
- )
258
- }
259
-
260
- const scope = scopeFor(tableAccess.readScope, ctx)
261
- if (scope && !rowMatchesScope(row, scope)) {
262
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
263
- }
264
-
265
- return row
266
- },
267
-
268
- async create(
269
- tableName: string,
270
- body: unknown,
271
- rawBody: string | undefined,
272
- idempotencyKey: string | undefined,
273
- ctx: CrudExecutionContext,
274
- ): Promise<CreateResult> {
275
- const { table, tableAccess } = resolveTable(tableName)
276
-
277
- const denied = await checkAccess(
278
- tableAccess.create,
279
- ctx,
280
- tableAccess.ownerColumn,
281
- )
282
- if (!denied.allowed) {
283
- throw new CrudOperationError(
284
- denied.status === 401 ? 401 : 403,
285
- ErrorCode.FORBIDDEN,
286
- 'Forbidden',
287
- )
288
- }
289
-
290
- if (!isRecord(body)) {
291
- throw new CrudOperationError(
292
- 400,
293
- ErrorCode.VALIDATION_ERROR,
294
- 'Invalid JSON body',
295
- )
296
- }
297
-
298
- const trimmedKey = idempotencyKey?.trim()
299
- const effectiveRawBody = rawBody ?? JSON.stringify(body)
300
-
301
- if (idempotency && trimmedKey) {
302
- const lookup = await lookupIdempotency(
303
- db,
304
- tableName,
305
- trimmedKey,
306
- effectiveRawBody,
307
- idempotency,
308
- )
309
- if (lookup.type === 'conflict') {
310
- throw new CrudOperationError(
311
- 409,
312
- ErrorCode.IDEMPOTENCY_CONFLICT,
313
- 'Idempotency key reused with different body',
314
- )
315
- }
316
- if (lookup.type === 'replay') {
317
- return {
318
- type: 'replay',
319
- status: lookup.status,
320
- body: lookup.response,
321
- record: JSON.parse(lookup.response) as Record<string, unknown>,
322
- }
323
- }
324
- }
325
-
326
- const values = sanitizeWriteBody(
327
- body,
328
- tableAccess,
329
- 'create',
330
- ctx.user?.id ?? null,
331
- )
332
-
333
- const scope = scopeFor(tableAccess.writeScope, { ...ctx, body })
334
- const stamped = scope ? stampScope(values, scope) : values
335
-
336
- let rows: Record<string, unknown>[]
337
- try {
338
- rows = await db.insert(table).values(stamped).returning()
339
- } catch (error) {
340
- if (isUniqueConstraintError(error)) {
341
- throw new CrudOperationError(
342
- 409,
343
- ErrorCode.CONFLICT,
344
- 'Record already exists',
345
- )
346
- }
347
- throw error
348
- }
349
- const created = rows[0] as Record<string, unknown>
350
- void realtime?.publish(table as never, 'create', created as never)
351
-
352
- if (idempotency && trimmedKey) {
353
- await storeIdempotency(
354
- db,
355
- tableName,
356
- trimmedKey,
357
- effectiveRawBody,
358
- 201,
359
- created,
360
- idempotency,
361
- )
362
- }
363
-
364
- return {
365
- type: 'created',
366
- status: 201,
367
- record: created,
368
- }
369
- },
370
-
371
- async update(
372
- tableName: string,
373
- rawId: string | number,
374
- body: unknown,
375
- ctx: CrudExecutionContext,
376
- ): Promise<Record<string, unknown>> {
377
- const { table, tableAccess, idCol } = resolveTable(tableName)
378
- const id = coerceId(rawId)
379
-
380
- const existing = await db
381
- .select()
382
- .from(table)
383
- .where(eq(idCol, id))
384
- if (!existing[0]) {
385
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
386
- }
387
- const existingRow = existing[0] as Record<string, unknown>
388
-
389
- const readScope = scopeFor(tableAccess.readScope, ctx)
390
- if (readScope && !rowMatchesScope(existingRow, readScope)) {
391
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
392
- }
393
-
394
- const denied = await checkAccess(
395
- tableAccess.update,
396
- { ...ctx, row: existingRow },
397
- tableAccess.ownerColumn,
398
- )
399
- if (!denied.allowed) {
400
- throw new CrudOperationError(
401
- denied.status === 401 ? 401 : 403,
402
- ErrorCode.FORBIDDEN,
403
- 'Forbidden',
404
- )
405
- }
406
-
407
- if (!isRecord(body)) {
408
- throw new CrudOperationError(
409
- 400,
410
- ErrorCode.VALIDATION_ERROR,
411
- 'Invalid JSON body',
412
- )
413
- }
414
-
415
- const values = sanitizeWriteBody(
416
- body,
417
- tableAccess,
418
- 'update',
419
- ctx.user?.id ?? null,
420
- )
421
-
422
- if (Object.keys(values).length === 0) {
423
- throw new CrudOperationError(
424
- 400,
425
- ErrorCode.VALIDATION_ERROR,
426
- 'No writable fields to update',
427
- )
428
- }
429
-
430
- const writeScope = scopeFor(tableAccess.writeScope, { ...ctx, body })
431
- const stamped = writeScope ? stampScope(values, writeScope) : values
432
-
433
- const rows = await db
434
- .update(table)
435
- .set(stamped)
436
- .where(eq(idCol, id))
437
- .returning()
438
-
439
- if (!rows[0]) {
440
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
441
- }
442
- const updated = rows[0] as Record<string, unknown>
443
- void realtime?.publish(table as never, 'update', updated as never)
444
- return updated
445
- },
446
-
447
- async delete(
448
- tableName: string,
449
- rawId: string | number,
450
- ctx: CrudExecutionContext,
451
- ): Promise<void> {
452
- const { table, tableAccess, idCol } = resolveTable(tableName)
453
- const id = coerceId(rawId)
454
-
455
- const existing = await db
456
- .select()
457
- .from(table)
458
- .where(eq(idCol, id))
459
- if (!existing[0]) {
460
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
461
- }
462
- const existingRow = existing[0] as Record<string, unknown>
463
-
464
- const readScope = scopeFor(tableAccess.readScope, ctx)
465
- if (readScope && !rowMatchesScope(existingRow, readScope)) {
466
- throw new CrudOperationError(404, ErrorCode.NOT_FOUND, 'Not found')
467
- }
468
-
469
- const denied = await checkAccess(
470
- tableAccess.delete,
471
- { ...ctx, row: existingRow },
472
- tableAccess.ownerColumn,
473
- )
474
- if (!denied.allowed) {
475
- throw new CrudOperationError(
476
- denied.status === 401 ? 401 : 403,
477
- ErrorCode.FORBIDDEN,
478
- 'Forbidden',
479
- )
480
- }
481
-
482
- await db.delete(table).where(eq(idCol, id))
483
- void realtime?.publish(table as never, 'delete', existingRow as never)
484
- },
485
- }
486
- }
487
-
488
- export type CrudOperations = ReturnType<typeof createCrudOperations>