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/access.ts DELETED
@@ -1,552 +0,0 @@
1
- import { getTableColumns, getTableName, isTable } from 'drizzle-orm'
2
-
3
- import { INTERNAL_TABLE_NAMES } from './internal-tables'
4
-
5
- export const AUTH_TABLE_NAMES = new Set([
6
- 'user',
7
- 'session',
8
- 'account',
9
- 'verification',
10
- ])
11
-
12
- const EXPOSEABLE_AUTH_TABLES = new Set(['user'])
13
-
14
- export type AccessUser = {
15
- id: string
16
- email: string
17
- name?: string
18
- role?: string
19
- }
20
-
21
- export type AccessContext = {
22
- user: AccessUser | null
23
- request: Request
24
- row?: Record<string, unknown>
25
- body?: Record<string, unknown>
26
- session?: { activeOrganizationId: string | null } | null
27
- }
28
-
29
- export type OperationRule =
30
- | 'public'
31
- | 'authenticated'
32
- | 'owner'
33
- | 'deny'
34
- | ((ctx: AccessContext) => boolean | Promise<boolean>)
35
-
36
- export type ScopeMap = Record<string, string | string[]>
37
- export type ScopeResolver = (ctx: AccessContext) => ScopeMap
38
-
39
- export type CrudOperation = 'list' | 'get' | 'create' | 'update' | 'delete'
40
-
41
- const RESERVED_LIST_PARAMS = new Set([
42
- 'limit',
43
- 'offset',
44
- 'sort',
45
- 'order',
46
- 'q',
47
- 'cursor',
48
- 'count',
49
- ])
50
-
51
- export type SortOrder = 'asc' | 'desc'
52
-
53
- export type DefaultSort = {
54
- column: string
55
- order: SortOrder
56
- }
57
-
58
- export type TableAccessInput = {
59
- crud?: boolean
60
- /** Opt the BetterAuth `user` table into auto-CRUD (read profiles, owner-update image). */
61
- exposeAuthTable?: boolean
62
- ownerColumn?: string
63
- list?: OperationRule
64
- get?: OperationRule
65
- create?: OperationRule
66
- update?: OperationRule
67
- delete?: OperationRule
68
- /** Explicit write allowlist. Entries override matching system-readonly
69
- * defaults such as `updatedAt`; `id` remains immutable on update. */
70
- writableColumns?: string[]
71
- readonlyColumns?: string[]
72
- /** Columns matched by `?q=` on list — opt-in; omitted columns are never searched. */
73
- searchableColumns?: string[]
74
- /** Columns filterable via flat `?column=value` query params. */
75
- filterableColumns?: string[]
76
- /** Columns allowed in `?sort=`. Defaults to `['id']`. */
77
- sortableColumns?: string[]
78
- /** Default list ordering when `?sort` is omitted. Defaults to `{ column: 'id', order: 'desc' }`. */
79
- defaultSort?: DefaultSort
80
- scope?: {
81
- read?: ScopeResolver
82
- write?: ScopeResolver
83
- }
84
- }
85
-
86
- export type ResolvedTableAccess = {
87
- tableKey: string
88
- tableName: string
89
- enabled: boolean
90
- ownerColumn?: string
91
- list: OperationRule
92
- get: OperationRule
93
- create: OperationRule
94
- update: OperationRule
95
- delete: OperationRule
96
- writableColumns?: string[]
97
- readonlyColumns: string[]
98
- searchableColumns?: string[]
99
- filterableColumns: string[]
100
- sortableColumns: string[]
101
- defaultSort: DefaultSort
102
- readScope?: ScopeResolver
103
- writeScope?: ScopeResolver
104
- }
105
-
106
- export type ResolvedAccess = Map<string, ResolvedTableAccess>
107
-
108
- /**
109
- * Look up a table's resolved access by its physical table name.
110
- *
111
- * `ResolvedAccess` is keyed by schema export name, not table name, so every
112
- * consumer that starts from a physical name needs this scan. It lives here so
113
- * CRUD, realtime, and route validation cannot drift apart on which tables they
114
- * consider enabled.
115
- */
116
- export function tableEntryForName(
117
- access: ResolvedAccess,
118
- tableName: string,
119
- ): ResolvedTableAccess | undefined {
120
- for (const entry of access.values()) {
121
- if (entry.tableName === tableName) return entry
122
- }
123
- return undefined
124
- }
125
-
126
- const DEFAULT_READONLY = [
127
- 'id',
128
- 'createdAt',
129
- 'updatedAt',
130
- 'created_at',
131
- 'updated_at',
132
- ]
133
-
134
- function getSchemaTables<TSchema extends Record<string, unknown>>(
135
- schema: TSchema,
136
- ) {
137
- const tables: {
138
- key: string
139
- table: Parameters<typeof getTableName>[0]
140
- name: string
141
- columns: string[]
142
- }[] = []
143
- for (const [key, value] of Object.entries(schema)) {
144
- if (!isTable(value)) continue
145
- const name = getTableName(value)
146
- const columns = Object.keys(getTableColumns(value))
147
- tables.push({ key, table: value, name, columns })
148
- }
149
- return tables
150
- }
151
-
152
- function resolveListAccess(
153
- input: TableAccessInput,
154
- columns: string[],
155
- ): Pick<
156
- ResolvedTableAccess,
157
- 'filterableColumns' | 'sortableColumns' | 'defaultSort'
158
- > {
159
- const sortableColumns =
160
- input.sortableColumns ?? (columns.includes('id') ? ['id'] : [])
161
- const defaultSort: DefaultSort = input.defaultSort ?? {
162
- column: sortableColumns[0] ?? 'id',
163
- order: 'desc',
164
- }
165
-
166
- if (!columns.includes(defaultSort.column)) {
167
- throw new Error(
168
- `[bunderstack] defaultSort.column "${defaultSort.column}" is not a column on this table`,
169
- )
170
- }
171
- if (!sortableColumns.includes(defaultSort.column)) {
172
- throw new Error(
173
- `[bunderstack] defaultSort.column "${defaultSort.column}" must be listed in sortableColumns`,
174
- )
175
- }
176
-
177
- const filterableColumns = input.filterableColumns ?? []
178
- for (const col of filterableColumns) {
179
- if (RESERVED_LIST_PARAMS.has(col)) {
180
- throw new Error(
181
- `[bunderstack] filterableColumns cannot include reserved query param "${col}"`,
182
- )
183
- }
184
- if (!columns.includes(col)) {
185
- throw new Error(
186
- `[bunderstack] filterableColumns references unknown column "${col}"`,
187
- )
188
- }
189
- }
190
-
191
- for (const col of sortableColumns) {
192
- if (RESERVED_LIST_PARAMS.has(col)) {
193
- throw new Error(
194
- `[bunderstack] sortableColumns cannot include reserved query param "${col}"`,
195
- )
196
- }
197
- if (!columns.includes(col)) {
198
- throw new Error(
199
- `[bunderstack] sortableColumns references unknown column "${col}"`,
200
- )
201
- }
202
- }
203
-
204
- return { filterableColumns, sortableColumns, defaultSort }
205
- }
206
-
207
- function resolveDefaults(
208
- input: TableAccessInput,
209
- ownerColumn: string | undefined,
210
- columns: string[],
211
- ): Omit<ResolvedTableAccess, 'tableKey' | 'tableName' | 'enabled'> {
212
- const listAccess = resolveListAccess(input, columns)
213
- const explicitlyWritable = new Set(input.writableColumns ?? [])
214
- return {
215
- ownerColumn,
216
- list: input.list ?? 'public',
217
- get: input.get ?? 'public',
218
- create: input.create ?? 'public',
219
- update: input.update ?? (ownerColumn ? 'owner' : 'deny'),
220
- delete: input.delete ?? (ownerColumn ? 'owner' : 'deny'),
221
- writableColumns: input.writableColumns,
222
- readonlyColumns: [
223
- ...DEFAULT_READONLY.filter(
224
- (column) => column === 'id' || !explicitlyWritable.has(column),
225
- ),
226
- ...(input.readonlyColumns ?? []),
227
- ...(ownerColumn ? [ownerColumn] : []),
228
- ],
229
- searchableColumns: input.searchableColumns,
230
- readScope: input.scope?.read,
231
- writeScope: input.scope?.write,
232
- ...listAccess,
233
- }
234
- }
235
-
236
- function detectOwnerColumn(
237
- columns: string[],
238
- input?: TableAccessInput,
239
- ): string | undefined {
240
- if (input?.ownerColumn) return input.ownerColumn
241
- if (columns.includes('userId')) return 'userId'
242
- return undefined
243
- }
244
-
245
- export function validateAndResolveAccess<
246
- TSchema extends Record<string, unknown>,
247
- >(
248
- schema: TSchema,
249
- accessInput?: Record<string, TableAccessInput>,
250
- ): ResolvedAccess {
251
- const tables = getSchemaTables(schema)
252
- const tableByKey = new Map(tables.map((t) => [t.key, t]))
253
- const resolved: ResolvedAccess = new Map()
254
-
255
- if (accessInput) {
256
- for (const key of Object.keys(accessInput)) {
257
- if (!tableByKey.has(key)) {
258
- throw new Error(
259
- `[bunderstack] access.${key} does not match any table in schema`,
260
- )
261
- }
262
- const tableName = tableByKey.get(key)!.name
263
- const input = accessInput[key]
264
- if (AUTH_TABLE_NAMES.has(tableName)) {
265
- if (input?.crud === false) continue
266
- if (!EXPOSEABLE_AUTH_TABLES.has(tableName) || !input?.exposeAuthTable) {
267
- throw new Error(
268
- `[bunderstack] access.${key} cannot target auth table "${tableName}" — use { crud: false } to silence, or exposeAuthTable on user`,
269
- )
270
- }
271
- }
272
- }
273
- }
274
-
275
- for (const { key, name, columns } of tables) {
276
- const input = accessInput?.[key]
277
- if (input?.crud === false) continue
278
- if (INTERNAL_TABLE_NAMES.has(name)) continue
279
-
280
- if (AUTH_TABLE_NAMES.has(name)) {
281
- if (!input?.exposeAuthTable || !EXPOSEABLE_AUTH_TABLES.has(name)) continue
282
-
283
- const ownerColumn = input.ownerColumn ?? 'id'
284
- if (!columns.includes(ownerColumn)) {
285
- throw new Error(
286
- `[bunderstack] access.${key}.ownerColumn "${ownerColumn}" is not a column on table "${name}"`,
287
- )
288
- }
289
-
290
- const defaults = resolveDefaults(
291
- {
292
- ...input,
293
- create: input.create ?? 'deny',
294
- delete: input.delete ?? 'deny',
295
- },
296
- ownerColumn,
297
- columns,
298
- )
299
-
300
- const existingReadonly = defaults.readonlyColumns.filter((col) =>
301
- columns.includes(col),
302
- )
303
- const resolvedReadonly = [...new Set(existingReadonly)]
304
-
305
- for (const col of [
306
- ...(defaults.writableColumns ?? []),
307
- ...(defaults.searchableColumns ?? []),
308
- ...defaults.filterableColumns,
309
- ...defaults.sortableColumns,
310
- ...resolvedReadonly,
311
- ]) {
312
- if (!columns.includes(col)) {
313
- throw new Error(
314
- `[bunderstack] access.${key} references unknown column "${col}" on table "${name}"`,
315
- )
316
- }
317
- }
318
-
319
- resolved.set(key, {
320
- tableKey: key,
321
- tableName: name,
322
- enabled: true,
323
- ...defaults,
324
- readonlyColumns: resolvedReadonly,
325
- })
326
- continue
327
- }
328
-
329
- const ownerColumn = detectOwnerColumn(columns, input)
330
- const hasExplicitRules = input !== undefined
331
- const hasConventionOwner =
332
- ownerColumn !== undefined &&
333
- input?.ownerColumn === undefined &&
334
- columns.includes('userId')
335
-
336
- if (!hasExplicitRules && !hasConventionOwner) continue
337
- if (
338
- !ownerColumn &&
339
- input?.crud !== true &&
340
- !input?.scope?.read &&
341
- !input?.scope?.write
342
- )
343
- continue
344
-
345
- if (input?.ownerColumn && !columns.includes(input.ownerColumn)) {
346
- throw new Error(
347
- `[bunderstack] access.${key}.ownerColumn "${input.ownerColumn}" is not a column on table "${name}"`,
348
- )
349
- }
350
-
351
- const defaults = resolveDefaults(input ?? {}, ownerColumn, columns)
352
- const existingReadonly = defaults.readonlyColumns.filter((col) =>
353
- columns.includes(col),
354
- )
355
- const resolvedReadonly = [...new Set(existingReadonly)]
356
-
357
- for (const col of [
358
- ...(defaults.writableColumns ?? []),
359
- ...(defaults.searchableColumns ?? []),
360
- ...defaults.filterableColumns,
361
- ...defaults.sortableColumns,
362
- ...resolvedReadonly,
363
- ]) {
364
- if (!columns.includes(col)) {
365
- throw new Error(
366
- `[bunderstack] access.${key} references unknown column "${col}" on table "${name}"`,
367
- )
368
- }
369
- }
370
-
371
- const needsOwner = [defaults.update, defaults.delete].some(
372
- (r) => r === 'owner',
373
- )
374
- if (needsOwner && !ownerColumn) {
375
- throw new Error(
376
- `[bunderstack] access.${key} requires ownerColumn for owner-based update/delete rules`,
377
- )
378
- }
379
-
380
- resolved.set(key, {
381
- tableKey: key,
382
- tableName: name,
383
- enabled: true,
384
- ...defaults,
385
- readonlyColumns: resolvedReadonly,
386
- })
387
- }
388
-
389
- return resolved
390
- }
391
-
392
- export function defineAccess<
393
- TSchema extends Record<string, unknown>,
394
- const TRules extends Record<string, TableAccessInput>,
395
- >(schema: TSchema, rules: TRules): TRules {
396
- validateAndResolveAccess(schema, rules)
397
- return rules
398
- }
399
-
400
- export function rowMatchesScope(
401
- row: Record<string, unknown>,
402
- scope: ScopeMap,
403
- ): boolean {
404
- for (const [col, expected] of Object.entries(scope)) {
405
- const actual = row[col]
406
- if (actual == null) return false
407
- if (Array.isArray(expected)) {
408
- if (!expected.map(String).includes(String(actual))) return false
409
- } else if (String(actual) !== String(expected)) {
410
- return false
411
- }
412
- }
413
- return true
414
- }
415
-
416
- export function stampScope(
417
- values: Record<string, unknown>,
418
- scope: ScopeMap,
419
- ): Record<string, unknown> {
420
- const out = { ...values }
421
- for (const [col, expected] of Object.entries(scope)) {
422
- if (!Array.isArray(expected)) out[col] = expected
423
- }
424
- return out
425
- }
426
-
427
- export function checkAccessSync(
428
- rule: Exclude<
429
- OperationRule,
430
- (ctx: AccessContext) => boolean | Promise<boolean>
431
- >,
432
- ctx: AccessContext,
433
- ownerColumn?: string,
434
- ): { allowed: boolean } {
435
- if (rule === 'deny') return { allowed: false }
436
- if (rule === 'public') return { allowed: true }
437
- if (!ctx.user) return { allowed: false }
438
- if (rule === 'authenticated') return { allowed: true }
439
- if (rule === 'owner') {
440
- if (!ownerColumn) return { allowed: false }
441
- const owner = ctx.row?.[ownerColumn]
442
- return { allowed: owner != null && String(owner) === ctx.user.id }
443
- }
444
- return { allowed: false }
445
- }
446
-
447
- export async function checkAccess(
448
- rule: OperationRule,
449
- ctx: AccessContext,
450
- ownerColumn?: string,
451
- ): Promise<{ allowed: boolean; status: 401 | 403 }> {
452
- if (rule === 'deny') return { allowed: false, status: 403 }
453
-
454
- if (typeof rule === 'function') {
455
- return (await rule(ctx))
456
- ? { allowed: true, status: 403 }
457
- : { allowed: false, status: 403 }
458
- }
459
-
460
- if (rule === 'public') return { allowed: true, status: 403 }
461
-
462
- if (!ctx.user) return { allowed: false, status: 401 }
463
-
464
- if (rule === 'authenticated') return { allowed: true, status: 403 }
465
-
466
- if (rule === 'owner') {
467
- if (!ownerColumn) return { allowed: false, status: 403 }
468
- const rowOwner = ctx.row?.[ownerColumn] ?? ctx.body?.[ownerColumn]
469
- if (rowOwner == null) return { allowed: false, status: 403 }
470
- if (String(rowOwner) !== ctx.user.id) return { allowed: false, status: 403 }
471
- return { allowed: true, status: 403 }
472
- }
473
-
474
- return { allowed: false, status: 403 }
475
- }
476
-
477
- export function sanitizeWriteBody(
478
- body: Record<string, unknown>,
479
- access: ResolvedTableAccess,
480
- mode: 'create' | 'update',
481
- userId: string | null,
482
- ): Record<string, unknown> {
483
- const out: Record<string, unknown> = {}
484
- const readonly = new Set(access.readonlyColumns)
485
-
486
- for (const [key, value] of Object.entries(body)) {
487
- // On create, allow client-supplied `id` even if it appears in readonlyColumns,
488
- // but still respect an explicit writableColumns allowlist if provided.
489
- if (mode === 'create' && key === 'id') {
490
- if (access.writableColumns && !access.writableColumns.includes(key))
491
- continue
492
- out[key] = value
493
- continue
494
- }
495
- if (readonly.has(key)) continue
496
- if (access.writableColumns && !access.writableColumns.includes(key))
497
- continue
498
- out[key] = value
499
- }
500
-
501
- if (mode === 'create' && access.ownerColumn) {
502
- out[access.ownerColumn] = userId ?? null
503
- }
504
-
505
- if (mode === 'update' && access.ownerColumn) {
506
- delete out[access.ownerColumn]
507
- }
508
-
509
- return out
510
- }
511
-
512
- export type AuthSessionResolver = {
513
- api: {
514
- getSession: (opts: { headers: Headers }) => Promise<{
515
- user: { id: string; email: string; name?: string; role?: string } | null
516
- session?: { activeOrganizationId?: string | null } | null
517
- } | null>
518
- }
519
- }
520
-
521
- export async function resolveAccessUser(
522
- auth: AuthSessionResolver | undefined,
523
- headers: Headers,
524
- ): Promise<AccessUser | null> {
525
- if (!auth) return null
526
- const session = await auth.api.getSession({ headers })
527
- if (!session?.user) return null
528
- return {
529
- id: session.user.id,
530
- email: session.user.email,
531
- name: session.user.name,
532
- role: session.user.role,
533
- }
534
- }
535
-
536
- export async function resolveSession(
537
- auth: AuthSessionResolver | undefined,
538
- headers: Headers,
539
- ): Promise<{ user: AccessUser | null; activeOrganizationId: string | null }> {
540
- if (!auth) return { user: null, activeOrganizationId: null }
541
- const session = await auth.api.getSession({ headers })
542
- if (!session?.user) return { user: null, activeOrganizationId: null }
543
- return {
544
- user: {
545
- id: session.user.id,
546
- email: session.user.email,
547
- name: session.user.name,
548
- role: session.user.role,
549
- },
550
- activeOrganizationId: session.session?.activeOrganizationId ?? null,
551
- }
552
- }
@@ -1,106 +0,0 @@
1
- import type { InferRouterInputs, InferRouterOutputs } from '@orpc/server'
2
-
3
- import { pgTable, text } from 'drizzle-orm/pg-core'
4
- import * as v from 'valibot'
5
-
6
- import type { ExposedApiTables } from './types'
7
-
8
- import { pglite } from '../database/pglite'
9
- import { createBunderstack } from '../index'
10
-
11
- type Equal<A, B> =
12
- (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
13
- ? true
14
- : false
15
- type Expect<T extends true> = T
16
-
17
- const posts = pgTable('posts', {
18
- id: text('id').primaryKey(),
19
- title: text('title').notNull(),
20
- })
21
-
22
- const privateNotes = pgTable('private_notes', {
23
- id: text('id').primaryKey(),
24
- content: text('content').notNull(),
25
- })
26
-
27
- const ownedPosts = pgTable('owned_posts', {
28
- id: text('id').primaryKey(),
29
- userId: text('user_id').notNull(),
30
- })
31
-
32
- type ImplicitTables = ExposedApiTables<
33
- { posts: typeof posts; ownedPosts: typeof ownedPosts },
34
- undefined
35
- >
36
- type _ImplicitAccessHidesUnownedTable = Expect<
37
- Equal<'posts' extends ImplicitTables ? true : false, false>
38
- >
39
- type _ImplicitAccessIncludesConventionTable = Expect<
40
- Equal<'ownedPosts' extends ImplicitTables ? true : false, true>
41
- >
42
-
43
- const typedApp = await createBunderstack({
44
- schema: { posts, privateNotes },
45
- database: { adapter: pglite() },
46
- processEnv: {
47
- DATABASE_URL: 'memory://',
48
- BUNDERSTACK_ROLE: 'web',
49
- },
50
- access: {
51
- posts: { crud: true, list: 'public', create: 'public' },
52
- privateNotes: { crud: false },
53
- },
54
- api: (o) => ({
55
- stats: o.protected
56
- .input(v.object({ period: v.picklist(['day', 'week']) }))
57
- .output(
58
- v.object({ period: v.picklist(['day', 'week']), userId: v.string() }),
59
- )
60
- .handler(async ({ input, context }) => {
61
- const _userId: string = context.user.id
62
- const _db = context.db
63
- const _env = context.env
64
- return {
65
- period: input.period,
66
- userId: context.user.id,
67
- }
68
- }),
69
- }),
70
- })
71
-
72
- type Api = NonNullable<typeof typedApp.$inferClient>['api']
73
-
74
- type _HasPosts = Expect<Equal<'posts' extends keyof Api ? true : false, true>>
75
- type _HidesPrivateNotes = Expect<
76
- Equal<'privateNotes' extends keyof Api ? true : false, false>
77
- >
78
- type _HasStats = Expect<Equal<'stats' extends keyof Api ? true : false, true>>
79
-
80
- type PostsInputs = InferRouterInputs<Api>['posts']
81
- type PostsOutputs = InferRouterOutputs<Api>['posts']
82
- type IsAny<T> = 0 extends 1 & T ? true : false
83
- type ExpectedUpdateInput = {
84
- params: { id: string }
85
- query?: Record<string, unknown>
86
- headers?: Record<string, unknown>
87
- body: { title?: string }
88
- }
89
-
90
- type _CreateInput = Expect<
91
- Equal<
92
- PostsInputs['create'],
93
- Partial<typeof posts.$inferInsert>
94
- >
95
- >
96
- type _GetInput = Expect<Equal<PostsInputs['get'], { id: string }>>
97
- type _UpdateInputToExpected = Expect<
98
- PostsInputs['update'] extends ExpectedUpdateInput ? true : false
99
- >
100
- type _ExpectedToUpdateInput = Expect<
101
- ExpectedUpdateInput extends PostsInputs['update'] ? true : false
102
- >
103
- type _ListItems = Expect<
104
- Equal<PostsOutputs['list']['items'], Array<{ id: string; title: string }>>
105
- >
106
- type _GetOutputIsTyped = Expect<Equal<IsAny<PostsOutputs['get']>, false>>
@@ -1,52 +0,0 @@
1
- import { os, type AnyRouter } from '@orpc/server'
2
-
3
- import type { ApiContext } from './context'
4
-
5
- import {
6
- BUNDERSTACK_ERRORS,
7
- BunderstackError,
8
- mapBunderstackErrors,
9
- } from '../errors'
10
- export type { ProtectedContextAdditions } from './types'
11
-
12
- export function createApiBuilder<
13
- TSchema extends Record<string, unknown> = Record<string, unknown>,
14
- TEnv = Record<string, unknown>,
15
- >() {
16
- const base = os
17
- .$context<ApiContext<TSchema, TEnv>>()
18
- .errors(BUNDERSTACK_ERRORS)
19
- .use(mapBunderstackErrors)
20
-
21
- const protectedProc = base.use(async ({ context, next }) => {
22
- const session = await context.getSession()
23
- if (!session.user) {
24
- throw new BunderstackError('UNAUTHORIZED', 'Authentication required')
25
- }
26
- return next({
27
- context: {
28
- user: session.user,
29
- session: {
30
- activeOrganizationId: session.activeOrganizationId,
31
- },
32
- },
33
- })
34
- })
35
-
36
- return {
37
- public: base,
38
- protected: protectedProc,
39
- webhook: base,
40
- }
41
- }
42
-
43
- export type BunderstackApiBuilder<
44
- TSchema extends Record<string, unknown>,
45
- TEnv = Record<string, unknown>,
46
- > = ReturnType<typeof createApiBuilder<TSchema, TEnv>>
47
-
48
- export type ApiFactory<
49
- TSchema extends Record<string, unknown>,
50
- TEnv,
51
- TCustomApiRouter extends AnyRouter,
52
- > = (builder: BunderstackApiBuilder<TSchema, TEnv>) => TCustomApiRouter