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/list-query.ts DELETED
@@ -1,416 +0,0 @@
1
- import {
2
- and,
3
- asc,
4
- desc,
5
- eq,
6
- getTableColumns,
7
- gt,
8
- ilike,
9
- inArray,
10
- is,
11
- like,
12
- lt,
13
- or,
14
- sql,
15
- type SQL,
16
- } from 'drizzle-orm'
17
- import { PgTable } from 'drizzle-orm/pg-core'
18
-
19
- import type { ResolvedTableAccess, SortOrder } from './access'
20
- import type { AnyDb } from './dialect'
21
-
22
- import { ErrorCode, ListQueryError } from './errors'
23
-
24
- /** Caps both `?limit=` and the number of values in a comma-separated `IN` filter. */
25
- export const MAX_LIST_LIMIT = 200
26
-
27
- export const RESERVED_LIST_PARAMS = new Set([
28
- 'limit',
29
- 'offset',
30
- 'sort',
31
- 'order',
32
- 'q',
33
- 'cursor',
34
- 'count',
35
- ])
36
-
37
- export type ParsedListParams = {
38
- limit: number
39
- offset?: number
40
- sort: string
41
- order: SortOrder
42
- q: string
43
- cursor?: string
44
- count: boolean
45
- filters: Record<string, unknown>
46
- }
47
-
48
- export type ListResult<T> = {
49
- items: T[]
50
- limit: number
51
- offset?: number
52
- cursor?: string
53
- nextCursor?: string
54
- hasMore: boolean
55
- total?: number
56
- q?: string
57
- sort: string
58
- order: SortOrder
59
- }
60
-
61
- type CursorPayload = {
62
- sort: string
63
- order: SortOrder
64
- v: string | number | null
65
- id: string | number
66
- }
67
-
68
- function isRecord(value: unknown): value is Record<string, unknown> {
69
- return value !== null && typeof value === 'object' && !Array.isArray(value)
70
- }
71
-
72
- function isCursorPayload(value: unknown): value is CursorPayload {
73
- if (!isRecord(value)) return false
74
- return (
75
- typeof value.sort === 'string' &&
76
- (value.order === 'asc' || value.order === 'desc') &&
77
- (value.v === null ||
78
- typeof value.v === 'string' ||
79
- typeof value.v === 'number') &&
80
- (typeof value.id === 'string' || typeof value.id === 'number')
81
- )
82
- }
83
-
84
- function parseBoolean(value: string | undefined): boolean {
85
- if (!value) return false
86
- const v = value.toLowerCase()
87
- return v === 'true' || v === '1'
88
- }
89
-
90
- function parseLimit(raw: string | undefined): number {
91
- if (raw === undefined || raw === '') return 20
92
- const n = Number(raw)
93
- if (!Number.isInteger(n) || n < 1) {
94
- throw new ListQueryError(
95
- `limit must be an integer between 1 and ${MAX_LIST_LIMIT}`,
96
- )
97
- }
98
- return Math.min(n, MAX_LIST_LIMIT)
99
- }
100
-
101
- function parseOffset(raw: string | undefined): number {
102
- if (raw === undefined || raw === '') return 0
103
- const n = Number(raw)
104
- if (!Number.isInteger(n) || n < 0) {
105
- throw new ListQueryError('offset must be a non-negative integer')
106
- }
107
- return n
108
- }
109
-
110
- function parseOrder(raw: string | undefined): SortOrder {
111
- if (!raw || raw === 'asc') return 'asc'
112
- if (raw === 'desc') return 'desc'
113
- throw new ListQueryError('order must be "asc" or "desc"')
114
- }
115
-
116
- export function parseListParams(
117
- url: URL,
118
- access: ResolvedTableAccess,
119
- ): ParsedListParams {
120
- const params = url.searchParams
121
- const limit = parseLimit(params.get('limit') ?? undefined)
122
- const cursor = params.get('cursor')?.trim() || undefined
123
- const hasOffset = params.has('offset') && params.get('offset') !== ''
124
- const offset = hasOffset
125
- ? parseOffset(params.get('offset') ?? undefined)
126
- : undefined
127
-
128
- if (cursor && hasOffset) {
129
- throw new ListQueryError('cursor and offset cannot be used together')
130
- }
131
-
132
- const sort = params.get('sort')?.trim() || access.defaultSort.column
133
- const order = params.has('order')
134
- ? parseOrder(params.get('order') ?? undefined)
135
- : params.has('sort')
136
- ? 'asc'
137
- : access.defaultSort.order
138
-
139
- if (!access.sortableColumns.includes(sort)) {
140
- throw new ListQueryError(`sort column "${sort}" is not allowed`)
141
- }
142
-
143
- const filters: Record<string, unknown> = {}
144
- for (const [key, value] of params.entries()) {
145
- if (RESERVED_LIST_PARAMS.has(key)) continue
146
- if (!access.filterableColumns.includes(key)) {
147
- throw new ListQueryError(`filter column "${key}" is not allowed`)
148
- }
149
- filters[key] = value === 'null' ? null : value
150
- }
151
-
152
- const q = params.get('q')?.trim().slice(0, 100) ?? ''
153
- const count = parseBoolean(params.get('count') ?? undefined)
154
-
155
- if (cursor) {
156
- const decoded = decodeCursor(cursor)
157
- if (decoded.sort !== sort || decoded.order !== order) {
158
- throw new ListQueryError(
159
- 'cursor does not match sort and order parameters',
160
- ErrorCode.INVALID_CURSOR,
161
- )
162
- }
163
- }
164
-
165
- return {
166
- limit,
167
- offset: cursor ? undefined : (offset ?? 0),
168
- sort,
169
- order,
170
- q,
171
- cursor,
172
- count,
173
- filters,
174
- }
175
- }
176
-
177
- function buildSearchWhere(
178
- table: Parameters<typeof getTableColumns>[0],
179
- searchableColumns: string[] | undefined,
180
- q: string,
181
- ): SQL | undefined {
182
- if (!q || !searchableColumns?.length) return undefined
183
- const columns = getTableColumns(table)
184
- const pattern = `%${q.replace(/[%_\\]/g, (ch) => `\\${ch}`)}%`
185
- // LIKE is case-insensitive in SQLite but case-sensitive in Postgres; use
186
- // ilike there so search behaves identically across dialects.
187
- const likeOp = is(table, PgTable) ? ilike : like
188
- const conditions = searchableColumns
189
- .filter((name) => name in columns)
190
- .map((name) => likeOp(columns[name]!, pattern))
191
- return conditions.length ? or(...conditions) : undefined
192
- }
193
-
194
- function coerceFilterValue(
195
- table: Parameters<typeof getTableColumns>[0],
196
- columnName: string,
197
- raw: unknown,
198
- ): unknown {
199
- if (raw === null) return null
200
- const col = getTableColumns(table)[columnName]
201
- if (!col) return raw
202
-
203
- const dataType = col.dataType
204
- if (
205
- dataType === 'number' ||
206
- dataType === 'integer' ||
207
- dataType === 'bigint'
208
- ) {
209
- const n = Number(raw)
210
- if (Number.isNaN(n)) {
211
- throw new ListQueryError(`filter "${columnName}" must be a number`)
212
- }
213
- return n
214
- }
215
- if (dataType === 'boolean') {
216
- const s = String(raw).toLowerCase()
217
- if (s === 'true' || s === '1') return true
218
- if (s === 'false' || s === '0') return false
219
- throw new ListQueryError(`filter "${columnName}" must be a boolean`)
220
- }
221
- if (dataType === 'date') {
222
- const d = new Date(raw as string | number)
223
- if (Number.isNaN(d.getTime())) {
224
- throw new ListQueryError(`filter "${columnName}" must be a valid date`)
225
- }
226
- return d
227
- }
228
- return String(raw)
229
- }
230
-
231
- function buildFilterWhere(
232
- table: Parameters<typeof getTableColumns>[0],
233
- filters: Record<string, unknown>,
234
- ): SQL | undefined {
235
- const columns = getTableColumns(table)
236
- const conditions: SQL[] = []
237
-
238
- for (const [name, raw] of Object.entries(filters)) {
239
- const col = columns[name]
240
- if (!col) continue
241
-
242
- if (raw === null) {
243
- conditions.push(sql`${col} IS NULL`)
244
- continue
245
- }
246
-
247
- // `?column=a,b,c` — TypeIDs and other filterable values never contain
248
- // commas, so this is a safe, zero-syntax way to do `column IN (...)`.
249
- if (typeof raw === 'string' && raw.includes(',')) {
250
- const parts = raw.split(',').filter((p) => p.length > 0)
251
- if (parts.length > MAX_LIST_LIMIT) {
252
- throw new ListQueryError(
253
- `filter "${name}" accepts at most ${MAX_LIST_LIMIT} comma-separated values`,
254
- )
255
- }
256
- const values = parts.map((p) => coerceFilterValue(table, name, p))
257
- conditions.push(inArray(col, values))
258
- continue
259
- }
260
-
261
- const value = coerceFilterValue(table, name, raw)
262
- if (value === null) {
263
- conditions.push(sql`${col} IS NULL`)
264
- } else {
265
- conditions.push(eq(col, value))
266
- }
267
- }
268
-
269
- return conditions.length ? and(...conditions) : undefined
270
- }
271
-
272
- function serializeCursorValue(value: unknown): string | number | null {
273
- if (value == null) return null
274
- if (value instanceof Date) return value.toISOString()
275
- if (typeof value === 'number' || typeof value === 'string') return value
276
- return String(value)
277
- }
278
-
279
- export function encodeCursor(payload: CursorPayload): string {
280
- return Buffer.from(JSON.stringify(payload)).toString('base64url')
281
- }
282
-
283
- export function decodeCursor(cursor: string): CursorPayload {
284
- try {
285
- const parsed = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'))
286
- if (!isCursorPayload(parsed)) {
287
- throw new Error('invalid cursor shape')
288
- }
289
- return parsed
290
- } catch {
291
- throw new ListQueryError('invalid cursor', ErrorCode.INVALID_CURSOR)
292
- }
293
- }
294
-
295
- function buildCursorWhere(
296
- table: Parameters<typeof getTableColumns>[0],
297
- sortColName: string,
298
- order: SortOrder,
299
- cursor: CursorPayload,
300
- idCol: unknown,
301
- ): SQL {
302
- const columns = getTableColumns(table)
303
- const sortCol = columns[sortColName]!
304
- const sortValue = coerceFilterValue(table, sortColName, cursor.v)
305
-
306
- if (order === 'desc') {
307
- return or(
308
- lt(sortCol, sortValue),
309
- and(eq(sortCol, sortValue), lt(idCol as never, cursor.id)),
310
- )!
311
- }
312
- return or(
313
- gt(sortCol, sortValue),
314
- and(eq(sortCol, sortValue), gt(idCol as never, cursor.id)),
315
- )!
316
- }
317
-
318
- function buildOrderBy(
319
- table: Parameters<typeof getTableColumns>[0],
320
- sortColName: string,
321
- order: SortOrder,
322
- idCol: unknown,
323
- ) {
324
- const columns = getTableColumns(table)
325
- const sortCol = columns[sortColName]!
326
- const idOrder = order === 'asc' ? asc(idCol as never) : desc(idCol as never)
327
- return order === 'asc' ? [asc(sortCol), idOrder] : [desc(sortCol), idOrder]
328
- }
329
-
330
- export async function executeList<T extends Record<string, unknown>>(
331
- db: AnyDb,
332
- table: Parameters<typeof getTableColumns>[0],
333
- access: ResolvedTableAccess,
334
- params: ParsedListParams,
335
- idCol: unknown,
336
- scopeWhere?: SQL,
337
- ): Promise<ListResult<T>> {
338
- const searchWhere = buildSearchWhere(
339
- table,
340
- access.searchableColumns,
341
- params.q,
342
- )
343
- const filterWhere = buildFilterWhere(table, params.filters)
344
- let where = and(
345
- ...(searchWhere ? [searchWhere] : []),
346
- ...(filterWhere ? [filterWhere] : []),
347
- ...(scopeWhere ? [scopeWhere] : []),
348
- )
349
-
350
- if (params.cursor) {
351
- const cursorPayload = decodeCursor(params.cursor)
352
- const cursorWhere = buildCursorWhere(
353
- table,
354
- params.sort,
355
- params.order,
356
- cursorPayload,
357
- idCol,
358
- )
359
- where = where ? and(where, cursorWhere) : cursorWhere
360
- }
361
-
362
- const orderBy = buildOrderBy(table, params.sort, params.order, idCol)
363
-
364
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
365
- let query = (db as any).select().from(table)
366
- if (where) query = query.where(where)
367
- query = query.orderBy(...orderBy)
368
-
369
- if (params.offset !== undefined) {
370
- query = query.limit(params.limit).offset(params.offset)
371
- } else {
372
- query = query.limit(params.limit)
373
- }
374
-
375
- const items = (await query) as T[]
376
-
377
- let total: number | undefined
378
- if (params.count) {
379
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
380
- let countQuery = (db as any)
381
- .select({ count: sql<number>`count(*)` })
382
- .from(table)
383
- if (where) countQuery = countQuery.where(where)
384
- const [row] = await countQuery
385
- total = Number(row?.count ?? 0)
386
- }
387
-
388
- const hasMore =
389
- total !== undefined && params.offset !== undefined
390
- ? params.offset + items.length < total
391
- : items.length === params.limit
392
-
393
- let nextCursor: string | undefined
394
- if (items.length === params.limit) {
395
- const last = items[items.length - 1]!
396
- nextCursor = encodeCursor({
397
- sort: params.sort,
398
- order: params.order,
399
- v: serializeCursorValue(last[params.sort]),
400
- id: last.id as string | number,
401
- })
402
- }
403
-
404
- return {
405
- items,
406
- limit: params.limit,
407
- ...(params.offset !== undefined ? { offset: params.offset } : {}),
408
- ...(params.cursor ? { cursor: params.cursor } : {}),
409
- ...(nextCursor ? { nextCursor } : {}),
410
- hasMore,
411
- ...(total !== undefined ? { total } : {}),
412
- ...(params.q ? { q: params.q } : {}),
413
- sort: params.sort,
414
- order: params.order,
415
- }
416
- }
package/src/manifest.ts DELETED
@@ -1,297 +0,0 @@
1
- import type { StandardSchemaV1 } from '@standard-schema/spec'
2
-
3
- import { getTableName, isTable } from 'drizzle-orm'
4
- import * as v from 'valibot'
5
-
6
- import type { Dialect } from './dialect'
7
- import type { EnvConfigInput } from './env'
8
- import type { JobsDefs } from './jobs/define'
9
- import type { ResolvedBucket, ResolvedStorageBuckets } from './storage/buckets'
10
-
11
- import {
12
- bunderstackFiles,
13
- bunderstackIdempotency,
14
- bunderstackJobs,
15
- } from './internal-tables'
16
- import { parseCron } from './jobs/cron'
17
- import {
18
- StandardSchemaValidationError,
19
- validateStandardSchema,
20
- } from './standard-schema'
21
-
22
- export type ManifestEnvVar = {
23
- key: string
24
- required: boolean
25
- scope: 'server' | 'client'
26
- }
27
-
28
- export type BunderstackManifest = {
29
- version: 3
30
- database: {
31
- dialect: Dialect
32
- migrationsDirectory: string
33
- tables: { exportName: string; physicalName: string; system: boolean }[]
34
- }
35
- storage: {
36
- defaultBucket: string
37
- buckets: { name: string; visibility: ResolvedBucket['visibility'] }[]
38
- }
39
- realtime: { required: boolean }
40
- environment: ManifestEnvVar[]
41
- background: {
42
- jobs: { name: string }[]
43
- cron: { name: string; schedule: string; timezone: 'UTC' }[]
44
- maintenance: {
45
- name: 'storage-sweep'
46
- schedule: string
47
- timezone: 'UTC'
48
- }[]
49
- }
50
- }
51
-
52
- const nonEmpty = v.pipe(v.string(), v.minLength(1))
53
- const migrationDirectory = v.pipe(
54
- nonEmpty,
55
- v.check(
56
- (value) =>
57
- value.startsWith('/') ||
58
- (!value.includes('\\') &&
59
- value.split('/').every((part) => part !== '..' && part !== '')),
60
- 'migrationsDirectory must be an absolute path or a relative path without traversal',
61
- ),
62
- )
63
- const cronSchedule = v.pipe(
64
- nonEmpty,
65
- v.check((value) => {
66
- try {
67
- parseCron(value)
68
- return true
69
- } catch {
70
- return false
71
- }
72
- }, 'invalid cron schedule'),
73
- )
74
-
75
- const manifestSchema = v.strictObject({
76
- version: v.literal(3),
77
- database: v.strictObject({
78
- dialect: v.picklist(['sqlite', 'pg']),
79
- migrationsDirectory: migrationDirectory,
80
- tables: v.array(
81
- v.strictObject({
82
- exportName: nonEmpty,
83
- physicalName: nonEmpty,
84
- system: v.boolean(),
85
- }),
86
- ),
87
- }),
88
- storage: v.strictObject({
89
- defaultBucket: nonEmpty,
90
- buckets: v.array(
91
- v.strictObject({
92
- name: nonEmpty,
93
- visibility: v.picklist(['public', 'private']),
94
- }),
95
- ),
96
- }),
97
- realtime: v.strictObject({ required: v.boolean() }),
98
- environment: v.array(
99
- v.strictObject({
100
- key: nonEmpty,
101
- required: v.boolean(),
102
- scope: v.picklist(['server', 'client']),
103
- }),
104
- ),
105
- background: v.strictObject({
106
- jobs: v.array(v.strictObject({ name: nonEmpty })),
107
- cron: v.array(
108
- v.strictObject({
109
- name: nonEmpty,
110
- schedule: cronSchedule,
111
- timezone: v.literal('UTC'),
112
- }),
113
- ),
114
- maintenance: v.array(
115
- v.strictObject({
116
- name: v.literal('storage-sweep'),
117
- schedule: cronSchedule,
118
- timezone: v.literal('UTC'),
119
- }),
120
- ),
121
- }),
122
- })
123
-
124
- function sortBy<T>(entries: readonly T[], key: (entry: T) => string): T[] {
125
- return [...entries].sort((left, right) => key(left).localeCompare(key(right)))
126
- }
127
-
128
- function rejectDuplicates(collection: string, values: readonly string[]): void {
129
- const seen = new Set<string>()
130
- for (const value of values) {
131
- if (seen.has(value))
132
- throw new Error(`[bunderstack] duplicate ${collection} "${value}"`)
133
- seen.add(value)
134
- }
135
- }
136
-
137
- function describeTables(schema: Record<string, unknown>) {
138
- const systemNames = new Set<string>(
139
- systemTables().map((table) => table.physicalName),
140
- )
141
- return sortBy(
142
- Object.entries(schema).flatMap(([exportName, value]) =>
143
- isTable(value) && !systemNames.has(getTableName(value))
144
- ? [{ exportName, physicalName: getTableName(value), system: false }]
145
- : [],
146
- ),
147
- (entry) => entry.physicalName,
148
- )
149
- }
150
-
151
- function describeSection(
152
- section: Record<string, StandardSchemaV1> | undefined,
153
- scope: ManifestEnvVar['scope'],
154
- ): ManifestEnvVar[] {
155
- return Object.entries(section ?? {}).map(([key, schema]) => {
156
- let required = false
157
- try {
158
- validateStandardSchema(schema, undefined, 'env')
159
- } catch (error) {
160
- if (!(error instanceof StandardSchemaValidationError)) throw error
161
- required = true
162
- }
163
- return { key, required, scope }
164
- })
165
- }
166
-
167
- function systemTables() {
168
- return [
169
- {
170
- exportName: '_system.files',
171
- physicalName: getTableName(bunderstackFiles),
172
- system: true,
173
- },
174
- {
175
- exportName: '_system.idempotency',
176
- physicalName: getTableName(bunderstackIdempotency),
177
- system: true,
178
- },
179
- {
180
- exportName: '_system.jobs',
181
- physicalName: getTableName(bunderstackJobs),
182
- system: true,
183
- },
184
- ]
185
- }
186
-
187
- export function parseManifest(value: unknown): BunderstackManifest {
188
- const manifest = validateStandardSchema(
189
- manifestSchema,
190
- value,
191
- 'manifest',
192
- ) as BunderstackManifest
193
- rejectDuplicates(
194
- 'database physical table',
195
- manifest.database.tables.map((entry) => entry.physicalName),
196
- )
197
- rejectDuplicates(
198
- 'database export table',
199
- manifest.database.tables.map((entry) => entry.exportName),
200
- )
201
- rejectDuplicates(
202
- 'storage bucket',
203
- manifest.storage.buckets.map((entry) => entry.name),
204
- )
205
- rejectDuplicates(
206
- 'environment key',
207
- manifest.environment.map((entry) => entry.key),
208
- )
209
- rejectDuplicates(
210
- 'background job',
211
- manifest.background.jobs.map((entry) => entry.name),
212
- )
213
- rejectDuplicates(
214
- 'background cron',
215
- manifest.background.cron.map((entry) => entry.name),
216
- )
217
- rejectDuplicates(
218
- 'background maintenance',
219
- manifest.background.maintenance.map((entry) => entry.name),
220
- )
221
- return manifest
222
- }
223
-
224
- export function buildManifest(args: {
225
- schema: Record<string, unknown>
226
- dialect: Dialect
227
- migrationsDirectory: string
228
- storage: ResolvedStorageBuckets
229
- envConfig: EnvConfigInput | undefined
230
- emailProvider: string | undefined
231
- realtime: boolean
232
- jobs: JobsDefs | undefined
233
- }): BunderstackManifest {
234
- const environment = [
235
- ...describeSection(args.envConfig?.server, 'server'),
236
- ...describeSection(args.envConfig?.client, 'client'),
237
- ...(args.emailProvider === 'resend'
238
- ? [{ key: 'RESEND_API_KEY', required: true, scope: 'server' as const }]
239
- : []),
240
- ...(args.emailProvider === 'smtp'
241
- ? [{ key: 'SMTP_URL', required: true, scope: 'server' as const }]
242
- : []),
243
- ]
244
- rejectDuplicates(
245
- 'environment key',
246
- environment.map((entry) => entry.key),
247
- )
248
-
249
- return parseManifest({
250
- version: 3,
251
- database: {
252
- dialect: args.dialect,
253
- migrationsDirectory: args.migrationsDirectory,
254
- tables: sortBy(
255
- [...systemTables(), ...describeTables(args.schema)],
256
- (entry) => entry.physicalName,
257
- ),
258
- },
259
- storage: {
260
- defaultBucket: args.storage.defaultBucket,
261
- buckets: sortBy(
262
- [...args.storage.buckets.values()].map((bucket) => ({
263
- name: bucket.name,
264
- visibility: bucket.visibility,
265
- })),
266
- (bucket) => bucket.name,
267
- ),
268
- },
269
- realtime: { required: args.realtime },
270
- environment: sortBy(environment, (entry) => entry.key),
271
- background: {
272
- jobs: sortBy(
273
- Object.entries(args.jobs ?? {})
274
- .filter(([, def]) => def.kind === 'job')
275
- .map(([name]) => ({ name })),
276
- (entry) => entry.name,
277
- ),
278
- cron: sortBy(
279
- Object.entries(args.jobs ?? {})
280
- .filter(([, def]) => def.kind === 'cron')
281
- .map(([name, def]) => ({
282
- name,
283
- schedule: def.kind === 'cron' ? def.schedule : '',
284
- timezone: 'UTC' as const,
285
- })),
286
- (entry) => entry.name,
287
- ),
288
- maintenance: [
289
- {
290
- name: 'storage-sweep',
291
- schedule: '0 4 * * *',
292
- timezone: 'UTC' as const,
293
- },
294
- ],
295
- },
296
- })
297
- }