@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,851 +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 {
7
- ContentRepository,
8
- EntryNotFoundError,
9
- RepositoryError,
10
- SlugConflictError,
11
- RelationTargetNotFoundError,
12
- type BulkFieldUpdate,
13
- type DraftSummary,
14
- type Seed,
15
- type Branch,
16
- type SelectOptions,
17
- buildSelectQuery,
18
- deserializeFromDb,
19
- serializeForDb,
20
- } from '@beechcms/core'
21
- import { BaseD1Repository } from './base.repository.d1'
22
-
23
- // ── Private helpers ───────────────────────────────────────────────────────────
24
-
25
- function multiRelBranches(seed: Seed): Branch[] {
26
- return seed.branches.filter(b => b.type === 'relation' && b.multiple === true)
27
- }
28
-
29
- function singleRelBranches(seed: Seed): Branch[] {
30
- return seed.branches.filter(b => b.type === 'relation' && !b.multiple)
31
- }
32
-
33
- function jTable(seedSlug: string, branchAlias: string): string {
34
- return `rel_${seedSlug}_${branchAlias}`
35
- }
36
-
37
- function jDraftTable(seedSlug: string, branchAlias: string): string {
38
- return `rel_${seedSlug}_${branchAlias}_drafts`
39
- }
40
-
41
- export class D1ContentRepository extends BaseD1Repository implements ContentRepository {
42
- /**
43
- * Deserializes a DB row using the Seed's branch definitions.
44
- * Skips multi-relation branches — their values come from junction tables.
45
- */
46
- private rowToData(seed: Seed, row: any): Record<string, any> {
47
- const data: Record<string, any> = {
48
- id: row.id,
49
- slug: row.slug,
50
- status: row.status,
51
- created_at: row.created_at,
52
- updated_at: row.updated_at,
53
- }
54
-
55
- for (const branch of seed.branches) {
56
- // Multi-relation values live in junction tables, not as columns on this table
57
- if (branch.type === 'relation' && branch.multiple === true) continue
58
- if (Object.hasOwn(row, branch.alias)) {
59
- data[branch.alias] = deserializeFromDb(branch, row[branch.alias])
60
- }
61
- }
62
-
63
- return data
64
- }
65
-
66
- /**
67
- * Fetches multi-relation arrays for a single entry and attaches them to `data`.
68
- */
69
- private async attachMultiRelations(
70
- seed: Seed,
71
- entryId: string,
72
- data: Record<string, any>,
73
- ): Promise<void> {
74
- const branches = multiRelBranches(seed)
75
- if (branches.length === 0) return
76
-
77
- const stmts = branches.map(b =>
78
- this.database
79
- .prepare(`SELECT target_id FROM ${jTable(seed.slug, b.alias)} WHERE parent_id = ? ORDER BY position ASC`)
80
- .bind(entryId),
81
- )
82
-
83
- const results = await this.database.batch(stmts)
84
- for (let i = 0; i < branches.length; i++) {
85
- data[branches[i].alias] = (results[i].results ?? []).map((r: any) => r.target_id)
86
- }
87
- }
88
-
89
- /**
90
- * Fetches multi-relation arrays for a list of entries and attaches them.
91
- * One query per multi-relation branch (O(R) queries, R = branch count).
92
- */
93
- private async attachMultiRelationsMany(
94
- seed: Seed,
95
- entries: Record<string, any>[],
96
- ): Promise<void> {
97
- const branches = multiRelBranches(seed)
98
- if (branches.length === 0 || entries.length === 0) return
99
-
100
- const ids = entries.map(e => e.id)
101
- const placeholders = ids.map(() => '?').join(', ')
102
-
103
- const stmts = branches.map(b =>
104
- this.database
105
- .prepare(
106
- `SELECT parent_id, target_id FROM ${jTable(seed.slug, b.alias)}` +
107
- ` WHERE parent_id IN (${placeholders}) ORDER BY parent_id, position ASC`,
108
- )
109
- .bind(...ids),
110
- )
111
-
112
- const results = await this.database.batch(stmts)
113
- for (let i = 0; i < branches.length; i++) {
114
- const branch = branches[i]
115
- const byParent = new Map<string, string[]>()
116
- for (const row of results[i].results ?? []) {
117
- const r = row as Record<string, unknown>
118
- const pid = r.parent_id as string
119
- if (!byParent.has(pid)) byParent.set(pid, [])
120
- byParent.get(pid)!.push(r.target_id as string)
121
- }
122
- for (const entry of entries) {
123
- entry[branch.alias] = byParent.get(entry.id) ?? []
124
- }
125
- }
126
- }
127
-
128
- async findMany(
129
- seed: Seed,
130
- options: SelectOptions
131
- ): Promise<{ items: Record<string, any>[]; total: number }> {
132
- try {
133
- const { sql, bindings } = buildSelectQuery(seed, options)
134
-
135
- const countSql = sql
136
- .replace(/SELECT .* FROM/, 'SELECT COUNT(*) as total FROM')
137
- .replace(/ ORDER BY .*$/, '')
138
- .replace(/ LIMIT \? OFFSET \?$/, '')
139
-
140
- const countBindings = bindings.slice(0, bindings.length - (options.pagination ? 2 : 0))
141
-
142
- const [batchResults, totalCountResult] = await this.database.batch([
143
- this.database.prepare(sql).bind(...bindings),
144
- this.database.prepare(countSql).bind(...countBindings),
145
- ])
146
-
147
- const contentEntries = (batchResults.results || []).map(row => this.rowToData(seed, row))
148
- const totalEntriesCount = (totalCountResult.results?.[0] as any)?.total || 0
149
-
150
- await this.attachMultiRelationsMany(seed, contentEntries)
151
-
152
- return { items: contentEntries, total: totalEntriesCount }
153
- } catch (error) {
154
- throw this.mapError(error, `findMany(${seed.slug})`)
155
- }
156
- }
157
-
158
- async findById(seed: Seed, id: string): Promise<Record<string, any>> {
159
- try {
160
- const tableName = this.getTableName(seed.slug)
161
- const entryRow = await this.database
162
- .prepare(`SELECT * FROM ${tableName} WHERE id = ? LIMIT 1`)
163
- .bind(id)
164
- .first()
165
-
166
- if (!entryRow) {
167
- throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
168
- }
169
-
170
- const data = this.rowToData(seed, entryRow)
171
- await this.attachMultiRelations(seed, id, data)
172
- return data
173
- } catch (error) {
174
- if (error instanceof EntryNotFoundError) throw error
175
- throw this.mapError(error, `findById(${seed.slug}, ${id})`)
176
- }
177
- }
178
-
179
- async findBySlug(seed: Seed, slug: string): Promise<Record<string, any>> {
180
- try {
181
- const tableName = this.getTableName(seed.slug)
182
- const entryRow = await this.database
183
- .prepare(`SELECT * FROM ${tableName} WHERE slug = ? LIMIT 1`)
184
- .bind(slug)
185
- .first()
186
-
187
- if (!entryRow) {
188
- throw new EntryNotFoundError(`Entry with slug "${slug}" not found in ${seed.slug}`)
189
- }
190
-
191
- const data = this.rowToData(seed, entryRow)
192
- await this.attachMultiRelations(seed, entryRow.id as string, data)
193
- return data
194
- } catch (error) {
195
- if (error instanceof EntryNotFoundError) throw error
196
- throw this.mapError(error, `findBySlug(${seed.slug}, ${slug})`)
197
- }
198
- }
199
-
200
- async getFacets(seed: Seed): Promise<{
201
- statuses: Record<string, number>
202
- tagsByColumn: Record<string, string[]>
203
- }> {
204
- try {
205
- const tableName = this.getTableName(seed.slug)
206
-
207
- const statusResults = await this.database
208
- .prepare(`SELECT status, COUNT(*) as count FROM ${tableName} GROUP BY status`)
209
- .all()
210
-
211
- const statusesCount: Record<string, number> = {}
212
- for (const statusRow of statusResults.results || []) {
213
- statusesCount[statusRow.status as string] = statusRow.count as number
214
- }
215
-
216
- const tagsByColumn: Record<string, string[]> = {}
217
- const tagBranches = seed.branches.filter(branch => branch.type === 'tags')
218
-
219
- for (const branch of tagBranches) {
220
- const tagResults = await this.database
221
- .prepare(`SELECT DISTINCT value FROM ${tableName}, json_each(${tableName}.${branch.alias}) WHERE value IS NOT NULL`)
222
- .all()
223
- tagsByColumn[branch.alias] = (tagResults.results || []).map(row => row.value as string)
224
- }
225
-
226
- return { statuses: statusesCount, tagsByColumn }
227
- } catch (error) {
228
- throw this.mapError(error, `getFacets(${seed.slug})`)
229
- }
230
- }
231
-
232
- async existsSlug(seed: Seed, slug: string, excludeId?: string): Promise<boolean> {
233
- try {
234
- const tableName = this.getTableName(seed.slug)
235
- let sql = `SELECT 1 FROM ${tableName} WHERE slug = ?`
236
- const queryBindings: any[] = [slug]
237
-
238
- if (excludeId) {
239
- sql += ` AND id != ?`
240
- queryBindings.push(excludeId)
241
- }
242
-
243
- const entryExistsResult = await this.database.prepare(sql).bind(...queryBindings).first()
244
- return entryExistsResult !== null
245
- } catch (error) {
246
- throw this.mapError(error, `existsSlug(${seed.slug}, ${slug})`)
247
- }
248
- }
249
-
250
-
251
- private buildCreateMainStmt(
252
- seed: Seed,
253
- id: string,
254
- slug: string,
255
- status: string,
256
- data: Record<string, any>
257
- ): D1PreparedStatement {
258
- const tableName = this.getTableName(seed.slug)
259
- const columnNames = ['id', 'slug', 'status']
260
- const placeholders = ['?', '?', '?']
261
- const queryBindings: any[] = [id, slug, status]
262
-
263
- const mRelAliases = new Set(multiRelBranches(seed).map(b => b.alias))
264
-
265
- for (const branch of seed.branches) {
266
- if (mRelAliases.has(branch.alias)) continue
267
- if (Object.hasOwn(data, branch.alias)) {
268
- columnNames.push(branch.alias)
269
- placeholders.push('?')
270
- queryBindings.push(serializeForDb(branch, data[branch.alias]))
271
- }
272
- }
273
-
274
- const mainSql = `INSERT INTO ${tableName} (${columnNames.join(', ')}) VALUES (${placeholders.join(', ')})`
275
- return this.database.prepare(mainSql).bind(...queryBindings)
276
- }
277
-
278
- private buildUpdateMainStmt(
279
- seed: Seed,
280
- id: string,
281
- data: Record<string, any>,
282
- status?: string
283
- ): { stmt: D1PreparedStatement | null; junctionUpdates: any[] } {
284
- const tableName = this.getTableName(seed.slug)
285
- const updateClauses: string[] = []
286
- const queryBindings: any[] = []
287
-
288
- const mRelBranches = multiRelBranches(seed)
289
- const mRelAliases = new Set(mRelBranches.map(b => b.alias))
290
-
291
- if (status) {
292
- updateClauses.push('status = ?')
293
- queryBindings.push(status)
294
- }
295
-
296
- for (const branch of seed.branches) {
297
- if (mRelAliases.has(branch.alias)) continue
298
- if (Object.hasOwn(data, branch.alias)) {
299
- updateClauses.push(`${branch.alias} = ?`)
300
- queryBindings.push(serializeForDb(branch, data[branch.alias]))
301
- }
302
- }
303
-
304
- const junctionUpdates = mRelBranches.filter(b => Object.hasOwn(data, b.alias))
305
-
306
- if (updateClauses.length === 0 && junctionUpdates.length === 0) {
307
- return { stmt: null, junctionUpdates: [] }
308
- }
309
-
310
- updateClauses.push('updated_at = (unixepoch())')
311
-
312
- const mainSql = `UPDATE ${tableName} SET ${updateClauses.join(', ')} WHERE id = ?`
313
- queryBindings.push(id)
314
-
315
- return {
316
- stmt: this.database.prepare(mainSql).bind(...queryBindings),
317
- junctionUpdates
318
- }
319
- }
320
-
321
- private buildJunctionInserts(seedSlug: string, parentId: string, branchAlias: string, targetIds: string[]): D1PreparedStatement[] {
322
- const jt = jTable(seedSlug, branchAlias)
323
- return targetIds.map((targetId, i) =>
324
- this.database
325
- .prepare(`INSERT INTO ${jt} (parent_id, target_id, position) VALUES (?, ?, ?)`)
326
- .bind(parentId, targetId, i),
327
- )
328
- }
329
-
330
- private buildDraftJunctionInserts(seedSlug: string, entryId: string, branchAlias: string, targetIds: string[]): D1PreparedStatement[] {
331
- const jdt = jDraftTable(seedSlug, branchAlias)
332
- return targetIds.map((targetId, i) =>
333
- this.database
334
- .prepare(`INSERT INTO ${jdt} (entry_id, target_id, position) VALUES (?, ?, ?)`)
335
- .bind(entryId, targetId, i),
336
- )
337
- }
338
-
339
- private async validatePublishDraftRelations(seed: Seed, draftRow: Record<string, unknown>, entryId: string) {
340
- for (const branch of singleRelBranches(seed)) {
341
- const value = draftRow[branch.alias]
342
- if (value == null) continue
343
- const exists = await this.database
344
- .prepare(`SELECT 1 FROM content_${branch.targetSeed} WHERE id = ? LIMIT 1`)
345
- .bind(value)
346
- .first()
347
- if (!exists) {
348
- throw new RelationTargetNotFoundError({
349
- alias: branch.alias,
350
- targetSeed: branch.targetSeed!,
351
- value: typeof value === 'string' ? value : (JSON.stringify(value) ?? 'undefined'),
352
- })
353
- }
354
- }
355
-
356
- const mRelBranches = multiRelBranches(seed)
357
- const mRelDraftIds = new Map<string, string[]>()
358
-
359
- for (const branch of mRelBranches) {
360
- const jdt = jDraftTable(seed.slug, branch.alias)
361
- const rows = await this.database
362
- .prepare(`SELECT target_id FROM ${jdt} WHERE entry_id = ? ORDER BY position ASC`)
363
- .bind(entryId)
364
- .all()
365
- const targetIds = (rows.results ?? []).map((r: any) => String(r.target_id))
366
-
367
- for (const targetId of targetIds) {
368
- const exists = await this.database
369
- .prepare(`SELECT 1 FROM content_${branch.targetSeed} WHERE id = ? LIMIT 1`)
370
- .bind(targetId)
371
- .first()
372
- if (!exists) {
373
- throw new RelationTargetNotFoundError({
374
- alias: branch.alias,
375
- targetSeed: branch.targetSeed!,
376
- value: targetId,
377
- })
378
- }
379
- }
380
- mRelDraftIds.set(branch.alias, targetIds)
381
- }
382
-
383
- return { mRelBranches, mRelDraftIds }
384
- }
385
-
386
- private getBulkArrayUpdateStmts(seedSlug: string, id: string, alias: string, update: BulkFieldUpdate): D1PreparedStatement[] {
387
- const jt = jTable(seedSlug, alias)
388
- const stmts: D1PreparedStatement[] = []
389
-
390
- if (update.kind === 'array_replace') {
391
- const deleteStmt = this.database.prepare(`DELETE FROM ${jt} WHERE parent_id = ?`).bind(id)
392
- const insertStmts = update.value.map((v, i) =>
393
- this.database.prepare(`INSERT INTO ${jt} (parent_id, target_id, position) VALUES (?, ?, ?)`).bind(id, v, i)
394
- )
395
- stmts.push(deleteStmt, ...insertStmts)
396
- } else if (update.kind === 'array_add') {
397
- for (const targetId of update.value) {
398
- stmts.push(
399
- this.database
400
- .prepare(
401
- `INSERT OR IGNORE INTO ${jt} (parent_id, target_id, position) VALUES (?, ?, (SELECT COALESCE(MAX(position), -1) + 1 FROM ${jt} WHERE parent_id = ?))`,
402
- )
403
- .bind(id, targetId, id),
404
- )
405
- }
406
- } else if (update.kind === 'array_remove' && update.value.length > 0) {
407
- const placeholders = update.value.map(() => '?').join(', ')
408
- stmts.push(
409
- this.database
410
- .prepare(`DELETE FROM ${jt} WHERE parent_id = ? AND target_id IN (${placeholders})`)
411
- .bind(id, ...update.value),
412
- )
413
- }
414
- return stmts
415
- }
416
-
417
- private async processBulkUpdateSingle(
418
- seedSlug: string,
419
- id: string,
420
- fields: Record<string, BulkFieldUpdate>,
421
- tableName: string
422
- ): Promise<void> {
423
- const stmts: D1PreparedStatement[] = []
424
-
425
- const setClauses: string[] = ['updated_at = (unixepoch())']
426
- const setBindings: unknown[] = []
427
-
428
- for (const [alias, update] of Object.entries(fields)) {
429
- if (update.kind === 'set') {
430
- setClauses.unshift(`${alias} = ?`)
431
- setBindings.push(update.value)
432
- } else {
433
- stmts.push(...this.getBulkArrayUpdateStmts(seedSlug, id, alias, update))
434
- }
435
- }
436
-
437
- stmts.unshift(
438
- this.database
439
- .prepare(`UPDATE ${tableName} SET ${setClauses.join(', ')} WHERE id = ?`)
440
- .bind(...setBindings, id),
441
- )
442
-
443
- const results = await this.database.batch(stmts)
444
- if ((results[0].meta?.changes ?? 0) === 0) {
445
- throw new Error('not-found')
446
- }
447
- }
448
-
449
- private async processBulkUpdateChunk(
450
- seedSlug: string,
451
- chunk: string[],
452
- fields: Record<string, BulkFieldUpdate>,
453
- tableName: string
454
- ): Promise<{ updated: number; failed: Array<{ id: string; reason: string }> }> {
455
- let updated = 0
456
- const failed: Array<{ id: string; reason: string }> = []
457
-
458
- for (const id of chunk) {
459
- try {
460
- await this.processBulkUpdateSingle(seedSlug, id, fields, tableName)
461
- updated++
462
- } catch (err) {
463
- const msg = err instanceof Error ? err.message : String(err)
464
- if (msg === 'not-found') {
465
- failed.push({ id, reason: 'not-found' })
466
- } else {
467
- const isFk = /FOREIGN KEY constraint failed/i.test(msg)
468
- failed.push({ id, reason: isFk ? `relation-target-not-found:${id}` : `error:${msg}` })
469
- }
470
- }
471
- }
472
- return { updated, failed }
473
- }
474
-
475
- async create(
476
- seed: Seed,
477
- id: string,
478
- slug: string,
479
- status: string,
480
- data: Record<string, any>
481
- ): Promise<void> {
482
- try {
483
- if (await this.existsSlug(seed, slug)) {
484
- throw new SlugConflictError(`Slug "${slug}" already exists for ${seed.slug}`)
485
- }
486
-
487
- const batchStmts: D1PreparedStatement[] = [
488
- this.buildCreateMainStmt(seed, id, slug, status, data),
489
- ]
490
-
491
- for (const branch of multiRelBranches(seed)) {
492
- const value = data[branch.alias]
493
- if (!Array.isArray(value) || value.length === 0) continue
494
- batchStmts.push(...this.buildJunctionInserts(seed.slug, id, branch.alias, value))
495
- }
496
-
497
- if (batchStmts.length === 1) {
498
- await batchStmts[0].run()
499
- } else {
500
- await this.database.batch(batchStmts)
501
- }
502
- } catch (error) {
503
- if (error instanceof SlugConflictError) throw error
504
- throw this.mapError(error, `create(${seed.slug})`)
505
- }
506
- }
507
-
508
- async update(
509
- seed: Seed,
510
- id: string,
511
- data: Record<string, any>,
512
- status?: string
513
- ): Promise<void> {
514
- try {
515
- const { stmt, junctionUpdates } = this.buildUpdateMainStmt(seed, id, data, status)
516
-
517
- if (!stmt && junctionUpdates.length === 0) return
518
-
519
- const batchStmts: D1PreparedStatement[] = []
520
- if (stmt) batchStmts.push(stmt)
521
-
522
- for (const branch of junctionUpdates) {
523
- const jt = jTable(seed.slug, branch.alias)
524
- const value = (data[branch.alias] ?? []) as string[]
525
- const deleteStmt = this.database.prepare(`DELETE FROM ${jt} WHERE parent_id = ?`).bind(id)
526
- batchStmts.push(deleteStmt, ...this.buildJunctionInserts(seed.slug, id, branch.alias, value))
527
- }
528
-
529
- if (batchStmts.length === 1) {
530
- const updateResult = await batchStmts[0].run()
531
- if (updateResult.meta.changes === 0) {
532
- throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
533
- }
534
- } else if (batchStmts.length > 1) {
535
- const results = await this.database.batch(batchStmts)
536
- if (stmt && (results[0].meta?.changes ?? 0) === 0) {
537
- throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
538
- }
539
- }
540
- } catch (error) {
541
- if (error instanceof EntryNotFoundError) throw error
542
- throw this.mapError(error, `update(${seed.slug}, ${id})`)
543
- }
544
- }
545
-
546
- async delete(seed: Seed, id: string): Promise<{ row: Record<string, any> }> {
547
- try {
548
- const tableName = this.getTableName(seed.slug)
549
-
550
- const entryRow = await this.database
551
- .prepare(`SELECT * FROM ${tableName} WHERE id = ?`)
552
- .bind(id)
553
- .first()
554
-
555
- if (!entryRow) {
556
- throw new EntryNotFoundError(`Entry ${id} not found in ${seed.slug}`)
557
- }
558
-
559
- await this.database.prepare(`DELETE FROM ${tableName} WHERE id = ?`).bind(id).run()
560
-
561
- return { row: this.rowToData(seed, entryRow) }
562
- } catch (error) {
563
- if (error instanceof EntryNotFoundError) throw error
564
- throw this.mapError(error, `delete(${seed.slug}, ${id})`)
565
- }
566
- }
567
-
568
- async saveDraft(seed: Seed, entryId: string, data: Record<string, any>): Promise<void> {
569
- try {
570
- if (!seed.allowDrafts) {
571
- throw new RepositoryError(`Drafts not allowed for ${seed.slug}`)
572
- }
573
-
574
- const draftTableName = this.getTableName(seed.slug, true)
575
- const mRelBranches = multiRelBranches(seed)
576
- const mRelAliases = new Set(mRelBranches.map(b => b.alias))
577
-
578
- const columnNames = ['entry_id']
579
- const placeholders = ['?']
580
- const queryBindings: any[] = [entryId]
581
- const updateClauses: string[] = []
582
-
583
- for (const branch of seed.branches) {
584
- if (mRelAliases.has(branch.alias)) continue
585
- if (Object.hasOwn(data, branch.alias)) {
586
- const serializedValue = serializeForDb(branch, data[branch.alias])
587
- columnNames.push(branch.alias)
588
- placeholders.push('?')
589
- queryBindings.push(serializedValue)
590
- updateClauses.push(`${branch.alias} = EXCLUDED.${branch.alias}`)
591
- }
592
- }
593
-
594
- updateClauses.push('updated_at = (unixepoch())')
595
-
596
- const sql = `
597
- INSERT INTO ${draftTableName} (${columnNames.join(', ')})
598
- VALUES (${placeholders.join(', ')})
599
- ON CONFLICT(entry_id) DO UPDATE SET ${updateClauses.join(', ')}
600
- `
601
-
602
- const junctionUpdates = mRelBranches.filter(b => Object.hasOwn(data, b.alias))
603
-
604
- if (junctionUpdates.length === 0) {
605
- await this.database.prepare(sql).bind(...queryBindings).run()
606
- return
607
- }
608
-
609
- const batchStmts: D1PreparedStatement[] = [
610
- this.database.prepare(sql).bind(...queryBindings),
611
- ]
612
-
613
- for (const branch of junctionUpdates) {
614
- const jdt = jDraftTable(seed.slug, branch.alias)
615
- const value = (data[branch.alias] ?? []) as string[]
616
- batchStmts.push(
617
- this.database.prepare(`DELETE FROM ${jdt} WHERE entry_id = ?`).bind(entryId),
618
- )
619
- for (let i = 0; i < value.length; i++) {
620
- batchStmts.push(
621
- this.database
622
- .prepare(`INSERT INTO ${jdt} (entry_id, target_id, position) VALUES (?, ?, ?)`)
623
- .bind(entryId, value[i], i),
624
- )
625
- }
626
- }
627
-
628
- await this.database.batch(batchStmts)
629
- } catch (error) {
630
- throw this.mapError(error, `saveDraft(${seed.slug}, ${entryId})`)
631
- }
632
- }
633
-
634
- async getDraft(seed: Seed, entryId: string): Promise<Record<string, any> | null> {
635
- try {
636
- if (!seed.allowDrafts) return null
637
-
638
- const draftTableName = this.getTableName(seed.slug, true)
639
- const draftRow = await this.database
640
- .prepare(`SELECT * FROM ${draftTableName} WHERE entry_id = ?`)
641
- .bind(entryId)
642
- .first()
643
-
644
- if (!draftRow) return null
645
-
646
- const mRelBranches = multiRelBranches(seed)
647
- const mRelAliases = new Set(mRelBranches.map(b => b.alias))
648
-
649
- const draftData: Record<string, any> = {}
650
- for (const branch of seed.branches) {
651
- if (mRelAliases.has(branch.alias)) continue
652
- if (draftRow[branch.alias] !== null) {
653
- draftData[branch.alias] = deserializeFromDb(branch, draftRow[branch.alias])
654
- }
655
- }
656
-
657
- // Fetch multi-relation draft arrays from junction draft tables
658
- if (mRelBranches.length > 0) {
659
- const stmts = mRelBranches.map(b =>
660
- this.database
661
- .prepare(`SELECT target_id FROM ${jDraftTable(seed.slug, b.alias)} WHERE entry_id = ? ORDER BY position ASC`)
662
- .bind(entryId),
663
- )
664
- const results = await this.database.batch(stmts)
665
- for (let i = 0; i < mRelBranches.length; i++) {
666
- const ids = (results[i].results ?? []).map((r: any) => r.target_id as string)
667
- if (ids.length > 0) {
668
- draftData[mRelBranches[i].alias] = ids
669
- }
670
- }
671
- }
672
-
673
- return draftData
674
- } catch (error) {
675
- throw this.mapError(error, `getDraft(${seed.slug}, ${entryId})`)
676
- }
677
- }
678
-
679
- async hasDraft(seed: Seed, entryId: string): Promise<boolean> {
680
- try {
681
- if (!seed.allowDrafts) return false
682
- const draftTableName = this.getTableName(seed.slug, true)
683
- const draftExistsResult = await this.database
684
- .prepare(`SELECT 1 FROM ${draftTableName} WHERE entry_id = ? LIMIT 1`)
685
- .bind(entryId)
686
- .first()
687
- return draftExistsResult !== null
688
- } catch (error) {
689
- throw this.mapError(error, `hasDraft(${seed.slug}, ${entryId})`)
690
- }
691
- }
692
-
693
- async publishDraft(seed: Seed, entryId: string): Promise<void> {
694
- try {
695
- if (!seed.allowDrafts) return
696
-
697
- const draftTableName = this.getTableName(seed.slug, true)
698
- const liveTableName = this.getTableName(seed.slug)
699
-
700
- const draftRow = await this.database
701
- .prepare(`SELECT * FROM ${draftTableName} WHERE entry_id = ?`)
702
- .bind(entryId)
703
- .first<Record<string, unknown>>()
704
-
705
- if (!draftRow) {
706
- throw new EntryNotFoundError(`No draft found for ${entryId} in ${seed.slug}`)
707
- }
708
-
709
- const { mRelBranches, mRelDraftIds } = await this.validatePublishDraftRelations(seed, draftRow, entryId)
710
-
711
- const mRelAliases = new Set(mRelBranches.map(b => b.alias))
712
- const updateClauses: string[] = []
713
- const queryBindings: any[] = []
714
-
715
- for (const branch of seed.branches) {
716
- if (mRelAliases.has(branch.alias)) continue
717
- if (draftRow[branch.alias] !== null) {
718
- updateClauses.push(`${branch.alias} = ?`)
719
- queryBindings.push(draftRow[branch.alias])
720
- }
721
- }
722
-
723
- updateClauses.push('updated_at = (unixepoch())')
724
- const updateSql = `UPDATE ${liveTableName} SET ${updateClauses.join(', ')} WHERE id = ?`
725
- queryBindings.push(entryId)
726
-
727
- const batchStmts: D1PreparedStatement[] = [
728
- this.database.prepare(updateSql).bind(...queryBindings),
729
- this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId),
730
- ]
731
-
732
- for (const branch of mRelBranches) {
733
- const lt = jTable(seed.slug, branch.alias)
734
- const jdt = jDraftTable(seed.slug, branch.alias)
735
- const targetIds = mRelDraftIds.get(branch.alias) ?? []
736
-
737
- batchStmts.push(
738
- this.database.prepare(`DELETE FROM ${lt} WHERE parent_id = ?`).bind(entryId),
739
- this.database.prepare(`DELETE FROM ${jdt} WHERE entry_id = ?`).bind(entryId),
740
- ...this.buildJunctionInserts(seed.slug, entryId, branch.alias, targetIds)
741
- )
742
- }
743
-
744
- await this.database.batch(batchStmts)
745
- } catch (error) {
746
- if (error instanceof EntryNotFoundError) throw error
747
- if (error instanceof RelationTargetNotFoundError) throw error
748
- throw this.mapError(error, `publishDraft(${seed.slug}, ${entryId})`)
749
- }
750
- }
751
-
752
- async bulkUpdate(
753
- seedSlug: string,
754
- ids: string[],
755
- fields: Record<string, BulkFieldUpdate>,
756
- ): Promise<{ updated: number; failed: Array<{ id: string; reason: string }> }> {
757
- const CHUNK = 50
758
- const tableName = this.getTableName(seedSlug)
759
- let updated = 0
760
- const failed: Array<{ id: string; reason: string }> = []
761
-
762
- for (let offset = 0; offset < ids.length; offset += CHUNK) {
763
- const chunk = ids.slice(offset, offset + CHUNK)
764
- const res = await this.processBulkUpdateChunk(seedSlug, chunk, fields, tableName)
765
- updated += res.updated
766
- failed.push(...res.failed)
767
- }
768
-
769
- return { updated, failed }
770
- }
771
-
772
- async deleteDraft(seed: Seed, entryId: string): Promise<void> {
773
- try {
774
- if (!seed.allowDrafts) return
775
- const draftTableName = this.getTableName(seed.slug, true)
776
- await this.database.prepare(`DELETE FROM ${draftTableName} WHERE entry_id = ?`).bind(entryId).run()
777
- } catch (error) {
778
- throw this.mapError(error, `deleteDraft(${seed.slug}, ${entryId})`)
779
- }
780
- }
781
-
782
- async findPendingDrafts(seeds: Seed[]): Promise<DraftSummary[]> {
783
- try {
784
- if (seeds.length === 0) return []
785
-
786
- const unionSelects: string[] = []
787
- const bindings: (string | number)[] = []
788
-
789
- for (const seed of seeds) {
790
- const draftTable = `content_${seed.slug}_drafts`
791
- const liveTable = `content_${seed.slug}`
792
- const titleCol = seed.displayNameAlias
793
- const seedLabel = seed.labelPlural ?? seed.label
794
-
795
- unionSelects.push(`
796
- SELECT
797
- ? AS seed_slug,
798
- ? AS seed_label,
799
- d.entry_id AS id,
800
- d.updated_at AS updated_at,
801
- COALESCE(d.${titleCol}, l.${titleCol}) AS title
802
- FROM ${draftTable} d
803
- LEFT JOIN ${liveTable} l ON l.id = d.entry_id
804
- `)
805
- bindings.push(seed.slug, seedLabel)
806
- }
807
-
808
- const sql = `
809
- WITH all_drafts AS (
810
- ${unionSelects.join('\nUNION ALL\n')}
811
- )
812
- SELECT
813
- ad.seed_slug AS seedSlug,
814
- ad.seed_label AS seedLabel,
815
- ad.id AS id,
816
- ad.updated_at AS updatedAt,
817
- ad.title AS title,
818
- al.user_name AS lastName,
819
- al.user_email AS lastEmail
820
- FROM all_drafts ad
821
- LEFT JOIN activity_logs al
822
- ON al.id = (
823
- SELECT id FROM activity_logs
824
- WHERE entity_id = ad.id
825
- AND entity_slug = ad.seed_slug
826
- AND action = 'update'
827
- AND json_extract(details, '$.note') = 'draft saved'
828
- ORDER BY created_at DESC
829
- LIMIT 1
830
- )
831
- ORDER BY ad.updated_at DESC
832
- `
833
-
834
- const { results } = await this.database.prepare(sql).bind(...bindings).all()
835
-
836
- return (results ?? []).map((row: any): DraftSummary => ({
837
- id: String(row.id),
838
- seedSlug: String(row.seedSlug),
839
- seedLabel: String(row.seedLabel),
840
- title: row.title != null && String(row.title).trim() ? String(row.title) : String(row.id),
841
- updatedAt: Number(row.updatedAt),
842
- lastModifiedBy: {
843
- name: row.lastName ?? null,
844
- email: row.lastEmail ?? '',
845
- },
846
- }))
847
- } catch (error) {
848
- throw this.mapError(error, 'findPendingDrafts')
849
- }
850
- }
851
- }