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

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