emdash 0.33.0 → 0.34.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 (860) hide show
  1. package/dist/{adapters-BMyNSSZi.d.mts → adapters-34EN2eXO.d.mts} +26 -2
  2. package/dist/adapters-34EN2eXO.d.mts.map +1 -0
  3. package/dist/{allowed-origins-CG1pACtS.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
  4. package/dist/{allowed-origins-CG1pACtS.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
  5. package/dist/api/route-utils.d.mts +3 -3
  6. package/dist/api/route-utils.mjs +22 -21
  7. package/dist/api/route-utils.mjs.map +1 -1
  8. package/dist/api/schemas/index.d.mts +2 -2
  9. package/dist/api/schemas/index.mjs +6 -5
  10. package/dist/{api-D2hunekQ.mjs → api-DdAsirBe.mjs} +98 -407
  11. package/dist/api-DdAsirBe.mjs.map +1 -0
  12. package/dist/{api-tokens-CSND0i1z.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
  13. package/dist/{api-tokens-CSND0i1z.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
  14. package/dist/{apply-kwNEJW2l.mjs → apply-D5zfeD09.mjs} +131 -107
  15. package/dist/apply-D5zfeD09.mjs.map +1 -0
  16. package/dist/astro/index.d.mts +12 -12
  17. package/dist/astro/index.d.mts.map +1 -1
  18. package/dist/astro/index.mjs +93 -35
  19. package/dist/astro/index.mjs.map +1 -1
  20. package/dist/astro/middleware/auth.d.mts +10 -10
  21. package/dist/astro/middleware/auth.mjs +8 -8
  22. package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
  23. package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
  24. package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
  25. package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
  26. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  27. package/dist/astro/middleware/redirect.mjs +10 -7
  28. package/dist/astro/middleware/redirect.mjs.map +1 -1
  29. package/dist/astro/middleware/request-context.mjs +3 -3
  30. package/dist/astro/middleware/setup.mjs +1 -1
  31. package/dist/astro/middleware.d.mts +12 -11
  32. package/dist/astro/middleware.d.mts.map +1 -1
  33. package/dist/astro/middleware.mjs +1677 -156
  34. package/dist/astro/middleware.mjs.map +1 -1
  35. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
  36. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  37. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
  38. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  39. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  40. package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
  41. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
  42. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
  43. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  44. package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
  45. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  46. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
  47. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  48. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +17 -17
  49. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +17 -17
  50. package/dist/astro/routes/api/admin/bylines/index.mjs +17 -17
  51. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +14 -13
  52. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  53. package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
  54. package/dist/astro/routes/api/admin/comments/bulk.mjs +12 -11
  55. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  56. package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
  57. package/dist/astro/routes/api/admin/comments/index.mjs +12 -11
  58. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
  60. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
  61. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
  62. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
  63. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
  64. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
  65. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
  66. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
  67. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
  68. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
  69. package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
  70. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  71. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
  72. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
  73. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
  74. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
  75. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
  76. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
  77. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
  78. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
  79. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  80. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  81. package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
  82. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +51 -43
  83. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  84. package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
  85. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +51 -43
  86. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  87. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +47 -42
  88. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  89. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
  90. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +47 -42
  91. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  92. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
  93. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +50 -42
  94. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +50 -42
  97. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/index.mjs +47 -42
  99. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  100. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  101. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +47 -42
  102. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  103. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
  104. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +50 -42
  105. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +47 -42
  107. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  108. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
  109. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +50 -42
  110. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  112. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +51 -43
  113. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  114. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +47 -42
  115. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  116. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  117. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +51 -43
  118. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  119. package/dist/astro/routes/api/admin/plugins/updates.mjs +47 -42
  120. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  121. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +47 -42
  122. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  123. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  124. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +47 -42
  125. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  126. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
  127. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
  128. package/dist/astro/routes/api/admin/users/_id_/index.mjs +9 -8
  129. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  130. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
  131. package/dist/astro/routes/api/admin/users/index.mjs +9 -8
  132. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  133. package/dist/astro/routes/api/auth/dev-bypass.mjs +7 -7
  134. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  135. package/dist/astro/routes/api/auth/invite/complete.mjs +13 -12
  136. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  137. package/dist/astro/routes/api/auth/invite/index.mjs +10 -9
  138. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  139. package/dist/astro/routes/api/auth/invite/register-options.mjs +12 -11
  140. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  141. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  142. package/dist/astro/routes/api/auth/magic-link/send.mjs +12 -11
  143. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  144. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  145. package/dist/astro/routes/api/auth/me.mjs +10 -9
  146. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  147. package/dist/astro/routes/api/auth/mode.mjs +4 -4
  148. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
  149. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  150. package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
  151. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  152. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  153. package/dist/astro/routes/api/auth/passkey/options.mjs +14 -13
  154. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  155. package/dist/astro/routes/api/auth/passkey/register/options.mjs +12 -11
  156. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  157. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +13 -12
  158. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  159. package/dist/astro/routes/api/auth/passkey/verify.mjs +13 -12
  160. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  161. package/dist/astro/routes/api/auth/signup/complete.mjs +13 -12
  162. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  163. package/dist/astro/routes/api/auth/signup/request.mjs +12 -11
  164. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  165. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  166. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +15 -14
  167. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  168. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +14 -13
  169. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  170. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  171. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
  172. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  173. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  174. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +13 -13
  175. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
  176. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  177. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +18 -17
  178. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  179. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +18 -17
  180. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  181. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  182. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  183. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
  184. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  185. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  186. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +44 -31
  187. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  188. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  189. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
  190. package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
  191. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  192. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  193. package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
  194. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  195. package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
  196. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  197. package/dist/astro/routes/api/dashboard.mjs +10 -8
  198. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  199. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  200. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  201. package/dist/astro/routes/api/import/probe.mjs +18 -16
  202. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  203. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  204. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  205. package/dist/astro/routes/api/import/wordpress/execute.mjs +15 -15
  206. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  207. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  208. package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
  209. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  210. package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
  211. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  212. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  213. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +14 -14
  214. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  215. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +18 -16
  216. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  217. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  218. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  219. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +28 -27
  220. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  221. package/dist/astro/routes/api/manifest.mjs +5 -5
  222. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  223. package/dist/astro/routes/api/mcp.mjs +174 -72
  224. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  225. package/dist/astro/routes/api/media/_id_/confirm.mjs +13 -11
  226. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  227. package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
  228. package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
  229. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  230. package/dist/astro/routes/api/media/_id_.mjs +16 -15
  231. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  232. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  233. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  234. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  235. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  236. package/dist/astro/routes/api/media/upload-url.mjs +11 -10
  237. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  238. package/dist/astro/routes/api/media.mjs +18 -15
  239. package/dist/astro/routes/api/media.mjs.map +1 -1
  240. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +11 -11
  241. package/dist/astro/routes/api/menus/_name_/items.mjs +11 -11
  242. package/dist/astro/routes/api/menus/_name_/reorder.mjs +11 -11
  243. package/dist/astro/routes/api/menus/_name_/translations.mjs +11 -11
  244. package/dist/astro/routes/api/menus/_name_.mjs +11 -11
  245. package/dist/astro/routes/api/menus/index.mjs +11 -11
  246. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  247. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  248. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  249. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  250. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  251. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  252. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  253. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  254. package/dist/astro/routes/api/openapi.json.mjs +152 -6
  255. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  256. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  257. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
  258. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  259. package/dist/astro/routes/api/redirects/404s/index.mjs +12 -11
  260. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  261. package/dist/astro/routes/api/redirects/404s/summary.mjs +12 -11
  262. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  263. package/dist/astro/routes/api/redirects/_id_.mjs +13 -12
  264. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  265. package/dist/astro/routes/api/redirects/index.mjs +13 -12
  266. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  267. package/dist/astro/routes/api/relations/_id_/index.mjs +18 -17
  268. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  269. package/dist/astro/routes/api/relations/_id_/translations.mjs +13 -12
  270. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  271. package/dist/astro/routes/api/relations/index.mjs +18 -17
  272. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  273. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  274. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  275. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +47 -42
  276. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  277. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +47 -42
  278. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  279. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +47 -42
  280. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  281. package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
  282. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +47 -42
  283. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  284. package/dist/astro/routes/api/schema/collections/index.mjs +47 -42
  285. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  286. package/dist/astro/routes/api/schema/collections/reorder.mjs +47 -42
  287. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
  288. package/dist/astro/routes/api/schema/index.mjs +10 -9
  289. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  290. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +47 -42
  291. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  292. package/dist/astro/routes/api/schema/orphans/index.mjs +47 -42
  293. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  294. package/dist/astro/routes/api/search/enable.mjs +13 -13
  295. package/dist/astro/routes/api/search/index.mjs +12 -12
  296. package/dist/astro/routes/api/search/rebuild.mjs +13 -13
  297. package/dist/astro/routes/api/search/stats.mjs +8 -8
  298. package/dist/astro/routes/api/search/suggest.mjs +12 -12
  299. package/dist/astro/routes/api/sections/_slug_.mjs +12 -11
  300. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  301. package/dist/astro/routes/api/sections/index.mjs +12 -11
  302. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  303. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -7
  304. package/dist/astro/routes/api/settings/backups/archives/index.mjs +6 -6
  305. package/dist/astro/routes/api/settings/backups/export.mjs +7 -7
  306. package/dist/astro/routes/api/settings/backups/index.mjs +8 -8
  307. package/dist/astro/routes/api/settings/email.mjs +5 -5
  308. package/dist/astro/routes/api/settings.mjs +14 -13
  309. package/dist/astro/routes/api/settings.mjs.map +1 -1
  310. package/dist/astro/routes/api/setup/admin-verify.mjs +14 -13
  311. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  312. package/dist/astro/routes/api/setup/admin.mjs +13 -12
  313. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  314. package/dist/astro/routes/api/setup/dev-bypass.mjs +30 -28
  315. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  316. package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
  317. package/dist/astro/routes/api/setup/index.mjs +32 -30
  318. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  319. package/dist/astro/routes/api/setup/status.mjs +5 -5
  320. package/dist/astro/routes/api/snapshot.mjs +8 -8
  321. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +16 -15
  322. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
  323. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +16 -15
  324. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  325. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +16 -15
  326. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  327. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  328. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +18 -16
  329. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  330. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
  331. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
  332. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
  333. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
  334. package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
  335. package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
  336. package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
  337. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
  338. package/dist/astro/routes/api/taxonomies/index.mjs +16 -15
  339. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  340. package/dist/astro/routes/api/themes/preview.mjs +6 -6
  341. package/dist/astro/routes/api/typegen.mjs +6 -6
  342. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  343. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  344. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  345. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
  346. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  347. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +12 -11
  348. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  349. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +12 -11
  350. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  351. package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -6
  352. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  353. package/dist/astro/routes/api/widget-areas/index.mjs +12 -11
  354. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  355. package/dist/astro/routes/api/widget-components.mjs +4 -4
  356. package/dist/astro/routes/robots.txt.mjs +7 -6
  357. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  358. package/dist/astro/routes/sitemap-_collection_.xml.mjs +10 -10
  359. package/dist/astro/routes/sitemap.xml.mjs +8 -7
  360. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  361. package/dist/astro/types.d.mts +35 -17
  362. package/dist/astro/types.d.mts.map +1 -1
  363. package/dist/auth/providers/github.d.mts +1 -1
  364. package/dist/auth/providers/google.d.mts +1 -1
  365. package/dist/{authorize-qRhWQnel.mjs → authorize-1iFJzPaZ.mjs} +2 -2
  366. package/dist/{authorize-qRhWQnel.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
  367. package/dist/{backup-CtA6LDNw.mjs → backup-LHjIAVNP.mjs} +4 -4
  368. package/dist/{backup-CtA6LDNw.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
  369. package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
  370. package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
  371. package/dist/bundle-CBEXrBnb.mjs +663 -0
  372. package/dist/bundle-CBEXrBnb.mjs.map +1 -0
  373. package/dist/{byline-fields-CNc0Yq5i.mjs → byline-fields-C5S8wygO.mjs} +2 -2
  374. package/dist/{byline-fields-CNc0Yq5i.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
  375. package/dist/{byline-Cp6csL84.mjs → byline-pEcoDps8.mjs} +5 -5
  376. package/dist/{byline-Cp6csL84.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
  377. package/dist/{byline-registry-ZkPKqEwC.mjs → byline-registry-DDun8sxh.mjs} +2 -2
  378. package/dist/{byline-registry-ZkPKqEwC.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
  379. package/dist/{bylines-CEj5CdT4.mjs → bylines-BWemdX2k.mjs} +7 -7
  380. package/dist/{bylines-CEj5CdT4.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
  381. package/dist/{bylines-cf-C9lmF.mjs → bylines-DJfSfXCL.mjs} +5 -5
  382. package/dist/{bylines-cf-C9lmF.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
  383. package/dist/{cache-C9FptTFR.mjs → cache-CBJaDy_E.mjs} +3 -3
  384. package/dist/{cache-C9FptTFR.mjs.map → cache-CBJaDy_E.mjs.map} +1 -1
  385. package/dist/{challenge-store-D9VNKELo.mjs → challenge-store-CZyFVaad.mjs} +1 -1
  386. package/dist/{challenge-store-D9VNKELo.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
  387. package/dist/{chunks-DbYzib_k.mjs → chunks-DfPax1I0.mjs} +2 -2
  388. package/dist/{chunks-DbYzib_k.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
  389. package/dist/cli/index.mjs +176 -698
  390. package/dist/cli/index.mjs.map +1 -1
  391. package/dist/client/cf-access.d.mts +1 -1
  392. package/dist/client/index.d.mts +74 -4
  393. package/dist/client/index.d.mts.map +1 -1
  394. package/dist/client/index.mjs +25 -2
  395. package/dist/client/index.mjs.map +1 -1
  396. package/dist/collection-deletion-C8BugSGz.mjs +779 -0
  397. package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
  398. package/dist/{comment-BIkJ6Fil.mjs → comment-Dxlep6cE.mjs} +2 -2
  399. package/dist/{comment-BIkJ6Fil.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
  400. package/dist/{comment-reaction-D-laEceb.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
  401. package/dist/{comment-reaction-D-laEceb.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
  402. package/dist/{comments-0_w6KChA.mjs → comments-CTOuhTSX.mjs} +3 -3
  403. package/dist/{comments-0_w6KChA.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
  404. package/dist/{components-BPxrRjpE.mjs → components-CQy3lh9x.mjs} +1 -1
  405. package/dist/{components-BPxrRjpE.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
  406. package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
  407. package/dist/config-C3EBstHi.mjs.map +1 -0
  408. package/dist/{content-CJ94NEx5.mjs → content-BNfwh7xX.mjs} +509 -78
  409. package/dist/content-BNfwh7xX.mjs.map +1 -0
  410. package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
  411. package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
  412. package/dist/content-refresh-DNERP4-f.mjs +2560 -0
  413. package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
  414. package/dist/{context-C_1nrqnW.mjs → context-DRgOvSnW.mjs} +25 -17
  415. package/dist/context-DRgOvSnW.mjs.map +1 -0
  416. package/dist/{cron-DRV-UC4K.mjs → cron-Y9aLtgrA.mjs} +1 -1
  417. package/dist/{cron-DRV-UC4K.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
  418. package/dist/{dashboard-DINnNVVU.mjs → dashboard-CB8J89rJ.mjs} +44 -10
  419. package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
  420. package/dist/db/index.d.mts +4 -4
  421. package/dist/db/index.mjs +2 -2
  422. package/dist/db/index.mjs.map +1 -1
  423. package/dist/db/libsql.d.mts +1 -1
  424. package/dist/db/postgres.d.mts +1 -1
  425. package/dist/db/sqlite.d.mts +1 -1
  426. package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
  427. package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
  428. package/dist/{default-BD2FG0xV.mjs → default-DdDl3fxv.mjs} +1 -1
  429. package/dist/{default-BD2FG0xV.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
  430. package/dist/{deferred-tasks-C-WRCcKo.d.mts → deferred-tasks-BMsa9F64.d.mts} +1 -1
  431. package/dist/{deferred-tasks-C-WRCcKo.d.mts.map → deferred-tasks-BMsa9F64.d.mts.map} +1 -1
  432. package/dist/{device-flow-CTqd8mud.mjs → device-flow-CaioYGK3.mjs} +4 -4
  433. package/dist/{device-flow-CTqd8mud.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
  434. package/dist/dist-D_6CKR8z.mjs +810 -0
  435. package/dist/dist-D_6CKR8z.mjs.map +1 -0
  436. package/dist/{email-console-C6WdcWbL.mjs → email-console-BHNjIwiB.mjs} +1 -1
  437. package/dist/{email-console-C6WdcWbL.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
  438. package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
  439. package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
  440. package/dist/{error-oh_9i6bR.mjs → error-DupyJZE2.mjs} +3 -3
  441. package/dist/{error-oh_9i6bR.mjs.map → error-DupyJZE2.mjs.map} +1 -1
  442. package/dist/{errors-BucQ8VYz.mjs → errors-CLUPm7aD.mjs} +14 -2
  443. package/dist/errors-CLUPm7aD.mjs.map +1 -0
  444. package/dist/{escape-CXps5F_v.mjs → escape-C_aLKP3H.mjs} +1 -1
  445. package/dist/{escape-CXps5F_v.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
  446. package/dist/{field-defs-cache-WD82dMQq.mjs → field-defs-cache-hg6QkjV9.mjs} +4 -4
  447. package/dist/{field-defs-cache-WD82dMQq.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
  448. package/dist/{fts-manager-BITakW_R.mjs → fts-manager-CZ-5dVwJ.mjs} +76 -50
  449. package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
  450. package/dist/{hreflang-BaLCNcDx.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
  451. package/dist/{hreflang-BaLCNcDx.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
  452. package/dist/{hreflang-BdZuVBhL.mjs → hreflang-DNswoYsi.mjs} +7 -7
  453. package/dist/{hreflang-BdZuVBhL.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
  454. package/dist/{import-C-9uqyT3.mjs → import-B41u8Rot.mjs} +5 -5
  455. package/dist/{import-C-9uqyT3.mjs.map → import-B41u8Rot.mjs.map} +1 -1
  456. package/dist/{index-M37iz29I.d.mts → index-C3Y-rsyN.d.mts} +226 -73
  457. package/dist/index-C3Y-rsyN.d.mts.map +1 -0
  458. package/dist/{index-MGw47uWq.d.mts → index-Cof4-Fab.d.mts} +5 -10
  459. package/dist/index-Cof4-Fab.d.mts.map +1 -0
  460. package/dist/index.d.mts +20 -19
  461. package/dist/index.mjs +73 -67
  462. package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
  463. package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
  464. package/dist/{load-DV1DZuUJ.mjs → load-DmvJrtxS.mjs} +2 -2
  465. package/dist/{load-DV1DZuUJ.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
  466. package/dist/{loader-CGugY7Zl.mjs → loader-DLBqRiGP.mjs} +95 -53
  467. package/dist/loader-DLBqRiGP.mjs.map +1 -0
  468. package/dist/{manifest-schema-Da7kj23y.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
  469. package/dist/{manifest-schema-Da7kj23y.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
  470. package/dist/media/image-endpoint.mjs +1 -1
  471. package/dist/media/index.d.mts +1 -1
  472. package/dist/media/index.mjs +3 -2
  473. package/dist/media/index.mjs.map +1 -1
  474. package/dist/media/local-runtime.d.mts +12 -12
  475. package/dist/media/local-runtime.mjs +6 -5
  476. package/dist/media/local-runtime.mjs.map +1 -1
  477. package/dist/{media-OhYL-7Io.mjs → media-BWWWb8kg.mjs} +92 -3
  478. package/dist/media-BWWWb8kg.mjs.map +1 -0
  479. package/dist/{media-BP7S-nHT.mjs → media-CzhAn2--.mjs} +2 -2
  480. package/dist/{media-BP7S-nHT.mjs.map → media-CzhAn2--.mjs.map} +1 -1
  481. package/dist/{media-allowlist-D-peH1H5.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
  482. package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
  483. package/dist/{media-upload-DtrwU2B0.mjs → media-upload-9yas9zGH.mjs} +10 -8
  484. package/dist/{media-upload-DtrwU2B0.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
  485. package/dist/{media-url-DYs_YLOx.mjs → media-url-BvoX88FA.mjs} +1 -1
  486. package/dist/{media-url-DYs_YLOx.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
  487. package/dist/{media-usage-BlxfTkN0.mjs → media-usage-De3qvwjc.mjs} +137 -60
  488. package/dist/media-usage-De3qvwjc.mjs.map +1 -0
  489. package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
  490. package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
  491. package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
  492. package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
  493. package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
  494. package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
  495. package/dist/{menus-Y54RDPIN.mjs → menus-CC-yn1F9.mjs} +351 -95
  496. package/dist/menus-CC-yn1F9.mjs.map +1 -0
  497. package/dist/{menus-BcQWxeXu.mjs → menus-fxvEDYE6.mjs} +3 -3
  498. package/dist/{menus-BcQWxeXu.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
  499. package/dist/{mode-BWyTBe3I.mjs → mode-Bpe-tNNy.mjs} +1 -1
  500. package/dist/{mode-BWyTBe3I.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
  501. package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
  502. package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
  503. package/dist/{oauth-authorization-ClvktmrL.mjs → oauth-authorization-BXO1MKej.mjs} +4 -4
  504. package/dist/{oauth-authorization-ClvktmrL.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
  505. package/dist/{oauth-clients-8ao-U66l.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
  506. package/dist/{oauth-clients-8ao-U66l.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
  507. package/dist/{oauth-state-store-B1ofdTiQ.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
  508. package/dist/{oauth-state-store-B1ofdTiQ.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
  509. package/dist/{oauth-user-lookup-CscEuIiR.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
  510. package/dist/{oauth-user-lookup-CscEuIiR.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
  511. package/dist/object-cache/memory.d.mts +1 -1
  512. package/dist/{options-CA4eVX6P.d.mts → options-DRe1pIBL.d.mts} +4 -3
  513. package/dist/options-DRe1pIBL.d.mts.map +1 -0
  514. package/dist/page/index.d.mts +2 -2
  515. package/dist/{parse-CJKNuewM.mjs → parse-DFbuc-Fh.mjs} +2 -2
  516. package/dist/{parse-CJKNuewM.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
  517. package/dist/{passkey-config-D-zrzMJI.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
  518. package/dist/{passkey-config-D-zrzMJI.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
  519. package/dist/{placeholder-DaeNS-VG.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
  520. package/dist/{placeholder-DaeNS-VG.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
  521. package/dist/plugin-types.d.mts +8 -1
  522. package/dist/plugin-types.d.mts.map +1 -1
  523. package/dist/plugin-utils.d.mts +10 -10
  524. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  525. package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
  526. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  527. package/dist/{portable-text-B_kqBmXB.mjs → portable-text-DhKbDAGr.mjs} +1 -1
  528. package/dist/{portable-text-B_kqBmXB.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
  529. package/dist/{preview-BU6PaLpM.mjs → preview-PW-nOnq4.mjs} +2 -2
  530. package/dist/{preview-BU6PaLpM.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
  531. package/dist/{public-url-Ox9oOlgp.mjs → public-url-CWoFhQT8.mjs} +1 -1
  532. package/dist/{public-url-Ox9oOlgp.mjs.map → public-url-CWoFhQT8.mjs.map} +1 -1
  533. package/dist/{query-C5w7PsRQ.mjs → query-BgjfiLUQ.mjs} +31 -49
  534. package/dist/query-BgjfiLUQ.mjs.map +1 -0
  535. package/dist/{query-rC9HKv1N.mjs → query-vgZNwIhh.mjs} +3 -3
  536. package/dist/{query-rC9HKv1N.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
  537. package/dist/{rate-limit-kug6q99V.mjs → rate-limit-L5anykgo.mjs} +2 -2
  538. package/dist/{rate-limit-kug6q99V.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
  539. package/dist/{redirect-DIJxXKOG.mjs → redirect-COsS6SWJ.mjs} +2 -2
  540. package/dist/{redirect-DIJxXKOG.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
  541. package/dist/{redirect-Egf2R9yP.mjs → redirect-KBNE3UEs.mjs} +1 -1
  542. package/dist/{redirect-Egf2R9yP.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
  543. package/dist/{redirects-Bzm812bY.mjs → redirects-CufRQt0Q.mjs} +4 -4
  544. package/dist/{redirects-Bzm812bY.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
  545. package/dist/{redirects-G8wAhWAf.mjs → redirects-cV9g7V-0.mjs} +133 -14
  546. package/dist/redirects-cV9g7V-0.mjs.map +1 -0
  547. package/dist/registry-C581V4TP.mjs +1275 -0
  548. package/dist/registry-C581V4TP.mjs.map +1 -0
  549. package/dist/{relation-Cbe3Sd3m.mjs → relation-DtmAQH7s.mjs} +3 -3
  550. package/dist/{relation-Cbe3Sd3m.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
  551. package/dist/{relations-Bw_E1TXo.mjs → relations-BBApI4e2.mjs} +3 -3
  552. package/dist/{relations-Bw_E1TXo.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
  553. package/dist/{relations-qHwpUe66.mjs → relations-DICSyfsh.mjs} +6 -6
  554. package/dist/{relations-qHwpUe66.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
  555. package/dist/{relations-G2ta7GIl.d.mts → relations-DItI3MN9.d.mts} +369 -35
  556. package/dist/relations-DItI3MN9.d.mts.map +1 -0
  557. package/dist/request-context.d.mts +1 -1
  558. package/dist/{request-meta-Df-jFbAd.mjs → request-meta-DgZJh3zG.mjs} +2 -2
  559. package/dist/{request-meta-Df-jFbAd.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
  560. package/dist/{resolve-DZzG4HqM.mjs → resolve-DOhVEvty.mjs} +2 -2
  561. package/dist/{resolve-DZzG4HqM.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
  562. package/dist/{runner-BFTrkYhu.mjs → runner-BiDvq5dv.mjs} +1157 -254
  563. package/dist/runner-BiDvq5dv.mjs.map +1 -0
  564. package/dist/{runner-BaADli8l.d.mts → runner-CgQbELYC.d.mts} +2 -2
  565. package/dist/{runner-BaADli8l.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
  566. package/dist/runtime.d.mts +11 -11
  567. package/dist/runtime.mjs +4 -3
  568. package/dist/runtime.mjs.map +1 -1
  569. package/dist/{schema-CiQXr6_F.mjs → schema-BFyTg38T.mjs} +3 -3
  570. package/dist/{schema-CiQXr6_F.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
  571. package/dist/schema-CeQSNwB3.mjs +390 -0
  572. package/dist/schema-CeQSNwB3.mjs.map +1 -0
  573. package/dist/{scopes-CNJVT5i5.mjs → scopes-BVeuhGBE.mjs} +3 -3
  574. package/dist/{scopes-CNJVT5i5.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
  575. package/dist/{search-FZhx5dDU.mjs → search-BpuPKZb0.mjs} +25 -15
  576. package/dist/search-BpuPKZb0.mjs.map +1 -0
  577. package/dist/{secrets-DLd8RXee.mjs → secrets-dtYs2hDo.mjs} +3 -3
  578. package/dist/{secrets-DLd8RXee.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
  579. package/dist/{sections-BrIrMUCn.mjs → sections-COctk6hf.mjs} +3 -3
  580. package/dist/{sections-BrIrMUCn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
  581. package/dist/seed/index.d.mts +2 -2
  582. package/dist/seed/index.mjs +24 -22
  583. package/dist/seo/index.d.mts +3 -3
  584. package/dist/seo/index.mjs +16 -15
  585. package/dist/seo/index.mjs.map +1 -1
  586. package/dist/{seo-CobNfOvb.mjs → seo-AFpVvTqr.mjs} +5 -3
  587. package/dist/seo-AFpVvTqr.mjs.map +1 -0
  588. package/dist/{seo-DTHRntNC.mjs → seo-CvDxfvFb.mjs} +2 -2
  589. package/dist/{seo-DTHRntNC.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
  590. package/dist/{service-B-pYGPgR.mjs → service-xMiMSTsR.mjs} +3 -3
  591. package/dist/{service-B-pYGPgR.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
  592. package/dist/{session-user-BvM9-zNl.mjs → session-user-BzVemLkw.mjs} +1 -1
  593. package/dist/{session-user-BvM9-zNl.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
  594. package/dist/{settings-kJhGiQ4z.mjs → settings-B372MV0p.mjs} +4 -4
  595. package/dist/{settings-kJhGiQ4z.mjs.map → settings-B372MV0p.mjs.map} +1 -1
  596. package/dist/{settings-DPHau-WH.mjs → settings-BE9aqn_9.mjs} +3 -3
  597. package/dist/{settings-DPHau-WH.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
  598. package/dist/{setup-complete-u50Qm8rY.mjs → setup-complete-DF7TAfD5.mjs} +1 -1
  599. package/dist/{setup-complete-u50Qm8rY.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
  600. package/dist/{setup-nonce-DCjiiOD2.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
  601. package/dist/{setup-nonce-DCjiiOD2.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
  602. package/dist/{single-flight-cache-DW3w11Pu.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
  603. package/dist/{single-flight-cache-DW3w11Pu.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
  604. package/dist/{site-url-I-lJ5kma.mjs → site-url-Bowsi9T-.mjs} +2 -2
  605. package/dist/{site-url-I-lJ5kma.mjs.map → site-url-Bowsi9T-.mjs.map} +1 -1
  606. package/dist/slugify-B994zkXC.mjs +17 -0
  607. package/dist/slugify-B994zkXC.mjs.map +1 -0
  608. package/dist/{snapshot-I_fjLDaD.mjs → snapshot-B5h6aPMh.mjs} +1 -1
  609. package/dist/{snapshot-I_fjLDaD.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
  610. package/dist/{ssrf-CTa6v21i.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
  611. package/dist/{ssrf-CTa6v21i.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
  612. package/dist/{state-CQvoefYP.mjs → state-DYwPue40.mjs} +1 -1
  613. package/dist/{state-CQvoefYP.mjs.map → state-DYwPue40.mjs.map} +1 -1
  614. package/dist/{status-CIsmlC1E.mjs → status-CWVYpbD8.mjs} +1 -1
  615. package/dist/{status-CIsmlC1E.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
  616. package/dist/storage/local.d.mts +1 -1
  617. package/dist/storage/local.mjs +1 -1
  618. package/dist/storage/s3.d.mts +1 -1
  619. package/dist/storage/s3.mjs +1 -1
  620. package/dist/{taxonomies-CLN1N92E.mjs → taxonomies-B0o9ExNV.mjs} +75 -64
  621. package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
  622. package/dist/{taxonomies-CKuaZER5.mjs → taxonomies-Dk3MAklL.mjs} +280 -56
  623. package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
  624. package/dist/{taxonomy-pOrB2kZe.mjs → taxonomy-CPeM8d2o.mjs} +131 -78
  625. package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
  626. package/dist/{tokens-DagaJGK8.mjs → tokens-C39klX8R.mjs} +2 -2
  627. package/dist/{tokens-DagaJGK8.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
  628. package/dist/{transport-DVsa5YYo.mjs → transport--CfOflrp.mjs} +1 -1
  629. package/dist/{transport-DVsa5YYo.mjs.map → transport--CfOflrp.mjs.map} +1 -1
  630. package/dist/{transport-CAwTaTp_.d.mts → transport-B_rfeU-a.d.mts} +1 -1
  631. package/dist/{transport-CAwTaTp_.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
  632. package/dist/{trusted-proxy-D20_7fyX.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
  633. package/dist/{trusted-proxy-D20_7fyX.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
  634. package/dist/{types-NBI3rF-_.mjs → types-75KIqzL9.mjs} +18 -2
  635. package/dist/types-75KIqzL9.mjs.map +1 -0
  636. package/dist/{types-CbUcuxxc.d.mts → types-BPuloofb.d.mts} +1 -1
  637. package/dist/{types-CbUcuxxc.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
  638. package/dist/{types-BjxTKxrZ.mjs → types-BW1syrlw.mjs} +1 -1
  639. package/dist/{types-BjxTKxrZ.mjs.map → types-BW1syrlw.mjs.map} +1 -1
  640. package/dist/{types-BnFP1M3J.d.mts → types-C3_AbkDP.d.mts} +32 -10
  641. package/dist/types-C3_AbkDP.d.mts.map +1 -0
  642. package/dist/{types-Dc4ntQjQ.d.mts → types-CAGdePf3.d.mts} +124 -1
  643. package/dist/types-CAGdePf3.d.mts.map +1 -0
  644. package/dist/{types-DLG4IFEQ.d.mts → types-CFUfyt1s.d.mts} +1 -1
  645. package/dist/{types-DLG4IFEQ.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
  646. package/dist/{types-LoagAGzH.d.mts → types-CPx2lIGo.d.mts} +1 -1
  647. package/dist/{types-LoagAGzH.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
  648. package/dist/{types-Cd8iuS1J.mjs → types-CQPWhPxE.mjs} +9 -12
  649. package/dist/types-CQPWhPxE.mjs.map +1 -0
  650. package/dist/{types-Cz8hNch2.d.mts → types-Cs-TFLNf.d.mts} +25 -5
  651. package/dist/types-Cs-TFLNf.d.mts.map +1 -0
  652. package/dist/types-CtzytVXn.mjs.map +1 -1
  653. package/dist/{types-BGWPMiBO.d.mts → types-D4_1Ga4L.d.mts} +44 -5
  654. package/dist/types-D4_1Ga4L.d.mts.map +1 -0
  655. package/dist/{types-C8oz_ayJ.d.mts → types-DefV3OiO.d.mts} +1 -1
  656. package/dist/{types-C8oz_ayJ.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
  657. package/dist/{types-BK6U01Ro.d.mts → types-X0MThr7x.d.mts} +1 -1
  658. package/dist/{types-BK6U01Ro.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
  659. package/dist/{upload-attempts-D8QAFLUJ.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
  660. package/dist/{upload-attempts-D8QAFLUJ.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
  661. package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
  662. package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
  663. package/dist/{user-DBrq4UVq.mjs → user-sHgJ62SP.mjs} +3 -3
  664. package/dist/{user-DBrq4UVq.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
  665. package/dist/{utils-hLzi_2Fn.mjs → utils-D4vD_Y98.mjs} +3 -3
  666. package/dist/{utils-hLzi_2Fn.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
  667. package/dist/{validate-mKo3RxBM.mjs → validate-BkqIG_Uj.mjs} +24 -5
  668. package/dist/validate-BkqIG_Uj.mjs.map +1 -0
  669. package/dist/{validate-C3oSmA7F.d.mts → validate-DeGtBN84.d.mts} +15 -7
  670. package/dist/validate-DeGtBN84.d.mts.map +1 -0
  671. package/dist/{validation-BgpucsvF.mjs → validation-CiJb_X5o.mjs} +11 -10
  672. package/dist/{validation-BgpucsvF.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
  673. package/dist/version-x0NjTScG.mjs +7 -0
  674. package/dist/{version-C4PnpmoV.mjs.map → version-x0NjTScG.mjs.map} +1 -1
  675. package/dist/{widgets-CJJtkiKh.mjs → widgets-Cwzvww0W.mjs} +3 -3
  676. package/dist/{widgets-CJJtkiKh.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
  677. package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
  678. package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
  679. package/dist/{wxr-taxonomies-9PPSy8X6.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
  680. package/dist/{wxr-taxonomies-9PPSy8X6.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
  681. package/dist/{zod-generator-CG64Hnbh.mjs → zod-generator-D11MhxG4.mjs} +52 -5
  682. package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
  683. package/package.json +16 -8
  684. package/src/api/errors.ts +12 -0
  685. package/src/api/handlers/content.ts +127 -29
  686. package/src/api/handlers/dashboard.ts +12 -2
  687. package/src/api/handlers/index.ts +16 -0
  688. package/src/api/handlers/manifest.ts +2 -0
  689. package/src/api/handlers/media-allowlist.ts +13 -2
  690. package/src/api/handlers/media-usage-work.ts +188 -0
  691. package/src/api/handlers/media-usage.ts +5 -3
  692. package/src/api/handlers/schema.ts +9 -3
  693. package/src/api/handlers/seo.ts +5 -2
  694. package/src/api/handlers/taxonomies.ts +345 -82
  695. package/src/api/media-usage-write-fence.ts +64 -0
  696. package/src/api/openapi/document.ts +199 -0
  697. package/src/api/schemas/common.ts +3 -1
  698. package/src/api/schemas/content.ts +122 -1
  699. package/src/api/schemas/media-usage.ts +133 -0
  700. package/src/api/schemas/schema.ts +78 -3
  701. package/src/api/schemas/taxonomies.ts +40 -8
  702. package/src/api/types.ts +1 -0
  703. package/src/astro/integration/index.ts +55 -42
  704. package/src/astro/integration/routes.ts +34 -4
  705. package/src/astro/integration/runtime.ts +15 -0
  706. package/src/astro/integration/virtual-modules.ts +25 -1
  707. package/src/astro/integration/vite-config.ts +16 -1
  708. package/src/astro/middleware/media-usage-write-fence.ts +30 -0
  709. package/src/astro/middleware/redirect.ts +12 -2
  710. package/src/astro/middleware.ts +75 -8
  711. package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
  712. package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
  713. package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
  714. package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
  715. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
  716. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
  717. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
  718. package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
  719. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
  720. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
  721. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  722. package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
  723. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
  724. package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
  725. package/src/astro/routes/api/mcp.ts +1 -0
  726. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
  727. package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
  728. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
  729. package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
  730. package/src/astro/routes/api/taxonomies/[name].ts +102 -0
  731. package/src/astro/types.ts +25 -4
  732. package/src/cleanup.ts +6 -12
  733. package/src/cli/commands/doctor.ts +193 -6
  734. package/src/cli/commands/export-seed.ts +4 -1
  735. package/src/client/index.ts +119 -0
  736. package/src/components/Block.astro +12 -0
  737. package/src/components/InlinePortableTextEditor.tsx +89 -1
  738. package/src/content/converters/mark-safety.ts +120 -0
  739. package/src/content/converters/portable-text-to-prosemirror.ts +25 -16
  740. package/src/content/converters/prosemirror-to-portable-text.ts +12 -2
  741. package/src/content-list-query.ts +29 -0
  742. package/src/database/migrations/060_collection_admin_config.ts +16 -0
  743. package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
  744. package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
  745. package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
  746. package/src/database/migrations/064_fts_plain_text.ts +253 -0
  747. package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
  748. package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
  749. package/src/database/migrations/067_indexed_content_fields.ts +39 -0
  750. package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
  751. package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
  752. package/src/database/migrations/070_collection_routable.ts +18 -0
  753. package/src/database/migrations/runner.ts +22 -0
  754. package/src/database/repositories/content.ts +861 -96
  755. package/src/database/repositories/media-usage-work.ts +848 -0
  756. package/src/database/repositories/media-usage.ts +1502 -138
  757. package/src/database/repositories/revision.ts +17 -6
  758. package/src/database/repositories/taxonomy.ts +234 -103
  759. package/src/database/repositories/types.ts +51 -2
  760. package/src/database/types.ts +134 -16
  761. package/src/db/adapters.ts +28 -0
  762. package/src/db/index.ts +3 -0
  763. package/src/emdash-runtime.ts +227 -20
  764. package/src/fields/file.ts +7 -4
  765. package/src/fields/image.ts +8 -1
  766. package/src/fields/types.ts +18 -5
  767. package/src/i18n/config.ts +32 -0
  768. package/src/i18n/repair-locale-casing.ts +0 -14
  769. package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
  770. package/src/import/wxr-taxonomies.ts +7 -47
  771. package/src/index.ts +25 -2
  772. package/src/loader.ts +119 -65
  773. package/src/mcp/server.ts +242 -34
  774. package/src/media/usage/activation.ts +626 -0
  775. package/src/media/usage/capture-triggers.ts +543 -0
  776. package/src/media/usage/cleanup.ts +305 -0
  777. package/src/media/usage/collection-deletion-processor.ts +356 -0
  778. package/src/media/usage/collection-deletion.ts +836 -0
  779. package/src/media/usage/content-fields.ts +40 -3
  780. package/src/media/usage/content-refresh.ts +356 -47
  781. package/src/media/usage/content-repair.ts +180 -61
  782. package/src/media/usage/content-snapshots.ts +85 -105
  783. package/src/media/usage/projection-fingerprint.ts +129 -0
  784. package/src/media/usage/reconciliation-processor.ts +372 -0
  785. package/src/media/usage/reconciliation.ts +1117 -0
  786. package/src/media/usage/source-key.ts +4 -0
  787. package/src/media/usage/types.ts +2 -0
  788. package/src/media/usage/work-processor.ts +248 -0
  789. package/src/menus/index.ts +246 -141
  790. package/src/plugin-types.ts +8 -0
  791. package/src/plugins/adapt-sandbox-entry.ts +24 -5
  792. package/src/plugins/context.ts +24 -7
  793. package/src/plugins/index.ts +9 -0
  794. package/src/plugins/routes.ts +43 -1
  795. package/src/plugins/sandbox/index.ts +10 -1
  796. package/src/plugins/sandbox/types.ts +78 -1
  797. package/src/plugins/scheduler/node.ts +13 -1
  798. package/src/plugins/scheduler/types.ts +2 -0
  799. package/src/plugins/types.ts +39 -2
  800. package/src/query.ts +29 -37
  801. package/src/scheduler-health.ts +46 -0
  802. package/src/schema/registry.ts +777 -255
  803. package/src/schema/types.ts +50 -8
  804. package/src/schema/url-pattern.ts +33 -0
  805. package/src/schema/zod-generator.ts +77 -5
  806. package/src/search/fts-manager.ts +117 -76
  807. package/src/search/query.ts +32 -11
  808. package/src/search/types.ts +2 -0
  809. package/src/seed/apply.ts +68 -15
  810. package/src/seed/types.ts +11 -3
  811. package/src/seed/validate.ts +52 -2
  812. package/src/taxonomies/index.ts +77 -75
  813. package/src/taxonomies/term-counts.ts +21 -8
  814. package/src/utils/slugify.ts +1 -29
  815. package/src/virtual-modules.d.ts +16 -0
  816. package/dist/adapters-BMyNSSZi.d.mts.map +0 -1
  817. package/dist/api-D2hunekQ.mjs.map +0 -1
  818. package/dist/apply-kwNEJW2l.mjs.map +0 -1
  819. package/dist/config-DyF3OKg_.mjs.map +0 -1
  820. package/dist/content-CJ94NEx5.mjs.map +0 -1
  821. package/dist/content-refresh-Xw7aKmou.mjs +0 -1629
  822. package/dist/content-refresh-Xw7aKmou.mjs.map +0 -1
  823. package/dist/context-C_1nrqnW.mjs.map +0 -1
  824. package/dist/dashboard-DINnNVVU.mjs.map +0 -1
  825. package/dist/errors-BucQ8VYz.mjs.map +0 -1
  826. package/dist/fts-manager-BITakW_R.mjs.map +0 -1
  827. package/dist/index-M37iz29I.d.mts.map +0 -1
  828. package/dist/index-MGw47uWq.d.mts.map +0 -1
  829. package/dist/loader-CGugY7Zl.mjs.map +0 -1
  830. package/dist/media-OhYL-7Io.mjs.map +0 -1
  831. package/dist/media-allowlist-D-peH1H5.mjs.map +0 -1
  832. package/dist/media-usage-BlxfTkN0.mjs.map +0 -1
  833. package/dist/menus-Y54RDPIN.mjs.map +0 -1
  834. package/dist/options-CA4eVX6P.d.mts.map +0 -1
  835. package/dist/query-C5w7PsRQ.mjs.map +0 -1
  836. package/dist/redirects-G8wAhWAf.mjs.map +0 -1
  837. package/dist/registry-Dbodb4Uo.mjs +0 -840
  838. package/dist/registry-Dbodb4Uo.mjs.map +0 -1
  839. package/dist/relations-G2ta7GIl.d.mts.map +0 -1
  840. package/dist/runner-BFTrkYhu.mjs.map +0 -1
  841. package/dist/search-FZhx5dDU.mjs.map +0 -1
  842. package/dist/seo-CobNfOvb.mjs.map +0 -1
  843. package/dist/slugify-C_tqlU4G.mjs +0 -30
  844. package/dist/slugify-C_tqlU4G.mjs.map +0 -1
  845. package/dist/taxonomies-CKuaZER5.mjs.map +0 -1
  846. package/dist/taxonomies-CLN1N92E.mjs.map +0 -1
  847. package/dist/taxonomy-pOrB2kZe.mjs.map +0 -1
  848. package/dist/types-BGWPMiBO.d.mts.map +0 -1
  849. package/dist/types-BnFP1M3J.d.mts.map +0 -1
  850. package/dist/types-Cd8iuS1J.mjs.map +0 -1
  851. package/dist/types-Cz8hNch2.d.mts.map +0 -1
  852. package/dist/types-Dc4ntQjQ.d.mts.map +0 -1
  853. package/dist/types-NBI3rF-_.mjs.map +0 -1
  854. package/dist/validate-C3oSmA7F.d.mts.map +0 -1
  855. package/dist/validate-mKo3RxBM.mjs.map +0 -1
  856. package/dist/version-C4PnpmoV.mjs +0 -7
  857. package/dist/zod-generator-CG64Hnbh.mjs.map +0 -1
  858. /package/dist/{api-tokens-Dai9dND0.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
  859. /package/dist/{ssrf-Cj568Dgs.mjs → ssrf-DpXgKsna.mjs} +0 -0
  860. /package/dist/{types-DKuWPYV5.mjs → types-CP1CflWf.mjs} +0 -0
@@ -1,7 +1,9 @@
1
1
  import { sql, type Kysely } from "kysely";
2
2
  import { ulid } from "ulidx";
3
3
 
4
+ import type { ContentFieldFilterValue, ContentFieldFilters } from "../../content-list-query.js";
4
5
  import { invalidateCollectionCache } from "../../object-cache/index.js";
6
+ import { isIndexableFieldType, type FieldType } from "../../schema/types.js";
5
7
  import { buildFtsPrefixMatch, buildSlugGlobPrefix } from "../../search/match.js";
6
8
  import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
7
9
  import { isMissingTableError } from "../../utils/db-errors.js";
@@ -16,10 +18,13 @@ import type {
16
18
  FindManyResult,
17
19
  ContentItem,
18
20
  ContentDateField,
21
+ ContentBylineFilter,
19
22
  } from "./types.js";
20
23
  import {
24
+ ContentCollectionNotFoundError,
21
25
  ContentMutationConflictError,
22
26
  EmDashValidationError,
27
+ InvalidCursorError,
23
28
  ScheduledNotDueError,
24
29
  encodeCursor,
25
30
  decodeCursor,
@@ -28,9 +33,25 @@ import {
28
33
  // Regex pattern for ULID validation
29
34
  const ULID_PATTERN = /^[0-9A-Z]{26}$/;
30
35
  const SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
36
+ const MAX_DRAFT_STAGE_ATTEMPTS = 32;
31
37
 
32
38
  // LIKE wildcards that must be escaped so user search input is matched literally.
33
39
  const LIKE_WILDCARD_RE = /[\\%_]/g;
40
+ const MAX_INDEXED_FIELD_FILTERS = 20;
41
+ const MAX_IN_FILTER_VALUES = SQL_BATCH_SIZE;
42
+ const MAX_FILTER_STRING_LENGTH = 2048;
43
+
44
+ type NormalizedFilterScalar = string | number;
45
+
46
+ type ResolvedFieldFilter =
47
+ | { column: string; kind: "null" }
48
+ | { column: string; kind: "exact"; value: NormalizedFilterScalar }
49
+ | { column: string; kind: "in"; values: NormalizedFilterScalar[] }
50
+ | {
51
+ column: string;
52
+ kind: "range";
53
+ bounds: Partial<Record<"gt" | "gte" | "lt" | "lte", NormalizedFilterScalar>>;
54
+ };
34
55
 
35
56
  function nullableColumnMatch(column: string, value: string | null): ReturnType<typeof sql> {
36
57
  validateIdentifier(column, "content column");
@@ -69,6 +90,31 @@ function matchesPublication(
69
90
  );
70
91
  }
71
92
 
93
+ function matchesLifecyclePublication(
94
+ observed: ContentItem,
95
+ existing: ContentItem,
96
+ liveRevisionId: string,
97
+ publishedAt: string,
98
+ updatedAt: string,
99
+ ): boolean {
100
+ if (
101
+ observed.version !== existing.version + 1 ||
102
+ observed.status !== "published" ||
103
+ observed.slug !== existing.slug ||
104
+ observed.liveRevisionId !== liveRevisionId ||
105
+ observed.draftRevisionId !== null ||
106
+ observed.scheduledAt !== null ||
107
+ observed.publishedAt !== publishedAt ||
108
+ observed.updatedAt !== updatedAt
109
+ ) {
110
+ return false;
111
+ }
112
+
113
+ return Object.entries(existing.data).every(([key, value]) =>
114
+ sameStoredValue(observed.data[key], value),
115
+ );
116
+ }
117
+
72
118
  function matchesPublicationFence(observed: ContentItem, existing: ContentItem): boolean {
73
119
  return (
74
120
  observed.version === existing.version &&
@@ -85,6 +131,51 @@ function isConfirmedStatementFailure(error: unknown): boolean {
85
131
  return typeof code === "string" && (code.startsWith("SQLITE_") || SQLSTATE_PATTERN.test(code));
86
132
  }
87
133
 
134
+ interface ResolvedOrderField {
135
+ column: string;
136
+ indexedCustomField: boolean;
137
+ }
138
+
139
+ type IndexedOrderValue = string | number | null;
140
+
141
+ interface IndexedFieldCursorPayload {
142
+ version: 1;
143
+ field: string;
144
+ value: IndexedOrderValue;
145
+ }
146
+
147
+ function encodeIndexedFieldCursor(field: string, value: IndexedOrderValue, id: string): string {
148
+ const payload: IndexedFieldCursorPayload = { version: 1, field, value };
149
+ return encodeCursor(JSON.stringify(payload), id);
150
+ }
151
+
152
+ function decodeIndexedFieldCursor(
153
+ cursor: string,
154
+ field: string,
155
+ ): { value: IndexedOrderValue; id: string } {
156
+ const { orderValue, id } = decodeCursor(cursor);
157
+ let payload: unknown;
158
+ try {
159
+ payload = JSON.parse(orderValue);
160
+ } catch {
161
+ throw new InvalidCursorError(cursor);
162
+ }
163
+
164
+ if (payload === null || typeof payload !== "object") {
165
+ throw new InvalidCursorError(cursor);
166
+ }
167
+ const candidate = payload as Partial<IndexedFieldCursorPayload>;
168
+ const validValue =
169
+ candidate.value === null ||
170
+ typeof candidate.value === "string" ||
171
+ typeof candidate.value === "number";
172
+ if (candidate.version !== 1 || candidate.field !== field || !validValue) {
173
+ throw new InvalidCursorError(cursor);
174
+ }
175
+
176
+ return { value: candidate.value as IndexedOrderValue, id };
177
+ }
178
+
88
179
  /**
89
180
  * Whitelist mapping a public date-filter field to its physical column. Keeping
90
181
  * this separate from `mapOrderField` makes the filterable set explicit and
@@ -97,6 +188,29 @@ const DATE_FILTER_COLUMNS: Record<ContentDateField, "created_at" | "updated_at"
97
188
  publishedAt: "published_at",
98
189
  };
99
190
 
191
+ /**
192
+ * Built-in sort fields → their physical columns. A closed set that blocks
193
+ * sorting by arbitrary columns; per-collection fields are allowed
194
+ * separately via `mapOrderField`'s `sortableExtras`.
195
+ */
196
+ const ORDER_FIELD_COLUMNS: Record<string, string> = {
197
+ createdAt: "created_at",
198
+ updatedAt: "updated_at",
199
+ publishedAt: "published_at",
200
+ scheduledAt: "scheduled_at",
201
+ deletedAt: "deleted_at",
202
+ title: "title",
203
+ name: "name",
204
+ slug: "slug",
205
+ status: "status",
206
+ locale: "locale",
207
+ };
208
+
209
+ /** True when `field` maps to a system column and needs no per-collection resolution. */
210
+ export function isSystemOrderField(field: string): boolean {
211
+ return field in ORDER_FIELD_COLUMNS;
212
+ }
213
+
100
214
  /**
101
215
  * System columns that exist in every ec_* table
102
216
  */
@@ -144,6 +258,16 @@ function serializeValue(value: unknown): unknown {
144
258
  return value;
145
259
  }
146
260
 
261
+ function writableContentData(data: Record<string, unknown>): Record<string, unknown> {
262
+ const writable: Record<string, unknown> = {};
263
+ for (const [key, value] of Object.entries(data)) {
264
+ if (SYSTEM_COLUMNS.has(key)) continue;
265
+ validateIdentifier(key, "content field name");
266
+ writable[key] = value;
267
+ }
268
+ return writable;
269
+ }
270
+
147
271
  /**
148
272
  * Deserialize a value from database storage
149
273
  * Attempts to parse JSON strings that look like objects/arrays
@@ -185,7 +309,7 @@ export class ContentRepository {
185
309
  * Create a new content item
186
310
  */
187
311
  async create(input: CreateContentInput): Promise<ContentItem> {
188
- const id = ulid();
312
+ const id = input.id ?? ulid();
189
313
  const now = new Date().toISOString();
190
314
 
191
315
  const {
@@ -283,7 +407,7 @@ export class ContentRepository {
283
407
  * (optionally scoped to a locale) and appends a numeric suffix (`-1`,
284
408
  * `-2`, etc.) on collision to guarantee uniqueness.
285
409
  *
286
- * Returns `null` if `baseSlug` is empty after slugification.
410
+ * Returns null when slug normalization cannot produce a value.
287
411
  */
288
412
  async generateUniqueSlug(type: string, text: string, locale?: string): Promise<string | null> {
289
413
  const baseSlug = slugify(text);
@@ -557,7 +681,13 @@ export class ContentRepository {
557
681
  // Determine ordering
558
682
  const orderField = options.orderBy?.field || "createdAt";
559
683
  const orderDirection = options.orderBy?.direction || "desc";
560
- const dbField = this.mapOrderField(orderField);
684
+ const resolvedOrderField = await this.resolveOrderField(
685
+ type,
686
+ orderField,
687
+ options.sortableExtras,
688
+ );
689
+ const dbField = resolvedOrderField.column;
690
+ const resolvedFieldFilters = await this.resolveFieldFilters(type, options.where?.fieldFilters);
561
691
 
562
692
  // Validate order direction to prevent injection
563
693
  const safeOrderDirection = orderDirection.toLowerCase() === "asc" ? "ASC" : "DESC";
@@ -584,40 +714,90 @@ export class ContentRepository {
584
714
 
585
715
  query = this.applySearchFilter(query, options.where, type);
586
716
  query = this.applyDateFilter(query, options.where);
717
+ query = this.applyBylineFilter(query, options.where, type);
718
+ query = this.applyFieldFilters(query, resolvedFieldFilters);
587
719
 
588
720
  // Handle cursor pagination — decodeCursor throws InvalidCursorError
589
721
  // on malformed input; let it propagate so handlers surface a
590
722
  // structured INVALID_CURSOR rather than silently returning page 1.
591
723
  if (options.cursor) {
592
- const { orderValue, id: cursorId } = decodeCursor(options.cursor);
593
-
594
- if (safeOrderDirection === "DESC") {
595
- query = query.where((eb) =>
596
- eb.or([
597
- eb(dbField as any, "<", orderValue),
598
- eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
599
- ]),
600
- );
724
+ if (resolvedOrderField.indexedCustomField) {
725
+ const { value, id: cursorId } = decodeIndexedFieldCursor(options.cursor, orderField);
726
+ const isPresent = sql<boolean>`${sql.ref(dbField)} IS NOT NULL`;
727
+ const falseLiteral = sql<boolean>`FALSE`;
728
+ const trueLiteral = sql<boolean>`TRUE`;
729
+ if (safeOrderDirection === "ASC" && value === null) {
730
+ query = query.where(sql<boolean>`
731
+ (${isPresent}) > ${falseLiteral}
732
+ OR ((${isPresent}) = ${falseLiteral} AND ${sql.ref("id")} > ${cursorId})
733
+ `);
734
+ } else if (safeOrderDirection === "DESC" && value === null) {
735
+ query = query.where(sql<boolean>`
736
+ (${isPresent}) = ${falseLiteral} AND ${sql.ref("id")} < ${cursorId}
737
+ `);
738
+ } else if (safeOrderDirection === "ASC") {
739
+ query = query.where(sql<boolean>`
740
+ (${isPresent}, ${sql.ref(dbField)}, ${sql.ref("id")})
741
+ > (${trueLiteral}, ${value}, ${cursorId})
742
+ `);
743
+ } else {
744
+ query = query.where(sql<boolean>`
745
+ (${isPresent}, ${sql.ref(dbField)}, ${sql.ref("id")})
746
+ < (${trueLiteral}, ${value}, ${cursorId})
747
+ `);
748
+ }
601
749
  } else {
602
- query = query.where((eb) =>
603
- eb.or([
604
- eb(dbField as any, ">", orderValue),
605
- eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
606
- ]),
607
- );
750
+ const { orderValue, id: cursorId } = decodeCursor(options.cursor);
751
+
752
+ if (safeOrderDirection === "DESC") {
753
+ query = query.where((eb) =>
754
+ eb.or([
755
+ eb(dbField as any, "<", orderValue),
756
+ eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
757
+ ]),
758
+ );
759
+ } else {
760
+ query = query.where((eb) =>
761
+ eb.or([
762
+ eb(dbField as any, ">", orderValue),
763
+ eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
764
+ ]),
765
+ );
766
+ }
608
767
  }
609
768
  }
610
769
 
611
770
  // Apply ordering and limit
612
- query = query
613
- .orderBy(dbField as any, safeOrderDirection === "ASC" ? "asc" : "desc")
614
- .orderBy("id", safeOrderDirection === "ASC" ? "asc" : "desc")
615
- .limit(limit + 1);
771
+ const indexedOrderFilter = resolvedOrderField.indexedCustomField
772
+ ? resolvedFieldFilters.find((filter) => filter.column === dbField)
773
+ : undefined;
774
+ if (resolvedOrderField.indexedCustomField && !indexedOrderFilter) {
775
+ query = query.orderBy(
776
+ sql<boolean>`${sql.ref(dbField)} IS NOT NULL`,
777
+ safeOrderDirection === "ASC" ? "asc" : "desc",
778
+ );
779
+ }
780
+ if (indexedOrderFilter?.kind !== "null") {
781
+ query = query.orderBy(dbField as any, safeOrderDirection === "ASC" ? "asc" : "desc");
782
+ }
783
+ query = query.orderBy("id", safeOrderDirection === "ASC" ? "asc" : "desc").limit(limit + 1);
616
784
 
617
785
  // Run the page fetch and the unbounded count together — the UI needs
618
786
  // both to render a stable denominator (kept on every page intentionally),
619
787
  // and issuing them in parallel on SQLite is essentially free.
620
- const [rows, total] = await Promise.all([query.execute(), this.count(type, options.where)]);
788
+ //
789
+ // Settled rather than raced: a collection whose table is missing rejects
790
+ // both, and `Promise.all` returns on the first. The loser stays in flight
791
+ // holding a pooled connection, so a Postgres pool destroyed in that window
792
+ // never finishes closing.
793
+ const [rowsResult, countResult] = await Promise.allSettled([
794
+ query.execute(),
795
+ this.countWithResolvedFilters(type, options.where, resolvedFieldFilters),
796
+ ]);
797
+ if (rowsResult.status === "rejected") throw rowsResult.reason;
798
+ if (countResult.status === "rejected") throw countResult.reason;
799
+ const rows = rowsResult.value;
800
+ const total = countResult.value;
621
801
  const hasMore = rows.length > limit;
622
802
  const items = rows.slice(0, limit);
623
803
 
@@ -629,11 +809,26 @@ export class ContentRepository {
629
809
  if (hasMore && items.length > 0) {
630
810
  const lastRow = items.at(-1) as Record<string, unknown>;
631
811
  const lastOrderValue = lastRow[dbField];
632
- const orderStr =
633
- typeof lastOrderValue === "string" || typeof lastOrderValue === "number"
634
- ? String(lastOrderValue)
635
- : "";
636
- mappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));
812
+ if (resolvedOrderField.indexedCustomField) {
813
+ if (
814
+ lastOrderValue !== null &&
815
+ typeof lastOrderValue !== "string" &&
816
+ typeof lastOrderValue !== "number"
817
+ ) {
818
+ throw new EmDashValidationError(`Invalid indexed value for order field: ${orderField}`);
819
+ }
820
+ mappedResult.nextCursor = encodeIndexedFieldCursor(
821
+ orderField,
822
+ lastOrderValue,
823
+ String(lastRow.id),
824
+ );
825
+ } else {
826
+ const orderStr =
827
+ typeof lastOrderValue === "string" || typeof lastOrderValue === "number"
828
+ ? String(lastOrderValue)
829
+ : "";
830
+ mappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));
831
+ }
637
832
  }
638
833
 
639
834
  return mappedResult;
@@ -676,11 +871,8 @@ export class ContentRepository {
676
871
 
677
872
  // Update data fields (skip system columns to prevent injection via data)
678
873
  if (input.data !== undefined && typeof input.data === "object") {
679
- for (const [key, value] of Object.entries(input.data)) {
680
- if (!SYSTEM_COLUMNS.has(key)) {
681
- validateIdentifier(key, "content field name");
682
- updates[key] = serializeValue(value);
683
- }
874
+ for (const [key, value] of Object.entries(writableContentData(input.data))) {
875
+ updates[key] = serializeValue(value);
684
876
  }
685
877
  }
686
878
 
@@ -697,18 +889,6 @@ export class ContentRepository {
697
889
  .where("deleted_at" as never, "is", null)
698
890
  .execute();
699
891
 
700
- // Re-stamp the taxonomy pivot only when a denormalized column actually
701
- // moved (status/publishedAt/scheduledAt). A plain content edit bumps
702
- // `updated_at` — which is not denormalized — so it needs no pivot write,
703
- // keeping the common edit path free of taxonomy write amplification.
704
- if (
705
- input.status !== undefined ||
706
- input.publishedAt !== undefined ||
707
- input.scheduledAt !== undefined
708
- ) {
709
- await this.restampEntryPivot(type, id);
710
- }
711
-
712
892
  if (hasColumnWrites) invalidateCollectionCache(type);
713
893
 
714
894
  const updated = await this.findById(type, id);
@@ -719,6 +899,159 @@ export class ContentRepository {
719
899
  return updated;
720
900
  }
721
901
 
902
+ /**
903
+ * Update plugin-authored fields without letting content columns diverge
904
+ * from the revision pointers that publication promotes.
905
+ */
906
+ async updateDraftAware(
907
+ type: string,
908
+ id: string,
909
+ input: UpdateContentInput,
910
+ ): Promise<ContentItem> {
911
+ const data = input.data ? writableContentData(input.data) : {};
912
+ const stagedSlug = typeof input.slug === "string" ? input.slug : undefined;
913
+ const hasDraftUpdate = Object.keys(data).length > 0 || stagedSlug !== undefined;
914
+
915
+ if (!hasDraftUpdate) {
916
+ return this.update(type, id, { ...input, data });
917
+ }
918
+
919
+ const collectionRows = await this.db
920
+ .selectFrom("_emdash_collections as collection")
921
+ .leftJoin("_emdash_fields as field", "field.collection_id", "collection.id")
922
+ .select(["collection.supports", "field.slug as fieldSlug"])
923
+ .where("collection.slug", "=", type)
924
+ .execute();
925
+ const supportsRaw = collectionRows[0]?.supports;
926
+ const supports: unknown = supportsRaw ? JSON.parse(supportsRaw) : [];
927
+ if (!Array.isArray(supports) || !supports.includes("revisions")) {
928
+ return this.update(type, id, { ...input, data });
929
+ }
930
+
931
+ const fieldSlugs = new Set(collectionRows.map((row) => row.fieldSlug).filter(Boolean));
932
+ for (const field of Object.keys(data)) {
933
+ if (!fieldSlugs.has(field)) {
934
+ throw new EmDashValidationError(`Unknown field '${field}' in collection '${type}'`);
935
+ }
936
+ }
937
+
938
+ const revisionRepo = new RevisionRepository(this.db);
939
+ let existing = await this.findById(type, id);
940
+
941
+ for (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {
942
+ let baseData = existing.data;
943
+ if (existing.draftRevisionId) {
944
+ const draft = await revisionRepo.findById(existing.draftRevisionId);
945
+ if (draft) baseData = draft.data;
946
+ }
947
+
948
+ const mergedData = { ...baseData, ...data };
949
+ if (stagedSlug !== undefined) mergedData._slug = stagedSlug;
950
+ const revision = await revisionRepo.create({
951
+ collection: type,
952
+ entryId: id,
953
+ data: mergedData,
954
+ ...(input.authorId ? { authorId: input.authorId } : {}),
955
+ });
956
+
957
+ let staged: boolean;
958
+ try {
959
+ staged = await this.replaceDraftRevisionForUpdate(type, id, revision.id, existing, input);
960
+ } catch (error) {
961
+ await this.deleteUnstagedRevision(revisionRepo, type, id, revision.id);
962
+ throw error;
963
+ }
964
+
965
+ if (staged) {
966
+ const updated = await this.findById(type, id);
967
+ if (!updated) throw new Error("Content not found");
968
+ const draftData: Record<string, unknown> = {};
969
+ for (const [key, value] of Object.entries(mergedData)) {
970
+ if (!key.startsWith("_")) draftData[key] = value;
971
+ }
972
+ return { ...updated, data: { ...updated.data, ...draftData } };
973
+ }
974
+
975
+ await this.deleteUnstagedRevision(revisionRepo, type, id, revision.id);
976
+ existing = await this.findById(type, id);
977
+ }
978
+
979
+ if (!existing) throw new Error("Content not found");
980
+ throw new ContentMutationConflictError();
981
+ }
982
+
983
+ private async deleteUnstagedRevision(
984
+ revisionRepo: RevisionRepository,
985
+ type: string,
986
+ id: string,
987
+ revisionId: string,
988
+ ): Promise<void> {
989
+ try {
990
+ await revisionRepo.deleteIfUnreferenced(type, id, revisionId);
991
+ } catch (error) {
992
+ console.error(`[content] Failed to clean up unstaged revision ${revisionId}:`, error);
993
+ }
994
+ }
995
+
996
+ private async replaceDraftRevisionForUpdate(
997
+ type: string,
998
+ id: string,
999
+ revisionId: string,
1000
+ expected: ContentItem,
1001
+ input: UpdateContentInput,
1002
+ ): Promise<boolean> {
1003
+ const tableName = getTableName(type);
1004
+ const assignments = [sql`draft_revision_id = ${revisionId}`];
1005
+ let liveMetadataChanged = false;
1006
+
1007
+ if (input.status !== undefined) {
1008
+ assignments.push(sql`status = ${input.status}`);
1009
+ liveMetadataChanged = true;
1010
+ }
1011
+ if (input.slug === null) {
1012
+ assignments.push(sql`slug = NULL`);
1013
+ liveMetadataChanged = true;
1014
+ }
1015
+ if (input.publishedAt !== undefined) {
1016
+ assignments.push(sql`published_at = ${input.publishedAt}`);
1017
+ liveMetadataChanged = true;
1018
+ }
1019
+ if (input.scheduledAt !== undefined) {
1020
+ assignments.push(sql`scheduled_at = ${input.scheduledAt}`);
1021
+ liveMetadataChanged = true;
1022
+ }
1023
+ if (input.authorId !== undefined) {
1024
+ assignments.push(sql`author_id = ${input.authorId}`);
1025
+ liveMetadataChanged = true;
1026
+ }
1027
+ if (input.primaryBylineId !== undefined) {
1028
+ assignments.push(sql`primary_byline_id = ${input.primaryBylineId}`);
1029
+ liveMetadataChanged = true;
1030
+ }
1031
+ if (liveMetadataChanged) assignments.push(sql`updated_at = ${new Date().toISOString()}`);
1032
+ assignments.push(sql`version = version + 1`);
1033
+
1034
+ const result = await sql`
1035
+ UPDATE ${sql.ref(tableName)}
1036
+ SET ${sql.join(assignments, sql`, `)}
1037
+ WHERE id = ${id}
1038
+ AND deleted_at IS NULL
1039
+ AND version = ${expected.version}
1040
+ AND ${nullableColumnMatch("live_revision_id", expected.liveRevisionId)}
1041
+ AND ${nullableColumnMatch("draft_revision_id", expected.draftRevisionId)}
1042
+ AND EXISTS (
1043
+ SELECT 1 FROM revisions
1044
+ WHERE revisions.id = ${revisionId}
1045
+ AND revisions.collection = ${type}
1046
+ AND revisions.entry_id = ${id}
1047
+ )
1048
+ `.execute(this.db);
1049
+
1050
+ const changed = (result.numAffectedRows ?? 0n) > 0n;
1051
+ if (changed && liveMetadataChanged) invalidateCollectionCache(type);
1052
+ return changed;
1053
+ }
1054
+
722
1055
  /**
723
1056
  * Delete content (soft delete - moves to trash)
724
1057
  */
@@ -735,7 +1068,6 @@ export class ContentRepository {
735
1068
 
736
1069
  const changed = (result.numAffectedRows ?? 0n) > 0n;
737
1070
  if (changed) {
738
- await this.restampEntryPivot(type, id);
739
1071
  invalidateCollectionCache(type);
740
1072
  }
741
1073
  return changed;
@@ -758,36 +1090,10 @@ export class ContentRepository {
758
1090
  const restored = result.rows[0];
759
1091
  if (!restored) return null;
760
1092
 
761
- await this.restampEntryPivot(type, id);
762
1093
  invalidateCollectionCache(type);
763
1094
  return this.mapRow(type, restored);
764
1095
  }
765
1096
 
766
- /**
767
- * Re-stamp the denormalized filter + sort columns on every
768
- * `content_taxonomies` pivot row for an entry from its authoritative `ec_*`
769
- * row (migration 051). Called after any mutation that moves one of those
770
- * columns so a taxonomy-filtered listing can seek the entry directly.
771
- *
772
- * A single correlated `UPDATE` reads the post-mutation values from `ec_*`, so
773
- * the pivot converges to the authoritative row. This is NOT atomic with the
774
- * `ec_*` mutation on D1 (no transactions), which is why the read path
775
- * re-checks the real predicates on the joined `ec_*` row. Untagged entries
776
- * have no pivot rows, so the statement is a cheap no-op for them.
777
- */
778
- private async restampEntryPivot(type: string, id: string): Promise<void> {
779
- const tableName = getTableName(type);
780
- await sql`
781
- UPDATE content_taxonomies
782
- SET (status, scheduled_at, deleted_at, locale, published_at, created_at) = (
783
- SELECT status, scheduled_at, deleted_at, locale, published_at, created_at
784
- FROM ${sql.ref(tableName)}
785
- WHERE ${sql.ref(tableName)}.id = ${id}
786
- )
787
- WHERE collection = ${type} AND entry_id = ${id}
788
- `.execute(this.db);
789
- }
790
-
791
1097
  /**
792
1098
  * Permanently delete content (cannot be undone)
793
1099
  */
@@ -953,7 +1259,7 @@ export class ContentRepository {
953
1259
  eb.or(
954
1260
  columns.map((col) => {
955
1261
  validateIdentifier(col, "search column");
956
- return eb(sql`lower(${sql.ref(col)})`, "like", sql`lower(${pattern}) escape '\\'`);
1262
+ return sql<boolean>`lower(CAST(${sql.ref(col)} AS TEXT)) LIKE lower(${pattern}) ESCAPE '\\'`;
957
1263
  }),
958
1264
  ),
959
1265
  );
@@ -984,10 +1290,128 @@ export class ContentRepository {
984
1290
  return next;
985
1291
  }
986
1292
 
1293
+ /**
1294
+ * Apply the optional byline filter as a correlated (NOT) EXISTS against
1295
+ * `_emdash_content_bylines`.
1296
+ *
1297
+ * Correlating from the content table preserves the outer sort index so
1298
+ * `LIMIT` can short-circuit. `mode: "none"` tests the junction rather than
1299
+ * `primary_byline_id` because the two are written in the same call but
1300
+ * are not atomically consistent, so the junction is authoritative.
1301
+ *
1302
+ * Whether a credit *renders* is locale-scoped; whether one *exists* is
1303
+ * not. Both are needed: the first decides what the filter matches, the
1304
+ * second decides whether the author fallback applies at all.
1305
+ */
1306
+ private applyBylineFilter<QB extends { where: (cb: (eb: any) => unknown) => QB }>(
1307
+ query: QB,
1308
+ where: { bylineFilter?: ContentBylineFilter } | undefined,
1309
+ type: string,
1310
+ ): QB {
1311
+ const filter = where?.bylineFilter;
1312
+ if (!filter) return query;
1313
+ const tableName = getTableName(type);
1314
+ const idColumn = `${tableName}.id`;
1315
+ const authorColumn = `${tableName}.author_id`;
1316
+ const localeColumn = `${tableName}.locale`;
1317
+
1318
+ // An explicit credit that actually renders — optionally within a given
1319
+ // set of translation groups. The junction stores a group, but a credit
1320
+ // resolves only where that group has a byline row at the locale the
1321
+ // list is scoped to, so this repeats the join `getContentBylinesMany`
1322
+ // makes. `locale` falls back to each entry's own when the list spans
1323
+ // locales.
1324
+ const creditRenders = (eb: any, bylineIds?: string[]) => {
1325
+ let sub = eb
1326
+ .selectFrom("_emdash_content_bylines as cb")
1327
+ .innerJoin("_emdash_bylines as b", "b.translation_group", "cb.byline_id")
1328
+ .select("cb.id")
1329
+ .where("cb.collection_slug", "=", type)
1330
+ .whereRef("cb.content_id", "=", idColumn);
1331
+ sub = filter.locale
1332
+ ? sub.where("b.locale", "=", filter.locale)
1333
+ : sub.whereRef("b.locale", "=", localeColumn);
1334
+ if (bylineIds) sub = sub.where("cb.byline_id", "in", bylineIds);
1335
+ return eb.exists(sub);
1336
+ };
1337
+
1338
+ // Whether the entry carries an explicit credit at all, at any locale.
1339
+ // Deliberately not locale-scoped: the author fallback is suppressed by
1340
+ // the presence of a junction row, even one that renders nothing here
1341
+ // (`hydrateBylinesMany` gates inference on `primaryBylineId`).
1342
+ const hasExplicitCredit = (eb: any) =>
1343
+ eb.exists(
1344
+ eb
1345
+ .selectFrom("_emdash_content_bylines as cb")
1346
+ .select("cb.id")
1347
+ .where("cb.collection_slug", "=", type)
1348
+ .whereRef("cb.content_id", "=", idColumn),
1349
+ );
1350
+
1351
+ // The entry's author owns a byline row — optionally within a given set
1352
+ // of translation groups — at the locale the list is scoped to. Matching
1353
+ // the locale is what keeps the filter agreeing with the list: an
1354
+ // inferred credit renders only when the author's byline has a row at
1355
+ // that locale (`hydrateBylinesMany` -> `findByUserIds`), and byline
1356
+ // translations start life with a null `user_id`, so a group translated
1357
+ // into the locale but not re-linked resolves to no credit. `locale`
1358
+ // falls back to each entry's own when the list spans locales.
1359
+ const authorHasByline = (eb: any, bylineIds?: string[]) => {
1360
+ let sub = eb
1361
+ .selectFrom("_emdash_bylines as b")
1362
+ .select("b.id")
1363
+ .whereRef("b.user_id", "=", authorColumn);
1364
+ sub = filter.locale
1365
+ ? sub.where("b.locale", "=", filter.locale)
1366
+ : sub.whereRef("b.locale", "=", localeColumn);
1367
+ if (bylineIds) sub = sub.where("b.translation_group", "in", bylineIds);
1368
+ return eb.exists(sub);
1369
+ };
1370
+
1371
+ if (filter.mode === "none") {
1372
+ return query.where((eb: any) => {
1373
+ const uncredited = eb.not(creditRenders(eb));
1374
+ // With inference on, "no byline" means none is rendered, so an
1375
+ // entry that falls through to an author byline is excluded too.
1376
+ return filter.includeInferred
1377
+ ? eb.and([uncredited, eb.or([hasExplicitCredit(eb), eb.not(authorHasByline(eb))])])
1378
+ : uncredited;
1379
+ });
1380
+ }
1381
+
1382
+ const bylineIds = filter.bylineIds ?? [];
1383
+ if (bylineIds.length === 0) {
1384
+ // A filter that resolved to no ids must match nothing rather than
1385
+ // silently degrade to "no filter" and return the whole collection.
1386
+ // `1 = 0` rather than a bound `false`: better-sqlite3 refuses to
1387
+ // bind JS booleans.
1388
+ return query.where(() => sql<boolean>`1 = 0`);
1389
+ }
1390
+
1391
+ return query.where((eb: any) => {
1392
+ if (!filter.includeInferred) return creditRenders(eb, bylineIds);
1393
+ // Inference applies only where no explicit credit exists, so an
1394
+ // entry credited to someone else never matches on its author.
1395
+ return eb.or([
1396
+ creditRenders(eb, bylineIds),
1397
+ eb.and([eb.not(hasExplicitCredit(eb)), authorHasByline(eb, bylineIds)]),
1398
+ ]);
1399
+ });
1400
+ }
1401
+
987
1402
  /**
988
1403
  * Count content items
989
1404
  */
990
1405
  async count(type: string, where?: FindManyOptions["where"]): Promise<number> {
1406
+ const resolvedFieldFilters = await this.resolveFieldFilters(type, where?.fieldFilters);
1407
+ return this.countWithResolvedFilters(type, where, resolvedFieldFilters);
1408
+ }
1409
+
1410
+ private async countWithResolvedFilters(
1411
+ type: string,
1412
+ where: FindManyOptions["where"] | undefined,
1413
+ resolvedFieldFilters: ResolvedFieldFilter[],
1414
+ ): Promise<number> {
991
1415
  const tableName = getTableName(type);
992
1416
 
993
1417
  let query = this.db
@@ -1009,6 +1433,8 @@ export class ContentRepository {
1009
1433
 
1010
1434
  query = this.applySearchFilter(query, where, type);
1011
1435
  query = this.applyDateFilter(query, where);
1436
+ query = this.applyBylineFilter(query, where, type);
1437
+ query = this.applyFieldFilters(query, resolvedFieldFilters);
1012
1438
 
1013
1439
  const result = await query.executeTakeFirst();
1014
1440
  return Number(result?.count || 0);
@@ -1037,8 +1463,16 @@ export class ContentRepository {
1037
1463
  // get overall statistics for a content type in a single query
1038
1464
  async getStats(
1039
1465
  type: string,
1040
- ): Promise<{ total: number; published: number; draft: number; scheduled: number }> {
1466
+ now = new Date(),
1467
+ ): Promise<{
1468
+ total: number;
1469
+ published: number;
1470
+ draft: number;
1471
+ scheduled: number;
1472
+ overdueScheduled: number;
1473
+ }> {
1041
1474
  const tableName = getTableName(type);
1475
+ const nowIso = now.toISOString();
1042
1476
 
1043
1477
  const result = await this.db
1044
1478
  .selectFrom(tableName as keyof Database)
@@ -1047,6 +1481,9 @@ export class ContentRepository {
1047
1481
  eb.fn.sum(eb.case().when("status", "=", "published").then(1).else(0).end()).as("published"),
1048
1482
  eb.fn.sum(eb.case().when("status", "=", "draft").then(1).else(0).end()).as("draft"),
1049
1483
  sql<number>`SUM(CASE WHEN scheduled_at IS NOT NULL THEN 1 ELSE 0 END)`.as("scheduled"),
1484
+ sql<number>`SUM(CASE WHEN scheduled_at IS NOT NULL AND scheduled_at <= ${nowIso} THEN 1 ELSE 0 END)`.as(
1485
+ "overdue_scheduled",
1486
+ ),
1050
1487
  ])
1051
1488
  .where("deleted_at" as never, "is", null)
1052
1489
  .executeTakeFirst();
@@ -1056,6 +1493,7 @@ export class ContentRepository {
1056
1493
  published: Number(result?.published || 0),
1057
1494
  draft: Number(result?.draft || 0),
1058
1495
  scheduled: Number(result?.scheduled || 0),
1496
+ overdueScheduled: Number(result?.overdue_scheduled || 0),
1059
1497
  };
1060
1498
  }
1061
1499
 
@@ -1097,7 +1535,6 @@ export class ContentRepository {
1097
1535
  AND deleted_at IS NULL
1098
1536
  `.execute(this.db);
1099
1537
 
1100
- await this.restampEntryPivot(type, id);
1101
1538
  invalidateCollectionCache(type);
1102
1539
 
1103
1540
  const updated = await this.findById(type, id);
@@ -1137,7 +1574,6 @@ export class ContentRepository {
1137
1574
  AND deleted_at IS NULL
1138
1575
  `.execute(this.db);
1139
1576
 
1140
- await this.restampEntryPivot(type, id);
1141
1577
  invalidateCollectionCache(type);
1142
1578
 
1143
1579
  const updated = await this.findById(type, id);
@@ -1319,6 +1755,8 @@ export class ContentRepository {
1319
1755
  * Syncs the draft revision's data into the content table columns so the
1320
1756
  * content table always reflects the published version.
1321
1757
  * If no draft revision exists, creates one from current data and publishes it.
1758
+ * When `promoteRevision` is false, publishes the current content-table data
1759
+ * by changing lifecycle metadata only.
1322
1760
  *
1323
1761
  * `publishedAt` (optional) overrides the publication timestamp. If omitted,
1324
1762
  * the existing `published_at` is preserved (idempotent re-publish keeps the
@@ -1336,6 +1774,8 @@ export class ContentRepository {
1336
1774
  publishedAt?: string,
1337
1775
  requireDue = false,
1338
1776
  expectedScheduledAt?: string,
1777
+ promoteRevision = true,
1778
+ requireSlug = true,
1339
1779
  ): Promise<ContentItem> {
1340
1780
  const tableName = getTableName(type);
1341
1781
  const now = new Date().toISOString();
@@ -1351,6 +1791,102 @@ export class ContentRepository {
1351
1791
  ) {
1352
1792
  throw new ScheduledNotDueError();
1353
1793
  }
1794
+ if (!promoteRevision && requireSlug && !existing.slug?.trim()) {
1795
+ throw new EmDashValidationError("Cannot publish routable content without a slug");
1796
+ }
1797
+
1798
+ if (!promoteRevision) {
1799
+ const revisionRepo = new RevisionRepository(this.db);
1800
+ let provisionalRevisionId: string | null = null;
1801
+ try {
1802
+ let liveRevisionId = existing.liveRevisionId;
1803
+ if (!liveRevisionId) {
1804
+ const revision = await revisionRepo.create({
1805
+ collection: type,
1806
+ entryId: id,
1807
+ data: existing.data,
1808
+ });
1809
+ liveRevisionId = revision.id;
1810
+ provisionalRevisionId = revision.id;
1811
+ }
1812
+
1813
+ const intendedPublishedAt = publishedAt ?? existing.publishedAt ?? now;
1814
+ const duePredicate = requireDue
1815
+ ? sql`AND scheduled_at IS NOT NULL AND scheduled_at <= ${now}`
1816
+ : sql``;
1817
+ let published = false;
1818
+ try {
1819
+ const result = await sql`
1820
+ UPDATE ${sql.ref(tableName)}
1821
+ SET live_revision_id = ${liveRevisionId},
1822
+ draft_revision_id = NULL,
1823
+ status = 'published',
1824
+ scheduled_at = NULL,
1825
+ published_at = ${intendedPublishedAt},
1826
+ updated_at = ${now},
1827
+ version = version + 1
1828
+ WHERE id = ${id}
1829
+ AND deleted_at IS NULL
1830
+ AND version = ${existing.version}
1831
+ AND status = ${existing.status}
1832
+ AND ${nullableColumnMatch("live_revision_id", existing.liveRevisionId)}
1833
+ AND ${nullableColumnMatch("draft_revision_id", existing.draftRevisionId)}
1834
+ AND ${nullableColumnMatch("scheduled_at", existing.scheduledAt)}
1835
+ ${duePredicate}
1836
+ `.execute(this.db);
1837
+ published = (result.numAffectedRows ?? 0n) > 0n;
1838
+ } catch (error) {
1839
+ if (isConfirmedStatementFailure(error)) throw error;
1840
+ let observed: ContentItem | null;
1841
+ try {
1842
+ observed = await this.findById(type, id);
1843
+ } catch (reconciliationError) {
1844
+ throw new Error("Unable to confirm whether content publication completed", {
1845
+ cause: reconciliationError,
1846
+ });
1847
+ }
1848
+ if (!observed) {
1849
+ throw new Error("Unable to confirm whether content publication completed", {
1850
+ cause: error,
1851
+ });
1852
+ }
1853
+ published = matchesLifecyclePublication(
1854
+ observed,
1855
+ existing,
1856
+ liveRevisionId,
1857
+ intendedPublishedAt,
1858
+ now,
1859
+ );
1860
+ if (!published && matchesPublicationFence(observed, existing)) throw error;
1861
+ if (!published) {
1862
+ throw new Error("Unable to confirm whether content publication completed", {
1863
+ cause: error,
1864
+ });
1865
+ }
1866
+ }
1867
+
1868
+ if (!published) {
1869
+ throw requireDue ? new ScheduledNotDueError() : new ContentMutationConflictError();
1870
+ }
1871
+
1872
+ invalidateCollectionCache(type);
1873
+ const updated = await this.findById(type, id);
1874
+ if (!updated) throw new Error("Content not found");
1875
+ return updated;
1876
+ } catch (error) {
1877
+ if (provisionalRevisionId) {
1878
+ try {
1879
+ await revisionRepo.deleteIfUnreferenced(type, id, provisionalRevisionId);
1880
+ } catch (cleanupError) {
1881
+ console.error(
1882
+ `[content] Failed to clean up provisional revision ${provisionalRevisionId}:`,
1883
+ cleanupError,
1884
+ );
1885
+ }
1886
+ }
1887
+ throw error;
1888
+ }
1889
+ }
1354
1890
 
1355
1891
  const revisionRepo = new RevisionRepository(this.db);
1356
1892
  let provisionalRevisionId: string | null = null;
@@ -1374,6 +1910,9 @@ export class ContentRepository {
1374
1910
 
1375
1911
  const stagedSlug = typeof revision.data._slug === "string" ? revision.data._slug : null;
1376
1912
  const intendedSlug = stagedSlug ?? existing.slug;
1913
+ if (requireSlug && !intendedSlug?.trim()) {
1914
+ throw new EmDashValidationError("Cannot publish routable content without a slug");
1915
+ }
1377
1916
  const intendedPublishedAt = publishedAt ?? existing.publishedAt ?? now;
1378
1917
  if (stagedSlug !== null && stagedSlug !== existing.slug && existing.locale !== null) {
1379
1918
  const conflict = await this.findBySlugIncludingTrashed(type, stagedSlug, existing.locale);
@@ -1464,8 +2003,6 @@ export class ContentRepository {
1464
2003
  }
1465
2004
 
1466
2005
  invalidateCollectionCache(type);
1467
- await this.restampEntryPivot(type, id);
1468
-
1469
2006
  const updated = await this.findById(type, id);
1470
2007
  if (!updated) {
1471
2008
  throw new Error("Content not found");
@@ -1531,7 +2068,6 @@ export class ContentRepository {
1531
2068
  AND deleted_at IS NULL
1532
2069
  `.execute(this.db);
1533
2070
 
1534
- await this.restampEntryPivot(type, id);
1535
2071
  invalidateCollectionCache(type);
1536
2072
 
1537
2073
  const updated = await this.findById(type, id);
@@ -1688,28 +2224,257 @@ export class ContentRepository {
1688
2224
  };
1689
2225
  }
1690
2226
 
2227
+ private normalizeFilterScalar(
2228
+ field: string,
2229
+ type: FieldType,
2230
+ value: unknown,
2231
+ ): NormalizedFilterScalar {
2232
+ if (type === "number" || type === "integer") {
2233
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2234
+ throw new EmDashValidationError(`Filter for field "${field}" must use a finite number`);
2235
+ }
2236
+ if (type === "integer" && !Number.isInteger(value)) {
2237
+ throw new EmDashValidationError(`Filter for field "${field}" must use an integer`);
2238
+ }
2239
+ return value;
2240
+ }
2241
+
2242
+ if (type === "boolean") {
2243
+ if (typeof value !== "boolean") {
2244
+ throw new EmDashValidationError(`Filter for field "${field}" must use a boolean`);
2245
+ }
2246
+ return value ? 1 : 0;
2247
+ }
2248
+
2249
+ if (typeof value !== "string") {
2250
+ throw new EmDashValidationError(`Filter for field "${field}" must use a string`);
2251
+ }
2252
+ if (value.length > MAX_FILTER_STRING_LENGTH) {
2253
+ throw new EmDashValidationError(
2254
+ `Filter value for field "${field}" exceeds ${MAX_FILTER_STRING_LENGTH} characters`,
2255
+ );
2256
+ }
2257
+ return value;
2258
+ }
2259
+
2260
+ private normalizeFieldFilter(
2261
+ field: string,
2262
+ type: FieldType,
2263
+ value: ContentFieldFilterValue,
2264
+ ): ResolvedFieldFilter {
2265
+ if (value === null) return { column: field, kind: "null" };
2266
+ if (typeof value !== "object") {
2267
+ return {
2268
+ column: field,
2269
+ kind: "exact",
2270
+ value: this.normalizeFilterScalar(field, type, value),
2271
+ };
2272
+ }
2273
+ if (Array.isArray(value)) {
2274
+ throw new EmDashValidationError(`Invalid filter for field "${field}"`);
2275
+ }
2276
+
2277
+ const record = value as Record<string, unknown>;
2278
+ const keys = Object.keys(record);
2279
+ if (keys.length === 1 && keys[0] === "in") {
2280
+ if (!Array.isArray(record.in) || record.in.length === 0) {
2281
+ throw new EmDashValidationError(`IN filter for field "${field}" must not be empty`);
2282
+ }
2283
+ if (record.in.length > MAX_IN_FILTER_VALUES) {
2284
+ throw new EmDashValidationError(
2285
+ `IN filter for field "${field}" exceeds ${MAX_IN_FILTER_VALUES} values`,
2286
+ );
2287
+ }
2288
+ return {
2289
+ column: field,
2290
+ kind: "in",
2291
+ values: record.in.map((entry) => this.normalizeFilterScalar(field, type, entry)),
2292
+ };
2293
+ }
2294
+
2295
+ const rangeKeys = new Set(["gt", "gte", "lt", "lte"]);
2296
+ if (keys.length === 0 || keys.some((key) => !rangeKeys.has(key))) {
2297
+ throw new EmDashValidationError(`Invalid filter operator for field "${field}"`);
2298
+ }
2299
+ if (type === "boolean") {
2300
+ throw new EmDashValidationError(`Boolean field "${field}" does not support range filters`);
2301
+ }
2302
+
2303
+ const bounds: Partial<Record<"gt" | "gte" | "lt" | "lte", NormalizedFilterScalar>> = {};
2304
+ for (const key of keys as Array<"gt" | "gte" | "lt" | "lte">) {
2305
+ if (record[key] === undefined) continue;
2306
+ bounds[key] = this.normalizeFilterScalar(field, type, record[key]);
2307
+ }
2308
+ if (Object.keys(bounds).length === 0) {
2309
+ throw new EmDashValidationError(`Range filter for field "${field}" has no bounds`);
2310
+ }
2311
+ return { column: field, kind: "range", bounds };
2312
+ }
2313
+
2314
+ private async collectionExists(type: string): Promise<boolean> {
2315
+ const collection = await this.db
2316
+ .selectFrom("_emdash_collections")
2317
+ .where("slug", "=", type)
2318
+ .select("id")
2319
+ .executeTakeFirst();
2320
+ return collection !== undefined;
2321
+ }
2322
+
2323
+ private async resolveFieldFilters(
2324
+ type: string,
2325
+ filters: ContentFieldFilters | undefined,
2326
+ ): Promise<ResolvedFieldFilter[]> {
2327
+ const resolvedFilters = filters ?? {};
2328
+ const fields = Object.keys(resolvedFilters);
2329
+ if (fields.length === 0) return [];
2330
+ if (fields.length > MAX_INDEXED_FIELD_FILTERS) {
2331
+ if (!(await this.collectionExists(type))) return [];
2332
+ throw new EmDashValidationError(
2333
+ `Content list queries support at most ${MAX_INDEXED_FIELD_FILTERS} indexed field filters`,
2334
+ );
2335
+ }
2336
+ const rows = await this.db
2337
+ .selectFrom("_emdash_fields as field")
2338
+ .innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id")
2339
+ .where("collection.slug", "=", type)
2340
+ .where("field.slug", "in", fields)
2341
+ .where("field.indexed", "=", 1)
2342
+ .select(["field.slug", "field.type"])
2343
+ .execute();
2344
+ const metadata = new Map(rows.map((row) => [row.slug, row.type as FieldType]));
2345
+
2346
+ if (metadata.size === 0 && !(await this.collectionExists(type))) return [];
2347
+
2348
+ for (const field of fields) {
2349
+ try {
2350
+ validateIdentifier(field, "content filter field");
2351
+ } catch {
2352
+ throw new EmDashValidationError(`Invalid content filter field: ${field}`);
2353
+ }
2354
+ }
2355
+
2356
+ const normalized = fields.map((field) => {
2357
+ const fieldType = metadata.get(field);
2358
+ if (!fieldType || !isIndexableFieldType(fieldType)) {
2359
+ throw new EmDashValidationError(
2360
+ `Cannot filter by field "${field}". Custom fields must be indexed before filtering.`,
2361
+ );
2362
+ }
2363
+ return this.normalizeFieldFilter(field, fieldType, resolvedFilters[field]);
2364
+ });
2365
+ const operandCount = normalized.reduce((total, filter) => {
2366
+ if (filter.kind === "null") return total;
2367
+ if (filter.kind === "exact") return total + 1;
2368
+ if (filter.kind === "in") return total + filter.values.length;
2369
+ return total + Object.keys(filter.bounds).length;
2370
+ }, 0);
2371
+ if (operandCount > SQL_BATCH_SIZE) {
2372
+ throw new EmDashValidationError(
2373
+ `Indexed field filters have a total operand budget of ${SQL_BATCH_SIZE}`,
2374
+ );
2375
+ }
2376
+ return normalized;
2377
+ }
2378
+
2379
+ private applyFieldFilters<QB extends { where: (cb: (eb: any) => unknown) => QB }>(
2380
+ query: QB,
2381
+ filters: ResolvedFieldFilter[],
2382
+ ): QB {
2383
+ let next = query;
2384
+ for (const filter of filters) {
2385
+ const column = sql.ref(filter.column);
2386
+ const isPresent = sql<boolean>`${column} IS NOT NULL`;
2387
+ if (filter.kind === "null") {
2388
+ next = next.where(() => sql<boolean>`(${isPresent}) = FALSE AND ${column} IS NULL`);
2389
+ continue;
2390
+ }
2391
+ if (filter.kind === "exact") {
2392
+ next = next.where(
2393
+ () => sql<boolean>`(${isPresent}) = TRUE AND ${column} = ${filter.value}`,
2394
+ );
2395
+ continue;
2396
+ }
2397
+ if (filter.kind === "in") {
2398
+ const values = sql.join(
2399
+ filter.values.map((value) => sql`${value}`),
2400
+ sql`, `,
2401
+ );
2402
+ next = next.where(() => sql<boolean>`(${isPresent}) = TRUE AND ${column} IN (${values})`);
2403
+ continue;
2404
+ }
2405
+
2406
+ next = next.where(() => sql<boolean>`(${isPresent}) = TRUE`);
2407
+ if (filter.bounds.gt !== undefined) {
2408
+ next = next.where(() => sql<boolean>`${column} > ${filter.bounds.gt}`);
2409
+ }
2410
+ if (filter.bounds.gte !== undefined) {
2411
+ next = next.where(() => sql<boolean>`${column} >= ${filter.bounds.gte}`);
2412
+ }
2413
+ if (filter.bounds.lt !== undefined) {
2414
+ next = next.where(() => sql<boolean>`${column} < ${filter.bounds.lt}`);
2415
+ }
2416
+ if (filter.bounds.lte !== undefined) {
2417
+ next = next.where(() => sql<boolean>`${column} <= ${filter.bounds.lte}`);
2418
+ }
2419
+ }
2420
+ return next;
2421
+ }
2422
+
1691
2423
  /**
1692
2424
  * Map order field names to database columns.
1693
2425
  * Only allows known fields to prevent column enumeration via crafted orderBy values.
1694
2426
  */
1695
- private mapOrderField(field: string): string {
1696
- const mapping: Record<string, string> = {
1697
- createdAt: "created_at",
1698
- updatedAt: "updated_at",
1699
- publishedAt: "published_at",
1700
- scheduledAt: "scheduled_at",
1701
- deletedAt: "deleted_at",
1702
- title: "title",
1703
- name: "name",
1704
- slug: "slug",
1705
- status: "status",
1706
- locale: "locale",
1707
- };
2427
+ private mapOrderField(field: string, sortableExtras: readonly string[] = []): string {
2428
+ const mapped = ORDER_FIELD_COLUMNS[field];
2429
+ if (mapped) return mapped;
2430
+
2431
+ // A collection's configured titleField/dateField are allowed as
2432
+ // sort columns. The caller passes the collection's *actual* values (resolved
2433
+ // server-side, never client-supplied), so this stays a closed set per
2434
+ // request and doesn't reopen the column-enumeration hole. The slug is a
2435
+ // validated identifier that maps directly to the column.
2436
+ if (sortableExtras.includes(field)) {
2437
+ validateIdentifier(field, "order field");
2438
+ return field;
2439
+ }
2440
+
2441
+ throw new EmDashValidationError(`Invalid order field: ${field}`);
2442
+ }
2443
+
2444
+ private async resolveOrderField(
2445
+ type: string,
2446
+ field: string,
2447
+ sortableExtras: readonly string[] = [],
2448
+ ): Promise<ResolvedOrderField> {
2449
+ try {
2450
+ return { column: this.mapOrderField(field, sortableExtras), indexedCustomField: false };
2451
+ } catch (error) {
2452
+ if (!(error instanceof EmDashValidationError)) throw error;
2453
+ }
2454
+
2455
+ const customField = await this.db
2456
+ .selectFrom("_emdash_collections as collection")
2457
+ .leftJoin("_emdash_fields as field", (join) =>
2458
+ join
2459
+ .onRef("field.collection_id", "=", "collection.id")
2460
+ .on("field.slug", "=", field)
2461
+ .on("field.indexed", "=", 1),
2462
+ )
2463
+ .where("collection.slug", "=", type)
2464
+ .select(["collection.id as collectionId", "field.slug as fieldSlug"])
2465
+ .executeTakeFirst();
1708
2466
 
1709
- const mapped = mapping[field];
1710
- if (!mapped) {
1711
- throw new EmDashValidationError(`Invalid order field: ${field}`);
2467
+ if (!customField) {
2468
+ throw new ContentCollectionNotFoundError(type);
1712
2469
  }
1713
- return mapped;
2470
+
2471
+ if (!customField.fieldSlug) {
2472
+ throw new EmDashValidationError(
2473
+ `Invalid order field: ${field}. Custom fields must be indexed before sorting.`,
2474
+ );
2475
+ }
2476
+
2477
+ validateIdentifier(customField.fieldSlug, "content order field");
2478
+ return { column: customField.fieldSlug, indexedCustomField: true };
1714
2479
  }
1715
2480
  }