emdash 0.18.0 → 0.19.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 (408) hide show
  1. package/dist/api/route-utils.d.mts +2 -2
  2. package/dist/api/route-utils.mjs +14 -14
  3. package/dist/api/schemas/index.d.mts +2 -2
  4. package/dist/api/schemas/index.mjs +3 -3
  5. package/dist/{api-Cs7DAACP.mjs → api-BZ6bhjYs.mjs} +88 -16
  6. package/dist/api-BZ6bhjYs.mjs.map +1 -0
  7. package/dist/{apply-BWMV4Zmw.mjs → apply-hQkKKBCf.mjs} +23 -23
  8. package/dist/apply-hQkKKBCf.mjs.map +1 -0
  9. package/dist/astro/index.d.mts +8 -8
  10. package/dist/astro/index.d.mts.map +1 -1
  11. package/dist/astro/index.mjs +113 -23
  12. package/dist/astro/index.mjs.map +1 -1
  13. package/dist/astro/middleware/auth.d.mts +7 -7
  14. package/dist/astro/middleware/auth.mjs +2 -2
  15. package/dist/astro/middleware/redirect.mjs +4 -4
  16. package/dist/astro/middleware/request-context.mjs +2 -2
  17. package/dist/astro/middleware.d.mts +26 -4
  18. package/dist/astro/middleware.d.mts.map +1 -1
  19. package/dist/astro/middleware.mjs +205 -173
  20. package/dist/astro/middleware.mjs.map +1 -1
  21. package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
  22. package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
  23. package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
  24. package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
  25. package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +5 -5
  26. package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
  27. package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
  28. package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
  29. package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +12 -12
  30. package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +12 -12
  31. package/dist/astro/routes/api/admin/bylines/index.mjs +12 -12
  32. package/dist/astro/routes/api/admin/comments/_id_/status.mjs +11 -11
  33. package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
  34. package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
  35. package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
  36. package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
  37. package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
  38. package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
  39. package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
  40. package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
  41. package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +31 -31
  42. package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +31 -31
  43. package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +30 -30
  44. package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +30 -30
  45. package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +30 -30
  46. package/dist/astro/routes/api/admin/plugins/index.mjs +30 -30
  47. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
  48. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +30 -30
  49. package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +30 -30
  50. package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +30 -30
  51. package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +30 -30
  52. package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +31 -31
  53. package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +30 -30
  54. package/dist/astro/routes/api/admin/plugins/registry/install.mjs +31 -31
  55. package/dist/astro/routes/api/admin/plugins/updates.mjs +30 -30
  56. package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +30 -30
  57. package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
  58. package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +30 -30
  59. package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
  60. package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
  61. package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
  62. package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
  63. package/dist/astro/routes/api/admin/users/index.mjs +5 -5
  64. package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
  65. package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
  66. package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
  67. package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
  68. package/dist/astro/routes/api/auth/invite/register-options.mjs +6 -6
  69. package/dist/astro/routes/api/auth/logout.mjs +2 -2
  70. package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
  71. package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
  72. package/dist/astro/routes/api/auth/me.mjs +6 -6
  73. package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +2 -2
  74. package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
  75. package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
  76. package/dist/astro/routes/api/auth/passkey/options.mjs +7 -7
  77. package/dist/astro/routes/api/auth/passkey/register/options.mjs +6 -6
  78. package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
  79. package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
  80. package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
  81. package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
  82. package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
  83. package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
  84. package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
  85. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +6 -5
  86. package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
  87. package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
  88. package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
  89. package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +8 -8
  90. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +9 -8
  91. package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
  92. package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
  93. package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
  94. package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
  95. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +12 -10
  96. package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
  97. package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +11 -11
  98. package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
  99. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +6 -5
  100. package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
  101. package/dist/astro/routes/api/content/_collection_/_id_.mjs +9 -8
  102. package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
  103. package/dist/astro/routes/api/content/_collection_/authors.d.mts +8 -0
  104. package/dist/astro/routes/api/content/_collection_/authors.d.mts.map +1 -0
  105. package/dist/astro/routes/api/content/_collection_/authors.mjs +19 -0
  106. package/dist/astro/routes/api/content/_collection_/authors.mjs.map +1 -0
  107. package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
  108. package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
  109. package/dist/astro/routes/api/dashboard.mjs +7 -7
  110. package/dist/astro/routes/api/dev/emails.mjs +2 -2
  111. package/dist/astro/routes/api/import/probe.d.mts +2 -2
  112. package/dist/astro/routes/api/import/probe.mjs +6 -6
  113. package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
  114. package/dist/astro/routes/api/import/wordpress/execute.d.mts +7 -7
  115. package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -9
  116. package/dist/astro/routes/api/import/wordpress/media.mjs +6 -6
  117. package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
  118. package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
  119. package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +6 -6
  120. package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +9 -9
  121. package/dist/astro/routes/api/manifest.mjs +3 -3
  122. package/dist/astro/routes/api/mcp.mjs +28 -28
  123. package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
  124. package/dist/astro/routes/api/media/_id_.mjs +6 -6
  125. package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
  126. package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
  127. package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
  128. package/dist/astro/routes/api/media/providers/index.mjs +3 -3
  129. package/dist/astro/routes/api/media/upload-url.mjs +6 -6
  130. package/dist/astro/routes/api/media.mjs +7 -7
  131. package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
  132. package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
  133. package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
  134. package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
  135. package/dist/astro/routes/api/menus/_name_.mjs +7 -7
  136. package/dist/astro/routes/api/menus/index.mjs +7 -7
  137. package/dist/astro/routes/api/oauth/authorize.mjs +1 -1
  138. package/dist/astro/routes/api/oauth/device/authorize.mjs +4 -4
  139. package/dist/astro/routes/api/oauth/device/code.mjs +5 -5
  140. package/dist/astro/routes/api/oauth/device/token.mjs +5 -5
  141. package/dist/astro/routes/api/oauth/register.mjs +2 -2
  142. package/dist/astro/routes/api/oauth/token/refresh.mjs +4 -4
  143. package/dist/astro/routes/api/oauth/token/revoke.mjs +4 -4
  144. package/dist/astro/routes/api/oauth/token.mjs +4 -4
  145. package/dist/astro/routes/api/openapi.json.mjs +17 -3
  146. package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
  147. package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
  148. package/dist/astro/routes/api/redirects/404s/index.mjs +9 -9
  149. package/dist/astro/routes/api/redirects/404s/summary.mjs +9 -9
  150. package/dist/astro/routes/api/redirects/_id_.mjs +10 -10
  151. package/dist/astro/routes/api/redirects/index.mjs +10 -10
  152. package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
  153. package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
  154. package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +30 -30
  155. package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +30 -30
  156. package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +30 -30
  157. package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +30 -30
  158. package/dist/astro/routes/api/schema/collections/index.mjs +30 -30
  159. package/dist/astro/routes/api/schema/index.mjs +6 -6
  160. package/dist/astro/routes/api/schema/orphans/_slug_.mjs +30 -30
  161. package/dist/astro/routes/api/schema/orphans/index.mjs +30 -30
  162. package/dist/astro/routes/api/search/enable.mjs +9 -9
  163. package/dist/astro/routes/api/search/index.mjs +8 -8
  164. package/dist/astro/routes/api/search/rebuild.mjs +9 -9
  165. package/dist/astro/routes/api/search/stats.mjs +6 -6
  166. package/dist/astro/routes/api/search/suggest.mjs +8 -8
  167. package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
  168. package/dist/astro/routes/api/sections/index.mjs +8 -8
  169. package/dist/astro/routes/api/settings/email.mjs +5 -5
  170. package/dist/astro/routes/api/settings.mjs +12 -12
  171. package/dist/astro/routes/api/setup/admin-verify.mjs +6 -6
  172. package/dist/astro/routes/api/setup/admin.mjs +6 -6
  173. package/dist/astro/routes/api/setup/dev-bypass.mjs +18 -18
  174. package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
  175. package/dist/astro/routes/api/setup/index.mjs +21 -21
  176. package/dist/astro/routes/api/setup/status.mjs +3 -3
  177. package/dist/astro/routes/api/snapshot.mjs +5 -5
  178. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -11
  179. package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -11
  180. package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -11
  181. package/dist/astro/routes/api/taxonomies/index.mjs +11 -11
  182. package/dist/astro/routes/api/themes/preview.mjs +5 -5
  183. package/dist/astro/routes/api/typegen.mjs +5 -5
  184. package/dist/astro/routes/api/well-known/auth.mjs +1 -1
  185. package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
  186. package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
  187. package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
  188. package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
  189. package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
  190. package/dist/astro/routes/api/widget-components.mjs +2 -2
  191. package/dist/astro/routes/robots.txt.mjs +6 -6
  192. package/dist/astro/routes/sitemap-_collection_.xml.mjs +6 -6
  193. package/dist/astro/routes/sitemap.xml.mjs +6 -6
  194. package/dist/astro/types.d.mts +15 -8
  195. package/dist/astro/types.d.mts.map +1 -1
  196. package/dist/{authorize-CotM4Yiu.mjs → authorize-C_8t2KGa.mjs} +2 -2
  197. package/dist/{authorize-CotM4Yiu.mjs.map → authorize-C_8t2KGa.mjs.map} +1 -1
  198. package/dist/{byline-CWQ9aSoz.mjs → byline-DUx48sJp.mjs} +6 -6
  199. package/dist/{byline-CWQ9aSoz.mjs.map → byline-DUx48sJp.mjs.map} +1 -1
  200. package/dist/{byline-fields-Dr-xcb6S.mjs → byline-fields-51kg6Vuv.mjs} +3 -3
  201. package/dist/{byline-fields-Dr-xcb6S.mjs.map → byline-fields-51kg6Vuv.mjs.map} +1 -1
  202. package/dist/{byline-fields-DC3Wkk-U.mjs → byline-fields-C_OsR-KF.mjs} +2 -2
  203. package/dist/{byline-fields-DC3Wkk-U.mjs.map → byline-fields-C_OsR-KF.mjs.map} +1 -1
  204. package/dist/{byline-fields-BNy7Ng1U.d.mts → byline-fields-DYXKDuNX.d.mts} +26 -2
  205. package/dist/byline-fields-DYXKDuNX.d.mts.map +1 -0
  206. package/dist/{byline-registry-CxK5g559.mjs → byline-registry-CWP7I71B.mjs} +3 -3
  207. package/dist/{byline-registry-CxK5g559.mjs.map → byline-registry-CWP7I71B.mjs.map} +1 -1
  208. package/dist/{bylines-LJMgENMI.mjs → bylines-Cx5n-WqP.mjs} +3 -3
  209. package/dist/{bylines-LJMgENMI.mjs.map → bylines-Cx5n-WqP.mjs.map} +1 -1
  210. package/dist/{bylines-BJSva1Un.mjs → bylines-wurS258E.mjs} +50 -6
  211. package/dist/{bylines-BJSva1Un.mjs.map → bylines-wurS258E.mjs.map} +1 -1
  212. package/dist/{cache-lZL7SgVb.mjs → cache-B_HzASVT.mjs} +3 -3
  213. package/dist/{cache-lZL7SgVb.mjs.map → cache-B_HzASVT.mjs.map} +1 -1
  214. package/dist/{chunks-BU-vP9Dh.mjs → chunks-BerYVuve.mjs} +2 -2
  215. package/dist/{chunks-BU-vP9Dh.mjs.map → chunks-BerYVuve.mjs.map} +1 -1
  216. package/dist/cli/index.mjs +40 -27
  217. package/dist/cli/index.mjs.map +1 -1
  218. package/dist/client/cf-access.d.mts +1 -1
  219. package/dist/client/index.d.mts +1 -1
  220. package/dist/{comment-C4jVbCM8.mjs → comment-sqQxNpN3.mjs} +2 -2
  221. package/dist/{comment-C4jVbCM8.mjs.map → comment-sqQxNpN3.mjs.map} +1 -1
  222. package/dist/{comments-BTAbC0Ek.mjs → comments-CJ0RZsYR.mjs} +3 -3
  223. package/dist/{comments-BTAbC0Ek.mjs.map → comments-CJ0RZsYR.mjs.map} +1 -1
  224. package/dist/{content-CyqOmOzm.mjs → content-BIlVx-RX.mjs} +132 -43
  225. package/dist/content-BIlVx-RX.mjs.map +1 -0
  226. package/dist/{context-DZ7bEh5-.mjs → context-GG52SPgh.mjs} +10 -10
  227. package/dist/{context-DZ7bEh5-.mjs.map → context-GG52SPgh.mjs.map} +1 -1
  228. package/dist/{cron-DZovZUnC.mjs → cron-BJ2ClIlj.mjs} +4 -3
  229. package/dist/cron-BJ2ClIlj.mjs.map +1 -0
  230. package/dist/{dashboard-B5WQpNTP.mjs → dashboard-2JgAMWxK.mjs} +4 -4
  231. package/dist/{dashboard-B5WQpNTP.mjs.map → dashboard-2JgAMWxK.mjs.map} +1 -1
  232. package/dist/db/index.d.mts +2 -2
  233. package/dist/db/index.mjs +1 -1
  234. package/dist/{device-flow-ptLrVINd.mjs → device-flow-s6_q3T7A.mjs} +2 -2
  235. package/dist/{device-flow-ptLrVINd.mjs.map → device-flow-s6_q3T7A.mjs.map} +1 -1
  236. package/dist/{error-DJOsMVSt.mjs → error-RwM4dD35.mjs} +2 -2
  237. package/dist/{error-DJOsMVSt.mjs.map → error-RwM4dD35.mjs.map} +1 -1
  238. package/dist/{fts-manager-DR1ERA0c.mjs → fts-manager-1RgHmopc.mjs} +2 -2
  239. package/dist/{fts-manager-DR1ERA0c.mjs.map → fts-manager-1RgHmopc.mjs.map} +1 -1
  240. package/dist/{index-D60_SzHG.d.mts → index-BpYeJO1E.d.mts} +2 -2
  241. package/dist/{index-D60_SzHG.d.mts.map → index-BpYeJO1E.d.mts.map} +1 -1
  242. package/dist/{index-CjKdMZ3U.d.mts → index-FfiTQJq2.d.mts} +199 -17
  243. package/dist/index-FfiTQJq2.d.mts.map +1 -0
  244. package/dist/index.d.mts +9 -9
  245. package/dist/index.mjs +43 -43
  246. package/dist/{load-6ZrRhepW.mjs → load-B84ohfBk.mjs} +2 -2
  247. package/dist/{load-6ZrRhepW.mjs.map → load-B84ohfBk.mjs.map} +1 -1
  248. package/dist/{loader-Dyx8dhFV.mjs → loader-CpZKpFz0.mjs} +32 -30
  249. package/dist/loader-CpZKpFz0.mjs.map +1 -0
  250. package/dist/media/index.mjs +1 -1
  251. package/dist/media/local-runtime.d.mts +7 -7
  252. package/dist/media/local-runtime.mjs +6 -6
  253. package/dist/{media-C-oovGCG.mjs → media-JOf3pNkw.mjs} +2 -2
  254. package/dist/{media-C-oovGCG.mjs.map → media-JOf3pNkw.mjs.map} +1 -1
  255. package/dist/{menus-DugoYwTX.mjs → menus-DX4_E01q.mjs} +3 -3
  256. package/dist/{menus-DugoYwTX.mjs.map → menus-DX4_E01q.mjs.map} +1 -1
  257. package/dist/{menus-BKkxXCmd.mjs → menus-Dp9xporj.mjs} +86 -9
  258. package/dist/menus-Dp9xporj.mjs.map +1 -0
  259. package/dist/{normalize-DVV8nbrL.mjs → normalize-CK5o04zr.mjs} +2 -2
  260. package/dist/{normalize-DVV8nbrL.mjs.map → normalize-CK5o04zr.mjs.map} +1 -1
  261. package/dist/{oauth-authorization-DvBAL75d.mjs → oauth-authorization-1aPAYjiC.mjs} +2 -2
  262. package/dist/{oauth-authorization-DvBAL75d.mjs.map → oauth-authorization-1aPAYjiC.mjs.map} +1 -1
  263. package/dist/{options-BL4X94qY.mjs → options-BPCVnesz.mjs} +1 -1
  264. package/dist/{options-BL4X94qY.mjs.map → options-BPCVnesz.mjs.map} +1 -1
  265. package/dist/{options-tb7DJROi.d.mts → options-D4MnavW_.d.mts} +3 -3
  266. package/dist/{options-tb7DJROi.d.mts.map → options-D4MnavW_.d.mts.map} +1 -1
  267. package/dist/{parse-BBkFmLVr.mjs → parse-CrGndy1A.mjs} +2 -2
  268. package/dist/{parse-BBkFmLVr.mjs.map → parse-CrGndy1A.mjs.map} +1 -1
  269. package/dist/{patterns-CqG5Ya3i.mjs → patterns-p-RBdTbM.mjs} +1 -1
  270. package/dist/{patterns-CqG5Ya3i.mjs.map → patterns-p-RBdTbM.mjs.map} +1 -1
  271. package/dist/plugin-utils.d.mts +7 -7
  272. package/dist/plugins/adapt-sandbox-entry.d.mts +7 -7
  273. package/dist/{query-Ctlq1aOk.mjs → query-BFQ029Ts.mjs} +21 -15
  274. package/dist/query-BFQ029Ts.mjs.map +1 -0
  275. package/dist/{rate-limit-CH6W6ikK.mjs → rate-limit-ClFFUga6.mjs} +2 -2
  276. package/dist/{rate-limit-CH6W6ikK.mjs.map → rate-limit-ClFFUga6.mjs.map} +1 -1
  277. package/dist/{redirect-C6tJA7tk.mjs → redirect-CRWIt8Zj.mjs} +3 -3
  278. package/dist/{redirect-C6tJA7tk.mjs.map → redirect-CRWIt8Zj.mjs.map} +1 -1
  279. package/dist/{redirects-C0L9JUk4.mjs → redirects-DEygMrRO.mjs} +25 -3
  280. package/dist/redirects-DEygMrRO.mjs.map +1 -0
  281. package/dist/{redirects-CacE9eQa.mjs → redirects-OIu6vQ2i.mjs} +5 -5
  282. package/dist/{redirects-CacE9eQa.mjs.map → redirects-OIu6vQ2i.mjs.map} +1 -1
  283. package/dist/{registry-CIDxZbhh.mjs → registry-brYh-rAT.mjs} +6 -6
  284. package/dist/{registry-CIDxZbhh.mjs.map → registry-brYh-rAT.mjs.map} +1 -1
  285. package/dist/{request-cache-BYMs-BGX.mjs → request-cache-D32LpnmI.mjs} +1 -1
  286. package/dist/{request-cache-BYMs-BGX.mjs.map → request-cache-D32LpnmI.mjs.map} +1 -1
  287. package/dist/{runner-pt6Wl-l-.mjs → runner--4wMWwKM.mjs} +217 -166
  288. package/dist/runner--4wMWwKM.mjs.map +1 -0
  289. package/dist/{runner-DM1yR5qd.d.mts → runner-BcRuXq_h.d.mts} +2 -2
  290. package/dist/{runner-DM1yR5qd.d.mts.map → runner-BcRuXq_h.d.mts.map} +1 -1
  291. package/dist/runtime.d.mts +7 -7
  292. package/dist/runtime.mjs +2 -2
  293. package/dist/{schema-B4tk0HAG.mjs → schema-CS7Eg5gh.mjs} +5 -5
  294. package/dist/{schema-B4tk0HAG.mjs.map → schema-CS7Eg5gh.mjs.map} +1 -1
  295. package/dist/{search-f-fNfwab.mjs → search-o-aQzHI1.mjs} +4 -4
  296. package/dist/{search-f-fNfwab.mjs.map → search-o-aQzHI1.mjs.map} +1 -1
  297. package/dist/{secrets-YYbTgB1w.mjs → secrets-C_ZtRos3.mjs} +2 -2
  298. package/dist/{secrets-YYbTgB1w.mjs.map → secrets-C_ZtRos3.mjs.map} +1 -1
  299. package/dist/{sections-biElLfT9.mjs → sections-DhsZ0ns9.mjs} +3 -3
  300. package/dist/{sections-biElLfT9.mjs.map → sections-DhsZ0ns9.mjs.map} +1 -1
  301. package/dist/seed/index.d.mts +2 -2
  302. package/dist/seed/index.mjs +16 -16
  303. package/dist/seo/index.d.mts +1 -1
  304. package/dist/{seo-BR39kvTF.mjs → seo-B5e6y9Wk.mjs} +2 -2
  305. package/dist/{seo-BR39kvTF.mjs.map → seo-B5e6y9Wk.mjs.map} +1 -1
  306. package/dist/{service-BhR2acnc.mjs → service-DAxg8RPR.mjs} +2 -2
  307. package/dist/{service-BhR2acnc.mjs.map → service-DAxg8RPR.mjs.map} +1 -1
  308. package/dist/{settings-b5zW1R1T.mjs → settings-B1p-gPUK.mjs} +5 -5
  309. package/dist/{settings-b5zW1R1T.mjs.map → settings-B1p-gPUK.mjs.map} +1 -1
  310. package/dist/{settings-D_NJvjgN.mjs → settings-DIsbHTRE.mjs} +3 -3
  311. package/dist/{settings-D_NJvjgN.mjs.map → settings-DIsbHTRE.mjs.map} +1 -1
  312. package/dist/{setup-complete-VoEZfasi.mjs → setup-complete-Yuv78yua.mjs} +2 -2
  313. package/dist/{setup-complete-VoEZfasi.mjs.map → setup-complete-Yuv78yua.mjs.map} +1 -1
  314. package/dist/{site-url-Cm8-sJy7.mjs → site-url-mEVmwIFi.mjs} +2 -2
  315. package/dist/{site-url-Cm8-sJy7.mjs.map → site-url-mEVmwIFi.mjs.map} +1 -1
  316. package/dist/{taxonomies-Crtzy4MT.mjs → taxonomies-BEW7S5AI.mjs} +7 -6
  317. package/dist/taxonomies-BEW7S5AI.mjs.map +1 -0
  318. package/dist/{taxonomies-Mhn9rjTQ.mjs → taxonomies-UusDXv3C.mjs} +4 -4
  319. package/dist/{taxonomies-Mhn9rjTQ.mjs.map → taxonomies-UusDXv3C.mjs.map} +1 -1
  320. package/dist/{taxonomy-DTZrIQpi.mjs → taxonomy-CdllE4oq.mjs} +3 -3
  321. package/dist/{taxonomy-DTZrIQpi.mjs.map → taxonomy-CdllE4oq.mjs.map} +1 -1
  322. package/dist/{transaction-NQj4VJ7Z.mjs → transaction-x2tJQ-A1.mjs} +1 -1
  323. package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-x2tJQ-A1.mjs.map} +1 -1
  324. package/dist/{transport-OnMNbsIA.d.mts → transport-BwQeeY2p.d.mts} +1 -1
  325. package/dist/{transport-OnMNbsIA.d.mts.map → transport-BwQeeY2p.d.mts.map} +1 -1
  326. package/dist/{types-K3MDsxpy.mjs → types-BXSUSAjt.mjs} +16 -3
  327. package/dist/{types-K3MDsxpy.mjs.map → types-BXSUSAjt.mjs.map} +1 -1
  328. package/dist/{types-D8bhH891.mjs → types-DZk_y-MU.mjs} +1 -1
  329. package/dist/{types-D8bhH891.mjs.map → types-DZk_y-MU.mjs.map} +1 -1
  330. package/dist/{types-DawhLFwy.d.mts → types-OT_Es5mp.d.mts} +26 -1
  331. package/dist/{types-DawhLFwy.d.mts.map → types-OT_Es5mp.d.mts.map} +1 -1
  332. package/dist/{types-i8_uzhMD.d.mts → types-WVmpZBJV.d.mts} +18 -3
  333. package/dist/types-WVmpZBJV.d.mts.map +1 -0
  334. package/dist/{user-DzEUl5zA.mjs → user-C0um7wrg.mjs} +18 -2
  335. package/dist/user-C0um7wrg.mjs.map +1 -0
  336. package/dist/{validate-Dy6nkNls.d.mts → validate-BPAHUSge.d.mts} +10 -2
  337. package/dist/validate-BPAHUSge.d.mts.map +1 -0
  338. package/dist/{validate-JCXcsqiY.mjs → validate-ZP9Dvg0P.mjs} +6 -3
  339. package/dist/validate-ZP9Dvg0P.mjs.map +1 -0
  340. package/dist/{validation-Bq-VyKJg.mjs → validation-CE5i4q0c.mjs} +5 -5
  341. package/dist/{validation-Bq-VyKJg.mjs.map → validation-CE5i4q0c.mjs.map} +1 -1
  342. package/dist/version-Dw0JXu45.mjs +7 -0
  343. package/dist/{version-CnS-Cr8A.mjs.map → version-Dw0JXu45.mjs.map} +1 -1
  344. package/dist/{widgets-Bap1eS1X.mjs → widgets-ClEnYQCH.mjs} +2 -2
  345. package/dist/{widgets-Bap1eS1X.mjs.map → widgets-ClEnYQCH.mjs.map} +1 -1
  346. package/dist/{zod-generator-BSDpkqSH.mjs → zod-generator-Djo_VHCt.mjs} +2 -2
  347. package/dist/{zod-generator-BSDpkqSH.mjs.map → zod-generator-Djo_VHCt.mjs.map} +1 -1
  348. package/package.json +5 -5
  349. package/src/api/handlers/content.ts +107 -8
  350. package/src/api/handlers/index.ts +2 -0
  351. package/src/api/openapi/document.ts +25 -0
  352. package/src/api/schemas/content.ts +33 -0
  353. package/src/astro/integration/index.ts +98 -0
  354. package/src/astro/integration/routes.ts +6 -0
  355. package/src/astro/integration/virtual-modules.ts +39 -0
  356. package/src/astro/integration/vite-config.ts +12 -0
  357. package/src/astro/middleware.ts +28 -0
  358. package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
  359. package/src/astro/routes/api/content/[collection]/[id]/publish.ts +4 -2
  360. package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +8 -4
  361. package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +4 -2
  362. package/src/astro/routes/api/content/[collection]/[id].ts +4 -2
  363. package/src/astro/routes/api/content/[collection]/authors.ts +34 -0
  364. package/src/astro/types.ts +8 -1
  365. package/src/bylines/index.ts +57 -0
  366. package/src/cli/commands/export-seed.ts +28 -12
  367. package/src/components/EmDashImage.astro +22 -4
  368. package/src/components/Image.astro +20 -3
  369. package/src/database/migrations/043_content_references.ts +121 -0
  370. package/src/database/migrations/runner.ts +2 -0
  371. package/src/database/repositories/content.ts +225 -67
  372. package/src/database/repositories/index.ts +7 -0
  373. package/src/database/repositories/relation.ts +467 -0
  374. package/src/database/repositories/types.ts +31 -0
  375. package/src/database/repositories/user.ts +18 -0
  376. package/src/database/types.ts +34 -0
  377. package/src/emdash-runtime.ts +141 -42
  378. package/src/index.ts +8 -1
  379. package/src/loader.ts +67 -34
  380. package/src/media/responsive.ts +125 -0
  381. package/src/plugins/cron.ts +3 -2
  382. package/src/plugins/index.ts +5 -0
  383. package/src/plugins/scheduler/node.ts +9 -2
  384. package/src/query.ts +32 -5
  385. package/src/scheduled-publish.ts +153 -0
  386. package/src/seed/apply.ts +16 -6
  387. package/src/seed/types.ts +9 -0
  388. package/src/seed/validate.ts +15 -0
  389. package/src/taxonomies/index.ts +1 -0
  390. package/src/virtual-modules.d.ts +11 -0
  391. package/dist/api-Cs7DAACP.mjs.map +0 -1
  392. package/dist/apply-BWMV4Zmw.mjs.map +0 -1
  393. package/dist/byline-fields-BNy7Ng1U.d.mts.map +0 -1
  394. package/dist/content-CyqOmOzm.mjs.map +0 -1
  395. package/dist/cron-DZovZUnC.mjs.map +0 -1
  396. package/dist/index-CjKdMZ3U.d.mts.map +0 -1
  397. package/dist/loader-Dyx8dhFV.mjs.map +0 -1
  398. package/dist/menus-BKkxXCmd.mjs.map +0 -1
  399. package/dist/query-Ctlq1aOk.mjs.map +0 -1
  400. package/dist/redirects-C0L9JUk4.mjs.map +0 -1
  401. package/dist/runner-pt6Wl-l-.mjs.map +0 -1
  402. package/dist/taxonomies-Crtzy4MT.mjs.map +0 -1
  403. package/dist/types-i8_uzhMD.d.mts.map +0 -1
  404. package/dist/user-DzEUl5zA.mjs.map +0 -1
  405. package/dist/validate-Dy6nkNls.d.mts.map +0 -1
  406. package/dist/validate-JCXcsqiY.mjs.map +0 -1
  407. package/dist/version-CnS-Cr8A.mjs +0 -7
  408. package/src/plugins/scheduler/piggyback.ts +0 -71
@@ -0,0 +1,467 @@
1
+ import type { Kysely, Selectable } from "kysely";
2
+ import { ulid } from "ulidx";
3
+
4
+ import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
5
+ import type { Database, RelationTable, ContentReferenceTable } from "../types.js";
6
+
7
+ export interface Relation {
8
+ id: string;
9
+ name: string;
10
+ parentCollection: string;
11
+ childCollection: string;
12
+ parentLabel: string;
13
+ childLabel: string;
14
+ locale: string;
15
+ translationGroup: string;
16
+ }
17
+
18
+ export interface CreateRelationInput {
19
+ name: string;
20
+ parentCollection: string;
21
+ childCollection: string;
22
+ parentLabel: string;
23
+ childLabel: string;
24
+ /** Omit to let the DB default (current value: 'en') apply. Higher layers
25
+ * resolve locale from request context / i18n config. */
26
+ locale?: string;
27
+ /** When set, joins the source relation's translation_group AND inherits its
28
+ * structural fields (name, parentCollection, childCollection). Only locale +
29
+ * labels may differ on a translation. */
30
+ translationOf?: string;
31
+ }
32
+
33
+ export interface UpdateRelationInput {
34
+ /** Only localized fields are mutable per row. Changing structural fields
35
+ * (name/collections) is a cross-group operation deferred to a later slice. */
36
+ parentLabel?: string;
37
+ childLabel?: string;
38
+ }
39
+
40
+ export interface ContentReference {
41
+ id: string;
42
+ relationGroup: string;
43
+ parentGroup: string;
44
+ childGroup: string;
45
+ sortOrder: number;
46
+ }
47
+
48
+ /**
49
+ * Content-references repository.
50
+ *
51
+ * Owns relation *definitions* (`_emdash_relations`, row-per-locale, mirroring
52
+ * `_emdash_taxonomy_defs`) and the *edge* junction (`_emdash_content_references`,
53
+ * keyed by `translation_group` so edges are locale-agnostic, mirroring
54
+ * `content_taxonomies`).
55
+ *
56
+ * Like `TaxonomyRepository`, this is not the validation boundary: it trusts its
57
+ * typed inputs. The API slice supplies Zod schemas at the route and enforces
58
+ * collection-agreement / relation-existence invariants in the handler. The repo
59
+ * does not resolve locale fallbacks — callers pass the locale they want.
60
+ */
61
+ export class RelationRepository {
62
+ constructor(private db: Kysely<Database>) {}
63
+
64
+ /**
65
+ * Create a relation. Without `translationOf`, mints a fresh group
66
+ * (`translation_group = id`, matching the migration backfill pattern). With
67
+ * `translationOf`, the structural fields (name, parentCollection,
68
+ * childCollection) and the translation_group are inherited from the source;
69
+ * locale and the two labels are taken from `input`.
70
+ */
71
+ async create(input: CreateRelationInput): Promise<Relation> {
72
+ const id = ulid();
73
+ const now = new Date().toISOString();
74
+
75
+ let translationGroup = id;
76
+ let name = input.name;
77
+ let parentCollection = input.parentCollection;
78
+ let childCollection = input.childCollection;
79
+
80
+ if (input.translationOf) {
81
+ const source = await this.findById(input.translationOf);
82
+ // translation_group is NOT NULL here, so we cannot fall back to a
83
+ // fresh group like TaxonomyRepository does — a bad translationOf must
84
+ // fail loudly rather than silently mint an unlinked relation.
85
+ if (!source) throw new Error("Source relation for translation not found");
86
+ translationGroup = source.translationGroup;
87
+ name = source.name;
88
+ parentCollection = source.parentCollection;
89
+ childCollection = source.childCollection;
90
+ }
91
+
92
+ await this.db
93
+ .insertInto("_emdash_relations")
94
+ .values({
95
+ id,
96
+ name,
97
+ parent_collection: parentCollection,
98
+ child_collection: childCollection,
99
+ parent_label: input.parentLabel,
100
+ child_label: input.childLabel,
101
+ created_at: now,
102
+ updated_at: now,
103
+ // Omit `locale` so the DB DEFAULT (configured defaultLocale)
104
+ // applies — matches TaxonomyRepository.create.
105
+ ...(input.locale !== undefined ? { locale: input.locale } : {}),
106
+ translation_group: translationGroup,
107
+ })
108
+ .execute();
109
+
110
+ const relation = await this.findById(id);
111
+ if (!relation) throw new Error("Failed to create relation");
112
+ return relation;
113
+ }
114
+
115
+ async findById(id: string): Promise<Relation | null> {
116
+ const row = await this.db
117
+ .selectFrom("_emdash_relations")
118
+ .selectAll()
119
+ .where("id", "=", id)
120
+ .executeTakeFirst();
121
+ return row ? this.rowToRelation(row) : null;
122
+ }
123
+
124
+ /**
125
+ * Find a relation by name. With `locale`, filter by it; without, return the
126
+ * lowest-locale-code match deterministically. Mirrors
127
+ * `TaxonomyRepository.findBySlug` — note this returns a single row, unlike
128
+ * `TaxonomyRepository.findByName` which returns every term in a taxonomy.
129
+ */
130
+ async findByName(name: string, locale?: string): Promise<Relation | null> {
131
+ let query = this.db.selectFrom("_emdash_relations").selectAll().where("name", "=", name);
132
+ if (locale !== undefined) query = query.where("locale", "=", locale);
133
+ const row = await query.orderBy("locale", "asc").executeTakeFirst();
134
+ return row ? this.rowToRelation(row) : null;
135
+ }
136
+
137
+ /** Every translation sibling (including itself) sharing a translation_group. */
138
+ async findTranslations(translationGroup: string): Promise<Relation[]> {
139
+ const rows = await this.db
140
+ .selectFrom("_emdash_relations")
141
+ .selectAll()
142
+ .where("translation_group", "=", translationGroup)
143
+ .orderBy("locale", "asc")
144
+ .execute();
145
+ return rows.map((row) => this.rowToRelation(row));
146
+ }
147
+
148
+ /**
149
+ * All relations, ordered by name then id (id is a stable tiebreak for
150
+ * relations sharing a name across locales). Optionally filtered by locale.
151
+ */
152
+ async list(locale?: string): Promise<Relation[]> {
153
+ let query = this.db
154
+ .selectFrom("_emdash_relations")
155
+ .selectAll()
156
+ .orderBy("name", "asc")
157
+ .orderBy("id", "asc");
158
+ if (locale !== undefined) query = query.where("locale", "=", locale);
159
+ const rows = await query.execute();
160
+ return rows.map((row) => this.rowToRelation(row));
161
+ }
162
+
163
+ /** Relations where `collection` is the parent OR the child side. */
164
+ async findForCollection(collection: string, locale?: string): Promise<Relation[]> {
165
+ let query = this.db
166
+ .selectFrom("_emdash_relations")
167
+ .selectAll()
168
+ .where((eb) =>
169
+ eb.or([eb("parent_collection", "=", collection), eb("child_collection", "=", collection)]),
170
+ )
171
+ .orderBy("name", "asc")
172
+ .orderBy("id", "asc");
173
+ if (locale !== undefined) query = query.where("locale", "=", locale);
174
+ const rows = await query.execute();
175
+ return rows.map((row) => this.rowToRelation(row));
176
+ }
177
+
178
+ /**
179
+ * Update the localized labels of one relation row. Structural fields are
180
+ * immutable here (a cross-group concern). No-ops when nothing is supplied.
181
+ */
182
+ async update(id: string, input: UpdateRelationInput): Promise<Relation | null> {
183
+ const existing = await this.findById(id);
184
+ if (!existing) return null;
185
+
186
+ const updates: Record<string, unknown> = {};
187
+ if (input.parentLabel !== undefined) updates.parent_label = input.parentLabel;
188
+ if (input.childLabel !== undefined) updates.child_label = input.childLabel;
189
+
190
+ if (Object.keys(updates).length > 0) {
191
+ updates.updated_at = new Date().toISOString();
192
+ await this.db.updateTable("_emdash_relations").set(updates).where("id", "=", id).execute();
193
+ }
194
+
195
+ return this.findById(id);
196
+ }
197
+
198
+ /**
199
+ * Delete one relation row. When it is the *last* translation of its group,
200
+ * purge edges referencing that group (application-layer cascade — group
201
+ * linking precludes a SQL FK). Mirrors `TaxonomyRepository.delete`.
202
+ */
203
+ async delete(id: string): Promise<boolean> {
204
+ const relation = await this.findById(id);
205
+ if (!relation) return false;
206
+
207
+ const siblings = await this.db
208
+ .selectFrom("_emdash_relations")
209
+ .select("id")
210
+ .where("translation_group", "=", relation.translationGroup)
211
+ .where("id", "!=", id)
212
+ .execute();
213
+ if (siblings.length === 0) {
214
+ await this.db
215
+ .deleteFrom("_emdash_content_references")
216
+ .where("relation_group", "=", relation.translationGroup)
217
+ .execute();
218
+ }
219
+
220
+ const result = await this.db
221
+ .deleteFrom("_emdash_relations")
222
+ .where("id", "=", id)
223
+ .executeTakeFirst();
224
+ return (result.numDeletedRows ?? 0n) > 0n;
225
+ }
226
+
227
+ /** Normalize a relation id OR group to its translation_group. Returns null
228
+ * for an unknown relation (edge methods then no-op, matching
229
+ * `TaxonomyRepository.attachToEntry`). */
230
+ private async resolveRelationGroup(idOrGroup: string): Promise<string | null> {
231
+ const row = await this.db
232
+ .selectFrom("_emdash_relations")
233
+ .select(["translation_group"])
234
+ .where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)]))
235
+ .executeTakeFirst();
236
+ return row?.translation_group ?? null;
237
+ }
238
+
239
+ private rowToReference(row: Selectable<ContentReferenceTable>): ContentReference {
240
+ return {
241
+ id: row.id,
242
+ relationGroup: row.relation_group,
243
+ parentGroup: row.parent_group,
244
+ childGroup: row.child_group,
245
+ sortOrder: row.sort_order,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * Link `parentGroup → childGroup` under a relation. `relation` is a relation
251
+ * id or group. Idempotent (onConflict doNothing against the unique edge).
252
+ * `sortOrder` defaults to append: max(sort_order)+1 within (relation, parent).
253
+ *
254
+ * The default-append MAX→INSERT is not atomic: concurrent appends without an
255
+ * explicit `sortOrder` may both read the same max and collide on sort_order,
256
+ * and onConflict silently drops the loser. Callers needing strict ordering
257
+ * under concurrency should pass `sortOrder` explicitly (or serialize).
258
+ */
259
+ async addReference(
260
+ relation: string,
261
+ parentGroup: string,
262
+ childGroup: string,
263
+ sortOrder?: number,
264
+ ): Promise<void> {
265
+ const relationGroup = await this.resolveRelationGroup(relation);
266
+ if (!relationGroup) return;
267
+
268
+ let order = sortOrder;
269
+ if (order === undefined) {
270
+ const max = await this.db
271
+ .selectFrom("_emdash_content_references")
272
+ .select((eb) => eb.fn.max("sort_order").as("max"))
273
+ .where("relation_group", "=", relationGroup)
274
+ .where("parent_group", "=", parentGroup)
275
+ .executeTakeFirst();
276
+ order = max?.max === null || max?.max === undefined ? 0 : Number(max.max) + 1;
277
+ }
278
+
279
+ await this.db
280
+ .insertInto("_emdash_content_references")
281
+ .values({
282
+ id: ulid(),
283
+ relation_group: relationGroup,
284
+ parent_group: parentGroup,
285
+ child_group: childGroup,
286
+ sort_order: order,
287
+ created_at: new Date().toISOString(),
288
+ })
289
+ .onConflict((oc) => oc.doNothing())
290
+ .execute();
291
+ }
292
+
293
+ /** Remove one `parentGroup → childGroup` edge under a relation. */
294
+ async removeReference(relation: string, parentGroup: string, childGroup: string): Promise<void> {
295
+ const relationGroup = await this.resolveRelationGroup(relation);
296
+ if (!relationGroup) return;
297
+
298
+ await this.db
299
+ .deleteFrom("_emdash_content_references")
300
+ .where("relation_group", "=", relationGroup)
301
+ .where("parent_group", "=", parentGroup)
302
+ .where("child_group", "=", childGroup)
303
+ .execute();
304
+ }
305
+
306
+ /** Forward traversal: a parent's children for a relation, ordered. */
307
+ async getChildren(relation: string, parentGroup: string): Promise<ContentReference[]> {
308
+ const relationGroup = await this.resolveRelationGroup(relation);
309
+ if (!relationGroup) return [];
310
+
311
+ const rows = await this.db
312
+ .selectFrom("_emdash_content_references")
313
+ .selectAll()
314
+ .where("relation_group", "=", relationGroup)
315
+ .where("parent_group", "=", parentGroup)
316
+ .orderBy("sort_order", "asc")
317
+ .orderBy("id", "asc")
318
+ .execute();
319
+ return rows.map((row) => this.rowToReference(row));
320
+ }
321
+
322
+ /** Backlink traversal: the parents that reference a child for a relation. */
323
+ async getParents(relation: string, childGroup: string): Promise<ContentReference[]> {
324
+ const relationGroup = await this.resolveRelationGroup(relation);
325
+ if (!relationGroup) return [];
326
+
327
+ const rows = await this.db
328
+ .selectFrom("_emdash_content_references")
329
+ .selectAll()
330
+ .where("relation_group", "=", relationGroup)
331
+ .where("child_group", "=", childGroup)
332
+ .orderBy("id", "asc")
333
+ .execute();
334
+ return rows.map((row) => this.rowToReference(row));
335
+ }
336
+
337
+ /**
338
+ * Replace all children of `parentGroup` under a relation with `childGroups`,
339
+ * assigning positional sort_order (index in the deduped array). Deletes the
340
+ * old set for this (relation, parent) and re-inserts — simple and correct;
341
+ * the set is small (one parent's children). Mirrors the intent of
342
+ * `TaxonomyRepository.setTermsForEntry`.
343
+ *
344
+ * A parent references a given child at most once (the unique edge), so
345
+ * duplicate `childGroups` are collapsed first-occurrence-wins rather than
346
+ * relying on the insert's onConflict to silently drop them. Not wrapped in a
347
+ * transaction: a crash between the delete and insert leaves the parent with
348
+ * no children — acceptable for a replace-all, since a retry restores state.
349
+ */
350
+ async setChildren(relation: string, parentGroup: string, childGroups: string[]): Promise<void> {
351
+ const relationGroup = await this.resolveRelationGroup(relation);
352
+ if (!relationGroup) return;
353
+
354
+ await this.db
355
+ .deleteFrom("_emdash_content_references")
356
+ .where("relation_group", "=", relationGroup)
357
+ .where("parent_group", "=", parentGroup)
358
+ .execute();
359
+
360
+ // Collapse duplicates so positional sort_order has no gaps.
361
+ const uniqueChildGroups = [...new Set(childGroups)];
362
+ if (uniqueChildGroups.length === 0) return;
363
+
364
+ const now = new Date().toISOString();
365
+ await this.db
366
+ .insertInto("_emdash_content_references")
367
+ .values(
368
+ uniqueChildGroups.map((childGroup, index) => ({
369
+ id: ulid(),
370
+ relation_group: relationGroup,
371
+ parent_group: parentGroup,
372
+ child_group: childGroup,
373
+ sort_order: index,
374
+ created_at: now,
375
+ })),
376
+ )
377
+ // Belt-and-suspenders: the DELETE above already cleared this
378
+ // (relation, parent), so no conflict is possible within one call.
379
+ // This is NOT a concurrency guarantee — delete-then-insert is not atomic.
380
+ .onConflict((oc) => oc.doNothing())
381
+ .execute();
382
+ }
383
+
384
+ /**
385
+ * Remove every edge where `group` is the parent OR the child — i.e. ensure no
386
+ * orphaned reference edges survive when a content entry is deleted. The
387
+ * application-layer cascade that group-linking precludes at the SQL level.
388
+ * Wiring this into the content-delete path is a later (handler) slice.
389
+ * Returns the number of edges removed.
390
+ */
391
+ async clearReferencesForGroup(group: string): Promise<number> {
392
+ const result = await this.db
393
+ .deleteFrom("_emdash_content_references")
394
+ .where((eb) => eb.or([eb("parent_group", "=", group), eb("child_group", "=", group)]))
395
+ .executeTakeFirst();
396
+ return Number(result.numDeletedRows ?? 0);
397
+ }
398
+
399
+ /** Count a parent's children under a relation. */
400
+ async countChildren(relation: string, parentGroup: string): Promise<number> {
401
+ const relationGroup = await this.resolveRelationGroup(relation);
402
+ if (!relationGroup) return 0;
403
+ const result = await this.db
404
+ .selectFrom("_emdash_content_references")
405
+ .select((eb) => eb.fn.count("id").as("count"))
406
+ .where("relation_group", "=", relationGroup)
407
+ .where("parent_group", "=", parentGroup)
408
+ .executeTakeFirst();
409
+ return Number(result?.count ?? 0);
410
+ }
411
+
412
+ /** Count a child's parents (backlinks) under a relation. */
413
+ async countParents(relation: string, childGroup: string): Promise<number> {
414
+ const relationGroup = await this.resolveRelationGroup(relation);
415
+ if (!relationGroup) return 0;
416
+ const result = await this.db
417
+ .selectFrom("_emdash_content_references")
418
+ .select((eb) => eb.fn.count("id").as("count"))
419
+ .where("relation_group", "=", relationGroup)
420
+ .where("child_group", "=", childGroup)
421
+ .executeTakeFirst();
422
+ return Number(result?.count ?? 0);
423
+ }
424
+
425
+ /**
426
+ * Batch child-counts for many parents under a relation. Chunks at
427
+ * SQL_BATCH_SIZE for D1's bind-parameter limit. Returns parent_group → count
428
+ * (parents with no children are absent from the map). Mirrors
429
+ * `TaxonomyRepository.countEntriesForTerms`.
430
+ */
431
+ async countChildrenForParents(
432
+ relation: string,
433
+ parentGroups: string[],
434
+ ): Promise<Map<string, number>> {
435
+ const counts = new Map<string, number>();
436
+ if (parentGroups.length === 0) return counts;
437
+ const relationGroup = await this.resolveRelationGroup(relation);
438
+ if (!relationGroup) return counts;
439
+
440
+ for (const chunk of chunks(parentGroups, SQL_BATCH_SIZE)) {
441
+ const rows = await this.db
442
+ .selectFrom("_emdash_content_references")
443
+ .select(["parent_group", (eb) => eb.fn.count("id").as("count")])
444
+ .where("relation_group", "=", relationGroup)
445
+ .where("parent_group", "in", chunk)
446
+ .groupBy("parent_group")
447
+ .execute();
448
+ for (const row of rows) {
449
+ counts.set(row.parent_group, Number(row.count ?? 0));
450
+ }
451
+ }
452
+ return counts;
453
+ }
454
+
455
+ private rowToRelation(row: Selectable<RelationTable>): Relation {
456
+ return {
457
+ id: row.id,
458
+ name: row.name,
459
+ parentCollection: row.parent_collection,
460
+ childCollection: row.child_collection,
461
+ parentLabel: row.parent_label,
462
+ childLabel: row.child_label,
463
+ locale: row.locale,
464
+ translationGroup: row.translation_group,
465
+ };
466
+ }
467
+ }
@@ -116,6 +116,22 @@ export interface ContentBylineCredit {
116
116
  source?: "explicit" | "inferred";
117
117
  }
118
118
 
119
+ /** A whitelisted timestamp column a content-list date range can filter on. */
120
+ export type ContentDateField = "createdAt" | "updatedAt" | "publishedAt";
121
+
122
+ /**
123
+ * Inclusive date-range filter for a single whitelisted timestamp column.
124
+ * Bounds are compared lexicographically against the stored ISO 8601 strings,
125
+ * which is correct because every timestamp is written via `toISOString()`.
126
+ * Callers wanting an inclusive upper bound should pass an end-of-day value
127
+ * (e.g. `2024-12-31T23:59:59.999Z`); the repository does not widen `to`.
128
+ */
129
+ export interface ContentDateFilter {
130
+ field: ContentDateField;
131
+ from?: string;
132
+ to?: string;
133
+ }
134
+
119
135
  export interface FindManyOptions {
120
136
  where?: {
121
137
  status?: string;
@@ -129,6 +145,8 @@ export interface FindManyOptions {
129
145
  * repository stays generic. Each name is validated as a SQL identifier.
130
146
  */
131
147
  searchColumns?: string[];
148
+ /** Inclusive date range over a whitelisted timestamp column. */
149
+ dateFilter?: ContentDateFilter;
132
150
  };
133
151
  orderBy?: {
134
152
  field: string;
@@ -238,3 +256,16 @@ export class EmDashValidationError extends Error {
238
256
  this.name = "EmDashValidationError";
239
257
  }
240
258
  }
259
+
260
+ /**
261
+ * Thrown by `publish()` when called with `requireDue` for a row that is no
262
+ * longer due (its `scheduled_at` was cleared or pushed into the future between
263
+ * selection and publish — e.g. an editor unscheduled it). Lets the scheduled
264
+ * sweep skip the row silently rather than treating it as a publish failure.
265
+ */
266
+ export class ScheduledNotDueError extends Error {
267
+ constructor(message = "Content is no longer scheduled to publish") {
268
+ super(message);
269
+ this.name = "ScheduledNotDueError";
270
+ }
271
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Kysely, Selectable, Updateable } from "kysely";
2
2
  import { ulid } from "ulidx";
3
3
 
4
+ import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
4
5
  import type { Database, UserTable } from "../types.js";
5
6
  import { encodeCursor, decodeCursor, type FindManyResult } from "./types.js";
6
7
 
@@ -85,6 +86,23 @@ export class UserRepository {
85
86
  return row ? this.rowToUser(row) : null;
86
87
  }
87
88
 
89
+ /**
90
+ * Batch-resolve users by ID. Returns only the users that exist; missing
91
+ * IDs are silently dropped. Chunked at `SQL_BATCH_SIZE` to stay within
92
+ * D1's bind-parameter limit.
93
+ */
94
+ async findByIds(ids: string[]): Promise<User[]> {
95
+ const unique = [...new Set(ids)].filter((id) => id.length > 0);
96
+ if (unique.length === 0) return [];
97
+
98
+ const out: User[] = [];
99
+ for (const batch of chunks(unique, SQL_BATCH_SIZE)) {
100
+ const rows = await this.db.selectFrom("users").selectAll().where("id", "in", batch).execute();
101
+ for (const row of rows) out.push(this.rowToUser(row));
102
+ }
103
+ return out;
104
+ }
105
+
88
106
  /**
89
107
  * Find user by email (case-insensitive)
90
108
  */
@@ -440,6 +440,8 @@ export interface Database {
440
440
  _emdash_byline_fields: BylineFieldTable;
441
441
  _emdash_byline_field_values: BylineFieldValueTable;
442
442
  _emdash_byline_field_group_values: BylineFieldGroupValueTable;
443
+ _emdash_relations: RelationTable;
444
+ _emdash_content_references: ContentReferenceTable;
443
445
  _emdash_rate_limits: RateLimitTable;
444
446
  }
445
447
 
@@ -573,6 +575,38 @@ export interface BylineFieldGroupValueTable {
573
575
  updated_at: Generated<string>;
574
576
  }
575
577
 
578
+ // Content references
579
+ //
580
+ // `_emdash_relations` defines relationship types (row-per-locale, like
581
+ // `_emdash_taxonomy_defs`). `_emdash_content_references` holds directed edges
582
+ // between content entries, linked by `translation_group` so they are
583
+ // locale-agnostic — no foreign keys, mirroring `content_taxonomies`.
584
+
585
+ export interface RelationTable {
586
+ id: string;
587
+ name: string;
588
+ parent_collection: string;
589
+ child_collection: string;
590
+ parent_label: string;
591
+ child_label: string;
592
+ locale: Generated<string>;
593
+ translation_group: string;
594
+ created_at: Generated<string>;
595
+ updated_at: Generated<string>;
596
+ }
597
+
598
+ export interface ContentReferenceTable {
599
+ id: string;
600
+ /** Stores `_emdash_relations.translation_group` (locale-agnostic). No FK. */
601
+ relation_group: string;
602
+ /** Parent entry's `translation_group`. */
603
+ parent_group: string;
604
+ /** Child entry's `translation_group`. */
605
+ child_group: string;
606
+ sort_order: Generated<number>;
607
+ created_at: Generated<string>;
608
+ }
609
+
576
610
  // Rate Limits
577
611
 
578
612
  export interface RateLimitTable {