emdash 0.32.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 (946) hide show
  1. package/dist/{adapters-C0ONzkUP.d.mts → adapters-34EN2eXO.d.mts} +34 -2
  2. package/dist/adapters-34EN2eXO.d.mts.map +1 -0
  3. package/dist/after-CBU2vkoO.mjs +96 -0
  4. package/dist/after-CBU2vkoO.mjs.map +1 -0
  5. package/dist/{allowed-origins-CCEi9bPI.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
  6. package/dist/{allowed-origins-CCEi9bPI.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
  7. package/dist/api/route-utils.d.mts +3 -4
  8. package/dist/api/route-utils.d.mts.map +1 -1
  9. package/dist/api/route-utils.mjs +26 -25
  10. package/dist/api/route-utils.mjs.map +1 -1
  11. package/dist/api/schemas/index.d.mts +2 -2
  12. package/dist/api/schemas/index.mjs +6 -5
  13. package/dist/{api-DgPbMlng.mjs → api-DdAsirBe.mjs} +321 -428
  14. package/dist/api-DdAsirBe.mjs.map +1 -0
  15. package/dist/{api-tokens-p-iMzvXR.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
  16. package/dist/{api-tokens-p-iMzvXR.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
  17. package/dist/{apply-1_6ra7NP.mjs → apply-D5zfeD09.mjs} +139 -102
  18. package/dist/apply-D5zfeD09.mjs.map +1 -0
  19. package/dist/astro/index.d.mts +12 -12
  20. package/dist/astro/index.d.mts.map +1 -1
  21. package/dist/astro/index.mjs +103 -35
  22. package/dist/astro/index.mjs.map +1 -1
  23. package/dist/astro/middleware/auth.d.mts +10 -10
  24. package/dist/astro/middleware/auth.mjs +9 -9
  25. package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
  26. package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
  27. package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
  28. package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
  29. package/dist/astro/middleware/redirect.d.mts.map +1 -1
  30. package/dist/astro/middleware/redirect.mjs +13 -10
  31. package/dist/astro/middleware/redirect.mjs.map +1 -1
  32. package/dist/astro/middleware/request-context.mjs +5 -5
  33. package/dist/astro/middleware/setup.mjs +1 -1
  34. package/dist/astro/middleware.d.mts +12 -11
  35. package/dist/astro/middleware.d.mts.map +1 -1
  36. package/dist/astro/middleware.mjs +1915 -223
  37. package/dist/astro/middleware.mjs.map +1 -1
  38. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
  39. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
  40. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
  41. package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
  42. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
  43. package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
  44. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
  45. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
  46. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
  47. package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
  48. package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
  49. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
  50. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
  51. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +20 -20
  52. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +20 -20
  53. package/dist/astro/routes/api/admin/bylines/index.mjs +20 -20
  54. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -17
  55. package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
  56. package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
  57. package/dist/astro/routes/api/admin/comments/bulk.mjs +14 -13
  58. package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
  59. package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
  60. package/dist/astro/routes/api/admin/comments/index.mjs +14 -13
  61. package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
  62. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
  63. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
  64. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
  65. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
  66. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
  67. package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
  68. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
  69. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
  70. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
  71. package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
  72. package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
  73. package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
  74. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
  75. package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
  76. package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
  77. package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
  78. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
  79. package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
  80. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
  81. package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
  82. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
  83. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
  84. package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
  85. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -47
  86. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
  87. package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
  88. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +56 -47
  89. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
  90. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
  91. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
  92. package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
  93. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
  94. package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
  95. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
  96. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +55 -46
  97. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
  98. package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
  99. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +55 -46
  100. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
  101. package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
  102. package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
  103. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
  104. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
  105. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
  106. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
  107. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +55 -46
  108. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
  109. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
  110. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
  111. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
  112. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +55 -46
  113. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
  114. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
  115. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +56 -47
  116. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
  117. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +52 -46
  118. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
  119. package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
  120. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +56 -47
  121. package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
  122. package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
  123. package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
  124. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
  125. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
  126. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
  127. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
  128. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
  129. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
  130. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
  131. package/dist/astro/routes/api/admin/users/_id_/index.mjs +10 -9
  132. package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
  133. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
  134. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -1
  135. package/dist/astro/routes/api/admin/users/index.mjs +9 -8
  136. package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
  137. package/dist/astro/routes/api/auth/dev-bypass.mjs +8 -8
  138. package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
  139. package/dist/astro/routes/api/auth/invite/complete.mjs +14 -13
  140. package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
  141. package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
  142. package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
  143. package/dist/astro/routes/api/auth/invite/register-options.mjs +13 -12
  144. package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
  145. package/dist/astro/routes/api/auth/logout.mjs +4 -4
  146. package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
  147. package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
  148. package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
  149. package/dist/astro/routes/api/auth/me.mjs +10 -9
  150. package/dist/astro/routes/api/auth/me.mjs.map +1 -1
  151. package/dist/astro/routes/api/auth/mode.mjs +4 -4
  152. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
  153. package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
  154. package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
  155. package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
  156. package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
  157. package/dist/astro/routes/api/auth/passkey/options.mjs +15 -14
  158. package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
  159. package/dist/astro/routes/api/auth/passkey/register/options.mjs +13 -12
  160. package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
  161. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +14 -13
  162. package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
  163. package/dist/astro/routes/api/auth/passkey/verify.mjs +14 -13
  164. package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
  165. package/dist/astro/routes/api/auth/signup/complete.mjs +14 -13
  166. package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
  167. package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
  168. package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
  169. package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
  170. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +20 -19
  171. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
  172. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +17 -16
  173. package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
  174. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
  175. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
  176. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
  177. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
  178. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +15 -15
  179. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
  180. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  181. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +21 -20
  182. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
  183. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +21 -20
  184. package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
  185. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
  186. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
  187. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
  188. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  189. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
  190. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +47 -34
  191. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
  192. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
  193. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
  194. package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
  195. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  196. package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
  197. package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
  198. package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
  199. package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
  200. package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
  201. package/dist/astro/routes/api/dashboard.mjs +12 -10
  202. package/dist/astro/routes/api/dashboard.mjs.map +1 -1
  203. package/dist/astro/routes/api/dev/emails.mjs +4 -4
  204. package/dist/astro/routes/api/import/probe.d.mts +3 -3
  205. package/dist/astro/routes/api/import/probe.mjs +22 -20
  206. package/dist/astro/routes/api/import/probe.mjs.map +1 -1
  207. package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
  208. package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
  209. package/dist/astro/routes/api/import/wordpress/execute.mjs +18 -18
  210. package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
  211. package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
  212. package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
  213. package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
  214. package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
  215. package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
  216. package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
  217. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +15 -15
  218. package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
  219. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +22 -20
  220. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
  221. package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
  222. package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
  223. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +32 -31
  224. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
  225. package/dist/astro/routes/api/manifest.mjs +6 -6
  226. package/dist/astro/routes/api/mcp.d.mts.map +1 -1
  227. package/dist/astro/routes/api/mcp.mjs +174 -72
  228. package/dist/astro/routes/api/mcp.mjs.map +1 -1
  229. package/dist/astro/routes/api/media/_id_/confirm.mjs +14 -12
  230. package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
  231. package/dist/astro/routes/api/media/_id_/upload.mjs +7 -7
  232. package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
  233. package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
  234. package/dist/astro/routes/api/media/_id_.mjs +16 -15
  235. package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
  236. package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
  237. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
  238. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
  239. package/dist/astro/routes/api/media/providers/index.mjs +4 -4
  240. package/dist/astro/routes/api/media/upload-url.mjs +11 -10
  241. package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
  242. package/dist/astro/routes/api/media.mjs +19 -16
  243. package/dist/astro/routes/api/media.mjs.map +1 -1
  244. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +13 -13
  245. package/dist/astro/routes/api/menus/_name_/items.mjs +13 -13
  246. package/dist/astro/routes/api/menus/_name_/reorder.mjs +13 -13
  247. package/dist/astro/routes/api/menus/_name_/translations.mjs +13 -13
  248. package/dist/astro/routes/api/menus/_name_.mjs +13 -13
  249. package/dist/astro/routes/api/menus/index.mjs +13 -13
  250. package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
  251. package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
  252. package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
  253. package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
  254. package/dist/astro/routes/api/oauth/register.mjs +4 -4
  255. package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
  256. package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
  257. package/dist/astro/routes/api/oauth/token.mjs +7 -7
  258. package/dist/astro/routes/api/openapi.json.mjs +183 -6
  259. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  260. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
  261. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
  262. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
  263. package/dist/astro/routes/api/redirects/404s/index.mjs +14 -13
  264. package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
  265. package/dist/astro/routes/api/redirects/404s/summary.mjs +14 -13
  266. package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
  267. package/dist/astro/routes/api/redirects/_id_.mjs +16 -14
  268. package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
  269. package/dist/astro/routes/api/redirects/index.mjs +16 -14
  270. package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
  271. package/dist/astro/routes/api/relations/_id_/index.mjs +21 -20
  272. package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
  273. package/dist/astro/routes/api/relations/_id_/translations.mjs +16 -15
  274. package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
  275. package/dist/astro/routes/api/relations/index.mjs +21 -20
  276. package/dist/astro/routes/api/relations/index.mjs.map +1 -1
  277. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
  278. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
  279. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
  280. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
  281. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
  282. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
  283. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
  284. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
  285. package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
  286. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
  287. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
  288. package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
  289. package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
  290. package/dist/astro/routes/api/schema/collections/reorder.d.mts +8 -0
  291. package/dist/astro/routes/api/schema/collections/reorder.d.mts.map +1 -0
  292. package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -0
  293. package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -0
  294. package/dist/astro/routes/api/schema/index.mjs +14 -11
  295. package/dist/astro/routes/api/schema/index.mjs.map +1 -1
  296. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
  297. package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
  298. package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
  299. package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
  300. package/dist/astro/routes/api/search/enable.d.mts +1 -0
  301. package/dist/astro/routes/api/search/enable.d.mts.map +1 -1
  302. package/dist/astro/routes/api/search/enable.mjs +19 -15
  303. package/dist/astro/routes/api/search/enable.mjs.map +1 -1
  304. package/dist/astro/routes/api/search/index.mjs +13 -13
  305. package/dist/astro/routes/api/search/rebuild.mjs +15 -15
  306. package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
  307. package/dist/astro/routes/api/search/stats.mjs +9 -9
  308. package/dist/astro/routes/api/search/suggest.mjs +13 -13
  309. package/dist/astro/routes/api/sections/_slug_.mjs +13 -12
  310. package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
  311. package/dist/astro/routes/api/sections/index.mjs +13 -12
  312. package/dist/astro/routes/api/sections/index.mjs.map +1 -1
  313. package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +8 -8
  314. package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
  315. package/dist/astro/routes/api/settings/backups/export.mjs +8 -8
  316. package/dist/astro/routes/api/settings/backups/index.mjs +9 -9
  317. package/dist/astro/routes/api/settings/email.mjs +6 -6
  318. package/dist/astro/routes/api/settings.mjs +18 -17
  319. package/dist/astro/routes/api/settings.mjs.map +1 -1
  320. package/dist/astro/routes/api/setup/admin-verify.mjs +15 -14
  321. package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
  322. package/dist/astro/routes/api/setup/admin.mjs +14 -13
  323. package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
  324. package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -32
  325. package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
  326. package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
  327. package/dist/astro/routes/api/setup/index.mjs +36 -34
  328. package/dist/astro/routes/api/setup/index.mjs.map +1 -1
  329. package/dist/astro/routes/api/setup/status.mjs +5 -5
  330. package/dist/astro/routes/api/snapshot.mjs +10 -10
  331. package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts +8 -0
  332. package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts.map +1 -0
  333. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +44 -0
  334. package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -0
  335. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +19 -18
  336. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
  337. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +19 -18
  338. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
  339. package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
  340. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +21 -19
  341. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
  342. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
  343. package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
  344. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
  345. package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
  346. package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
  347. package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
  348. package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
  349. package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
  350. package/dist/astro/routes/api/taxonomies/index.mjs +19 -18
  351. package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
  352. package/dist/astro/routes/api/themes/preview.mjs +8 -8
  353. package/dist/astro/routes/api/typegen.mjs +6 -6
  354. package/dist/astro/routes/api/well-known/auth.mjs +2 -2
  355. package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
  356. package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
  357. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
  358. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
  359. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +13 -12
  360. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
  361. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +13 -12
  362. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
  363. package/dist/astro/routes/api/widget-areas/_name_.mjs +8 -7
  364. package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
  365. package/dist/astro/routes/api/widget-areas/index.mjs +13 -12
  366. package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
  367. package/dist/astro/routes/api/widget-components.mjs +4 -4
  368. package/dist/astro/routes/robots.txt.mjs +11 -10
  369. package/dist/astro/routes/robots.txt.mjs.map +1 -1
  370. package/dist/astro/routes/sitemap-_collection_.xml.mjs +14 -14
  371. package/dist/astro/routes/sitemap.xml.mjs +12 -11
  372. package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
  373. package/dist/astro/types.d.mts +41 -17
  374. package/dist/astro/types.d.mts.map +1 -1
  375. package/dist/auth/providers/github.d.mts +1 -1
  376. package/dist/auth/providers/google.d.mts +1 -1
  377. package/dist/{authorize-R5iYYrpa.mjs → authorize-1iFJzPaZ.mjs} +2 -2
  378. package/dist/{authorize-R5iYYrpa.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
  379. package/dist/{backup-LpQ1V_sR.mjs → backup-LHjIAVNP.mjs} +5 -5
  380. package/dist/{backup-LpQ1V_sR.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
  381. package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
  382. package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
  383. package/dist/bundle-CBEXrBnb.mjs +663 -0
  384. package/dist/bundle-CBEXrBnb.mjs.map +1 -0
  385. package/dist/{byline-fields-T9ZLZMxJ.mjs → byline-fields-C5S8wygO.mjs} +2 -2
  386. package/dist/{byline-fields-T9ZLZMxJ.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
  387. package/dist/{byline-C5TAqs8N.mjs → byline-pEcoDps8.mjs} +9 -9
  388. package/dist/{byline-C5TAqs8N.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
  389. package/dist/{byline-registry-BCuOp4UF.mjs → byline-registry-DDun8sxh.mjs} +4 -4
  390. package/dist/{byline-registry-BCuOp4UF.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
  391. package/dist/{bylines-B8-WGdja.mjs → bylines-BWemdX2k.mjs} +8 -8
  392. package/dist/{bylines-B8-WGdja.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
  393. package/dist/{bylines-DGuqBLjV.mjs → bylines-DJfSfXCL.mjs} +5 -5
  394. package/dist/{bylines-DGuqBLjV.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
  395. package/dist/cache-CBJaDy_E.mjs +108 -0
  396. package/dist/cache-CBJaDy_E.mjs.map +1 -0
  397. package/dist/{challenge-store-BFzgFRog.mjs → challenge-store-CZyFVaad.mjs} +1 -1
  398. package/dist/{challenge-store-BFzgFRog.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
  399. package/dist/{chunks-D5dlPeRb.mjs → chunks-DfPax1I0.mjs} +2 -2
  400. package/dist/{chunks-D5dlPeRb.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
  401. package/dist/cli/index.mjs +193 -707
  402. package/dist/cli/index.mjs.map +1 -1
  403. package/dist/client/cf-access.d.mts +1 -1
  404. package/dist/client/index.d.mts +74 -4
  405. package/dist/client/index.d.mts.map +1 -1
  406. package/dist/client/index.mjs +25 -2
  407. package/dist/client/index.mjs.map +1 -1
  408. package/dist/collection-deletion-C8BugSGz.mjs +779 -0
  409. package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
  410. package/dist/{comment-DnTxxVHv.mjs → comment-Dxlep6cE.mjs} +3 -3
  411. package/dist/{comment-DnTxxVHv.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
  412. package/dist/{comment-reaction-BUlUz2bh.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
  413. package/dist/{comment-reaction-BUlUz2bh.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
  414. package/dist/{comments-QPKgK3nk.mjs → comments-CTOuhTSX.mjs} +3 -3
  415. package/dist/{comments-QPKgK3nk.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
  416. package/dist/{components-DZkL4Fx0.mjs → components-CQy3lh9x.mjs} +1 -1
  417. package/dist/{components-DZkL4Fx0.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
  418. package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
  419. package/dist/config-C3EBstHi.mjs.map +1 -0
  420. package/dist/{content-CpfKV9QE.mjs → content-BNfwh7xX.mjs} +667 -183
  421. package/dist/content-BNfwh7xX.mjs.map +1 -0
  422. package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
  423. package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
  424. package/dist/content-refresh-DNERP4-f.mjs +2560 -0
  425. package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
  426. package/dist/{context-kO4YZB03.mjs → context-DRgOvSnW.mjs} +28 -20
  427. package/dist/context-DRgOvSnW.mjs.map +1 -0
  428. package/dist/{cron-BlKIMD_e.mjs → cron-Y9aLtgrA.mjs} +1 -1
  429. package/dist/{cron-BlKIMD_e.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
  430. package/dist/{dashboard-DoBwPnON.mjs → dashboard-CB8J89rJ.mjs} +45 -11
  431. package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
  432. package/dist/db/index.d.mts +4 -4
  433. package/dist/db/index.mjs +3 -3
  434. package/dist/db/index.mjs.map +1 -1
  435. package/dist/db/libsql.d.mts +1 -1
  436. package/dist/db/postgres.d.mts +1 -1
  437. package/dist/db/sqlite.d.mts +1 -1
  438. package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
  439. package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
  440. package/dist/{default-C4v9KP56.mjs → default-DdDl3fxv.mjs} +1 -1
  441. package/dist/{default-C4v9KP56.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
  442. package/dist/deferred-tasks-BMsa9F64.d.mts +9 -0
  443. package/dist/deferred-tasks-BMsa9F64.d.mts.map +1 -0
  444. package/dist/{device-flow-DhUsyJU6.mjs → device-flow-CaioYGK3.mjs} +5 -5
  445. package/dist/{device-flow-DhUsyJU6.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
  446. package/dist/{dialect-helpers-DFwyWjTM.mjs → dialect-helpers-BSbfaO3F.mjs} +24 -16
  447. package/dist/dialect-helpers-BSbfaO3F.mjs.map +1 -0
  448. package/dist/dist-D_6CKR8z.mjs +810 -0
  449. package/dist/dist-D_6CKR8z.mjs.map +1 -0
  450. package/dist/{email-console-C-9Ng8DM.mjs → email-console-BHNjIwiB.mjs} +1 -1
  451. package/dist/{email-console-C-9Ng8DM.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
  452. package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
  453. package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
  454. package/dist/{error-DmmN74gW.mjs → error-DupyJZE2.mjs} +3 -3
  455. package/dist/{error-DmmN74gW.mjs.map → error-DupyJZE2.mjs.map} +1 -1
  456. package/dist/{errors-DtEXIQQV.mjs → errors-CLUPm7aD.mjs} +15 -2
  457. package/dist/errors-CLUPm7aD.mjs.map +1 -0
  458. package/dist/{escape-CdW31WsG.mjs → escape-C_aLKP3H.mjs} +1 -1
  459. package/dist/{escape-CdW31WsG.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
  460. package/dist/{field-defs-cache-QMVnzTH6.mjs → field-defs-cache-hg6QkjV9.mjs} +5 -5
  461. package/dist/{field-defs-cache-QMVnzTH6.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
  462. package/dist/{fts-manager-vncsZIJm.mjs → fts-manager-CZ-5dVwJ.mjs} +111 -61
  463. package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
  464. package/dist/{hash-Cb7U1b5M.mjs → hash-Cg0TXJPV.mjs} +1 -1
  465. package/dist/{hash-Cb7U1b5M.mjs.map → hash-Cg0TXJPV.mjs.map} +1 -1
  466. package/dist/{hreflang-CZbSx6Bg.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
  467. package/dist/{hreflang-CZbSx6Bg.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
  468. package/dist/{hreflang-Bg-zMpkr.mjs → hreflang-DNswoYsi.mjs} +7 -7
  469. package/dist/{hreflang-Bg-zMpkr.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
  470. package/dist/{import-PKhLeXvn.mjs → import-B41u8Rot.mjs} +7 -7
  471. package/dist/{import-PKhLeXvn.mjs.map → import-B41u8Rot.mjs.map} +1 -1
  472. package/dist/{index-j9rTh3cp.d.mts → index-C3Y-rsyN.d.mts} +286 -93
  473. package/dist/index-C3Y-rsyN.d.mts.map +1 -0
  474. package/dist/{index-BeNto_Vs.d.mts → index-Cof4-Fab.d.mts} +5 -10
  475. package/dist/index-Cof4-Fab.d.mts.map +1 -0
  476. package/dist/index.d.mts +20 -19
  477. package/dist/index.mjs +79 -71
  478. package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
  479. package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
  480. package/dist/{load-BwTdWE8B.mjs → load-DmvJrtxS.mjs} +2 -2
  481. package/dist/{load-BwTdWE8B.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
  482. package/dist/{loader-C1XOLV5b.mjs → loader-DLBqRiGP.mjs} +120 -64
  483. package/dist/loader-DLBqRiGP.mjs.map +1 -0
  484. package/dist/{manifest-schema-bCq54i7F.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
  485. package/dist/{manifest-schema-bCq54i7F.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
  486. package/dist/media/image-endpoint.mjs +1 -1
  487. package/dist/media/index.d.mts +1 -1
  488. package/dist/media/index.mjs +3 -2
  489. package/dist/media/index.mjs.map +1 -1
  490. package/dist/media/local-runtime.d.mts +12 -12
  491. package/dist/media/local-runtime.mjs +10 -9
  492. package/dist/media/local-runtime.mjs.map +1 -1
  493. package/dist/{media-DWIzoVFL.mjs → media-BWWWb8kg.mjs} +92 -3
  494. package/dist/media-BWWWb8kg.mjs.map +1 -0
  495. package/dist/{media-tQDZEdu7.mjs → media-CzhAn2--.mjs} +2 -2
  496. package/dist/{media-tQDZEdu7.mjs.map → media-CzhAn2--.mjs.map} +1 -1
  497. package/dist/{media-allowlist-B7TPD6aq.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
  498. package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
  499. package/dist/{media-upload-Bc9Nc5Zf.mjs → media-upload-9yas9zGH.mjs} +11 -9
  500. package/dist/{media-upload-Bc9Nc5Zf.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
  501. package/dist/{media-url-BCm5vBn6.mjs → media-url-BvoX88FA.mjs} +1 -1
  502. package/dist/{media-url-BCm5vBn6.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
  503. package/dist/{media-usage-DPJcNaAq.mjs → media-usage-De3qvwjc.mjs} +138 -61
  504. package/dist/media-usage-De3qvwjc.mjs.map +1 -0
  505. package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
  506. package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
  507. package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
  508. package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
  509. package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
  510. package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
  511. package/dist/{menus-C6qhqP4X.mjs → menus-CC-yn1F9.mjs} +554 -121
  512. package/dist/menus-CC-yn1F9.mjs.map +1 -0
  513. package/dist/{menus-BxPLFfIc.mjs → menus-fxvEDYE6.mjs} +5 -5
  514. package/dist/{menus-BxPLFfIc.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
  515. package/dist/{mode-fiXRMfeA.mjs → mode-Bpe-tNNy.mjs} +1 -1
  516. package/dist/{mode-fiXRMfeA.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
  517. package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
  518. package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
  519. package/dist/{oauth-authorization-C9tFinly.mjs → oauth-authorization-BXO1MKej.mjs} +5 -5
  520. package/dist/{oauth-authorization-C9tFinly.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
  521. package/dist/{oauth-clients-BwTxE1aE.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
  522. package/dist/{oauth-clients-BwTxE1aE.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
  523. package/dist/{oauth-state-store-DaRUwPWO.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
  524. package/dist/{oauth-state-store-DaRUwPWO.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
  525. package/dist/{oauth-user-lookup-CzuzOSlF.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
  526. package/dist/{oauth-user-lookup-CzuzOSlF.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
  527. package/dist/object-cache/memory.d.mts +1 -1
  528. package/dist/{object-cache-Bok5j2ae.mjs → object-cache-71tPfV4A.mjs} +106 -5
  529. package/dist/object-cache-71tPfV4A.mjs.map +1 -0
  530. package/dist/{options-BlmBHTvX.mjs → options-CBXIHUoT.mjs} +1 -1
  531. package/dist/{options-BlmBHTvX.mjs.map → options-CBXIHUoT.mjs.map} +1 -1
  532. package/dist/{options-aeIs8WFu.d.mts → options-DRe1pIBL.d.mts} +4 -3
  533. package/dist/options-DRe1pIBL.d.mts.map +1 -0
  534. package/dist/page/index.d.mts +2 -2
  535. package/dist/{parse-BL49yb9D.mjs → parse-DFbuc-Fh.mjs} +2 -2
  536. package/dist/{parse-BL49yb9D.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
  537. package/dist/{passkey-config-KMhgFjdQ.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
  538. package/dist/{passkey-config-KMhgFjdQ.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
  539. package/dist/{placeholder-Dxo5Zt58.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
  540. package/dist/{placeholder-Dxo5Zt58.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
  541. package/dist/plugin-types.d.mts +8 -1
  542. package/dist/plugin-types.d.mts.map +1 -1
  543. package/dist/plugin-utils.d.mts +10 -10
  544. package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
  545. package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
  546. package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
  547. package/dist/{portable-text-CNahFfj6.mjs → portable-text-DhKbDAGr.mjs} +1 -1
  548. package/dist/{portable-text-CNahFfj6.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
  549. package/dist/{preview-D4Jnbfx7.mjs → preview-PW-nOnq4.mjs} +2 -2
  550. package/dist/{preview-D4Jnbfx7.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
  551. package/dist/{public-url-DSGTnJFw.mjs → public-url-CWoFhQT8.mjs} +14 -3
  552. package/dist/public-url-CWoFhQT8.mjs.map +1 -0
  553. package/dist/{query-DCiXI7OZ.mjs → query-BgjfiLUQ.mjs} +38 -77
  554. package/dist/query-BgjfiLUQ.mjs.map +1 -0
  555. package/dist/{query-1xOcEy18.mjs → query-vgZNwIhh.mjs} +4 -4
  556. package/dist/{query-1xOcEy18.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
  557. package/dist/{rate-limit-Vkp9gQ1Y.mjs → rate-limit-L5anykgo.mjs} +2 -2
  558. package/dist/{rate-limit-Vkp9gQ1Y.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
  559. package/dist/{redirect-BJ6d1yHh.mjs → redirect-COsS6SWJ.mjs} +3 -3
  560. package/dist/{redirect-BJ6d1yHh.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
  561. package/dist/{redirect-CDgfv9vh.mjs → redirect-KBNE3UEs.mjs} +1 -1
  562. package/dist/{redirect-CDgfv9vh.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
  563. package/dist/{redirects-Cwkq2H-G.mjs → redirects-CufRQt0Q.mjs} +5 -5
  564. package/dist/{redirects-Cwkq2H-G.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
  565. package/dist/{redirects-BOn2DIUs.mjs → redirects-cV9g7V-0.mjs} +149 -16
  566. package/dist/redirects-cV9g7V-0.mjs.map +1 -0
  567. package/dist/registry-C581V4TP.mjs +1275 -0
  568. package/dist/registry-C581V4TP.mjs.map +1 -0
  569. package/dist/{relation-DJ5DZh7B.mjs → relation-DtmAQH7s.mjs} +3 -3
  570. package/dist/{relation-DJ5DZh7B.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
  571. package/dist/{relations-C4duJnwI.mjs → relations-BBApI4e2.mjs} +3 -3
  572. package/dist/{relations-C4duJnwI.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
  573. package/dist/{relations-DpTjftCl.mjs → relations-DICSyfsh.mjs} +6 -6
  574. package/dist/{relations-DpTjftCl.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
  575. package/dist/{relations-DHFQGX0h.d.mts → relations-DItI3MN9.d.mts} +401 -40
  576. package/dist/relations-DItI3MN9.d.mts.map +1 -0
  577. package/dist/request-context.d.mts +3 -0
  578. package/dist/request-context.d.mts.map +1 -1
  579. package/dist/request-context.mjs.map +1 -1
  580. package/dist/{request-meta-DzXYYI-n.mjs → request-meta-DgZJh3zG.mjs} +2 -2
  581. package/dist/{request-meta-DzXYYI-n.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
  582. package/dist/{resolve-BUvFE0Lr.mjs → resolve-DOhVEvty.mjs} +2 -2
  583. package/dist/{resolve-BUvFE0Lr.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
  584. package/dist/{runner-BsI18UgP.mjs → runner-BiDvq5dv.mjs} +1404 -235
  585. package/dist/runner-BiDvq5dv.mjs.map +1 -0
  586. package/dist/{runner-CHdkERbt.d.mts → runner-CgQbELYC.d.mts} +2 -2
  587. package/dist/{runner-CHdkERbt.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
  588. package/dist/runtime.d.mts +11 -11
  589. package/dist/runtime.mjs +5 -4
  590. package/dist/runtime.mjs.map +1 -1
  591. package/dist/{schema-eqZhkR0S.mjs → schema-BFyTg38T.mjs} +3 -3
  592. package/dist/{schema-eqZhkR0S.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
  593. package/dist/schema-CeQSNwB3.mjs +390 -0
  594. package/dist/schema-CeQSNwB3.mjs.map +1 -0
  595. package/dist/{scopes-Be2d8bSb.mjs → scopes-BVeuhGBE.mjs} +3 -3
  596. package/dist/{scopes-Be2d8bSb.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
  597. package/dist/{search-CeO678cp.mjs → search-BpuPKZb0.mjs} +26 -16
  598. package/dist/search-BpuPKZb0.mjs.map +1 -0
  599. package/dist/{secrets-870d-7yA.mjs → secrets-dtYs2hDo.mjs} +5 -5
  600. package/dist/{secrets-870d-7yA.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
  601. package/dist/{sections-DSuEfirn.mjs → sections-COctk6hf.mjs} +3 -3
  602. package/dist/{sections-DSuEfirn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
  603. package/dist/seed/index.d.mts +2 -2
  604. package/dist/seed/index.mjs +28 -26
  605. package/dist/seo/index.d.mts +3 -3
  606. package/dist/seo/index.mjs +20 -19
  607. package/dist/seo/index.mjs.map +1 -1
  608. package/dist/{seo-C3GDfT0V.mjs → seo-AFpVvTqr.mjs} +6 -4
  609. package/dist/seo-AFpVvTqr.mjs.map +1 -0
  610. package/dist/{seo-BAPKiED8.mjs → seo-CvDxfvFb.mjs} +3 -3
  611. package/dist/{seo-BAPKiED8.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
  612. package/dist/{service-DyFT-k-R.mjs → service-xMiMSTsR.mjs} +4 -4
  613. package/dist/{service-DyFT-k-R.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
  614. package/dist/{session-user-BrK2zz7S.mjs → session-user-BzVemLkw.mjs} +2 -2
  615. package/dist/{session-user-BrK2zz7S.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
  616. package/dist/{settings-DgKouY2S.mjs → settings-B372MV0p.mjs} +7 -7
  617. package/dist/{settings-DgKouY2S.mjs.map → settings-B372MV0p.mjs.map} +1 -1
  618. package/dist/{settings-CREKyj8I.mjs → settings-BE9aqn_9.mjs} +3 -3
  619. package/dist/{settings-CREKyj8I.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
  620. package/dist/{setup-complete-L7bMK2VI.mjs → setup-complete-DF7TAfD5.mjs} +2 -2
  621. package/dist/{setup-complete-L7bMK2VI.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
  622. package/dist/{setup-nonce-Ct5tjWcq.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
  623. package/dist/{setup-nonce-Ct5tjWcq.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
  624. package/dist/{single-flight-cache-C2exrGAi.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
  625. package/dist/{single-flight-cache-C2exrGAi.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
  626. package/dist/site-url-Bowsi9T-.mjs +16 -0
  627. package/dist/site-url-Bowsi9T-.mjs.map +1 -0
  628. package/dist/slugify-B994zkXC.mjs +17 -0
  629. package/dist/slugify-B994zkXC.mjs.map +1 -0
  630. package/dist/{snapshot-BdpUJKD-.mjs → snapshot-B5h6aPMh.mjs} +2 -2
  631. package/dist/{snapshot-BdpUJKD-.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
  632. package/dist/{ssrf-CviKqWmq.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
  633. package/dist/{ssrf-CviKqWmq.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
  634. package/dist/{state-xxv6ZTMv.mjs → state-DYwPue40.mjs} +1 -1
  635. package/dist/{state-xxv6ZTMv.mjs.map → state-DYwPue40.mjs.map} +1 -1
  636. package/dist/{status-COfycGIU.mjs → status-CWVYpbD8.mjs} +1 -1
  637. package/dist/{status-COfycGIU.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
  638. package/dist/storage/local.d.mts +1 -1
  639. package/dist/storage/local.mjs +1 -1
  640. package/dist/storage/s3.d.mts +1 -1
  641. package/dist/storage/s3.mjs +1 -1
  642. package/dist/{taxonomies-B61CRSha.mjs → taxonomies-B0o9ExNV.mjs} +85 -75
  643. package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
  644. package/dist/{taxonomies-Dbdki3Un.mjs → taxonomies-Dk3MAklL.mjs} +360 -56
  645. package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
  646. package/dist/taxonomy-CPeM8d2o.mjs +484 -0
  647. package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
  648. package/dist/{tokens-DVltpO2D.mjs → tokens-C39klX8R.mjs} +2 -2
  649. package/dist/{tokens-DVltpO2D.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
  650. package/dist/{transaction-D0FOsb3X.mjs → transaction-NQj4VJ7Z.mjs} +1 -1
  651. package/dist/{transaction-D0FOsb3X.mjs.map → transaction-NQj4VJ7Z.mjs.map} +1 -1
  652. package/dist/{transport-BWQo0Foy.mjs → transport--CfOflrp.mjs} +1 -1
  653. package/dist/{transport-BWQo0Foy.mjs.map → transport--CfOflrp.mjs.map} +1 -1
  654. package/dist/{transport-DokJOScS.d.mts → transport-B_rfeU-a.d.mts} +1 -1
  655. package/dist/{transport-DokJOScS.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
  656. package/dist/{trusted-proxy-CwjQj0YG.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
  657. package/dist/{trusted-proxy-CwjQj0YG.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
  658. package/dist/{types-o7xo7VgH.mjs → types-75KIqzL9.mjs} +20 -3
  659. package/dist/types-75KIqzL9.mjs.map +1 -0
  660. package/dist/{types-C9P3ysxq.d.mts → types-BPuloofb.d.mts} +1 -1
  661. package/dist/{types-C9P3ysxq.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
  662. package/dist/{types-BD40g7zz.mjs → types-BW1syrlw.mjs} +1 -1
  663. package/dist/{types-BD40g7zz.mjs.map → types-BW1syrlw.mjs.map} +1 -1
  664. package/dist/{types-jISEalTX.d.mts → types-C3_AbkDP.d.mts} +53 -10
  665. package/dist/types-C3_AbkDP.d.mts.map +1 -0
  666. package/dist/{types-84Bque3-.d.mts → types-CAGdePf3.d.mts} +133 -1
  667. package/dist/types-CAGdePf3.d.mts.map +1 -0
  668. package/dist/{types-CcSZCcF1.d.mts → types-CFUfyt1s.d.mts} +1 -1
  669. package/dist/{types-CcSZCcF1.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
  670. package/dist/{types-DQLZ58GV.d.mts → types-CPx2lIGo.d.mts} +1 -1
  671. package/dist/{types-DQLZ58GV.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
  672. package/dist/{types-XrQQ-Aex.mjs → types-CQPWhPxE.mjs} +15 -11
  673. package/dist/types-CQPWhPxE.mjs.map +1 -0
  674. package/dist/{types-BQY7VYw6.d.mts → types-Cs-TFLNf.d.mts} +28 -8
  675. package/dist/types-Cs-TFLNf.d.mts.map +1 -0
  676. package/dist/types-CtzytVXn.mjs +18 -0
  677. package/dist/types-CtzytVXn.mjs.map +1 -0
  678. package/dist/{types-DqZVPILm.d.mts → types-D4_1Ga4L.d.mts} +44 -5
  679. package/dist/types-D4_1Ga4L.d.mts.map +1 -0
  680. package/dist/{types-Dr2GLlwu.d.mts → types-DefV3OiO.d.mts} +1 -1
  681. package/dist/{types-Dr2GLlwu.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
  682. package/dist/{types-DsAMyD_o.d.mts → types-X0MThr7x.d.mts} +1 -1
  683. package/dist/{types-DsAMyD_o.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
  684. package/dist/{upload-attempts-C5yd6Gae.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
  685. package/dist/{upload-attempts-C5yd6Gae.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
  686. package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
  687. package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
  688. package/dist/{user-BAumEmpA.mjs → user-sHgJ62SP.mjs} +3 -3
  689. package/dist/{user-BAumEmpA.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
  690. package/dist/{utils-BbUgmeZG.mjs → utils-D4vD_Y98.mjs} +3 -3
  691. package/dist/{utils-BbUgmeZG.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
  692. package/dist/{validate-Bs_wT2ul.mjs → validate-BkqIG_Uj.mjs} +24 -5
  693. package/dist/validate-BkqIG_Uj.mjs.map +1 -0
  694. package/dist/{validate-VPnKoIzW.mjs → validate-BpwoDOgt.mjs} +25 -2
  695. package/dist/validate-BpwoDOgt.mjs.map +1 -0
  696. package/dist/{validate-CdYcY-Ln.d.mts → validate-DeGtBN84.d.mts} +25 -7
  697. package/dist/validate-DeGtBN84.d.mts.map +1 -0
  698. package/dist/{validation-BngR0nit.mjs → validation-CiJb_X5o.mjs} +14 -13
  699. package/dist/{validation-BngR0nit.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
  700. package/dist/version-x0NjTScG.mjs +7 -0
  701. package/dist/{version-Bdfb34kk.mjs.map → version-x0NjTScG.mjs.map} +1 -1
  702. package/dist/{widgets-L-VndTKD.mjs → widgets-Cwzvww0W.mjs} +3 -3
  703. package/dist/{widgets-L-VndTKD.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
  704. package/dist/{wxr-taxonomies-DueaLFd9.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
  705. package/dist/{wxr-taxonomies-DueaLFd9.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
  706. package/dist/{wxr-taxonomies-BexjtNid.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
  707. package/dist/{wxr-taxonomies-BexjtNid.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
  708. package/dist/{zod-generator-DMzfga3g.mjs → zod-generator-D11MhxG4.mjs} +53 -6
  709. package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
  710. package/package.json +18 -7
  711. package/src/after.ts +6 -1
  712. package/src/api/errors.ts +13 -0
  713. package/src/api/handlers/content.ts +172 -33
  714. package/src/api/handlers/dashboard.ts +12 -2
  715. package/src/api/handlers/index.ts +16 -0
  716. package/src/api/handlers/manifest.ts +2 -0
  717. package/src/api/handlers/marketplace.ts +14 -0
  718. package/src/api/handlers/media-allowlist.ts +13 -2
  719. package/src/api/handlers/media-usage-work.ts +188 -0
  720. package/src/api/handlers/media-usage.ts +5 -3
  721. package/src/api/handlers/registry.ts +14 -0
  722. package/src/api/handlers/revision.ts +19 -5
  723. package/src/api/handlers/schema.ts +46 -3
  724. package/src/api/handlers/seo.ts +5 -2
  725. package/src/api/handlers/snapshot.ts +1 -1
  726. package/src/api/handlers/taxonomies.ts +446 -83
  727. package/src/api/media-usage-write-fence.ts +64 -0
  728. package/src/api/openapi/document.ts +247 -0
  729. package/src/api/public-url.ts +14 -2
  730. package/src/api/schemas/common.ts +3 -1
  731. package/src/api/schemas/content.ts +122 -1
  732. package/src/api/schemas/media-usage.ts +133 -0
  733. package/src/api/schemas/schema.ts +91 -3
  734. package/src/api/schemas/search.ts +2 -0
  735. package/src/api/schemas/taxonomies.ts +58 -8
  736. package/src/api/site-url.ts +16 -6
  737. package/src/api/types.ts +1 -0
  738. package/src/astro/integration/index.ts +55 -42
  739. package/src/astro/integration/routes.ts +48 -4
  740. package/src/astro/integration/runtime.ts +28 -0
  741. package/src/astro/integration/virtual-modules.ts +27 -1
  742. package/src/astro/integration/vite-config.ts +16 -1
  743. package/src/astro/middleware/media-usage-write-fence.ts +30 -0
  744. package/src/astro/middleware/redirect.ts +14 -12
  745. package/src/astro/middleware/scoped-db.ts +35 -4
  746. package/src/astro/middleware.ts +131 -30
  747. package/src/astro/routes/api/admin/comments/[id]/status.ts +1 -1
  748. package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
  749. package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
  750. package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
  751. package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
  752. package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
  753. package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
  754. package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
  755. package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
  756. package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
  757. package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
  758. package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
  759. package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
  760. package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +2 -2
  761. package/src/astro/routes/api/auth/invite/index.ts +2 -2
  762. package/src/astro/routes/api/auth/magic-link/send.ts +2 -2
  763. package/src/astro/routes/api/auth/signup/request.ts +2 -2
  764. package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +1 -1
  765. package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
  766. package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
  767. package/src/astro/routes/api/mcp.ts +1 -0
  768. package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
  769. package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
  770. package/src/astro/routes/api/schema/collections/reorder.ts +31 -0
  771. package/src/astro/routes/api/schema/index.ts +2 -0
  772. package/src/astro/routes/api/search/enable.ts +5 -1
  773. package/src/astro/routes/api/search/rebuild.ts +6 -1
  774. package/src/astro/routes/api/taxonomies/[name]/reorder.ts +41 -0
  775. package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
  776. package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
  777. package/src/astro/routes/api/taxonomies/[name].ts +102 -0
  778. package/src/astro/types.ts +31 -4
  779. package/src/cleanup.ts +25 -26
  780. package/src/cli/commands/dev.ts +13 -3
  781. package/src/cli/commands/doctor.ts +193 -6
  782. package/src/cli/commands/export-seed.ts +6 -1
  783. package/src/cli/index.ts +1 -1
  784. package/src/client/index.ts +119 -0
  785. package/src/components/Block.astro +12 -0
  786. package/src/components/CommentForm.astro +1 -1
  787. package/src/components/Comments.astro +1 -1
  788. package/src/components/InlinePortableTextEditor.tsx +260 -32
  789. package/src/components/OrderedList.astro +14 -0
  790. package/src/components/PortableText.astro +26 -4
  791. package/src/components/index.ts +12 -1
  792. package/src/components/ordered-list.ts +549 -0
  793. package/src/content/converters/mark-safety.ts +120 -0
  794. package/src/content/converters/numbered-list.ts +148 -0
  795. package/src/content/converters/portable-text-to-prosemirror.ts +84 -25
  796. package/src/content/converters/prosemirror-to-portable-text.ts +35 -14
  797. package/src/content/converters/types.ts +2 -0
  798. package/src/content/portable-text-lists.ts +205 -0
  799. package/src/content-list-query.ts +29 -0
  800. package/src/database/dialect-helpers.ts +3 -22
  801. package/src/database/migrations/055_content_translation_group_locale_index.ts +67 -0
  802. package/src/database/migrations/056_taxonomy_term_sort_order.ts +189 -0
  803. package/src/database/migrations/057_collection_hidden.ts +24 -0
  804. package/src/database/migrations/058_collection_sort_order.ts +20 -0
  805. package/src/database/migrations/059_revision_prune_queue.ts +36 -0
  806. package/src/database/migrations/060_collection_admin_config.ts +16 -0
  807. package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
  808. package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
  809. package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
  810. package/src/database/migrations/064_fts_plain_text.ts +253 -0
  811. package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
  812. package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
  813. package/src/database/migrations/067_indexed_content_fields.ts +39 -0
  814. package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
  815. package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
  816. package/src/database/migrations/070_collection_routable.ts +18 -0
  817. package/src/database/migrations/runner.ts +32 -0
  818. package/src/database/repositories/content.ts +1051 -273
  819. package/src/database/repositories/media-usage-work.ts +848 -0
  820. package/src/database/repositories/media-usage.ts +1502 -138
  821. package/src/database/repositories/revision.ts +98 -22
  822. package/src/database/repositories/taxonomy.ts +429 -103
  823. package/src/database/repositories/types.ts +58 -2
  824. package/src/database/types.ts +147 -17
  825. package/src/database/validate.ts +39 -0
  826. package/src/db/adapters.ts +36 -0
  827. package/src/db/index.ts +3 -0
  828. package/src/deferred-tasks.ts +71 -0
  829. package/src/emdash-runtime.ts +493 -110
  830. package/src/fields/file.ts +7 -4
  831. package/src/fields/image.ts +8 -1
  832. package/src/fields/types.ts +18 -5
  833. package/src/i18n/config.ts +32 -0
  834. package/src/i18n/repair-locale-casing.ts +0 -14
  835. package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
  836. package/src/import/wxr-taxonomies.ts +7 -47
  837. package/src/index.ts +27 -2
  838. package/src/loader.ts +165 -89
  839. package/src/mcp/server.ts +242 -34
  840. package/src/media/usage/activation.ts +626 -0
  841. package/src/media/usage/capture-triggers.ts +543 -0
  842. package/src/media/usage/cleanup.ts +305 -0
  843. package/src/media/usage/collection-deletion-processor.ts +356 -0
  844. package/src/media/usage/collection-deletion.ts +836 -0
  845. package/src/media/usage/content-fields.ts +40 -3
  846. package/src/media/usage/content-refresh.ts +356 -47
  847. package/src/media/usage/content-repair.ts +180 -61
  848. package/src/media/usage/content-snapshots.ts +85 -105
  849. package/src/media/usage/gc.ts +50 -0
  850. package/src/media/usage/projection-fingerprint.ts +129 -0
  851. package/src/media/usage/reconciliation-processor.ts +372 -0
  852. package/src/media/usage/reconciliation.ts +1117 -0
  853. package/src/media/usage/source-key.ts +4 -0
  854. package/src/media/usage/types.ts +2 -0
  855. package/src/media/usage/work-processor.ts +248 -0
  856. package/src/menus/index.ts +246 -141
  857. package/src/object-cache/index.ts +160 -3
  858. package/src/plugin-types.ts +8 -0
  859. package/src/plugins/adapt-sandbox-entry.ts +24 -5
  860. package/src/plugins/context.ts +24 -7
  861. package/src/plugins/index.ts +9 -0
  862. package/src/plugins/routes.ts +77 -1
  863. package/src/plugins/sandbox/index.ts +10 -1
  864. package/src/plugins/sandbox/types.ts +78 -1
  865. package/src/plugins/scheduler/node.ts +13 -1
  866. package/src/plugins/scheduler/types.ts +2 -0
  867. package/src/plugins/storage-indexes.ts +50 -11
  868. package/src/plugins/types.ts +39 -2
  869. package/src/query.ts +50 -79
  870. package/src/redirects/cache.ts +79 -19
  871. package/src/request-context.ts +3 -0
  872. package/src/scheduled-publish.ts +6 -1
  873. package/src/scheduler-health.ts +46 -0
  874. package/src/schema/registry.ts +863 -249
  875. package/src/schema/types.ts +74 -8
  876. package/src/schema/url-pattern.ts +33 -0
  877. package/src/schema/zod-generator.ts +77 -5
  878. package/src/search/fts-manager.ts +167 -86
  879. package/src/search/index.ts +2 -0
  880. package/src/search/query.ts +32 -11
  881. package/src/search/types.ts +8 -0
  882. package/src/seed/apply.ts +89 -16
  883. package/src/seed/types.ts +21 -3
  884. package/src/seed/validate.ts +52 -2
  885. package/src/taxonomies/index.ts +88 -82
  886. package/src/taxonomies/term-counts.ts +23 -12
  887. package/src/ui-comments.ts +19 -0
  888. package/src/ui.ts +11 -3
  889. package/src/utils/slugify.ts +1 -29
  890. package/src/virtual-modules.d.ts +24 -0
  891. package/dist/adapters-C0ONzkUP.d.mts.map +0 -1
  892. package/dist/after-B1IIdH3Y.mjs +0 -29
  893. package/dist/after-B1IIdH3Y.mjs.map +0 -1
  894. package/dist/api-DgPbMlng.mjs.map +0 -1
  895. package/dist/apply-1_6ra7NP.mjs.map +0 -1
  896. package/dist/cache-B26cufFd.mjs +0 -65
  897. package/dist/cache-B26cufFd.mjs.map +0 -1
  898. package/dist/config-DyF3OKg_.mjs.map +0 -1
  899. package/dist/content-CpfKV9QE.mjs.map +0 -1
  900. package/dist/content-refresh-DgqUBeTv.mjs +0 -1629
  901. package/dist/content-refresh-DgqUBeTv.mjs.map +0 -1
  902. package/dist/context-kO4YZB03.mjs.map +0 -1
  903. package/dist/dashboard-DoBwPnON.mjs.map +0 -1
  904. package/dist/dialect-helpers-DFwyWjTM.mjs.map +0 -1
  905. package/dist/errors-DtEXIQQV.mjs.map +0 -1
  906. package/dist/fts-manager-vncsZIJm.mjs.map +0 -1
  907. package/dist/index-BeNto_Vs.d.mts.map +0 -1
  908. package/dist/index-j9rTh3cp.d.mts.map +0 -1
  909. package/dist/loader-C1XOLV5b.mjs.map +0 -1
  910. package/dist/media-DWIzoVFL.mjs.map +0 -1
  911. package/dist/media-allowlist-B7TPD6aq.mjs.map +0 -1
  912. package/dist/media-usage-DPJcNaAq.mjs.map +0 -1
  913. package/dist/menus-C6qhqP4X.mjs.map +0 -1
  914. package/dist/object-cache-Bok5j2ae.mjs.map +0 -1
  915. package/dist/options-aeIs8WFu.d.mts.map +0 -1
  916. package/dist/public-url-DSGTnJFw.mjs.map +0 -1
  917. package/dist/query-DCiXI7OZ.mjs.map +0 -1
  918. package/dist/redirects-BOn2DIUs.mjs.map +0 -1
  919. package/dist/registry-BP1JK2xh.mjs +0 -795
  920. package/dist/registry-BP1JK2xh.mjs.map +0 -1
  921. package/dist/relations-DHFQGX0h.d.mts.map +0 -1
  922. package/dist/runner-BsI18UgP.mjs.map +0 -1
  923. package/dist/search-CeO678cp.mjs.map +0 -1
  924. package/dist/seo-C3GDfT0V.mjs.map +0 -1
  925. package/dist/site-url-8N3kSyVZ.mjs +0 -13
  926. package/dist/site-url-8N3kSyVZ.mjs.map +0 -1
  927. package/dist/slugify-C_tqlU4G.mjs +0 -30
  928. package/dist/slugify-C_tqlU4G.mjs.map +0 -1
  929. package/dist/taxonomies-B61CRSha.mjs.map +0 -1
  930. package/dist/taxonomies-Dbdki3Un.mjs.map +0 -1
  931. package/dist/taxonomy-DvwWAPvA.mjs +0 -304
  932. package/dist/taxonomy-DvwWAPvA.mjs.map +0 -1
  933. package/dist/types-84Bque3-.d.mts.map +0 -1
  934. package/dist/types-BQY7VYw6.d.mts.map +0 -1
  935. package/dist/types-DqZVPILm.d.mts.map +0 -1
  936. package/dist/types-XrQQ-Aex.mjs.map +0 -1
  937. package/dist/types-jISEalTX.d.mts.map +0 -1
  938. package/dist/types-o7xo7VgH.mjs.map +0 -1
  939. package/dist/validate-Bs_wT2ul.mjs.map +0 -1
  940. package/dist/validate-CdYcY-Ln.d.mts.map +0 -1
  941. package/dist/validate-VPnKoIzW.mjs.map +0 -1
  942. package/dist/version-Bdfb34kk.mjs +0 -7
  943. package/dist/zod-generator-DMzfga3g.mjs.map +0 -1
  944. /package/dist/{api-tokens-CEsW_jCg.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
  945. /package/dist/{ssrf-BB__xfNM.mjs → ssrf-DpXgKsna.mjs} +0 -0
  946. /package/dist/{types-CK0LxPUQ.mjs → types-CP1CflWf.mjs} +0 -0
@@ -0,0 +1,2560 @@
1
+ import { t as validateIdentifier } from "./validate-BpwoDOgt.mjs";
2
+ import { p as tableExists, s as isPostgres } from "./dialect-helpers-BSbfaO3F.mjs";
3
+ import { i as isI18nEnabled } from "./config-C3EBstHi.mjs";
4
+ import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-DfPax1I0.mjs";
5
+ import { i as InvalidCursorError, o as decodeCursor, s as encodeCursor } from "./types-CQPWhPxE.mjs";
6
+ import { t as withTransaction } from "./transaction-NQj4VJ7Z.mjs";
7
+ import { t as INTERNAL_MEDIA_PREFIX } from "./normalize-Bc-s_r-a.mjs";
8
+ import { n as normalizeMime } from "./mime-DfIVjNkr.mjs";
9
+ import { sql } from "kysely";
10
+ import { ulid } from "ulidx";
11
+
12
+ //#region src/media/usage/projection-fingerprint.ts
13
+ const MEDIA_USAGE_PROJECTION_FINGERPRINT_VERSION = 1;
14
+ const FINGERPRINT_PREFIX = `media-usage-projection:v${MEDIA_USAGE_PROJECTION_FINGERPRINT_VERSION}:sha256:`;
15
+ const FINGERPRINT_PATTERN = new RegExp(`^${FINGERPRINT_PREFIX}[a-f0-9]{64}$`);
16
+ async function buildMediaUsageProjectionFingerprint(input) {
17
+ if (!input.collectionId) throw new Error("Media usage projection fingerprints require a collection identity");
18
+ const canonicalOccurrences = input.occurrences.map((occurrence) => ({
19
+ fieldSlug: occurrence.fieldSlug,
20
+ fieldPath: occurrence.fieldPath,
21
+ occurrenceIndex: occurrence.occurrenceIndex ?? 0,
22
+ referenceType: occurrence.referenceType,
23
+ mediaId: occurrence.mediaId,
24
+ provider: occurrence.provider,
25
+ providerAssetId: occurrence.providerAssetId,
26
+ mediaKind: occurrence.mediaKind ?? null,
27
+ mimeType: occurrence.mimeType ?? null
28
+ })).map((occurrence) => ({
29
+ occurrence,
30
+ key: canonicalJson(occurrence)
31
+ })).toSorted((a, b) => compareCanonicalStrings(a.key, b.key)).map(({ occurrence }) => occurrence);
32
+ return buildCanonicalSha256Fingerprint(FINGERPRINT_PREFIX, {
33
+ fingerprintVersion: MEDIA_USAGE_PROJECTION_FINGERPRINT_VERSION,
34
+ collectionId: input.collectionId,
35
+ extractionSchema: normalizeExtractionFields(input.extractionFields),
36
+ source: {
37
+ sourceKey: input.source.sourceKey,
38
+ sourceType: input.source.sourceType,
39
+ collectionSlug: input.source.collectionSlug ?? null,
40
+ contentId: input.source.contentId ?? null,
41
+ sourceVariant: input.source.sourceVariant,
42
+ locale: input.source.locale ?? null,
43
+ translationGroup: input.source.translationGroup ?? null,
44
+ contentSlug: input.source.contentSlug ?? null,
45
+ contentTitle: input.source.contentTitle ?? null,
46
+ contentStatus: input.source.contentStatus ?? null,
47
+ contentScheduledAt: input.source.contentScheduledAt ?? null,
48
+ contentDeletedAt: input.source.contentDeletedAt ?? null,
49
+ revisionId: input.source.revisionId ?? null,
50
+ schemaVersion: input.source.schemaVersion ?? 1,
51
+ sourceCompleteness: input.source.sourceCompleteness ?? "complete"
52
+ },
53
+ occurrences: canonicalOccurrences
54
+ });
55
+ }
56
+ async function buildCanonicalSha256Fingerprint(prefix, payload) {
57
+ const encodedPayload = new TextEncoder().encode(canonicalJson(payload));
58
+ const digest = await crypto.subtle.digest("SHA-256", encodedPayload);
59
+ return {
60
+ fingerprint: `${prefix}${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`,
61
+ byteLength: encodedPayload.byteLength
62
+ };
63
+ }
64
+ function normalizeExtractionFields(fields) {
65
+ return fields.map((field) => {
66
+ if (field.type !== "repeater") return {
67
+ slug: field.slug,
68
+ type: field.type
69
+ };
70
+ return {
71
+ slug: field.slug,
72
+ type: field.type,
73
+ subFields: (field.validation?.subFields ?? []).map((subField) => ({
74
+ slug: subField.slug,
75
+ type: subField.type
76
+ })).toSorted((a, b) => compareCanonicalStrings(a.slug, b.slug))
77
+ };
78
+ }).toSorted((a, b) => compareCanonicalStrings(String(a.slug), String(b.slug)));
79
+ }
80
+ function compareCanonicalStrings(a, b) {
81
+ return a < b ? -1 : a > b ? 1 : 0;
82
+ }
83
+ function isMediaUsageProjectionFingerprint(value) {
84
+ return typeof value === "string" && FINGERPRINT_PATTERN.test(value);
85
+ }
86
+ function canonicalJson(value) {
87
+ return JSON.stringify(canonicalize(value));
88
+ }
89
+ function canonicalize(value) {
90
+ if (value === void 0) return null;
91
+ if (typeof value === "bigint") return value.toString();
92
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
93
+ if (Array.isArray(value)) return value.map((item) => canonicalize(item));
94
+ if (!isRecord$3(value)) return value;
95
+ const canonical = {};
96
+ for (const key of Object.keys(value).toSorted()) canonical[key] = canonicalize(value[key]);
97
+ return canonical;
98
+ }
99
+ function isRecord$3(value) {
100
+ return typeof value === "object" && value !== null && !Array.isArray(value);
101
+ }
102
+
103
+ //#endregion
104
+ //#region src/database/repositories/media-usage.ts
105
+ const OCCURRENCE_BIND_COLUMNS = 13;
106
+ const MEDIA_USAGE_GENERATION_WRITE_LEASE_MS = 3600 * 1e3;
107
+ const OCCURRENCE_INSERT_BATCH_SIZE = Math.max(1, Math.floor(SQL_BATCH_SIZE / OCCURRENCE_BIND_COLUMNS));
108
+ function cleanupDeleteBatchSize(cleanupLease) {
109
+ return cleanupLease ? SQL_BATCH_SIZE - 3 : SQL_BATCH_SIZE;
110
+ }
111
+ function canIssueCleanupStatement(canIssueStatement) {
112
+ return canIssueStatement?.() ?? true;
113
+ }
114
+ function cleanupDurationSeconds(value) {
115
+ if (!Number.isSafeInteger(value) || value < 0) throw new Error("Media usage cleanup duration must be a non-negative whole number of seconds");
116
+ return value;
117
+ }
118
+ const CONTENT_SOURCE_ELIGIBILITY = sql`(
119
+ s.source_variant = 'draft_overlay'
120
+ OR (
121
+ s.source_variant = 'columns'
122
+ AND (
123
+ s.content_status = 'published'
124
+ OR NOT EXISTS (
125
+ SELECT 1
126
+ FROM _emdash_media_usage_sources AS overlay
127
+ WHERE overlay.source_type = 'content'
128
+ AND overlay.collection_slug = s.collection_slug
129
+ AND overlay.content_id = s.content_id
130
+ AND overlay.source_variant = 'draft_overlay'
131
+ AND ${contentSourceMatchesActiveCollection("overlay", "s.collection_id")}
132
+ )
133
+ )
134
+ )
135
+ )`;
136
+ function contentSourceMatchesActiveCollection(source, currentCollectionId) {
137
+ return sql`(
138
+ NOT EXISTS (
139
+ SELECT 1
140
+ FROM _emdash_media_usage_activation AS activation
141
+ WHERE activation.task_key = 'incremental_capture'
142
+ AND activation.state = 'active'
143
+ )
144
+ OR (
145
+ ${sql.ref(`${source}.collection_id`)} = ${sql.ref(currentCollectionId)}
146
+ AND ${sql.ref(`${source}.identity_version`)} = 1
147
+ )
148
+ )`;
149
+ }
150
+ /** Persistence-only repository for the internal media usage projection tables. */
151
+ var MediaUsageRepository = class {
152
+ constructor(db) {
153
+ this.db = db;
154
+ }
155
+ async replaceSource(source, occurrences) {
156
+ const generation = ulid();
157
+ if (!await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
158
+ await withTransaction(this.db, async (trx) => {
159
+ if (!await this.lockCanonicalSourceCollection(trx, source)) throw new Error(`Media usage collection is no longer current for ${source.sourceKey}`);
160
+ await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
161
+ if (!await this.upsertSource(trx, source, generation, now, leaseToken)) throw new Error(`Media usage generation lease expired for ${source.sourceKey}`);
162
+ });
163
+ })) throw new Error(`Media usage collection is no longer current for ${source.sourceKey}`);
164
+ const replaced = await this.findSource(source.sourceKey);
165
+ if (!replaced) throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
166
+ return replaced;
167
+ }
168
+ async replaceSourceIfCurrent(source, occurrences, expectedCurrentGeneration) {
169
+ if (expectedCurrentGeneration !== null && await this.projectionMatchesCurrentGeneration(source, expectedCurrentGeneration)) return {
170
+ replaced: false,
171
+ unchanged: true,
172
+ source: null
173
+ };
174
+ const generation = ulid();
175
+ let replaced = false;
176
+ await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
177
+ const row = this.buildSourceRow(source, generation, now);
178
+ await withTransaction(this.db, async (trx) => {
179
+ if (!await this.lockCanonicalSourceCollection(trx, source)) return;
180
+ await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
181
+ if (expectedCurrentGeneration === null) {
182
+ replaced = await this.insertSourceIfAbsent(trx, row, leaseToken);
183
+ return;
184
+ }
185
+ replaced = await this.updateSourceIfGeneration(trx, row, expectedCurrentGeneration, leaseToken);
186
+ });
187
+ });
188
+ return {
189
+ replaced,
190
+ unchanged: false,
191
+ source: replaced ? null : await this.findSource(source.sourceKey)
192
+ };
193
+ }
194
+ async findSource(sourceKey) {
195
+ const row = await this.db.selectFrom("_emdash_media_usage_sources").selectAll().where("source_key", "=", sourceKey).executeTakeFirst();
196
+ return row ? rowToSource(row) : null;
197
+ }
198
+ async findSources(sourceKeys) {
199
+ const uniqueSourceKeys = [...new Set(sourceKeys)];
200
+ const sources = /* @__PURE__ */ new Map();
201
+ if (uniqueSourceKeys.length === 0) return sources;
202
+ for (const sourceKeyBatch of chunks(uniqueSourceKeys, SQL_BATCH_SIZE)) {
203
+ const rows = await this.db.selectFrom("_emdash_media_usage_sources").selectAll().where("source_key", "in", sourceKeyBatch).execute();
204
+ for (const row of rows) {
205
+ const source = rowToSource(row);
206
+ sources.set(source.sourceKey, source);
207
+ }
208
+ }
209
+ return sources;
210
+ }
211
+ async measureSourceGenerationDeletion(sourceKey, generation, maxOccurrences) {
212
+ if (!Number.isSafeInteger(maxOccurrences) || maxOccurrences < 0) throw new Error("Media usage deletion measurement requires a non-negative row limit");
213
+ const payload = sql`
214
+ COALESCE(field_slug, '') || COALESCE(field_path, '') ||
215
+ COALESCE(reference_type, '') || COALESCE(media_id, '') ||
216
+ COALESCE(provider, '') || COALESCE(provider_asset_id, '') ||
217
+ COALESCE(media_kind, '') || COALESCE(mime_type, '')
218
+ `;
219
+ const occurrenceBytes = isPostgres(this.db) ? sql`octet_length(${payload})` : sql`length(CAST(${payload} AS BLOB))`;
220
+ const rows = await this.db.selectFrom("_emdash_media_usage").select(occurrenceBytes.as("occurrence_bytes")).where("source_key", "=", sourceKey).where("generation", "=", generation).limit(maxOccurrences + 1).execute();
221
+ return {
222
+ occurrenceCount: rows.length,
223
+ occurrenceBytes: rows.reduce((total, row) => total + Number(row.occurrence_bytes), 0),
224
+ exceedsOccurrenceLimit: rows.length > maxOccurrences
225
+ };
226
+ }
227
+ async replaceSourceIfMatching(source, occurrences, expectedSource) {
228
+ if (expectedSource !== null && await this.projectionMatchesExpectedSource(source, expectedSource)) return {
229
+ replaced: false,
230
+ unchanged: true,
231
+ source: null
232
+ };
233
+ const generation = ulid();
234
+ let replaced = false;
235
+ await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
236
+ const row = this.buildSourceRow(source, generation, now);
237
+ await withTransaction(this.db, async (trx) => {
238
+ if (!await this.lockCanonicalSourceCollection(trx, source)) return;
239
+ await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
240
+ if (expectedSource === null) {
241
+ replaced = await this.insertSourceIfAbsent(trx, row, leaseToken);
242
+ return;
243
+ }
244
+ replaced = await this.updateSourceIfMatching(trx, row, expectedSource, leaseToken);
245
+ });
246
+ });
247
+ return {
248
+ replaced,
249
+ unchanged: false,
250
+ source: replaced ? null : await this.findSource(source.sourceKey)
251
+ };
252
+ }
253
+ async markSourceAttempted(source) {
254
+ if (source.collectionId !== void 0 && source.collectionId !== null) {
255
+ const expectedSource = await this.findSource(source.sourceKey);
256
+ if (!(await this.markSourceAttemptedIfMatching(source, expectedSource)).attempted) throw new Error(`Canonical media usage source ${source.sourceKey} is no longer current`);
257
+ const attempted = await this.findSource(source.sourceKey);
258
+ if (!attempted) throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
259
+ return attempted;
260
+ }
261
+ const generation = ulid();
262
+ await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
263
+ const row = this.buildAttemptedSourceRow(source, generation, now);
264
+ const updates = this.attemptedSourceUpdateSet(source, row);
265
+ if (((await this.db.insertInto("_emdash_media_usage_sources").values(row).onConflict((oc) => oc.column("source_key").doUpdateSet(updates)).executeTakeFirst()).numInsertedOrUpdatedRows ?? 0n) <= 0n) throw new Error(`Media usage generation lease expired for ${source.sourceKey}`);
266
+ });
267
+ const attempted = await this.findSource(source.sourceKey);
268
+ if (!attempted) throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
269
+ return attempted;
270
+ }
271
+ async markSourceAttemptedIfMatching(source, expectedSource) {
272
+ const generation = ulid();
273
+ let attempted = false;
274
+ if (expectedSource === null) await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
275
+ const row = this.buildAttemptedSourceRow(source, generation, now);
276
+ await withTransaction(this.db, async (trx) => {
277
+ if (!await this.lockCanonicalSourceCollection(trx, source)) return;
278
+ attempted = await this.persistSourceIfWriteLease(trx, row, leaseToken, sql`ON CONFLICT (source_key) DO NOTHING`);
279
+ });
280
+ });
281
+ else {
282
+ const row = this.buildAttemptedSourceRow(source, generation, (/* @__PURE__ */ new Date()).toISOString());
283
+ await withTransaction(this.db, async (trx) => {
284
+ if (!await this.lockCanonicalSourceCollection(trx, source)) return;
285
+ attempted = await this.updateAttemptedSourceIfMatching(trx, source, row, expectedSource);
286
+ });
287
+ }
288
+ return {
289
+ attempted,
290
+ source: attempted ? null : await this.findSource(source.sourceKey)
291
+ };
292
+ }
293
+ async findActiveEntryCountsByMediaIds(mediaIds) {
294
+ const uniqueMediaIds = [...new Set(mediaIds)];
295
+ const counts = new Map(uniqueMediaIds.map((mediaId) => [mediaId, 0]));
296
+ for (const mediaIdBatch of chunks(uniqueMediaIds, SQL_BATCH_SIZE)) {
297
+ const visibleEntries = this.currentContentMediaUsageBaseQuery().select([
298
+ "u.media_id as media_id",
299
+ "s.collection_slug as collection_slug",
300
+ "s.content_id as content_id"
301
+ ]).where("u.media_id", "in", mediaIdBatch).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_sources as deleted_source").select("deleted_source.source_key").where("deleted_source.source_type", "=", "content").whereRef("deleted_source.collection_slug", "=", "s.collection_slug").whereRef("deleted_source.content_id", "=", "s.content_id").where("deleted_source.source_variant", "in", ["columns", "draft_overlay"]).where(contentSourceMatchesActiveCollection("deleted_source", "collection.id")).where("deleted_source.content_deleted_at", "is not", null)))).distinct().as("visible_entries");
302
+ const rows = await this.db.selectFrom(visibleEntries).select("media_id").select((eb) => eb.fn.countAll().as("usage_count")).groupBy("media_id").execute();
303
+ for (const row of rows) if (row.media_id !== null) counts.set(row.media_id, Number(row.usage_count));
304
+ }
305
+ return counts;
306
+ }
307
+ async findCollectionIndexStatusScopes(identity) {
308
+ return (await this.db.selectFrom("_emdash_collections as collection").leftJoin("_emdash_media_usage_index_status as status", (join) => join.on("status.adapter_id", "=", identity.adapterId).on("status.scope_type", "=", identity.scopeType).onRef("status.scope_key", "=", "collection.slug")).select([
309
+ "collection.slug as collection_slug",
310
+ "status.status as status",
311
+ "status.schema_version as schema_version",
312
+ "status.reconciliation_required as reconciliation_required"
313
+ ]).orderBy("collection.slug", "asc").execute()).map((row) => ({
314
+ collectionSlug: row.collection_slug,
315
+ status: row.status,
316
+ schemaVersion: row.schema_version === null ? null : Number(row.schema_version),
317
+ reconciliationRequired: row.reconciliation_required !== null && Number(row.reconciliation_required) !== 0
318
+ }));
319
+ }
320
+ async findCurrentEntryUsagePageByMediaId(mediaId, options = {}) {
321
+ const requestedLimit = Math.floor(options.limit ?? 50);
322
+ const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(1, requestedLimit), 100) : 50;
323
+ const cursor = options.cursor ? decodeCursor(options.cursor) : null;
324
+ if (cursor && (cursor.orderValue.length === 0 || cursor.id.length === 0)) throw new InvalidCursorError(options.cursor ?? "");
325
+ let matchedGroups = this.currentContentMediaUsageBaseQuery().select([
326
+ "collection.id as collection_id",
327
+ "s.collection_slug as collection_slug",
328
+ "s.content_id as content_id"
329
+ ]).where("u.media_id", "=", mediaId).distinct();
330
+ if (cursor) matchedGroups = matchedGroups.where((eb) => eb.or([eb("s.collection_slug", ">", cursor.orderValue), eb.and([eb("s.collection_slug", "=", cursor.orderValue), eb("s.content_id", ">", cursor.id)])]));
331
+ matchedGroups = matchedGroups.orderBy("s.collection_slug", "asc").orderBy("s.content_id", "asc").limit(limit + 1);
332
+ const rows = await this.db.with("matched_groups", () => matchedGroups).with("page_groups", (db) => db.selectFrom("matched_groups").selectAll().orderBy("collection_slug", "asc").orderBy("content_id", "asc").limit(limit)).with("entry_state", (db) => db.selectFrom("page_groups as page").crossJoin("_emdash_media_usage_sources as state").select([
333
+ "page.collection_id",
334
+ "page.collection_slug",
335
+ "page.content_id"
336
+ ]).select((eb) => eb.fn.max("state.content_deleted_at").as("entry_deleted_at")).whereRef("page.collection_slug", "=", "state.collection_slug").whereRef("page.content_id", "=", "state.content_id").where("state.source_type", "=", "content").where("state.source_variant", "in", ["columns", "draft_overlay"]).where(contentSourceMatchesActiveCollection("state", "page.collection_id")).groupBy([
337
+ "page.collection_id",
338
+ "page.collection_slug",
339
+ "page.content_id"
340
+ ])).selectFrom("entry_state as page").crossJoin("_emdash_media_usage_sources as s").crossJoin("_emdash_media_usage as u").whereRef("page.collection_slug", "=", "s.collection_slug").whereRef("page.content_id", "=", "s.content_id").where(contentSourceMatchesActiveCollection("s", "page.collection_id")).whereRef("s.source_key", "=", "u.source_key").whereRef("s.current_generation", "=", "u.generation").select(currentUsageSelect).select("page.entry_deleted_at").select(sql`CASE
341
+ WHEN (SELECT COUNT(*) FROM matched_groups) > ${limit} THEN 1
342
+ ELSE 0
343
+ END`.as("has_more")).where("u.media_id", "=", mediaId).where("s.source_type", "=", "content").where("s.collection_slug", "is not", null).where("s.content_id", "is not", null).where("s.source_variant", "in", ["columns", "draft_overlay"]).where(CONTENT_SOURCE_ELIGIBILITY).orderBy("s.collection_slug", "asc").orderBy("s.content_id", "asc").orderBy("s.source_variant", "asc").orderBy("s.source_key", "asc").orderBy("u.field_path", "asc").orderBy("u.occurrence_index", "asc").orderBy("u.id", "asc").execute();
344
+ const items = groupUsageRows(rows);
345
+ const result = { items };
346
+ if (Number(rows[0]?.has_more ?? 0) === 1 && items.length > 0) {
347
+ const last = items.at(-1);
348
+ result.nextCursor = encodeCursor(last.collectionSlug, last.contentId);
349
+ }
350
+ return result;
351
+ }
352
+ async findCurrentUsageByMediaId(mediaId) {
353
+ return (await this.db.selectFrom("_emdash_media_usage_sources as s").innerJoin("_emdash_media_usage as u", (join) => join.onRef("u.source_key", "=", "s.source_key").onRef("u.generation", "=", "s.current_generation")).select(currentUsageSelect).where("u.media_id", "=", mediaId).orderBy("s.source_key", "asc").orderBy("u.field_path", "asc").orderBy("u.occurrence_index", "asc").execute()).map(rowToUsageRecord);
354
+ }
355
+ async findCurrentUsageByProviderAsset(provider, providerAssetId) {
356
+ return (await this.db.selectFrom("_emdash_media_usage_sources as s").innerJoin("_emdash_media_usage as u", (join) => join.onRef("u.source_key", "=", "s.source_key").onRef("u.generation", "=", "s.current_generation")).select(currentUsageSelect).where("u.provider", "=", provider).where("u.provider_asset_id", "=", providerAssetId).orderBy("s.source_key", "asc").orderBy("u.field_path", "asc").orderBy("u.occurrence_index", "asc").execute()).map(rowToUsageRecord);
357
+ }
358
+ async findCurrentUsagePageByMediaId(mediaId, options = {}) {
359
+ return this.findCurrentUsagePage((query) => query.where("u.media_id", "=", mediaId), options);
360
+ }
361
+ async findCurrentUsagePageByProviderAsset(provider, providerAssetId, options = {}) {
362
+ return this.findCurrentUsagePage((query) => query.where("u.provider", "=", provider).where("u.provider_asset_id", "=", providerAssetId), options);
363
+ }
364
+ async deleteSource(sourceKey) {
365
+ return this.deleteSources([sourceKey]);
366
+ }
367
+ async deleteSourceIfCurrent(sourceKey, expectedCurrentGeneration) {
368
+ let deleted = false;
369
+ await withTransaction(this.db, async (trx) => {
370
+ await this.lockCleanupBeforeSourceDelete(trx);
371
+ const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where("current_generation", "=", expectedCurrentGeneration).executeTakeFirst();
372
+ deleted = Number(result.numDeletedRows ?? 0) > 0;
373
+ if (!deleted) return;
374
+ await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedCurrentGeneration);
375
+ });
376
+ return {
377
+ deleted,
378
+ source: await this.findSource(sourceKey)
379
+ };
380
+ }
381
+ async deleteSourceIfMatching(sourceKey, expectedSource) {
382
+ let deleted = false;
383
+ await withTransaction(this.db, async (trx) => {
384
+ await this.lockCleanupBeforeSourceDelete(trx);
385
+ const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(expectedSource.collectionId, expectedSource.collectionSlug)).executeTakeFirst();
386
+ deleted = Number(result.numDeletedRows ?? 0) > 0;
387
+ if (!deleted) return;
388
+ await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedSource.currentGeneration);
389
+ });
390
+ return {
391
+ deleted,
392
+ source: await this.findSource(sourceKey)
393
+ };
394
+ }
395
+ async deleteSourceIfMatchingContentAbsent(sourceKey, expectedSource, collectionSlug, contentId) {
396
+ validateIdentifier(collectionSlug, "collection slug");
397
+ const tableName = `ec_${collectionSlug}`;
398
+ let deleted = false;
399
+ await withTransaction(this.db, async (trx) => {
400
+ await this.lockCleanupBeforeSourceDelete(trx);
401
+ const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(expectedSource.collectionId, expectedSource.collectionSlug)).where(sql`NOT EXISTS (SELECT 1 FROM ${sql.ref(tableName)} WHERE id = ${contentId})`).executeTakeFirst();
402
+ deleted = Number(result.numDeletedRows ?? 0) > 0;
403
+ if (!deleted) return;
404
+ await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedSource.currentGeneration);
405
+ });
406
+ const contentPresent = deleted ? false : await this.contentRowExists(tableName, contentId);
407
+ return {
408
+ deleted,
409
+ contentPresent,
410
+ source: deleted || contentPresent ? null : await this.findSource(sourceKey)
411
+ };
412
+ }
413
+ async deleteSources(sourceKeys) {
414
+ return this.deleteSourceKeys(sourceKeys);
415
+ }
416
+ async deleteContentSources(collectionSlug, contentId) {
417
+ const sourceKeys = (await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_type", "=", "content").where("collection_slug", "=", collectionSlug).where("content_id", "=", contentId).execute()).map((row) => row.source_key);
418
+ return this.deleteSourceKeys(sourceKeys);
419
+ }
420
+ async deleteCollectionSources(collectionSlug) {
421
+ let deleted = 0;
422
+ while (true) {
423
+ const sourceRows = await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_type", "=", "content").where("collection_slug", "=", collectionSlug).orderBy("source_key", "asc").limit(SQL_BATCH_SIZE).execute();
424
+ if (sourceRows.length === 0) break;
425
+ deleted += await this.deleteSourceKeys(sourceRows.map((row) => row.source_key));
426
+ }
427
+ return deleted;
428
+ }
429
+ async findCollectionContentSources(collectionSlug, collectionId) {
430
+ let query = this.db.selectFrom("_emdash_media_usage_sources").selectAll().where("source_type", "=", "content").where("collection_slug", "=", collectionSlug).orderBy("source_key", "asc");
431
+ if (collectionId !== void 0) query = query.where("collection_id", "=", collectionId);
432
+ return (await query.execute()).map((row) => rowToSource(row));
433
+ }
434
+ async claimMediaUsageCleanup(input) {
435
+ const leaseDurationSeconds = cleanupDurationSeconds(input.leaseDurationSeconds);
436
+ const nextEligibleDelaySeconds = cleanupDurationSeconds(input.nextEligibleDelaySeconds);
437
+ const sweepSafetyWindowSeconds = cleanupDurationSeconds(input.sweepSafetyWindowSeconds);
438
+ const claimedAt = this.cleanupTimestampOffset(0);
439
+ const leaseExpiresAt = this.cleanupTimestampOffset(leaseDurationSeconds);
440
+ const nextEligibleAt = this.cleanupTimestampOffset(nextEligibleDelaySeconds);
441
+ const sweepBeforeAt = this.cleanupTimestampOffset(-sweepSafetyWindowSeconds);
442
+ const row = await this.db.updateTable("_emdash_media_usage_cleanup").set({
443
+ lease_token: input.leaseToken,
444
+ lease_expires_at: leaseExpiresAt,
445
+ next_eligible_at: nextEligibleAt,
446
+ last_started_at: claimedAt,
447
+ updated_at: claimedAt,
448
+ scan_before_at: sql`CASE
449
+ WHEN scan_before_at IS NULL THEN ${sweepBeforeAt}
450
+ ELSE scan_before_at
451
+ END`
452
+ }).where("task_key", "=", "projection_gc").where(this.cleanupTimestampIsDue("next_eligible_at")).where((eb) => eb.or([eb("lease_token", "is", null), this.cleanupTimestampIsDue("lease_expires_at")])).returning([
453
+ "cursor_created_at",
454
+ "cursor_id",
455
+ "last_started_at",
456
+ "scan_before_at",
457
+ "consecutive_failures"
458
+ ]).executeTakeFirst();
459
+ if (!row) return null;
460
+ if (!row.last_started_at || !row.scan_before_at) throw new Error("Media usage cleanup claim did not persist its database timestamps");
461
+ return {
462
+ leaseToken: input.leaseToken,
463
+ cursor: row.cursor_created_at && row.cursor_id ? {
464
+ createdAt: row.cursor_created_at,
465
+ id: row.cursor_id
466
+ } : null,
467
+ claimedAt: row.last_started_at,
468
+ scanBeforeAt: row.scan_before_at,
469
+ consecutiveFailures: row.consecutive_failures
470
+ };
471
+ }
472
+ async findMediaUsageCleanupCandidates(input) {
473
+ let query = this.db.selectFrom("_emdash_media_usage as u").leftJoin("_emdash_media_usage_sources as s", "s.source_key", "u.source_key").leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select([
474
+ "u.id as id",
475
+ "u.source_key as source_key",
476
+ "u.generation as generation",
477
+ "u.created_at as created_at",
478
+ "s.current_generation as current_generation",
479
+ "s.indexed_at as indexed_at",
480
+ "writer.expires_at as write_lease_expires_at"
481
+ ]).where("u.created_at", "<", input.cutoff).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(Math.max(0, Math.floor(input.limit)));
482
+ if (input.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(input.cleanupLease));
483
+ if (input.cursor) query = query.where((eb) => eb.or([eb("u.created_at", ">", input.cursor.createdAt), eb.and([eb("u.created_at", "=", input.cursor.createdAt), eb("u.id", ">", input.cursor.id)])]));
484
+ return (await query.execute()).map((row) => ({
485
+ id: row.id,
486
+ sourceKey: row.source_key,
487
+ generation: row.generation,
488
+ createdAt: row.created_at,
489
+ currentGeneration: row.current_generation,
490
+ indexedAt: row.indexed_at,
491
+ writeLeaseExpiresAt: row.write_lease_expires_at
492
+ }));
493
+ }
494
+ async completeMediaUsageCleanup(input) {
495
+ const updates = {
496
+ lease_token: null,
497
+ lease_expires_at: null,
498
+ cursor_created_at: input.sweepComplete ? null : input.nextCursor?.createdAt ?? null,
499
+ cursor_id: input.sweepComplete ? null : input.nextCursor?.id ?? null,
500
+ ...input.sweepComplete ? { scan_before_at: null } : {},
501
+ consecutive_failures: 0,
502
+ last_completed_at: this.cleanupTimestampOffset(0),
503
+ last_candidate_count: input.candidateCount,
504
+ last_deleted_orphans: input.deletedOrphans,
505
+ last_deleted_stale: input.deletedStale,
506
+ last_deleted_abandoned: input.deletedAbandoned,
507
+ last_deleted_write_leases: input.deletedWriteLeases,
508
+ last_backlog_lower_bound: input.backlogLowerBound,
509
+ last_scan_has_more: input.scanHasMore ? 1 : 0,
510
+ last_duration_ms: input.durationMs,
511
+ last_error_code: null,
512
+ updated_at: this.cleanupTimestampOffset(0)
513
+ };
514
+ const result = await this.db.updateTable("_emdash_media_usage_cleanup").set(updates).where("task_key", "=", "projection_gc").where("lease_token", "=", input.leaseToken).where(this.cleanupLeaseExpiryIsInFuture("_emdash_media_usage_cleanup.lease_expires_at")).executeTakeFirst();
515
+ return Number(result.numUpdatedRows ?? 0) > 0;
516
+ }
517
+ async failMediaUsageCleanup(input) {
518
+ const retryDelaySeconds = cleanupDurationSeconds(input.retryDelaySeconds);
519
+ const result = await this.db.updateTable("_emdash_media_usage_cleanup").set({
520
+ lease_token: null,
521
+ lease_expires_at: null,
522
+ next_eligible_at: this.cleanupTimestampOffset(retryDelaySeconds),
523
+ consecutive_failures: input.consecutiveFailures,
524
+ last_completed_at: this.cleanupTimestampOffset(0),
525
+ last_duration_ms: input.durationMs,
526
+ last_error_code: input.errorCode,
527
+ updated_at: this.cleanupTimestampOffset(0)
528
+ }).where("task_key", "=", "projection_gc").where("lease_token", "=", input.leaseToken).where(this.cleanupLeaseExpiryIsInFuture("_emdash_media_usage_cleanup.lease_expires_at")).executeTakeFirst();
529
+ return Number(result.numUpdatedRows ?? 0) > 0;
530
+ }
531
+ async deleteOrphanOccurrencesOlderThan(cutoff, limit, options = {}) {
532
+ const batchLimit = Math.floor(limit);
533
+ if (batchLimit <= 0) return 0;
534
+ if (options.candidateIds) return this.deleteOrphanCandidateIds(options.candidateIds.slice(0, batchLimit), cutoff, options.cleanupLease, options.canIssueStatement);
535
+ if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
536
+ let query = this.db.selectFrom("_emdash_media_usage as u").leftJoin("_emdash_media_usage_sources as s", (join) => join.onRef("s.source_key", "=", "u.source_key")).leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select("u.id").where("s.source_key", "is", null).where("u.created_at", "<", cutoff).where(this.noActiveGenerationWriteExpression("u")).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(batchLimit);
537
+ if (options.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
538
+ const rows = await query.execute();
539
+ return this.deleteOrphanCandidateIds(rows.map((row) => row.id), cutoff, options.cleanupLease, options.canIssueStatement);
540
+ }
541
+ async deleteStaleGenerationsOlderThan(cutoff, limit, options = {}) {
542
+ const batchLimit = Math.floor(limit);
543
+ if (batchLimit <= 0) return 0;
544
+ if (options.candidateIds) return this.deleteStaleCandidateIds(options.candidateIds.slice(0, batchLimit), cutoff, options.cleanupLease, options.canIssueStatement);
545
+ if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
546
+ let query = this.db.selectFrom("_emdash_media_usage as u").innerJoin("_emdash_media_usage_sources as s", (join) => join.onRef("s.source_key", "=", "u.source_key")).leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select("u.id").where("u.created_at", "<", cutoff).whereRef("u.generation", "!=", "s.current_generation").whereRef("u.created_at", "<", "s.indexed_at").where(this.noActiveGenerationWriteExpression("u")).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(batchLimit);
547
+ if (options.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
548
+ const rows = await query.execute();
549
+ return this.deleteStaleCandidateIds(rows.map((row) => row.id), cutoff, options.cleanupLease, options.canIssueStatement);
550
+ }
551
+ async deleteAbandonedGenerationsOlderThan(cutoff, limit, options = {}) {
552
+ const batchLimit = Math.floor(limit);
553
+ if (batchLimit <= 0) return 0;
554
+ if (options.candidateIds) return this.deleteAbandonedCandidateIds(options.candidateIds.slice(0, batchLimit), cutoff, options.cleanupLease, options.canIssueStatement);
555
+ if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
556
+ let query = this.db.selectFrom("_emdash_media_usage as u").innerJoin("_emdash_media_usage_sources as s", (join) => join.onRef("s.source_key", "=", "u.source_key")).leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select("u.id").where("u.created_at", "<", cutoff).whereRef("u.generation", "!=", "s.current_generation").whereRef("u.created_at", ">=", "s.indexed_at").where(this.noActiveGenerationWriteExpression("u")).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(batchLimit);
557
+ if (options.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
558
+ const rows = await query.execute();
559
+ return this.deleteAbandonedCandidateIds(rows.map((row) => row.id), cutoff, options.cleanupLease, options.canIssueStatement);
560
+ }
561
+ async deleteExpiredGenerationWriteLeases(limit, cleanupLease, canIssueStatement) {
562
+ const batchLimit = Math.floor(limit);
563
+ if (batchLimit <= 0 || !canIssueCleanupStatement(canIssueStatement)) return 0;
564
+ let query = this.db.selectFrom("_emdash_media_usage_generation_writes").select("lease_token").where(this.generationWriteLeaseHasExpired("expires_at")).orderBy("expires_at", "asc").orderBy("lease_token", "asc").limit(batchLimit);
565
+ if (cleanupLease) query = query.where(this.activeCleanupLeaseExpression(cleanupLease));
566
+ const rows = await query.execute();
567
+ if (rows.length === 0 || !canIssueCleanupStatement(canIssueStatement)) return 0;
568
+ let deleteQuery = this.db.deleteFrom("_emdash_media_usage_generation_writes").where("lease_token", "in", rows.map((row) => row.lease_token)).where(this.generationWriteLeaseHasExpired("expires_at"));
569
+ if (cleanupLease) deleteQuery = deleteQuery.where(this.activeCleanupLeaseExpression(cleanupLease));
570
+ const result = await deleteQuery.executeTakeFirst();
571
+ return Number(result.numDeletedRows ?? 0);
572
+ }
573
+ async upsertIndexStatus(input) {
574
+ const now = input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
575
+ const row = {
576
+ adapter_id: input.adapterId,
577
+ scope_type: input.scopeType,
578
+ scope_key: input.scopeKey,
579
+ status: input.status,
580
+ schema_version: input.schemaVersion ?? 1,
581
+ started_at: input.startedAt ?? null,
582
+ completed_at: input.completedAt ?? null,
583
+ cursor: input.cursor ?? null,
584
+ indexed_source_count: input.indexedSourceCount ?? 0,
585
+ failed_source_count: input.failedSourceCount ?? 0,
586
+ last_error_code: input.lastErrorCode ?? null,
587
+ updated_at: now
588
+ };
589
+ await this.db.insertInto("_emdash_media_usage_index_status").values(row).onConflict((oc) => oc.columns([
590
+ "adapter_id",
591
+ "scope_type",
592
+ "scope_key"
593
+ ]).doUpdateSet({
594
+ status: row.status,
595
+ schema_version: row.schema_version,
596
+ started_at: row.started_at,
597
+ completed_at: row.completed_at,
598
+ cursor: row.cursor,
599
+ indexed_source_count: row.indexed_source_count,
600
+ failed_source_count: row.failed_source_count,
601
+ last_error_code: row.last_error_code,
602
+ updated_at: row.updated_at
603
+ })).execute();
604
+ const status = await this.findIndexStatus(input);
605
+ if (!status) throw new Error(`Media usage index status ${input.adapterId}:${input.scopeType}:${input.scopeKey} was not persisted`);
606
+ return status;
607
+ }
608
+ async invalidateIndexStatusForSchemaChange(collectionSlug) {
609
+ const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
610
+ change_epoch: sql`change_epoch + 1`,
611
+ status: "stale",
612
+ completed_at: null,
613
+ cursor: null,
614
+ last_error_code: "CONTENT_USAGE_STALE",
615
+ reconciliation_required: 1,
616
+ updated_at: this.sortableUtcTimestamp()
617
+ }).where("status.adapter_id", "=", "content-media").where("status.scope_type", "=", "collection").where("status.scope_key", "=", collectionSlug).where("status.capture_state", "=", "active").where((eb) => eb.exists(eb.selectFrom("_emdash_collections as collection").select("collection.id").whereRef("collection.id", "=", "status.collection_id").whereRef("collection.slug", "=", "status.scope_key"))).where(sql`EXISTS (
618
+ SELECT 1
619
+ FROM _emdash_media_usage_activation AS activation
620
+ WHERE activation.task_key = 'incremental_capture'
621
+ AND activation.state = 'active'
622
+ )`).executeTakeFirst();
623
+ return Number(result.numUpdatedRows ?? 0) === 1;
624
+ }
625
+ async beginIndexStatusRepair(input) {
626
+ return this.upsertIndexStatus({
627
+ adapterId: input.adapterId,
628
+ scopeType: input.scopeType,
629
+ scopeKey: input.scopeKey,
630
+ status: "running",
631
+ schemaVersion: input.schemaVersion,
632
+ startedAt: input.startedAt,
633
+ completedAt: null,
634
+ cursor: input.runToken,
635
+ indexedSourceCount: 0,
636
+ failedSourceCount: 0,
637
+ lastErrorCode: null,
638
+ updatedAt: input.updatedAt
639
+ });
640
+ }
641
+ async finalizeIndexStatusRepairIfRunning(input) {
642
+ const updates = {
643
+ status: input.status,
644
+ completed_at: input.completedAt,
645
+ cursor: null,
646
+ indexed_source_count: input.indexedSourceCount ?? 0,
647
+ failed_source_count: input.failedSourceCount ?? 0,
648
+ last_error_code: input.lastErrorCode ?? null,
649
+ updated_at: input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
650
+ };
651
+ if (input.schemaVersion !== void 0) updates.schema_version = input.schemaVersion;
652
+ const result = await this.db.updateTable("_emdash_media_usage_index_status").set(updates).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("status", "=", "running").where("cursor", "=", input.runToken).executeTakeFirst();
653
+ return {
654
+ finalized: Number(result.numUpdatedRows ?? 0) > 0,
655
+ status: await this.findIndexStatus(input)
656
+ };
657
+ }
658
+ async beginIndexStatusRepairAtCurrentEpoch(input) {
659
+ const now = this.sortableUtcTimestamp();
660
+ const row = await this.db.updateTable("_emdash_media_usage_index_status").set({
661
+ status: "running",
662
+ schema_version: input.schemaVersion,
663
+ started_at: now,
664
+ completed_at: null,
665
+ cursor: input.runToken,
666
+ indexed_source_count: 0,
667
+ failed_source_count: 0,
668
+ last_error_code: null,
669
+ reconciliation_required: 1,
670
+ updated_at: now
671
+ }).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("collection_id", "=", input.collectionId).where("capture_state", "=", "active").where(sql`EXISTS (
672
+ SELECT 1
673
+ FROM _emdash_collections AS collection
674
+ WHERE collection.id = ${input.collectionId}
675
+ AND collection.slug = ${input.scopeKey}
676
+ )`).where(sql`EXISTS (
677
+ SELECT 1
678
+ FROM _emdash_media_usage_activation AS activation
679
+ WHERE activation.task_key = 'incremental_capture'
680
+ AND activation.state = 'active'
681
+ )`).returning(["change_epoch", "started_at"]).executeTakeFirst();
682
+ if (!row?.started_at) return null;
683
+ return {
684
+ changeEpoch: row.change_epoch,
685
+ startedAt: row.started_at
686
+ };
687
+ }
688
+ async finalizeIndexStatusRepairAtEpoch(input) {
689
+ const now = this.sortableUtcTimestamp();
690
+ const updates = {
691
+ status: input.status,
692
+ schema_version: input.schemaVersion,
693
+ completed_at: now,
694
+ cursor: null,
695
+ indexed_source_count: input.indexedSourceCount,
696
+ failed_source_count: input.failedSourceCount,
697
+ last_error_code: input.lastErrorCode,
698
+ reconciliation_required: input.status === "complete" ? 0 : 1,
699
+ updated_at: now
700
+ };
701
+ let query = this.db.updateTable("_emdash_media_usage_index_status").set(updates).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("collection_id", "=", input.collectionId).where("status", "=", "running").where("cursor", "=", input.runToken).where("change_epoch", "=", input.startingEpoch).where(sql`EXISTS (
702
+ SELECT 1
703
+ FROM _emdash_collections AS collection
704
+ WHERE collection.id = ${input.collectionId}
705
+ AND collection.slug = ${input.scopeKey}
706
+ )`);
707
+ if (input.status === "complete") query = query.where(sql`NOT EXISTS (
708
+ SELECT 1
709
+ FROM _emdash_media_usage_work AS work
710
+ WHERE work.collection_id = ${input.collectionId}
711
+ )`);
712
+ const result = await query.executeTakeFirst();
713
+ const finalized = Number(result.numUpdatedRows ?? 0) > 0;
714
+ if (!finalized) await this.db.updateTable("_emdash_media_usage_index_status").set({
715
+ status: "stale",
716
+ completed_at: null,
717
+ cursor: null,
718
+ last_error_code: "CONTENT_USAGE_REPAIR_CONFLICT",
719
+ reconciliation_required: 1,
720
+ updated_at: this.sortableUtcTimestamp()
721
+ }).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("collection_id", "=", input.collectionId).where("status", "=", "running").where("cursor", "=", input.runToken).execute();
722
+ return {
723
+ finalized,
724
+ status: await this.findIndexStatusForCollection(input, input.collectionId)
725
+ };
726
+ }
727
+ async recordIncrementalSuccess(input) {
728
+ const observed = await this.db.selectFrom("_emdash_media_usage_index_status").select("change_epoch").where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", input.collectionSlug).where("collection_id", "=", input.collectionId).where("capture_state", "=", "active").where(sql`EXISTS (
729
+ SELECT 1
730
+ FROM _emdash_collections AS collection
731
+ WHERE collection.id = ${input.collectionId}
732
+ AND collection.slug = ${input.collectionSlug}
733
+ )`).executeTakeFirst();
734
+ if (!observed) return false;
735
+ const canComplete = sql`(
736
+ reconciliation_required = 0
737
+ AND status IN ('complete', 'stale', 'partial')
738
+ AND NOT EXISTS (
739
+ SELECT 1
740
+ FROM _emdash_media_usage_work AS work
741
+ WHERE work.collection_id = ${input.collectionId}
742
+ )
743
+ )`;
744
+ const now = this.sortableUtcTimestamp();
745
+ const result = await this.db.updateTable("_emdash_media_usage_index_status").set({
746
+ status: sql`CASE WHEN ${canComplete} THEN 'complete' ELSE status END`,
747
+ completed_at: sql`CASE WHEN ${canComplete} THEN ${now} ELSE completed_at END`,
748
+ last_error_code: sql`CASE WHEN ${canComplete} THEN NULL ELSE last_error_code END`,
749
+ last_incremental_success_at: now,
750
+ updated_at: now
751
+ }).where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", input.collectionSlug).where("collection_id", "=", input.collectionId).where("change_epoch", "=", observed.change_epoch).where("capture_state", "=", "active").where(sql`EXISTS (
752
+ SELECT 1
753
+ FROM _emdash_collections AS collection
754
+ WHERE collection.id = ${input.collectionId}
755
+ AND collection.slug = ${input.collectionSlug}
756
+ )`).executeTakeFirst();
757
+ return Number(result.numUpdatedRows ?? 0) > 0;
758
+ }
759
+ async recordIncrementalFailure(input) {
760
+ const now = this.sortableUtcTimestamp();
761
+ const automaticRunOwnsCoverage = sql`EXISTS (
762
+ SELECT 1
763
+ FROM _emdash_media_usage_reconciliations AS reconciliation
764
+ WHERE reconciliation.collection_id = ${input.collectionId}
765
+ AND reconciliation.run_token = cursor
766
+ )`;
767
+ const result = await this.db.updateTable("_emdash_media_usage_index_status").set({
768
+ status: sql`CASE
769
+ WHEN ${automaticRunOwnsCoverage} THEN status
770
+ WHEN reconciliation_required = 0 THEN 'partial'
771
+ WHEN status = 'running' THEN 'stale'
772
+ ELSE status
773
+ END`,
774
+ completed_at: sql`CASE
775
+ WHEN ${automaticRunOwnsCoverage} THEN completed_at
776
+ WHEN reconciliation_required = 0 OR status = 'running' THEN NULL
777
+ ELSE completed_at
778
+ END`,
779
+ cursor: sql`CASE
780
+ WHEN ${automaticRunOwnsCoverage} THEN cursor
781
+ WHEN status = 'running' THEN NULL
782
+ ELSE cursor
783
+ END`,
784
+ last_error_code: input.errorCode,
785
+ updated_at: now
786
+ }).where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", input.collectionSlug).where("collection_id", "=", input.collectionId).where(sql`EXISTS (
787
+ SELECT 1
788
+ FROM _emdash_media_usage_work AS work
789
+ WHERE work.collection_id = ${input.collectionId}
790
+ AND work.content_id = ${input.contentId}
791
+ AND work.work_version = ${input.workVersion}
792
+ AND work.state = 'failed'
793
+ AND work.last_error_code = ${input.errorCode}
794
+ )`).where(sql`EXISTS (
795
+ SELECT 1
796
+ FROM _emdash_collections AS collection
797
+ WHERE collection.id = ${input.collectionId}
798
+ AND collection.slug = ${input.collectionSlug}
799
+ )`).executeTakeFirst();
800
+ return Number(result.numUpdatedRows ?? 0) > 0;
801
+ }
802
+ async findIndexStatus(identity) {
803
+ const row = await this.db.selectFrom("_emdash_media_usage_index_status").selectAll().where("adapter_id", "=", identity.adapterId).where("scope_type", "=", identity.scopeType).where("scope_key", "=", identity.scopeKey).executeTakeFirst();
804
+ return row ? rowToIndexStatus(row) : null;
805
+ }
806
+ async findIndexStatusForCollection(identity, collectionId) {
807
+ const row = await this.db.selectFrom("_emdash_media_usage_index_status").selectAll().where("adapter_id", "=", identity.adapterId).where("scope_type", "=", identity.scopeType).where("scope_key", "=", identity.scopeKey).where("collection_id", "=", collectionId).executeTakeFirst();
808
+ return row ? rowToIndexStatus(row) : null;
809
+ }
810
+ async deleteIndexStatus(identity, collectionId) {
811
+ let query = this.db.deleteFrom("_emdash_media_usage_index_status").where("adapter_id", "=", identity.adapterId).where("scope_type", "=", identity.scopeType).where("scope_key", "=", identity.scopeKey);
812
+ if (collectionId !== void 0) query = query.where("collection_id", "=", collectionId);
813
+ const result = await query.executeTakeFirst();
814
+ return Number(result.numDeletedRows ?? 0);
815
+ }
816
+ sortableUtcTimestamp() {
817
+ return isPostgres(this.db) ? sql`to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')` : sql`strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
818
+ }
819
+ async findCurrentUsagePage(applyFilter, options) {
820
+ const limit = Math.min(Math.max(1, options.limit ?? 50), 100);
821
+ let query = applyFilter(this.currentUsageBaseQuery()).orderBy("u.id", "asc").limit(limit + 1);
822
+ if (options.cursor) {
823
+ const { id } = decodeCursor(options.cursor);
824
+ query = query.where("u.id", ">", id);
825
+ }
826
+ const rows = await query.execute();
827
+ const items = rows.slice(0, limit).map(rowToUsageRecord);
828
+ const result = { items };
829
+ if (rows.length > limit && items.length > 0) {
830
+ const last = items.at(-1);
831
+ result.nextCursor = encodeCursor(last.occurrence.id, last.occurrence.id);
832
+ }
833
+ return result;
834
+ }
835
+ currentUsageBaseQuery() {
836
+ return this.db.selectFrom("_emdash_media_usage_sources as s").innerJoin("_emdash_media_usage as u", (join) => join.onRef("u.source_key", "=", "s.source_key").onRef("u.generation", "=", "s.current_generation")).select(currentUsageSelect);
837
+ }
838
+ currentContentMediaUsageBaseQuery() {
839
+ return this.db.selectFrom("_emdash_media_usage as u").crossJoin("_emdash_media_usage_sources as s").innerJoin("_emdash_collections as collection", "collection.slug", "s.collection_slug").whereRef("s.source_key", "=", "u.source_key").whereRef("s.current_generation", "=", "u.generation").where("s.source_type", "=", "content").where("s.collection_slug", "is not", null).where("s.content_id", "is not", null).where("s.source_variant", "in", ["columns", "draft_overlay"]).where(contentSourceMatchesActiveCollection("s", "collection.id")).where(CONTENT_SOURCE_ELIGIBILITY);
840
+ }
841
+ async deleteOrphanCandidateIds(ids, cutoff, cleanupLease, canIssueStatement) {
842
+ let deleted = 0;
843
+ for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
844
+ if (!canIssueCleanupStatement(canIssueStatement)) break;
845
+ if (cleanupLease) {
846
+ await this.markOrphanCandidatesForCleanup(idBatch, cutoff, cleanupLease);
847
+ if (!canIssueCleanupStatement(canIssueStatement)) break;
848
+ }
849
+ let query = this.db.deleteFrom("_emdash_media_usage").where("id", "in", idBatch).where("created_at", "<", cutoff).where(sql`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources source WHERE source.source_key = _emdash_media_usage.source_key)`).where(this.noActiveGenerationWriteExpression());
850
+ if (cleanupLease) query = query.where("cleanup_lease_token", "=", cleanupLease.leaseToken).where(this.activeCleanupLeaseExpression(cleanupLease));
851
+ const result = await query.executeTakeFirst();
852
+ deleted += Number(result.numDeletedRows ?? 0);
853
+ }
854
+ return deleted;
855
+ }
856
+ async deleteStaleCandidateIds(ids, cutoff, cleanupLease, canIssueStatement) {
857
+ let deleted = 0;
858
+ for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
859
+ if (!canIssueCleanupStatement(canIssueStatement)) break;
860
+ if (cleanupLease) {
861
+ await this.markStaleCandidatesForCleanup(idBatch, cutoff, cleanupLease);
862
+ if (!canIssueCleanupStatement(canIssueStatement)) break;
863
+ }
864
+ let query = this.db.deleteFrom("_emdash_media_usage").where("id", "in", idBatch).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", "<", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression());
865
+ if (cleanupLease) query = query.where("cleanup_lease_token", "=", cleanupLease.leaseToken).where(this.activeCleanupLeaseExpression(cleanupLease));
866
+ const result = await query.executeTakeFirst();
867
+ deleted += Number(result.numDeletedRows ?? 0);
868
+ }
869
+ return deleted;
870
+ }
871
+ async deleteAbandonedCandidateIds(ids, cutoff, cleanupLease, canIssueStatement) {
872
+ let deleted = 0;
873
+ for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
874
+ if (!canIssueCleanupStatement(canIssueStatement)) break;
875
+ if (cleanupLease) {
876
+ await this.markAbandonedCandidatesForCleanup(idBatch, cutoff, cleanupLease);
877
+ if (!canIssueCleanupStatement(canIssueStatement)) break;
878
+ }
879
+ let query = this.db.deleteFrom("_emdash_media_usage").where("id", "in", idBatch).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", ">=", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression());
880
+ if (cleanupLease) query = query.where("cleanup_lease_token", "=", cleanupLease.leaseToken).where(this.activeCleanupLeaseExpression(cleanupLease));
881
+ const result = await query.executeTakeFirst();
882
+ deleted += Number(result.numDeletedRows ?? 0);
883
+ }
884
+ return deleted;
885
+ }
886
+ async markOrphanCandidatesForCleanup(ids, cutoff, cleanupLease) {
887
+ await this.db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: cleanupLease.leaseToken }).where("id", "in", ids).where("created_at", "<", cutoff).where(sql`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources source WHERE source.source_key = _emdash_media_usage.source_key)`).where(this.noActiveGenerationWriteExpression()).where(this.activeCleanupLeaseExpression(cleanupLease)).execute();
888
+ }
889
+ async markStaleCandidatesForCleanup(ids, cutoff, cleanupLease) {
890
+ await this.db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: cleanupLease.leaseToken }).where("id", "in", ids).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", "<", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression()).where(this.activeCleanupLeaseExpression(cleanupLease)).execute();
891
+ }
892
+ async markAbandonedCandidatesForCleanup(ids, cutoff, cleanupLease) {
893
+ await this.db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: cleanupLease.leaseToken }).where("id", "in", ids).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", ">=", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression()).where(this.activeCleanupLeaseExpression(cleanupLease)).execute();
894
+ }
895
+ noActiveGenerationWriteExpression(usageTable = "_emdash_media_usage") {
896
+ return sql`NOT EXISTS (
897
+ SELECT 1
898
+ FROM _emdash_media_usage_generation_writes AS writer
899
+ WHERE writer.source_key = ${sql.ref(`${usageTable}.source_key`)}
900
+ AND writer.generation = ${sql.ref(`${usageTable}.generation`)}
901
+ AND ${this.generationWriteLeaseExpiryIsInFuture("writer.expires_at")}
902
+ )`;
903
+ }
904
+ activeCleanupLeaseExpression(cleanupLease) {
905
+ const rowLock = isPostgres(this.db) ? sql` FOR UPDATE` : sql``;
906
+ return sql`EXISTS (
907
+ SELECT 1
908
+ FROM _emdash_media_usage_cleanup AS cleanup
909
+ WHERE cleanup.task_key = 'projection_gc'
910
+ AND cleanup.lease_token = ${cleanupLease.leaseToken}
911
+ AND ${this.cleanupLeaseExpiryIsInFuture("cleanup.lease_expires_at")}
912
+ ${rowLock}
913
+ )`;
914
+ }
915
+ cleanupLeaseExpiryIsInFuture(column) {
916
+ const leaseExpiresAt = sql.ref(column);
917
+ return isPostgres(this.db) ? sql`${leaseExpiresAt}::timestamptz > clock_timestamp()` : sql`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
918
+ }
919
+ cleanupTimestampIsDue(column) {
920
+ const timestamp = sql.ref(column);
921
+ return isPostgres(this.db) ? sql`${timestamp}::timestamptz <= clock_timestamp()` : sql`${timestamp} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
922
+ }
923
+ cleanupTimestampOffset(offsetSeconds) {
924
+ if (isPostgres(this.db)) return sql`to_char(
925
+ (clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
926
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
927
+ )`;
928
+ return sql`strftime(
929
+ '%Y-%m-%dT%H:%M:%fZ',
930
+ 'now',
931
+ ${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
932
+ )`;
933
+ }
934
+ generationWriteLeaseHasExpired(column) {
935
+ const leaseExpiresAt = sql.ref(column);
936
+ return isPostgres(this.db) ? sql`${leaseExpiresAt}::timestamptz <= clock_timestamp()` : sql`${leaseExpiresAt} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
937
+ }
938
+ async deleteSourceKeys(sourceKeys) {
939
+ const uniqueSourceKeys = [...new Set(sourceKeys)];
940
+ if (uniqueSourceKeys.length === 0) return 0;
941
+ return withTransaction(this.db, async (trx) => {
942
+ await this.lockCleanupBeforeSourceDelete(trx);
943
+ let deleted = 0;
944
+ for (const sourceKeyBatch of chunks(uniqueSourceKeys, SQL_BATCH_SIZE)) {
945
+ const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "in", sourceKeyBatch).executeTakeFirst();
946
+ deleted += Number(result.numDeletedRows ?? 0);
947
+ await trx.updateTable("_emdash_media_usage").set({ cleanup_lease_token: null }).where("source_key", "in", sourceKeyBatch).execute();
948
+ await trx.deleteFrom("_emdash_media_usage").where("source_key", "in", sourceKeyBatch).execute();
949
+ }
950
+ return deleted;
951
+ });
952
+ }
953
+ async deleteSourceGenerationOccurrences(db, sourceKey, generation) {
954
+ await db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: null }).where("source_key", "=", sourceKey).where("generation", "=", generation).execute();
955
+ await db.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).where("generation", "=", generation).execute();
956
+ }
957
+ async lockCleanupBeforeSourceDelete(db) {
958
+ if (!isPostgres(this.db)) return;
959
+ await sql`
960
+ SELECT 1
961
+ FROM _emdash_media_usage_cleanup
962
+ WHERE task_key = 'projection_gc'
963
+ FOR SHARE
964
+ `.execute(db);
965
+ }
966
+ async insertOccurrences(db, sourceKey, generation, occurrences, now) {
967
+ if (occurrences.length === 0) return;
968
+ const rows = occurrences.map((occurrence) => ({
969
+ id: ulid(),
970
+ source_key: sourceKey,
971
+ generation,
972
+ field_slug: occurrence.fieldSlug,
973
+ field_path: occurrence.fieldPath,
974
+ occurrence_index: occurrence.occurrenceIndex ?? 0,
975
+ reference_type: occurrence.referenceType,
976
+ media_id: occurrence.mediaId,
977
+ provider: occurrence.provider,
978
+ provider_asset_id: occurrence.providerAssetId,
979
+ media_kind: occurrence.mediaKind ?? null,
980
+ mime_type: occurrence.mimeType ?? null,
981
+ created_at: now
982
+ }));
983
+ for (const rowBatch of chunks(rows, OCCURRENCE_INSERT_BATCH_SIZE)) await db.insertInto("_emdash_media_usage").values(rowBatch).execute();
984
+ }
985
+ async lockCanonicalSourceCollection(db, source) {
986
+ if (source.collectionId === void 0 || source.collectionId === null) return true;
987
+ if (!source.collectionSlug) return false;
988
+ if (!isPostgres(this.db)) return true;
989
+ return await db.selectFrom("_emdash_collections").select("id").where("id", "=", source.collectionId).where("slug", "=", source.collectionSlug).forKeyShare().executeTakeFirst() !== void 0;
990
+ }
991
+ async upsertSource(db, source, generation, now, leaseToken) {
992
+ const row = this.buildSourceRow(source, generation, now);
993
+ return this.persistSourceIfWriteLease(db, row, leaseToken, sql`
994
+ ON CONFLICT (source_key) DO UPDATE SET
995
+ source_type = excluded.source_type,
996
+ collection_id = excluded.collection_id,
997
+ collection_slug = excluded.collection_slug,
998
+ content_id = excluded.content_id,
999
+ source_variant = excluded.source_variant,
1000
+ locale = excluded.locale,
1001
+ translation_group = excluded.translation_group,
1002
+ content_slug = excluded.content_slug,
1003
+ content_title = excluded.content_title,
1004
+ content_status = excluded.content_status,
1005
+ content_scheduled_at = excluded.content_scheduled_at,
1006
+ content_deleted_at = excluded.content_deleted_at,
1007
+ revision_id = excluded.revision_id,
1008
+ current_generation = excluded.current_generation,
1009
+ schema_version = excluded.schema_version,
1010
+ source_updated_at = excluded.source_updated_at,
1011
+ source_version = excluded.source_version,
1012
+ source_fingerprint = excluded.source_fingerprint,
1013
+ identity_version = excluded.identity_version,
1014
+ source_completeness = excluded.source_completeness,
1015
+ last_attempted_at = excluded.last_attempted_at,
1016
+ last_error_code = excluded.last_error_code,
1017
+ indexed_at = excluded.indexed_at,
1018
+ updated_at = excluded.updated_at
1019
+ `);
1020
+ }
1021
+ async insertSourceIfAbsent(db, row, leaseToken) {
1022
+ return this.persistSourceIfWriteLease(db, row, leaseToken, sql`ON CONFLICT (source_key) DO NOTHING`);
1023
+ }
1024
+ async persistSourceIfWriteLease(db, row, leaseToken, conflict) {
1025
+ const result = await sql`
1026
+ INSERT INTO _emdash_media_usage_sources (
1027
+ source_key,
1028
+ source_type,
1029
+ collection_id,
1030
+ collection_slug,
1031
+ content_id,
1032
+ source_variant,
1033
+ locale,
1034
+ translation_group,
1035
+ content_slug,
1036
+ content_title,
1037
+ content_status,
1038
+ content_scheduled_at,
1039
+ content_deleted_at,
1040
+ revision_id,
1041
+ current_generation,
1042
+ schema_version,
1043
+ source_updated_at,
1044
+ source_version,
1045
+ source_fingerprint,
1046
+ identity_version,
1047
+ source_completeness,
1048
+ last_attempted_at,
1049
+ last_error_code,
1050
+ indexed_at,
1051
+ updated_at
1052
+ )
1053
+ SELECT
1054
+ ${row.source_key},
1055
+ ${row.source_type},
1056
+ ${row.collection_id},
1057
+ ${row.collection_slug},
1058
+ ${row.content_id},
1059
+ ${row.source_variant},
1060
+ ${row.locale},
1061
+ ${row.translation_group},
1062
+ ${row.content_slug},
1063
+ ${row.content_title},
1064
+ ${row.content_status},
1065
+ ${row.content_scheduled_at},
1066
+ ${row.content_deleted_at},
1067
+ ${row.revision_id},
1068
+ ${row.current_generation},
1069
+ ${row.schema_version},
1070
+ ${row.source_updated_at},
1071
+ ${row.source_version},
1072
+ ${row.source_fingerprint},
1073
+ ${row.identity_version},
1074
+ ${row.source_completeness},
1075
+ ${row.last_attempted_at},
1076
+ ${row.last_error_code},
1077
+ ${row.indexed_at},
1078
+ ${row.updated_at}
1079
+ WHERE EXISTS (
1080
+ SELECT 1
1081
+ FROM _emdash_media_usage_generation_writes
1082
+ WHERE source_key = ${row.source_key}
1083
+ AND generation = ${row.current_generation}
1084
+ AND lease_token = ${leaseToken}
1085
+ AND ${this.generationWriteLeaseExpiryIsInFuture("expires_at")}
1086
+ )
1087
+ AND ${this.currentCollectionExists(row.collection_id, row.collection_slug)}
1088
+ AND ${this.currentCanonicalContentExists(row)}
1089
+ ${conflict}
1090
+ `.execute(db);
1091
+ return Number(result.numAffectedRows ?? 0) > 0;
1092
+ }
1093
+ generationWriteLeaseExpression(row, leaseToken) {
1094
+ return (eb) => eb.exists(eb.selectFrom("_emdash_media_usage_generation_writes").select("source_key").where("source_key", "=", row.source_key).where("generation", "=", row.current_generation).where("lease_token", "=", leaseToken).where(this.generationWriteLeaseExpiryIsInFuture("_emdash_media_usage_generation_writes.expires_at")));
1095
+ }
1096
+ generationWriteLeaseExpiryIsInFuture(column) {
1097
+ const leaseExpiresAt = sql.ref(column);
1098
+ return isPostgres(this.db) ? sql`${leaseExpiresAt}::timestamptz > clock_timestamp()` : sql`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
1099
+ }
1100
+ async withGenerationWriteLease(source, generation, write) {
1101
+ const leaseToken = ulid();
1102
+ const owner = (await sql`
1103
+ INSERT INTO _emdash_media_usage_generation_writes (
1104
+ source_key, generation, lease_token, expires_at, created_at
1105
+ )
1106
+ SELECT
1107
+ ${source.sourceKey},
1108
+ ${generation},
1109
+ ${leaseToken},
1110
+ ${this.generationWriteLeaseTimestampOffset(MEDIA_USAGE_GENERATION_WRITE_LEASE_MS / 1e3)},
1111
+ ${this.generationWriteLeaseTimestampOffset(0)}
1112
+ WHERE ${this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null)}
1113
+ RETURNING created_at
1114
+ `.execute(this.db)).rows[0];
1115
+ if (!owner) return false;
1116
+ try {
1117
+ await write(leaseToken, owner.created_at);
1118
+ return true;
1119
+ } finally {
1120
+ try {
1121
+ await this.db.deleteFrom("_emdash_media_usage_generation_writes").where("source_key", "=", source.sourceKey).where("generation", "=", generation).where("lease_token", "=", leaseToken).execute();
1122
+ } catch (error) {
1123
+ console.error("[media-usage] Failed to release generation write lease:", error);
1124
+ }
1125
+ }
1126
+ }
1127
+ generationWriteLeaseTimestampOffset(offsetSeconds) {
1128
+ if (isPostgres(this.db)) return sql`to_char(
1129
+ (clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
1130
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
1131
+ )`;
1132
+ return sql`strftime(
1133
+ '%Y-%m-%dT%H:%M:%fZ',
1134
+ 'now',
1135
+ ${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
1136
+ )`;
1137
+ }
1138
+ async updateSourceIfGeneration(db, row, expectedCurrentGeneration, leaseToken) {
1139
+ const result = await db.updateTable("_emdash_media_usage_sources").set(this.sourceUpdateSet(row)).where("source_key", "=", row.source_key).where("current_generation", "=", expectedCurrentGeneration).where(this.generationWriteLeaseExpression(row, leaseToken)).where(this.currentCollectionExists(row.collection_id, row.collection_slug)).where(this.currentCanonicalContentExists(row)).executeTakeFirst();
1140
+ return Number(result.numUpdatedRows ?? 0) > 0;
1141
+ }
1142
+ async updateSourceIfMatching(db, row, expectedSource, leaseToken) {
1143
+ const result = await db.updateTable("_emdash_media_usage_sources").set(this.sourceUpdateSet(row)).where("source_key", "=", row.source_key).where(this.sourceMatchExpression(expectedSource)).where(this.generationWriteLeaseExpression(row, leaseToken)).where(this.currentCollectionExists(row.collection_id, row.collection_slug)).where(this.currentCanonicalContentExists(row)).executeTakeFirst();
1144
+ return Number(result.numUpdatedRows ?? 0) > 0;
1145
+ }
1146
+ async updateAttemptedSourceIfMatching(db, source, row, expectedSource) {
1147
+ const result = await db.updateTable("_emdash_media_usage_sources").set(this.attemptedSourceUpdateSet(source, row)).where("source_key", "=", row.source_key).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(row.collection_id, row.collection_slug)).where(this.currentCanonicalContentExists(row)).executeTakeFirst();
1148
+ return Number(result.numUpdatedRows ?? 0) > 0;
1149
+ }
1150
+ sourceMatchExpression(expectedSource) {
1151
+ return (eb) => eb.and([
1152
+ eb("current_generation", "=", expectedSource.currentGeneration),
1153
+ eb("source_completeness", "=", expectedSource.sourceCompleteness),
1154
+ this.nullableStringExpression(eb, "collection_id", expectedSource.collectionId),
1155
+ this.nullableStringExpression(eb, "updated_at", expectedSource.updatedAt),
1156
+ this.nullableStringExpression(eb, "source_fingerprint", expectedSource.sourceFingerprint),
1157
+ this.nullableStringExpression(eb, "source_updated_at", expectedSource.sourceUpdatedAt),
1158
+ this.nullableNumberExpression(eb, "source_version", expectedSource.sourceVersion),
1159
+ this.nullableNumberExpression(eb, "identity_version", expectedSource.identityVersion),
1160
+ this.nullableStringExpression(eb, "revision_id", expectedSource.revisionId),
1161
+ this.nullableStringExpression(eb, "last_attempted_at", expectedSource.lastAttemptedAt),
1162
+ this.nullableStringExpression(eb, "last_error_code", expectedSource.lastErrorCode)
1163
+ ]);
1164
+ }
1165
+ async projectionMatchesCurrentGeneration(source, expectedCurrentGeneration) {
1166
+ const fingerprint = source.sourceFingerprint;
1167
+ if (!isMediaUsageProjectionFingerprint(fingerprint)) return false;
1168
+ return await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_key", "=", source.sourceKey).where("current_generation", "=", expectedCurrentGeneration).where("source_fingerprint", "=", fingerprint).where("source_completeness", "=", source.sourceCompleteness ?? "complete").where("last_error_code", "is", null).where(this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null)).executeTakeFirst() !== void 0;
1169
+ }
1170
+ async projectionMatchesExpectedSource(source, expectedSource) {
1171
+ const fingerprint = source.sourceFingerprint;
1172
+ if (!isMediaUsageProjectionFingerprint(fingerprint) || expectedSource.sourceFingerprint !== fingerprint || expectedSource.sourceCompleteness !== (source.sourceCompleteness ?? "complete") || expectedSource.lastErrorCode !== null) return false;
1173
+ return await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_key", "=", source.sourceKey).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null)).executeTakeFirst() !== void 0;
1174
+ }
1175
+ nullableStringExpression(eb, column, value) {
1176
+ return value === null ? eb(column, "is", null) : eb(column, "=", value);
1177
+ }
1178
+ currentCollectionExists(collectionId, collectionSlug) {
1179
+ if (collectionId === null) return sql`1 = 1`;
1180
+ return sql`EXISTS (
1181
+ SELECT 1
1182
+ FROM _emdash_collections
1183
+ WHERE id = ${collectionId}
1184
+ AND slug = ${collectionSlug}
1185
+ )`;
1186
+ }
1187
+ currentCanonicalContentExists(row) {
1188
+ if (row.collection_id === null || row.identity_version !== 1 || row.source_type !== "content") return sql`1 = 1`;
1189
+ if (!row.collection_slug || !row.content_id || row.source_version === null || row.source_updated_at === null) return sql`1 = 0`;
1190
+ validateIdentifier(row.collection_slug, "collection slug");
1191
+ const tableName = `ec_${row.collection_slug}`;
1192
+ validateIdentifier(tableName, "content table");
1193
+ const revisionColumn = row.source_variant === "columns" ? "live_revision_id" : row.source_variant === "draft_overlay" ? "draft_revision_id" : null;
1194
+ if (!revisionColumn) return sql`1 = 0`;
1195
+ const revision = sql.ref(`content.${revisionColumn}`);
1196
+ const revisionMatches = row.revision_id === null ? sql`${revision} IS NULL` : sql`${revision} = ${row.revision_id}`;
1197
+ return sql`EXISTS (
1198
+ SELECT 1
1199
+ FROM ${sql.ref(tableName)} AS content
1200
+ WHERE content.id = ${row.content_id}
1201
+ AND content.version = ${row.source_version}
1202
+ AND content.updated_at = ${row.source_updated_at}
1203
+ AND ${revisionMatches}
1204
+ )`;
1205
+ }
1206
+ nullableNumberExpression(eb, column, value) {
1207
+ return value === null ? eb(column, "is", null) : eb(column, "=", value);
1208
+ }
1209
+ async contentRowExists(tableName, contentId) {
1210
+ return (await sql`
1211
+ SELECT id
1212
+ FROM ${sql.ref(tableName)}
1213
+ WHERE id = ${contentId}
1214
+ LIMIT 1
1215
+ `.execute(this.db)).rows.length > 0;
1216
+ }
1217
+ buildSourceRow(source, generation, now) {
1218
+ return {
1219
+ source_key: source.sourceKey,
1220
+ source_type: source.sourceType,
1221
+ collection_id: source.collectionId ?? null,
1222
+ collection_slug: source.collectionSlug ?? null,
1223
+ content_id: source.contentId ?? null,
1224
+ source_variant: source.sourceVariant,
1225
+ locale: source.locale ?? null,
1226
+ translation_group: source.translationGroup ?? null,
1227
+ content_slug: source.contentSlug ?? null,
1228
+ content_title: source.contentTitle ?? null,
1229
+ content_status: source.contentStatus ?? null,
1230
+ content_scheduled_at: source.contentScheduledAt ?? null,
1231
+ content_deleted_at: source.contentDeletedAt ?? null,
1232
+ revision_id: source.revisionId ?? null,
1233
+ current_generation: generation,
1234
+ schema_version: source.schemaVersion ?? 1,
1235
+ source_updated_at: source.sourceUpdatedAt ?? null,
1236
+ source_version: source.sourceVersion ?? null,
1237
+ source_fingerprint: source.sourceFingerprint ?? null,
1238
+ identity_version: source.identityVersion ?? null,
1239
+ source_completeness: source.sourceCompleteness ?? "complete",
1240
+ last_attempted_at: source.lastAttemptedAt ?? now,
1241
+ last_error_code: null,
1242
+ indexed_at: now,
1243
+ updated_at: now
1244
+ };
1245
+ }
1246
+ buildAttemptedSourceRow(source, generation, now) {
1247
+ return {
1248
+ source_key: source.sourceKey,
1249
+ source_type: source.sourceType,
1250
+ collection_id: source.collectionId ?? null,
1251
+ collection_slug: source.collectionSlug ?? null,
1252
+ content_id: source.contentId ?? null,
1253
+ source_variant: source.sourceVariant,
1254
+ locale: source.locale ?? null,
1255
+ translation_group: source.translationGroup ?? null,
1256
+ content_slug: source.contentSlug ?? null,
1257
+ content_title: source.contentTitle ?? null,
1258
+ content_status: source.contentStatus ?? null,
1259
+ content_scheduled_at: source.contentScheduledAt ?? null,
1260
+ content_deleted_at: source.contentDeletedAt ?? null,
1261
+ revision_id: source.revisionId ?? null,
1262
+ current_generation: generation,
1263
+ schema_version: source.schemaVersion ?? 1,
1264
+ source_updated_at: source.sourceUpdatedAt ?? null,
1265
+ source_version: source.sourceVersion ?? null,
1266
+ source_fingerprint: source.sourceFingerprint ?? null,
1267
+ identity_version: source.identityVersion ?? null,
1268
+ source_completeness: source.sourceCompleteness ?? (source.lastErrorCode ? "failed" : "unknown"),
1269
+ last_attempted_at: source.lastAttemptedAt ?? now,
1270
+ last_error_code: source.lastErrorCode ?? null,
1271
+ indexed_at: now,
1272
+ updated_at: now
1273
+ };
1274
+ }
1275
+ attemptedSourceUpdateSet(source, row) {
1276
+ const updates = {
1277
+ source_type: row.source_type,
1278
+ source_variant: row.source_variant,
1279
+ source_completeness: row.source_completeness,
1280
+ last_attempted_at: row.last_attempted_at,
1281
+ last_error_code: row.last_error_code,
1282
+ updated_at: row.updated_at
1283
+ };
1284
+ if (source.collectionSlug !== void 0) updates.collection_slug = row.collection_slug;
1285
+ if (source.collectionId !== void 0) updates.collection_id = row.collection_id;
1286
+ if (source.contentId !== void 0) updates.content_id = row.content_id;
1287
+ if (source.locale !== void 0) updates.locale = row.locale;
1288
+ if (source.translationGroup !== void 0) updates.translation_group = row.translation_group;
1289
+ if (source.contentSlug !== void 0) updates.content_slug = row.content_slug;
1290
+ if (source.contentTitle !== void 0) updates.content_title = row.content_title;
1291
+ if (source.contentStatus !== void 0) updates.content_status = row.content_status;
1292
+ if (source.contentScheduledAt !== void 0) updates.content_scheduled_at = row.content_scheduled_at;
1293
+ if (source.contentDeletedAt !== void 0) updates.content_deleted_at = row.content_deleted_at;
1294
+ if (source.revisionId !== void 0) updates.revision_id = row.revision_id;
1295
+ if (source.schemaVersion !== void 0) updates.schema_version = row.schema_version;
1296
+ if (source.sourceUpdatedAt !== void 0) updates.source_updated_at = row.source_updated_at;
1297
+ if (source.sourceVersion !== void 0) updates.source_version = row.source_version;
1298
+ if (source.sourceFingerprint !== void 0) updates.source_fingerprint = row.source_fingerprint;
1299
+ if (source.identityVersion !== void 0) updates.identity_version = row.identity_version;
1300
+ return updates;
1301
+ }
1302
+ sourceUpdateSet(row) {
1303
+ return {
1304
+ source_type: row.source_type,
1305
+ collection_id: row.collection_id,
1306
+ collection_slug: row.collection_slug,
1307
+ content_id: row.content_id,
1308
+ source_variant: row.source_variant,
1309
+ locale: row.locale,
1310
+ translation_group: row.translation_group,
1311
+ content_slug: row.content_slug,
1312
+ content_title: row.content_title,
1313
+ content_status: row.content_status,
1314
+ content_scheduled_at: row.content_scheduled_at,
1315
+ content_deleted_at: row.content_deleted_at,
1316
+ revision_id: row.revision_id,
1317
+ current_generation: row.current_generation,
1318
+ schema_version: row.schema_version,
1319
+ source_updated_at: row.source_updated_at,
1320
+ source_version: row.source_version,
1321
+ source_fingerprint: row.source_fingerprint,
1322
+ identity_version: row.identity_version,
1323
+ source_completeness: row.source_completeness,
1324
+ last_attempted_at: row.last_attempted_at,
1325
+ last_error_code: row.last_error_code,
1326
+ indexed_at: row.indexed_at,
1327
+ updated_at: row.updated_at
1328
+ };
1329
+ }
1330
+ };
1331
+ const currentUsageSelect = [
1332
+ "s.source_key as source_key",
1333
+ "s.source_type as source_type",
1334
+ "s.collection_id as collection_id",
1335
+ "s.collection_slug as collection_slug",
1336
+ "s.content_id as content_id",
1337
+ "s.source_variant as source_variant",
1338
+ "s.locale as locale",
1339
+ "s.translation_group as translation_group",
1340
+ "s.content_slug as content_slug",
1341
+ "s.content_title as content_title",
1342
+ "s.content_status as content_status",
1343
+ "s.content_scheduled_at as content_scheduled_at",
1344
+ "s.content_deleted_at as content_deleted_at",
1345
+ "s.revision_id as revision_id",
1346
+ "s.current_generation as current_generation",
1347
+ "s.schema_version as schema_version",
1348
+ "s.source_updated_at as source_updated_at",
1349
+ "s.source_version as source_version",
1350
+ "s.source_fingerprint as source_fingerprint",
1351
+ "s.identity_version as identity_version",
1352
+ "s.source_completeness as source_completeness",
1353
+ "s.last_attempted_at as last_attempted_at",
1354
+ "s.last_error_code as last_error_code",
1355
+ "s.indexed_at as indexed_at",
1356
+ "s.created_at as source_created_at",
1357
+ "s.updated_at as source_row_updated_at",
1358
+ "u.id as occurrence_id",
1359
+ "u.generation as generation",
1360
+ "u.field_slug as field_slug",
1361
+ "u.field_path as field_path",
1362
+ "u.occurrence_index as occurrence_index",
1363
+ "u.reference_type as reference_type",
1364
+ "u.media_id as media_id",
1365
+ "u.provider as provider",
1366
+ "u.provider_asset_id as provider_asset_id",
1367
+ "u.media_kind as media_kind",
1368
+ "u.mime_type as mime_type",
1369
+ "u.created_at as occurrence_created_at"
1370
+ ];
1371
+ function groupUsageRows(rows) {
1372
+ const groups = [];
1373
+ for (const row of rows) {
1374
+ if (row.collection_slug === null || row.content_id === null) continue;
1375
+ const record = rowToUsageRecord(row);
1376
+ let group = groups.at(-1);
1377
+ if (!group || group.collectionSlug !== row.collection_slug || group.contentId !== row.content_id) {
1378
+ group = {
1379
+ collectionSlug: row.collection_slug,
1380
+ contentId: row.content_id,
1381
+ contentDeletedAt: row.entry_deleted_at,
1382
+ sources: []
1383
+ };
1384
+ groups.push(group);
1385
+ }
1386
+ let source = group.sources.at(-1);
1387
+ if (!source || source.source.sourceKey !== record.source.sourceKey) {
1388
+ source = {
1389
+ source: record.source,
1390
+ occurrences: []
1391
+ };
1392
+ group.sources.push(source);
1393
+ }
1394
+ source.occurrences.push(record.occurrence);
1395
+ }
1396
+ return groups;
1397
+ }
1398
+ function rowToSource(row) {
1399
+ return {
1400
+ sourceKey: row.source_key,
1401
+ sourceType: row.source_type,
1402
+ collectionId: row.collection_id,
1403
+ collectionSlug: row.collection_slug,
1404
+ contentId: row.content_id,
1405
+ sourceVariant: row.source_variant,
1406
+ locale: row.locale,
1407
+ translationGroup: row.translation_group,
1408
+ contentSlug: row.content_slug,
1409
+ contentTitle: row.content_title,
1410
+ contentStatus: row.content_status,
1411
+ contentScheduledAt: row.content_scheduled_at,
1412
+ contentDeletedAt: row.content_deleted_at,
1413
+ revisionId: row.revision_id,
1414
+ currentGeneration: row.current_generation,
1415
+ schemaVersion: Number(row.schema_version),
1416
+ sourceUpdatedAt: row.source_updated_at,
1417
+ sourceVersion: row.source_version === null ? null : Number(row.source_version),
1418
+ sourceFingerprint: row.source_fingerprint,
1419
+ identityVersion: row.identity_version === null ? null : Number(row.identity_version),
1420
+ sourceCompleteness: row.source_completeness,
1421
+ lastAttemptedAt: row.last_attempted_at,
1422
+ lastErrorCode: row.last_error_code,
1423
+ indexedAt: row.indexed_at,
1424
+ createdAt: row.created_at,
1425
+ updatedAt: row.updated_at
1426
+ };
1427
+ }
1428
+ function rowToOccurrence(row) {
1429
+ return {
1430
+ id: row.id,
1431
+ sourceKey: row.source_key,
1432
+ generation: row.generation,
1433
+ fieldSlug: row.field_slug,
1434
+ fieldPath: row.field_path,
1435
+ occurrenceIndex: Number(row.occurrence_index),
1436
+ referenceType: row.reference_type,
1437
+ mediaId: row.media_id,
1438
+ provider: row.provider,
1439
+ providerAssetId: row.provider_asset_id,
1440
+ mediaKind: row.media_kind,
1441
+ mimeType: row.mime_type,
1442
+ createdAt: row.created_at
1443
+ };
1444
+ }
1445
+ function rowToUsageRecord(row) {
1446
+ return {
1447
+ source: rowToSource({
1448
+ source_key: row.source_key,
1449
+ source_type: row.source_type,
1450
+ collection_id: row.collection_id,
1451
+ collection_slug: row.collection_slug,
1452
+ content_id: row.content_id,
1453
+ source_variant: row.source_variant,
1454
+ locale: row.locale,
1455
+ translation_group: row.translation_group,
1456
+ content_slug: row.content_slug,
1457
+ content_title: row.content_title,
1458
+ content_status: row.content_status,
1459
+ content_scheduled_at: row.content_scheduled_at,
1460
+ content_deleted_at: row.content_deleted_at,
1461
+ revision_id: row.revision_id,
1462
+ current_generation: row.current_generation,
1463
+ schema_version: row.schema_version,
1464
+ source_updated_at: row.source_updated_at,
1465
+ source_version: row.source_version,
1466
+ source_fingerprint: row.source_fingerprint,
1467
+ identity_version: row.identity_version,
1468
+ source_completeness: row.source_completeness,
1469
+ last_attempted_at: row.last_attempted_at,
1470
+ last_error_code: row.last_error_code,
1471
+ indexed_at: row.indexed_at,
1472
+ created_at: row.source_created_at,
1473
+ updated_at: row.source_row_updated_at
1474
+ }),
1475
+ occurrence: rowToOccurrence({
1476
+ id: row.occurrence_id,
1477
+ source_key: row.source_key,
1478
+ generation: row.generation,
1479
+ field_slug: row.field_slug,
1480
+ field_path: row.field_path,
1481
+ occurrence_index: row.occurrence_index,
1482
+ reference_type: row.reference_type,
1483
+ media_id: row.media_id,
1484
+ provider: row.provider,
1485
+ provider_asset_id: row.provider_asset_id,
1486
+ media_kind: row.media_kind,
1487
+ mime_type: row.mime_type,
1488
+ created_at: row.occurrence_created_at,
1489
+ cleanup_lease_token: null
1490
+ })
1491
+ };
1492
+ }
1493
+ function rowToIndexStatus(row) {
1494
+ return {
1495
+ adapterId: row.adapter_id,
1496
+ scopeType: row.scope_type,
1497
+ scopeKey: row.scope_key,
1498
+ status: row.status,
1499
+ schemaVersion: Number(row.schema_version),
1500
+ startedAt: row.started_at,
1501
+ completedAt: row.completed_at,
1502
+ cursor: row.cursor,
1503
+ indexedSourceCount: Number(row.indexed_source_count),
1504
+ failedSourceCount: Number(row.failed_source_count),
1505
+ lastErrorCode: row.last_error_code,
1506
+ updatedAt: row.updated_at
1507
+ };
1508
+ }
1509
+
1510
+ //#endregion
1511
+ //#region src/media/usage/types.ts
1512
+ const CONTENT_SOURCE_SCHEMA_VERSION = 1;
1513
+
1514
+ //#endregion
1515
+ //#region src/media/usage/content-fields.ts
1516
+ async function buildContentMediaUsageFieldFingerprint(discovery) {
1517
+ return (await buildCanonicalSha256Fingerprint("media-usage-fields:v1:sha256:", {
1518
+ fingerprintVersion: 1,
1519
+ contentSourceSchemaVersion: CONTENT_SOURCE_SCHEMA_VERSION,
1520
+ extractionFields: discovery.extractionFields.map((field) => ({
1521
+ slug: field.slug,
1522
+ type: field.type,
1523
+ ...field.type === "repeater" ? { subFields: (field.validation?.subFields ?? []).map((subField) => ({
1524
+ slug: subField.slug,
1525
+ type: subField.type
1526
+ })).toSorted(compareFieldIdentity) } : {}
1527
+ })).toSorted(compareFieldIdentity),
1528
+ displayFieldSlugs: discovery.displayFieldSlugs.toSorted(compareStrings)
1529
+ })).fingerprint;
1530
+ }
1531
+ var MediaUsageFieldDiscoveryError = class extends Error {
1532
+ constructor(message, code) {
1533
+ super(message);
1534
+ this.code = code;
1535
+ this.name = "MediaUsageFieldDiscoveryError";
1536
+ }
1537
+ };
1538
+ const DISPLAY_FIELD_SLUGS = ["title", "name"];
1539
+ const SUPPORTED_TOP_LEVEL_TYPES = [
1540
+ "file",
1541
+ "image",
1542
+ "portableText"
1543
+ ];
1544
+ async function loadContentMediaUsageFields(db, collectionSlug, collectionId) {
1545
+ validateIdentifier(collectionSlug, "collection slug");
1546
+ let query = db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
1547
+ "_emdash_fields.slug",
1548
+ "_emdash_fields.type",
1549
+ "_emdash_fields.validation"
1550
+ ]).where("_emdash_collections.slug", "=", collectionSlug);
1551
+ if (collectionId !== void 0) query = query.where("_emdash_collections.id", "=", collectionId);
1552
+ const rows = await query.execute();
1553
+ const extractionFields = [];
1554
+ const rowBySlug = /* @__PURE__ */ new Map();
1555
+ for (const row of rows) {
1556
+ rowBySlug.set(row.slug, row);
1557
+ if (isSupportedTopLevelType(row.type)) {
1558
+ validateIdentifier(row.slug, "media usage field slug");
1559
+ extractionFields.push({
1560
+ slug: row.slug,
1561
+ type: row.type
1562
+ });
1563
+ continue;
1564
+ }
1565
+ if (row.type === "repeater") {
1566
+ validateIdentifier(row.slug, "media usage field slug");
1567
+ const subFields = normalizeRepeaterImageSubFields(row.validation);
1568
+ if (subFields.length > 0) extractionFields.push({
1569
+ slug: row.slug,
1570
+ type: "repeater",
1571
+ validation: { subFields }
1572
+ });
1573
+ }
1574
+ }
1575
+ extractionFields.sort((a, b) => a.slug.localeCompare(b.slug));
1576
+ return {
1577
+ extractionFields,
1578
+ displayFieldSlugs: DISPLAY_FIELD_SLUGS.filter((slug) => {
1579
+ if (!rowBySlug.has(slug)) return false;
1580
+ validateIdentifier(slug, "media usage display field slug");
1581
+ return true;
1582
+ })
1583
+ };
1584
+ }
1585
+ function normalizeRepeaterImageSubFields(rawValidation) {
1586
+ const validation = parseValidation(rawValidation);
1587
+ if (!isRecord$2(validation) || !Array.isArray(validation.subFields)) return [];
1588
+ const subFields = [];
1589
+ for (const subField of validation.subFields) {
1590
+ if (!isRecord$2(subField) || subField.type !== "image") continue;
1591
+ if (typeof subField.slug !== "string") continue;
1592
+ validateIdentifier(subField.slug, "media usage repeater sub-field slug");
1593
+ subFields.push({
1594
+ slug: subField.slug,
1595
+ type: "image"
1596
+ });
1597
+ }
1598
+ return subFields.toSorted((a, b) => a.slug.localeCompare(b.slug));
1599
+ }
1600
+ function parseValidation(rawValidation) {
1601
+ if (!rawValidation) return null;
1602
+ try {
1603
+ return JSON.parse(rawValidation);
1604
+ } catch {
1605
+ throw new MediaUsageFieldDiscoveryError("Repeater field validation must be valid JSON before media usage can be discovered", "INVALID_REPEATER_VALIDATION");
1606
+ }
1607
+ }
1608
+ function isSupportedTopLevelType(value) {
1609
+ return SUPPORTED_TOP_LEVEL_TYPES.includes(value);
1610
+ }
1611
+ function isRecord$2(value) {
1612
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1613
+ }
1614
+ function compareFieldIdentity(a, b) {
1615
+ return compareStrings(a.slug, b.slug);
1616
+ }
1617
+ function compareStrings(a, b) {
1618
+ return a < b ? -1 : a > b ? 1 : 0;
1619
+ }
1620
+
1621
+ //#endregion
1622
+ //#region src/media/usage/extractor.ts
1623
+ const URL_LIKE_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
1624
+ function extractMediaUsageOccurrences({ fields, data }) {
1625
+ const occurrences = [];
1626
+ const seen = /* @__PURE__ */ new Set();
1627
+ for (const field of fields) {
1628
+ const value = data[field.slug];
1629
+ if (field.type === "image") {
1630
+ addOccurrence(occurrences, seen, {
1631
+ fieldSlug: field.slug,
1632
+ fieldPath: field.slug,
1633
+ referenceType: "image_field",
1634
+ value,
1635
+ fallbackKind: "image"
1636
+ });
1637
+ continue;
1638
+ }
1639
+ if (field.type === "file") {
1640
+ addOccurrence(occurrences, seen, {
1641
+ fieldSlug: field.slug,
1642
+ fieldPath: field.slug,
1643
+ referenceType: "file_field",
1644
+ value,
1645
+ fallbackKind: null
1646
+ });
1647
+ continue;
1648
+ }
1649
+ if (field.type === "repeater") {
1650
+ extractRepeaterOccurrences(occurrences, seen, field.slug, value, field.validation?.subFields);
1651
+ continue;
1652
+ }
1653
+ if (field.type === "portableText") extractPortableTextOccurrences(occurrences, seen, field.slug, value);
1654
+ }
1655
+ return occurrences;
1656
+ }
1657
+ function extractRepeaterOccurrences(occurrences, seen, fieldSlug, value, subFields) {
1658
+ if (!Array.isArray(value) || !Array.isArray(subFields)) return;
1659
+ for (const [itemIndex, item] of value.entries()) {
1660
+ if (!isRecord$1(item)) continue;
1661
+ for (const subField of subFields) {
1662
+ if (subField.type !== "image") continue;
1663
+ addOccurrence(occurrences, seen, {
1664
+ fieldSlug,
1665
+ fieldPath: `${fieldSlug}[${itemIndex}].${subField.slug}`,
1666
+ referenceType: "image_field",
1667
+ value: item[subField.slug],
1668
+ fallbackKind: "image"
1669
+ });
1670
+ }
1671
+ }
1672
+ }
1673
+ function extractPortableTextOccurrences(occurrences, seen, fieldSlug, value) {
1674
+ if (!Array.isArray(value)) return;
1675
+ for (const [blockIndex, block] of value.entries()) {
1676
+ if (!isRecord$1(block) || block._type !== "image" || !isRecord$1(block.asset)) continue;
1677
+ const provider = normalizeProvider(block.asset.provider);
1678
+ const ref = readPortableTextAssetRef(block.asset, provider);
1679
+ if (!ref) continue;
1680
+ addRefOccurrence(occurrences, seen, {
1681
+ fieldSlug,
1682
+ fieldPath: `${fieldSlug}[${blockIndex}].asset.${ref.key}`,
1683
+ referenceType: "portable_text_image",
1684
+ ref: buildMediaRef({
1685
+ id: ref.id,
1686
+ provider,
1687
+ mimeType: normalizeMimeValue(block.asset.mimeType),
1688
+ fallbackKind: "image"
1689
+ })
1690
+ });
1691
+ }
1692
+ }
1693
+ function addOccurrence(occurrences, seen, input) {
1694
+ const ref = readMediaRef(input.value, input.fallbackKind);
1695
+ if (!ref) return;
1696
+ addRefOccurrence(occurrences, seen, {
1697
+ fieldSlug: input.fieldSlug,
1698
+ fieldPath: input.fieldPath,
1699
+ referenceType: input.referenceType,
1700
+ ref
1701
+ });
1702
+ }
1703
+ function addRefOccurrence(occurrences, seen, input) {
1704
+ if (!input.ref) return;
1705
+ const occurrence = {
1706
+ fieldSlug: input.fieldSlug,
1707
+ fieldPath: input.fieldPath,
1708
+ occurrenceIndex: 0,
1709
+ referenceType: input.referenceType,
1710
+ mediaId: input.ref.mediaId,
1711
+ provider: input.ref.provider,
1712
+ providerAssetId: input.ref.providerAssetId,
1713
+ mediaKind: input.ref.mediaKind,
1714
+ mimeType: input.ref.mimeType
1715
+ };
1716
+ const key = [
1717
+ occurrence.fieldSlug,
1718
+ occurrence.fieldPath,
1719
+ occurrence.occurrenceIndex,
1720
+ occurrence.referenceType,
1721
+ occurrence.provider,
1722
+ occurrence.providerAssetId,
1723
+ occurrence.mediaId ?? ""
1724
+ ].join("\0");
1725
+ if (seen.has(key)) return;
1726
+ seen.add(key);
1727
+ occurrences.push(occurrence);
1728
+ }
1729
+ function readMediaRef(value, fallbackKind) {
1730
+ if (typeof value === "string") {
1731
+ const id = normalizeLocalMediaId(value);
1732
+ return id ? buildMediaRef({
1733
+ id,
1734
+ provider: "local",
1735
+ mimeType: null,
1736
+ fallbackKind
1737
+ }) : null;
1738
+ }
1739
+ if (!isRecord$1(value)) return null;
1740
+ const provider = normalizeProvider(value.provider);
1741
+ const id = provider === "local" ? normalizeLocalMediaId(value.id) : normalizeStableId(value.id);
1742
+ if (!id) return null;
1743
+ return buildMediaRef({
1744
+ id,
1745
+ provider,
1746
+ mimeType: normalizeMimeValue(value.mimeType),
1747
+ fallbackKind
1748
+ });
1749
+ }
1750
+ function buildMediaRef(input) {
1751
+ const provider = normalizeProvider(input.provider);
1752
+ if (provider === "external") return null;
1753
+ return {
1754
+ mediaId: provider === "local" ? input.id : null,
1755
+ provider,
1756
+ providerAssetId: input.id,
1757
+ mediaKind: mediaKindFromMime(input.mimeType) ?? input.fallbackKind,
1758
+ mimeType: input.mimeType
1759
+ };
1760
+ }
1761
+ function readPortableTextAssetRef(asset, provider) {
1762
+ const normalizeId = provider === "local" ? normalizeLocalMediaId : normalizeStableId;
1763
+ const ref = normalizeId(asset._ref);
1764
+ if (ref) return {
1765
+ key: "_ref",
1766
+ id: ref
1767
+ };
1768
+ const id = normalizeId(asset.id);
1769
+ if (id) return {
1770
+ key: "id",
1771
+ id
1772
+ };
1773
+ return null;
1774
+ }
1775
+ function normalizeProvider(value) {
1776
+ return readString$1(value)?.trim() || "local";
1777
+ }
1778
+ function normalizeLocalMediaId(value) {
1779
+ const id = normalizeStableId(value);
1780
+ if (!id) return null;
1781
+ return id.includes("/") ? null : id;
1782
+ }
1783
+ function normalizeStableId(value) {
1784
+ if (typeof value !== "string") return null;
1785
+ const trimmed = value.trim();
1786
+ if (!trimmed) return null;
1787
+ if (URL_LIKE_RE.test(trimmed)) return null;
1788
+ if (trimmed.startsWith(INTERNAL_MEDIA_PREFIX)) return null;
1789
+ return trimmed;
1790
+ }
1791
+ function normalizeMimeValue(value) {
1792
+ if (typeof value !== "string") return null;
1793
+ const normalized = normalizeMime(value);
1794
+ return normalized.includes("/") ? normalized : null;
1795
+ }
1796
+ function mediaKindFromMime(mimeType) {
1797
+ if (!mimeType) return null;
1798
+ if (mimeType.startsWith("image/")) return "image";
1799
+ if (mimeType.startsWith("video/")) return "video";
1800
+ if (mimeType.startsWith("audio/")) return "audio";
1801
+ if (mimeType.startsWith("font/") || mimeType.startsWith("application/font-")) return "font";
1802
+ if (mimeType.startsWith("text/")) return "text";
1803
+ if (isDocumentMime(mimeType)) return "document";
1804
+ if (isArchiveMime(mimeType)) return "archive";
1805
+ return "other";
1806
+ }
1807
+ function isDocumentMime(mimeType) {
1808
+ return mimeType === "application/pdf" || mimeType === "application/msword" || mimeType === "application/rtf" || mimeType === "application/vnd.ms-excel" || mimeType === "application/vnd.ms-powerpoint" || mimeType.startsWith("application/vnd.openxmlformats-officedocument.");
1809
+ }
1810
+ function isArchiveMime(mimeType) {
1811
+ return mimeType === "application/zip" || mimeType === "application/gzip" || mimeType === "application/x-tar" || mimeType === "application/x-7z-compressed" || mimeType === "application/x-rar-compressed" || mimeType === "application/vnd.rar";
1812
+ }
1813
+ function readString$1(value) {
1814
+ return typeof value === "string" ? value : null;
1815
+ }
1816
+ function isRecord$1(value) {
1817
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1818
+ }
1819
+
1820
+ //#endregion
1821
+ //#region src/media/usage/source-key.ts
1822
+ const MEDIA_USAGE_CONTENT_SOURCE_VARIANTS = ["columns", "draft_overlay"];
1823
+ function buildContentMediaUsageSourceKey(input) {
1824
+ if (input.collectionId) return `content:${input.collectionId}:${input.contentId}:${input.sourceVariant}`;
1825
+ return `content:${input.collectionSlug}:${input.contentId}:${input.sourceVariant}`;
1826
+ }
1827
+
1828
+ //#endregion
1829
+ //#region src/media/usage/content-snapshots.ts
1830
+ const CONTENT_COLLECTION_ID_RESULT = "__emdash_media_usage_collection_id";
1831
+ const CONTENT_SYSTEM_COLUMNS = [
1832
+ "id",
1833
+ "slug",
1834
+ "status",
1835
+ "created_at",
1836
+ "updated_at",
1837
+ "published_at",
1838
+ "scheduled_at",
1839
+ "deleted_at",
1840
+ "version",
1841
+ "live_revision_id",
1842
+ "draft_revision_id",
1843
+ "locale",
1844
+ "translation_group"
1845
+ ];
1846
+ async function loadContentMediaUsageSnapshots(db, collectionSlug, contentId, fieldDiscovery, options = {}) {
1847
+ validateIdentifier(collectionSlug, "collection slug");
1848
+ if (options.identityVersion !== void 0 && !options.collectionId) throw new Error("Canonical media usage snapshots require a collection identity");
1849
+ const discovery = fieldDiscovery ?? await loadContentMediaUsageFields(db, collectionSlug);
1850
+ const row = await loadContentRow(db, collectionSlug, contentId, [...discovery.extractionFields.map((field) => field.slug), ...discovery.displayFieldSlugs], options.collectionId);
1851
+ if (!row) return {
1852
+ success: false,
1853
+ error: "CONTENT_NOT_FOUND"
1854
+ };
1855
+ const collectionId = readString(row[CONTENT_COLLECTION_ID_RESULT]);
1856
+ if (!collectionId) throw new Error("Media usage snapshot query did not return a collection identity");
1857
+ const columnsData = projectData(row, discovery.extractionFields.map((field) => field.slug));
1858
+ const displayData = projectRawData(row, discovery.displayFieldSlugs);
1859
+ const occurrences = extractMediaUsageOccurrences({
1860
+ fields: discovery.extractionFields,
1861
+ data: columnsData
1862
+ });
1863
+ const columnsRevisionId = readNullableString(row.live_revision_id);
1864
+ const columnsSource = buildContentSource({
1865
+ collectionId: options.collectionId,
1866
+ collectionSlug,
1867
+ identityVersion: options.identityVersion,
1868
+ row,
1869
+ displayData,
1870
+ sourceVariant: "columns",
1871
+ revisionId: columnsRevisionId
1872
+ });
1873
+ const columnsProjection = await buildMediaUsageProjectionFingerprint({
1874
+ collectionId,
1875
+ source: columnsSource,
1876
+ occurrences,
1877
+ extractionFields: discovery.extractionFields
1878
+ });
1879
+ columnsSource.sourceFingerprint = columnsProjection.fingerprint;
1880
+ const snapshots = [{
1881
+ source: columnsSource,
1882
+ occurrences,
1883
+ fields: discovery.extractionFields,
1884
+ projectionByteLength: columnsProjection.byteLength
1885
+ }];
1886
+ const draftRevisionId = readNullableString(row.draft_revision_id);
1887
+ if (draftRevisionId) {
1888
+ const attemptedDraftSource = buildContentSource({
1889
+ collectionId: options.collectionId,
1890
+ collectionSlug,
1891
+ identityVersion: options.identityVersion,
1892
+ row,
1893
+ displayData,
1894
+ sourceVariant: "draft_overlay",
1895
+ revisionId: draftRevisionId
1896
+ });
1897
+ const revisionResult = await loadRevisionRow(db, draftRevisionId);
1898
+ if (!revisionResult) return {
1899
+ success: false,
1900
+ error: "DRAFT_REVISION_NOT_FOUND",
1901
+ source: attemptedDraftSource,
1902
+ snapshots
1903
+ };
1904
+ if (!revisionResult.success) return {
1905
+ success: false,
1906
+ error: "DRAFT_REVISION_INVALID",
1907
+ source: attemptedDraftSource,
1908
+ snapshots
1909
+ };
1910
+ const revision = revisionResult.revision;
1911
+ if (revision.collection !== collectionSlug || revision.entryId !== row.id) return {
1912
+ success: false,
1913
+ error: "DRAFT_REVISION_MISMATCH",
1914
+ source: attemptedDraftSource,
1915
+ snapshots
1916
+ };
1917
+ const revisionData = stripRevisionMetadata(revision.data);
1918
+ const draftOverlayData = {
1919
+ ...columnsData,
1920
+ ...revisionData
1921
+ };
1922
+ const draftDisplayData = {
1923
+ ...displayData,
1924
+ ...projectPresentData(revisionData, discovery.displayFieldSlugs)
1925
+ };
1926
+ const draftContentSlug = readNullableString(revision.data._slug) ?? readNullableString(row.slug);
1927
+ const draftOccurrences = extractMediaUsageOccurrences({
1928
+ fields: discovery.extractionFields,
1929
+ data: draftOverlayData
1930
+ });
1931
+ const draftSource = buildContentSource({
1932
+ collectionId: options.collectionId,
1933
+ collectionSlug,
1934
+ identityVersion: options.identityVersion,
1935
+ row,
1936
+ displayData: draftDisplayData,
1937
+ sourceVariant: "draft_overlay",
1938
+ revisionId: draftRevisionId,
1939
+ contentSlug: draftContentSlug
1940
+ });
1941
+ const draftProjection = await buildMediaUsageProjectionFingerprint({
1942
+ collectionId,
1943
+ source: draftSource,
1944
+ occurrences: draftOccurrences,
1945
+ extractionFields: discovery.extractionFields
1946
+ });
1947
+ draftSource.sourceFingerprint = draftProjection.fingerprint;
1948
+ snapshots.push({
1949
+ source: draftSource,
1950
+ occurrences: draftOccurrences,
1951
+ fields: discovery.extractionFields,
1952
+ projectionByteLength: draftProjection.byteLength
1953
+ });
1954
+ }
1955
+ return {
1956
+ success: true,
1957
+ snapshots
1958
+ };
1959
+ }
1960
+ async function loadContentRow(db, collectionSlug, contentId, fieldSlugs, expectedCollectionId) {
1961
+ const tableName = getContentTableName(collectionSlug);
1962
+ const columnRefs = uniqueColumns([...CONTENT_SYSTEM_COLUMNS, ...fieldSlugs]).map((column) => sql.ref(`content.${column}`));
1963
+ return (await sql`
1964
+ SELECT
1965
+ ${sql.join(columnRefs, sql`, `)},
1966
+ collection.id AS __emdash_media_usage_collection_id
1967
+ FROM ${sql.ref(tableName)} AS content
1968
+ INNER JOIN _emdash_collections AS collection
1969
+ ON collection.slug = ${collectionSlug}
1970
+ ${expectedCollectionId ? sql`AND collection.id = ${expectedCollectionId}` : sql``}
1971
+ WHERE content.id = ${contentId}
1972
+ LIMIT 1
1973
+ `.execute(db)).rows[0] ?? null;
1974
+ }
1975
+ async function loadRevisionRow(db, revisionId) {
1976
+ const row = await db.selectFrom("revisions").select([
1977
+ "id",
1978
+ "collection",
1979
+ "entry_id",
1980
+ "data"
1981
+ ]).where("id", "=", revisionId).executeTakeFirst();
1982
+ if (!row) return null;
1983
+ const data = parseRevisionData(row.data);
1984
+ if (!data) return { success: false };
1985
+ return {
1986
+ success: true,
1987
+ revision: {
1988
+ id: row.id,
1989
+ collection: row.collection,
1990
+ entryId: row.entry_id,
1991
+ data
1992
+ }
1993
+ };
1994
+ }
1995
+ function buildContentSource(input) {
1996
+ const { collectionId, collectionSlug, identityVersion, row, displayData, sourceVariant, revisionId } = input;
1997
+ const contentId = readString(row.id) ?? "";
1998
+ const contentSlug = input.contentSlug ?? readNullableString(row.slug);
1999
+ return {
2000
+ sourceKey: buildContentMediaUsageSourceKey({
2001
+ collectionId,
2002
+ collectionSlug,
2003
+ contentId,
2004
+ sourceVariant
2005
+ }),
2006
+ sourceType: "content",
2007
+ collectionId,
2008
+ collectionSlug,
2009
+ contentId,
2010
+ sourceVariant,
2011
+ locale: readNullableString(row.locale),
2012
+ translationGroup: readNullableString(row.translation_group),
2013
+ contentSlug,
2014
+ contentTitle: deriveContentTitle(displayData, contentSlug, contentId),
2015
+ contentStatus: readNullableString(row.status),
2016
+ contentScheduledAt: readNullableString(row.scheduled_at),
2017
+ contentDeletedAt: readNullableString(row.deleted_at),
2018
+ revisionId,
2019
+ schemaVersion: CONTENT_SOURCE_SCHEMA_VERSION,
2020
+ sourceUpdatedAt: readNullableString(row.updated_at),
2021
+ sourceVersion: readNumber(row.version),
2022
+ identityVersion
2023
+ };
2024
+ }
2025
+ function projectData(row, fieldSlugs) {
2026
+ const data = {};
2027
+ for (const fieldSlug of fieldSlugs) data[fieldSlug] = deserializeValue(row[fieldSlug] ?? null);
2028
+ return data;
2029
+ }
2030
+ function projectRawData(row, fieldSlugs) {
2031
+ const data = {};
2032
+ for (const fieldSlug of fieldSlugs) data[fieldSlug] = row[fieldSlug] ?? null;
2033
+ return data;
2034
+ }
2035
+ function projectPresentData(row, fieldSlugs) {
2036
+ const data = {};
2037
+ for (const fieldSlug of fieldSlugs) if (Object.hasOwn(row, fieldSlug)) data[fieldSlug] = row[fieldSlug];
2038
+ return data;
2039
+ }
2040
+ function uniqueColumns(columns) {
2041
+ const unique = [...new Set(columns)];
2042
+ for (const column of unique) validateIdentifier(column, "content media usage column");
2043
+ return unique;
2044
+ }
2045
+ function getContentTableName(collectionSlug) {
2046
+ validateIdentifier(collectionSlug, "collection slug");
2047
+ return `ec_${collectionSlug}`;
2048
+ }
2049
+ function deserializeValue(value) {
2050
+ if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) try {
2051
+ return JSON.parse(value);
2052
+ } catch {
2053
+ return value;
2054
+ }
2055
+ return value;
2056
+ }
2057
+ function parseRevisionData(value) {
2058
+ if (typeof value === "string") try {
2059
+ const parsed = JSON.parse(value);
2060
+ return isRecord(parsed) ? parsed : null;
2061
+ } catch {
2062
+ return null;
2063
+ }
2064
+ return isRecord(value) ? value : null;
2065
+ }
2066
+ function stripRevisionMetadata(data) {
2067
+ const stripped = {};
2068
+ for (const [key, value] of Object.entries(data)) if (!key.startsWith("_")) stripped[key] = value;
2069
+ return stripped;
2070
+ }
2071
+ function deriveContentTitle(displayData, contentSlug, contentId) {
2072
+ for (const fieldSlug of ["title", "name"]) {
2073
+ const value = displayData[fieldSlug];
2074
+ if (typeof value === "string" && value.trim()) return value;
2075
+ }
2076
+ return contentSlug ?? contentId;
2077
+ }
2078
+ function readString(value) {
2079
+ return typeof value === "string" ? value : null;
2080
+ }
2081
+ function readNullableString(value) {
2082
+ return value === null || value === void 0 ? null : readString(value);
2083
+ }
2084
+ function readNumber(value) {
2085
+ if (typeof value === "number" && Number.isFinite(value)) return value;
2086
+ if (typeof value === "bigint") return Number(value);
2087
+ if (typeof value === "string" && value) {
2088
+ const parsed = Number(value);
2089
+ return Number.isFinite(parsed) ? parsed : null;
2090
+ }
2091
+ return null;
2092
+ }
2093
+ function isRecord(value) {
2094
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2095
+ }
2096
+
2097
+ //#endregion
2098
+ //#region src/media/usage/content-refresh.ts
2099
+ const CONTENT_MEDIA_USAGE_ADAPTER_ID = "content-media";
2100
+ const CONTENT_MEDIA_USAGE_COLLECTION_SCOPE = "collection";
2101
+ const CONTENT_USAGE_LOCKS_KEY = Symbol.for("emdash.mediaUsage.contentLocks");
2102
+ const CONTENT_USAGE_COLLECTION_LOCKS_KEY = Symbol.for("emdash.mediaUsage.collectionLocks");
2103
+ const CONTENT_USAGE_REFRESH_MAX_ATTEMPTS = 2;
2104
+ const MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS = Object.freeze({
2105
+ maxOccurrenceMutationUnitsPerClaim: 12,
2106
+ maxProjectionMutationBytesPerClaim: 512 * 1024
2107
+ });
2108
+ const ZERO_RESULT = {
2109
+ success: true,
2110
+ refreshedSourceCount: 0,
2111
+ deletedSourceCount: 0,
2112
+ failedSourceCount: 0
2113
+ };
2114
+ function createContentMediaUsageAdmissionBudget() {
2115
+ return {
2116
+ remainingOccurrenceMutationUnits: MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxOccurrenceMutationUnitsPerClaim,
2117
+ remainingProjectionMutationBytes: MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxProjectionMutationBytesPerClaim,
2118
+ hasReservedMutation: false
2119
+ };
2120
+ }
2121
+ async function planContentMediaUsageProjectionAdmission(repo, snapshots, observedSources, canonicalSourceKeys, budget) {
2122
+ const snapshotSourceKeys = new Set(snapshots.map((snapshot) => snapshot.source.sourceKey));
2123
+ const absentSources = canonicalSourceKeys.filter((sourceKey) => !snapshotSourceKeys.has(sourceKey)).map((sourceKey) => observedSources.get(sourceKey)).filter((source) => source !== void 0);
2124
+ let deletionOccurrenceUnits = 0;
2125
+ let deletionBytes = 0;
2126
+ for (const source of absentSources) {
2127
+ const measurement = await repo.measureSourceGenerationDeletion(source.sourceKey, source.currentGeneration, MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxOccurrenceMutationUnitsPerClaim);
2128
+ if (measurement.exceedsOccurrenceLimit) return budget.hasReservedMutation ? { outcome: "claim_budget_deferred" } : { outcome: "intrinsic_resource_limit" };
2129
+ deletionOccurrenceUnits += measurement.occurrenceCount;
2130
+ deletionBytes += storedMediaUsageSourceByteLength(source) + measurement.occurrenceBytes * 2;
2131
+ }
2132
+ const noOpSourceKeys = /* @__PURE__ */ new Set();
2133
+ let cost = projectionAdmissionCost(snapshots, noOpSourceKeys, deletionOccurrenceUnits, deletionBytes);
2134
+ if (exceedsProjectionAdmissionLimits(cost)) {
2135
+ for (const snapshot of snapshots) {
2136
+ const expectedSource = observedSources.get(snapshot.source.sourceKey);
2137
+ if (expectedSource && await repo.projectionMatchesExpectedSource(snapshot.source, expectedSource)) noOpSourceKeys.add(snapshot.source.sourceKey);
2138
+ }
2139
+ cost = projectionAdmissionCost(snapshots, noOpSourceKeys, deletionOccurrenceUnits, deletionBytes);
2140
+ }
2141
+ if (exceedsProjectionAdmissionLimits(cost)) return budget.hasReservedMutation ? { outcome: "claim_budget_deferred" } : { outcome: "intrinsic_resource_limit" };
2142
+ if (cost.occurrenceMutationUnits > budget.remainingOccurrenceMutationUnits || cost.projectionMutationBytes > budget.remainingProjectionMutationBytes) return { outcome: "claim_budget_deferred" };
2143
+ budget.remainingOccurrenceMutationUnits -= cost.occurrenceMutationUnits;
2144
+ budget.remainingProjectionMutationBytes -= cost.projectionMutationBytes;
2145
+ if (cost.occurrenceMutationUnits > 0 || cost.projectionMutationBytes > 0) budget.hasReservedMutation = true;
2146
+ return {
2147
+ outcome: "admitted",
2148
+ noOpSourceKeys,
2149
+ absentSources,
2150
+ ...cost
2151
+ };
2152
+ }
2153
+ function projectionAdmissionCost(snapshots, noOpSourceKeys, deletionOccurrenceUnits, deletionBytes) {
2154
+ return snapshots.reduce((cost, snapshot) => {
2155
+ if (noOpSourceKeys.has(snapshot.source.sourceKey)) return cost;
2156
+ cost.occurrenceMutationUnits += snapshot.occurrences.length;
2157
+ cost.projectionMutationBytes += snapshot.projectionByteLength;
2158
+ return cost;
2159
+ }, {
2160
+ occurrenceMutationUnits: deletionOccurrenceUnits,
2161
+ projectionMutationBytes: deletionBytes
2162
+ });
2163
+ }
2164
+ function exceedsProjectionAdmissionLimits(cost) {
2165
+ return cost.occurrenceMutationUnits > MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxOccurrenceMutationUnitsPerClaim || cost.projectionMutationBytes > MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxProjectionMutationBytesPerClaim;
2166
+ }
2167
+ function storedMediaUsageSourceByteLength(source) {
2168
+ return new TextEncoder().encode(JSON.stringify(source)).byteLength;
2169
+ }
2170
+ async function refreshContentMediaUsage(db, collectionSlug, contentId) {
2171
+ validateIdentifier(collectionSlug, "collection slug");
2172
+ return withContentUsageCollectionLock(collectionSlug, () => withContentUsageLock(collectionSlug, contentId, () => refreshContentMediaUsageUnlocked(db, collectionSlug, contentId, {})));
2173
+ }
2174
+ async function refreshContentMediaUsageForWork(db, collectionId, collectionSlug, contentId) {
2175
+ validateIdentifier(collectionSlug, "collection slug");
2176
+ if (!collectionId) throw new Error("Durable media usage work requires a collection identity");
2177
+ return withContentUsageCollectionLock(collectionSlug, () => withContentUsageLock(collectionSlug, contentId, () => refreshContentMediaUsageUnlocked(db, collectionSlug, contentId, {
2178
+ collectionId,
2179
+ durableWork: true
2180
+ })));
2181
+ }
2182
+ async function refreshContentMediaUsageUnlocked(db, collectionSlug, contentId, options) {
2183
+ try {
2184
+ let conflictResult = null;
2185
+ if (options.durableWork) options.admissionBudget = createContentMediaUsageAdmissionBudget();
2186
+ for (let attempt = 0; attempt < CONTENT_USAGE_REFRESH_MAX_ATTEMPTS; attempt++) {
2187
+ const result = await refreshContentMediaUsageAttempt(db, collectionSlug, contentId, options);
2188
+ if (result.errorCode !== "CONTENT_USAGE_GENERATION_CONFLICT") return result;
2189
+ conflictResult = result;
2190
+ if (options.admissionBudget?.hasReservedMutation) break;
2191
+ }
2192
+ if (options.durableWork) return generationConflictResult({
2193
+ refreshedSourceCount: conflictResult?.refreshedSourceCount ?? 0,
2194
+ deletedSourceCount: conflictResult?.deletedSourceCount ?? 0
2195
+ });
2196
+ return markGenerationConflict(db, collectionSlug, {
2197
+ refreshedSourceCount: conflictResult?.refreshedSourceCount ?? 0,
2198
+ deletedSourceCount: conflictResult?.deletedSourceCount ?? 0
2199
+ });
2200
+ } catch (error) {
2201
+ console.error(`[media-usage] Failed to refresh ${collectionSlug}/${contentId}:`, error);
2202
+ if (!options.durableWork) await markContentMediaUsageCollectionStaleSafely(db, collectionSlug, "CONTENT_USAGE_REFRESH_ERROR");
2203
+ return {
2204
+ success: false,
2205
+ refreshedSourceCount: 0,
2206
+ deletedSourceCount: 0,
2207
+ failedSourceCount: 0,
2208
+ errorCode: "CONTENT_USAGE_REFRESH_ERROR"
2209
+ };
2210
+ }
2211
+ }
2212
+ async function refreshContentMediaUsageAttempt(db, collectionSlug, contentId, options) {
2213
+ const repo = new MediaUsageRepository(db);
2214
+ const canonicalSourceKeys = contentSourceKeys(collectionSlug, contentId, options.collectionId);
2215
+ const observedSources = await repo.findSources(canonicalSourceKeys);
2216
+ const snapshotsResult = await loadContentMediaUsageSnapshots(db, collectionSlug, contentId, void 0, options.collectionId ? {
2217
+ collectionId: options.collectionId,
2218
+ identityVersion: 1
2219
+ } : void 0);
2220
+ if (!snapshotsResult.success) {
2221
+ if (snapshotsResult.error === "CONTENT_NOT_FOUND" && options.collectionId) {
2222
+ if (!await contentCollectionExists(db, collectionSlug, options.collectionId)) return generationConflictResult({
2223
+ refreshedSourceCount: 0,
2224
+ deletedSourceCount: 0
2225
+ });
2226
+ if (!options.admissionBudget) throw new Error("Durable media usage work requires an admission budget");
2227
+ const admission = await planContentMediaUsageProjectionAdmission(repo, [], observedSources, canonicalSourceKeys, options.admissionBudget);
2228
+ if (admission.outcome !== "admitted") return admissionFailureResult(admission.outcome);
2229
+ return deleteCanonicalContentSourcesIfAbsent(repo, admission.absentSources, collectionSlug, contentId);
2230
+ }
2231
+ if (snapshotsResult.error === "CONTENT_NOT_FOUND" && !await contentCollectionExists(db, collectionSlug)) {
2232
+ const deletedSourceCount = await repo.deleteContentSources(collectionSlug, contentId);
2233
+ return {
2234
+ ...ZERO_RESULT,
2235
+ deletedSourceCount
2236
+ };
2237
+ }
2238
+ return options.durableWork ? snapshotFailureResult(snapshotsResult) : markSnapshotFailure(db, collectionSlug, snapshotsResult);
2239
+ }
2240
+ if (!await contentCollectionExists(db, collectionSlug, options.collectionId)) {
2241
+ if (options.collectionId) return generationConflictResult({
2242
+ refreshedSourceCount: 0,
2243
+ deletedSourceCount: 0
2244
+ });
2245
+ const deletedSourceCount = await repo.deleteContentSources(collectionSlug, contentId);
2246
+ return {
2247
+ ...ZERO_RESULT,
2248
+ deletedSourceCount
2249
+ };
2250
+ }
2251
+ const admission = options.admissionBudget ? await planContentMediaUsageProjectionAdmission(repo, snapshotsResult.snapshots, observedSources, canonicalSourceKeys, options.admissionBudget) : null;
2252
+ if (admission && admission.outcome !== "admitted") return admissionFailureResult(admission.outcome);
2253
+ let refreshedSourceCount = 0;
2254
+ for (const snapshot of snapshotsResult.snapshots) {
2255
+ if (admission?.outcome === "admitted" && admission.noOpSourceKeys.has(snapshot.source.sourceKey)) {
2256
+ refreshedSourceCount++;
2257
+ continue;
2258
+ }
2259
+ const result = await repo.replaceSourceIfMatching(snapshot.source, snapshot.occurrences, observedSources.get(snapshot.source.sourceKey) ?? null);
2260
+ if (result.unchanged) {
2261
+ refreshedSourceCount++;
2262
+ continue;
2263
+ }
2264
+ if (!result.replaced) return generationConflictResult({
2265
+ refreshedSourceCount,
2266
+ deletedSourceCount: 0
2267
+ });
2268
+ refreshedSourceCount++;
2269
+ }
2270
+ if (!await contentCollectionExists(db, collectionSlug, options.collectionId)) {
2271
+ if (options.collectionId) return generationConflictResult({
2272
+ refreshedSourceCount,
2273
+ deletedSourceCount: 0
2274
+ });
2275
+ const deletedSourceCount = await repo.deleteContentSources(collectionSlug, contentId);
2276
+ return {
2277
+ ...ZERO_RESULT,
2278
+ deletedSourceCount
2279
+ };
2280
+ }
2281
+ const expectedSourceKeys = new Set(snapshotsResult.snapshots.map((snapshot) => snapshot.source.sourceKey));
2282
+ const absentSources = admission?.outcome === "admitted" ? admission.absentSources : canonicalSourceKeys.filter((sourceKey) => !expectedSourceKeys.has(sourceKey)).map((sourceKey) => observedSources.get(sourceKey)).filter((source) => source !== void 0);
2283
+ let deletedSourceCount = 0;
2284
+ for (const expectedSource of absentSources) {
2285
+ const result = await repo.deleteSourceIfMatching(expectedSource.sourceKey, expectedSource);
2286
+ if (result.deleted) {
2287
+ deletedSourceCount++;
2288
+ continue;
2289
+ }
2290
+ if (result.source) return generationConflictResult({
2291
+ refreshedSourceCount,
2292
+ deletedSourceCount
2293
+ });
2294
+ }
2295
+ return {
2296
+ success: true,
2297
+ refreshedSourceCount,
2298
+ deletedSourceCount,
2299
+ failedSourceCount: 0
2300
+ };
2301
+ }
2302
+ function contentSourceKeys(collectionSlug, contentId, collectionId) {
2303
+ return MEDIA_USAGE_CONTENT_SOURCE_VARIANTS.map((sourceVariant) => buildContentMediaUsageSourceKey({
2304
+ collectionId,
2305
+ collectionSlug,
2306
+ contentId,
2307
+ sourceVariant
2308
+ }));
2309
+ }
2310
+ function admissionFailureResult(outcome) {
2311
+ return {
2312
+ ...generationConflictResult({
2313
+ refreshedSourceCount: 0,
2314
+ deletedSourceCount: 0
2315
+ }),
2316
+ errorCode: outcome === "intrinsic_resource_limit" ? "CONTENT_USAGE_RESOURCE_LIMIT" : "CONTENT_USAGE_GENERATION_CONFLICT"
2317
+ };
2318
+ }
2319
+ async function markGenerationConflict(db, collectionSlug, counts) {
2320
+ await markContentMediaUsageCollectionStaleSafely(db, collectionSlug, "CONTENT_USAGE_GENERATION_CONFLICT");
2321
+ return {
2322
+ success: false,
2323
+ refreshedSourceCount: counts.refreshedSourceCount,
2324
+ deletedSourceCount: counts.deletedSourceCount,
2325
+ failedSourceCount: 0,
2326
+ errorCode: "CONTENT_USAGE_GENERATION_CONFLICT"
2327
+ };
2328
+ }
2329
+ function generationConflictResult(counts) {
2330
+ return {
2331
+ success: false,
2332
+ refreshedSourceCount: counts.refreshedSourceCount,
2333
+ deletedSourceCount: counts.deletedSourceCount,
2334
+ failedSourceCount: 0,
2335
+ errorCode: "CONTENT_USAGE_GENERATION_CONFLICT"
2336
+ };
2337
+ }
2338
+ async function contentCollectionExists(db, collectionSlug, collectionId) {
2339
+ let query = db.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug);
2340
+ if (collectionId) query = query.where("id", "=", collectionId);
2341
+ return await query.executeTakeFirst() !== void 0;
2342
+ }
2343
+ async function deleteContentMediaUsage(db, collectionSlug, contentId) {
2344
+ validateIdentifier(collectionSlug, "collection slug");
2345
+ return withContentUsageCollectionLock(collectionSlug, () => withContentUsageLock(collectionSlug, contentId, () => deleteContentMediaUsageUnlocked(db, collectionSlug, contentId)));
2346
+ }
2347
+ async function deleteContentMediaUsageUnlocked(db, collectionSlug, contentId) {
2348
+ try {
2349
+ const deletedSourceCount = await new MediaUsageRepository(db).deleteContentSources(collectionSlug, contentId);
2350
+ return {
2351
+ ...ZERO_RESULT,
2352
+ deletedSourceCount
2353
+ };
2354
+ } catch (error) {
2355
+ console.error(`[media-usage] Failed to delete usage for ${collectionSlug}/${contentId}:`, error);
2356
+ await markContentMediaUsageCollectionStaleSafely(db, collectionSlug, "CONTENT_USAGE_DELETE_ERROR");
2357
+ return {
2358
+ success: false,
2359
+ refreshedSourceCount: 0,
2360
+ deletedSourceCount: 0,
2361
+ failedSourceCount: 0,
2362
+ errorCode: "CONTENT_USAGE_DELETE_ERROR"
2363
+ };
2364
+ }
2365
+ }
2366
+ async function deleteContentMediaUsageCollection(db, collectionSlug) {
2367
+ validateIdentifier(collectionSlug, "collection slug");
2368
+ return withContentUsageCollectionLock(collectionSlug, () => deleteContentMediaUsageCollectionUnlocked(db, collectionSlug));
2369
+ }
2370
+ async function deleteContentMediaUsageCollectionUnlocked(db, collectionSlug) {
2371
+ try {
2372
+ const repo = new MediaUsageRepository(db);
2373
+ const deletedSourceCount = await repo.deleteCollectionSources(collectionSlug);
2374
+ await repo.deleteIndexStatus({
2375
+ adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
2376
+ scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
2377
+ scopeKey: collectionSlug
2378
+ });
2379
+ return {
2380
+ ...ZERO_RESULT,
2381
+ deletedSourceCount
2382
+ };
2383
+ } catch (error) {
2384
+ console.error(`[media-usage] Failed to delete usage for collection ${collectionSlug}:`, error);
2385
+ try {
2386
+ await new MediaUsageRepository(db).deleteIndexStatus({
2387
+ adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
2388
+ scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
2389
+ scopeKey: collectionSlug
2390
+ });
2391
+ } catch (statusError) {
2392
+ console.error(`[media-usage] Failed to clear usage status for deleted collection ${collectionSlug}:`, statusError);
2393
+ }
2394
+ return {
2395
+ success: false,
2396
+ refreshedSourceCount: 0,
2397
+ deletedSourceCount: 0,
2398
+ failedSourceCount: 0,
2399
+ errorCode: "CONTENT_USAGE_DELETE_ERROR"
2400
+ };
2401
+ }
2402
+ }
2403
+ async function refreshContentMediaUsageAfterWrite(db, collectionSlug, contentId) {
2404
+ const result = await refreshContentMediaUsage(db, collectionSlug, contentId);
2405
+ if (!result.success) console.error(`[media-usage] Usage refresh for ${collectionSlug}/${contentId} finished with ${result.errorCode}`);
2406
+ }
2407
+ async function markContentMediaUsageCollectionStale(db, collectionSlug, lastErrorCode) {
2408
+ validateIdentifier(collectionSlug, "collection slug");
2409
+ const repo = new MediaUsageRepository(db);
2410
+ const identity = {
2411
+ adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
2412
+ scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
2413
+ scopeKey: collectionSlug
2414
+ };
2415
+ const existing = await repo.findIndexStatus(identity);
2416
+ await repo.upsertIndexStatus({
2417
+ ...identity,
2418
+ status: "stale",
2419
+ schemaVersion: existing?.schemaVersion ?? CONTENT_SOURCE_SCHEMA_VERSION,
2420
+ startedAt: existing?.startedAt ?? null,
2421
+ completedAt: existing?.completedAt ?? null,
2422
+ cursor: existing?.cursor ?? null,
2423
+ indexedSourceCount: existing?.indexedSourceCount ?? 0,
2424
+ failedSourceCount: existing?.failedSourceCount ?? 0,
2425
+ lastErrorCode
2426
+ });
2427
+ }
2428
+ async function invalidateContentMediaUsageSchemaChange(db, collectionSlug) {
2429
+ validateIdentifier(collectionSlug, "collection slug");
2430
+ if (!await tableExists(db, "_emdash_media_usage_activation")) return false;
2431
+ if ((await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state !== "active") return false;
2432
+ if (!await new MediaUsageRepository(db).invalidateIndexStatusForSchemaChange(collectionSlug)) throw new Error(`Cannot invalidate media usage coverage for collection ${collectionSlug}`);
2433
+ return true;
2434
+ }
2435
+ async function findNonTranslatableSiblingContentIds(db, collectionSlug, updatedContentId, translationGroup, updatedData) {
2436
+ if (!isI18nEnabled() || !updatedData || !translationGroup) return [];
2437
+ validateIdentifier(collectionSlug, "collection slug");
2438
+ const collection = await db.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug).executeTakeFirst();
2439
+ if (!collection) return [];
2440
+ const touchedNonTranslatableSlugs = (await db.selectFrom("_emdash_fields").select("slug").where("collection_id", "=", collection.id).where("translatable", "=", 0).execute()).filter((field) => field.slug in updatedData).map((field) => field.slug);
2441
+ if (touchedNonTranslatableSlugs.length === 0) return [];
2442
+ const usageFields = await loadContentMediaUsageFields(db, collectionSlug);
2443
+ const usageRelevantSlugs = new Set([...usageFields.extractionFields.map((field) => field.slug), ...usageFields.displayFieldSlugs]);
2444
+ if (!touchedNonTranslatableSlugs.some((slug) => usageRelevantSlugs.has(slug))) return [];
2445
+ const tableName = `ec_${collectionSlug}`;
2446
+ return (await sql`
2447
+ SELECT id
2448
+ FROM ${sql.ref(tableName)}
2449
+ WHERE translation_group = ${translationGroup}
2450
+ AND id != ${updatedContentId}
2451
+ ORDER BY id ASC
2452
+ `.execute(db)).rows.map((row) => row.id);
2453
+ }
2454
+ async function markSnapshotFailure(db, collectionSlug, result) {
2455
+ const repo = new MediaUsageRepository(db);
2456
+ if (result.source) await repo.markSourceAttempted({
2457
+ ...result.source,
2458
+ sourceCompleteness: "failed",
2459
+ lastErrorCode: result.error
2460
+ });
2461
+ await markContentMediaUsageCollectionStale(db, collectionSlug, result.error);
2462
+ return {
2463
+ success: false,
2464
+ refreshedSourceCount: 0,
2465
+ deletedSourceCount: 0,
2466
+ failedSourceCount: result.source ? 1 : 0,
2467
+ errorCode: result.error
2468
+ };
2469
+ }
2470
+ function snapshotFailureResult(result) {
2471
+ return {
2472
+ success: false,
2473
+ refreshedSourceCount: 0,
2474
+ deletedSourceCount: 0,
2475
+ failedSourceCount: result.source ? 1 : 0,
2476
+ errorCode: result.error
2477
+ };
2478
+ }
2479
+ async function deleteCanonicalContentSourcesIfAbsent(repo, observedSources, collectionSlug, contentId) {
2480
+ let deletedSourceCount = 0;
2481
+ for (const source of observedSources) {
2482
+ const result = await repo.deleteSourceIfMatchingContentAbsent(source.sourceKey, source, collectionSlug, contentId);
2483
+ if (result.deleted) {
2484
+ deletedSourceCount++;
2485
+ continue;
2486
+ }
2487
+ if (result.contentPresent || result.source) return generationConflictResult({
2488
+ refreshedSourceCount: 0,
2489
+ deletedSourceCount
2490
+ });
2491
+ }
2492
+ return {
2493
+ ...ZERO_RESULT,
2494
+ deletedSourceCount
2495
+ };
2496
+ }
2497
+ async function markContentMediaUsageCollectionStaleSafely(db, collectionSlug, lastErrorCode) {
2498
+ try {
2499
+ await markContentMediaUsageCollectionStale(db, collectionSlug, lastErrorCode);
2500
+ return true;
2501
+ } catch (error) {
2502
+ console.error(`[media-usage] Failed to mark ${collectionSlug} stale:`, error);
2503
+ return false;
2504
+ }
2505
+ }
2506
+ async function withContentUsageLock(collectionSlug, contentId, fn) {
2507
+ const locks = getContentUsageLocks();
2508
+ const lockKey = `${collectionSlug}\0${contentId}`;
2509
+ const previous = locks.get(lockKey) ?? Promise.resolve();
2510
+ let releaseCurrent;
2511
+ const current = new Promise((resolve) => {
2512
+ releaseCurrent = resolve;
2513
+ });
2514
+ const next = previous.catch(() => {}).then(() => current);
2515
+ locks.set(lockKey, next);
2516
+ try {
2517
+ await previous.catch(() => {});
2518
+ return await fn();
2519
+ } finally {
2520
+ releaseCurrent();
2521
+ if (locks.get(lockKey) === next) locks.delete(lockKey);
2522
+ }
2523
+ }
2524
+ async function withContentUsageCollectionLock(collectionSlug, fn) {
2525
+ const locks = getContentUsageCollectionLocks();
2526
+ const previous = locks.get(collectionSlug) ?? Promise.resolve();
2527
+ let releaseCurrent;
2528
+ const current = new Promise((resolve) => {
2529
+ releaseCurrent = resolve;
2530
+ });
2531
+ const next = previous.catch(() => {}).then(() => current);
2532
+ locks.set(collectionSlug, next);
2533
+ try {
2534
+ await previous.catch(() => {});
2535
+ return await fn();
2536
+ } finally {
2537
+ releaseCurrent();
2538
+ if (locks.get(collectionSlug) === next) locks.delete(collectionSlug);
2539
+ }
2540
+ }
2541
+ function getContentUsageLocks() {
2542
+ const global = globalThis;
2543
+ const existing = global[CONTENT_USAGE_LOCKS_KEY];
2544
+ if (existing instanceof Map) return existing;
2545
+ const locks = /* @__PURE__ */ new Map();
2546
+ global[CONTENT_USAGE_LOCKS_KEY] = locks;
2547
+ return locks;
2548
+ }
2549
+ function getContentUsageCollectionLocks() {
2550
+ const global = globalThis;
2551
+ const existing = global[CONTENT_USAGE_COLLECTION_LOCKS_KEY];
2552
+ if (existing instanceof Map) return existing;
2553
+ const locks = /* @__PURE__ */ new Map();
2554
+ global[CONTENT_USAGE_COLLECTION_LOCKS_KEY] = locks;
2555
+ return locks;
2556
+ }
2557
+
2558
+ //#endregion
2559
+ export { loadContentMediaUsageFields as _, findNonTranslatableSiblingContentIds as a, markContentMediaUsageCollectionStaleSafely as c, withContentUsageCollectionLock as d, loadContentMediaUsageSnapshots as f, buildContentMediaUsageFieldFingerprint as g, MediaUsageFieldDiscoveryError as h, deleteContentMediaUsageCollection as i, refreshContentMediaUsageAfterWrite as l, buildContentMediaUsageSourceKey as m, CONTENT_MEDIA_USAGE_COLLECTION_SCOPE as n, invalidateContentMediaUsageSchemaChange as o, MEDIA_USAGE_CONTENT_SOURCE_VARIANTS as p, deleteContentMediaUsage as r, markContentMediaUsageCollectionStale as s, CONTENT_MEDIA_USAGE_ADAPTER_ID as t, refreshContentMediaUsageForWork as u, CONTENT_SOURCE_SCHEMA_VERSION as v, MediaUsageRepository as y };
2560
+ //# sourceMappingURL=content-refresh-DNERP4-f.mjs.map