@beechcms/api 0.6.0-preview.2 → 0.6.0-preview.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (388) hide show
  1. package/README.md +4 -4
  2. package/assets/dashboard/BeechLogo.svg +18 -18
  3. package/assets/dashboard/BeechLogoLIght.svg +48 -48
  4. package/assets/dashboard/assets/chart-BwlCt-IA.js +39 -0
  5. package/assets/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  6. package/assets/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  7. package/assets/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  8. package/assets/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  9. package/assets/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  10. package/assets/dashboard/assets/index-BcVN-DNy.js +642 -0
  11. package/assets/dashboard/assets/index-Cf1yRCqG.css +1 -0
  12. package/assets/dashboard/assets/lora-cyrillic-ext-wght-normal-YcVxMP-u.woff2 +0 -0
  13. package/assets/dashboard/assets/lora-cyrillic-wght-normal-UWPuZLjM.woff2 +0 -0
  14. package/assets/dashboard/assets/lora-latin-ext-wght-normal-C2Wlntb9.woff2 +0 -0
  15. package/assets/dashboard/assets/lora-latin-wght-normal-BiLcIKcI.woff2 +0 -0
  16. package/assets/dashboard/assets/lora-math-wght-normal-sOy7rEfW.woff2 +0 -0
  17. package/assets/dashboard/assets/lora-symbols-wght-normal-DQ5VrUkH.woff2 +0 -0
  18. package/assets/dashboard/assets/lora-vietnamese-wght-normal-CfJ7gtf3.woff2 +0 -0
  19. package/assets/dashboard/assets/pie-chart-recharts-BMWpjFzf.js +1 -0
  20. package/assets/dashboard/assets/timeseries-chart-recharts-8_GwAFR0.js +1 -0
  21. package/assets/dashboard/beechLogoDark.svg +48 -48
  22. package/assets/dashboard/index.html +18 -18
  23. package/assets/dashboard/noResult.svg +42 -42
  24. package/assets/dashboard/sol.svg +3 -3
  25. package/assets/dashboard/undraw_enter_nwx3.svg +36 -36
  26. package/dist/auth/__fixtures__/in-memory-hash-provider.d.ts +5 -0
  27. package/dist/auth/__fixtures__/static-token-service.d.ts +6 -0
  28. package/dist/auth/bcrypt-hash-provider.d.ts +8 -0
  29. package/dist/auth/constants.d.ts +10 -0
  30. package/dist/auth/jose-token-service.d.ts +14 -0
  31. package/dist/auth/login.d.ts +32 -0
  32. package/dist/auth/refresh.d.ts +1 -0
  33. package/dist/factory.d.ts +45 -0
  34. package/dist/features/automations/action-executors/create-entry.executor.d.ts +7 -0
  35. package/dist/features/automations/action-executors/edit-field.executor.d.ts +7 -0
  36. package/dist/features/automations/action-executors/index.d.ts +13 -0
  37. package/dist/features/automations/action-executors/send-mail.executor.d.ts +7 -0
  38. package/dist/features/automations/action-executors/set-variable.executor.d.ts +15 -0
  39. package/dist/features/automations/action-executors/webhook.executor.d.ts +7 -0
  40. package/dist/features/automations/automation-runner.d.ts +13 -0
  41. package/dist/features/automations/automation-runner.utils.d.ts +3 -0
  42. package/dist/features/automations/automations.handler.d.ts +7 -0
  43. package/dist/features/automations/automations.schema.d.ts +191 -0
  44. package/dist/features/automations/context-resolver.d.ts +8 -0
  45. package/dist/features/automations/cron-runner.d.ts +9 -0
  46. package/dist/features/automations/cron-runner.utils.d.ts +1 -0
  47. package/dist/features/automations/filter-translation.d.ts +9 -0
  48. package/dist/features/automations/index.d.ts +7 -0
  49. package/dist/features/automations/template-grammar.d.ts +73 -0
  50. package/dist/features/automations/var-access-resolver.d.ts +6 -0
  51. package/dist/features/automations/when-evaluator.d.ts +3 -0
  52. package/dist/features/automations/when-pushdown.d.ts +2 -0
  53. package/dist/features/backrefs/backrefs.handler.d.ts +3 -0
  54. package/dist/features/backrefs/d1-backref.repository.d.ts +17 -0
  55. package/dist/features/backrefs/index.d.ts +1 -0
  56. package/dist/features/content/constants.d.ts +12 -0
  57. package/dist/features/content/handlers/bulk.handler.d.ts +3 -0
  58. package/dist/features/content/handlers/create.d.ts +3 -0
  59. package/dist/features/content/handlers/delete.d.ts +3 -0
  60. package/dist/features/content/handlers/facets.d.ts +3 -0
  61. package/dist/features/content/handlers/get.d.ts +4 -0
  62. package/dist/features/content/handlers/helpers.d.ts +12 -0
  63. package/dist/features/content/handlers/list.d.ts +3 -0
  64. package/dist/features/content/handlers/update.d.ts +3 -0
  65. package/dist/features/content/index.d.ts +4 -0
  66. package/dist/features/dashboard-layout/dashboard-layout.handler.d.ts +7 -0
  67. package/dist/features/dashboard-layout/index.d.ts +1 -0
  68. package/dist/features/draft/draft.handler.d.ts +4 -0
  69. package/dist/features/draft/draft.middleware.d.ts +9 -0
  70. package/dist/features/draft/index.d.ts +1 -0
  71. package/{src/features/email/email.provider.ts → dist/features/email/email.provider.d.ts} +19 -24
  72. package/dist/features/email/email.service.d.ts +13 -0
  73. package/dist/features/email/email.types.d.ts +94 -0
  74. package/{src/features/email/index.ts → dist/features/email/index.d.ts} +3 -16
  75. package/dist/features/email/providers/resend.d.ts +36 -0
  76. package/dist/features/email/providers/smtp.d.ts +11 -0
  77. package/dist/features/email/templates/automation-mail.d.ts +8 -0
  78. package/dist/features/email/templates/password-changed.d.ts +15 -0
  79. package/dist/features/email/templates/password-reset.d.ts +18 -0
  80. package/dist/features/email/templates/shell.d.ts +58 -0
  81. package/dist/features/notifications/index.d.ts +1 -0
  82. package/dist/features/notifications/notifications.handler.d.ts +15 -0
  83. package/dist/features/password-reset/index.d.ts +6 -0
  84. package/dist/features/password-reset/request.d.ts +10 -0
  85. package/dist/features/password-reset/reset.d.ts +9 -0
  86. package/dist/features/rotate-field/index.d.ts +1 -0
  87. package/dist/features/rotate-field/rotate-field.handler.d.ts +7 -0
  88. package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
  89. package/dist/features/schema/schema.handler.d.ts +7 -0
  90. package/dist/features/seeds/index.d.ts +1 -0
  91. package/dist/features/seeds/seeds.handler.d.ts +6 -0
  92. package/dist/features/settings/settings.handler.d.ts +7 -0
  93. package/dist/features/setup/index.d.ts +7 -0
  94. package/dist/features/stats/index.d.ts +1 -0
  95. package/dist/features/stats/stats.handler.d.ts +7 -0
  96. package/dist/features/webhooks/index.d.ts +3 -0
  97. package/dist/index.d.ts +7 -0
  98. package/dist/index.js +9732 -0
  99. package/dist/media-utils.d.ts +25 -0
  100. package/dist/middleware/auth-providers.middleware.d.ts +8 -0
  101. package/dist/middleware/observability.middleware.d.ts +20 -0
  102. package/dist/middleware/queue.middleware.d.ts +11 -0
  103. package/dist/middleware/rate-limit.middleware.d.ts +9 -0
  104. package/dist/middleware/repository.middleware.d.ts +34 -0
  105. package/dist/middleware/seed-registry.middleware.d.ts +10 -0
  106. package/dist/middleware/storage.middleware.d.ts +7 -0
  107. package/dist/middleware.d.ts +11 -0
  108. package/dist/public/access-policy.d.ts +12 -0
  109. package/dist/public/api-key-middleware.d.ts +6 -0
  110. package/dist/public/cache-utils.d.ts +10 -0
  111. package/dist/public/entry-projection.d.ts +2 -0
  112. package/dist/public/idempotency.d.ts +9 -0
  113. package/dist/public/index.d.ts +11 -0
  114. package/dist/public/problem-details.d.ts +31 -0
  115. package/dist/public/public-add.d.ts +3 -0
  116. package/dist/public/public-edit.d.ts +5 -0
  117. package/dist/public/public-errors.d.ts +13 -0
  118. package/dist/public/public-read.d.ts +3 -0
  119. package/dist/public/public-routes.d.ts +3 -0
  120. package/dist/public/query-builder.d.ts +28 -0
  121. package/dist/public/rate-limit-middleware.d.ts +3 -0
  122. package/dist/public/read-list.d.ts +26 -0
  123. package/dist/public/read-single.d.ts +22 -0
  124. package/dist/public/response-builder.d.ts +22 -0
  125. package/dist/public/sanitize.d.ts +23 -0
  126. package/{src/public/slug-utils.ts → dist/public/slug-utils.d.ts} +3 -10
  127. package/dist/rate-limit/cloudflare-rate-limiter.d.ts +6 -0
  128. package/dist/rate-limit/in-memory-rate-limiter.d.ts +7 -0
  129. package/dist/rate-limit/no-op-rate-limiter.d.ts +4 -0
  130. package/dist/search-utils.d.ts +54 -0
  131. package/dist/search.d.ts +6 -0
  132. package/dist/shared/apply-policies.d.ts +10 -0
  133. package/dist/shared/automations.repository.d1.d.ts +12 -0
  134. package/dist/shared/background-notification-service.d.ts +18 -0
  135. package/dist/shared/base.repository.d1.d.ts +18 -0
  136. package/dist/shared/cloudflare-queue-service.d.ts +11 -0
  137. package/dist/shared/content-utils.d.ts +21 -0
  138. package/dist/shared/content.repository.d1.d.ts +68 -0
  139. package/dist/shared/d1-activity-log.repository.d.ts +13 -0
  140. package/dist/shared/d1-activity-logger.d.ts +20 -0
  141. package/dist/shared/d1-analytics.repository.d.ts +20 -0
  142. package/dist/shared/d1-content-scan.repository.d.ts +7 -0
  143. package/dist/shared/d1-notification.repository.d.ts +17 -0
  144. package/dist/shared/d1-password-reset-token.repository.d.ts +10 -0
  145. package/dist/shared/d1-search.repository.d.ts +14 -0
  146. package/dist/shared/d1-session.repository.d.ts +12 -0
  147. package/dist/shared/d1-setup-checklist.repository.d.ts +12 -0
  148. package/dist/shared/d1-user.repository.d.ts +18 -0
  149. package/dist/shared/d1-widget.repository.d.ts +31 -0
  150. package/dist/shared/dashboard-layout.repository.d1.d.ts +9 -0
  151. package/dist/shared/demo-data-sql.d.ts +1 -0
  152. package/dist/shared/demo-data.repository.d1.d.ts +6 -0
  153. package/dist/shared/execution-context-scheduler.d.ts +6 -0
  154. package/dist/shared/fixed-clock.d.ts +13 -0
  155. package/dist/shared/fts-sync.d.ts +1 -0
  156. package/dist/shared/idempotency.repository.d1.d.ts +16 -0
  157. package/dist/shared/in-memory-activity-logger.d.ts +11 -0
  158. package/dist/shared/in-memory-notification-service.d.ts +11 -0
  159. package/dist/shared/in-memory-queue-service.d.ts +16 -0
  160. package/dist/shared/in-memory-seed.repository.d.ts +18 -0
  161. package/dist/shared/media.repository.d1.d.ts +35 -0
  162. package/dist/shared/qstash-notification-service.d.ts +18 -0
  163. package/dist/shared/query-utils.d.ts +36 -0
  164. package/dist/shared/queue-consumer.d.ts +9 -0
  165. package/dist/shared/request-utils.d.ts +13 -0
  166. package/dist/shared/schema-mutator.d1.d.ts +12 -0
  167. package/dist/shared/seed-layout.repository.d1.d.ts +9 -0
  168. package/dist/shared/seed-registry-cache.d.ts +12 -0
  169. package/dist/shared/seed.repository.d1.d.ts +13 -0
  170. package/dist/shared/sequential-id-generator.d.ts +13 -0
  171. package/dist/shared/site-settings.repository.d1.d.ts +7 -0
  172. package/dist/shared/storage/factory.d.ts +8 -0
  173. package/dist/shared/storage/s3-bucket.d.ts +43 -0
  174. package/dist/shared/storage-utils.d.ts +7 -0
  175. package/dist/shared/system-stats.repository.d1.d.ts +25 -0
  176. package/dist/types.d.ts +87 -0
  177. package/dist/upload.d.ts +16 -0
  178. package/dist/widget.d.ts +7 -0
  179. package/migrations/0000_v040_base.sql +265 -265
  180. package/migrations/0029_automations.sql +14 -14
  181. package/migrations/0030_test_seeds.sql +125 -125
  182. package/package.json +28 -18
  183. package/assets/dashboard/assets/chart-BrJFdsGZ.js +0 -39
  184. package/assets/dashboard/assets/index-CIaoMxuB.css +0 -1
  185. package/assets/dashboard/assets/index-wTKgygsg.js +0 -636
  186. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +0 -1
  187. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +0 -1
  188. package/src/auth/__fixtures__/in-memory-hash-provider.ts +0 -17
  189. package/src/auth/__fixtures__/static-token-service.ts +0 -22
  190. package/src/auth/bcrypt-hash-provider.ts +0 -24
  191. package/src/auth/constants.ts +0 -14
  192. package/src/auth/generate-refresh-token.test.ts +0 -23
  193. package/src/auth/hash-provider.test.ts +0 -55
  194. package/src/auth/jose-token-service.ts +0 -62
  195. package/src/auth/jwt-claims-passthrough.test.ts +0 -87
  196. package/src/auth/login.test.ts +0 -100
  197. package/src/auth/login.ts +0 -78
  198. package/src/auth/refresh.ts +0 -11
  199. package/src/auth/token-service.test.ts +0 -90
  200. package/src/factory.ts +0 -370
  201. package/src/features/automations/__tests__/action-executors.test.ts +0 -255
  202. package/src/features/automations/__tests__/automation-runner.test.ts +0 -196
  203. package/src/features/automations/__tests__/automation-runner.utils.test.ts +0 -60
  204. package/src/features/automations/__tests__/automations.handler.test.ts +0 -264
  205. package/src/features/automations/__tests__/automations.repository.test.ts +0 -163
  206. package/src/features/automations/__tests__/automations.schema.test.ts +0 -224
  207. package/src/features/automations/__tests__/context-resolver.test.ts +0 -126
  208. package/src/features/automations/__tests__/cron-runner.test.ts +0 -267
  209. package/src/features/automations/__tests__/cron-runner.utils.test.ts +0 -144
  210. package/src/features/automations/__tests__/set-variable.executor.test.ts +0 -383
  211. package/src/features/automations/__tests__/template-grammar.test.ts +0 -308
  212. package/src/features/automations/__tests__/webhook.executor.test.ts +0 -142
  213. package/src/features/automations/__tests__/when-evaluator.test.ts +0 -274
  214. package/src/features/automations/__tests__/when-pushdown.test.ts +0 -281
  215. package/src/features/automations/action-executors/create-entry.executor.ts +0 -27
  216. package/src/features/automations/action-executors/edit-field.executor.ts +0 -26
  217. package/src/features/automations/action-executors/index.ts +0 -37
  218. package/src/features/automations/action-executors/send-mail.executor.ts +0 -53
  219. package/src/features/automations/action-executors/set-variable.executor.ts +0 -159
  220. package/src/features/automations/action-executors/webhook.executor.ts +0 -67
  221. package/src/features/automations/automation-runner.ts +0 -85
  222. package/src/features/automations/automation-runner.utils.ts +0 -47
  223. package/src/features/automations/automations.handler.ts +0 -197
  224. package/src/features/automations/automations.schema.ts +0 -177
  225. package/src/features/automations/context-resolver.ts +0 -152
  226. package/src/features/automations/cron-runner.ts +0 -140
  227. package/src/features/automations/cron-runner.utils.ts +0 -44
  228. package/src/features/automations/filter-translation.ts +0 -46
  229. package/src/features/automations/index.ts +0 -16
  230. package/src/features/automations/template-grammar.ts +0 -245
  231. package/src/features/automations/var-access-resolver.ts +0 -140
  232. package/src/features/automations/when-evaluator.ts +0 -87
  233. package/src/features/automations/when-pushdown.ts +0 -57
  234. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +0 -359
  235. package/src/features/backrefs/backrefs.handler.ts +0 -168
  236. package/src/features/backrefs/d1-backref.repository.ts +0 -84
  237. package/src/features/backrefs/index.ts +0 -5
  238. package/src/features/content/constants.ts +0 -16
  239. package/src/features/content/handlers/bulk.handler.ts +0 -185
  240. package/src/features/content/handlers/create.ts +0 -129
  241. package/src/features/content/handlers/delete.ts +0 -62
  242. package/src/features/content/handlers/facets.ts +0 -49
  243. package/src/features/content/handlers/get.ts +0 -120
  244. package/src/features/content/handlers/helpers.test.ts +0 -180
  245. package/src/features/content/handlers/helpers.ts +0 -93
  246. package/src/features/content/handlers/list.ts +0 -174
  247. package/src/features/content/handlers/update.ts +0 -174
  248. package/src/features/content/index.ts +0 -26
  249. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +0 -388
  250. package/src/features/dashboard-layout/dashboard-layout.handler.ts +0 -206
  251. package/src/features/dashboard-layout/index.ts +0 -5
  252. package/src/features/draft/draft.handler.ts +0 -195
  253. package/src/features/draft/draft.middleware.ts +0 -66
  254. package/src/features/draft/index.ts +0 -5
  255. package/src/features/email/email.service.ts +0 -98
  256. package/src/features/email/email.types.ts +0 -119
  257. package/src/features/email/providers/resend.ts +0 -67
  258. package/src/features/email/providers/smtp.ts +0 -55
  259. package/src/features/email/templates/automation-mail.ts +0 -19
  260. package/src/features/email/templates/password-changed.ts +0 -63
  261. package/src/features/email/templates/password-reset.ts +0 -68
  262. package/src/features/email/templates/shell.ts +0 -96
  263. package/src/features/notifications/index.ts +0 -5
  264. package/src/features/notifications/notifications.handler.ts +0 -105
  265. package/src/features/password-reset/index.ts +0 -19
  266. package/src/features/password-reset/request.ts +0 -92
  267. package/src/features/password-reset/reset.ts +0 -102
  268. package/src/features/rotate-field/index.ts +0 -5
  269. package/src/features/rotate-field/rotate-field.handler.ts +0 -132
  270. package/src/features/rotate-field/rotate-field.schema.ts +0 -17
  271. package/src/features/schema/schema.handler.ts +0 -131
  272. package/src/features/seeds/index.ts +0 -5
  273. package/src/features/seeds/seeds.handler.test.ts +0 -1199
  274. package/src/features/seeds/seeds.handler.ts +0 -558
  275. package/src/features/settings/__tests__/settings.handler.test.ts +0 -555
  276. package/src/features/settings/settings.handler.ts +0 -392
  277. package/src/features/setup/index.ts +0 -288
  278. package/src/features/stats/index.ts +0 -5
  279. package/src/features/stats/stats.handler.ts +0 -420
  280. package/src/features/webhooks/index.ts +0 -63
  281. package/src/index.ts +0 -65
  282. package/src/media-utils.ts +0 -82
  283. package/src/middleware/auth-providers.middleware.test.ts +0 -85
  284. package/src/middleware/auth-providers.middleware.ts +0 -45
  285. package/src/middleware/observability.middleware.test.ts +0 -93
  286. package/src/middleware/observability.middleware.ts +0 -70
  287. package/src/middleware/rate-limit.middleware.ts +0 -45
  288. package/src/middleware/repository.middleware.ts +0 -117
  289. package/src/middleware/seed-registry.middleware.test.ts +0 -97
  290. package/src/middleware/seed-registry.middleware.ts +0 -21
  291. package/src/middleware/storage.middleware.ts +0 -25
  292. package/src/middleware.ts +0 -45
  293. package/src/public/access-policy.ts +0 -27
  294. package/src/public/api-key-middleware.ts +0 -57
  295. package/src/public/cache-utils.ts +0 -38
  296. package/src/public/entry-projection.ts +0 -46
  297. package/src/public/idempotency.ts +0 -23
  298. package/src/public/index.ts +0 -16
  299. package/src/public/problem-details.ts +0 -124
  300. package/src/public/public-add.ts +0 -114
  301. package/src/public/public-edit.ts +0 -163
  302. package/src/public/public-errors.ts +0 -19
  303. package/src/public/public-read.ts +0 -63
  304. package/src/public/public-routes.ts +0 -88
  305. package/src/public/query-builder.test.ts +0 -224
  306. package/src/public/query-builder.ts +0 -156
  307. package/src/public/rate-limit-middleware.ts +0 -34
  308. package/src/public/read-list.ts +0 -54
  309. package/src/public/read-single.ts +0 -48
  310. package/src/public/response-builder.ts +0 -30
  311. package/src/public/sanitize.ts +0 -69
  312. package/src/rate-limit/cloudflare-rate-limiter.test.ts +0 -30
  313. package/src/rate-limit/cloudflare-rate-limiter.ts +0 -15
  314. package/src/rate-limit/in-memory-rate-limiter.test.ts +0 -37
  315. package/src/rate-limit/in-memory-rate-limiter.ts +0 -17
  316. package/src/rate-limit/no-op-rate-limiter.test.ts +0 -22
  317. package/src/rate-limit/no-op-rate-limiter.ts +0 -11
  318. package/src/search-utils.test.ts +0 -211
  319. package/src/search-utils.ts +0 -213
  320. package/src/search.ts +0 -65
  321. package/src/shared/apply-policies.test.ts +0 -81
  322. package/src/shared/apply-policies.ts +0 -67
  323. package/src/shared/automations.repository.d1.ts +0 -150
  324. package/src/shared/background-notification-service.test.ts +0 -62
  325. package/src/shared/background-notification-service.ts +0 -52
  326. package/src/shared/base.repository.d1.ts +0 -32
  327. package/src/shared/content-utils.test.ts +0 -165
  328. package/src/shared/content-utils.ts +0 -86
  329. package/src/shared/content.repository.d1.test.ts +0 -507
  330. package/src/shared/content.repository.d1.ts +0 -851
  331. package/src/shared/d1-activity-log.repository.test.ts +0 -140
  332. package/src/shared/d1-activity-log.repository.ts +0 -105
  333. package/src/shared/d1-activity-logger.test.ts +0 -86
  334. package/src/shared/d1-activity-logger.ts +0 -67
  335. package/src/shared/d1-analytics.repository.test.ts +0 -78
  336. package/src/shared/d1-analytics.repository.ts +0 -85
  337. package/src/shared/d1-content-scan.repository.test.ts +0 -80
  338. package/src/shared/d1-content-scan.repository.ts +0 -37
  339. package/src/shared/d1-notification.repository.test.ts +0 -128
  340. package/src/shared/d1-notification.repository.ts +0 -118
  341. package/src/shared/d1-password-reset-token.repository.test.ts +0 -81
  342. package/src/shared/d1-password-reset-token.repository.ts +0 -56
  343. package/src/shared/d1-search.repository.test.ts +0 -87
  344. package/src/shared/d1-search.repository.ts +0 -88
  345. package/src/shared/d1-session.repository.test.ts +0 -125
  346. package/src/shared/d1-session.repository.ts +0 -102
  347. package/src/shared/d1-setup-checklist.repository.ts +0 -36
  348. package/src/shared/d1-user.repository.test.ts +0 -151
  349. package/src/shared/d1-user.repository.ts +0 -119
  350. package/src/shared/d1-widget.repository.test.ts +0 -267
  351. package/src/shared/d1-widget.repository.ts +0 -385
  352. package/src/shared/dashboard-layout.repository.d1.test.ts +0 -135
  353. package/src/shared/dashboard-layout.repository.d1.ts +0 -58
  354. package/src/shared/demo-data-sql.ts +0 -216
  355. package/src/shared/demo-data.repository.d1.ts +0 -20
  356. package/src/shared/execution-context-scheduler.ts +0 -13
  357. package/src/shared/fixed-clock.ts +0 -25
  358. package/src/shared/fts-sync.ts +0 -8
  359. package/src/shared/idempotency.repository.d1.test.ts +0 -83
  360. package/src/shared/idempotency.repository.d1.ts +0 -60
  361. package/src/shared/in-memory-activity-logger.ts +0 -19
  362. package/src/shared/in-memory-notification-service.ts +0 -19
  363. package/src/shared/in-memory-seed.repository.ts +0 -58
  364. package/src/shared/media.repository.d1.test.ts +0 -107
  365. package/src/shared/media.repository.d1.ts +0 -68
  366. package/src/shared/qstash-notification-service.test.ts +0 -67
  367. package/src/shared/qstash-notification-service.ts +0 -55
  368. package/src/shared/query-utils.ts +0 -141
  369. package/src/shared/request-utils.ts +0 -26
  370. package/src/shared/schema-mutator.d1.test.ts +0 -142
  371. package/src/shared/schema-mutator.d1.ts +0 -64
  372. package/src/shared/seed-layout.repository.d1.test.ts +0 -114
  373. package/src/shared/seed-layout.repository.d1.ts +0 -62
  374. package/src/shared/seed-registry-cache.test.ts +0 -68
  375. package/src/shared/seed-registry-cache.ts +0 -49
  376. package/src/shared/seed.repository.d1.test.ts +0 -143
  377. package/src/shared/seed.repository.d1.ts +0 -98
  378. package/src/shared/sequential-id-generator.ts +0 -33
  379. package/src/shared/site-settings.repository.d1.ts +0 -53
  380. package/src/shared/storage/factory.ts +0 -41
  381. package/src/shared/storage/s3-bucket.ts +0 -195
  382. package/src/shared/storage-utils.ts +0 -40
  383. package/src/shared/system-stats.repository.d1.test.ts +0 -58
  384. package/src/shared/system-stats.repository.d1.ts +0 -48
  385. package/src/types.ts +0 -90
  386. package/src/upload.ts +0 -198
  387. package/src/widget.test.ts +0 -85
  388. package/src/widget.ts +0 -251
@@ -1,107 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect, vi } from 'vitest'
6
- import { D1MediaRepository } from './media.repository.d1'
7
-
8
- function makeMockDb(opts: { firstResult?: unknown; allResults?: unknown[] } = {}) {
9
- const { firstResult = null, allResults = [] } = opts
10
- const runMock = vi.fn().mockResolvedValue({ success: true })
11
- const firstMock = vi.fn().mockResolvedValue(firstResult)
12
- const allMock = vi.fn().mockResolvedValue({ results: allResults })
13
- // bind() returns the same interface so chains like prepare().bind().all() work.
14
- // The statement also exposes first/all/run directly for queries that skip bind().
15
- const bindMock = vi.fn(() => ({ run: runMock, first: firstMock, all: allMock }))
16
- const stmt = { bind: bindMock, run: runMock, first: firstMock, all: allMock }
17
- const prepareMock = vi.fn(() => stmt)
18
- return { db: { prepare: prepareMock } as any, prepareMock, bindMock, runMock, firstMock, allMock }
19
- }
20
-
21
- describe('D1MediaRepository', () => {
22
- describe('trackUpload', () => {
23
- it('calls INSERT INTO media_objects with correct bound values', async () => {
24
- const { db, prepareMock, bindMock } = makeMockDb()
25
- const obj = { key: 'k1', filename: 'img.png', mime_type: 'image/png', size_bytes: 1024, uploaded_by: 'user-1' }
26
- await new D1MediaRepository(db).trackUpload(obj)
27
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('INSERT INTO media_objects'))
28
- expect(bindMock).toHaveBeenCalledWith('k1', 'img.png', 'image/png', 1024, 'user-1')
29
- })
30
- })
31
-
32
- describe('getByKey', () => {
33
- it('returns the media object when found', async () => {
34
- const row = { key: 'k1', filename: 'img.png', mime_type: 'image/png', size_bytes: 512, uploaded_by: 'u1', created_at: 1000 }
35
- const { db } = makeMockDb({ firstResult: row })
36
- const result = await new D1MediaRepository(db).getByKey('k1')
37
- expect(result).toEqual(row)
38
- })
39
-
40
- it('returns null when the key is not found', async () => {
41
- const { db } = makeMockDb({ firstResult: null })
42
- expect(await new D1MediaRepository(db).getByKey('ghost')).toBeNull()
43
- })
44
-
45
- it('passes the key as bound value', async () => {
46
- const { db, bindMock } = makeMockDb()
47
- await new D1MediaRepository(db).getByKey('my-key')
48
- expect(bindMock).toHaveBeenCalledWith('my-key')
49
- })
50
- })
51
-
52
- describe('untrack', () => {
53
- it('calls DELETE FROM media_objects with the key', async () => {
54
- const { db, prepareMock, bindMock } = makeMockDb()
55
- await new D1MediaRepository(db).untrack('k1')
56
- expect(prepareMock).toHaveBeenCalledWith(expect.stringContaining('DELETE FROM media_objects'))
57
- expect(bindMock).toHaveBeenCalledWith('k1')
58
- })
59
- })
60
-
61
- describe('list', () => {
62
- it('returns items and total from D1', async () => {
63
- const rows = [
64
- { key: 'k1', filename: 'a.png', mime_type: 'image/png', size_bytes: 100, uploaded_by: 'u1', created_at: 1000 },
65
- ]
66
- const { db, allMock, firstMock } = makeMockDb()
67
- // list() calls all() for items then first() for count (without bind)
68
- allMock.mockResolvedValueOnce({ results: rows })
69
- firstMock.mockResolvedValueOnce({ total: 1 })
70
-
71
- const result = await new D1MediaRepository(db).list({ limit: 10, offset: 0 })
72
- expect(result.items).toHaveLength(1)
73
- expect(result.total).toBe(1)
74
- })
75
-
76
- it('returns empty items and 0 total when D1 returns nothing', async () => {
77
- const { db, allMock, firstMock } = makeMockDb()
78
- allMock.mockResolvedValueOnce({ results: [] })
79
- firstMock.mockResolvedValueOnce(null) // count row is null → 0
80
-
81
- const result = await new D1MediaRepository(db).list({ limit: 10, offset: 0 })
82
- expect(result.items).toEqual([])
83
- expect(result.total).toBe(0)
84
- })
85
-
86
- it('passes limit and offset as bound values for the list query', async () => {
87
- const { db, bindMock, allMock, firstMock } = makeMockDb()
88
- allMock.mockResolvedValueOnce({ results: [] })
89
- firstMock.mockResolvedValueOnce({ total: 0 })
90
-
91
- await new D1MediaRepository(db).list({ limit: 5, offset: 10 })
92
- expect(bindMock).toHaveBeenCalledWith(5, 10)
93
- })
94
- })
95
-
96
- describe('count', () => {
97
- it('returns the count from D1', async () => {
98
- const { db } = makeMockDb({ firstResult: { total: 42 } })
99
- expect(await new D1MediaRepository(db).count()).toBe(42)
100
- })
101
-
102
- it('returns 0 when D1 returns null', async () => {
103
- const { db } = makeMockDb({ firstResult: null })
104
- expect(await new D1MediaRepository(db).count()).toBe(0)
105
- })
106
- })
107
- })
@@ -1,68 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { MediaRepository, MediaObject } from '@beechcms/core'
6
-
7
- /**
8
- * Implementation of MediaRepository using Cloudflare D1.
9
- * Provides methods to track media uploads, retrieve media metadata, and manage the media library state in the database.
10
- */
11
- export class D1MediaRepository implements MediaRepository {
12
- constructor(private database: D1Database) {}
13
-
14
- /**
15
- * Registers a new media upload in the database.
16
- */
17
- async trackUpload(mediaObject: Omit<MediaObject, 'created_at'>): Promise<void> {
18
- await this.database.prepare(
19
- 'INSERT INTO media_objects (key, filename, mime_type, size_bytes, uploaded_by) VALUES (?, ?, ?, ?, ?)'
20
- ).bind(
21
- mediaObject.key,
22
- mediaObject.filename,
23
- mediaObject.mime_type,
24
- mediaObject.size_bytes,
25
- mediaObject.uploaded_by
26
- ).run()
27
- }
28
-
29
- /**
30
- * Retrieves a media object's metadata by its unique key.
31
- */
32
- async getByKey(mediaKey: string): Promise<MediaObject | null> {
33
- return await this.database.prepare(
34
- 'SELECT key, filename, mime_type, size_bytes, uploaded_by, created_at FROM media_objects WHERE key = ?'
35
- ).bind(mediaKey).first<MediaObject>()
36
- }
37
-
38
- /**
39
- * Removes the metadata tracking for a specific media key.
40
- */
41
- async untrack(mediaKey: string): Promise<void> {
42
- await this.database.prepare('DELETE FROM media_objects WHERE key = ?').bind(mediaKey).run()
43
- }
44
-
45
- /**
46
- * Lists media objects with pagination support.
47
- */
48
- async list(paginationOptions: { limit: number; offset: number }): Promise<{ items: MediaObject[]; total: number }> {
49
- const { results: mediaItems } = await this.database.prepare(
50
- 'SELECT key, filename, mime_type, size_bytes, uploaded_by, created_at FROM media_objects ORDER BY created_at DESC LIMIT ? OFFSET ?'
51
- ).bind(paginationOptions.limit, paginationOptions.offset).all<MediaObject>()
52
-
53
- const totalCountRecord = await this.database.prepare('SELECT COUNT(*) as total FROM media_objects').first<{ total: number }>()
54
-
55
- return {
56
- items: mediaItems ?? [],
57
- total: totalCountRecord?.total ?? 0
58
- }
59
- }
60
-
61
- /**
62
- * Returns the total number of media objects in the database.
63
- */
64
- async count(): Promise<number> {
65
- const totalCountRecord = await this.database.prepare('SELECT COUNT(*) as total FROM media_objects').first<{ total: number }>()
66
- return totalCountRecord?.total ?? 0
67
- }
68
- }
@@ -1,67 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect, vi, beforeEach } from 'vitest'
6
- import { QStashNotificationService } from './qstash-notification-service'
7
- import { Client } from '@upstash/qstash'
8
-
9
- // Mock the @upstash/qstash Client
10
- vi.mock('@upstash/qstash', () => {
11
- const publishJSON = vi.fn().mockResolvedValue({ messageId: 'msg_123' })
12
- return {
13
- Client: vi.fn().mockImplementation(function () {
14
- return { publishJSON }
15
- }),
16
- }
17
- })
18
-
19
- describe('QStashNotificationService', () => {
20
- beforeEach(() => {
21
- vi.clearAllMocks()
22
- })
23
-
24
- it('delegates publishing to the QStash client with correct URL', async () => {
25
- const service = new QStashNotificationService('fake_token', 'https://beechcms.test/')
26
- await service.notify({
27
- title: 'Hello',
28
- message: 'World',
29
- type: 'success',
30
- })
31
-
32
- const clientInstance = vi.mocked(Client).mock.results[0].value
33
- expect(clientInstance.publishJSON).toHaveBeenCalledWith({
34
- url: 'https://beechcms.test/api/webhooks/qstash',
35
- body: {
36
- title: 'Hello',
37
- message: 'World',
38
- type: 'success',
39
- },
40
- })
41
- })
42
-
43
- it('delegates to the background scheduler when provided', () => {
44
- const schedule = vi.fn()
45
- const service = new QStashNotificationService('fake_token', 'https://beechcms.test', schedule)
46
-
47
- service.notify({ title: 'T', message: 'M' })
48
-
49
- expect(schedule).toHaveBeenCalledTimes(1)
50
- expect(schedule.mock.calls[0][0]).toBeInstanceOf(Promise)
51
- })
52
-
53
- it('never throws to the caller when the publish write fails', async () => {
54
- // Override the mock for this specific test
55
- const mockPublish = vi.fn().mockRejectedValue(new Error('network error'))
56
- vi.mocked(Client).mockImplementationOnce(function () {
57
- return { publishJSON: mockPublish }
58
- } as any)
59
-
60
- const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
61
- const service = new QStashNotificationService('fake_token', 'https://beechcms.test')
62
-
63
- await expect(service.notify({ title: 'T', message: 'M' })).resolves.toBeUndefined()
64
- expect(consoleSpy).toHaveBeenCalled()
65
- consoleSpy.mockRestore()
66
- })
67
- })
@@ -1,55 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { INotificationService, CreateNotificationInput } from '@beechcms/core'
6
- import { Client } from '@upstash/qstash'
7
-
8
- type ScheduleBackgroundTask = (task: Promise<unknown>) => void
9
-
10
- /**
11
- * QStash implementation of {@link INotificationService}.
12
- *
13
- * This service acts as a publisher, sending the notification payload to Upstash QStash,
14
- * which guarantees delivery and retries. QStash will then forward the payload to the
15
- * local webhook endpoint where it is actually inserted into D1.
16
- */
17
- export class QStashNotificationService implements INotificationService {
18
- private readonly client: Client
19
- private readonly webhookUrl: string
20
-
21
- constructor(
22
- token: string,
23
- appUrl: string,
24
- private readonly scheduleBackgroundTask?: ScheduleBackgroundTask,
25
- qstashUrl?: string
26
- ) {
27
- this.client = new Client({
28
- token,
29
- ...(qstashUrl ? { baseUrl: qstashUrl } : {})
30
- })
31
- this.webhookUrl = `${appUrl.replace(/\/$/, '')}/api/webhooks/qstash`
32
- }
33
-
34
- notify(input: CreateNotificationInput): Promise<void> | void {
35
- const publishPromise = this.runPublish(input)
36
-
37
- if (this.scheduleBackgroundTask) {
38
- this.scheduleBackgroundTask(publishPromise)
39
- return
40
- }
41
-
42
- return publishPromise
43
- }
44
-
45
- private async runPublish(input: CreateNotificationInput): Promise<void> {
46
- try {
47
- await this.client.publishJSON({
48
- url: this.webhookUrl,
49
- body: input,
50
- })
51
- } catch (error) {
52
- console.error('QStashNotificationService: failed to publish notification', error)
53
- }
54
- }
55
- }
@@ -1,141 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { FilterGroup, FilterType } from '@beechcms/core'
6
-
7
- /** Entry parsata per le risposte API — contratto immutabile (C7). */
8
- export interface ContentEntry {
9
- id: string
10
- schema_slug: string
11
- slug: string | null
12
- status: string
13
- data: Record<string, unknown>
14
- hasPendingDraft: boolean
15
- created_at: number | null
16
- updated_at: number | null
17
- }
18
-
19
- export type QueryFilterType = FilterType
20
-
21
- export type QueryFilterOperator =
22
- | 'eq'
23
- | 'gt'
24
- | 'gte'
25
- | 'lt'
26
- | 'lte'
27
- | 'contains'
28
- | 'is_empty'
29
- | 'is_not_empty'
30
-
31
- export interface QueryFilterCondition {
32
- id?: string
33
- op: QueryFilterOperator
34
- value: string | number | boolean | null
35
- }
36
-
37
- /** Formato inviato dalla dashboard (usa columnId). */
38
- export interface QueryFilterGroup {
39
- columnId: string
40
- label?: string
41
- type: QueryFilterType
42
- conditions: QueryFilterCondition[]
43
- }
44
-
45
- const QUERY_FILTER_OPERATOR_SET = new Set<QueryFilterOperator>([
46
- 'eq', 'gt', 'gte', 'lt', 'lte', 'contains', 'is_empty', 'is_not_empty',
47
- ])
48
-
49
- function isQueryFilterOperator(value: unknown): value is QueryFilterOperator {
50
- return typeof value === 'string' && QUERY_FILTER_OPERATOR_SET.has(value as QueryFilterOperator)
51
- }
52
-
53
- export function cleanStr(val: unknown): string | null {
54
- return (typeof val === 'string' && val.trim()) || null
55
- }
56
-
57
- export function safeParseJson(data: unknown): Record<string, unknown> {
58
- const cleaned = cleanStr(data)
59
- if (!cleaned) return {}
60
- try {
61
- const parsed = JSON.parse(cleaned)
62
- return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
63
- ? (parsed as Record<string, unknown>)
64
- : {}
65
- } catch {
66
- return {}
67
- }
68
- }
69
-
70
- export function parsePositiveInt(value: string | undefined, fallback: number): number {
71
- if (!value) return fallback
72
- const parsed = Number.parseInt(value, 10)
73
- if (Number.isNaN(parsed) || parsed < 1) return fallback
74
- return parsed
75
- }
76
-
77
- function parseCondition(cond: unknown): QueryFilterCondition | null {
78
- if (!cond || typeof cond !== 'object') return null
79
- const candidate = cond as Record<string, unknown>
80
- if (!isQueryFilterOperator(candidate.op)) return null
81
- const rawValue = candidate.value
82
- let parsedValue: QueryFilterCondition['value'] = null
83
- if (typeof rawValue === 'string' || typeof rawValue === 'number' || typeof rawValue === 'boolean') {
84
- parsedValue = rawValue
85
- }
86
- return {
87
- id: typeof candidate.id === 'string' ? candidate.id : undefined,
88
- op: candidate.op,
89
- value: parsedValue,
90
- }
91
- }
92
-
93
- function parseFilterGroup(group: unknown): QueryFilterGroup | null {
94
- if (!group || typeof group !== 'object') return null
95
- const { columnId, type, label, conditions: rawConds } = group as Partial<QueryFilterGroup>
96
- if (typeof columnId !== 'string' || typeof type !== 'string' || !Array.isArray(rawConds)) {
97
- return null
98
- }
99
- const validConditions: QueryFilterCondition[] = []
100
- for (const cond of rawConds) {
101
- const parsedCond = parseCondition(cond)
102
- if (parsedCond) validConditions.push(parsedCond)
103
- }
104
- if (validConditions.length === 0) return null
105
- return {
106
- columnId,
107
- label: typeof label === 'string' ? label : undefined,
108
- type,
109
- conditions: validConditions,
110
- }
111
- }
112
-
113
- /** Parse sicuro dei filtri da query-string (formato dashboard con columnId). */
114
- export function parseQueryFilters(raw: string | undefined): QueryFilterGroup[] {
115
- if (!raw) return []
116
- let parsed: unknown
117
- try {
118
- parsed = JSON.parse(raw)
119
- } catch {
120
- return []
121
- }
122
- if (!parsed || typeof parsed !== 'object') return []
123
- const result: QueryFilterGroup[] = []
124
- for (const group of Object.values(parsed as Record<string, unknown>)) {
125
- const parsedGroup = parseFilterGroup(group)
126
- if (parsedGroup) result.push(parsedGroup)
127
- }
128
- return result
129
- }
130
-
131
- /**
132
- * Converte QueryFilterGroup[] (formato dashboard, columnId) in FilterGroup[]
133
- * compatibile con buildSelectQuery del Botanical Engine.
134
- */
135
- export function toEngineFilters(groups: QueryFilterGroup[]): FilterGroup[] {
136
- return groups.map((g) => ({
137
- column: g.columnId,
138
- type: g.type,
139
- conditions: g.conditions,
140
- }))
141
- }
@@ -1,26 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { HonoRequest } from "hono"
6
-
7
- /** The header Cloudflare sets on every incoming request to the Worker. */
8
- const CLOUDFLARE_CLIENT_IP_HEADER = "cf-connecting-ip"
9
-
10
- /** The fallback value used when the IP header is absent (local dev, unit tests). */
11
- const UNKNOWN_IP = "unknown"
12
-
13
- /**
14
- * Extracts the real client IP address from a Cloudflare Worker request.
15
- *
16
- * Cloudflare injects the cf-connecting-ip header on every request that
17
- * passes through the edge. In local development (wrangler dev) or in
18
- * unit tests the header may be absent, in which case the string "unknown"
19
- * is returned so that rate-limiter keys remain non-empty and safe to use.
20
- *
21
- * Never derive security decisions from this value alone; treat it as a
22
- * best-effort hint for rate-limiting and logging purposes only.
23
- */
24
- export function getClientIp(request: HonoRequest): string {
25
- return request.raw.headers.get(CLOUDFLARE_CLIENT_IP_HEADER) ?? UNKNOWN_IP
26
- }
@@ -1,142 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect, vi } from 'vitest'
6
- import { D1SchemaMutator } from './schema-mutator.d1'
7
-
8
- function makeMockDb() {
9
- const runMock = vi.fn().mockResolvedValue({ success: true })
10
- const firstMock = vi.fn().mockResolvedValue(null)
11
- const allMock = vi.fn().mockResolvedValue({ results: [] })
12
- const bindMock = vi.fn(() => ({ run: runMock, first: firstMock, all: allMock }))
13
- const stmt = { bind: bindMock, run: runMock, first: firstMock, all: allMock }
14
- const prepareMock = vi.fn(() => stmt)
15
- const batchMock = vi.fn().mockResolvedValue([])
16
-
17
- return {
18
- db: {
19
- prepare: prepareMock,
20
- batch: batchMock,
21
- } as unknown as D1Database,
22
- prepareMock,
23
- bindMock,
24
- runMock,
25
- firstMock,
26
- allMock,
27
- batchMock,
28
- }
29
- }
30
-
31
- describe('D1SchemaMutator', () => {
32
- describe('getColumns', () => {
33
- it('returns null if table has no columns (empty result)', async () => {
34
- const { db, allMock } = makeMockDb()
35
- allMock.mockResolvedValueOnce({ results: [] })
36
- const mutator = new D1SchemaMutator(db)
37
- const cols = await mutator.getColumns('users')
38
- expect(cols).toBeNull()
39
- })
40
-
41
- it('returns set of column names when columns are found', async () => {
42
- const { db, allMock, prepareMock } = makeMockDb()
43
- allMock.mockResolvedValueOnce({
44
- results: [{ name: 'id' }, { name: 'email' }],
45
- })
46
- const mutator = new D1SchemaMutator(db)
47
- const cols = await mutator.getColumns('users')
48
- expect(prepareMock).toHaveBeenCalledWith('PRAGMA table_info(users)')
49
- expect(cols).toEqual(new Set(['id', 'email']))
50
- })
51
-
52
- it('throws error if table name is unsafe', async () => {
53
- const { db } = makeMockDb()
54
- const mutator = new D1SchemaMutator(db)
55
- await expect(mutator.getColumns('users; DROP TABLE users;')).rejects.toThrow(
56
- 'Unsafe table name',
57
- )
58
- })
59
- })
60
-
61
- describe('execDdl', () => {
62
- it('returns early if statements are empty', async () => {
63
- const { db, batchMock } = makeMockDb()
64
- const mutator = new D1SchemaMutator(db)
65
- await mutator.execDdl([])
66
- expect(batchMock).not.toHaveBeenCalled()
67
- })
68
-
69
- it('prepares and executes batch DDL statements', async () => {
70
- const { db, batchMock, prepareMock } = makeMockDb()
71
- const mutator = new D1SchemaMutator(db)
72
- await mutator.execDdl(['CREATE TABLE test (id TEXT)', 'CREATE INDEX idx ON test (id)'])
73
- expect(prepareMock).toHaveBeenCalledTimes(2)
74
- expect(batchMock).toHaveBeenCalled()
75
- })
76
- })
77
-
78
- describe('dropTable', () => {
79
- it('executes DROP TABLE IF EXISTS for a safe identifier', async () => {
80
- const { db, prepareMock, runMock } = makeMockDb()
81
- const mutator = new D1SchemaMutator(db)
82
- await mutator.dropTable('articles')
83
- expect(prepareMock).toHaveBeenCalledWith('DROP TABLE IF EXISTS articles')
84
- expect(runMock).toHaveBeenCalled()
85
- })
86
-
87
- it('throws error on unsafe table name', async () => {
88
- const { db } = makeMockDb()
89
- const mutator = new D1SchemaMutator(db)
90
- await expect(mutator.dropTable('articles; --')).rejects.toThrow('Unsafe identifier')
91
- })
92
- })
93
-
94
- describe('dropColumn', () => {
95
- it('executes ALTER TABLE DROP COLUMN for safe identifiers', async () => {
96
- const { db, prepareMock, runMock } = makeMockDb()
97
- const mutator = new D1SchemaMutator(db)
98
- await mutator.dropColumn('articles', 'title')
99
- expect(prepareMock).toHaveBeenCalledWith('ALTER TABLE articles DROP COLUMN title')
100
- expect(runMock).toHaveBeenCalled()
101
- })
102
-
103
- it('throws error on unsafe column name', async () => {
104
- const { db } = makeMockDb()
105
- const mutator = new D1SchemaMutator(db)
106
- await expect(mutator.dropColumn('articles', 'title;')).rejects.toThrow('Unsafe identifier')
107
- })
108
- })
109
-
110
- describe('renameColumn', () => {
111
- it('executes ALTER TABLE RENAME COLUMN for safe identifiers', async () => {
112
- const { db, prepareMock, runMock } = makeMockDb()
113
- const mutator = new D1SchemaMutator(db)
114
- await mutator.renameColumn('articles', 'old_name', 'new_name')
115
- expect(prepareMock).toHaveBeenCalledWith('ALTER TABLE articles RENAME COLUMN old_name TO new_name')
116
- expect(runMock).toHaveBeenCalled()
117
- })
118
-
119
- it('throws error on unsafe parameters', async () => {
120
- const { db } = makeMockDb()
121
- const mutator = new D1SchemaMutator(db)
122
- await expect(mutator.renameColumn('articles', 'old', 'new --')).rejects.toThrow('Unsafe identifier')
123
- })
124
- })
125
-
126
- describe('execDestructive', () => {
127
- it('returns early if statements are empty', async () => {
128
- const { db, batchMock } = makeMockDb()
129
- const mutator = new D1SchemaMutator(db)
130
- await mutator.execDestructive([])
131
- expect(batchMock).not.toHaveBeenCalled()
132
- })
133
-
134
- it('prepares and executes batch destructive statements', async () => {
135
- const { db, batchMock, prepareMock } = makeMockDb()
136
- const mutator = new D1SchemaMutator(db)
137
- await mutator.execDestructive(['DROP TABLE test'])
138
- expect(prepareMock).toHaveBeenCalledWith('DROP TABLE test')
139
- expect(batchMock).toHaveBeenCalled()
140
- })
141
- })
142
- })
@@ -1,64 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- /// <reference types="@cloudflare/workers-types" />
6
- import type { ISchemaMutator } from '@beechcms/core'
7
-
8
- export class D1SchemaMutator implements ISchemaMutator {
9
- constructor(private readonly db: D1Database) {}
10
-
11
- async getColumns(table: string): Promise<Set<string> | null> {
12
- // PRAGMA table_info returns rows {cid,name,type,notnull,dflt_value,pk}.
13
- // Table name cannot be parameterized in PRAGMA — validate it is a safe identifier.
14
- if (!/^[A-Za-z0-9_]+$/.test(table)) throw new Error(`Unsafe table name: ${table}`)
15
- const rs = await this.db.prepare(`PRAGMA table_info(${table})`).all<{ name: string }>()
16
- const rows = rs.results ?? []
17
- if (rows.length === 0) return null // table absent
18
- return new Set(rows.map(r => r.name))
19
- }
20
-
21
- async execDdl(statements: string[]): Promise<void> {
22
- if (statements.length === 0) return
23
- // D1 batch is atomic per call; if one statement fails, the rest roll back.
24
- await this.db.batch(statements.map(s => this.db.prepare(s)))
25
- }
26
-
27
- // --- destructive (sprint 06 — Danger Zone) -------------------------------
28
- // These emit IRREVERSIBLE SQL. Identifiers cannot be parameterized in DDL, so
29
- // every table/column name is validated against the same `^[A-Za-z0-9_]+$`
30
- // guard getColumns already applies before interpolation.
31
-
32
- private static assertIdentifier(name: string): void {
33
- if (!/^[A-Za-z0-9_]+$/.test(name)) throw new Error(`Unsafe identifier: ${name}`)
34
- }
35
-
36
- async dropTable(table: string): Promise<void> {
37
- D1SchemaMutator.assertIdentifier(table)
38
- await this.db.prepare(`DROP TABLE IF EXISTS ${table}`).run()
39
- }
40
-
41
- async dropColumn(table: string, column: string): Promise<void> {
42
- D1SchemaMutator.assertIdentifier(table)
43
- D1SchemaMutator.assertIdentifier(column)
44
- await this.db.prepare(`ALTER TABLE ${table} DROP COLUMN ${column}`).run()
45
- }
46
-
47
- async renameColumn(table: string, from: string, to: string): Promise<void> {
48
- D1SchemaMutator.assertIdentifier(table)
49
- D1SchemaMutator.assertIdentifier(from)
50
- D1SchemaMutator.assertIdentifier(to)
51
- await this.db.prepare(`ALTER TABLE ${table} RENAME COLUMN ${from} TO ${to}`).run()
52
- }
53
-
54
- async execDestructive(statements: string[]): Promise<void> {
55
- if (statements.length === 0) return
56
- // The statements come from the core destructive generators
57
- // (generateDropTable / generateRetypeColumn / planFtsRebuild), which only
58
- // interpolate slugs/aliases already validated by the seed registry
59
- // (`^[a-z0-9_]+$`). FTS trigger bodies legitimately contain internal
60
- // semicolons (BEGIN … ; END), so we do not split or reject on `;`.
61
- // D1 batch is atomic per call; if one statement fails, the rest roll back.
62
- await this.db.batch(statements.map(s => this.db.prepare(s)))
63
- }
64
- }