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.
- package/dist/api/route-utils.d.mts +2 -2
- package/dist/api/route-utils.mjs +14 -14
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +3 -3
- package/dist/{api-Cs7DAACP.mjs → api-BZ6bhjYs.mjs} +88 -16
- package/dist/api-BZ6bhjYs.mjs.map +1 -0
- package/dist/{apply-BWMV4Zmw.mjs → apply-hQkKKBCf.mjs} +23 -23
- package/dist/apply-hQkKKBCf.mjs.map +1 -0
- package/dist/astro/index.d.mts +8 -8
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +113 -23
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +7 -7
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.mjs +4 -4
- package/dist/astro/middleware/request-context.mjs +2 -2
- package/dist/astro/middleware.d.mts +26 -4
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +205 -173
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +3 -3
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +8 -8
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +8 -8
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +8 -8
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +12 -12
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +12 -12
- package/dist/astro/routes/api/admin/bylines/index.mjs +12 -12
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +11 -11
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +31 -31
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +31 -31
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/index.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +31 -31
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +30 -30
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +31 -31
- package/dist/astro/routes/api/admin/plugins/updates.mjs +30 -30
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +30 -30
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +30 -30
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
- package/dist/astro/routes/api/admin/users/index.mjs +5 -5
- package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/index.mjs +7 -7
- package/dist/astro/routes/api/auth/invite/register-options.mjs +6 -6
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +6 -6
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +7 -7
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/verify.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/complete.mjs +6 -6
- package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +6 -5
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +9 -8
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +12 -10
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +6 -5
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +9 -8
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.d.mts +8 -0
- package/dist/astro/routes/api/content/_collection_/authors.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/authors.mjs +19 -0
- package/dist/astro/routes/api/content/_collection_/authors.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
- package/dist/astro/routes/api/dashboard.mjs +7 -7
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/import/probe.d.mts +2 -2
- package/dist/astro/routes/api/import/probe.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +7 -7
- package/dist/astro/routes/api/import/wordpress/execute.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/media.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +9 -9
- package/dist/astro/routes/api/manifest.mjs +3 -3
- package/dist/astro/routes/api/mcp.mjs +28 -28
- package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
- package/dist/astro/routes/api/media/_id_.mjs +6 -6
- package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
- package/dist/astro/routes/api/media/providers/index.mjs +3 -3
- package/dist/astro/routes/api/media/upload-url.mjs +6 -6
- package/dist/astro/routes/api/media.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_.mjs +7 -7
- package/dist/astro/routes/api/menus/index.mjs +7 -7
- package/dist/astro/routes/api/oauth/authorize.mjs +1 -1
- package/dist/astro/routes/api/oauth/device/authorize.mjs +4 -4
- package/dist/astro/routes/api/oauth/device/code.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/token.mjs +5 -5
- package/dist/astro/routes/api/oauth/register.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/refresh.mjs +4 -4
- package/dist/astro/routes/api/oauth/token/revoke.mjs +4 -4
- package/dist/astro/routes/api/oauth/token.mjs +4 -4
- package/dist/astro/routes/api/openapi.json.mjs +17 -3
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
- package/dist/astro/routes/api/redirects/404s/index.mjs +9 -9
- package/dist/astro/routes/api/redirects/404s/summary.mjs +9 -9
- package/dist/astro/routes/api/redirects/_id_.mjs +10 -10
- package/dist/astro/routes/api/redirects/index.mjs +10 -10
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +30 -30
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +30 -30
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +30 -30
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +30 -30
- package/dist/astro/routes/api/schema/collections/index.mjs +30 -30
- package/dist/astro/routes/api/schema/index.mjs +6 -6
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +30 -30
- package/dist/astro/routes/api/schema/orphans/index.mjs +30 -30
- package/dist/astro/routes/api/search/enable.mjs +9 -9
- package/dist/astro/routes/api/search/index.mjs +8 -8
- package/dist/astro/routes/api/search/rebuild.mjs +9 -9
- package/dist/astro/routes/api/search/stats.mjs +6 -6
- package/dist/astro/routes/api/search/suggest.mjs +8 -8
- package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
- package/dist/astro/routes/api/sections/index.mjs +8 -8
- package/dist/astro/routes/api/settings/email.mjs +5 -5
- package/dist/astro/routes/api/settings.mjs +12 -12
- package/dist/astro/routes/api/setup/admin-verify.mjs +6 -6
- package/dist/astro/routes/api/setup/admin.mjs +6 -6
- package/dist/astro/routes/api/setup/dev-bypass.mjs +18 -18
- package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
- package/dist/astro/routes/api/setup/index.mjs +21 -21
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +5 -5
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +11 -11
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +11 -11
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +11 -11
- package/dist/astro/routes/api/taxonomies/index.mjs +11 -11
- package/dist/astro/routes/api/themes/preview.mjs +5 -5
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
- package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +6 -6
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +6 -6
- package/dist/astro/routes/sitemap.xml.mjs +6 -6
- package/dist/astro/types.d.mts +15 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{authorize-CotM4Yiu.mjs → authorize-C_8t2KGa.mjs} +2 -2
- package/dist/{authorize-CotM4Yiu.mjs.map → authorize-C_8t2KGa.mjs.map} +1 -1
- package/dist/{byline-CWQ9aSoz.mjs → byline-DUx48sJp.mjs} +6 -6
- package/dist/{byline-CWQ9aSoz.mjs.map → byline-DUx48sJp.mjs.map} +1 -1
- package/dist/{byline-fields-Dr-xcb6S.mjs → byline-fields-51kg6Vuv.mjs} +3 -3
- package/dist/{byline-fields-Dr-xcb6S.mjs.map → byline-fields-51kg6Vuv.mjs.map} +1 -1
- package/dist/{byline-fields-DC3Wkk-U.mjs → byline-fields-C_OsR-KF.mjs} +2 -2
- package/dist/{byline-fields-DC3Wkk-U.mjs.map → byline-fields-C_OsR-KF.mjs.map} +1 -1
- package/dist/{byline-fields-BNy7Ng1U.d.mts → byline-fields-DYXKDuNX.d.mts} +26 -2
- package/dist/byline-fields-DYXKDuNX.d.mts.map +1 -0
- package/dist/{byline-registry-CxK5g559.mjs → byline-registry-CWP7I71B.mjs} +3 -3
- package/dist/{byline-registry-CxK5g559.mjs.map → byline-registry-CWP7I71B.mjs.map} +1 -1
- package/dist/{bylines-LJMgENMI.mjs → bylines-Cx5n-WqP.mjs} +3 -3
- package/dist/{bylines-LJMgENMI.mjs.map → bylines-Cx5n-WqP.mjs.map} +1 -1
- package/dist/{bylines-BJSva1Un.mjs → bylines-wurS258E.mjs} +50 -6
- package/dist/{bylines-BJSva1Un.mjs.map → bylines-wurS258E.mjs.map} +1 -1
- package/dist/{cache-lZL7SgVb.mjs → cache-B_HzASVT.mjs} +3 -3
- package/dist/{cache-lZL7SgVb.mjs.map → cache-B_HzASVT.mjs.map} +1 -1
- package/dist/{chunks-BU-vP9Dh.mjs → chunks-BerYVuve.mjs} +2 -2
- package/dist/{chunks-BU-vP9Dh.mjs.map → chunks-BerYVuve.mjs.map} +1 -1
- package/dist/cli/index.mjs +40 -27
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/{comment-C4jVbCM8.mjs → comment-sqQxNpN3.mjs} +2 -2
- package/dist/{comment-C4jVbCM8.mjs.map → comment-sqQxNpN3.mjs.map} +1 -1
- package/dist/{comments-BTAbC0Ek.mjs → comments-CJ0RZsYR.mjs} +3 -3
- package/dist/{comments-BTAbC0Ek.mjs.map → comments-CJ0RZsYR.mjs.map} +1 -1
- package/dist/{content-CyqOmOzm.mjs → content-BIlVx-RX.mjs} +132 -43
- package/dist/content-BIlVx-RX.mjs.map +1 -0
- package/dist/{context-DZ7bEh5-.mjs → context-GG52SPgh.mjs} +10 -10
- package/dist/{context-DZ7bEh5-.mjs.map → context-GG52SPgh.mjs.map} +1 -1
- package/dist/{cron-DZovZUnC.mjs → cron-BJ2ClIlj.mjs} +4 -3
- package/dist/cron-BJ2ClIlj.mjs.map +1 -0
- package/dist/{dashboard-B5WQpNTP.mjs → dashboard-2JgAMWxK.mjs} +4 -4
- package/dist/{dashboard-B5WQpNTP.mjs.map → dashboard-2JgAMWxK.mjs.map} +1 -1
- package/dist/db/index.d.mts +2 -2
- package/dist/db/index.mjs +1 -1
- package/dist/{device-flow-ptLrVINd.mjs → device-flow-s6_q3T7A.mjs} +2 -2
- package/dist/{device-flow-ptLrVINd.mjs.map → device-flow-s6_q3T7A.mjs.map} +1 -1
- package/dist/{error-DJOsMVSt.mjs → error-RwM4dD35.mjs} +2 -2
- package/dist/{error-DJOsMVSt.mjs.map → error-RwM4dD35.mjs.map} +1 -1
- package/dist/{fts-manager-DR1ERA0c.mjs → fts-manager-1RgHmopc.mjs} +2 -2
- package/dist/{fts-manager-DR1ERA0c.mjs.map → fts-manager-1RgHmopc.mjs.map} +1 -1
- package/dist/{index-D60_SzHG.d.mts → index-BpYeJO1E.d.mts} +2 -2
- package/dist/{index-D60_SzHG.d.mts.map → index-BpYeJO1E.d.mts.map} +1 -1
- package/dist/{index-CjKdMZ3U.d.mts → index-FfiTQJq2.d.mts} +199 -17
- package/dist/index-FfiTQJq2.d.mts.map +1 -0
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +43 -43
- package/dist/{load-6ZrRhepW.mjs → load-B84ohfBk.mjs} +2 -2
- package/dist/{load-6ZrRhepW.mjs.map → load-B84ohfBk.mjs.map} +1 -1
- package/dist/{loader-Dyx8dhFV.mjs → loader-CpZKpFz0.mjs} +32 -30
- package/dist/loader-CpZKpFz0.mjs.map +1 -0
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +6 -6
- package/dist/{media-C-oovGCG.mjs → media-JOf3pNkw.mjs} +2 -2
- package/dist/{media-C-oovGCG.mjs.map → media-JOf3pNkw.mjs.map} +1 -1
- package/dist/{menus-DugoYwTX.mjs → menus-DX4_E01q.mjs} +3 -3
- package/dist/{menus-DugoYwTX.mjs.map → menus-DX4_E01q.mjs.map} +1 -1
- package/dist/{menus-BKkxXCmd.mjs → menus-Dp9xporj.mjs} +86 -9
- package/dist/menus-Dp9xporj.mjs.map +1 -0
- package/dist/{normalize-DVV8nbrL.mjs → normalize-CK5o04zr.mjs} +2 -2
- package/dist/{normalize-DVV8nbrL.mjs.map → normalize-CK5o04zr.mjs.map} +1 -1
- package/dist/{oauth-authorization-DvBAL75d.mjs → oauth-authorization-1aPAYjiC.mjs} +2 -2
- package/dist/{oauth-authorization-DvBAL75d.mjs.map → oauth-authorization-1aPAYjiC.mjs.map} +1 -1
- package/dist/{options-BL4X94qY.mjs → options-BPCVnesz.mjs} +1 -1
- package/dist/{options-BL4X94qY.mjs.map → options-BPCVnesz.mjs.map} +1 -1
- package/dist/{options-tb7DJROi.d.mts → options-D4MnavW_.d.mts} +3 -3
- package/dist/{options-tb7DJROi.d.mts.map → options-D4MnavW_.d.mts.map} +1 -1
- package/dist/{parse-BBkFmLVr.mjs → parse-CrGndy1A.mjs} +2 -2
- package/dist/{parse-BBkFmLVr.mjs.map → parse-CrGndy1A.mjs.map} +1 -1
- package/dist/{patterns-CqG5Ya3i.mjs → patterns-p-RBdTbM.mjs} +1 -1
- package/dist/{patterns-CqG5Ya3i.mjs.map → patterns-p-RBdTbM.mjs.map} +1 -1
- package/dist/plugin-utils.d.mts +7 -7
- package/dist/plugins/adapt-sandbox-entry.d.mts +7 -7
- package/dist/{query-Ctlq1aOk.mjs → query-BFQ029Ts.mjs} +21 -15
- package/dist/query-BFQ029Ts.mjs.map +1 -0
- package/dist/{rate-limit-CH6W6ikK.mjs → rate-limit-ClFFUga6.mjs} +2 -2
- package/dist/{rate-limit-CH6W6ikK.mjs.map → rate-limit-ClFFUga6.mjs.map} +1 -1
- package/dist/{redirect-C6tJA7tk.mjs → redirect-CRWIt8Zj.mjs} +3 -3
- package/dist/{redirect-C6tJA7tk.mjs.map → redirect-CRWIt8Zj.mjs.map} +1 -1
- package/dist/{redirects-C0L9JUk4.mjs → redirects-DEygMrRO.mjs} +25 -3
- package/dist/redirects-DEygMrRO.mjs.map +1 -0
- package/dist/{redirects-CacE9eQa.mjs → redirects-OIu6vQ2i.mjs} +5 -5
- package/dist/{redirects-CacE9eQa.mjs.map → redirects-OIu6vQ2i.mjs.map} +1 -1
- package/dist/{registry-CIDxZbhh.mjs → registry-brYh-rAT.mjs} +6 -6
- package/dist/{registry-CIDxZbhh.mjs.map → registry-brYh-rAT.mjs.map} +1 -1
- package/dist/{request-cache-BYMs-BGX.mjs → request-cache-D32LpnmI.mjs} +1 -1
- package/dist/{request-cache-BYMs-BGX.mjs.map → request-cache-D32LpnmI.mjs.map} +1 -1
- package/dist/{runner-pt6Wl-l-.mjs → runner--4wMWwKM.mjs} +217 -166
- package/dist/runner--4wMWwKM.mjs.map +1 -0
- package/dist/{runner-DM1yR5qd.d.mts → runner-BcRuXq_h.d.mts} +2 -2
- package/dist/{runner-DM1yR5qd.d.mts.map → runner-BcRuXq_h.d.mts.map} +1 -1
- package/dist/runtime.d.mts +7 -7
- package/dist/runtime.mjs +2 -2
- package/dist/{schema-B4tk0HAG.mjs → schema-CS7Eg5gh.mjs} +5 -5
- package/dist/{schema-B4tk0HAG.mjs.map → schema-CS7Eg5gh.mjs.map} +1 -1
- package/dist/{search-f-fNfwab.mjs → search-o-aQzHI1.mjs} +4 -4
- package/dist/{search-f-fNfwab.mjs.map → search-o-aQzHI1.mjs.map} +1 -1
- package/dist/{secrets-YYbTgB1w.mjs → secrets-C_ZtRos3.mjs} +2 -2
- package/dist/{secrets-YYbTgB1w.mjs.map → secrets-C_ZtRos3.mjs.map} +1 -1
- package/dist/{sections-biElLfT9.mjs → sections-DhsZ0ns9.mjs} +3 -3
- package/dist/{sections-biElLfT9.mjs.map → sections-DhsZ0ns9.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +16 -16
- package/dist/seo/index.d.mts +1 -1
- package/dist/{seo-BR39kvTF.mjs → seo-B5e6y9Wk.mjs} +2 -2
- package/dist/{seo-BR39kvTF.mjs.map → seo-B5e6y9Wk.mjs.map} +1 -1
- package/dist/{service-BhR2acnc.mjs → service-DAxg8RPR.mjs} +2 -2
- package/dist/{service-BhR2acnc.mjs.map → service-DAxg8RPR.mjs.map} +1 -1
- package/dist/{settings-b5zW1R1T.mjs → settings-B1p-gPUK.mjs} +5 -5
- package/dist/{settings-b5zW1R1T.mjs.map → settings-B1p-gPUK.mjs.map} +1 -1
- package/dist/{settings-D_NJvjgN.mjs → settings-DIsbHTRE.mjs} +3 -3
- package/dist/{settings-D_NJvjgN.mjs.map → settings-DIsbHTRE.mjs.map} +1 -1
- package/dist/{setup-complete-VoEZfasi.mjs → setup-complete-Yuv78yua.mjs} +2 -2
- package/dist/{setup-complete-VoEZfasi.mjs.map → setup-complete-Yuv78yua.mjs.map} +1 -1
- package/dist/{site-url-Cm8-sJy7.mjs → site-url-mEVmwIFi.mjs} +2 -2
- package/dist/{site-url-Cm8-sJy7.mjs.map → site-url-mEVmwIFi.mjs.map} +1 -1
- package/dist/{taxonomies-Crtzy4MT.mjs → taxonomies-BEW7S5AI.mjs} +7 -6
- package/dist/taxonomies-BEW7S5AI.mjs.map +1 -0
- package/dist/{taxonomies-Mhn9rjTQ.mjs → taxonomies-UusDXv3C.mjs} +4 -4
- package/dist/{taxonomies-Mhn9rjTQ.mjs.map → taxonomies-UusDXv3C.mjs.map} +1 -1
- package/dist/{taxonomy-DTZrIQpi.mjs → taxonomy-CdllE4oq.mjs} +3 -3
- package/dist/{taxonomy-DTZrIQpi.mjs.map → taxonomy-CdllE4oq.mjs.map} +1 -1
- package/dist/{transaction-NQj4VJ7Z.mjs → transaction-x2tJQ-A1.mjs} +1 -1
- package/dist/{transaction-NQj4VJ7Z.mjs.map → transaction-x2tJQ-A1.mjs.map} +1 -1
- package/dist/{transport-OnMNbsIA.d.mts → transport-BwQeeY2p.d.mts} +1 -1
- package/dist/{transport-OnMNbsIA.d.mts.map → transport-BwQeeY2p.d.mts.map} +1 -1
- package/dist/{types-K3MDsxpy.mjs → types-BXSUSAjt.mjs} +16 -3
- package/dist/{types-K3MDsxpy.mjs.map → types-BXSUSAjt.mjs.map} +1 -1
- package/dist/{types-D8bhH891.mjs → types-DZk_y-MU.mjs} +1 -1
- package/dist/{types-D8bhH891.mjs.map → types-DZk_y-MU.mjs.map} +1 -1
- package/dist/{types-DawhLFwy.d.mts → types-OT_Es5mp.d.mts} +26 -1
- package/dist/{types-DawhLFwy.d.mts.map → types-OT_Es5mp.d.mts.map} +1 -1
- package/dist/{types-i8_uzhMD.d.mts → types-WVmpZBJV.d.mts} +18 -3
- package/dist/types-WVmpZBJV.d.mts.map +1 -0
- package/dist/{user-DzEUl5zA.mjs → user-C0um7wrg.mjs} +18 -2
- package/dist/user-C0um7wrg.mjs.map +1 -0
- package/dist/{validate-Dy6nkNls.d.mts → validate-BPAHUSge.d.mts} +10 -2
- package/dist/validate-BPAHUSge.d.mts.map +1 -0
- package/dist/{validate-JCXcsqiY.mjs → validate-ZP9Dvg0P.mjs} +6 -3
- package/dist/validate-ZP9Dvg0P.mjs.map +1 -0
- package/dist/{validation-Bq-VyKJg.mjs → validation-CE5i4q0c.mjs} +5 -5
- package/dist/{validation-Bq-VyKJg.mjs.map → validation-CE5i4q0c.mjs.map} +1 -1
- package/dist/version-Dw0JXu45.mjs +7 -0
- package/dist/{version-CnS-Cr8A.mjs.map → version-Dw0JXu45.mjs.map} +1 -1
- package/dist/{widgets-Bap1eS1X.mjs → widgets-ClEnYQCH.mjs} +2 -2
- package/dist/{widgets-Bap1eS1X.mjs.map → widgets-ClEnYQCH.mjs.map} +1 -1
- package/dist/{zod-generator-BSDpkqSH.mjs → zod-generator-Djo_VHCt.mjs} +2 -2
- package/dist/{zod-generator-BSDpkqSH.mjs.map → zod-generator-Djo_VHCt.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/api/handlers/content.ts +107 -8
- package/src/api/handlers/index.ts +2 -0
- package/src/api/openapi/document.ts +25 -0
- package/src/api/schemas/content.ts +33 -0
- package/src/astro/integration/index.ts +98 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/virtual-modules.ts +39 -0
- package/src/astro/integration/vite-config.ts +12 -0
- package/src/astro/middleware.ts +28 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id].ts +4 -2
- package/src/astro/routes/api/content/[collection]/authors.ts +34 -0
- package/src/astro/types.ts +8 -1
- package/src/bylines/index.ts +57 -0
- package/src/cli/commands/export-seed.ts +28 -12
- package/src/components/EmDashImage.astro +22 -4
- package/src/components/Image.astro +20 -3
- package/src/database/migrations/043_content_references.ts +121 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +225 -67
- package/src/database/repositories/index.ts +7 -0
- package/src/database/repositories/relation.ts +467 -0
- package/src/database/repositories/types.ts +31 -0
- package/src/database/repositories/user.ts +18 -0
- package/src/database/types.ts +34 -0
- package/src/emdash-runtime.ts +141 -42
- package/src/index.ts +8 -1
- package/src/loader.ts +67 -34
- package/src/media/responsive.ts +125 -0
- package/src/plugins/cron.ts +3 -2
- package/src/plugins/index.ts +5 -0
- package/src/plugins/scheduler/node.ts +9 -2
- package/src/query.ts +32 -5
- package/src/scheduled-publish.ts +153 -0
- package/src/seed/apply.ts +16 -6
- package/src/seed/types.ts +9 -0
- package/src/seed/validate.ts +15 -0
- package/src/taxonomies/index.ts +1 -0
- package/src/virtual-modules.d.ts +11 -0
- package/dist/api-Cs7DAACP.mjs.map +0 -1
- package/dist/apply-BWMV4Zmw.mjs.map +0 -1
- package/dist/byline-fields-BNy7Ng1U.d.mts.map +0 -1
- package/dist/content-CyqOmOzm.mjs.map +0 -1
- package/dist/cron-DZovZUnC.mjs.map +0 -1
- package/dist/index-CjKdMZ3U.d.mts.map +0 -1
- package/dist/loader-Dyx8dhFV.mjs.map +0 -1
- package/dist/menus-BKkxXCmd.mjs.map +0 -1
- package/dist/query-Ctlq1aOk.mjs.map +0 -1
- package/dist/redirects-C0L9JUk4.mjs.map +0 -1
- package/dist/runner-pt6Wl-l-.mjs.map +0 -1
- package/dist/taxonomies-Crtzy4MT.mjs.map +0 -1
- package/dist/types-i8_uzhMD.d.mts.map +0 -1
- package/dist/user-DzEUl5zA.mjs.map +0 -1
- package/dist/validate-Dy6nkNls.d.mts.map +0 -1
- package/dist/validate-JCXcsqiY.mjs.map +0 -1
- package/dist/version-CnS-Cr8A.mjs +0 -7
- 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
|
*/
|
package/src/database/types.ts
CHANGED
|
@@ -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 {
|