emdash 0.25.1 → 0.27.0

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 (418) hide show
  1. package/dist/{adapters-DAAjOID4.d.mts → adapters-u037EnTR.d.mts} +1 -1
  2. package/dist/{adapters-DAAjOID4.d.mts.map → adapters-u037EnTR.d.mts.map} +1 -1
  3. package/dist/api/route-utils.d.mts +3 -3
  4. package/dist/api/route-utils.mjs +11 -11
  5. package/dist/api/schemas/index.d.mts +1 -1
  6. package/dist/{api-D2_sbavn.mjs → api-80kycR9o.mjs} +86 -30
  7. package/dist/api-80kycR9o.mjs.map +1 -0
  8. package/dist/{apply-Bk2mAuLn.mjs → apply-DSypk6Iu.mjs} +20 -18
  9. package/dist/apply-DSypk6Iu.mjs.map +1 -0
  10. package/dist/astro/index.d.mts +11 -10
  11. package/dist/astro/index.d.mts.map +1 -1
  12. package/dist/astro/index.mjs +12 -3
  13. package/dist/astro/index.mjs.map +1 -1
  14. package/dist/astro/middleware/auth.d.mts +10 -9
  15. package/dist/astro/middleware/auth.d.mts.map +1 -1
  16. package/dist/astro/middleware/auth.mjs +4 -3
  17. package/dist/astro/middleware/auth.mjs.map +1 -1
  18. package/dist/astro/middleware/redirect.mjs +4 -4
  19. package/dist/astro/middleware.d.mts +2 -9
  20. package/dist/astro/middleware.d.mts.map +1 -1
  21. package/dist/astro/middleware.mjs +86 -85
  22. package/dist/astro/middleware.mjs.map +1 -1
  23. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
  24. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
  25. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  26. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  27. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +3 -3
  28. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
  29. package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
  30. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
  31. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +10 -10
  32. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +10 -10
  33. package/dist/astro/routes/api/admin/bylines/index.mjs +10 -10
  34. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +8 -8
  35. package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
  36. package/dist/astro/routes/api/admin/comments/bulk.mjs +7 -7
  37. package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
  38. package/dist/astro/routes/api/admin/comments/index.mjs +7 -7
  39. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
  40. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
  41. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  42. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  43. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +29 -29
  44. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  45. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +29 -29
  46. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  47. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +29 -29
  48. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  49. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +28 -28
  50. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +28 -28
  51. package/dist/astro/routes/api/admin/plugins/index.mjs +29 -29
  52. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  54. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +28 -28
  55. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +28 -28
  56. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +28 -28
  57. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +28 -28
  58. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +29 -29
  59. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +28 -28
  60. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +29 -29
  61. package/dist/astro/routes/api/admin/plugins/updates.mjs +28 -28
  62. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +28 -28
  63. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  64. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +28 -28
  65. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
  66. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  67. package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
  68. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
  69. package/dist/astro/routes/api/admin/users/index.mjs +3 -3
  70. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  71. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  72. package/dist/astro/routes/api/auth/invite/complete.mjs +3 -3
  73. package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
  74. package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
  75. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  76. package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
  77. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  78. package/dist/astro/routes/api/auth/me.mjs +4 -4
  79. package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
  80. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  81. package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
  82. package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
  83. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
  84. package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
  85. package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
  86. package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
  87. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  88. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +7 -7
  89. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +7 -7
  90. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  91. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
  92. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  93. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  94. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +4 -4
  95. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
  96. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  97. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  98. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +4 -4
  99. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +9 -9
  100. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  101. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
  102. package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
  103. package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
  104. package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
  105. package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
  106. package/dist/astro/routes/api/dashboard.mjs +8 -8
  107. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  108. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  109. package/dist/astro/routes/api/import/probe.mjs +4 -4
  110. package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
  111. package/dist/astro/routes/api/import/wordpress/execute.d.mts +10 -9
  112. package/dist/astro/routes/api/import/wordpress/execute.d.mts.map +1 -1
  113. package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -9
  114. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -9
  115. package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
  116. package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
  117. package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
  118. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
  119. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  120. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
  121. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
  122. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +7 -7
  123. package/dist/astro/routes/api/manifest.d.mts.map +1 -1
  124. package/dist/astro/routes/api/manifest.mjs +13 -4
  125. package/dist/astro/routes/api/manifest.mjs.map +1 -1
  126. package/dist/astro/routes/api/mcp.mjs +34 -34
  127. package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
  128. package/dist/astro/routes/api/media/_id_.mjs +4 -4
  129. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  130. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  131. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  132. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  133. package/dist/astro/routes/api/media/upload-url.mjs +4 -4
  134. package/dist/astro/routes/api/media.mjs +5 -5
  135. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +6 -6
  136. package/dist/astro/routes/api/menus/_name_/items.mjs +6 -6
  137. package/dist/astro/routes/api/menus/_name_/reorder.mjs +6 -6
  138. package/dist/astro/routes/api/menus/_name_/translations.mjs +6 -6
  139. package/dist/astro/routes/api/menus/_name_.mjs +6 -6
  140. package/dist/astro/routes/api/menus/index.mjs +6 -6
  141. package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
  142. package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
  143. package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
  144. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  145. package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
  146. package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
  147. package/dist/astro/routes/api/oauth/token.mjs +2 -2
  148. package/dist/astro/routes/api/openapi.json.mjs +2 -2
  149. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  150. package/dist/astro/routes/api/redirects/404s/index.mjs +6 -6
  151. package/dist/astro/routes/api/redirects/404s/summary.mjs +6 -6
  152. package/dist/astro/routes/api/redirects/_id_.mjs +7 -7
  153. package/dist/astro/routes/api/redirects/index.mjs +7 -7
  154. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  155. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  156. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +28 -28
  157. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +28 -28
  158. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +28 -28
  159. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +28 -28
  160. package/dist/astro/routes/api/schema/collections/index.mjs +28 -28
  161. package/dist/astro/routes/api/schema/index.mjs +6 -6
  162. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +28 -28
  163. package/dist/astro/routes/api/schema/orphans/index.mjs +28 -28
  164. package/dist/astro/routes/api/search/enable.mjs +7 -7
  165. package/dist/astro/routes/api/search/index.mjs +6 -6
  166. package/dist/astro/routes/api/search/rebuild.mjs +7 -7
  167. package/dist/astro/routes/api/search/stats.mjs +6 -6
  168. package/dist/astro/routes/api/search/suggest.mjs +6 -6
  169. package/dist/astro/routes/api/sections/_slug_.mjs +6 -6
  170. package/dist/astro/routes/api/sections/index.mjs +6 -6
  171. package/dist/astro/routes/api/settings/email.mjs +4 -4
  172. package/dist/astro/routes/api/settings.mjs +9 -9
  173. package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
  174. package/dist/astro/routes/api/setup/admin.mjs +3 -3
  175. package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
  176. package/dist/astro/routes/api/setup/dev-bypass.mjs +20 -18
  177. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  178. package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
  179. package/dist/astro/routes/api/setup/index.mjs +18 -18
  180. package/dist/astro/routes/api/setup/status.mjs +3 -3
  181. package/dist/astro/routes/api/snapshot.mjs +3 -3
  182. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +9 -9
  183. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +9 -9
  184. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +9 -9
  185. package/dist/astro/routes/api/taxonomies/index.mjs +9 -9
  186. package/dist/astro/routes/api/themes/preview.mjs +3 -3
  187. package/dist/astro/routes/api/typegen.mjs +5 -5
  188. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
  189. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -6
  190. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -6
  191. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  192. package/dist/astro/routes/api/widget-areas/index.mjs +6 -6
  193. package/dist/astro/routes/api/widget-components.mjs +2 -2
  194. package/dist/astro/routes/robots.txt.mjs +5 -5
  195. package/dist/astro/routes/sitemap-_collection_.xml.mjs +10 -9
  196. package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
  197. package/dist/astro/routes/sitemap.xml.mjs +5 -5
  198. package/dist/astro/types.d.mts +12 -10
  199. package/dist/astro/types.d.mts.map +1 -1
  200. package/dist/auth/providers/github.d.mts +1 -1
  201. package/dist/auth/providers/google.d.mts +1 -1
  202. package/dist/{authorize-CuxZ0i2t.mjs → authorize-C3a_zGsM.mjs} +2 -2
  203. package/dist/{authorize-CuxZ0i2t.mjs.map → authorize-C3a_zGsM.mjs.map} +1 -1
  204. package/dist/{byline-C-YajfV7.mjs → byline-DEKSVPTy.mjs} +6 -6
  205. package/dist/{byline-C-YajfV7.mjs.map → byline-DEKSVPTy.mjs.map} +1 -1
  206. package/dist/{byline-fields-KD3JFRuH.d.mts → byline-fields-CtQ6blXd.d.mts} +32 -32
  207. package/dist/{byline-fields-KD3JFRuH.d.mts.map → byline-fields-CtQ6blXd.d.mts.map} +1 -1
  208. package/dist/{bylines-CPc_q2Vd.mjs → bylines-CiqgTHWF.mjs} +5 -5
  209. package/dist/{bylines-CPc_q2Vd.mjs.map → bylines-CiqgTHWF.mjs.map} +1 -1
  210. package/dist/{bylines-D4KADDAi.mjs → bylines-Ds4-otXl.mjs} +4 -4
  211. package/dist/{bylines-D4KADDAi.mjs.map → bylines-Ds4-otXl.mjs.map} +1 -1
  212. package/dist/{cache-C35VWD5D.mjs → cache-Ciu3e3AF.mjs} +2 -2
  213. package/dist/{cache-C35VWD5D.mjs.map → cache-Ciu3e3AF.mjs.map} +1 -1
  214. package/dist/{chunks-BHgcHh2C.mjs → chunks-CZFiauig.mjs} +2 -2
  215. package/dist/{chunks-BHgcHh2C.mjs.map → chunks-CZFiauig.mjs.map} +1 -1
  216. package/dist/cli/index.mjs +17 -17
  217. package/dist/cli/index.mjs.map +1 -1
  218. package/dist/client/cf-access.d.mts +1 -1
  219. package/dist/client/index.d.mts +1 -1
  220. package/dist/{comment-BDx50EX5.mjs → comment-CH1IrFFs.mjs} +3 -3
  221. package/dist/{comment-BDx50EX5.mjs.map → comment-CH1IrFFs.mjs.map} +1 -1
  222. package/dist/{comment-reaction-Gv_pgfI3.mjs → comment-reaction-kruNY-kP.mjs} +2 -2
  223. package/dist/{comment-reaction-Gv_pgfI3.mjs.map → comment-reaction-kruNY-kP.mjs.map} +1 -1
  224. package/dist/{comments-CXHuQ6jI.mjs → comments-BUJyXbgq.mjs} +3 -3
  225. package/dist/{comments-CXHuQ6jI.mjs.map → comments-BUJyXbgq.mjs.map} +1 -1
  226. package/dist/{content-B43-YrIq.mjs → content-RmfHoWqK.mjs} +4 -4
  227. package/dist/{content-B43-YrIq.mjs.map → content-RmfHoWqK.mjs.map} +1 -1
  228. package/dist/{context-BW3N7wyt.mjs → context-CzUU_WzL.mjs} +7 -7
  229. package/dist/{context-BW3N7wyt.mjs.map → context-CzUU_WzL.mjs.map} +1 -1
  230. package/dist/{dashboard-FIe0S512.mjs → dashboard-CVyGhift.mjs} +4 -4
  231. package/dist/{dashboard-FIe0S512.mjs.map → dashboard-CVyGhift.mjs.map} +1 -1
  232. package/dist/db/index.d.mts +3 -3
  233. package/dist/db/index.mjs +1 -1
  234. package/dist/db/libsql.d.mts +1 -1
  235. package/dist/db/postgres.d.mts +1 -1
  236. package/dist/db/sqlite.d.mts +1 -1
  237. package/dist/{error-DXnK2wF4.mjs → error-BHkLZk-w.mjs} +2 -2
  238. package/dist/{error-DXnK2wF4.mjs.map → error-BHkLZk-w.mjs.map} +1 -1
  239. package/dist/{field-defs-cache-CYYx8xlG.mjs → field-defs-cache-BA-8ZPiT.mjs} +2 -2
  240. package/dist/{field-defs-cache-CYYx8xlG.mjs.map → field-defs-cache-BA-8ZPiT.mjs.map} +1 -1
  241. package/dist/{fts-manager-DW_NR-Fs.mjs → fts-manager-Db9jGavf.mjs} +2 -2
  242. package/dist/{fts-manager-DW_NR-Fs.mjs.map → fts-manager-Db9jGavf.mjs.map} +1 -1
  243. package/dist/{index-Cz6_JBL3.d.mts → index-CDZZjrLT.d.mts} +2430 -2363
  244. package/dist/index-CDZZjrLT.d.mts.map +1 -0
  245. package/dist/{index-qbL02uhs.d.mts → index-DGIjmUXQ.d.mts} +3 -3
  246. package/dist/{index-qbL02uhs.d.mts.map → index-DGIjmUXQ.d.mts.map} +1 -1
  247. package/dist/index.d.mts +16 -15
  248. package/dist/index.mjs +38 -38
  249. package/dist/{load-DMFi4n1p.mjs → load-DpHICVPh.mjs} +2 -2
  250. package/dist/{load-DMFi4n1p.mjs.map → load-DpHICVPh.mjs.map} +1 -1
  251. package/dist/{loader-DrpofRon.mjs → loader-4K37qA-y.mjs} +81 -18
  252. package/dist/loader-4K37qA-y.mjs.map +1 -0
  253. package/dist/{manifest-schema-BOfPXse0.mjs → manifest-schema-kPGX7VS-.mjs} +3 -1
  254. package/dist/manifest-schema-kPGX7VS-.mjs.map +1 -0
  255. package/dist/media/local-runtime.d.mts +11 -10
  256. package/dist/media/local-runtime.d.mts.map +1 -1
  257. package/dist/media/local-runtime.mjs +5 -5
  258. package/dist/{media-BjKxjzk8.mjs → media-C0nRRfs1.mjs} +2 -2
  259. package/dist/{media-BjKxjzk8.mjs.map → media-C0nRRfs1.mjs.map} +1 -1
  260. package/dist/{menus-CxqJ-z-N.mjs → menus-DzRyC1ID.mjs} +50 -68
  261. package/dist/menus-DzRyC1ID.mjs.map +1 -0
  262. package/dist/{menus-8O6xNcYW.mjs → menus-SL2K0xq2.mjs} +3 -3
  263. package/dist/{menus-8O6xNcYW.mjs.map → menus-SL2K0xq2.mjs.map} +1 -1
  264. package/dist/object-cache/memory.d.mts +1 -1
  265. package/dist/{object-cache-SEb2IM4Z.mjs → object-cache-CHbHv83-.mjs} +8 -4
  266. package/dist/object-cache-CHbHv83-.mjs.map +1 -0
  267. package/dist/{options-BvClpP5Q.d.mts → options-41nCWqi9.d.mts} +2 -2
  268. package/dist/{options-BvClpP5Q.d.mts.map → options-41nCWqi9.d.mts.map} +1 -1
  269. package/dist/page/index.d.mts +6 -3
  270. package/dist/page/index.d.mts.map +1 -1
  271. package/dist/page/index.mjs +3 -2
  272. package/dist/page/index.mjs.map +1 -1
  273. package/dist/{parse-DqMWHQ8x.mjs → parse-CqyZj3Hp.mjs} +2 -2
  274. package/dist/{parse-DqMWHQ8x.mjs.map → parse-CqyZj3Hp.mjs.map} +1 -1
  275. package/dist/plugin-types.d.mts +4 -2
  276. package/dist/plugin-types.d.mts.map +1 -1
  277. package/dist/plugin-utils.d.mts +10 -9
  278. package/dist/plugin-utils.d.mts.map +1 -1
  279. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -9
  280. package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
  281. package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
  282. package/dist/{query-34pfm02Q.mjs → query-B0M-9LYR.mjs} +14 -14
  283. package/dist/{query-34pfm02Q.mjs.map → query-B0M-9LYR.mjs.map} +1 -1
  284. package/dist/{rate-limit-CBQK4ygJ.mjs → rate-limit-BwoUdIpC.mjs} +2 -2
  285. package/dist/{rate-limit-CBQK4ygJ.mjs.map → rate-limit-BwoUdIpC.mjs.map} +1 -1
  286. package/dist/{redirect-C1KVGYWe.mjs → redirect-lMOzG04S.mjs} +2 -2
  287. package/dist/{redirect-C1KVGYWe.mjs.map → redirect-lMOzG04S.mjs.map} +1 -1
  288. package/dist/{redirects-DgnypVir.mjs → redirects-9218MmYl.mjs} +3 -3
  289. package/dist/{redirects-DgnypVir.mjs.map → redirects-9218MmYl.mjs.map} +1 -1
  290. package/dist/{registry-3aOAS4My.mjs → registry-DP-iEtdR.mjs} +4 -4
  291. package/dist/{registry-3aOAS4My.mjs.map → registry-DP-iEtdR.mjs.map} +1 -1
  292. package/dist/{runner-CEtq-6Sm.d.mts → runner-BbR3DfrL.d.mts} +2 -2
  293. package/dist/{runner-CEtq-6Sm.d.mts.map → runner-BbR3DfrL.d.mts.map} +1 -1
  294. package/dist/{runner-BsquExlK.mjs → runner-CFngB4WZ.mjs} +296 -191
  295. package/dist/runner-CFngB4WZ.mjs.map +1 -0
  296. package/dist/runtime.d.mts +10 -9
  297. package/dist/runtime.d.mts.map +1 -1
  298. package/dist/runtime.mjs +2 -2
  299. package/dist/scheduled-publish-BMljtY5G.d.mts +11 -0
  300. package/dist/scheduled-publish-BMljtY5G.d.mts.map +1 -0
  301. package/dist/{schema-BGwx8AQY.mjs → schema-BfJ7bIXH.mjs} +5 -5
  302. package/dist/{schema-BGwx8AQY.mjs.map → schema-BfJ7bIXH.mjs.map} +1 -1
  303. package/dist/{search-Gl747zMP.mjs → search-C7t3PH1J.mjs} +6 -4
  304. package/dist/search-C7t3PH1J.mjs.map +1 -0
  305. package/dist/{sections-C-umuuLj.mjs → sections-Dm0CVX23.mjs} +3 -3
  306. package/dist/{sections-C-umuuLj.mjs.map → sections-Dm0CVX23.mjs.map} +1 -1
  307. package/dist/seed/index.d.mts +2 -2
  308. package/dist/seed/index.mjs +15 -15
  309. package/dist/{seo-BNzkiK_W.mjs → seo-Dd3tbY_k.mjs} +3 -3
  310. package/dist/{seo-BNzkiK_W.mjs.map → seo-Dd3tbY_k.mjs.map} +1 -1
  311. package/dist/{service-CQHK_10l.mjs → service-CTrqp_Jy.mjs} +2 -2
  312. package/dist/{service-CQHK_10l.mjs.map → service-CTrqp_Jy.mjs.map} +1 -1
  313. package/dist/{settings-C4tXRYiG.mjs → settings-BX8cWBus.mjs} +4 -4
  314. package/dist/{settings-C4tXRYiG.mjs.map → settings-BX8cWBus.mjs.map} +1 -1
  315. package/dist/{settings-fPSR6dJa.mjs → settings-CZ7ZP9cW.mjs} +3 -3
  316. package/dist/{settings-fPSR6dJa.mjs.map → settings-CZ7ZP9cW.mjs.map} +1 -1
  317. package/dist/storage/local.d.mts +1 -1
  318. package/dist/storage/s3.d.mts +1 -1
  319. package/dist/{taxonomies-DbAtL6m4.mjs → taxonomies-DChKcVxf.mjs} +5 -4
  320. package/dist/taxonomies-DChKcVxf.mjs.map +1 -0
  321. package/dist/{taxonomies-n8w72lLQ.mjs → taxonomies-ckPjz-6d.mjs} +75 -20
  322. package/dist/taxonomies-ckPjz-6d.mjs.map +1 -0
  323. package/dist/{taxonomy-BBUlGug5.mjs → taxonomy-Cru-he6s.mjs} +4 -4
  324. package/dist/{taxonomy-BBUlGug5.mjs.map → taxonomy-Cru-he6s.mjs.map} +1 -1
  325. package/dist/{transport-BZ-03aFT.d.mts → transport-Blrl2k_o.d.mts} +1 -1
  326. package/dist/{transport-BZ-03aFT.d.mts.map → transport-Blrl2k_o.d.mts.map} +1 -1
  327. package/dist/{types-h2Cwbzi2.d.mts → types-BYnDBVwe.d.mts} +24 -5
  328. package/dist/{types-h2Cwbzi2.d.mts.map → types-BYnDBVwe.d.mts.map} +1 -1
  329. package/dist/{types-BG8_XTLj.d.mts → types-BkZ8DUEI.d.mts} +1 -1
  330. package/dist/{types-BG8_XTLj.d.mts.map → types-BkZ8DUEI.d.mts.map} +1 -1
  331. package/dist/{types-DrYMm9s5.d.mts → types-ByChcBgE.d.mts} +1 -1
  332. package/dist/{types-DrYMm9s5.d.mts.map → types-ByChcBgE.d.mts.map} +1 -1
  333. package/dist/{types-BWqGVhFn.d.mts → types-CvuKO5Pn.d.mts} +1 -1
  334. package/dist/{types-BWqGVhFn.d.mts.map → types-CvuKO5Pn.d.mts.map} +1 -1
  335. package/dist/{types-CmI1OyQg.mjs → types-DU4TSFS5.mjs} +2 -2
  336. package/dist/{types-CmI1OyQg.mjs.map → types-DU4TSFS5.mjs.map} +1 -1
  337. package/dist/{types-CUsXGRVj.d.mts → types-Dbqff978.d.mts} +1 -1
  338. package/dist/{types-CUsXGRVj.d.mts.map → types-Dbqff978.d.mts.map} +1 -1
  339. package/dist/{types-fS7-VTdR.d.mts → types-Y09-wtyU.d.mts} +1 -1
  340. package/dist/{types-fS7-VTdR.d.mts.map → types-Y09-wtyU.d.mts.map} +1 -1
  341. package/dist/{types-B-gIxFCE.d.mts → types-xIfVRNLp.d.mts} +38 -1
  342. package/dist/types-xIfVRNLp.d.mts.map +1 -0
  343. package/dist/{user-JAuc8VUf.mjs → user-DgGbKL9s.mjs} +3 -3
  344. package/dist/{user-JAuc8VUf.mjs.map → user-DgGbKL9s.mjs.map} +1 -1
  345. package/dist/{validate-Va_An3qb.d.mts → validate-9ECmtEpJ.d.mts} +10 -5
  346. package/dist/{validate-Va_An3qb.d.mts.map → validate-9ECmtEpJ.d.mts.map} +1 -1
  347. package/dist/{validate-BzXHWnkV.mjs → validate-CRkcYZAR.mjs} +2 -2
  348. package/dist/{validate-BzXHWnkV.mjs.map → validate-CRkcYZAR.mjs.map} +1 -1
  349. package/dist/{validation-B6DKPTtU.mjs → validation-sR26SOwD.mjs} +5 -5
  350. package/dist/{validation-B6DKPTtU.mjs.map → validation-sR26SOwD.mjs.map} +1 -1
  351. package/dist/version-CrqhKGWn.mjs +7 -0
  352. package/dist/{version-DHdCA24u.mjs.map → version-CrqhKGWn.mjs.map} +1 -1
  353. package/dist/{widgets-C2RbFN0G.mjs → widgets-DRgtn7Cl.mjs} +2 -2
  354. package/dist/{widgets-C2RbFN0G.mjs.map → widgets-DRgtn7Cl.mjs.map} +1 -1
  355. package/dist/{zod-generator-xEHODMJf.mjs → zod-generator-C5JaV--i.mjs} +6 -3
  356. package/dist/zod-generator-C5JaV--i.mjs.map +1 -0
  357. package/package.json +8 -8
  358. package/src/api/handlers/content.ts +59 -16
  359. package/src/api/handlers/plugins.ts +77 -10
  360. package/src/api/handlers/taxonomies.ts +5 -1
  361. package/src/astro/integration/virtual-modules.ts +15 -0
  362. package/src/astro/integration/vite-config.ts +19 -1
  363. package/src/astro/middleware/auth.ts +1 -0
  364. package/src/astro/middleware.ts +1 -0
  365. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +6 -1
  366. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +6 -1
  367. package/src/astro/routes/api/admin/plugins/[id]/index.ts +1 -0
  368. package/src/astro/routes/api/admin/plugins/index.ts +1 -0
  369. package/src/astro/routes/api/manifest.ts +20 -1
  370. package/src/astro/routes/api/setup/dev-bypass.ts +7 -2
  371. package/src/astro/routes/sitemap-[collection].xml.ts +5 -4
  372. package/src/astro/types.ts +4 -0
  373. package/src/cli/commands/seed.ts +1 -1
  374. package/src/components/EmDashHead.astro +23 -3
  375. package/src/components/JsonLdScript.astro +9 -0
  376. package/src/components/LiveSearch.astro +40 -4
  377. package/src/components/live-search-routing.ts +30 -0
  378. package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +39 -18
  379. package/src/database/migrations/046_media_usage_index.ts +115 -0
  380. package/src/database/migrations/047_restore_taxonomy_parent_index.ts +29 -0
  381. package/src/database/migrations/048_restore_content_taxonomies_term_index.ts +32 -0
  382. package/src/database/migrations/runner.ts +6 -0
  383. package/src/database/repositories/media-usage.ts +444 -0
  384. package/src/database/types.ts +39 -0
  385. package/src/emdash-runtime.ts +61 -52
  386. package/src/index.ts +3 -0
  387. package/src/loader.ts +110 -31
  388. package/src/media/usage/extractor.ts +302 -0
  389. package/src/media/usage/types.ts +46 -0
  390. package/src/object-cache/index.ts +8 -2
  391. package/src/page/metadata.ts +23 -3
  392. package/src/plugin-types.ts +10 -0
  393. package/src/plugins/define-plugin.ts +14 -5
  394. package/src/plugins/hooks.ts +53 -65
  395. package/src/plugins/index.ts +5 -0
  396. package/src/plugins/manager.ts +22 -0
  397. package/src/plugins/manifest-schema.ts +2 -0
  398. package/src/plugins/types.ts +35 -3
  399. package/src/schema/zod-generator.ts +9 -1
  400. package/src/search/query.ts +3 -0
  401. package/src/search/types.ts +2 -0
  402. package/src/seed/apply.ts +8 -2
  403. package/src/seed/types.ts +6 -1
  404. package/src/taxonomies/index.ts +121 -18
  405. package/dist/api-D2_sbavn.mjs.map +0 -1
  406. package/dist/apply-Bk2mAuLn.mjs.map +0 -1
  407. package/dist/index-Cz6_JBL3.d.mts.map +0 -1
  408. package/dist/loader-DrpofRon.mjs.map +0 -1
  409. package/dist/manifest-schema-BOfPXse0.mjs.map +0 -1
  410. package/dist/menus-CxqJ-z-N.mjs.map +0 -1
  411. package/dist/object-cache-SEb2IM4Z.mjs.map +0 -1
  412. package/dist/runner-BsquExlK.mjs.map +0 -1
  413. package/dist/search-Gl747zMP.mjs.map +0 -1
  414. package/dist/taxonomies-DbAtL6m4.mjs.map +0 -1
  415. package/dist/taxonomies-n8w72lLQ.mjs.map +0 -1
  416. package/dist/types-B-gIxFCE.d.mts.map +0 -1
  417. package/dist/version-DHdCA24u.mjs +0 -7
  418. package/dist/zod-generator-xEHODMJf.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"settings-C4tXRYiG.mjs","names":[],"sources":["../src/settings/index.ts"],"sourcesContent":["/**\n * Site Settings API\n *\n * Functions for getting and setting global site configuration.\n * Settings are stored in the options table with 'site:' prefix.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { after } from \"../after.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport { cachedQuery, invalidateObjectCache } from \"../object-cache/index.js\";\nimport { peekRequestCache, requestCached } from \"../request-cache.js\";\n\n/** Object-cache namespace for site settings. */\nconst SETTINGS_CACHE_NAMESPACE = \"settings\";\nimport type { Storage } from \"../storage/types.js\";\nimport {\n\tcreateSingleFlightCache,\n\ttype SingleFlightCache,\n\tinvalidateSingleFlightCache,\n\tsingleFlightCached,\n} from \"../utils/single-flight-cache.js\";\nimport type { SiteSettings, SiteSettingKey, MediaReference, SeoSettings } from \"./types.js\";\n\n/** Prefix for site settings in the options table */\nconst SETTINGS_PREFIX = \"site:\";\n\n/**\n * Worker-isolate cache for the resolved `site:*` settings.\n *\n * Site settings (title, logo, SEO defaults) change rarely but are read on\n * every public request. Caching across the isolate's lifetime drops the\n * `options WHERE name LIKE 'site:%'` prefix scan from once-per-request to\n * once-per-isolate. Cross-isolate staleness is bounded by isolate lifetime\n * (workerd typically recycles within minutes); acceptable for chrome.\n *\n * Backed by single-flight-cache.ts: concurrent cold reads coalesce onto one\n * query via a reclaimable single-flight lock and the resolved *value* is\n * cached — never a shared in-flight promise, so a cancelled request can't\n * poison the isolate (see that file's header). Stored on globalThis with a\n * Symbol.for key so Vite SSR chunk duplication doesn't produce two\n * independent caches (same pattern as request-context.ts).\n */\nconst SITE_SETTINGS_CACHE_KEY = Symbol.for(\"emdash:site-settings\");\nconst g = globalThis as Record<symbol, unknown>;\nconst settingsCache: SingleFlightCache<Partial<SiteSettings>> =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts)\n\t(g[SITE_SETTINGS_CACHE_KEY] as SingleFlightCache<Partial<SiteSettings>> | undefined) ??\n\t(() => {\n\t\tconst c = createSingleFlightCache<Partial<SiteSettings>>();\n\t\tg[SITE_SETTINGS_CACHE_KEY] = c;\n\t\treturn c;\n\t})();\n\n/**\n * Bump the isolate-wide site-settings cache version, forcing the next\n * `getSiteSettings()` to re-query the database.\n *\n * Called from every `site:*` write path. Other isolates still serve their\n * own cached copy until they expire — staleness bounded by isolate lifetime.\n */\nexport function invalidateSiteSettingsCache(): void {\n\tinvalidateSingleFlightCache(settingsCache);\n\t// Cross-isolate invalidation for the optional distributed object cache.\n\tinvalidateObjectCache(SETTINGS_CACHE_NAMESPACE);\n}\n\n/**\n * Type guard for MediaReference values\n */\nfunction isMediaReference(value: unknown): value is MediaReference {\n\treturn typeof value === \"object\" && value !== null && \"mediaId\" in value;\n}\n\n/**\n * Resolve a media reference to include the full URL plus content metadata.\n *\n * Pulls `mimeType` and intrinsic dimensions from the media row so callers\n * can emit correct head tags (e.g. `<link rel=\"icon\" type=\"image/svg+xml\">`,\n * which Chromium requires when the URL has no `.svg` extension) without\n * a second round-trip to the media table.\n */\nasync function resolveMediaReference(\n\tmediaRef: MediaReference | undefined,\n\tdb: Kysely<Database>,\n\t_storage: Storage | null,\n): Promise<MediaReference | undefined> {\n\tif (!mediaRef?.mediaId) {\n\t\treturn mediaRef;\n\t}\n\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst media = await mediaRepo.findById(mediaRef.mediaId);\n\n\t\tif (media) {\n\t\t\t// Construct URL using the same pattern as API handlers\n\t\t\treturn {\n\t\t\t\t...mediaRef,\n\t\t\t\turl: `/_emdash/api/media/file/${media.storageKey}`,\n\t\t\t\tcontentType: media.mimeType,\n\t\t\t\t...(media.width !== null ? { width: media.width } : {}),\n\t\t\t\t...(media.height !== null ? { height: media.height } : {}),\n\t\t\t};\n\t\t}\n\t} catch {\n\t\t// If media not found or error, return the reference as-is\n\t}\n\n\treturn mediaRef;\n}\n\n/**\n * Get a single site setting by key\n *\n * Returns `undefined` if the setting has not been configured.\n * For media settings (logo, favicon), the URL is resolved automatically.\n *\n * @param key - The setting key (e.g., \"title\", \"logo\", \"social\")\n * @returns The setting value, or undefined if not set\n *\n * @example\n * ```ts\n * import { getSiteSetting } from \"emdash\";\n *\n * const title = await getSiteSetting(\"title\");\n * const logo = await getSiteSetting(\"logo\");\n * console.log(logo?.url); // Resolved URL\n * ```\n */\nexport async function getSiteSetting<K extends SiteSettingKey>(\n\tkey: K,\n): Promise<SiteSettings[K] | undefined> {\n\t// If `getSiteSettings()` has already been called in this request,\n\t// read from that (request-cached) batch rather than firing a second\n\t// options-table query. Common layout: a Base template pulls the\n\t// whole settings object up-front, then `EmDashHead` or a plugin\n\t// asks for one key — no reason the singular call should round-trip\n\t// again.\n\tconst primed = peekRequestCache<Partial<SiteSettings>>(\"siteSettings\");\n\tif (primed) {\n\t\tconst settings = await primed;\n\t\treturn settings[key];\n\t}\n\n\t// Otherwise cache per-key. Templates that pull several settings\n\t// independently still share the in-flight query for each one.\n\treturn requestCached(`siteSetting:${key}`, async () => {\n\t\tconst db = await getDb();\n\t\treturn getSiteSettingWithDb(key, db);\n\t});\n}\n\n/**\n * Get a single site setting by key (with explicit db)\n *\n * @internal Use `getSiteSetting()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingWithDb<K extends SiteSettingKey>(\n\tkey: K,\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<SiteSettings[K] | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<SiteSettings[K]>(`${SETTINGS_PREFIX}${key}`);\n\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\n\t// Resolve media references if needed.\n\t// TS cannot narrow generic K from key equality checks — this is a known limitation.\n\t// We use the non-generic getSiteSettingsWithDb for media resolution instead.\n\tif ((key === \"logo\" || key === \"favicon\") && isMediaReference(value)) {\n\t\tconst resolved = await resolveMediaReference(value, db, storage);\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality; resolved type is correct\n\t\treturn resolved as SiteSettings[K] | undefined;\n\t}\n\n\tif (key === \"seo\" && value && typeof value === \"object\") {\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\tconst seo = value as SeoSettings;\n\t\tif (seo.defaultOgImage) {\n\t\t\tconst resolved = {\n\t\t\t\t...seo,\n\t\t\t\tdefaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage),\n\t\t\t};\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\t\treturn resolved as SiteSettings[K] | undefined;\n\t\t}\n\t}\n\n\treturn value;\n}\n\n/**\n * Get all site settings\n *\n * Returns all configured settings. Unset values are undefined.\n * Media references (logo/favicon) are resolved to include URLs.\n *\n * @example\n * ```ts\n * import { getSiteSettings } from \"emdash\";\n *\n * const settings = await getSiteSettings();\n * console.log(settings.title); // \"My Site\"\n * console.log(settings.logo?.url); // \"/_emdash/api/media/file/abc123\"\n * ```\n */\nexport function getSiteSettings(): Promise<Partial<SiteSettings>> {\n\t// requestCached dedupes within a single request; singleFlightCached\n\t// coalesces across requests and caches the resolved value for the\n\t// global scope's lifetime without ever sharing an awaitable promise. The\n\t// distributed object cache (cachedQuery) sits beneath both, backing cold\n\t// isolates without a database round-trip.\n\treturn requestCached(\"siteSettings\", () =>\n\t\tsingleFlightCached(\n\t\t\tsettingsCache,\n\t\t\t() =>\n\t\t\t\tcachedQuery({\n\t\t\t\t\tnamespace: SETTINGS_CACHE_NAMESPACE,\n\t\t\t\t\tkey: \"all\",\n\t\t\t\t\tload: async () => {\n\t\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\t\treturn getSiteSettingsWithDb(db);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t),\n\t);\n}\n\n/**\n * Get all site settings (with explicit db)\n *\n * @internal Use `getSiteSettings()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingsWithDb(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<Partial<SiteSettings>> {\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(SETTINGS_PREFIX);\n\n\tconst settings: Record<string, unknown> = {};\n\n\t// Convert Map to settings object, removing the prefix\n\tfor (const [key, value] of allOptions) {\n\t\tconst settingKey = key.replace(SETTINGS_PREFIX, \"\");\n\t\tsettings[settingKey] = value;\n\t}\n\n\tconst typedSettings = settings as Partial<SiteSettings>;\n\n\t// Resolve media references\n\tif (typedSettings.logo) {\n\t\ttypedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);\n\t}\n\tif (typedSettings.favicon) {\n\t\ttypedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);\n\t}\n\tif (typedSettings.seo?.defaultOgImage) {\n\t\ttypedSettings.seo = {\n\t\t\t...typedSettings.seo,\n\t\t\tdefaultOgImage: await resolveMediaReference(typedSettings.seo.defaultOgImage, db, storage),\n\t\t};\n\t}\n\n\treturn typedSettings;\n}\n\n/**\n * Set site settings (internal function used by admin API)\n *\n * Merges provided settings with existing ones. Only provided fields are updated.\n * Media references should include just the mediaId; URLs are resolved on read.\n *\n * @param settings - Partial settings object with values to update\n * @param db - Kysely database instance\n * @returns Promise that resolves when settings are saved\n *\n * @internal\n *\n * @example\n * ```ts\n * // Update multiple settings at once\n * await setSiteSettings({\n * title: \"My Site\",\n * tagline: \"Welcome\",\n * logo: { mediaId: \"med_123\", alt: \"Logo\" }\n * }, db);\n * ```\n */\nexport async function setSiteSettings(\n\tsettings: Partial<SiteSettings>,\n\tdb: Kysely<Database>,\n): Promise<void> {\n\tconst options = new OptionsRepository(db);\n\n\t// Convert settings to options format\n\tconst updates: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\tupdates[`${SETTINGS_PREFIX}${key}`] = value;\n\t\t}\n\t}\n\n\ttry {\n\t\tawait options.setMany(updates);\n\t} finally {\n\t\tinvalidateSiteSettingsCache();\n\t}\n}\n\n/**\n * Get a single plugin setting by key.\n *\n * Plugin settings are stored in the options table under\n * `plugin:<pluginId>:settings:<key>`.\n */\nexport async function getPluginSetting<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n): Promise<T | undefined> {\n\tconst db = await getDb();\n\treturn getPluginSettingWithDb<T>(pluginId, key, db);\n}\n\n/**\n * Get a single plugin setting by key (with explicit db).\n *\n * @internal Use `getPluginSetting()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingWithDb<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n\tdb: Kysely<Database>,\n): Promise<T | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<T>(`plugin:${pluginId}:settings:${key}`);\n\treturn value ?? undefined;\n}\n\n/**\n * Get all persisted plugin settings for a plugin.\n *\n * Defaults declared in `admin.settingsSchema` are not materialized\n * automatically; callers should apply their own fallback defaults.\n */\nexport async function getPluginSettings(pluginId: string): Promise<Record<string, unknown>> {\n\tconst db = await getDb();\n\treturn getPluginSettingsWithDb(pluginId, db);\n}\n\n/**\n * Get all persisted plugin settings for a plugin (with explicit db).\n *\n * @internal Use `getPluginSettings()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingsWithDb(\n\tpluginId: string,\n\tdb: Kysely<Database>,\n): Promise<Record<string, unknown>> {\n\tconst prefix = `plugin:${pluginId}:settings:`;\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(prefix);\n\n\tconst settings: Record<string, unknown> = {};\n\tfor (const [key, value] of allOptions) {\n\t\tif (!key.startsWith(prefix)) {\n\t\t\tcontinue;\n\t\t}\n\t\tsettings[key.slice(prefix.length)] = value;\n\t}\n\n\treturn settings;\n}\n"],"mappings":";;;;;;;;;;AAkBA,MAAM,2BAA2B;;AAWjC,MAAM,kBAAkB;;;;;;;;;;;;;;;;;AAkBxB,MAAM,0BAA0B,OAAO,IAAI,uBAAuB;AAClE,MAAM,IAAI;AACV,MAAM,gBAEJ,EAAE,mCACI;CACN,MAAM,IAAI,yBAAgD;AAC1D,GAAE,2BAA2B;AAC7B,QAAO;IACJ;;;;;;;;AASL,SAAgB,8BAAoC;AACnD,6BAA4B,cAAc;AAE1C,uBAAsB,yBAAyB;;;;;AAMhD,SAAS,iBAAiB,OAAyC;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa;;;;;;;;;;AAWpE,eAAe,sBACd,UACA,IACA,UACsC;AACtC,KAAI,CAAC,UAAU,QACd,QAAO;AAGR,KAAI;EAEH,MAAM,QAAQ,MADI,IAAI,gBAAgB,GAAG,CACX,SAAS,SAAS,QAAQ;AAExD,MAAI,MAEH,QAAO;GACN,GAAG;GACH,KAAK,2BAA2B,MAAM;GACtC,aAAa,MAAM;GACnB,GAAI,MAAM,UAAU,OAAO,EAAE,OAAO,MAAM,OAAO,GAAG,EAAE;GACtD,GAAI,MAAM,WAAW,OAAO,EAAE,QAAQ,MAAM,QAAQ,GAAG,EAAE;GACzD;SAEK;AAIR,QAAO;;;;;;;;;;;;;;;;;;;;AAqBR,eAAsB,eACrB,KACuC;CAOvC,MAAM,SAAS,iBAAwC,eAAe;AACtE,KAAI,OAEH,SADiB,MAAM,QACP;AAKjB,QAAO,cAAc,eAAe,OAAO,YAAY;AAEtD,SAAO,qBAAqB,KADjB,MAAM,OAAO,CACY;GACnC;;;;;;;;AASH,eAAsB,qBACrB,KACA,IACA,UAA0B,MACa;CAEvC,MAAM,QAAQ,MADE,IAAI,kBAAkB,GAAG,CACb,IAAqB,GAAG,kBAAkB,MAAM;AAE5E,KAAI,CAAC,MACJ;AAMD,MAAK,QAAQ,UAAU,QAAQ,cAAc,iBAAiB,MAAM,CAGnE,QAFiB,MAAM,sBAAsB,OAAO,IAAI,QAAQ;AAKjE,KAAI,QAAQ,SAAS,SAAS,OAAO,UAAU,UAAU;EAExD,MAAM,MAAM;AACZ,MAAI,IAAI,eAMP,QALiB;GAChB,GAAG;GACH,gBAAgB,MAAM,sBAAsB,IAAI,gBAAgB,IAAI,QAAQ;GAC5E;;AAMH,QAAO;;;;;;;;;;;;;;;;;AAkBR,SAAgB,kBAAkD;AAMjE,QAAO,cAAc,sBACpB,mBACC,qBAEC,YAAY;EACX,WAAW;EACX,KAAK;EACL,MAAM,YAAY;AAEjB,UAAO,sBADI,MAAM,OAAO,CACQ;;EAEjC,CAAC,EACH;EAAE,SAAS,YAAY,YAAY,QAAQ;EAAE,gBAAgB;EAAQ,CACrE,CACD;;;;;;;;AASF,eAAsB,sBACrB,IACA,UAA0B,MACO;CAEjC,MAAM,aAAa,MADH,IAAI,kBAAkB,GAAG,CACR,YAAY,gBAAgB;CAE7D,MAAM,WAAoC,EAAE;AAG5C,MAAK,MAAM,CAAC,KAAK,UAAU,YAAY;EACtC,MAAM,aAAa,IAAI,QAAQ,iBAAiB,GAAG;AACnD,WAAS,cAAc;;CAGxB,MAAM,gBAAgB;AAGtB,KAAI,cAAc,KACjB,eAAc,OAAO,MAAM,sBAAsB,cAAc,MAAM,IAAI,QAAQ;AAElF,KAAI,cAAc,QACjB,eAAc,UAAU,MAAM,sBAAsB,cAAc,SAAS,IAAI,QAAQ;AAExF,KAAI,cAAc,KAAK,eACtB,eAAc,MAAM;EACnB,GAAG,cAAc;EACjB,gBAAgB,MAAM,sBAAsB,cAAc,IAAI,gBAAgB,IAAI,QAAQ;EAC1F;AAGF,QAAO;;;;;;;;;;;;;;;;;;;;;;;;AAyBR,eAAsB,gBACrB,UACA,IACgB;CAChB,MAAM,UAAU,IAAI,kBAAkB,GAAG;CAGzC,MAAM,UAAmC,EAAE;AAC3C,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS,CAClD,KAAI,UAAU,OACb,SAAQ,GAAG,kBAAkB,SAAS;AAIxC,KAAI;AACH,QAAM,QAAQ,QAAQ,QAAQ;WACrB;AACT,+BAA6B;;;;;;;;;AAU/B,eAAsB,iBACrB,UACA,KACyB;AAEzB,QAAO,uBAA0B,UAAU,KADhC,MAAM,OAAO,CAC2B;;;;;;;AAQpD,eAAsB,uBACrB,UACA,KACA,IACyB;AAGzB,QADc,MADE,IAAI,kBAAkB,GAAG,CACb,IAAO,UAAU,SAAS,YAAY,MAAM,IACxD;;;;;;;;AASjB,eAAsB,kBAAkB,UAAoD;AAE3F,QAAO,wBAAwB,UADpB,MAAM,OAAO,CACoB;;;;;;;AAQ7C,eAAsB,wBACrB,UACA,IACmC;CACnC,MAAM,SAAS,UAAU,SAAS;CAElC,MAAM,aAAa,MADH,IAAI,kBAAkB,GAAG,CACR,YAAY,OAAO;CAEpD,MAAM,WAAoC,EAAE;AAC5C,MAAK,MAAM,CAAC,KAAK,UAAU,YAAY;AACtC,MAAI,CAAC,IAAI,WAAW,OAAO,CAC1B;AAED,WAAS,IAAI,MAAM,OAAO,OAAO,IAAI;;AAGtC,QAAO"}
1
+ {"version":3,"file":"settings-BX8cWBus.mjs","names":[],"sources":["../src/settings/index.ts"],"sourcesContent":["/**\n * Site Settings API\n *\n * Functions for getting and setting global site configuration.\n * Settings are stored in the options table with 'site:' prefix.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { after } from \"../after.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getDb } from \"../loader.js\";\nimport { cachedQuery, invalidateObjectCache } from \"../object-cache/index.js\";\nimport { peekRequestCache, requestCached } from \"../request-cache.js\";\n\n/** Object-cache namespace for site settings. */\nconst SETTINGS_CACHE_NAMESPACE = \"settings\";\nimport type { Storage } from \"../storage/types.js\";\nimport {\n\tcreateSingleFlightCache,\n\ttype SingleFlightCache,\n\tinvalidateSingleFlightCache,\n\tsingleFlightCached,\n} from \"../utils/single-flight-cache.js\";\nimport type { SiteSettings, SiteSettingKey, MediaReference, SeoSettings } from \"./types.js\";\n\n/** Prefix for site settings in the options table */\nconst SETTINGS_PREFIX = \"site:\";\n\n/**\n * Worker-isolate cache for the resolved `site:*` settings.\n *\n * Site settings (title, logo, SEO defaults) change rarely but are read on\n * every public request. Caching across the isolate's lifetime drops the\n * `options WHERE name LIKE 'site:%'` prefix scan from once-per-request to\n * once-per-isolate. Cross-isolate staleness is bounded by isolate lifetime\n * (workerd typically recycles within minutes); acceptable for chrome.\n *\n * Backed by single-flight-cache.ts: concurrent cold reads coalesce onto one\n * query via a reclaimable single-flight lock and the resolved *value* is\n * cached — never a shared in-flight promise, so a cancelled request can't\n * poison the isolate (see that file's header). Stored on globalThis with a\n * Symbol.for key so Vite SSR chunk duplication doesn't produce two\n * independent caches (same pattern as request-context.ts).\n */\nconst SITE_SETTINGS_CACHE_KEY = Symbol.for(\"emdash:site-settings\");\nconst g = globalThis as Record<symbol, unknown>;\nconst settingsCache: SingleFlightCache<Partial<SiteSettings>> =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see request-context.ts)\n\t(g[SITE_SETTINGS_CACHE_KEY] as SingleFlightCache<Partial<SiteSettings>> | undefined) ??\n\t(() => {\n\t\tconst c = createSingleFlightCache<Partial<SiteSettings>>();\n\t\tg[SITE_SETTINGS_CACHE_KEY] = c;\n\t\treturn c;\n\t})();\n\n/**\n * Bump the isolate-wide site-settings cache version, forcing the next\n * `getSiteSettings()` to re-query the database.\n *\n * Called from every `site:*` write path. Other isolates still serve their\n * own cached copy until they expire — staleness bounded by isolate lifetime.\n */\nexport function invalidateSiteSettingsCache(): void {\n\tinvalidateSingleFlightCache(settingsCache);\n\t// Cross-isolate invalidation for the optional distributed object cache.\n\tinvalidateObjectCache(SETTINGS_CACHE_NAMESPACE);\n}\n\n/**\n * Type guard for MediaReference values\n */\nfunction isMediaReference(value: unknown): value is MediaReference {\n\treturn typeof value === \"object\" && value !== null && \"mediaId\" in value;\n}\n\n/**\n * Resolve a media reference to include the full URL plus content metadata.\n *\n * Pulls `mimeType` and intrinsic dimensions from the media row so callers\n * can emit correct head tags (e.g. `<link rel=\"icon\" type=\"image/svg+xml\">`,\n * which Chromium requires when the URL has no `.svg` extension) without\n * a second round-trip to the media table.\n */\nasync function resolveMediaReference(\n\tmediaRef: MediaReference | undefined,\n\tdb: Kysely<Database>,\n\t_storage: Storage | null,\n): Promise<MediaReference | undefined> {\n\tif (!mediaRef?.mediaId) {\n\t\treturn mediaRef;\n\t}\n\n\ttry {\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst media = await mediaRepo.findById(mediaRef.mediaId);\n\n\t\tif (media) {\n\t\t\t// Construct URL using the same pattern as API handlers\n\t\t\treturn {\n\t\t\t\t...mediaRef,\n\t\t\t\turl: `/_emdash/api/media/file/${media.storageKey}`,\n\t\t\t\tcontentType: media.mimeType,\n\t\t\t\t...(media.width !== null ? { width: media.width } : {}),\n\t\t\t\t...(media.height !== null ? { height: media.height } : {}),\n\t\t\t};\n\t\t}\n\t} catch {\n\t\t// If media not found or error, return the reference as-is\n\t}\n\n\treturn mediaRef;\n}\n\n/**\n * Get a single site setting by key\n *\n * Returns `undefined` if the setting has not been configured.\n * For media settings (logo, favicon), the URL is resolved automatically.\n *\n * @param key - The setting key (e.g., \"title\", \"logo\", \"social\")\n * @returns The setting value, or undefined if not set\n *\n * @example\n * ```ts\n * import { getSiteSetting } from \"emdash\";\n *\n * const title = await getSiteSetting(\"title\");\n * const logo = await getSiteSetting(\"logo\");\n * console.log(logo?.url); // Resolved URL\n * ```\n */\nexport async function getSiteSetting<K extends SiteSettingKey>(\n\tkey: K,\n): Promise<SiteSettings[K] | undefined> {\n\t// If `getSiteSettings()` has already been called in this request,\n\t// read from that (request-cached) batch rather than firing a second\n\t// options-table query. Common layout: a Base template pulls the\n\t// whole settings object up-front, then `EmDashHead` or a plugin\n\t// asks for one key — no reason the singular call should round-trip\n\t// again.\n\tconst primed = peekRequestCache<Partial<SiteSettings>>(\"siteSettings\");\n\tif (primed) {\n\t\tconst settings = await primed;\n\t\treturn settings[key];\n\t}\n\n\t// Otherwise cache per-key. Templates that pull several settings\n\t// independently still share the in-flight query for each one.\n\treturn requestCached(`siteSetting:${key}`, async () => {\n\t\tconst db = await getDb();\n\t\treturn getSiteSettingWithDb(key, db);\n\t});\n}\n\n/**\n * Get a single site setting by key (with explicit db)\n *\n * @internal Use `getSiteSetting()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingWithDb<K extends SiteSettingKey>(\n\tkey: K,\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<SiteSettings[K] | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<SiteSettings[K]>(`${SETTINGS_PREFIX}${key}`);\n\n\tif (!value) {\n\t\treturn undefined;\n\t}\n\n\t// Resolve media references if needed.\n\t// TS cannot narrow generic K from key equality checks — this is a known limitation.\n\t// We use the non-generic getSiteSettingsWithDb for media resolution instead.\n\tif ((key === \"logo\" || key === \"favicon\") && isMediaReference(value)) {\n\t\tconst resolved = await resolveMediaReference(value, db, storage);\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality; resolved type is correct\n\t\treturn resolved as SiteSettings[K] | undefined;\n\t}\n\n\tif (key === \"seo\" && value && typeof value === \"object\") {\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\tconst seo = value as SeoSettings;\n\t\tif (seo.defaultOgImage) {\n\t\t\tconst resolved = {\n\t\t\t\t...seo,\n\t\t\t\tdefaultOgImage: await resolveMediaReference(seo.defaultOgImage, db, storage),\n\t\t\t};\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- TS can't narrow generic K from key equality\n\t\t\treturn resolved as SiteSettings[K] | undefined;\n\t\t}\n\t}\n\n\treturn value;\n}\n\n/**\n * Get all site settings\n *\n * Returns all configured settings. Unset values are undefined.\n * Media references (logo/favicon) are resolved to include URLs.\n *\n * @example\n * ```ts\n * import { getSiteSettings } from \"emdash\";\n *\n * const settings = await getSiteSettings();\n * console.log(settings.title); // \"My Site\"\n * console.log(settings.logo?.url); // \"/_emdash/api/media/file/abc123\"\n * ```\n */\nexport function getSiteSettings(): Promise<Partial<SiteSettings>> {\n\t// requestCached dedupes within a single request; singleFlightCached\n\t// coalesces across requests and caches the resolved value for the\n\t// global scope's lifetime without ever sharing an awaitable promise. The\n\t// distributed object cache (cachedQuery) sits beneath both, backing cold\n\t// isolates without a database round-trip.\n\treturn requestCached(\"siteSettings\", () =>\n\t\tsingleFlightCached(\n\t\t\tsettingsCache,\n\t\t\t() =>\n\t\t\t\tcachedQuery({\n\t\t\t\t\tnamespace: SETTINGS_CACHE_NAMESPACE,\n\t\t\t\t\tkey: \"all\",\n\t\t\t\t\tload: async () => {\n\t\t\t\t\t\tconst db = await getDb();\n\t\t\t\t\t\treturn getSiteSettingsWithDb(db);\n\t\t\t\t\t},\n\t\t\t\t}),\n\t\t\t{ anchor: (promise) => after(() => promise), ownerTimeoutMs: 30_000 },\n\t\t),\n\t);\n}\n\n/**\n * Get all site settings (with explicit db)\n *\n * @internal Use `getSiteSettings()` in templates. This variant is for admin routes\n * that already have a database handle.\n */\nexport async function getSiteSettingsWithDb(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null = null,\n): Promise<Partial<SiteSettings>> {\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(SETTINGS_PREFIX);\n\n\tconst settings: Record<string, unknown> = {};\n\n\t// Convert Map to settings object, removing the prefix\n\tfor (const [key, value] of allOptions) {\n\t\tconst settingKey = key.replace(SETTINGS_PREFIX, \"\");\n\t\tsettings[settingKey] = value;\n\t}\n\n\tconst typedSettings = settings as Partial<SiteSettings>;\n\n\t// Resolve media references\n\tif (typedSettings.logo) {\n\t\ttypedSettings.logo = await resolveMediaReference(typedSettings.logo, db, storage);\n\t}\n\tif (typedSettings.favicon) {\n\t\ttypedSettings.favicon = await resolveMediaReference(typedSettings.favicon, db, storage);\n\t}\n\tif (typedSettings.seo?.defaultOgImage) {\n\t\ttypedSettings.seo = {\n\t\t\t...typedSettings.seo,\n\t\t\tdefaultOgImage: await resolveMediaReference(typedSettings.seo.defaultOgImage, db, storage),\n\t\t};\n\t}\n\n\treturn typedSettings;\n}\n\n/**\n * Set site settings (internal function used by admin API)\n *\n * Merges provided settings with existing ones. Only provided fields are updated.\n * Media references should include just the mediaId; URLs are resolved on read.\n *\n * @param settings - Partial settings object with values to update\n * @param db - Kysely database instance\n * @returns Promise that resolves when settings are saved\n *\n * @internal\n *\n * @example\n * ```ts\n * // Update multiple settings at once\n * await setSiteSettings({\n * title: \"My Site\",\n * tagline: \"Welcome\",\n * logo: { mediaId: \"med_123\", alt: \"Logo\" }\n * }, db);\n * ```\n */\nexport async function setSiteSettings(\n\tsettings: Partial<SiteSettings>,\n\tdb: Kysely<Database>,\n): Promise<void> {\n\tconst options = new OptionsRepository(db);\n\n\t// Convert settings to options format\n\tconst updates: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(settings)) {\n\t\tif (value !== undefined) {\n\t\t\tupdates[`${SETTINGS_PREFIX}${key}`] = value;\n\t\t}\n\t}\n\n\ttry {\n\t\tawait options.setMany(updates);\n\t} finally {\n\t\tinvalidateSiteSettingsCache();\n\t}\n}\n\n/**\n * Get a single plugin setting by key.\n *\n * Plugin settings are stored in the options table under\n * `plugin:<pluginId>:settings:<key>`.\n */\nexport async function getPluginSetting<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n): Promise<T | undefined> {\n\tconst db = await getDb();\n\treturn getPluginSettingWithDb<T>(pluginId, key, db);\n}\n\n/**\n * Get a single plugin setting by key (with explicit db).\n *\n * @internal Use `getPluginSetting()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingWithDb<T = unknown>(\n\tpluginId: string,\n\tkey: string,\n\tdb: Kysely<Database>,\n): Promise<T | undefined> {\n\tconst options = new OptionsRepository(db);\n\tconst value = await options.get<T>(`plugin:${pluginId}:settings:${key}`);\n\treturn value ?? undefined;\n}\n\n/**\n * Get all persisted plugin settings for a plugin.\n *\n * Defaults declared in `admin.settingsSchema` are not materialized\n * automatically; callers should apply their own fallback defaults.\n */\nexport async function getPluginSettings(pluginId: string): Promise<Record<string, unknown>> {\n\tconst db = await getDb();\n\treturn getPluginSettingsWithDb(pluginId, db);\n}\n\n/**\n * Get all persisted plugin settings for a plugin (with explicit db).\n *\n * @internal Use `getPluginSettings()` in templates and plugin rendering code.\n */\nexport async function getPluginSettingsWithDb(\n\tpluginId: string,\n\tdb: Kysely<Database>,\n): Promise<Record<string, unknown>> {\n\tconst prefix = `plugin:${pluginId}:settings:`;\n\tconst options = new OptionsRepository(db);\n\tconst allOptions = await options.getByPrefix(prefix);\n\n\tconst settings: Record<string, unknown> = {};\n\tfor (const [key, value] of allOptions) {\n\t\tif (!key.startsWith(prefix)) {\n\t\t\tcontinue;\n\t\t}\n\t\tsettings[key.slice(prefix.length)] = value;\n\t}\n\n\treturn settings;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { a as __exportAll } from "./runner-BsquExlK.mjs";
2
- import { a as getSiteSettingsWithDb, s as setSiteSettings } from "./settings-C4tXRYiG.mjs";
1
+ import { a as __exportAll } from "./runner-CFngB4WZ.mjs";
2
+ import { a as getSiteSettingsWithDb, s as setSiteSettings } from "./settings-BX8cWBus.mjs";
3
3
 
4
4
  //#region src/api/handlers/settings.ts
5
5
  var settings_exports = /* @__PURE__ */ __exportAll({
@@ -48,4 +48,4 @@ async function handleSettingsUpdate(db, storage, input) {
48
48
 
49
49
  //#endregion
50
50
  export { handleSettingsUpdate as n, settings_exports as r, handleSettingsGet as t };
51
- //# sourceMappingURL=settings-fPSR6dJa.mjs.map
51
+ //# sourceMappingURL=settings-CZ7ZP9cW.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"settings-fPSR6dJa.mjs","names":[],"sources":["../src/api/handlers/settings.ts"],"sourcesContent":["/**\n * Settings handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { getSiteSettingsWithDb, setSiteSettings } from \"../../settings/index.js\";\nimport type { SiteSettings } from \"../../settings/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/**\n * Get all site settings\n */\nexport async function handleSettingsGet(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tconst settings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: settings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_READ_ERROR\", message: \"Failed to get settings\" },\n\t\t};\n\t}\n}\n\n/**\n * Update site settings\n */\nexport async function handleSettingsUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tinput: Partial<SiteSettings>,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tawait setSiteSettings(input, db);\n\t\tconst updatedSettings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: updatedSettings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_UPDATE_ERROR\", message: \"Failed to update settings\" },\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;AAeA,eAAsB,kBACrB,IACA,SAC4C;AAC5C,KAAI;AAEH,SAAO;GAAE,SAAS;GAAM,MADP,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACjC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAuB,SAAS;IAA0B;GACzE;;;;;;AAOH,eAAsB,qBACrB,IACA,SACA,OAC4C;AAC5C,KAAI;AACH,QAAM,gBAAgB,OAAO,GAAG;AAEhC,SAAO;GAAE,SAAS;GAAM,MADA,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACxC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAyB,SAAS;IAA6B;GAC9E"}
1
+ {"version":3,"file":"settings-CZ7ZP9cW.mjs","names":[],"sources":["../src/api/handlers/settings.ts"],"sourcesContent":["/**\n * Settings handlers\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { getSiteSettingsWithDb, setSiteSettings } from \"../../settings/index.js\";\nimport type { SiteSettings } from \"../../settings/types.js\";\nimport type { Storage } from \"../../storage/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n/**\n * Get all site settings\n */\nexport async function handleSettingsGet(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tconst settings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: settings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_READ_ERROR\", message: \"Failed to get settings\" },\n\t\t};\n\t}\n}\n\n/**\n * Update site settings\n */\nexport async function handleSettingsUpdate(\n\tdb: Kysely<Database>,\n\tstorage: Storage | null,\n\tinput: Partial<SiteSettings>,\n): Promise<ApiResult<Partial<SiteSettings>>> {\n\ttry {\n\t\tawait setSiteSettings(input, db);\n\t\tconst updatedSettings = await getSiteSettingsWithDb(db, storage);\n\t\treturn { success: true, data: updatedSettings };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"SETTINGS_UPDATE_ERROR\", message: \"Failed to update settings\" },\n\t\t};\n\t}\n}\n"],"mappings":";;;;;;;;;;;AAeA,eAAsB,kBACrB,IACA,SAC4C;AAC5C,KAAI;AAEH,SAAO;GAAE,SAAS;GAAM,MADP,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACjC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAuB,SAAS;IAA0B;GACzE;;;;;;AAOH,eAAsB,qBACrB,IACA,SACA,OAC4C;AAC5C,KAAI;AACH,QAAM,gBAAgB,OAAO,GAAG;AAEhC,SAAO;GAAE,SAAS;GAAM,MADA,MAAM,sBAAsB,IAAI,QAAQ;GACjB;SACxC;AACP,SAAO;GACN,SAAS;GACT,OAAO;IAAE,MAAM;IAAyB,SAAS;IAA6B;GAC9E"}
@@ -1,4 +1,4 @@
1
- import { a as ListOptions, d as Storage, l as SignedUploadOptions, n as DownloadResult, o as ListResult, p as UploadResult, s as LocalStorageConfig, u as SignedUploadUrl } from "../types-DrYMm9s5.mjs";
1
+ import { a as ListOptions, d as Storage, l as SignedUploadOptions, n as DownloadResult, o as ListResult, p as UploadResult, s as LocalStorageConfig, u as SignedUploadUrl } from "../types-ByChcBgE.mjs";
2
2
 
3
3
  //#region src/storage/local.d.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { a as ListOptions, c as S3StorageConfig, d as Storage, l as SignedUploadOptions, n as DownloadResult, o as ListResult, p as UploadResult, u as SignedUploadUrl } from "../types-DrYMm9s5.mjs";
1
+ import { a as ListOptions, c as S3StorageConfig, d as Storage, l as SignedUploadOptions, n as DownloadResult, o as ListResult, p as UploadResult, u as SignedUploadUrl } from "../types-ByChcBgE.mjs";
2
2
 
3
3
  //#region src/storage/s3.d.ts
4
4
  /**
@@ -1,6 +1,6 @@
1
- import { a as __exportAll } from "./runner-BsquExlK.mjs";
2
- import { t as TaxonomyRepository } from "./taxonomy-BBUlGug5.mjs";
3
- import { l as invalidateTermCache } from "./taxonomies-n8w72lLQ.mjs";
1
+ import { a as __exportAll } from "./runner-CFngB4WZ.mjs";
2
+ import { t as TaxonomyRepository } from "./taxonomy-Cru-he6s.mjs";
3
+ import { l as invalidateTaxonomyDefsCache, u as invalidateTermCache } from "./taxonomies-ckPjz-6d.mjs";
4
4
  import { ulid } from "ulidx";
5
5
 
6
6
  //#region src/api/handlers/taxonomies.ts
@@ -147,6 +147,7 @@ async function handleTaxonomyCreate(db, input) {
147
147
  ...input.locale !== void 0 ? { locale: input.locale } : {},
148
148
  translation_group: translationGroup ?? id
149
149
  }).execute();
150
+ invalidateTaxonomyDefsCache();
150
151
  return {
151
152
  success: true,
152
153
  data: { taxonomy: rowToDef(await db.selectFrom("_emdash_taxonomy_defs").selectAll().where("id", "=", id).executeTakeFirstOrThrow()) }
@@ -512,4 +513,4 @@ async function handleTermDelete(db, taxonomyName, termSlug, options = {}) {
512
513
 
513
514
  //#endregion
514
515
  export { handleTermGet as a, handleTermUpdate as c, handleTermDelete as i, taxonomies_exports as l, handleTaxonomyList as n, handleTermList as o, handleTermCreate as r, handleTermTranslations as s, handleTaxonomyCreate as t };
515
- //# sourceMappingURL=taxonomies-DbAtL6m4.mjs.map
516
+ //# sourceMappingURL=taxonomies-DChKcVxf.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"taxonomies-DChKcVxf.mjs","names":[],"sources":["../src/api/handlers/taxonomies.ts"],"sourcesContent":["/**\n * Taxonomy and term CRUD handlers.\n *\n * i18n: terms and defs are per-locale. `(name, slug, locale)` is unique for\n * terms; `(name, locale)` for defs. Translations of the same term/def share a\n * `translation_group`. The content_taxonomies pivot stores translation_groups\n * so assignments span every locale of a post.\n */\n\nimport type { Kysely, Selectable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { TaxonomyRepository } from \"../../database/repositories/taxonomy.js\";\nimport type { Database, TaxonomyDefTable } from \"../../database/types.js\";\nimport { invalidateTaxonomyDefsCache, invalidateTermCache } from \"../../taxonomies/index.js\";\nimport type { ApiResult } from \"../types.js\";\n\nconst NAME_PATTERN = /^[a-z][a-z0-9_]*$/;\n\n// ---------------------------------------------------------------------------\n// Response types\n// ---------------------------------------------------------------------------\n\nexport interface TaxonomyDef {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\thierarchical: boolean;\n\tcollections: string[];\n\tlocale: string;\n\ttranslationGroup: string | null;\n}\n\nexport interface TaxonomyListResponse {\n\ttaxonomies: TaxonomyDef[];\n}\n\nexport interface TermData {\n\tid: string;\n\tname: string;\n\tslug: string;\n\tlabel: string;\n\tparentId: string | null;\n\tdescription?: string;\n\tlocale: string;\n\ttranslationGroup: string | null;\n}\n\nexport interface TermWithCount extends TermData {\n\tcount: number;\n\tchildren: TermWithCount[];\n}\n\nexport interface TermListResponse {\n\tterms: TermWithCount[];\n}\n\nexport interface TermResponse {\n\tterm: TermData;\n}\n\nexport interface TermGetResponse {\n\tterm: TermData & {\n\t\tcount: number;\n\t\tchildren: Array<{ id: string; slug: string; label: string }>;\n\t};\n}\n\nexport interface TermTranslationsResponse {\n\ttranslationGroup: string | null;\n\ttranslations: Array<{\n\t\tid: string;\n\t\tslug: string;\n\t\tlabel: string;\n\t\tlocale: string;\n\t}>;\n}\n\nexport interface TaxonomyDefTranslationsResponse {\n\ttranslationGroup: string | null;\n\ttranslations: Array<{\n\t\tid: string;\n\t\tname: string;\n\t\tlabel: string;\n\t\tlocale: string;\n\t}>;\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Build tree structure from flat terms\n */\nfunction buildTree(flatTerms: TermWithCount[]): TermWithCount[] {\n\t// `parentId` holds the parent's translation_group, so resolve links by group.\n\t// Key by (locale, group): a child's parent lives in the same locale, and an\n\t// unfiltered list mixes locales whose translated siblings share a group —\n\t// keying by group alone would collide and misattach children across locales.\n\tconst byLocaleGroup = new Map<string, TermWithCount>();\n\tconst roots: TermWithCount[] = [];\n\tfor (const term of flatTerms) {\n\t\tbyLocaleGroup.set(`${term.locale}::${term.translationGroup ?? term.id}`, term);\n\t}\n\tfor (const term of flatTerms) {\n\t\tconst parent = term.parentId\n\t\t\t? byLocaleGroup.get(`${term.locale}::${term.parentId}`)\n\t\t\t: undefined;\n\t\tif (parent) {\n\t\t\tparent.children.push(term);\n\t\t} else {\n\t\t\troots.push(term);\n\t\t}\n\t}\n\treturn roots;\n}\n\n/**\n * Look up a taxonomy definition by name (optionally scoped to a locale).\n * Returns the lowest-locale match when no locale is provided.\n */\nasync function requireTaxonomyDef(\n\tdb: Kysely<Database>,\n\tname: string,\n\tlocale?: string,\n): Promise<\n\t| { success: true; def: Selectable<TaxonomyDefTable> }\n\t| { success: false; error: { code: string; message: string } }\n> {\n\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll().where(\"name\", \"=\", name);\n\tif (locale !== undefined) query = query.where(\"locale\", \"=\", locale);\n\tconst def = await query.orderBy(\"locale\", \"asc\").executeTakeFirst();\n\tif (!def) {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"NOT_FOUND\", message: `Taxonomy '${name}' not found` },\n\t\t};\n\t}\n\treturn { success: true, def };\n}\n\nfunction rowToDef(row: Selectable<TaxonomyDefTable>): TaxonomyDef {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlabel: row.label,\n\t\tlabelSingular: row.label_singular ?? undefined,\n\t\thierarchical: row.hierarchical === 1,\n\t\tcollections: row.collections ? JSON.parse(row.collections) : [],\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n// ---------------------------------------------------------------------------\n// Taxonomy definition handlers\n// ---------------------------------------------------------------------------\n\n/**\n * List all taxonomy definitions\n */\nexport async function handleTaxonomyList(\n\tdb: Kysely<Database>,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<TaxonomyListResponse>> {\n\ttry {\n\t\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll();\n\t\tif (options.locale !== undefined) query = query.where(\"locale\", \"=\", options.locale);\n\t\tconst [rows, collectionRows] = await Promise.all([\n\t\t\tquery.execute(),\n\t\t\tdb.selectFrom(\"_emdash_collections\").select(\"slug\").execute(),\n\t\t]);\n\n\t\t// Filter orphan collection references on read so the response stays\n\t\t// consistent with `schema_list_collections`. Storage is untouched —\n\t\t// re-creating the collection re-links automatically.\n\t\tconst realCollections = new Set(collectionRows.map((r) => r.slug));\n\n\t\tconst taxonomies: TaxonomyDef[] = rows.map((row) => {\n\t\t\tconst def = rowToDef(row);\n\t\t\treturn { ...def, collections: def.collections.filter((slug) => realCollections.has(slug)) };\n\t\t});\n\n\t\treturn { success: true, data: { taxonomies } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TAXONOMY_LIST_ERROR\", message: \"Failed to list taxonomies\" },\n\t\t};\n\t}\n}\n\n/**\n * Create a new taxonomy definition\n */\nexport async function handleTaxonomyCreate(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tname: string;\n\t\tlabel: string;\n\t\tlabelSingular?: string;\n\t\thierarchical?: boolean;\n\t\tcollections?: string[];\n\t\tlocale?: string;\n\t\ttranslationOf?: string;\n\t},\n): Promise<ApiResult<{ taxonomy: TaxonomyDef }>> {\n\ttry {\n\t\tif (!NAME_PATTERN.test(input.name)) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage:\n\t\t\t\t\t\t\"Taxonomy name must start with a letter and contain only lowercase letters, numbers, and underscores\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst collections = [...new Set(input.collections ?? [])];\n\t\tif (collections.length > 0) {\n\t\t\tconst existingCollections = await db\n\t\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t\t.select(\"slug\")\n\t\t\t\t.where(\"slug\", \"in\", collections)\n\t\t\t\t.execute();\n\t\t\tconst existingSlugs = new Set(existingCollections.map((c) => c.slug));\n\t\t\tconst invalid = collections.filter((c) => !existingSlugs.has(c));\n\t\t\tif (invalid.length > 0) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\t\tmessage: `Unknown collection(s): ${invalid.join(\", \")}`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tlet translationGroup: string | null = null;\n\t\tif (input.translationOf) {\n\t\t\tconst source = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", input.translationOf)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (!source) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Source taxonomy for translation not found\" },\n\t\t\t\t};\n\t\t\t}\n\t\t\ttranslationGroup = source.translation_group ?? source.id;\n\t\t}\n\n\t\t// Duplicate guard scoped to locale (so the same name can exist in ES\n\t\t// and EN).\n\t\tif (input.locale !== undefined) {\n\t\t\tconst existing = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.select(\"id\")\n\t\t\t\t.where(\"name\", \"=\", input.name)\n\t\t\t\t.where(\"locale\", \"=\", input.locale)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (existing) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\t\tmessage: `Taxonomy '${input.name}' already exists in locale '${input.locale}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\tconst id = ulid();\n\t\tawait db\n\t\t\t.insertInto(\"_emdash_taxonomy_defs\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tname: input.name,\n\t\t\t\tlabel: input.label,\n\t\t\t\tlabel_singular: input.labelSingular ?? null,\n\t\t\t\thierarchical: input.hierarchical ? 1 : 0,\n\t\t\t\tcollections: JSON.stringify(collections),\n\t\t\t\t...(input.locale !== undefined ? { locale: input.locale } : {}),\n\t\t\t\ttranslation_group: translationGroup ?? id,\n\t\t\t})\n\t\t\t.execute();\n\n\t\t// A new def changes which taxonomies exist — drop the isolate-wide\n\t\t// defs/names caches so this isolate reflects it immediately.\n\t\tinvalidateTaxonomyDefsCache();\n\n\t\tconst row = await db\n\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirstOrThrow();\n\t\treturn { success: true, data: { taxonomy: rowToDef(row) } };\n\t} catch (error) {\n\t\tif (error instanceof Error && error.message.includes(\"UNIQUE constraint failed\")) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"CONFLICT\", message: `Taxonomy '${input.name}' already exists` },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TAXONOMY_CREATE_ERROR\", message: \"Failed to create taxonomy\" },\n\t\t};\n\t}\n}\n\n/**\n * List every locale translation of a taxonomy def (by id or translation_group).\n */\nexport async function handleTaxonomyDefTranslations(\n\tdb: Kysely<Database>,\n\tidOrGroup: string,\n): Promise<ApiResult<TaxonomyDefTranslationsResponse>> {\n\ttry {\n\t\tconst anchor = await db\n\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t.selectAll()\n\t\t\t.where((eb) => eb.or([eb(\"id\", \"=\", idOrGroup), eb(\"translation_group\", \"=\", idOrGroup)]))\n\t\t\t.executeTakeFirst();\n\t\tif (!anchor) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Taxonomy not found\" },\n\t\t\t};\n\t\t}\n\t\tconst group = anchor.translation_group ?? anchor.id;\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t.selectAll()\n\t\t\t.where(\"translation_group\", \"=\", group)\n\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t.execute();\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\ttranslationGroup: group,\n\t\t\t\ttranslations: rows.map((r) => ({\n\t\t\t\t\tid: r.id,\n\t\t\t\t\tname: r.name,\n\t\t\t\t\tlabel: r.label,\n\t\t\t\t\tlocale: r.locale,\n\t\t\t\t})),\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"TAXONOMY_TRANSLATIONS_ERROR\",\n\t\t\t\tmessage: \"Failed to list taxonomy translations\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Term handlers\n// ---------------------------------------------------------------------------\n\n/**\n * List all terms for a taxonomy (returns tree for hierarchical taxonomies)\n */\nexport async function handleTermList(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<TermListResponse>> {\n\ttry {\n\t\t// Definitions are per-locale but terms aren't bound to the def's locale —\n\t\t// just ensure the taxonomy exists somewhere.\n\t\tconst lookup = await requireTaxonomyDef(db, taxonomyName);\n\t\tif (!lookup.success) return lookup;\n\n\t\tconst repo = new TaxonomyRepository(db);\n\t\tconst terms = await repo.findByName(taxonomyName, { locale: options.locale });\n\n\t\t// Batch count entries per term in a single query (replaces N+1 pattern).\n\t\t// content_taxonomies.taxonomy_id stores the translation_group, so we\n\t\t// look up by group and map back to each term's id.\n\t\tconst groups = terms.map((t) => t.translationGroup ?? t.id);\n\t\tconst countsByGroup = await repo.countEntriesForTerms(groups);\n\n\t\tconst termData: TermWithCount[] = terms.map((term) => ({\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tparentId: term.parentId,\n\t\t\tdescription: typeof term.data?.description === \"string\" ? term.data.description : undefined,\n\t\t\tchildren: [],\n\t\t\tcount: countsByGroup.get(term.translationGroup ?? term.id) ?? 0,\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translationGroup,\n\t\t}));\n\n\t\tconst isHierarchical = lookup.def.hierarchical === 1;\n\t\tconst result = isHierarchical ? buildTree(termData) : termData;\n\t\treturn { success: true, data: { terms: result } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TERM_LIST_ERROR\", message: \"Failed to list terms\" },\n\t\t};\n\t}\n}\n\n/**\n * Validate a parent term reference for create/update.\n *\n * Returns `null` on success or a structured error message that callers\n * wrap in their own ApiResult.\n *\n * - `parentId === undefined` -> no-op (no parent change requested).\n * - `parentId === null` -> caller intends to detach; no-op here.\n * - parent must exist (FK exists -> term row not soft-deleted).\n * - parent must live in the same taxonomy.\n * - reject a parent in the term's own translation_group (self-parent),\n * including the create path: a translation inherits its source's group, so\n * `selfGroup` carries that prospective group when `termId` is absent.\n * - on update, walk up the parent chain to detect cycles.\n */\nasync function validateParentTerm(\n\trepo: TaxonomyRepository,\n\ttaxonomyName: string,\n\ttermId: string | undefined,\n\tparentId: string | null | undefined,\n\tselfGroup?: string | null,\n): Promise<{ code: \"VALIDATION_ERROR\"; message: string } | null> {\n\tif (parentId === undefined || parentId === null) return null;\n\n\tconst parent = await repo.findById(parentId);\n\tif (!parent) {\n\t\treturn {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: `Parent term '${parentId}' not found`,\n\t\t};\n\t}\n\tif (parent.name !== taxonomyName) {\n\t\treturn {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: `Parent term '${parentId}' belongs to taxonomy '${parent.name}', not '${taxonomyName}'`,\n\t\t};\n\t}\n\n\t// Parentage keys off translation_group (what `parent_id` persists), so the\n\t// self-parent and cycle checks compare groups, not row ids — picking a\n\t// sibling translation of the term as its parent is still self-parenting.\n\tconst parentGroup = parent.translationGroup ?? parent.id;\n\tlet termGroup: string | null = selfGroup ?? null;\n\tif (termId !== undefined) {\n\t\tconst term = await repo.findById(termId);\n\t\ttermGroup = term ? (term.translationGroup ?? term.id) : termId;\n\t}\n\tif (termGroup !== null && parentGroup === termGroup) {\n\t\treturn {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: \"A term cannot be its own parent\",\n\t\t};\n\t}\n\n\t// Walk up the parent chain. Two checks fold into one walk:\n\t// - Cycle detection (only on update — a non-existent term-being-\n\t// created can't be its own ancestor): if the walk revisits termId\n\t// the proposed parent makes the term a descendant of itself.\n\t// - Depth bound: refuse to extend a chain past MAX_DEPTH ancestors.\n\t// Runs on both create and update so a malicious or buggy caller\n\t// can't grow the tree without limit.\n\t//\n\t// The depth-exceeded error fires only when we hit the limit AND there\n\t// was still chain to walk — a legitimate chain of exactly MAX_DEPTH\n\t// ancestors exits with `cursor === null` and is accepted.\n\t// `parent_id` stores a translation_group, which always equals the anchor\n\t// row's id, so findById(cursor) still resolves each ancestor.\n\tconst MAX_DEPTH = 100;\n\tlet cursor: string | null = parent.parentId;\n\tlet steps = 0;\n\twhile (cursor !== null && steps < MAX_DEPTH) {\n\t\tif (termGroup !== null && cursor === termGroup) {\n\t\t\treturn {\n\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\tmessage: \"Cycle detected: cannot make a descendant the parent\",\n\t\t\t};\n\t\t}\n\t\tconst next = await repo.findById(cursor);\n\t\tif (!next) break;\n\t\tcursor = next.parentId;\n\t\tsteps++;\n\t}\n\tif (cursor !== null && steps >= MAX_DEPTH) {\n\t\treturn {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: \"Parent chain exceeds maximum depth\",\n\t\t};\n\t}\n\n\treturn null;\n}\n\n/**\n * Create a new term in a taxonomy\n */\nexport async function handleTermCreate(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\tinput: {\n\t\tslug: string;\n\t\tlabel: string;\n\t\tparentId?: string | null;\n\t\tdescription?: string;\n\t\tlocale?: string;\n\t\ttranslationOf?: string;\n\t},\n): Promise<ApiResult<TermResponse>> {\n\ttry {\n\t\t// Taxonomy definitions are per-locale, but terms can exist in any locale\n\t\t// regardless of whether the def has been translated there. Look up the\n\t\t// def across all locales — we only care that it *exists*.\n\t\tconst lookup = await requireTaxonomyDef(db, taxonomyName);\n\t\tif (!lookup.success) return lookup;\n\n\t\tconst repo = new TaxonomyRepository(db);\n\n\t\t// Coerce empty-string parentId to undefined (treat as \"no parent\").\n\t\tconst parentId =\n\t\t\tinput.parentId === \"\" || input.parentId === undefined ? undefined : input.parentId;\n\n\t\t// Conflict check is scoped to locale (per-locale slugs are unique).\n\t\tconst existing = await repo.findBySlug(taxonomyName, input.slug, input.locale);\n\t\tif (existing) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\tmessage: input.locale\n\t\t\t\t\t\t? `Term '${input.slug}' already exists in '${taxonomyName}' (${input.locale})`\n\t\t\t\t\t\t: `Term with slug '${input.slug}' already exists in taxonomy '${taxonomyName}'`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// No locale re-pointing needed: `repo.create` persists the parent's\n\t\t// translation_group in `parent_id`, so a child stays nested under the\n\t\t// parent in every locale automatically — including parents translated\n\t\t// after the child was created.\n\n\t\t// A translation inherits its source's translation_group; pass that\n\t\t// prospective group so validateParentTerm can reject a parent in the same\n\t\t// group (cross-locale self-parent) even though the term doesn't exist yet.\n\t\tlet selfGroup: string | null = null;\n\t\tif (input.translationOf) {\n\t\t\tconst source = await repo.findById(input.translationOf);\n\t\t\tselfGroup = source ? (source.translationGroup ?? source.id) : null;\n\t\t}\n\n\t\t// Validate parentId: must exist AND belong to the same taxonomy.\n\t\t// (Cycle check is N/A on create — the term doesn't exist yet.)\n\t\tconst parentError = await validateParentTerm(\n\t\t\trepo,\n\t\t\ttaxonomyName,\n\t\t\tundefined,\n\t\t\tparentId,\n\t\t\tselfGroup,\n\t\t);\n\t\tif (parentError) {\n\t\t\treturn { success: false, error: parentError };\n\t\t}\n\n\t\tconst term = await repo.create({\n\t\t\tname: taxonomyName,\n\t\t\tslug: input.slug,\n\t\t\tlabel: input.label,\n\t\t\tparentId: parentId ?? undefined,\n\t\t\tdata: input.description ? { description: input.description } : undefined,\n\t\t\tlocale: input.locale,\n\t\t\ttranslationOf: input.translationOf,\n\t\t});\n\n\t\tinvalidateTermCache();\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tterm: {\n\t\t\t\t\tid: term.id,\n\t\t\t\t\tname: term.name,\n\t\t\t\t\tslug: term.slug,\n\t\t\t\t\tlabel: term.label,\n\t\t\t\t\tparentId: term.parentId,\n\t\t\t\t\tdescription:\n\t\t\t\t\t\ttypeof term.data?.description === \"string\" ? term.data.description : undefined,\n\t\t\t\t\tlocale: term.locale,\n\t\t\t\t\ttranslationGroup: term.translationGroup,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TERM_CREATE_ERROR\", message: \"Failed to create term\" },\n\t\t};\n\t}\n}\n\n/**\n * Get a single term by slug\n */\nexport async function handleTermGet(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<TermGetResponse>> {\n\ttry {\n\t\tconst repo = new TaxonomyRepository(db);\n\t\tconst term = await repo.findBySlug(taxonomyName, termSlug, options.locale);\n\n\t\tif (!term) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Term '${termSlug}' not found in taxonomy '${taxonomyName}'`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst count = await repo.countEntriesWithTerm(term.id);\n\t\t// Children share this term's translation_group as their parent_id; scope\n\t\t// to the term's own locale so the response stays within one locale's tree.\n\t\tconst children = await repo.findChildren(term.id, term.locale);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tterm: {\n\t\t\t\t\tid: term.id,\n\t\t\t\t\tname: term.name,\n\t\t\t\t\tslug: term.slug,\n\t\t\t\t\tlabel: term.label,\n\t\t\t\t\tparentId: term.parentId,\n\t\t\t\t\tdescription:\n\t\t\t\t\t\ttypeof term.data?.description === \"string\" ? term.data.description : undefined,\n\t\t\t\t\tcount,\n\t\t\t\t\tchildren: children.map((c) => ({ id: c.id, slug: c.slug, label: c.label })),\n\t\t\t\t\tlocale: term.locale,\n\t\t\t\t\ttranslationGroup: term.translationGroup,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TERM_GET_ERROR\", message: \"Failed to get term\" },\n\t\t};\n\t}\n}\n\n/** List every translation of a term (by id or translation_group). */\nexport async function handleTermTranslations(\n\tdb: Kysely<Database>,\n\tidOrGroup: string,\n): Promise<ApiResult<TermTranslationsResponse>> {\n\ttry {\n\t\tconst anchor = await db\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where((eb) => eb.or([eb(\"id\", \"=\", idOrGroup), eb(\"translation_group\", \"=\", idOrGroup)]))\n\t\t\t.executeTakeFirst();\n\t\tif (!anchor) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: \"Term not found\" },\n\t\t\t};\n\t\t}\n\t\tconst group = anchor.translation_group ?? anchor.id;\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"translation_group\", \"=\", group)\n\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t.execute();\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\ttranslationGroup: group,\n\t\t\t\ttranslations: rows.map((r) => ({\n\t\t\t\t\tid: r.id,\n\t\t\t\t\tslug: r.slug,\n\t\t\t\t\tlabel: r.label,\n\t\t\t\t\tlocale: r.locale,\n\t\t\t\t})),\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TERM_TRANSLATIONS_ERROR\", message: \"Failed to list term translations\" },\n\t\t};\n\t}\n}\n\n/**\n * Update a term\n */\nexport async function handleTermUpdate(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\tinput: { slug?: string; label?: string; parentId?: string | null; description?: string },\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<TermResponse>> {\n\ttry {\n\t\tconst repo = new TaxonomyRepository(db);\n\t\tconst term = await repo.findBySlug(taxonomyName, termSlug, options.locale);\n\n\t\tif (!term) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Term '${termSlug}' not found in taxonomy '${taxonomyName}'`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Coerce empty-string slug/parentId to undefined (treat as \"no change\").\n\t\t// `null` parentId is a valid request meaning \"detach from parent\".\n\t\tconst newSlug = input.slug === \"\" || input.slug === undefined ? undefined : input.slug;\n\t\tconst newParentId =\n\t\t\tinput.parentId === \"\" || input.parentId === undefined ? undefined : input.parentId;\n\n\t\t// Check if new slug conflicts (per-locale uniqueness).\n\t\tif (newSlug !== undefined && newSlug !== termSlug) {\n\t\t\tconst existing = await repo.findBySlug(taxonomyName, newSlug, options.locale);\n\t\t\tif (existing && existing.id !== term.id) {\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: {\n\t\t\t\t\t\tcode: \"CONFLICT\",\n\t\t\t\t\t\tmessage: `Term with slug '${newSlug}' already exists in taxonomy '${taxonomyName}'`,\n\t\t\t\t\t},\n\t\t\t\t};\n\t\t\t}\n\t\t}\n\n\t\t// Validate parentId: existence, same-taxonomy, no self-parent, no cycle.\n\t\tconst parentError = await validateParentTerm(repo, taxonomyName, term.id, newParentId);\n\t\tif (parentError) {\n\t\t\treturn { success: false, error: parentError };\n\t\t}\n\n\t\tconst updated = await repo.update(term.id, {\n\t\t\tslug: newSlug,\n\t\t\tlabel: input.label,\n\t\t\tparentId: newParentId,\n\t\t\tdata: input.description !== undefined ? { description: input.description } : undefined,\n\t\t});\n\n\t\tinvalidateTermCache();\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"TERM_UPDATE_ERROR\", message: \"Failed to update term\" },\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tterm: {\n\t\t\t\t\tid: updated.id,\n\t\t\t\t\tname: updated.name,\n\t\t\t\t\tslug: updated.slug,\n\t\t\t\t\tlabel: updated.label,\n\t\t\t\t\tparentId: updated.parentId,\n\t\t\t\t\tdescription:\n\t\t\t\t\t\ttypeof updated.data?.description === \"string\" ? updated.data.description : undefined,\n\t\t\t\t\tlocale: updated.locale,\n\t\t\t\t\ttranslationGroup: updated.translationGroup,\n\t\t\t\t},\n\t\t\t},\n\t\t};\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TERM_UPDATE_ERROR\", message: \"Failed to update term\" },\n\t\t};\n\t}\n}\n\n/**\n * Delete a term\n */\nexport async function handleTermDelete(\n\tdb: Kysely<Database>,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\toptions: { locale?: string } = {},\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new TaxonomyRepository(db);\n\t\tconst term = await repo.findBySlug(taxonomyName, termSlug, options.locale);\n\n\t\tif (!term) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"NOT_FOUND\",\n\t\t\t\t\tmessage: `Term '${termSlug}' not found in taxonomy '${taxonomyName}'`,\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\t// Block deletion if the term's group still parents any child in any\n\t\t// locale — children store this group in parent_id, so removing the term\n\t\t// would orphan them (or null their parent_id via the self-FK).\n\t\tconst children = await repo.findChildren(term.id);\n\t\tif (children.length > 0) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: {\n\t\t\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\t\t\tmessage: \"Cannot delete term with children. Delete children first.\",\n\t\t\t\t},\n\t\t\t};\n\t\t}\n\n\t\tconst deleted = await repo.delete(term.id);\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"TERM_DELETE_ERROR\", message: \"Failed to delete term\" },\n\t\t\t};\n\t\t}\n\n\t\tinvalidateTermCache();\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch {\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: { code: \"TERM_DELETE_ERROR\", message: \"Failed to delete term\" },\n\t\t};\n\t}\n}\n"],"mappings":"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"}
@@ -1,9 +1,10 @@
1
- import { a as __exportAll } from "./runner-BsquExlK.mjs";
2
- import { d as invalidateTaxonomyObjectCache, n as cachedQuery, r as contentNamespace, t as CacheNamespace } from "./object-cache-SEb2IM4Z.mjs";
3
- import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BHgcHh2C.mjs";
1
+ import { a as __exportAll } from "./runner-CFngB4WZ.mjs";
2
+ import { getRequestContext } from "./request-context.mjs";
3
+ import { d as invalidateTaxonomyObjectCache, f as isObjectCacheActive, n as cachedQuery, r as contentNamespace, t as CacheNamespace } from "./object-cache-CHbHv83-.mjs";
4
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-CZFiauig.mjs";
4
5
  import { i as setRequestCacheEntry, n as peekRequestCache, r as requestCached } from "./request-cache-KCNHp_RJ.mjs";
5
6
  import { n as isMissingTableError } from "./db-errors-CK46D-ly.mjs";
6
- import { a as getDb } from "./loader-DrpofRon.mjs";
7
+ import { a as getDb, s as resetTaxonomyNamesCache } from "./loader-4K37qA-y.mjs";
7
8
  import { i as resolveLocaleChain, r as resolveLocale } from "./resolve-C7I0qiR0.mjs";
8
9
 
9
10
  //#region src/taxonomies/index.ts
@@ -28,33 +29,87 @@ var taxonomies_exports = /* @__PURE__ */ __exportAll({
28
29
  getTaxonomyTerms: () => getTaxonomyTerms,
29
30
  getTerm: () => getTerm,
30
31
  getTermsForEntries: () => getTermsForEntries,
32
+ invalidateTaxonomyDefsCache: () => invalidateTaxonomyDefsCache,
31
33
  invalidateTermCache: () => invalidateTermCache,
32
34
  primeFoldedEntryTerms: () => primeFoldedEntryTerms
33
35
  });
36
+ /** Invalidate cached taxonomy term data and any content that hydrates terms. */
37
+ function invalidateTermCache() {
38
+ invalidateTaxonomyObjectCache();
39
+ }
40
+ const TAXONOMY_DEFS_CACHE_KEY = Symbol.for("emdash:taxonomy-defs");
41
+ const taxonomyDefsStore = globalThis;
42
+ const defsHolder = taxonomyDefsStore[TAXONOMY_DEFS_CACHE_KEY] ?? (() => {
43
+ const h = {
44
+ version: 0,
45
+ cache: /* @__PURE__ */ new Map()
46
+ };
47
+ taxonomyDefsStore[TAXONOMY_DEFS_CACHE_KEY] = h;
48
+ return h;
49
+ })();
34
50
  /**
35
- * Invalidate cached taxonomy data in the distributed object cache (and any
36
- * content that hydrates taxonomy terms). The legacy in-isolate term cache was
37
- * removed, so this used to be a no-op; it now drives object-cache invalidation.
51
+ * Invalidate the isolate-wide taxonomy-definitions cache (and the related
52
+ * loader taxonomy-names cache). Called from every taxonomy-def write path
53
+ * (`handleTaxonomyCreate`, seed application). Other isolates refresh on their
54
+ * next recycle — staleness bounded by isolate lifetime.
38
55
  */
39
- function invalidateTermCache() {
56
+ function invalidateTaxonomyDefsCache() {
57
+ defsHolder.version++;
58
+ defsHolder.cache.clear();
59
+ resetTaxonomyNamesCache();
40
60
  invalidateTaxonomyObjectCache();
41
61
  }
42
62
  /**
63
+ * Fetch taxonomy definitions straight from the database (no caching).
64
+ */
65
+ async function fetchTaxonomyDefs(locale) {
66
+ let query = (await getDb()).selectFrom("_emdash_taxonomy_defs").selectAll();
67
+ if (locale !== void 0) query = query.where("locale", "=", locale);
68
+ return (await query.execute()).map(rowToTaxonomyDef);
69
+ }
70
+ /**
71
+ * Resolve taxonomy defs through the isolate fallback cache, bypassing it for
72
+ * isolated databases. The returned promise is cached (not the resolved value)
73
+ * so concurrent cold-isolate readers share one in-flight query; a rejection
74
+ * evicts the entry so the next caller retries.
75
+ */
76
+ function loadTaxonomyDefs(localeKey, locale) {
77
+ if (getRequestContext()?.dbIsIsolated === true) return fetchTaxonomyDefs(locale);
78
+ const existing = defsHolder.cache.get(localeKey);
79
+ if (existing && existing.version === defsHolder.version) return existing.promise;
80
+ const version = defsHolder.version;
81
+ const promise = fetchTaxonomyDefs(locale).catch((error) => {
82
+ const current = defsHolder.cache.get(localeKey);
83
+ if (current && current.promise === promise) defsHolder.cache.delete(localeKey);
84
+ throw error;
85
+ });
86
+ defsHolder.cache.set(localeKey, {
87
+ version,
88
+ promise
89
+ });
90
+ return promise;
91
+ }
92
+ /**
43
93
  * Get every taxonomy definition. Definitions are per-locale (one row per
44
94
  * locale inside the same translation_group) — by default we resolve to the
45
95
  * active locale.
96
+ *
97
+ * Two-tier cache: per-request via `requestCached` (so a single render that
98
+ * hydrates terms for several collections pays at most one call), then
99
+ * per-isolate via the global holder (so warm renders issue zero queries).
100
+ * The `requestCached` key is unchanged so `getTaxonomyDef`'s peek still hits.
46
101
  */
47
102
  async function getTaxonomyDefs(options = {}) {
48
103
  const locale = resolveLocale(options.locale);
49
- return requestCached(`taxonomy-defs:${locale ?? "*"}`, () => cachedQuery({
50
- namespace: CacheNamespace.TAXONOMIES,
51
- key: `defs:${locale ?? "*"}`,
52
- load: async () => {
53
- let query = (await getDb()).selectFrom("_emdash_taxonomy_defs").selectAll();
54
- if (locale !== void 0) query = query.where("locale", "=", locale);
55
- return (await query.execute()).map(rowToTaxonomyDef);
56
- }
57
- }));
104
+ const localeKey = locale ?? "*";
105
+ return requestCached(`taxonomy-defs:${localeKey}`, async () => {
106
+ if (await isObjectCacheActive()) return cachedQuery({
107
+ namespace: CacheNamespace.TAXONOMIES,
108
+ key: `defs:${localeKey}`,
109
+ load: () => fetchTaxonomyDefs(locale)
110
+ });
111
+ return loadTaxonomyDefs(localeKey, locale);
112
+ });
58
113
  }
59
114
  /**
60
115
  * Get a single taxonomy definition by name. Uses the fallback chain so even
@@ -419,7 +474,7 @@ function primeFoldedEntryTerms(collection, perEntry, options = {}) {
419
474
  * the content query respect the active locale.
420
475
  */
421
476
  async function getEntriesByTerm(collection, taxonomyName, termSlug, options = {}) {
422
- const { getEmDashCollection } = await import("./query-34pfm02Q.mjs").then((n) => n.o);
477
+ const { getEmDashCollection } = await import("./query-B0M-9LYR.mjs").then((n) => n.o);
423
478
  const queryOptions = { where: { [taxonomyName]: termSlug } };
424
479
  if (options.locale !== void 0) queryOptions.locale = options.locale;
425
480
  const { entries } = await getEmDashCollection(collection, queryOptions);
@@ -469,5 +524,5 @@ function buildTree(flatTerms, counts) {
469
524
  }
470
525
 
471
526
  //#endregion
472
- export { getTaxonomyDefs as a, getTermsForEntries as c, getTaxonomyDef as i, invalidateTermCache as l, getEntriesByTerm as n, getTaxonomyTerms as o, getEntryTerms as r, getTerm as s, getAllTermsForEntries as t, taxonomies_exports as u };
473
- //# sourceMappingURL=taxonomies-n8w72lLQ.mjs.map
527
+ export { getTaxonomyDefs as a, getTermsForEntries as c, taxonomies_exports as d, getTaxonomyDef as i, invalidateTaxonomyDefsCache as l, getEntriesByTerm as n, getTaxonomyTerms as o, getEntryTerms as r, getTerm as s, getAllTermsForEntries as t, invalidateTermCache as u };
528
+ //# sourceMappingURL=taxonomies-ckPjz-6d.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"taxonomies-ckPjz-6d.mjs","names":[],"sources":["../src/taxonomies/index.ts"],"sourcesContent":["/**\n * Runtime API for taxonomies.\n *\n * All helpers are locale-aware. When a locale is not passed explicitly we fall\n * back to the request context or the configured `defaultLocale` (see\n * `i18n/resolve.ts`).\n *\n * Because `content_taxonomies.taxonomy_id` stores the translation_group (not a\n * specific term id), the joins here are `taxonomies.translation_group =\n * content_taxonomies.taxonomy_id` + filter by `taxonomies.locale`, which picks\n * the right per-locale term.\n */\n\nimport { resolveLocale, resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb, resetTaxonomyNamesCache } from \"../loader.js\";\nimport {\n\tcachedQuery,\n\tCacheNamespace,\n\tcontentNamespace,\n\tinvalidateTaxonomyObjectCache,\n\tisObjectCacheActive,\n} from \"../object-cache/index.js\";\nimport { peekRequestCache, requestCached, setRequestCacheEntry } from \"../request-cache.js\";\nimport { getRequestContext } from \"../request-context.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../utils/chunks.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\nimport type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from \"./types.js\";\n\nexport interface TaxonomyQueryOptions {\n\tlocale?: string;\n}\n\n/** Invalidate cached taxonomy term data and any content that hydrates terms. */\nexport function invalidateTermCache(): void {\n\tinvalidateTaxonomyObjectCache();\n}\n\n/**\n * Worker-isolate cache for taxonomy definitions, keyed by resolved locale.\n *\n * Taxonomy *definitions* (the \"category\"/\"tag\" taxonomies themselves, not\n * their terms) are read on every public render that hydrates entry terms —\n * `getAllTermsForEntries` → `getCollectionTaxonomyNames` → `getTaxonomyDefs` —\n * but change extremely rarely: they're created via the admin API or applied\n * from a seed, and there is no edit/delete-def path. Caching them across the\n * isolate lifetime drops the per-render `SELECT * FROM _emdash_taxonomy_defs`\n * to once-per-isolate.\n *\n * Stored on globalThis behind a Symbol key (same pattern as\n * `settings/index.ts`) so the bundler duplicating this module across SSR\n * chunks can't produce two independent caches.\n *\n * When the distributed object cache is configured, it is authoritative and this\n * fallback is bypassed so cross-isolate epoch invalidation cannot be undercut\n * by stale per-isolate data on an object-cache miss.\n *\n * **Isolated databases bypass the cache.** Playground / DO preview requests\n * set `requestContext.dbIsIsolated`; they point at a divergent schema, so we\n * skip both reading and writing the global holder and fall back to the\n * per-request cache (same precedent as `getTaxonomyNames` / byline field defs).\n */\ninterface TaxonomyDefsHolder {\n\tversion: number;\n\t/** locale key (\"*\" for \"all locales\") → { version it was fetched at, promise }. */\n\tcache: Map<string, { version: number; promise: Promise<TaxonomyDef[]> }>;\n}\n\nconst TAXONOMY_DEFS_CACHE_KEY = Symbol.for(\"emdash:taxonomy-defs\");\nconst taxonomyDefsStore = globalThis as Record<symbol, unknown>;\nconst defsHolder: TaxonomyDefsHolder =\n\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- globalThis singleton pattern (see settings/index.ts)\n\t(taxonomyDefsStore[TAXONOMY_DEFS_CACHE_KEY] as TaxonomyDefsHolder | undefined) ??\n\t(() => {\n\t\tconst h: TaxonomyDefsHolder = { version: 0, cache: new Map() };\n\t\ttaxonomyDefsStore[TAXONOMY_DEFS_CACHE_KEY] = h;\n\t\treturn h;\n\t})();\n\n/**\n * Invalidate the isolate-wide taxonomy-definitions cache (and the related\n * loader taxonomy-names cache). Called from every taxonomy-def write path\n * (`handleTaxonomyCreate`, seed application). Other isolates refresh on their\n * next recycle — staleness bounded by isolate lifetime.\n */\nexport function invalidateTaxonomyDefsCache(): void {\n\tdefsHolder.version++;\n\tdefsHolder.cache.clear();\n\tresetTaxonomyNamesCache();\n\tinvalidateTaxonomyObjectCache();\n}\n\n/**\n * Test/internal helper: clear the per-isolate taxonomy-defs cache. Useful for\n * unit tests that insert defs directly and need to force a refetch without\n * going through a write path. Production code should rely on\n * `invalidateTaxonomyDefsCache()`.\n */\nexport function resetTaxonomyDefsCacheForTests(): void {\n\tdefsHolder.version++;\n\tdefsHolder.cache.clear();\n}\n\n/**\n * Fetch taxonomy definitions straight from the database (no caching).\n */\nasync function fetchTaxonomyDefs(locale: string | undefined): Promise<TaxonomyDef[]> {\n\tconst db = await getDb();\n\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll();\n\tif (locale !== undefined) query = query.where(\"locale\", \"=\", locale);\n\tconst rows = await query.execute();\n\treturn rows.map(rowToTaxonomyDef);\n}\n\n/**\n * Resolve taxonomy defs through the isolate fallback cache, bypassing it for\n * isolated databases. The returned promise is cached (not the resolved value)\n * so concurrent cold-isolate readers share one in-flight query; a rejection\n * evicts the entry so the next caller retries.\n */\nfunction loadTaxonomyDefs(localeKey: string, locale: string | undefined): Promise<TaxonomyDef[]> {\n\tif (getRequestContext()?.dbIsIsolated === true) {\n\t\treturn fetchTaxonomyDefs(locale);\n\t}\n\tconst existing = defsHolder.cache.get(localeKey);\n\tif (existing && existing.version === defsHolder.version) {\n\t\treturn existing.promise;\n\t}\n\tconst version = defsHolder.version;\n\tconst promise = fetchTaxonomyDefs(locale).catch((error: unknown) => {\n\t\tconst current = defsHolder.cache.get(localeKey);\n\t\tif (current && current.promise === promise) {\n\t\t\tdefsHolder.cache.delete(localeKey);\n\t\t}\n\t\tthrow error;\n\t});\n\tdefsHolder.cache.set(localeKey, { version, promise });\n\treturn promise;\n}\n\n/**\n * Get every taxonomy definition. Definitions are per-locale (one row per\n * locale inside the same translation_group) — by default we resolve to the\n * active locale.\n *\n * Two-tier cache: per-request via `requestCached` (so a single render that\n * hydrates terms for several collections pays at most one call), then\n * per-isolate via the global holder (so warm renders issue zero queries).\n * The `requestCached` key is unchanged so `getTaxonomyDef`'s peek still hits.\n */\nexport async function getTaxonomyDefs(options: TaxonomyQueryOptions = {}): Promise<TaxonomyDef[]> {\n\tconst locale = resolveLocale(options.locale);\n\tconst localeKey = locale ?? \"*\";\n\treturn requestCached(`taxonomy-defs:${localeKey}`, async () => {\n\t\tif (await isObjectCacheActive()) {\n\t\t\treturn cachedQuery({\n\t\t\t\tnamespace: CacheNamespace.TAXONOMIES,\n\t\t\t\tkey: `defs:${localeKey}`,\n\t\t\t\tload: () => fetchTaxonomyDefs(locale),\n\t\t\t});\n\t\t}\n\t\treturn loadTaxonomyDefs(localeKey, locale);\n\t});\n}\n\n/**\n * Get a single taxonomy definition by name. Uses the fallback chain so even\n * if there is no translation for the active locale we still return something.\n *\n * If `getTaxonomyDefs()` has already loaded the full list in this request\n * (which happens during entry-term hydration on every page that renders a\n * collection), search the matching def in memory rather than running a\n * second query against `_emdash_taxonomy_defs`.\n */\nexport async function getTaxonomyDef(\n\tname: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyDef | null> {\n\tconst chain = resolveLocaleChain(options.locale);\n\tconst peekKey = `taxonomy-defs:${resolveLocale(options.locale) ?? \"*\"}`;\n\tconst allDefs = peekRequestCache<TaxonomyDef[]>(peekKey);\n\tif (allDefs) {\n\t\tconst defs = await allDefs;\n\t\tif (chain.length === 0) return defs.find((d) => d.name === name) ?? null;\n\t\tfor (const locale of chain) {\n\t\t\tconst found = defs.find((d) => d.name === name && d.locale === locale);\n\t\t\tif (found) return found;\n\t\t}\n\t\treturn null;\n\t}\n\n\treturn requestCached(`taxonomy-def:${name}:${chain.join(\",\")}`, async () => {\n\t\tconst db = await getDb();\n\n\t\tif (chain.length === 0) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.orderBy(\"locale\", \"asc\")\n\t\t\t\t.executeTakeFirst();\n\t\t\treturn row ? rowToTaxonomyDef(row) : null;\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await db\n\t\t\t\t.selectFrom(\"_emdash_taxonomy_defs\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"name\", \"=\", name)\n\t\t\t\t.where(\"locale\", \"=\", locale)\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (row) return rowToTaxonomyDef(row);\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * All terms of a taxonomy in a specific locale (flat for non-hierarchical,\n * tree for hierarchical).\n */\nexport async function getTaxonomyTerms(\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\treturn requestCached(`taxonomy-terms:${taxonomyName}:${locale ?? \"*\"}`, () =>\n\t\tcachedQuery({\n\t\t\tnamespace: CacheNamespace.TAXONOMIES,\n\t\t\tkey: `terms:${taxonomyName}:${locale ?? \"*\"}`,\n\t\t\tload: () => loadTaxonomyTerms(taxonomyName, locale, options),\n\t\t}),\n\t);\n}\n\nasync function loadTaxonomyTerms(\n\ttaxonomyName: string,\n\tlocale: string | undefined,\n\toptions: TaxonomyQueryOptions,\n): Promise<TaxonomyTerm[]> {\n\tconst db = await getDb();\n\n\tconst def = await getTaxonomyDef(taxonomyName, options);\n\tif (!def) return [];\n\n\tlet termsQuery = db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t.orderBy(\"label\", \"asc\");\n\tif (locale !== undefined) termsQuery = termsQuery.where(\"locale\", \"=\", locale);\n\tconst rows = await termsQuery.execute();\n\n\t// Counts are keyed by translation_group (what the pivot stores) and are\n\t// locale-independent, so the aggregate is shared across every taxonomy\n\t// rendered in this request (Categories + Tags widgets, etc.).\n\tconst counts = await getTaxonomyTermCounts();\n\n\tconst flatTerms: TaxonomyTermRow[] = rows.map((row) => ({\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparent_id: row.parent_id,\n\t\tdata: row.data,\n\t\tlocale: row.locale,\n\t\ttranslation_group: row.translation_group,\n\t}));\n\n\tif (def.hierarchical) return buildTree(flatTerms, counts);\n\n\treturn flatTerms.map((term) => ({\n\t\tid: term.id,\n\t\tname: term.name,\n\t\tslug: term.slug,\n\t\tlabel: term.label,\n\t\tdescription: term.data ? JSON.parse(term.data).description : undefined,\n\t\tchildren: [],\n\t\tcount: counts.get(term.translation_group ?? term.id) ?? 0,\n\t\tlocale: term.locale,\n\t\ttranslationGroup: term.translation_group,\n\t}));\n}\n\n/**\n * Per-translation-group usage counts across all taxonomies, in one aggregate\n * scan of `content_taxonomies`. Counts are locale-independent (the pivot stores\n * translation_group), so a single request-cached entry serves every taxonomy\n * that renders during the request.\n */\nfunction getTaxonomyTermCounts(): Promise<Map<string, number>> {\n\treturn requestCached(\"taxonomy-term-counts\", async () => {\n\t\tconst db = await getDb();\n\t\tconst countsResult = await db\n\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t.select([\"taxonomy_id\"])\n\t\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t\t.groupBy(\"taxonomy_id\")\n\t\t\t.execute();\n\t\tconst counts = new Map<string, number>();\n\t\tfor (const row of countsResult) counts.set(row.taxonomy_id, row.count);\n\t\treturn counts;\n\t});\n}\n\n/**\n * Get a single term by (taxonomy, slug). Honours the fallback chain — if the\n * slug exists in a fallback locale, we return that row (useful for deep-linking\n * to a term page when the translation is missing).\n */\nexport async function getTerm(\n\ttaxonomyName: string,\n\tslug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm | null> {\n\tconst chain = resolveLocaleChain(options.locale);\n\t// Cached under the shared taxonomies epoch (bumped on any taxonomy / term\n\t// assignment write). The `count` reflects content_taxonomies rows; a stale\n\t// count after a bare content delete is bounded by the entry's TTL.\n\treturn cachedQuery({\n\t\tnamespace: CacheNamespace.TAXONOMIES,\n\t\tkey: `term:${taxonomyName}:${slug}:${chain.join(\",\")}`,\n\t\tload: () => loadTerm(taxonomyName, slug, chain),\n\t});\n}\n\nasync function loadTerm(\n\ttaxonomyName: string,\n\tslug: string,\n\tchain: string[],\n): Promise<TaxonomyTerm | null> {\n\tconst db = await getDb();\n\n\tlet row: Awaited<ReturnType<ReturnType<typeof selectTerm>[\"executeTakeFirst\"]>>;\n\tconst selectTerm = () =>\n\t\tdb\n\t\t\t.selectFrom(\"taxonomies\")\n\t\t\t.selectAll()\n\t\t\t.where(\"name\", \"=\", taxonomyName)\n\t\t\t.where(\"slug\", \"=\", slug);\n\n\tif (chain.length === 0) {\n\t\trow = await selectTerm().orderBy(\"locale\", \"asc\").executeTakeFirst();\n\t} else {\n\t\trow = undefined;\n\t\tfor (const locale of chain) {\n\t\t\trow = await selectTerm().where(\"locale\", \"=\", locale).executeTakeFirst();\n\t\t\tif (row) break;\n\t\t}\n\t}\n\n\tif (!row) return null;\n\n\tlet childrenQuery = db\n\t\t.selectFrom(\"taxonomies\")\n\t\t.selectAll()\n\t\t// Children store the parent's translation_group in parent_id (not a row\n\t\t// id), so a translated parent still owns its children in its own locale.\n\t\t.where(\"parent_id\", \"=\", row.translation_group ?? row.id)\n\t\t.orderBy(\"label\", \"asc\");\n\tconst termLocale = row.locale;\n\tif (termLocale) childrenQuery = childrenQuery.where(\"locale\", \"=\", termLocale);\n\n\t// The usage-count and children queries both depend only on the term row,\n\t// so run them concurrently to save a round trip on remote databases.\n\tconst [countResult, childRows] = await Promise.all([\n\t\tdb\n\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t.select((eb) => eb.fn.count<number>(\"entry_id\").as(\"count\"))\n\t\t\t.where(\"taxonomy_id\", \"=\", row.translation_group ?? row.id)\n\t\t\t.executeTakeFirst(),\n\t\tchildrenQuery.execute(),\n\t]);\n\tconst count = countResult?.count ?? 0;\n\n\tconst children = childRows.map<TaxonomyTerm>((child) => ({\n\t\tid: child.id,\n\t\tname: child.name,\n\t\tslug: child.slug,\n\t\tlabel: child.label,\n\t\tparentId: child.parent_id ?? undefined,\n\t\tchildren: [],\n\t\tlocale: child.locale,\n\t\ttranslationGroup: child.translation_group,\n\t}));\n\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\tparentId: row.parent_id ?? undefined,\n\t\tdescription: row.data ? JSON.parse(row.data).description : undefined,\n\t\tchildren,\n\t\tcount,\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Terms assigned to a content entry, resolved into the active locale. Terms\n * whose translation_group lacks a row in the requested locale are omitted.\n */\nexport function getEntryTerms(\n\tcollection: string,\n\tentryId: string,\n\ttaxonomyName?: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<TaxonomyTerm[]> {\n\tconst locale = resolveLocale(options.locale);\n\t// requestCached short-circuits to values primed by getAllTermsForEntries\n\t// during entry hydration (same key shape). On a warm content-cache hit\n\t// hydration doesn't run, so the inner cachedQuery serves this from KV\n\t// instead of falling through to D1 on every request.\n\treturn requestCached(\n\t\t`terms:${collection}:${entryId}:${taxonomyName ?? \"*\"}:${locale ?? \"*\"}`,\n\t\t() =>\n\t\t\tcachedQuery({\n\t\t\t\tnamespace: [contentNamespace(collection), CacheNamespace.TAXONOMIES],\n\t\t\t\tkey: `entryTerms:${collection}:${entryId}:${taxonomyName ?? \"*\"}:${locale ?? \"*\"}`,\n\t\t\t\tload: async () => {\n\t\t\t\t\tconst db = await getDb();\n\n\t\t\t\t\tlet query = db\n\t\t\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t\t\t.innerJoin(\n\t\t\t\t\t\t\t\"taxonomies\",\n\t\t\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t\t\t\t\"content_taxonomies.taxonomy_id\",\n\t\t\t\t\t\t)\n\t\t\t\t\t\t.selectAll(\"taxonomies\")\n\t\t\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t\t\t.where(\"content_taxonomies.entry_id\", \"=\", entryId);\n\n\t\t\t\t\tif (taxonomyName) query = query.where(\"taxonomies.name\", \"=\", taxonomyName);\n\t\t\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\n\t\t\t\t\tconst rows = await query.execute();\n\t\t\t\t\treturn rows.map<TaxonomyTerm>((row) => ({\n\t\t\t\t\t\tid: row.id,\n\t\t\t\t\t\tname: row.name,\n\t\t\t\t\t\tslug: row.slug,\n\t\t\t\t\t\tlabel: row.label,\n\t\t\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\t\t\tchildren: [],\n\t\t\t\t\t\tlocale: row.locale,\n\t\t\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t\t\t}));\n\t\t\t\t},\n\t\t\t}),\n\t);\n}\n\n/**\n * Terms for multiple entries of one taxonomy, single query.\n */\nexport async function getTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\ttaxonomyName: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, TaxonomyTerm[]>> {\n\tconst uniqueIds = [...new Set(entryIds)];\n\tif (uniqueIds.length === 0) return new Map();\n\tconst locale = resolveLocale(options.locale);\n\tconst localeKey = locale ?? \"*\";\n\n\t// The query result is a Map, which JSON can't represent — cache it as an\n\t// array of [entryId, terms] pairs and rebuild the Map on read.\n\tconst load = async (): Promise<Array<[string, TaxonomyTerm[]]>> => {\n\t\tconst result = new Map<string, TaxonomyTerm[]>();\n\t\tfor (const id of uniqueIds) result.set(id, []);\n\n\t\t// Entry-term hydration (getAllTermsForEntries -> primeEntryTermsCache)\n\t\t// seeds the per-entry cache under the same key getEntryTerms uses:\n\t\t// `terms:${collection}:${entryId}:${taxonomyName}:${localeKey}`, storing a\n\t\t// TaxonomyTerm[] (including `[]` for entries with no terms). Satisfy those\n\t\t// from cache and run the batched query only for the ids that missed.\n\t\tconst missedIds: string[] = [];\n\t\ttype CacheRead = { id: string; terms: TaxonomyTerm[] } | { id: string; miss: true };\n\t\tconst cacheReads: Array<Promise<CacheRead>> = [];\n\t\tfor (const id of uniqueIds) {\n\t\t\tconst cached = peekRequestCache<TaxonomyTerm[]>(\n\t\t\t\t`terms:${collection}:${id}:${taxonomyName}:${localeKey}`,\n\t\t\t);\n\t\t\tif (cached) {\n\t\t\t\t// A peeked promise can reject (e.g. a sibling getEntryTerms hit a\n\t\t\t\t// missing table). Treat a rejection as a cache miss so the batched\n\t\t\t\t// query path -- and its isMissingTableError guard below -- still runs,\n\t\t\t\t// rather than propagating an uncaught error.\n\t\t\t\tcacheReads.push(\n\t\t\t\t\tcached.then(\n\t\t\t\t\t\t(terms): CacheRead => ({ id, terms }),\n\t\t\t\t\t\t(): CacheRead => ({ id, miss: true }),\n\t\t\t\t\t),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tmissedIds.push(id);\n\t\t\t}\n\t\t}\n\t\tfor (const read of await Promise.all(cacheReads)) {\n\t\t\tif (\"miss\" in read) {\n\t\t\t\tmissedIds.push(read.id);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// Return a private copy. The cached array and its term objects are shared\n\t\t\t// with getEntryTerms/getAllTermsForEntries (primeEntryTermsCache stores\n\t\t\t// the same references), so a caller that mutates the result -- sorting in\n\t\t\t// place, pushing into `children` -- must not poison the cache. The\n\t\t\t// pre-cache implementation always returned freshly built arrays.\n\t\t\tresult.set(\n\t\t\t\tread.id,\n\t\t\t\tread.terms.map((t) => ({ ...t, children: [...t.children] })),\n\t\t\t);\n\t\t}\n\n\t\tif (missedIds.length === 0) return [...result.entries()];\n\n\t\tconst db = await getDb();\n\t\tfor (const chunk of chunks(missedIds, SQL_BATCH_SIZE)) {\n\t\t\tlet rows;\n\t\t\ttry {\n\t\t\t\tlet query = db\n\t\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t\t.select([\n\t\t\t\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\t\t\t\"taxonomies.id\",\n\t\t\t\t\t\t\"taxonomies.name\",\n\t\t\t\t\t\t\"taxonomies.slug\",\n\t\t\t\t\t\t\"taxonomies.label\",\n\t\t\t\t\t\t\"taxonomies.parent_id\",\n\t\t\t\t\t\t\"taxonomies.locale\",\n\t\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t\t])\n\t\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t\t.where(\"content_taxonomies.entry_id\", \"in\", chunk)\n\t\t\t\t\t.where(\"taxonomies.name\", \"=\", taxonomyName)\n\t\t\t\t\t// Match the order getAllTermsForEntries (the cache primer) uses, so\n\t\t\t\t\t// cache-hit and DB-miss entries in one result are ordered consistently.\n\t\t\t\t\t.orderBy(\"taxonomies.label\", \"asc\");\n\t\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\t\t\t\trows = await query.execute();\n\t\t\t} catch (error) {\n\t\t\t\tif (isMissingTableError(error)) return [...result.entries()];\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\t\tid: row.id,\n\t\t\t\t\tname: row.name,\n\t\t\t\t\tslug: row.slug,\n\t\t\t\t\tlabel: row.label,\n\t\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\t\tchildren: [],\n\t\t\t\t\tlocale: row.locale,\n\t\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t\t};\n\t\t\t\tconst terms = result.get(row.entry_id);\n\t\t\t\tif (terms) terms.push(term);\n\t\t\t}\n\t\t}\n\n\t\treturn [...result.entries()];\n\t};\n\n\t// Key on the sorted unique ids. Bound the key length: very large batches\n\t// (rare; they come from collection hydration, already served by the content\n\t// cache) bypass the object cache rather than blow past KV's key limit.\n\tconst idKey = uniqueIds.toSorted().join(\",\");\n\tconst pairs =\n\t\tidKey.length <= 256\n\t\t\t? await cachedQuery({\n\t\t\t\t\tnamespace: [contentNamespace(collection), CacheNamespace.TAXONOMIES],\n\t\t\t\t\tkey: `termsForEntries:${collection}:${taxonomyName}:${locale ?? \"*\"}:${idKey}`,\n\t\t\t\t\tload,\n\t\t\t\t})\n\t\t\t: await load();\n\n\treturn new Map(pairs);\n}\n\n/**\n * Batch-fetch terms for multiple entries across ALL taxonomies in one query.\n * Primes the request-cache for subsequent per-entry calls to `getEntryTerms`.\n */\nexport async function getAllTermsForEntries(\n\tcollection: string,\n\tentryIds: string[],\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {\n\tconst result = new Map<string, Record<string, TaxonomyTerm[]>>();\n\tconst uniqueIds = [...new Set(entryIds)];\n\tfor (const id of uniqueIds) result.set(id, {});\n\tif (uniqueIds.length === 0) return result;\n\n\tconst db = await getDb();\n\tconst locale = resolveLocale(options.locale);\n\tconst applicableTaxonomyNames = await getCollectionTaxonomyNames(collection, { locale });\n\n\tfor (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {\n\t\tlet rows;\n\t\ttry {\n\t\t\tlet query = db\n\t\t\t\t.selectFrom(\"content_taxonomies\")\n\t\t\t\t.innerJoin(\"taxonomies\", \"taxonomies.translation_group\", \"content_taxonomies.taxonomy_id\")\n\t\t\t\t.select([\n\t\t\t\t\t\"content_taxonomies.entry_id\",\n\t\t\t\t\t\"taxonomies.id\",\n\t\t\t\t\t\"taxonomies.name\",\n\t\t\t\t\t\"taxonomies.slug\",\n\t\t\t\t\t\"taxonomies.label\",\n\t\t\t\t\t\"taxonomies.parent_id\",\n\t\t\t\t\t\"taxonomies.locale\",\n\t\t\t\t\t\"taxonomies.translation_group\",\n\t\t\t\t])\n\t\t\t\t.where(\"content_taxonomies.collection\", \"=\", collection)\n\t\t\t\t.where(\"content_taxonomies.entry_id\", \"in\", chunk)\n\t\t\t\t.orderBy(\"taxonomies.label\", \"asc\");\n\t\t\tif (locale !== undefined) query = query.where(\"taxonomies.locale\", \"=\", locale);\n\t\t\trows = await query.execute();\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) {\n\t\t\t\tfor (const id of uniqueIds) {\n\t\t\t\t\tprimeEntryTermsCache(collection, id, {}, applicableTaxonomyNames, locale);\n\t\t\t\t}\n\t\t\t\treturn result;\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const row of rows) {\n\t\t\tconst term: TaxonomyTerm = {\n\t\t\t\tid: row.id,\n\t\t\t\tname: row.name,\n\t\t\t\tslug: row.slug,\n\t\t\t\tlabel: row.label,\n\t\t\t\tparentId: row.parent_id ?? undefined,\n\t\t\t\tchildren: [],\n\t\t\t\tlocale: row.locale,\n\t\t\t\ttranslationGroup: row.translation_group,\n\t\t\t};\n\t\t\tconst byTaxonomy = result.get(row.entry_id);\n\t\t\tif (!byTaxonomy) continue;\n\t\t\tconst existing = byTaxonomy[row.name];\n\t\t\tif (existing) existing.push(term);\n\t\t\telse byTaxonomy[row.name] = [term];\n\t\t}\n\t}\n\n\tfor (const [entryId, byTaxonomy] of result) {\n\t\tprimeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames, locale);\n\t}\n\n\treturn result;\n}\n\n/**\n * Return the list of taxonomy names applicable to a collection, request-\n * cached so a page render only pays for it once.\n *\n * Returns an empty list when taxonomies haven't been defined yet.\n */\nasync function getCollectionTaxonomyNames(\n\tcollection: string,\n\toptions: TaxonomyQueryOptions,\n): Promise<string[]> {\n\ttry {\n\t\tconst defs = await getTaxonomyDefs(options);\n\t\treturn defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) return [];\n\t\tthrow error;\n\t}\n}\n\n/**\n * Pre-populate the request-cache for every getEntryTerms call-shape that\n * could hit this entry:\n *\n * getEntryTerms(collection, entryId) -> key `terms:C:E:*`\n * getEntryTerms(collection, entryId, \"tag\") -> key `terms:C:E:tag`\n * getEntryTerms(collection, entryId, \"category\") -> key `terms:C:E:category`\n * ...one per taxonomy that applies to this collection\n *\n * Taxonomies with no rows on this entry are seeded with `[]` so legacy\n * callers short-circuit to the cached empty array instead of re-querying.\n */\nfunction primeEntryTermsCache(\n\tcollection: string,\n\tentryId: string,\n\tbyTaxonomy: Record<string, TaxonomyTerm[]>,\n\tapplicableTaxonomyNames: string[],\n\tlocale: string | undefined,\n): void {\n\tconst localeKey = locale ?? \"*\";\n\tfor (const name of applicableTaxonomyNames) {\n\t\tsetRequestCacheEntry(\n\t\t\t`terms:${collection}:${entryId}:${name}:${localeKey}`,\n\t\t\tbyTaxonomy[name] ?? [],\n\t\t);\n\t}\n\tfor (const [name, terms] of Object.entries(byTaxonomy)) {\n\t\tsetRequestCacheEntry(`terms:${collection}:${entryId}:${name}:${localeKey}`, terms);\n\t}\n\tconst allTerms = Object.values(byTaxonomy).flat();\n\tsetRequestCacheEntry(`terms:${collection}:${entryId}:*:${localeKey}`, allTerms);\n}\n\n/**\n * Prime the per-entry request cache from terms that were folded into the\n * content query (query.ts `hydrateEntryTerms` fast path), so subsequent\n * `getEntryTerms` calls in the same render hit the cache instead of issuing an\n * N+1 query. Seeds the wildcard key and one key per taxonomy present on the\n * entry — purely from the folded data, with no DB lookup.\n *\n * Unlike `getAllTermsForEntries`, this deliberately does NOT seed `[]` for\n * taxonomies that apply to the collection but have no rows on the entry: doing\n * so would require a `getTaxonomyDefs` query, adding a round trip to every fold\n * render to serve the rarer `getEntryTerms(id, absentTaxonomy)` case from cache.\n * That call simply falls through to its own cached query. Keeping the key shape\n * here (rather than in query.ts) prevents the two from drifting.\n */\nexport function primeFoldedEntryTerms(\n\tcollection: string,\n\tperEntry: Array<{ entryId: string; byTaxonomy: Record<string, TaxonomyTerm[]> }>,\n\toptions: TaxonomyQueryOptions = {},\n): void {\n\tif (perEntry.length === 0) return;\n\tconst locale = resolveLocale(options.locale);\n\tfor (const { entryId, byTaxonomy } of perEntry) {\n\t\tprimeEntryTermsCache(collection, entryId, byTaxonomy, [], locale);\n\t}\n}\n\n/**\n * Get entries by term. Both the lookup (term slug in the active locale) and\n * the content query respect the active locale.\n */\nexport async function getEntriesByTerm(\n\tcollection: string,\n\ttaxonomyName: string,\n\ttermSlug: string,\n\toptions: TaxonomyQueryOptions = {},\n): Promise<Array<{ id: string; data: Record<string, unknown> }>> {\n\tconst { getEmDashCollection } = await import(\"../query.js\");\n\n\tconst queryOptions: Record<string, unknown> = {\n\t\twhere: { [taxonomyName]: termSlug },\n\t};\n\tif (options.locale !== undefined) queryOptions.locale = options.locale;\n\tconst { entries } = await getEmDashCollection(collection, queryOptions);\n\treturn entries;\n}\n\nfunction rowToTaxonomyDef(row: {\n\tid: string;\n\tname: string;\n\tlabel: string;\n\tlabel_singular: string | null;\n\thierarchical: number;\n\tcollections: string | null;\n\tlocale: string;\n\ttranslation_group: string | null;\n}): TaxonomyDef {\n\treturn {\n\t\tid: row.id,\n\t\tname: row.name,\n\t\tlabel: row.label,\n\t\tlabelSingular: row.label_singular ?? undefined,\n\t\thierarchical: row.hierarchical === 1,\n\t\tcollections: row.collections ? JSON.parse(row.collections) : [],\n\t\tlocale: row.locale,\n\t\ttranslationGroup: row.translation_group,\n\t};\n}\n\n/**\n * Build tree structure from flat terms\n */\nfunction buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): TaxonomyTerm[] {\n\t// parent_id holds the parent's translation_group, so link children by it.\n\t// Key by (locale, group): a child's parent lives in the same locale, and an\n\t// unfiltered set mixes locales whose translated siblings share a group —\n\t// keying by group alone would collide and misattach children across locales.\n\tconst byLocaleGroup = new Map<string, TaxonomyTerm>();\n\tconst nodes: TaxonomyTerm[] = [];\n\tconst roots: TaxonomyTerm[] = [];\n\n\tfor (const term of flatTerms) {\n\t\tconst node: TaxonomyTerm = {\n\t\t\tid: term.id,\n\t\t\tname: term.name,\n\t\t\tslug: term.slug,\n\t\t\tlabel: term.label,\n\t\t\tparentId: term.parent_id ?? undefined,\n\t\t\tdescription: term.data ? JSON.parse(term.data).description : undefined,\n\t\t\tchildren: [],\n\t\t\tcount: counts.get(term.translation_group ?? term.id) ?? 0,\n\t\t\tlocale: term.locale,\n\t\t\ttranslationGroup: term.translation_group,\n\t\t};\n\t\tbyLocaleGroup.set(`${term.locale}::${term.translation_group ?? term.id}`, node);\n\t\tnodes.push(node);\n\t}\n\n\tfor (const node of nodes) {\n\t\tconst parent = node.parentId\n\t\t\t? byLocaleGroup.get(`${node.locale}::${node.parentId}`)\n\t\t\t: undefined;\n\t\tif (parent) {\n\t\t\tparent.children.push(node);\n\t\t} else {\n\t\t\troots.push(node);\n\t\t}\n\t}\n\n\treturn roots;\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { a as __exportAll } from "./runner-BsquExlK.mjs";
2
- import { d as invalidateTaxonomyObjectCache } from "./object-cache-SEb2IM4Z.mjs";
1
+ import { a as __exportAll } from "./runner-CFngB4WZ.mjs";
2
+ import { d as invalidateTaxonomyObjectCache } from "./object-cache-CHbHv83-.mjs";
3
3
  import { ulid } from "ulidx";
4
4
 
5
5
  //#region src/database/repositories/taxonomy.ts
@@ -230,7 +230,7 @@ var TaxonomyRepository = class {
230
230
  */
231
231
  async countEntriesForTerms(translationGroups) {
232
232
  if (translationGroups.length === 0) return /* @__PURE__ */ new Map();
233
- const { chunks, SQL_BATCH_SIZE } = await import("./chunks-BHgcHh2C.mjs").then((n) => n.r);
233
+ const { chunks, SQL_BATCH_SIZE } = await import("./chunks-CZFiauig.mjs").then((n) => n.r);
234
234
  const counts = /* @__PURE__ */ new Map();
235
235
  for (const chunk of chunks(translationGroups, SQL_BATCH_SIZE)) {
236
236
  const rows = await this.db.selectFrom("content_taxonomies").select(["taxonomy_id", (eb) => eb.fn.count("entry_id").as("count")]).where("taxonomy_id", "in", chunk).groupBy("taxonomy_id").execute();
@@ -254,4 +254,4 @@ var TaxonomyRepository = class {
254
254
 
255
255
  //#endregion
256
256
  export { taxonomy_exports as n, TaxonomyRepository as t };
257
- //# sourceMappingURL=taxonomy-BBUlGug5.mjs.map
257
+ //# sourceMappingURL=taxonomy-Cru-he6s.mjs.map