@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,69 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { validateAndSanitizeSeedPayload } from '@beechcms/core'
6
- import type { Seed, ValidationDetail } from '@beechcms/core'
7
-
8
- type PublicSanitizeSuccess = {
9
- ok: true
10
- data: Record<string, unknown>
11
- }
12
-
13
- type PublicSanitizeFailure = {
14
- ok: false
15
- status: 400 | 422
16
- code: 'validation_failed' | 'dangerous_content'
17
- message: string
18
- details?: ValidationDetail[]
19
- }
20
-
21
- export type PublicSanitizeResult = PublicSanitizeSuccess | PublicSanitizeFailure
22
-
23
- /**
24
- * Public API Adapter: uses the core foundation and maps errors into the sprint format.
25
- */
26
- export function sanitizePublicPayload(
27
- seed: Seed,
28
- payload: Record<string, unknown>,
29
- options: {
30
- allowNull?: boolean
31
- operation?: 'create' | 'update'
32
- requireAtLeastOneValidField?: boolean
33
- enforceRequiredFields?: boolean
34
- } = {}
35
- ): PublicSanitizeResult {
36
- const operation = options.operation ?? 'create'
37
- const result = validateAndSanitizeSeedPayload(seed, payload, {
38
- allowNull: options.allowNull ?? false,
39
- operation,
40
- requireAtLeastOneValidField: options.requireAtLeastOneValidField ?? true,
41
- enforceRequiredFields: options.enforceRequiredFields ?? true,
42
- })
43
-
44
- if (result.dangerousFields.length > 0) {
45
- const field = result.dangerousFields[0]
46
- return {
47
- ok: false,
48
- status: 422,
49
- code: 'dangerous_content',
50
- message: `Content rejected: dangerous markup detected in field '${field}'`,
51
- }
52
- }
53
-
54
- if (result.details.length > 0) {
55
- return {
56
- ok: false,
57
- status: 400,
58
- code: 'validation_failed',
59
- message: 'Validation failed',
60
- details: result.details,
61
- }
62
- }
63
-
64
- return {
65
- ok: true,
66
- data: result.data,
67
- }
68
- }
69
-
@@ -1,30 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect, vi } from 'vitest'
6
- import { CloudflareRateLimiter } from './cloudflare-rate-limiter'
7
-
8
- describe('CloudflareRateLimiter', () => {
9
- it('returns isAllowed: true when the Cloudflare binding grants the request', async () => {
10
- const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
11
- const limiter = new CloudflareRateLimiter(mockBinding as any)
12
- const result = await limiter.checkLimit('192.168.1.1:login')
13
- expect(result.isAllowed).toBe(true)
14
- })
15
-
16
- it('returns isAllowed: false when the Cloudflare binding blocks the request', async () => {
17
- const mockBinding = { limit: vi.fn().mockResolvedValue({ success: false }) }
18
- const limiter = new CloudflareRateLimiter(mockBinding as any)
19
- const result = await limiter.checkLimit('192.168.1.1:login')
20
- expect(result.isAllowed).toBe(false)
21
- })
22
-
23
- it('forwards the exact key to the binding so per-key accounting is correct', async () => {
24
- const mockBinding = { limit: vi.fn().mockResolvedValue({ success: true }) }
25
- const limiter = new CloudflareRateLimiter(mockBinding as any)
26
- const key = '10.0.0.1:some-seed:publicApiRead'
27
- await limiter.checkLimit(key)
28
- expect(mockBinding.limit).toHaveBeenCalledWith({ key })
29
- })
30
- })
@@ -1,15 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- /// <reference types="@cloudflare/workers-types" />
6
- import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
7
-
8
- export class CloudflareRateLimiter implements IRateLimiter {
9
- constructor(private readonly binding: RateLimit) {}
10
-
11
- async checkLimit(key: string): Promise<RateLimitResult> {
12
- const { success } = await this.binding.limit({ key })
13
- return { isAllowed: success }
14
- }
15
- }
@@ -1,37 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect } from 'vitest'
6
- import { InMemoryRateLimiter } from './in-memory-rate-limiter'
7
-
8
- describe('InMemoryRateLimiter', () => {
9
- it('allows requests up to the configured maximum hit count', async () => {
10
- const limiter = new InMemoryRateLimiter(3)
11
- expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
12
- expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
13
- expect((await limiter.checkLimit('key')).isAllowed).toBe(true)
14
- })
15
-
16
- it('blocks the very next request after maxAllowedHits is reached', async () => {
17
- const limiter = new InMemoryRateLimiter(2)
18
- await limiter.checkLimit('key')
19
- await limiter.checkLimit('key')
20
- expect((await limiter.checkLimit('key')).isAllowed).toBe(false)
21
- })
22
-
23
- it('tracks hit counts independently for each key', async () => {
24
- const limiter = new InMemoryRateLimiter(1)
25
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
26
- expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
27
- // Both keys exhausted independently
28
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(false)
29
- expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(false)
30
- })
31
-
32
- it('maxAllowedHits = 1 allows exactly one request before blocking', async () => {
33
- const limiter = new InMemoryRateLimiter(1)
34
- expect((await limiter.checkLimit('k')).isAllowed).toBe(true)
35
- expect((await limiter.checkLimit('k')).isAllowed).toBe(false)
36
- })
37
- })
@@ -1,17 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
6
-
7
- export class InMemoryRateLimiter implements IRateLimiter {
8
- private readonly hitCounts = new Map<string, number>()
9
-
10
- constructor(private readonly maxAllowedHits: number) {}
11
-
12
- async checkLimit(key: string): Promise<RateLimitResult> {
13
- const currentHits = (this.hitCounts.get(key) ?? 0) + 1
14
- this.hitCounts.set(key, currentHits)
15
- return { isAllowed: currentHits <= this.maxAllowedHits }
16
- }
17
- }
@@ -1,22 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect } from 'vitest'
6
- import { NoOpRateLimiter } from './no-op-rate-limiter'
7
-
8
- describe('NoOpRateLimiter', () => {
9
- it('always returns isAllowed: true regardless of the key', async () => {
10
- const limiter = new NoOpRateLimiter()
11
- for (let i = 0; i < 100; i++) {
12
- expect((await limiter.checkLimit('any-key')).isAllowed).toBe(true)
13
- }
14
- })
15
-
16
- it('allows all keys without accumulating state', async () => {
17
- const limiter = new NoOpRateLimiter()
18
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
19
- expect((await limiter.checkLimit('ip-b')).isAllowed).toBe(true)
20
- expect((await limiter.checkLimit('ip-a')).isAllowed).toBe(true)
21
- })
22
- })
@@ -1,11 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import type { IRateLimiter, RateLimitResult } from '@beechcms/core'
6
-
7
- export class NoOpRateLimiter implements IRateLimiter {
8
- async checkLimit(_key: string): Promise<RateLimitResult> {
9
- return { isAllowed: true }
10
- }
11
- }
@@ -1,211 +0,0 @@
1
- // SPDX-License-Identifier: BUSL-1.1
2
- // Copyright (c) 2024–2026 Flavio De Musso. All rights reserved.
3
- // See LICENSE in the repository root for license terms.
4
-
5
- import { describe, it, expect } from 'vitest'
6
- import { encodeCursor, decodeCursor, buildFtsQuery, mapFtsRow } from './search-utils'
7
- import type { Seed } from '@beechcms/core'
8
-
9
- const TEXT_SEED = {
10
- slug: 'articoli',
11
- displayNameAlias: 'title',
12
- branches: [
13
- { id: 'br_01', alias: 'title', type: 'text', policies: { search: true } },
14
- ],
15
- } as unknown as Seed
16
-
17
- const SECOND_SEED = {
18
- slug: 'team',
19
- displayNameAlias: 'name',
20
- branches: [
21
- { id: 'br_01', alias: 'name', type: 'text', policies: { search: true } },
22
- ],
23
- } as unknown as Seed
24
-
25
- const NO_FTS_SEED = {
26
- slug: 'prodotti',
27
- displayNameAlias: 'nome',
28
- branches: [
29
- { id: 'br_01', alias: 'price', type: 'number' },
30
- ],
31
- } as unknown as Seed
32
-
33
- // ─── encodeCursor / decodeCursor ─────────────────────────────────────────────
34
-
35
- describe('encodeCursor / decodeCursor', () => {
36
- it('roundtrip preserves rank and entryId', () => {
37
- const cursor = encodeCursor(-1.23456, 'entry-abc')
38
- const decoded = decodeCursor(cursor)
39
- expect(decoded?.rank).toBeCloseTo(-1.23456)
40
- expect(decoded?.entryId).toBe('entry-abc')
41
- })
42
-
43
- it('entryId without colons roundtrips correctly (UUID-style IDs)', () => {
44
- const entryId = 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
45
- const cursor = encodeCursor(-3.7, entryId)
46
- const decoded = decodeCursor(cursor)
47
- expect(decoded?.rank).toBeCloseTo(-3.7)
48
- expect(decoded?.entryId).toBe(entryId)
49
- })
50
-
51
- it('returns null for invalid base64', () => {
52
- expect(decodeCursor('!!!not-base64!!!')).toBeNull()
53
- })
54
-
55
- it('returns null when decoded string has no colon separator', () => {
56
- expect(decodeCursor(btoa('noseparator'))).toBeNull()
57
- })
58
- })
59
-
60
- // ─── buildFtsQuery ───────────────────────────────────────────────────────────
61
-
62
- describe('buildFtsQuery', () => {
63
- it('returns empty-result query when no seed has a searchable FTS branch', () => {
64
- const result = buildFtsQuery(
65
- { q: 'hello', schemaSlug: null, status: null, limit: 20, cursor: null },
66
- [NO_FTS_SEED],
67
- )
68
- expect(result.sql).toContain('WHERE 1=0')
69
- expect(result.binds).toHaveLength(0)
70
- expect(result.countSql).toContain('SELECT 0 as total')
71
- })
72
-
73
- it('throws EMPTY_QUERY when all terms are stripped or too short (single chars)', () => {
74
- expect(() =>
75
- buildFtsQuery({ q: 'a b', schemaSlug: null, status: null, limit: 20, cursor: null }, [TEXT_SEED]),
76
- ).toThrow('EMPTY_QUERY')
77
- })
78
-
79
- it('generates a query referencing the seed FTS and content tables', () => {
80
- const result = buildFtsQuery(
81
- { q: 'hello', schemaSlug: null, status: null, limit: 20, cursor: null },
82
- [TEXT_SEED],
83
- )
84
- expect(result.sql).toContain('fts_articoli')
85
- expect(result.sql).toContain('content_articoli')
86
- expect(result.sql).toContain('LIMIT ?')
87
- expect(result.binds.at(-1)).toBe(21) // limit + 1 for has-more detection
88
- })
89
-
90
- it('UNION ALLs multiple seeds when no schemaSlug filter is set', () => {
91
- const result = buildFtsQuery(
92
- { q: 'test', schemaSlug: null, status: null, limit: 10, cursor: null },
93
- [TEXT_SEED, SECOND_SEED],
94
- )
95
- expect(result.sql).toContain('UNION ALL')
96
- expect(result.sql).toContain('fts_articoli')
97
- expect(result.sql).toContain('fts_team')
98
- })
99
-
100
- it('limits query to the requested schemaSlug when provided', () => {
101
- const result = buildFtsQuery(
102
- { q: 'test', schemaSlug: 'articoli', status: null, limit: 20, cursor: null },
103
- [TEXT_SEED, SECOND_SEED],
104
- )
105
- expect(result.sql).toContain('fts_articoli')
106
- expect(result.sql).not.toContain('fts_team')
107
- })
108
-
109
- it('adds status filter to WHERE clause and bind values', () => {
110
- const result = buildFtsQuery(
111
- { q: 'hello', schemaSlug: null, status: 'published', limit: 20, cursor: null },
112
- [TEXT_SEED],
113
- )
114
- expect(result.sql).toContain('ce.status = ?')
115
- expect(result.binds).toContain('published')
116
- })
117
-
118
- it('adds cursor-based pagination condition when cursor is valid', () => {
119
- const cursor = encodeCursor(-1.5, 'entry-123')
120
- const result = buildFtsQuery(
121
- { q: 'hello', schemaSlug: null, status: null, limit: 20, cursor },
122
- [TEXT_SEED],
123
- )
124
- expect(result.sql).toContain('bm25')
125
- expect(result.binds).toContain('entry-123')
126
- })
127
-
128
- it('ignores an invalid cursor and produces no pagination condition', () => {
129
- const result = buildFtsQuery(
130
- { q: 'hello', schemaSlug: null, status: null, limit: 20, cursor: 'bad-cursor' },
131
- [TEXT_SEED],
132
- )
133
- expect(result.binds).not.toContain('entry-123')
134
- })
135
-
136
- it('countSql wraps each seed count in a SUM', () => {
137
- const result = buildFtsQuery(
138
- { q: 'test', schemaSlug: null, status: null, limit: 10, cursor: null },
139
- [TEXT_SEED, SECOND_SEED],
140
- )
141
- expect(result.countSql).toContain('SUM')
142
- expect(result.countSql).toContain('fts_articoli')
143
- expect(result.countSql).toContain('fts_team')
144
- })
145
-
146
- it('count binds do not include the limit+1 sentinel', () => {
147
- const result = buildFtsQuery(
148
- { q: 'hello', schemaSlug: null, status: null, limit: 5, cursor: null },
149
- [TEXT_SEED],
150
- )
151
- expect(result.countBinds).not.toContain(6) // limit + 1 must not appear in count binds
152
- })
153
-
154
- it('single-character terms (length < 2) are filtered out', () => {
155
- // 'a b c' — single-char terms discarded; result depends on remaining terms
156
- expect(() =>
157
- buildFtsQuery({ q: 'a b c', schemaSlug: null, status: null, limit: 20, cursor: null }, [TEXT_SEED]),
158
- ).toThrow('EMPTY_QUERY')
159
- })
160
-
161
- it('numeric terms are quoted without prefix expansion', () => {
162
- const result = buildFtsQuery(
163
- { q: '2024', schemaSlug: null, status: null, limit: 20, cursor: null },
164
- [TEXT_SEED],
165
- )
166
- expect(result.binds[0]).toContain('"2024"')
167
- })
168
- })
169
-
170
- // ─── mapFtsRow ───────────────────────────────────────────────────────────────
171
-
172
- describe('mapFtsRow', () => {
173
- it('maps all FtsRow fields to SearchResultItem', () => {
174
- const row = {
175
- entry_id: 'e1', schema_slug: 'articoli', slug: 'my-post',
176
- status: 'published', title: 'My Post', excerpt: 'A snippet', rank: -1,
177
- }
178
- const result = mapFtsRow(row)
179
- expect(result.id).toBe('e1')
180
- expect(result.schema_slug).toBe('articoli')
181
- expect(result.slug).toBe('my-post')
182
- expect(result.status).toBe('published')
183
- expect(result.title).toBe('My Post')
184
- expect(result.excerpt).toBe('A snippet')
185
- expect(result.data).toEqual({})
186
- })
187
-
188
- it('strips HTML tags from excerpt but preserves <mark> and </mark>', () => {
189
- const row = {
190
- entry_id: 'e1', schema_slug: 'a', slug: null, status: 'draft',
191
- title: null, excerpt: '<p>A <mark>word</mark> here</p>', rank: 0,
192
- }
193
- expect(mapFtsRow(row).excerpt).toBe('A <mark>word</mark> here')
194
- })
195
-
196
- it('collapses multiple whitespace characters in excerpt', () => {
197
- const row = {
198
- entry_id: 'e1', schema_slug: 'a', slug: null, status: 'draft',
199
- title: null, excerpt: '<p> lots of space </p>', rank: 0,
200
- }
201
- expect(mapFtsRow(row).excerpt).toBe('lots of space')
202
- })
203
-
204
- it('returns empty string for null title', () => {
205
- const row = {
206
- entry_id: 'e1', schema_slug: 'a', slug: null, status: 'draft',
207
- title: null, excerpt: '', rank: 0,
208
- }
209
- expect(mapFtsRow(row).title).toBe('')
210
- })
211
- })
@@ -1,213 +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
- // apps/api/src/search-utils.ts
6
- // Pure functions — zero Hono dependencies, importable from Vitest.
7
- // v0.4.0: FTS is per-seed (fts_{slug}), joined with content_{slug} for metadata.
8
-
9
- import type { Seed, SearchResultRow } from "@beechcms/core"
10
-
11
- // ─── Types ───────────────────────────────────────────────────────────────────
12
-
13
- // Row returned by UNION ALL query across fts_{slug} JOIN content_{slug}
14
- export interface FtsRow {
15
- entry_id: string
16
- schema_slug: string
17
- slug: string | null
18
- status: string
19
- title: string | null
20
- excerpt: string
21
- rank: number
22
- }
23
-
24
- export interface SearchResultItem {
25
- id: string
26
- schema_slug: string
27
- slug: string | null
28
- status: string
29
- title: string
30
- excerpt: string
31
- data: Record<string, unknown>
32
- }
33
-
34
- export interface SearchResponse {
35
- items: SearchResultItem[]
36
- nextCursor: string | null
37
- total: number
38
- }
39
-
40
- export interface SearchQueryParams {
41
- q: string
42
- schemaSlug: string | null
43
- status: string | null
44
- limit: number // already clamped 1–50 by handler
45
- cursor: string | null
46
- }
47
-
48
- // ─── Cursor ──────────────────────────────────────────────────────────────────
49
-
50
- export function encodeCursor(rank: number, entryId: string): string {
51
- return btoa(`${rank}:${entryId}`)
52
- }
53
-
54
- export function decodeCursor(cursor: string): { rank: number; entryId: string } | null {
55
- try {
56
- const decoded = atob(cursor)
57
- const sep = decoded.lastIndexOf(":")
58
- if (sep === -1) return null
59
- return {
60
- rank: Number.parseFloat(decoded.slice(0, sep)),
61
- entryId: decoded.slice(sep + 1),
62
- }
63
- } catch {
64
- return null
65
- }
66
- }
67
-
68
- // ─── Helpers ─────────────────────────────────────────────────────────────────
69
-
70
- function hasSearchableFts(seed: Seed): boolean {
71
- return seed.branches.some(b =>
72
- (b.type === 'text' || b.type === 'richtext') && b.policies?.search !== false
73
- )
74
- }
75
-
76
- function buildMatchExpr(q: string): string {
77
- const MIN_PREFIX = 3
78
- const safeQ = q.replace(/["*^()]/g, " ").trim()
79
- const terms = safeQ.split(/\s+/).filter(t => t.length >= 2)
80
- if (terms.length === 0) throw new Error("EMPTY_QUERY")
81
-
82
- return terms
83
- .map(t => {
84
- if (/^\d+$/.test(t)) return `"${t}"`
85
- if (t.length <= MIN_PREFIX) return `"${t}"*`
86
- const prefixes: string[] = []
87
- for (let i = MIN_PREFIX; i <= t.length; i++) {
88
- prefixes.push(`"${t.slice(0, i)}"*`)
89
- }
90
- return `(${prefixes.join(" OR ")})`
91
- })
92
- .join(" ")
93
- }
94
-
95
- // ─── Query builder ───────────────────────────────────────────────────────────
96
-
97
- /**
98
- * Builds a UNION ALL query across all per-seed fts_{slug} tables (v0.4.0).
99
- * Each SELECT joins fts_{slug} with content_{slug} to fetch title, slug, status.
100
- * Seeds param: full registry — filtered internally by schemaSlug and FTS availability.
101
- */
102
- export function buildFtsQuery(
103
- params: SearchQueryParams,
104
- seeds: Seed[],
105
- ): {
106
- sql: string
107
- binds: unknown[]
108
- countSql: string
109
- countBinds: unknown[]
110
- } {
111
- const { q, schemaSlug, status, limit, cursor } = params
112
-
113
- const matchExpr = buildMatchExpr(q) // throws EMPTY_QUERY if needed
114
-
115
- const targetSeeds = seeds.filter(s =>
116
- hasSearchableFts(s) && (schemaSlug === null || s.slug === schemaSlug)
117
- )
118
-
119
- if (targetSeeds.length === 0) {
120
- return {
121
- sql: "SELECT NULL as entry_id, NULL as schema_slug, NULL as slug, NULL as status, NULL as title, '' as excerpt, 0 as rank WHERE 1=0",
122
- binds: [],
123
- countSql: "SELECT 0 as total",
124
- countBinds: [],
125
- }
126
- }
127
-
128
- const decoded = cursor ? decodeCursor(cursor) : null
129
-
130
- const parts: string[] = []
131
- const binds: unknown[] = []
132
- const countParts: string[] = []
133
- const countBinds: unknown[] = []
134
-
135
- for (const seed of targetSeeds) {
136
- const fts = `fts_${seed.slug}`
137
- const table = `content_${seed.slug}`
138
- const title = seed.displayNameAlias
139
-
140
- // Main query per seed
141
- const where: string[] = [`${fts} MATCH ?`]
142
- const lb: unknown[] = [matchExpr]
143
-
144
- if (status) { where.push("ce.status = ?"); lb.push(status) }
145
-
146
- if (decoded) {
147
- where.push(`(bm25(${fts}) > ? OR (bm25(${fts}) = ? AND f.entry_id > ?))`)
148
- lb.push(decoded.rank, decoded.rank, decoded.entryId)
149
- }
150
-
151
- parts.push(
152
- `SELECT f.entry_id, '${seed.slug}' AS schema_slug, ce.slug, ce.status,` +
153
- ` ce.${title} AS title,` +
154
- ` snippet(${fts}, 1, '<mark>', '</mark>', '…', 16) AS excerpt,` +
155
- ` bm25(${fts}) AS rank` +
156
- ` FROM ${fts} f JOIN ${table} ce ON ce.id = f.entry_id` +
157
- ` WHERE ${where.join(' AND ')}`
158
- )
159
- binds.push(...lb)
160
-
161
- // Count per seed (no cursor, no limit)
162
- const cw: string[] = [`${fts} MATCH ?`]
163
- const cb: unknown[] = [matchExpr]
164
- if (status) { cw.push("ce.status = ?"); cb.push(status) }
165
-
166
- countParts.push(
167
- `SELECT COUNT(*) as c FROM ${fts} f JOIN ${table} ce ON ce.id = f.entry_id WHERE ${cw.join(' AND ')}`
168
- )
169
- countBinds.push(...cb)
170
- }
171
-
172
- const sql = `${parts.join(' UNION ALL ')} ORDER BY rank, entry_id LIMIT ?`
173
- binds.push(limit + 1)
174
-
175
- const countSql = `SELECT SUM(c) as total FROM (${countParts.join(' UNION ALL ')})`
176
-
177
- return { sql, binds, countSql, countBinds }
178
- }
179
-
180
- // ─── Mapper ──────────────────────────────────────────────────────────────────
181
-
182
- function stripHtmlPreserveMark(html: string): string {
183
- return html.replace(/<(?!\/?mark\b)[^>]*>/gi, ' ').replace(/\s+/g, ' ').trim()
184
- }
185
-
186
- export function mapFtsRow(row: FtsRow): SearchResultItem {
187
- return {
188
- id: row.entry_id,
189
- schema_slug: row.schema_slug,
190
- slug: row.slug,
191
- status: row.status,
192
- title: row.title ?? "",
193
- excerpt: stripHtmlPreserveMark(row.excerpt ?? ""),
194
- data: {},
195
- }
196
- }
197
-
198
- /**
199
- * Maps a repository-shaped SearchResultRow (camelCase) to the wire-format
200
- * SearchResultItem returned by the /api/search route. Keeps the HTML
201
- * stripping behaviour previously inlined inside mapFtsRow.
202
- */
203
- export function mapSearchResultRow(row: SearchResultRow): SearchResultItem {
204
- return {
205
- id: row.entryId,
206
- schema_slug: row.schemaSlug,
207
- slug: row.slug,
208
- status: row.status,
209
- title: row.title ?? "",
210
- excerpt: stripHtmlPreserveMark(row.excerpt ?? ""),
211
- data: {},
212
- }
213
- }