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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/README.md +4 -4
  2. package/assets/dashboard/assets/chart-Cws4Yj6K.js +39 -0
  3. package/assets/dashboard/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
  4. package/assets/dashboard/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
  5. package/assets/dashboard/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
  6. package/assets/dashboard/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
  7. package/assets/dashboard/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
  8. package/assets/dashboard/assets/index-BjPpppHe.css +1 -0
  9. package/assets/dashboard/assets/index-CrW-zhkA.js +642 -0
  10. package/assets/dashboard/assets/lora-cyrillic-ext-wght-normal-YcVxMP-u.woff2 +0 -0
  11. package/assets/dashboard/assets/lora-cyrillic-wght-normal-UWPuZLjM.woff2 +0 -0
  12. package/assets/dashboard/assets/lora-latin-ext-wght-normal-C2Wlntb9.woff2 +0 -0
  13. package/assets/dashboard/assets/lora-latin-wght-normal-BiLcIKcI.woff2 +0 -0
  14. package/assets/dashboard/assets/lora-math-wght-normal-sOy7rEfW.woff2 +0 -0
  15. package/assets/dashboard/assets/lora-symbols-wght-normal-DQ5VrUkH.woff2 +0 -0
  16. package/assets/dashboard/assets/lora-vietnamese-wght-normal-CfJ7gtf3.woff2 +0 -0
  17. package/assets/dashboard/assets/pie-chart-recharts-CAxoDQp2.js +1 -0
  18. package/assets/dashboard/assets/timeseries-chart-recharts-CixDW-LM.js +1 -0
  19. package/assets/dashboard/index.html +2 -2
  20. package/dist/auth/__fixtures__/in-memory-hash-provider.d.ts +5 -0
  21. package/dist/auth/__fixtures__/static-token-service.d.ts +6 -0
  22. package/dist/auth/bcrypt-hash-provider.d.ts +8 -0
  23. package/dist/auth/constants.d.ts +10 -0
  24. package/dist/auth/jose-token-service.d.ts +14 -0
  25. package/dist/auth/login.d.ts +32 -0
  26. package/dist/auth/refresh.d.ts +1 -0
  27. package/dist/factory.d.ts +39 -0
  28. package/dist/features/automations/action-executors/create-entry.executor.d.ts +7 -0
  29. package/dist/features/automations/action-executors/edit-field.executor.d.ts +7 -0
  30. package/dist/features/automations/action-executors/index.d.ts +13 -0
  31. package/dist/features/automations/action-executors/send-mail.executor.d.ts +7 -0
  32. package/dist/features/automations/action-executors/set-variable.executor.d.ts +15 -0
  33. package/dist/features/automations/action-executors/webhook.executor.d.ts +7 -0
  34. package/dist/features/automations/automation-runner.d.ts +13 -0
  35. package/dist/features/automations/automation-runner.utils.d.ts +3 -0
  36. package/dist/features/automations/automations.handler.d.ts +7 -0
  37. package/dist/features/automations/automations.schema.d.ts +191 -0
  38. package/dist/features/automations/context-resolver.d.ts +8 -0
  39. package/dist/features/automations/cron-runner.d.ts +9 -0
  40. package/dist/features/automations/cron-runner.utils.d.ts +1 -0
  41. package/dist/features/automations/filter-translation.d.ts +9 -0
  42. package/dist/features/automations/index.d.ts +7 -0
  43. package/dist/features/automations/template-grammar.d.ts +73 -0
  44. package/dist/features/automations/var-access-resolver.d.ts +6 -0
  45. package/dist/features/automations/when-evaluator.d.ts +3 -0
  46. package/dist/features/automations/when-pushdown.d.ts +2 -0
  47. package/dist/features/backrefs/backrefs.handler.d.ts +3 -0
  48. package/dist/features/backrefs/d1-backref.repository.d.ts +17 -0
  49. package/dist/features/backrefs/index.d.ts +1 -0
  50. package/dist/features/content/constants.d.ts +12 -0
  51. package/dist/features/content/handlers/bulk.handler.d.ts +3 -0
  52. package/dist/features/content/handlers/create.d.ts +3 -0
  53. package/dist/features/content/handlers/delete.d.ts +3 -0
  54. package/dist/features/content/handlers/facets.d.ts +3 -0
  55. package/dist/features/content/handlers/get.d.ts +4 -0
  56. package/dist/features/content/handlers/helpers.d.ts +12 -0
  57. package/dist/features/content/handlers/list.d.ts +3 -0
  58. package/dist/features/content/handlers/update.d.ts +3 -0
  59. package/dist/features/content/index.d.ts +4 -0
  60. package/dist/features/dashboard-layout/dashboard-layout.handler.d.ts +7 -0
  61. package/dist/features/dashboard-layout/index.d.ts +1 -0
  62. package/dist/features/draft/draft.handler.d.ts +4 -0
  63. package/dist/features/draft/draft.middleware.d.ts +9 -0
  64. package/dist/features/draft/index.d.ts +1 -0
  65. package/{src/features/email/email.provider.ts → dist/features/email/email.provider.d.ts} +19 -24
  66. package/dist/features/email/email.service.d.ts +13 -0
  67. package/dist/features/email/email.types.d.ts +94 -0
  68. package/{src/features/email/index.ts → dist/features/email/index.d.ts} +3 -16
  69. package/dist/features/email/providers/resend.d.ts +36 -0
  70. package/dist/features/email/providers/smtp.d.ts +11 -0
  71. package/dist/features/email/templates/automation-mail.d.ts +8 -0
  72. package/dist/features/email/templates/password-changed.d.ts +15 -0
  73. package/dist/features/email/templates/password-reset.d.ts +18 -0
  74. package/dist/features/email/templates/shell.d.ts +58 -0
  75. package/dist/features/notifications/index.d.ts +1 -0
  76. package/dist/features/notifications/notifications.handler.d.ts +15 -0
  77. package/dist/features/password-reset/index.d.ts +6 -0
  78. package/dist/features/password-reset/request.d.ts +10 -0
  79. package/dist/features/password-reset/reset.d.ts +9 -0
  80. package/dist/features/rotate-field/index.d.ts +1 -0
  81. package/dist/features/rotate-field/rotate-field.handler.d.ts +7 -0
  82. package/dist/features/rotate-field/rotate-field.schema.d.ts +11 -0
  83. package/dist/features/schema/schema.handler.d.ts +7 -0
  84. package/dist/features/seeds/index.d.ts +1 -0
  85. package/dist/features/seeds/seeds.handler.d.ts +6 -0
  86. package/dist/features/settings/settings.handler.d.ts +7 -0
  87. package/dist/features/setup/index.d.ts +7 -0
  88. package/dist/features/stats/index.d.ts +1 -0
  89. package/dist/features/stats/stats.handler.d.ts +7 -0
  90. package/dist/features/webhooks/index.d.ts +3 -0
  91. package/dist/index.d.ts +6 -0
  92. package/dist/index.js +9666 -0
  93. package/dist/media-utils.d.ts +25 -0
  94. package/dist/middleware/auth-providers.middleware.d.ts +8 -0
  95. package/dist/middleware/observability.middleware.d.ts +20 -0
  96. package/dist/middleware/rate-limit.middleware.d.ts +9 -0
  97. package/dist/middleware/repository.middleware.d.ts +34 -0
  98. package/dist/middleware/seed-registry.middleware.d.ts +10 -0
  99. package/dist/middleware/storage.middleware.d.ts +7 -0
  100. package/dist/middleware.d.ts +11 -0
  101. package/dist/public/access-policy.d.ts +12 -0
  102. package/dist/public/api-key-middleware.d.ts +6 -0
  103. package/dist/public/cache-utils.d.ts +10 -0
  104. package/dist/public/entry-projection.d.ts +2 -0
  105. package/dist/public/idempotency.d.ts +9 -0
  106. package/dist/public/index.d.ts +11 -0
  107. package/dist/public/problem-details.d.ts +31 -0
  108. package/dist/public/public-add.d.ts +3 -0
  109. package/dist/public/public-edit.d.ts +5 -0
  110. package/dist/public/public-errors.d.ts +13 -0
  111. package/dist/public/public-read.d.ts +3 -0
  112. package/dist/public/public-routes.d.ts +3 -0
  113. package/dist/public/query-builder.d.ts +28 -0
  114. package/dist/public/rate-limit-middleware.d.ts +3 -0
  115. package/dist/public/read-list.d.ts +26 -0
  116. package/dist/public/read-single.d.ts +22 -0
  117. package/dist/public/response-builder.d.ts +22 -0
  118. package/dist/public/sanitize.d.ts +23 -0
  119. package/{src/public/slug-utils.ts → dist/public/slug-utils.d.ts} +3 -10
  120. package/dist/rate-limit/cloudflare-rate-limiter.d.ts +6 -0
  121. package/dist/rate-limit/in-memory-rate-limiter.d.ts +7 -0
  122. package/dist/rate-limit/no-op-rate-limiter.d.ts +4 -0
  123. package/dist/search-utils.d.ts +54 -0
  124. package/dist/search.d.ts +6 -0
  125. package/dist/shared/apply-policies.d.ts +10 -0
  126. package/dist/shared/automations.repository.d1.d.ts +12 -0
  127. package/dist/shared/background-notification-service.d.ts +18 -0
  128. package/dist/shared/base.repository.d1.d.ts +18 -0
  129. package/dist/shared/content-utils.d.ts +21 -0
  130. package/dist/shared/content.repository.d1.d.ts +68 -0
  131. package/dist/shared/d1-activity-log.repository.d.ts +13 -0
  132. package/dist/shared/d1-activity-logger.d.ts +20 -0
  133. package/dist/shared/d1-analytics.repository.d.ts +20 -0
  134. package/dist/shared/d1-content-scan.repository.d.ts +7 -0
  135. package/dist/shared/d1-notification.repository.d.ts +17 -0
  136. package/dist/shared/d1-password-reset-token.repository.d.ts +10 -0
  137. package/dist/shared/d1-search.repository.d.ts +14 -0
  138. package/dist/shared/d1-session.repository.d.ts +12 -0
  139. package/dist/shared/d1-setup-checklist.repository.d.ts +12 -0
  140. package/dist/shared/d1-user.repository.d.ts +18 -0
  141. package/dist/shared/d1-widget.repository.d.ts +31 -0
  142. package/dist/shared/dashboard-layout.repository.d1.d.ts +9 -0
  143. package/dist/shared/demo-data-sql.d.ts +1 -0
  144. package/dist/shared/demo-data.repository.d1.d.ts +6 -0
  145. package/dist/shared/execution-context-scheduler.d.ts +6 -0
  146. package/dist/shared/fixed-clock.d.ts +13 -0
  147. package/dist/shared/fts-sync.d.ts +1 -0
  148. package/dist/shared/idempotency.repository.d1.d.ts +16 -0
  149. package/dist/shared/in-memory-activity-logger.d.ts +11 -0
  150. package/dist/shared/in-memory-notification-service.d.ts +11 -0
  151. package/dist/shared/in-memory-seed.repository.d.ts +18 -0
  152. package/dist/shared/media.repository.d1.d.ts +35 -0
  153. package/dist/shared/qstash-notification-service.d.ts +18 -0
  154. package/dist/shared/query-utils.d.ts +36 -0
  155. package/dist/shared/request-utils.d.ts +13 -0
  156. package/dist/shared/schema-mutator.d1.d.ts +12 -0
  157. package/dist/shared/seed-layout.repository.d1.d.ts +9 -0
  158. package/dist/shared/seed-registry-cache.d.ts +12 -0
  159. package/dist/shared/seed.repository.d1.d.ts +13 -0
  160. package/dist/shared/sequential-id-generator.d.ts +13 -0
  161. package/dist/shared/site-settings.repository.d1.d.ts +7 -0
  162. package/dist/shared/storage/factory.d.ts +8 -0
  163. package/dist/shared/storage/s3-bucket.d.ts +43 -0
  164. package/dist/shared/storage-utils.d.ts +7 -0
  165. package/dist/shared/system-stats.repository.d1.d.ts +25 -0
  166. package/dist/types.d.ts +85 -0
  167. package/dist/upload.d.ts +16 -0
  168. package/dist/widget.d.ts +7 -0
  169. package/package.json +27 -18
  170. package/assets/dashboard/assets/chart-BrJFdsGZ.js +0 -39
  171. package/assets/dashboard/assets/index-CIaoMxuB.css +0 -1
  172. package/assets/dashboard/assets/index-wTKgygsg.js +0 -636
  173. package/assets/dashboard/assets/pie-chart-recharts-BEaE1FOE.js +0 -1
  174. package/assets/dashboard/assets/timeseries-chart-recharts-DmWM6imF.js +0 -1
  175. package/src/auth/__fixtures__/in-memory-hash-provider.ts +0 -17
  176. package/src/auth/__fixtures__/static-token-service.ts +0 -22
  177. package/src/auth/bcrypt-hash-provider.ts +0 -24
  178. package/src/auth/constants.ts +0 -14
  179. package/src/auth/generate-refresh-token.test.ts +0 -23
  180. package/src/auth/hash-provider.test.ts +0 -55
  181. package/src/auth/jose-token-service.ts +0 -62
  182. package/src/auth/jwt-claims-passthrough.test.ts +0 -87
  183. package/src/auth/login.test.ts +0 -100
  184. package/src/auth/login.ts +0 -78
  185. package/src/auth/refresh.ts +0 -11
  186. package/src/auth/token-service.test.ts +0 -90
  187. package/src/factory.ts +0 -370
  188. package/src/features/automations/__tests__/action-executors.test.ts +0 -255
  189. package/src/features/automations/__tests__/automation-runner.test.ts +0 -196
  190. package/src/features/automations/__tests__/automation-runner.utils.test.ts +0 -60
  191. package/src/features/automations/__tests__/automations.handler.test.ts +0 -264
  192. package/src/features/automations/__tests__/automations.repository.test.ts +0 -163
  193. package/src/features/automations/__tests__/automations.schema.test.ts +0 -224
  194. package/src/features/automations/__tests__/context-resolver.test.ts +0 -126
  195. package/src/features/automations/__tests__/cron-runner.test.ts +0 -267
  196. package/src/features/automations/__tests__/cron-runner.utils.test.ts +0 -144
  197. package/src/features/automations/__tests__/set-variable.executor.test.ts +0 -383
  198. package/src/features/automations/__tests__/template-grammar.test.ts +0 -308
  199. package/src/features/automations/__tests__/webhook.executor.test.ts +0 -142
  200. package/src/features/automations/__tests__/when-evaluator.test.ts +0 -274
  201. package/src/features/automations/__tests__/when-pushdown.test.ts +0 -281
  202. package/src/features/automations/action-executors/create-entry.executor.ts +0 -27
  203. package/src/features/automations/action-executors/edit-field.executor.ts +0 -26
  204. package/src/features/automations/action-executors/index.ts +0 -37
  205. package/src/features/automations/action-executors/send-mail.executor.ts +0 -53
  206. package/src/features/automations/action-executors/set-variable.executor.ts +0 -159
  207. package/src/features/automations/action-executors/webhook.executor.ts +0 -67
  208. package/src/features/automations/automation-runner.ts +0 -85
  209. package/src/features/automations/automation-runner.utils.ts +0 -47
  210. package/src/features/automations/automations.handler.ts +0 -197
  211. package/src/features/automations/automations.schema.ts +0 -177
  212. package/src/features/automations/context-resolver.ts +0 -152
  213. package/src/features/automations/cron-runner.ts +0 -140
  214. package/src/features/automations/cron-runner.utils.ts +0 -44
  215. package/src/features/automations/filter-translation.ts +0 -46
  216. package/src/features/automations/index.ts +0 -16
  217. package/src/features/automations/template-grammar.ts +0 -245
  218. package/src/features/automations/var-access-resolver.ts +0 -140
  219. package/src/features/automations/when-evaluator.ts +0 -87
  220. package/src/features/automations/when-pushdown.ts +0 -57
  221. package/src/features/backrefs/__tests__/backrefs.handler.test.ts +0 -359
  222. package/src/features/backrefs/backrefs.handler.ts +0 -168
  223. package/src/features/backrefs/d1-backref.repository.ts +0 -84
  224. package/src/features/backrefs/index.ts +0 -5
  225. package/src/features/content/constants.ts +0 -16
  226. package/src/features/content/handlers/bulk.handler.ts +0 -185
  227. package/src/features/content/handlers/create.ts +0 -129
  228. package/src/features/content/handlers/delete.ts +0 -62
  229. package/src/features/content/handlers/facets.ts +0 -49
  230. package/src/features/content/handlers/get.ts +0 -120
  231. package/src/features/content/handlers/helpers.test.ts +0 -180
  232. package/src/features/content/handlers/helpers.ts +0 -93
  233. package/src/features/content/handlers/list.ts +0 -174
  234. package/src/features/content/handlers/update.ts +0 -174
  235. package/src/features/content/index.ts +0 -26
  236. package/src/features/dashboard-layout/__tests__/dashboard-layout.handler.test.ts +0 -388
  237. package/src/features/dashboard-layout/dashboard-layout.handler.ts +0 -206
  238. package/src/features/dashboard-layout/index.ts +0 -5
  239. package/src/features/draft/draft.handler.ts +0 -195
  240. package/src/features/draft/draft.middleware.ts +0 -66
  241. package/src/features/draft/index.ts +0 -5
  242. package/src/features/email/email.service.ts +0 -98
  243. package/src/features/email/email.types.ts +0 -119
  244. package/src/features/email/providers/resend.ts +0 -67
  245. package/src/features/email/providers/smtp.ts +0 -55
  246. package/src/features/email/templates/automation-mail.ts +0 -19
  247. package/src/features/email/templates/password-changed.ts +0 -63
  248. package/src/features/email/templates/password-reset.ts +0 -68
  249. package/src/features/email/templates/shell.ts +0 -96
  250. package/src/features/notifications/index.ts +0 -5
  251. package/src/features/notifications/notifications.handler.ts +0 -105
  252. package/src/features/password-reset/index.ts +0 -19
  253. package/src/features/password-reset/request.ts +0 -92
  254. package/src/features/password-reset/reset.ts +0 -102
  255. package/src/features/rotate-field/index.ts +0 -5
  256. package/src/features/rotate-field/rotate-field.handler.ts +0 -132
  257. package/src/features/rotate-field/rotate-field.schema.ts +0 -17
  258. package/src/features/schema/schema.handler.ts +0 -131
  259. package/src/features/seeds/index.ts +0 -5
  260. package/src/features/seeds/seeds.handler.test.ts +0 -1199
  261. package/src/features/seeds/seeds.handler.ts +0 -558
  262. package/src/features/settings/__tests__/settings.handler.test.ts +0 -555
  263. package/src/features/settings/settings.handler.ts +0 -392
  264. package/src/features/setup/index.ts +0 -288
  265. package/src/features/stats/index.ts +0 -5
  266. package/src/features/stats/stats.handler.ts +0 -420
  267. package/src/features/webhooks/index.ts +0 -63
  268. package/src/index.ts +0 -65
  269. package/src/media-utils.ts +0 -82
  270. package/src/middleware/auth-providers.middleware.test.ts +0 -85
  271. package/src/middleware/auth-providers.middleware.ts +0 -45
  272. package/src/middleware/observability.middleware.test.ts +0 -93
  273. package/src/middleware/observability.middleware.ts +0 -70
  274. package/src/middleware/rate-limit.middleware.ts +0 -45
  275. package/src/middleware/repository.middleware.ts +0 -117
  276. package/src/middleware/seed-registry.middleware.test.ts +0 -97
  277. package/src/middleware/seed-registry.middleware.ts +0 -21
  278. package/src/middleware/storage.middleware.ts +0 -25
  279. package/src/middleware.ts +0 -45
  280. package/src/public/access-policy.ts +0 -27
  281. package/src/public/api-key-middleware.ts +0 -57
  282. package/src/public/cache-utils.ts +0 -38
  283. package/src/public/entry-projection.ts +0 -46
  284. package/src/public/idempotency.ts +0 -23
  285. package/src/public/index.ts +0 -16
  286. package/src/public/problem-details.ts +0 -124
  287. package/src/public/public-add.ts +0 -114
  288. package/src/public/public-edit.ts +0 -163
  289. package/src/public/public-errors.ts +0 -19
  290. package/src/public/public-read.ts +0 -63
  291. package/src/public/public-routes.ts +0 -88
  292. package/src/public/query-builder.test.ts +0 -224
  293. package/src/public/query-builder.ts +0 -156
  294. package/src/public/rate-limit-middleware.ts +0 -34
  295. package/src/public/read-list.ts +0 -54
  296. package/src/public/read-single.ts +0 -48
  297. package/src/public/response-builder.ts +0 -30
  298. package/src/public/sanitize.ts +0 -69
  299. package/src/rate-limit/cloudflare-rate-limiter.test.ts +0 -30
  300. package/src/rate-limit/cloudflare-rate-limiter.ts +0 -15
  301. package/src/rate-limit/in-memory-rate-limiter.test.ts +0 -37
  302. package/src/rate-limit/in-memory-rate-limiter.ts +0 -17
  303. package/src/rate-limit/no-op-rate-limiter.test.ts +0 -22
  304. package/src/rate-limit/no-op-rate-limiter.ts +0 -11
  305. package/src/search-utils.test.ts +0 -211
  306. package/src/search-utils.ts +0 -213
  307. package/src/search.ts +0 -65
  308. package/src/shared/apply-policies.test.ts +0 -81
  309. package/src/shared/apply-policies.ts +0 -67
  310. package/src/shared/automations.repository.d1.ts +0 -150
  311. package/src/shared/background-notification-service.test.ts +0 -62
  312. package/src/shared/background-notification-service.ts +0 -52
  313. package/src/shared/base.repository.d1.ts +0 -32
  314. package/src/shared/content-utils.test.ts +0 -165
  315. package/src/shared/content-utils.ts +0 -86
  316. package/src/shared/content.repository.d1.test.ts +0 -507
  317. package/src/shared/content.repository.d1.ts +0 -851
  318. package/src/shared/d1-activity-log.repository.test.ts +0 -140
  319. package/src/shared/d1-activity-log.repository.ts +0 -105
  320. package/src/shared/d1-activity-logger.test.ts +0 -86
  321. package/src/shared/d1-activity-logger.ts +0 -67
  322. package/src/shared/d1-analytics.repository.test.ts +0 -78
  323. package/src/shared/d1-analytics.repository.ts +0 -85
  324. package/src/shared/d1-content-scan.repository.test.ts +0 -80
  325. package/src/shared/d1-content-scan.repository.ts +0 -37
  326. package/src/shared/d1-notification.repository.test.ts +0 -128
  327. package/src/shared/d1-notification.repository.ts +0 -118
  328. package/src/shared/d1-password-reset-token.repository.test.ts +0 -81
  329. package/src/shared/d1-password-reset-token.repository.ts +0 -56
  330. package/src/shared/d1-search.repository.test.ts +0 -87
  331. package/src/shared/d1-search.repository.ts +0 -88
  332. package/src/shared/d1-session.repository.test.ts +0 -125
  333. package/src/shared/d1-session.repository.ts +0 -102
  334. package/src/shared/d1-setup-checklist.repository.ts +0 -36
  335. package/src/shared/d1-user.repository.test.ts +0 -151
  336. package/src/shared/d1-user.repository.ts +0 -119
  337. package/src/shared/d1-widget.repository.test.ts +0 -267
  338. package/src/shared/d1-widget.repository.ts +0 -385
  339. package/src/shared/dashboard-layout.repository.d1.test.ts +0 -135
  340. package/src/shared/dashboard-layout.repository.d1.ts +0 -58
  341. package/src/shared/demo-data-sql.ts +0 -216
  342. package/src/shared/demo-data.repository.d1.ts +0 -20
  343. package/src/shared/execution-context-scheduler.ts +0 -13
  344. package/src/shared/fixed-clock.ts +0 -25
  345. package/src/shared/fts-sync.ts +0 -8
  346. package/src/shared/idempotency.repository.d1.test.ts +0 -83
  347. package/src/shared/idempotency.repository.d1.ts +0 -60
  348. package/src/shared/in-memory-activity-logger.ts +0 -19
  349. package/src/shared/in-memory-notification-service.ts +0 -19
  350. package/src/shared/in-memory-seed.repository.ts +0 -58
  351. package/src/shared/media.repository.d1.test.ts +0 -107
  352. package/src/shared/media.repository.d1.ts +0 -68
  353. package/src/shared/qstash-notification-service.test.ts +0 -67
  354. package/src/shared/qstash-notification-service.ts +0 -55
  355. package/src/shared/query-utils.ts +0 -141
  356. package/src/shared/request-utils.ts +0 -26
  357. package/src/shared/schema-mutator.d1.test.ts +0 -142
  358. package/src/shared/schema-mutator.d1.ts +0 -64
  359. package/src/shared/seed-layout.repository.d1.test.ts +0 -114
  360. package/src/shared/seed-layout.repository.d1.ts +0 -62
  361. package/src/shared/seed-registry-cache.test.ts +0 -68
  362. package/src/shared/seed-registry-cache.ts +0 -49
  363. package/src/shared/seed.repository.d1.test.ts +0 -143
  364. package/src/shared/seed.repository.d1.ts +0 -98
  365. package/src/shared/sequential-id-generator.ts +0 -33
  366. package/src/shared/site-settings.repository.d1.ts +0 -53
  367. package/src/shared/storage/factory.ts +0 -41
  368. package/src/shared/storage/s3-bucket.ts +0 -195
  369. package/src/shared/storage-utils.ts +0 -40
  370. package/src/shared/system-stats.repository.d1.test.ts +0 -58
  371. package/src/shared/system-stats.repository.d1.ts +0 -48
  372. package/src/types.ts +0 -90
  373. package/src/upload.ts +0 -198
  374. package/src/widget.test.ts +0 -85
  375. 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
- }