emdash 0.23.0 → 0.24.1
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.mjs +10 -10
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-BQ_yKRdG.mjs → api-d_0wi9fW.mjs} +13 -13
- package/dist/{api-BQ_yKRdG.mjs.map → api-d_0wi9fW.mjs.map} +1 -1
- package/dist/{apply-CWRyxIoO.mjs → apply-BE9UaWX0.mjs} +16 -16
- package/dist/{apply-CWRyxIoO.mjs.map → apply-BE9UaWX0.mjs.map} +1 -1
- package/dist/astro/index.d.mts +2 -2
- package/dist/astro/index.mjs +25 -6
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +2 -2
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.mjs +4 -4
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +46 -46
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +3 -3
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +3 -3
- 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 +3 -3
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +4 -4
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/index.mjs +9 -9
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
- 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 +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +26 -26
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +27 -27
- package/dist/astro/routes/api/admin/plugins/updates.mjs +26 -26
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +26 -26
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +26 -26
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +2 -2
- package/dist/astro/routes/api/admin/users/index.mjs +3 -3
- 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 +3 -3
- package/dist/astro/routes/api/auth/invite/index.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/register-options.mjs +3 -3
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/complete.mjs +3 -3
- package/dist/astro/routes/api/auth/signup/request.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +6 -6
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
- 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 +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +4 -4
- 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.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/authors.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/index.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/trash.mjs +4 -4
- 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.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +2 -2
- package/dist/astro/routes/api/import/wordpress/execute.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress/media.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +6 -6
- package/dist/astro/routes/api/manifest.mjs +3 -3
- package/dist/astro/routes/api/mcp.mjs +34 -34
- package/dist/astro/routes/api/media/_id_/confirm.mjs +4 -4
- package/dist/astro/routes/api/media/_id_.mjs +4 -4
- 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 +4 -4
- package/dist/astro/routes/api/media.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/items.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_/translations.mjs +5 -5
- package/dist/astro/routes/api/menus/_name_.mjs +5 -5
- package/dist/astro/routes/api/menus/index.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/authorize.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/code.mjs +4 -4
- package/dist/astro/routes/api/oauth/device/token.mjs +4 -4
- package/dist/astro/routes/api/oauth/register.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/refresh.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/revoke.mjs +3 -3
- package/dist/astro/routes/api/oauth/token.mjs +2 -2
- package/dist/astro/routes/api/openapi.json.mjs +2 -2
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +3 -3
- package/dist/astro/routes/api/redirects/404s/index.mjs +6 -6
- package/dist/astro/routes/api/redirects/404s/summary.mjs +6 -6
- package/dist/astro/routes/api/redirects/_id_.mjs +7 -7
- package/dist/astro/routes/api/redirects/index.mjs +7 -7
- 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 +26 -26
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +26 -26
- package/dist/astro/routes/api/schema/collections/index.mjs +26 -26
- package/dist/astro/routes/api/schema/index.mjs +6 -6
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +26 -26
- package/dist/astro/routes/api/schema/orphans/index.mjs +26 -26
- package/dist/astro/routes/api/search/enable.mjs +7 -7
- package/dist/astro/routes/api/search/index.mjs +6 -6
- package/dist/astro/routes/api/search/rebuild.mjs +7 -7
- package/dist/astro/routes/api/search/stats.mjs +6 -6
- package/dist/astro/routes/api/search/suggest.mjs +6 -6
- package/dist/astro/routes/api/sections/_slug_.mjs +6 -6
- package/dist/astro/routes/api/sections/index.mjs +6 -6
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +8 -8
- package/dist/astro/routes/api/setup/admin-verify.mjs +3 -3
- package/dist/astro/routes/api/setup/admin.mjs +3 -3
- package/dist/astro/routes/api/setup/dev-bypass.mjs +16 -16
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +17 -17
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +3 -3
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +8 -8
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +8 -8
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +8 -8
- package/dist/astro/routes/api/taxonomies/index.mjs +8 -8
- package/dist/astro/routes/api/themes/preview.mjs +3 -3
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/index.mjs +6 -6
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +4 -4
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +4 -4
- package/dist/astro/routes/sitemap.xml.mjs +4 -4
- package/dist/astro/types.d.mts +2 -2
- package/dist/{authorize-Cse0k7B0.mjs → authorize-B__u-iZJ.mjs} +2 -2
- package/dist/{authorize-Cse0k7B0.mjs.map → authorize-B__u-iZJ.mjs.map} +1 -1
- package/dist/{byline-DVo-liaP.mjs → byline-C-YajfV7.mjs} +5 -5
- package/dist/{byline-DVo-liaP.mjs.map → byline-C-YajfV7.mjs.map} +1 -1
- package/dist/{byline-fields-RFPkDLRw.d.mts → byline-fields-Ca56gaT6.d.mts} +6 -6
- package/dist/{byline-fields-RFPkDLRw.d.mts.map → byline-fields-Ca56gaT6.d.mts.map} +1 -1
- package/dist/{bylines-f5ppBpD5.mjs → bylines-BMtJnPnG.mjs} +5 -5
- package/dist/{bylines-f5ppBpD5.mjs.map → bylines-BMtJnPnG.mjs.map} +1 -1
- package/dist/{bylines-pIKgc_TL.mjs → bylines-D4KADDAi.mjs} +4 -4
- package/dist/{bylines-pIKgc_TL.mjs.map → bylines-D4KADDAi.mjs.map} +1 -1
- package/dist/{cache-Q6V4U-Xz.mjs → cache-BUJe2w6Z.mjs} +2 -2
- package/dist/{cache-Q6V4U-Xz.mjs.map → cache-BUJe2w6Z.mjs.map} +1 -1
- package/dist/{chunks-SyjZaYwV.mjs → chunks-BHgcHh2C.mjs} +2 -2
- package/dist/{chunks-SyjZaYwV.mjs.map → chunks-BHgcHh2C.mjs.map} +1 -1
- package/dist/cli/index.mjs +17 -17
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{comment-80UyJJUR.mjs → comment-BDx50EX5.mjs} +2 -2
- package/dist/{comment-80UyJJUR.mjs.map → comment-BDx50EX5.mjs.map} +1 -1
- package/dist/{comment-reaction-Dyb-AutY.mjs → comment-reaction-Gv_pgfI3.mjs} +2 -2
- package/dist/{comment-reaction-Dyb-AutY.mjs.map → comment-reaction-Gv_pgfI3.mjs.map} +1 -1
- package/dist/{comments-Cczx7AvD.mjs → comments-CXHuQ6jI.mjs} +3 -3
- package/dist/{comments-Cczx7AvD.mjs.map → comments-CXHuQ6jI.mjs.map} +1 -1
- package/dist/{content-2PTDNnoh.mjs → content-PoVGKjp0.mjs} +3 -3
- package/dist/{content-2PTDNnoh.mjs.map → content-PoVGKjp0.mjs.map} +1 -1
- package/dist/{context-B1W8soLv.mjs → context-Dvws41_A.mjs} +7 -7
- package/dist/{context-B1W8soLv.mjs.map → context-Dvws41_A.mjs.map} +1 -1
- package/dist/{dashboard-CYjpQZ6-.mjs → dashboard-DsYU8JKS.mjs} +4 -4
- package/dist/{dashboard-CYjpQZ6-.mjs.map → dashboard-DsYU8JKS.mjs.map} +1 -1
- package/dist/db/index.mjs +1 -1
- package/dist/{error-Bl6kZgtt.mjs → error-lq_fL-RS.mjs} +2 -2
- package/dist/{error-Bl6kZgtt.mjs.map → error-lq_fL-RS.mjs.map} +1 -1
- package/dist/{field-defs-cache-ztrdzUGb.mjs → field-defs-cache-CYYx8xlG.mjs} +2 -2
- package/dist/{field-defs-cache-ztrdzUGb.mjs.map → field-defs-cache-CYYx8xlG.mjs.map} +1 -1
- package/dist/{fts-manager-CQuHt-vS.mjs → fts-manager-DW_NR-Fs.mjs} +2 -2
- package/dist/{fts-manager-CQuHt-vS.mjs.map → fts-manager-DW_NR-Fs.mjs.map} +1 -1
- package/dist/{index-BaIj5hIq.d.mts → index-BroXT-vD.d.mts} +98 -22
- package/dist/index-BroXT-vD.d.mts.map +1 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +36 -36
- package/dist/{load-BNo-ER8C.mjs → load-DMFi4n1p.mjs} +2 -2
- package/dist/{load-BNo-ER8C.mjs.map → load-DMFi4n1p.mjs.map} +1 -1
- package/dist/{loader-Dn74CG1b.mjs → loader-BR2SgfEc.mjs} +10 -4
- package/dist/loader-BR2SgfEc.mjs.map +1 -0
- package/dist/media/local-runtime.d.mts +2 -2
- package/dist/media/local-runtime.mjs +4 -4
- package/dist/{media-Bw0sA6rb.mjs → media-CTjJoJTh.mjs} +2 -2
- package/dist/{media-Bw0sA6rb.mjs.map → media-CTjJoJTh.mjs.map} +1 -1
- package/dist/{menus-CYs0WZoL.mjs → menus-8O6xNcYW.mjs} +2 -2
- package/dist/{menus-CYs0WZoL.mjs.map → menus-8O6xNcYW.mjs.map} +1 -1
- package/dist/{menus-DS8VTZgo.mjs → menus-GvRSpgIA.mjs} +5 -5
- package/dist/{menus-DS8VTZgo.mjs.map → menus-GvRSpgIA.mjs.map} +1 -1
- package/dist/{parse-C9hpjUyY.mjs → parse-DHI873qU.mjs} +2 -2
- package/dist/{parse-C9hpjUyY.mjs.map → parse-DHI873qU.mjs.map} +1 -1
- package/dist/plugin-utils.d.mts +2 -2
- package/dist/plugins/adapt-sandbox-entry.d.mts +2 -2
- package/dist/{query-DgUHZLWB.mjs → query-DZdlS6Cu.mjs} +23 -16
- package/dist/query-DZdlS6Cu.mjs.map +1 -0
- package/dist/{rate-limit-BuQ5d4X_.mjs → rate-limit-D1Y6VFVP.mjs} +2 -2
- package/dist/{rate-limit-BuQ5d4X_.mjs.map → rate-limit-D1Y6VFVP.mjs.map} +1 -1
- package/dist/{redirect-Bifv_yHv.mjs → redirect-C1KVGYWe.mjs} +2 -2
- package/dist/{redirect-Bifv_yHv.mjs.map → redirect-C1KVGYWe.mjs.map} +1 -1
- package/dist/{redirects-BZ8Mxn8H.mjs → redirects-DgnypVir.mjs} +3 -3
- package/dist/{redirects-BZ8Mxn8H.mjs.map → redirects-DgnypVir.mjs.map} +1 -1
- package/dist/{registry-CK1SS6pi.mjs → registry-CJkCubOJ.mjs} +4 -4
- package/dist/{registry-CK1SS6pi.mjs.map → registry-CJkCubOJ.mjs.map} +1 -1
- package/dist/{runner-DYYnW530.mjs → runner-BsquExlK.mjs} +246 -174
- package/dist/runner-BsquExlK.mjs.map +1 -0
- package/dist/runner-CEtq-6Sm.d.mts.map +1 -1
- package/dist/runtime.d.mts +2 -2
- package/dist/runtime.mjs +2 -2
- package/dist/{schema-CbFRFoM7.mjs → schema-DMjXuvNU.mjs} +4 -4
- package/dist/{schema-CbFRFoM7.mjs.map → schema-DMjXuvNU.mjs.map} +1 -1
- package/dist/{search-C-waE_YW.mjs → search-D1wKk3lc.mjs} +4 -4
- package/dist/{search-C-waE_YW.mjs.map → search-D1wKk3lc.mjs.map} +1 -1
- package/dist/{sections-osX9U-YJ.mjs → sections-CCMic8dB.mjs} +3 -3
- package/dist/{sections-osX9U-YJ.mjs.map → sections-CCMic8dB.mjs.map} +1 -1
- package/dist/seed/index.mjs +14 -14
- package/dist/{seo-4FovMwNS.mjs → seo-3QrZ1EPT.mjs} +2 -2
- package/dist/{seo-4FovMwNS.mjs.map → seo-3QrZ1EPT.mjs.map} +1 -1
- package/dist/{service-jby1RRmU.mjs → service-CQHK_10l.mjs} +2 -2
- package/dist/{service-jby1RRmU.mjs.map → service-CQHK_10l.mjs.map} +1 -1
- package/dist/{settings-4bRT2NIf.mjs → settings-C3Yi5Co3.mjs} +3 -3
- package/dist/{settings-4bRT2NIf.mjs.map → settings-C3Yi5Co3.mjs.map} +1 -1
- package/dist/{settings-t-H2jPuJ.mjs → settings-D9rvaLuY.mjs} +3 -3
- package/dist/{settings-t-H2jPuJ.mjs.map → settings-D9rvaLuY.mjs.map} +1 -1
- package/dist/{taxonomies-Dxrc_szG.mjs → taxonomies-CurpLtNC.mjs} +29 -21
- package/dist/taxonomies-CurpLtNC.mjs.map +1 -0
- package/dist/{taxonomies-BBB8O-CU.mjs → taxonomies-DhLaNJji.mjs} +34 -27
- package/dist/taxonomies-DhLaNJji.mjs.map +1 -0
- package/dist/{taxonomy-B2HQuVf5.mjs → taxonomy-BBUlGug5.mjs} +33 -7
- package/dist/taxonomy-BBUlGug5.mjs.map +1 -0
- package/dist/{types-DXOIbece.mjs → types-CmI1OyQg.mjs} +2 -2
- package/dist/{types-DXOIbece.mjs.map → types-CmI1OyQg.mjs.map} +1 -1
- package/dist/{user-Db-Rti_z.mjs → user-JAuc8VUf.mjs} +3 -3
- package/dist/{user-Db-Rti_z.mjs.map → user-JAuc8VUf.mjs.map} +1 -1
- package/dist/{validate-0Utjspbs.mjs → validate-BzXHWnkV.mjs} +2 -2
- package/dist/{validate-0Utjspbs.mjs.map → validate-BzXHWnkV.mjs.map} +1 -1
- package/dist/{validation-D9EnzH1T.mjs → validation-CY1gk4Ar.mjs} +5 -5
- package/dist/{validation-D9EnzH1T.mjs.map → validation-CY1gk4Ar.mjs.map} +1 -1
- package/dist/version-C0JPFUG8.mjs +7 -0
- package/dist/{version-BgVM_1MU.mjs.map → version-C0JPFUG8.mjs.map} +1 -1
- package/dist/{widgets-BSTzPUYa.mjs → widgets-Dri0spI4.mjs} +2 -2
- package/dist/{widgets-BSTzPUYa.mjs.map → widgets-Dri0spI4.mjs.map} +1 -1
- package/dist/{zod-generator-ElVQOav7.mjs → zod-generator-xEHODMJf.mjs} +2 -2
- package/dist/{zod-generator-ElVQOav7.mjs.map → zod-generator-xEHODMJf.mjs.map} +1 -1
- package/package.json +6 -6
- package/src/api/handlers/taxonomies.ts +62 -32
- package/src/astro/integration/shims/use-sync-external-store-with-selector.js +71 -0
- package/src/astro/integration/shims/use-sync-external-store.js +1 -0
- package/src/astro/integration/vite-config.ts +47 -8
- package/src/astro/middleware.ts +13 -3
- package/src/cli/commands/export-seed.ts +5 -4
- package/src/database/migrations/045_taxonomy_parent_group.ts +68 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/taxonomy.ts +50 -7
- package/src/loader.ts +64 -8
- package/src/query.ts +93 -18
- package/src/taxonomies/index.ts +20 -8
- package/dist/index-BaIj5hIq.d.mts.map +0 -1
- package/dist/loader-Dn74CG1b.mjs.map +0 -1
- package/dist/query-DgUHZLWB.mjs.map +0 -1
- package/dist/runner-DYYnW530.mjs.map +0 -1
- package/dist/taxonomies-BBB8O-CU.mjs.map +0 -1
- package/dist/taxonomies-Dxrc_szG.mjs.map +0 -1
- package/dist/taxonomy-B2HQuVf5.mjs.map +0 -1
- package/dist/version-BgVM_1MU.mjs +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zod-generator-ElVQOav7.mjs","names":[],"sources":["../src/schema/zod-generator.ts"],"sourcesContent":["import { z, type ZodTypeAny } from \"zod\";\n\nimport { hashString } from \"../utils/hash.js\";\nimport type { Field, FieldType, CollectionWithFields } from \"./types.js\";\n\n/** Pattern to split on underscores, hyphens, and spaces for PascalCase conversion */\nconst PASCAL_CASE_SPLIT_PATTERN = /[_\\-\\s]+/;\n\n/**\n * Generate a Zod schema from a collection's field definitions\n *\n * This allows runtime validation of content based on dynamically\n * defined schemas stored in D1.\n */\nexport function generateZodSchema(\n\tcollection: CollectionWithFields,\n): z.ZodObject<Record<string, ZodTypeAny>> {\n\tconst shape: Record<string, ZodTypeAny> = {};\n\n\tfor (const field of collection.fields) {\n\t\tshape[field.slug] = generateFieldSchema(field);\n\t}\n\n\treturn z.object(shape);\n}\n\n/**\n * Generate Zod schema for a single field\n */\nexport function generateFieldSchema(field: Field): ZodTypeAny {\n\tlet schema = getBaseSchema(field.type, field);\n\n\t// Apply validation rules\n\tif (field.validation) {\n\t\tschema = applyValidation(schema, field);\n\t}\n\n\t// Apply required/optional. Non-required fields use `.nullish()` rather\n\t// than `.optional()` because the underlying SQLite columns are nullable\n\t// (see `SchemaRegistry.addFieldColumn` -- non-required fields are added\n\t// without `NOT NULL`). The admin re-sends what it loaded from the\n\t// server on autosave, so any field that's actually `null` in the DB\n\t// must round-trip cleanly through the validator. `.optional()` only\n\t// accepts `undefined`; `.nullish()` accepts both `undefined` and\n\t// `null`. (#867 — autosave failures on seeded entries.)\n\tif (!field.required) {\n\t\tschema = schema.nullish();\n\t}\n\n\t// Apply default value\n\tif (field.defaultValue !== undefined) {\n\t\tschema = schema.default(field.defaultValue);\n\t}\n\n\treturn schema;\n}\n\n/**\n * Get base Zod schema for a field type\n */\nfunction getBaseSchema(type: FieldType, field: Field): ZodTypeAny {\n\tswitch (type) {\n\t\tcase \"url\":\n\t\t\treturn z.string().url();\n\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\t\treturn z.string();\n\n\t\tcase \"number\":\n\t\t\treturn z.number();\n\n\t\tcase \"integer\":\n\t\t\treturn z.number().int();\n\n\t\tcase \"boolean\":\n\t\t\t// Boolean fields map to `INTEGER` columns (`FIELD_TYPE_TO_COLUMN`\n\t\t\t// in `schema/types.ts`) and `serializeValue` in\n\t\t\t// `database/repositories/content.ts` writes booleans as 0/1.\n\t\t\t// `deserializeValue` never converts them back, so reads return\n\t\t\t// numbers. Coerce the stored 0/1 shape here so a GET → POST\n\t\t\t// round-trip on a boolean field passes validation. Other inputs\n\t\t\t// (strings, other numbers) fall through to `z.boolean()` and\n\t\t\t// produce its standard rejection.\n\t\t\treturn z.preprocess((v) => (v === 0 || v === 1 ? Boolean(v) : v), z.boolean());\n\n\t\tcase \"datetime\":\n\t\t\treturn z.string().datetime().or(z.string().date());\n\n\t\tcase \"select\": {\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\tconst [first, ...rest] = options;\n\t\t\t\treturn z.enum([first, ...rest]);\n\t\t\t}\n\t\t\treturn z.string();\n\t\t}\n\n\t\tcase \"multiSelect\": {\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\tconst [first, ...rest] = multiOptions;\n\t\t\t\treturn z.array(z.enum([first, ...rest]));\n\t\t\t}\n\t\t\treturn z.array(z.string());\n\t\t}\n\n\t\tcase \"portableText\":\n\t\t\t// Portable Text is an array of blocks. We require `_type` because\n\t\t\t// renderers dispatch on it, but `_key` is intentionally optional:\n\t\t\t// it's a UI-layer concern that the editor regenerates on every\n\t\t\t// change (see `PortableTextEditor`), and the rest of this schema\n\t\t\t// uses `.passthrough()` for everything below the top level. Making\n\t\t\t// `_key` strictly required here was an accidentally tight invariant\n\t\t\t// that rejected any seed/import data not authored against the\n\t\t\t// editor (#867 — autosave failures on seeded template content).\n\t\t\treturn z.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\t_type: z.string(),\n\t\t\t\t\t\t_key: z.string().optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.passthrough(),\n\t\t\t);\n\n\t\tcase \"image\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\talt: z.string().optional(),\n\t\t\t\twidth: z.number().optional(),\n\t\t\t\theight: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"cloudflare-images\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Admin-side preview URL for external providers (not persisted by plugins) */\n\t\t\t\tpreviewUrl: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"file\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\tfilename: z.string().optional(),\n\t\t\t\tmimeType: z.string().optional(),\n\t\t\t\tsize: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"s3\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"reference\":\n\t\t\treturn z.string(); // Reference ID\n\n\t\tcase \"json\":\n\t\t\treturn z.unknown();\n\n\t\tdefault:\n\t\t\treturn z.unknown();\n\t}\n}\n\n/**\n * Apply validation rules to a schema\n */\nfunction applyValidation(schema: ZodTypeAny, field: Field): ZodTypeAny {\n\tconst validation = field.validation;\n\tif (!validation) return schema;\n\n\t// String validations\n\tif (schema instanceof z.ZodString) {\n\t\tlet strSchema = schema;\n\t\tif (validation.minLength !== undefined) {\n\t\t\tstrSchema = strSchema.min(validation.minLength);\n\t\t}\n\t\tif (validation.maxLength !== undefined) {\n\t\t\tstrSchema = strSchema.max(validation.maxLength);\n\t\t}\n\t\tif (validation.pattern) {\n\t\t\tstrSchema = strSchema.regex(new RegExp(validation.pattern));\n\t\t}\n\t\treturn strSchema;\n\t}\n\n\t// Number validations\n\tif (schema instanceof z.ZodNumber) {\n\t\tlet numSchema = schema;\n\t\tif (validation.min !== undefined) {\n\t\t\tnumSchema = numSchema.min(validation.min);\n\t\t}\n\t\tif (validation.max !== undefined) {\n\t\t\tnumSchema = numSchema.max(validation.max);\n\t\t}\n\t\treturn numSchema;\n\t}\n\n\treturn schema;\n}\n\n/**\n * Schema cache to avoid regenerating schemas on every request\n */\nconst schemaCache = new Map<string, { schema: z.ZodObject<any>; version: string }>();\n\n/**\n * Get or generate a cached schema for a collection\n */\nexport function getCachedSchema(\n\tcollection: CollectionWithFields,\n\tversion?: string,\n): z.ZodObject<any> {\n\tconst cacheKey = collection.slug;\n\tconst cached = schemaCache.get(cacheKey);\n\n\t// If version matches, return cached schema\n\tif (cached && (!version || cached.version === version)) {\n\t\treturn cached.schema;\n\t}\n\n\t// Generate new schema\n\tconst schema = generateZodSchema(collection);\n\n\t// Cache it\n\tschemaCache.set(cacheKey, {\n\t\tschema,\n\t\tversion: version || collection.updatedAt,\n\t});\n\n\treturn schema;\n}\n\n/**\n * Invalidate cached schema for a collection\n */\nexport function invalidateSchemaCache(slug: string): void {\n\tschemaCache.delete(slug);\n}\n\n/**\n * Clear all cached schemas\n */\nexport function clearSchemaCache(): void {\n\tschemaCache.clear();\n}\n\n/**\n * Validate data against a collection's schema\n */\nexport function validateContent(\n\tcollection: CollectionWithFields,\n\tdata: unknown,\n): { success: true; data: unknown } | { success: false; errors: z.ZodError } {\n\tconst schema = getCachedSchema(collection);\n\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Generate TypeScript interface from field definitions\n * Used by CLI `emdash types` to generate types\n */\nexport function generateTypeScript(\n\tcollection: CollectionWithFields,\n\tinterfaceName: string = getInterfaceName(collection),\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(`export interface ${interfaceName} {`);\n\tlines.push(` id: string;`);\n\tlines.push(` slug: string | null;`);\n\tlines.push(` status: string;`);\n\n\tfor (const field of collection.fields) {\n\t\tconst tsType = fieldTypeToTypeScript(field);\n\t\tconst optional = field.required ? \"\" : \"?\";\n\t\tlines.push(` ${field.slug}${optional}: ${tsType};`);\n\t}\n\n\tlines.push(` createdAt: Date;`);\n\tlines.push(` updatedAt: Date;`);\n\tlines.push(` publishedAt: Date | null;`);\n\t// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry\n\tlines.push(` bylines?: ContentBylineCredit[];`);\n\t// Taxonomy terms are eagerly loaded by getEmDashCollection/getEmDashEntry,\n\t// keyed by taxonomy name (e.g. data.terms?.tag)\n\tlines.push(` terms?: Record<string, TaxonomyTerm[]>;`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete types file with module augmentation\n * This produces emdash-env.d.ts content that provides typed query functions\n */\nexport function generateTypesFile(collections: CollectionWithFields[]): string {\n\tconst lines: string[] = [];\n\n\t// Header\n\tlines.push(`// Generated by EmDash on dev server start`);\n\tlines.push(`// Do not edit manually`);\n\tlines.push(``);\n\tlines.push(`/// <reference types=\"emdash/locals\" />`);\n\tlines.push(``);\n\n\t// Check if we need PortableTextBlock import\n\tconst needsPortableText = collections.some((c) =>\n\t\tc.fields.some((f) => f.type === \"portableText\"),\n\t);\n\n\t// Build imports - ContentBylineCredit and TaxonomyTerm are always needed\n\t// for the hydrated bylines/terms fields\n\tconst imports = [\"ContentBylineCredit\", \"TaxonomyTerm\"];\n\tif (needsPortableText) {\n\t\timports.push(\"PortableTextBlock\");\n\t}\n\tlines.push(`import type { ${imports.join(\", \")} } from \"emdash\";`);\n\tlines.push(``);\n\n\t// Singularizing the slug can map two distinct slugs to the same name\n\t// (e.g. `book` and `books` both -> `Book`), so resolve collisions up front\n\t// to keep every interface identifier unique within the file.\n\tconst interfaceNames = uniqueInterfaceNames(collections);\n\n\t// Generate individual interfaces\n\tfor (const collection of collections) {\n\t\tlines.push(generateTypeScript(collection, interfaceNames.get(collection.slug)));\n\t\tlines.push(``);\n\t}\n\n\t// Generate the Collections interface for module augmentation\n\tlines.push(`declare module \"emdash\" {`);\n\tlines.push(` interface EmDashCollections {`);\n\tfor (const collection of collections) {\n\t\tlines.push(` ${collection.slug}: ${interfaceNames.get(collection.slug)};`);\n\t}\n\tlines.push(` }`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate schema hash for cache invalidation\n */\nexport async function generateSchemaHash(collections: CollectionWithFields[]): Promise<string> {\n\tconst str = JSON.stringify(\n\t\tcollections.map((c) => ({\n\t\t\tslug: c.slug,\n\t\t\tfields: c.fields.map((f) => ({\n\t\t\t\tslug: f.slug,\n\t\t\t\ttype: f.type,\n\t\t\t\trequired: f.required,\n\t\t\t\tvalidation: f.validation,\n\t\t\t})),\n\t\t})),\n\t);\n\treturn hashString(str);\n}\n\n/**\n * Map field type to TypeScript type\n */\nfunction fieldTypeToTypeScript(field: Field): string {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\tcase \"url\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\treturn \"number\";\n\n\t\tcase \"boolean\":\n\t\t\treturn \"boolean\";\n\n\t\tcase \"select\":\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\treturn options.map((o) => `\"${o}\"`).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\n\t\tcase \"multiSelect\":\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\treturn `(${multiOptions.map((o) => `\"${o}\"`).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\n\t\tcase \"portableText\":\n\t\t\treturn \"PortableTextBlock[]\";\n\n\t\tcase \"image\":\n\t\t\treturn \"{ id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"file\":\n\t\t\treturn \"{ id: string; src?: string; filename?: string; mimeType?: string; size?: number; provider?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"reference\":\n\t\t\t// Could be enhanced to include the referenced collection type\n\t\t\treturn \"string\";\n\n\t\tcase \"json\":\n\t\t\treturn \"unknown\";\n\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Convert string to PascalCase (handles slugs, spaces, etc.)\n */\nfunction pascalCase(str: string): string {\n\treturn str\n\t\t.split(PASCAL_CASE_SPLIT_PATTERN)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\n/**\n * Naive singularization for slug-derived interface names. Handles the common\n * English plural endings; intentionally simple, not a full inflector.\n */\nfunction singularize(str: string): string {\n\tif (str.endsWith(\"ies\")) {\n\t\treturn str.slice(0, -3) + \"y\";\n\t}\n\tif (\n\t\tstr.endsWith(\"es\") &&\n\t\t(str.endsWith(\"sses\") || str.endsWith(\"xes\") || str.endsWith(\"ches\") || str.endsWith(\"shes\"))\n\t) {\n\t\treturn str.slice(0, -2);\n\t}\n\tif (str.endsWith(\"s\") && !str.endsWith(\"ss\")) {\n\t\treturn str.slice(0, -1);\n\t}\n\treturn str;\n}\n\n/**\n * Get the interface name for a collection.\n *\n * Derived from the slug, not the human label. Slugs are constrained to\n * `/^[a-z][a-z0-9_]*$/`, so PascalCasing one always yields a valid TS\n * identifier; labels are arbitrary and user-controlled (punctuation, spaces,\n * duplicates across collections), which produced syntactically invalid or\n * duplicate interface names. The slug is singularized first because the\n * interface describes a single entry, not the collection (`posts` -> `Post`).\n *\n * Singularization can map two distinct slugs onto the same name, so callers\n * generating more than one interface must dedupe -- see `uniqueInterfaceNames`.\n */\nfunction getInterfaceName(collection: CollectionWithFields): string {\n\treturn pascalCase(singularize(collection.slug));\n}\n\n/**\n * Resolve interface names for a set of collections, guaranteeing each is\n * unique within the file. Collisions (from singularization or PascalCasing\n * collapsing distinct slugs) get a numeric suffix in collection order, so the\n * generated `.d.ts` never declares two interfaces with the same identifier.\n *\n * The suffix is chosen against the set of names already emitted, not a\n * per-base counter, so a generated name can't collide with another slug's\n * base name (e.g. slugs `book`, `books`, `book2`: `books` -> `Book2` would\n * clash with `book2`, so it advances to `Book3`).\n */\nexport function uniqueInterfaceNames(collections: CollectionWithFields[]): Map<string, string> {\n\tconst used = new Set<string>();\n\tconst names = new Map<string, string>();\n\tfor (const collection of collections) {\n\t\tconst base = getInterfaceName(collection);\n\t\tlet name = base;\n\t\tlet suffix = 2;\n\t\twhile (used.has(name)) {\n\t\t\tname = `${base}${suffix}`;\n\t\t\tsuffix++;\n\t\t}\n\t\tused.add(name);\n\t\tnames.set(collection.slug, name);\n\t}\n\treturn names;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"zod-generator-xEHODMJf.mjs","names":[],"sources":["../src/schema/zod-generator.ts"],"sourcesContent":["import { z, type ZodTypeAny } from \"zod\";\n\nimport { hashString } from \"../utils/hash.js\";\nimport type { Field, FieldType, CollectionWithFields } from \"./types.js\";\n\n/** Pattern to split on underscores, hyphens, and spaces for PascalCase conversion */\nconst PASCAL_CASE_SPLIT_PATTERN = /[_\\-\\s]+/;\n\n/**\n * Generate a Zod schema from a collection's field definitions\n *\n * This allows runtime validation of content based on dynamically\n * defined schemas stored in D1.\n */\nexport function generateZodSchema(\n\tcollection: CollectionWithFields,\n): z.ZodObject<Record<string, ZodTypeAny>> {\n\tconst shape: Record<string, ZodTypeAny> = {};\n\n\tfor (const field of collection.fields) {\n\t\tshape[field.slug] = generateFieldSchema(field);\n\t}\n\n\treturn z.object(shape);\n}\n\n/**\n * Generate Zod schema for a single field\n */\nexport function generateFieldSchema(field: Field): ZodTypeAny {\n\tlet schema = getBaseSchema(field.type, field);\n\n\t// Apply validation rules\n\tif (field.validation) {\n\t\tschema = applyValidation(schema, field);\n\t}\n\n\t// Apply required/optional. Non-required fields use `.nullish()` rather\n\t// than `.optional()` because the underlying SQLite columns are nullable\n\t// (see `SchemaRegistry.addFieldColumn` -- non-required fields are added\n\t// without `NOT NULL`). The admin re-sends what it loaded from the\n\t// server on autosave, so any field that's actually `null` in the DB\n\t// must round-trip cleanly through the validator. `.optional()` only\n\t// accepts `undefined`; `.nullish()` accepts both `undefined` and\n\t// `null`. (#867 — autosave failures on seeded entries.)\n\tif (!field.required) {\n\t\tschema = schema.nullish();\n\t}\n\n\t// Apply default value\n\tif (field.defaultValue !== undefined) {\n\t\tschema = schema.default(field.defaultValue);\n\t}\n\n\treturn schema;\n}\n\n/**\n * Get base Zod schema for a field type\n */\nfunction getBaseSchema(type: FieldType, field: Field): ZodTypeAny {\n\tswitch (type) {\n\t\tcase \"url\":\n\t\t\treturn z.string().url();\n\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\t\treturn z.string();\n\n\t\tcase \"number\":\n\t\t\treturn z.number();\n\n\t\tcase \"integer\":\n\t\t\treturn z.number().int();\n\n\t\tcase \"boolean\":\n\t\t\t// Boolean fields map to `INTEGER` columns (`FIELD_TYPE_TO_COLUMN`\n\t\t\t// in `schema/types.ts`) and `serializeValue` in\n\t\t\t// `database/repositories/content.ts` writes booleans as 0/1.\n\t\t\t// `deserializeValue` never converts them back, so reads return\n\t\t\t// numbers. Coerce the stored 0/1 shape here so a GET → POST\n\t\t\t// round-trip on a boolean field passes validation. Other inputs\n\t\t\t// (strings, other numbers) fall through to `z.boolean()` and\n\t\t\t// produce its standard rejection.\n\t\t\treturn z.preprocess((v) => (v === 0 || v === 1 ? Boolean(v) : v), z.boolean());\n\n\t\tcase \"datetime\":\n\t\t\treturn z.string().datetime().or(z.string().date());\n\n\t\tcase \"select\": {\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\tconst [first, ...rest] = options;\n\t\t\t\treturn z.enum([first, ...rest]);\n\t\t\t}\n\t\t\treturn z.string();\n\t\t}\n\n\t\tcase \"multiSelect\": {\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\tconst [first, ...rest] = multiOptions;\n\t\t\t\treturn z.array(z.enum([first, ...rest]));\n\t\t\t}\n\t\t\treturn z.array(z.string());\n\t\t}\n\n\t\tcase \"portableText\":\n\t\t\t// Portable Text is an array of blocks. We require `_type` because\n\t\t\t// renderers dispatch on it, but `_key` is intentionally optional:\n\t\t\t// it's a UI-layer concern that the editor regenerates on every\n\t\t\t// change (see `PortableTextEditor`), and the rest of this schema\n\t\t\t// uses `.passthrough()` for everything below the top level. Making\n\t\t\t// `_key` strictly required here was an accidentally tight invariant\n\t\t\t// that rejected any seed/import data not authored against the\n\t\t\t// editor (#867 — autosave failures on seeded template content).\n\t\t\treturn z.array(\n\t\t\t\tz\n\t\t\t\t\t.object({\n\t\t\t\t\t\t_type: z.string(),\n\t\t\t\t\t\t_key: z.string().optional(),\n\t\t\t\t\t})\n\t\t\t\t\t.passthrough(),\n\t\t\t);\n\n\t\tcase \"image\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\talt: z.string().optional(),\n\t\t\t\twidth: z.number().optional(),\n\t\t\t\theight: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"cloudflare-images\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Admin-side preview URL for external providers (not persisted by plugins) */\n\t\t\t\tpreviewUrl: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"file\":\n\t\t\treturn z.object({\n\t\t\t\tid: z.string(),\n\t\t\t\tsrc: z.string().optional(),\n\t\t\t\tfilename: z.string().optional(),\n\t\t\t\tmimeType: z.string().optional(),\n\t\t\t\tsize: z.number().optional(),\n\t\t\t\t/** Provider ID (e.g. \"local\", \"s3\") */\n\t\t\t\tprovider: z.string().optional(),\n\t\t\t\t/** Provider-specific metadata; for local media this carries storageKey */\n\t\t\t\tmeta: z.record(z.string(), z.unknown()).optional(),\n\t\t\t});\n\n\t\tcase \"reference\":\n\t\t\treturn z.string(); // Reference ID\n\n\t\tcase \"json\":\n\t\t\treturn z.unknown();\n\n\t\tdefault:\n\t\t\treturn z.unknown();\n\t}\n}\n\n/**\n * Apply validation rules to a schema\n */\nfunction applyValidation(schema: ZodTypeAny, field: Field): ZodTypeAny {\n\tconst validation = field.validation;\n\tif (!validation) return schema;\n\n\t// String validations\n\tif (schema instanceof z.ZodString) {\n\t\tlet strSchema = schema;\n\t\tif (validation.minLength !== undefined) {\n\t\t\tstrSchema = strSchema.min(validation.minLength);\n\t\t}\n\t\tif (validation.maxLength !== undefined) {\n\t\t\tstrSchema = strSchema.max(validation.maxLength);\n\t\t}\n\t\tif (validation.pattern) {\n\t\t\tstrSchema = strSchema.regex(new RegExp(validation.pattern));\n\t\t}\n\t\treturn strSchema;\n\t}\n\n\t// Number validations\n\tif (schema instanceof z.ZodNumber) {\n\t\tlet numSchema = schema;\n\t\tif (validation.min !== undefined) {\n\t\t\tnumSchema = numSchema.min(validation.min);\n\t\t}\n\t\tif (validation.max !== undefined) {\n\t\t\tnumSchema = numSchema.max(validation.max);\n\t\t}\n\t\treturn numSchema;\n\t}\n\n\treturn schema;\n}\n\n/**\n * Schema cache to avoid regenerating schemas on every request\n */\nconst schemaCache = new Map<string, { schema: z.ZodObject<any>; version: string }>();\n\n/**\n * Get or generate a cached schema for a collection\n */\nexport function getCachedSchema(\n\tcollection: CollectionWithFields,\n\tversion?: string,\n): z.ZodObject<any> {\n\tconst cacheKey = collection.slug;\n\tconst cached = schemaCache.get(cacheKey);\n\n\t// If version matches, return cached schema\n\tif (cached && (!version || cached.version === version)) {\n\t\treturn cached.schema;\n\t}\n\n\t// Generate new schema\n\tconst schema = generateZodSchema(collection);\n\n\t// Cache it\n\tschemaCache.set(cacheKey, {\n\t\tschema,\n\t\tversion: version || collection.updatedAt,\n\t});\n\n\treturn schema;\n}\n\n/**\n * Invalidate cached schema for a collection\n */\nexport function invalidateSchemaCache(slug: string): void {\n\tschemaCache.delete(slug);\n}\n\n/**\n * Clear all cached schemas\n */\nexport function clearSchemaCache(): void {\n\tschemaCache.clear();\n}\n\n/**\n * Validate data against a collection's schema\n */\nexport function validateContent(\n\tcollection: CollectionWithFields,\n\tdata: unknown,\n): { success: true; data: unknown } | { success: false; errors: z.ZodError } {\n\tconst schema = getCachedSchema(collection);\n\n\tconst result = schema.safeParse(data);\n\n\tif (result.success) {\n\t\treturn { success: true, data: result.data };\n\t}\n\n\treturn { success: false, errors: result.error };\n}\n\n/**\n * Generate TypeScript interface from field definitions\n * Used by CLI `emdash types` to generate types\n */\nexport function generateTypeScript(\n\tcollection: CollectionWithFields,\n\tinterfaceName: string = getInterfaceName(collection),\n): string {\n\tconst lines: string[] = [];\n\n\tlines.push(`export interface ${interfaceName} {`);\n\tlines.push(` id: string;`);\n\tlines.push(` slug: string | null;`);\n\tlines.push(` status: string;`);\n\n\tfor (const field of collection.fields) {\n\t\tconst tsType = fieldTypeToTypeScript(field);\n\t\tconst optional = field.required ? \"\" : \"?\";\n\t\tlines.push(` ${field.slug}${optional}: ${tsType};`);\n\t}\n\n\tlines.push(` createdAt: Date;`);\n\tlines.push(` updatedAt: Date;`);\n\tlines.push(` publishedAt: Date | null;`);\n\t// Bylines are eagerly loaded by getEmDashCollection/getEmDashEntry\n\tlines.push(` bylines?: ContentBylineCredit[];`);\n\t// Taxonomy terms are eagerly loaded by getEmDashCollection/getEmDashEntry,\n\t// keyed by taxonomy name (e.g. data.terms?.tag)\n\tlines.push(` terms?: Record<string, TaxonomyTerm[]>;`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate a complete types file with module augmentation\n * This produces emdash-env.d.ts content that provides typed query functions\n */\nexport function generateTypesFile(collections: CollectionWithFields[]): string {\n\tconst lines: string[] = [];\n\n\t// Header\n\tlines.push(`// Generated by EmDash on dev server start`);\n\tlines.push(`// Do not edit manually`);\n\tlines.push(``);\n\tlines.push(`/// <reference types=\"emdash/locals\" />`);\n\tlines.push(``);\n\n\t// Check if we need PortableTextBlock import\n\tconst needsPortableText = collections.some((c) =>\n\t\tc.fields.some((f) => f.type === \"portableText\"),\n\t);\n\n\t// Build imports - ContentBylineCredit and TaxonomyTerm are always needed\n\t// for the hydrated bylines/terms fields\n\tconst imports = [\"ContentBylineCredit\", \"TaxonomyTerm\"];\n\tif (needsPortableText) {\n\t\timports.push(\"PortableTextBlock\");\n\t}\n\tlines.push(`import type { ${imports.join(\", \")} } from \"emdash\";`);\n\tlines.push(``);\n\n\t// Singularizing the slug can map two distinct slugs to the same name\n\t// (e.g. `book` and `books` both -> `Book`), so resolve collisions up front\n\t// to keep every interface identifier unique within the file.\n\tconst interfaceNames = uniqueInterfaceNames(collections);\n\n\t// Generate individual interfaces\n\tfor (const collection of collections) {\n\t\tlines.push(generateTypeScript(collection, interfaceNames.get(collection.slug)));\n\t\tlines.push(``);\n\t}\n\n\t// Generate the Collections interface for module augmentation\n\tlines.push(`declare module \"emdash\" {`);\n\tlines.push(` interface EmDashCollections {`);\n\tfor (const collection of collections) {\n\t\tlines.push(` ${collection.slug}: ${interfaceNames.get(collection.slug)};`);\n\t}\n\tlines.push(` }`);\n\tlines.push(`}`);\n\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Generate schema hash for cache invalidation\n */\nexport async function generateSchemaHash(collections: CollectionWithFields[]): Promise<string> {\n\tconst str = JSON.stringify(\n\t\tcollections.map((c) => ({\n\t\t\tslug: c.slug,\n\t\t\tfields: c.fields.map((f) => ({\n\t\t\t\tslug: f.slug,\n\t\t\t\ttype: f.type,\n\t\t\t\trequired: f.required,\n\t\t\t\tvalidation: f.validation,\n\t\t\t})),\n\t\t})),\n\t);\n\treturn hashString(str);\n}\n\n/**\n * Map field type to TypeScript type\n */\nfunction fieldTypeToTypeScript(field: Field): string {\n\tswitch (field.type) {\n\t\tcase \"string\":\n\t\tcase \"text\":\n\t\tcase \"slug\":\n\t\tcase \"url\":\n\t\tcase \"datetime\":\n\t\t\treturn \"string\";\n\n\t\tcase \"number\":\n\t\tcase \"integer\":\n\t\t\treturn \"number\";\n\n\t\tcase \"boolean\":\n\t\t\treturn \"boolean\";\n\n\t\tcase \"select\":\n\t\t\tconst options = field.validation?.options;\n\t\t\tif (options && options.length > 0) {\n\t\t\t\treturn options.map((o) => `\"${o}\"`).join(\" | \");\n\t\t\t}\n\t\t\treturn \"string\";\n\n\t\tcase \"multiSelect\":\n\t\t\tconst multiOptions = field.validation?.options;\n\t\t\tif (multiOptions && multiOptions.length > 0) {\n\t\t\t\treturn `(${multiOptions.map((o) => `\"${o}\"`).join(\" | \")})[]`;\n\t\t\t}\n\t\t\treturn \"string[]\";\n\n\t\tcase \"portableText\":\n\t\t\treturn \"PortableTextBlock[]\";\n\n\t\tcase \"image\":\n\t\t\treturn \"{ id: string; src?: string; alt?: string; width?: number; height?: number; provider?: string; previewUrl?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"file\":\n\t\t\treturn \"{ id: string; src?: string; filename?: string; mimeType?: string; size?: number; provider?: string; meta?: Record<string, unknown> }\";\n\n\t\tcase \"reference\":\n\t\t\t// Could be enhanced to include the referenced collection type\n\t\t\treturn \"string\";\n\n\t\tcase \"json\":\n\t\t\treturn \"unknown\";\n\n\t\tdefault:\n\t\t\treturn \"unknown\";\n\t}\n}\n\n/**\n * Convert string to PascalCase (handles slugs, spaces, etc.)\n */\nfunction pascalCase(str: string): string {\n\treturn str\n\t\t.split(PASCAL_CASE_SPLIT_PATTERN)\n\t\t.filter(Boolean)\n\t\t.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\n\t\t.join(\"\");\n}\n\n/**\n * Naive singularization for slug-derived interface names. Handles the common\n * English plural endings; intentionally simple, not a full inflector.\n */\nfunction singularize(str: string): string {\n\tif (str.endsWith(\"ies\")) {\n\t\treturn str.slice(0, -3) + \"y\";\n\t}\n\tif (\n\t\tstr.endsWith(\"es\") &&\n\t\t(str.endsWith(\"sses\") || str.endsWith(\"xes\") || str.endsWith(\"ches\") || str.endsWith(\"shes\"))\n\t) {\n\t\treturn str.slice(0, -2);\n\t}\n\tif (str.endsWith(\"s\") && !str.endsWith(\"ss\")) {\n\t\treturn str.slice(0, -1);\n\t}\n\treturn str;\n}\n\n/**\n * Get the interface name for a collection.\n *\n * Derived from the slug, not the human label. Slugs are constrained to\n * `/^[a-z][a-z0-9_]*$/`, so PascalCasing one always yields a valid TS\n * identifier; labels are arbitrary and user-controlled (punctuation, spaces,\n * duplicates across collections), which produced syntactically invalid or\n * duplicate interface names. The slug is singularized first because the\n * interface describes a single entry, not the collection (`posts` -> `Post`).\n *\n * Singularization can map two distinct slugs onto the same name, so callers\n * generating more than one interface must dedupe -- see `uniqueInterfaceNames`.\n */\nfunction getInterfaceName(collection: CollectionWithFields): string {\n\treturn pascalCase(singularize(collection.slug));\n}\n\n/**\n * Resolve interface names for a set of collections, guaranteeing each is\n * unique within the file. Collisions (from singularization or PascalCasing\n * collapsing distinct slugs) get a numeric suffix in collection order, so the\n * generated `.d.ts` never declares two interfaces with the same identifier.\n *\n * The suffix is chosen against the set of names already emitted, not a\n * per-base counter, so a generated name can't collide with another slug's\n * base name (e.g. slugs `book`, `books`, `book2`: `books` -> `Book2` would\n * clash with `book2`, so it advances to `Book3`).\n */\nexport function uniqueInterfaceNames(collections: CollectionWithFields[]): Map<string, string> {\n\tconst used = new Set<string>();\n\tconst names = new Map<string, string>();\n\tfor (const collection of collections) {\n\t\tconst base = getInterfaceName(collection);\n\t\tlet name = base;\n\t\tlet suffix = 2;\n\t\twhile (used.has(name)) {\n\t\t\tname = `${base}${suffix}`;\n\t\t\tsuffix++;\n\t\t}\n\t\tused.add(name);\n\t\tnames.set(collection.slug, name);\n\t}\n\treturn names;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emdash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Astro-native CMS with WordPress migration support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.mjs",
|
|
@@ -230,11 +230,11 @@
|
|
|
230
230
|
"upng-js": "^2.1.0",
|
|
231
231
|
"zod": "^4.4.1",
|
|
232
232
|
"@atcute/client": "^5.0.0",
|
|
233
|
-
"@emdash-cms/
|
|
234
|
-
"@emdash-cms/auth": "0.
|
|
235
|
-
"@emdash-cms/gutenberg-to-portable-text": "0.23.0",
|
|
233
|
+
"@emdash-cms/gutenberg-to-portable-text": "0.24.1",
|
|
234
|
+
"@emdash-cms/auth": "0.24.1",
|
|
236
235
|
"@emdash-cms/plugin-types": "0.1.0",
|
|
237
|
-
"@emdash-cms/registry-client": "0.3.2"
|
|
236
|
+
"@emdash-cms/registry-client": "0.3.2",
|
|
237
|
+
"@emdash-cms/admin": "0.24.1"
|
|
238
238
|
},
|
|
239
239
|
"optionalDependencies": {
|
|
240
240
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
@@ -267,7 +267,7 @@
|
|
|
267
267
|
"vite": "^6.0.0",
|
|
268
268
|
"vitest": "^4.1.5",
|
|
269
269
|
"zod-openapi": "^5.4.6",
|
|
270
|
-
"@emdash-cms/blocks": "0.
|
|
270
|
+
"@emdash-cms/blocks": "0.24.1"
|
|
271
271
|
},
|
|
272
272
|
"repository": {
|
|
273
273
|
"type": "git",
|
|
@@ -95,12 +95,21 @@ export interface TaxonomyDefTranslationsResponse {
|
|
|
95
95
|
* Build tree structure from flat terms
|
|
96
96
|
*/
|
|
97
97
|
function buildTree(flatTerms: TermWithCount[]): TermWithCount[] {
|
|
98
|
-
|
|
98
|
+
// `parentId` holds the parent's translation_group, so resolve links by group.
|
|
99
|
+
// Key by (locale, group): a child's parent lives in the same locale, and an
|
|
100
|
+
// unfiltered list mixes locales whose translated siblings share a group —
|
|
101
|
+
// keying by group alone would collide and misattach children across locales.
|
|
102
|
+
const byLocaleGroup = new Map<string, TermWithCount>();
|
|
99
103
|
const roots: TermWithCount[] = [];
|
|
100
|
-
for (const term of flatTerms) map.set(term.id, term);
|
|
101
104
|
for (const term of flatTerms) {
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
byLocaleGroup.set(`${term.locale}::${term.translationGroup ?? term.id}`, term);
|
|
106
|
+
}
|
|
107
|
+
for (const term of flatTerms) {
|
|
108
|
+
const parent = term.parentId
|
|
109
|
+
? byLocaleGroup.get(`${term.locale}::${term.parentId}`)
|
|
110
|
+
: undefined;
|
|
111
|
+
if (parent) {
|
|
112
|
+
parent.children.push(term);
|
|
104
113
|
} else {
|
|
105
114
|
roots.push(term);
|
|
106
115
|
}
|
|
@@ -411,24 +420,20 @@ export async function handleTermList(
|
|
|
411
420
|
* - `parentId === null` -> caller intends to detach; no-op here.
|
|
412
421
|
* - parent must exist (FK exists -> term row not soft-deleted).
|
|
413
422
|
* - parent must live in the same taxonomy.
|
|
414
|
-
* -
|
|
415
|
-
*
|
|
423
|
+
* - reject a parent in the term's own translation_group (self-parent),
|
|
424
|
+
* including the create path: a translation inherits its source's group, so
|
|
425
|
+
* `selfGroup` carries that prospective group when `termId` is absent.
|
|
426
|
+
* - on update, walk up the parent chain to detect cycles.
|
|
416
427
|
*/
|
|
417
428
|
async function validateParentTerm(
|
|
418
429
|
repo: TaxonomyRepository,
|
|
419
430
|
taxonomyName: string,
|
|
420
431
|
termId: string | undefined,
|
|
421
432
|
parentId: string | null | undefined,
|
|
433
|
+
selfGroup?: string | null,
|
|
422
434
|
): Promise<{ code: "VALIDATION_ERROR"; message: string } | null> {
|
|
423
435
|
if (parentId === undefined || parentId === null) return null;
|
|
424
436
|
|
|
425
|
-
if (termId !== undefined && parentId === termId) {
|
|
426
|
-
return {
|
|
427
|
-
code: "VALIDATION_ERROR",
|
|
428
|
-
message: "A term cannot be its own parent",
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
|
|
432
437
|
const parent = await repo.findById(parentId);
|
|
433
438
|
if (!parent) {
|
|
434
439
|
return {
|
|
@@ -443,6 +448,22 @@ async function validateParentTerm(
|
|
|
443
448
|
};
|
|
444
449
|
}
|
|
445
450
|
|
|
451
|
+
// Parentage keys off translation_group (what `parent_id` persists), so the
|
|
452
|
+
// self-parent and cycle checks compare groups, not row ids — picking a
|
|
453
|
+
// sibling translation of the term as its parent is still self-parenting.
|
|
454
|
+
const parentGroup = parent.translationGroup ?? parent.id;
|
|
455
|
+
let termGroup: string | null = selfGroup ?? null;
|
|
456
|
+
if (termId !== undefined) {
|
|
457
|
+
const term = await repo.findById(termId);
|
|
458
|
+
termGroup = term ? (term.translationGroup ?? term.id) : termId;
|
|
459
|
+
}
|
|
460
|
+
if (termGroup !== null && parentGroup === termGroup) {
|
|
461
|
+
return {
|
|
462
|
+
code: "VALIDATION_ERROR",
|
|
463
|
+
message: "A term cannot be its own parent",
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
446
467
|
// Walk up the parent chain. Two checks fold into one walk:
|
|
447
468
|
// - Cycle detection (only on update — a non-existent term-being-
|
|
448
469
|
// created can't be its own ancestor): if the walk revisits termId
|
|
@@ -454,11 +475,13 @@ async function validateParentTerm(
|
|
|
454
475
|
// The depth-exceeded error fires only when we hit the limit AND there
|
|
455
476
|
// was still chain to walk — a legitimate chain of exactly MAX_DEPTH
|
|
456
477
|
// ancestors exits with `cursor === null` and is accepted.
|
|
478
|
+
// `parent_id` stores a translation_group, which always equals the anchor
|
|
479
|
+
// row's id, so findById(cursor) still resolves each ancestor.
|
|
457
480
|
const MAX_DEPTH = 100;
|
|
458
481
|
let cursor: string | null = parent.parentId;
|
|
459
482
|
let steps = 0;
|
|
460
483
|
while (cursor !== null && steps < MAX_DEPTH) {
|
|
461
|
-
if (
|
|
484
|
+
if (termGroup !== null && cursor === termGroup) {
|
|
462
485
|
return {
|
|
463
486
|
code: "VALIDATION_ERROR",
|
|
464
487
|
message: "Cycle detected: cannot make a descendant the parent",
|
|
@@ -504,7 +527,7 @@ export async function handleTermCreate(
|
|
|
504
527
|
const repo = new TaxonomyRepository(db);
|
|
505
528
|
|
|
506
529
|
// Coerce empty-string parentId to undefined (treat as "no parent").
|
|
507
|
-
|
|
530
|
+
const parentId =
|
|
508
531
|
input.parentId === "" || input.parentId === undefined ? undefined : input.parentId;
|
|
509
532
|
|
|
510
533
|
// Conflict check is scoped to locale (per-locale slugs are unique).
|
|
@@ -521,27 +544,29 @@ export async function handleTermCreate(
|
|
|
521
544
|
};
|
|
522
545
|
}
|
|
523
546
|
|
|
524
|
-
//
|
|
525
|
-
//
|
|
526
|
-
|
|
547
|
+
// No locale re-pointing needed: `repo.create` persists the parent's
|
|
548
|
+
// translation_group in `parent_id`, so a child stays nested under the
|
|
549
|
+
// parent in every locale automatically — including parents translated
|
|
550
|
+
// after the child was created.
|
|
551
|
+
|
|
552
|
+
// A translation inherits its source's translation_group; pass that
|
|
553
|
+
// prospective group so validateParentTerm can reject a parent in the same
|
|
554
|
+
// group (cross-locale self-parent) even though the term doesn't exist yet.
|
|
555
|
+
let selfGroup: string | null = null;
|
|
556
|
+
if (input.translationOf) {
|
|
527
557
|
const source = await repo.findById(input.translationOf);
|
|
528
|
-
|
|
529
|
-
const sourceParent = await repo.findById(parentId);
|
|
530
|
-
if (sourceParent?.translationGroup) {
|
|
531
|
-
const translatedParent = await db
|
|
532
|
-
.selectFrom("taxonomies")
|
|
533
|
-
.select("id")
|
|
534
|
-
.where("translation_group", "=", sourceParent.translationGroup)
|
|
535
|
-
.where("locale", "=", input.locale)
|
|
536
|
-
.executeTakeFirst();
|
|
537
|
-
if (translatedParent) parentId = translatedParent.id;
|
|
538
|
-
}
|
|
539
|
-
}
|
|
558
|
+
selfGroup = source ? (source.translationGroup ?? source.id) : null;
|
|
540
559
|
}
|
|
541
560
|
|
|
542
561
|
// Validate parentId: must exist AND belong to the same taxonomy.
|
|
543
562
|
// (Cycle check is N/A on create — the term doesn't exist yet.)
|
|
544
|
-
const parentError = await validateParentTerm(
|
|
563
|
+
const parentError = await validateParentTerm(
|
|
564
|
+
repo,
|
|
565
|
+
taxonomyName,
|
|
566
|
+
undefined,
|
|
567
|
+
parentId,
|
|
568
|
+
selfGroup,
|
|
569
|
+
);
|
|
545
570
|
if (parentError) {
|
|
546
571
|
return { success: false, error: parentError };
|
|
547
572
|
}
|
|
@@ -606,7 +631,9 @@ export async function handleTermGet(
|
|
|
606
631
|
}
|
|
607
632
|
|
|
608
633
|
const count = await repo.countEntriesWithTerm(term.id);
|
|
609
|
-
|
|
634
|
+
// Children share this term's translation_group as their parent_id; scope
|
|
635
|
+
// to the term's own locale so the response stays within one locale's tree.
|
|
636
|
+
const children = await repo.findChildren(term.id, term.locale);
|
|
610
637
|
|
|
611
638
|
return {
|
|
612
639
|
success: true,
|
|
@@ -791,6 +818,9 @@ export async function handleTermDelete(
|
|
|
791
818
|
};
|
|
792
819
|
}
|
|
793
820
|
|
|
821
|
+
// Block deletion if the term's group still parents any child in any
|
|
822
|
+
// locale — children store this group in parent_id, so removing the term
|
|
823
|
+
// would orphan them (or null their parent_id via the self-FK).
|
|
794
824
|
const children = await repo.findChildren(term.id);
|
|
795
825
|
if (children.length > 0) {
|
|
796
826
|
return {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
const objectIs =
|
|
4
|
+
typeof Object.is === "function"
|
|
5
|
+
? Object.is
|
|
6
|
+
: (x, y) => (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
|
|
7
|
+
|
|
8
|
+
export function useSyncExternalStoreWithSelector(
|
|
9
|
+
subscribe,
|
|
10
|
+
getSnapshot,
|
|
11
|
+
getServerSnapshot,
|
|
12
|
+
selector,
|
|
13
|
+
isEqual,
|
|
14
|
+
) {
|
|
15
|
+
const instRef = React.useRef(null);
|
|
16
|
+
if (instRef.current === null) {
|
|
17
|
+
instRef.current = { hasValue: false, value: null };
|
|
18
|
+
}
|
|
19
|
+
const inst = instRef.current;
|
|
20
|
+
|
|
21
|
+
const [getSelection, getServerSelection] = React.useMemo(() => {
|
|
22
|
+
let hasMemo = false;
|
|
23
|
+
let memoizedSnapshot;
|
|
24
|
+
let memoizedSelection;
|
|
25
|
+
|
|
26
|
+
const memoizedSelector = (nextSnapshot) => {
|
|
27
|
+
if (!hasMemo) {
|
|
28
|
+
hasMemo = true;
|
|
29
|
+
memoizedSnapshot = nextSnapshot;
|
|
30
|
+
const nextSelection = selector(nextSnapshot);
|
|
31
|
+
if (isEqual !== undefined && inst.hasValue) {
|
|
32
|
+
const currentSelection = inst.value;
|
|
33
|
+
if (isEqual(currentSelection, nextSelection)) {
|
|
34
|
+
memoizedSelection = currentSelection;
|
|
35
|
+
return currentSelection;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
memoizedSelection = nextSelection;
|
|
39
|
+
return nextSelection;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const previousSelection = memoizedSelection;
|
|
43
|
+
if (objectIs(memoizedSnapshot, nextSnapshot)) {
|
|
44
|
+
return previousSelection;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nextSelection = selector(nextSnapshot);
|
|
48
|
+
if (isEqual !== undefined && isEqual(previousSelection, nextSelection)) {
|
|
49
|
+
memoizedSnapshot = nextSnapshot;
|
|
50
|
+
return previousSelection;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
memoizedSnapshot = nextSnapshot;
|
|
54
|
+
memoizedSelection = nextSelection;
|
|
55
|
+
return nextSelection;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
return [
|
|
59
|
+
() => memoizedSelector(getSnapshot()),
|
|
60
|
+
getServerSnapshot === undefined ? undefined : () => memoizedSelector(getServerSnapshot()),
|
|
61
|
+
];
|
|
62
|
+
}, [getSnapshot, getServerSnapshot, selector, isEqual]);
|
|
63
|
+
|
|
64
|
+
const value = React.useSyncExternalStore(subscribe, getSelection, getServerSelection);
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
inst.hasValue = true;
|
|
67
|
+
inst.value = value;
|
|
68
|
+
}, [inst, value]);
|
|
69
|
+
React.useDebugValue(value);
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useSyncExternalStore } from "react";
|
|
@@ -145,6 +145,15 @@ function resolveAdminSource(projectRoot: string): string | undefined {
|
|
|
145
145
|
return undefined;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
function resolveIntegrationShim(fileName: string): string {
|
|
149
|
+
const currentDir = dirname(fileURLToPath(import.meta.url));
|
|
150
|
+
const sourceShimPath = resolve(currentDir, "shims", fileName);
|
|
151
|
+
if (existsSync(sourceShimPath)) {
|
|
152
|
+
return sourceShimPath;
|
|
153
|
+
}
|
|
154
|
+
return resolve(currentDir, "..", "..", "src", "astro", "integration", "shims", fileName);
|
|
155
|
+
}
|
|
156
|
+
|
|
148
157
|
export interface VitePluginOptions {
|
|
149
158
|
/** Serializable config (database, storage, auth descriptors) */
|
|
150
159
|
serializableConfig: Record<string, unknown>;
|
|
@@ -159,7 +168,10 @@ export interface VitePluginOptions {
|
|
|
159
168
|
/**
|
|
160
169
|
* Creates the EmDash virtual modules Vite plugin.
|
|
161
170
|
*/
|
|
162
|
-
export function createVirtualModulesPlugin(
|
|
171
|
+
export function createVirtualModulesPlugin(
|
|
172
|
+
options: VitePluginOptions,
|
|
173
|
+
astroCommand: "dev" | "build" | "preview" | "sync",
|
|
174
|
+
): Plugin {
|
|
163
175
|
const { serializableConfig, resolvedConfig, pluginDescriptors, astroConfig } = options;
|
|
164
176
|
|
|
165
177
|
let viteCommand: "build" | "serve" | undefined;
|
|
@@ -294,8 +306,16 @@ export function createVirtualModulesPlugin(options: VitePluginOptions): Plugin {
|
|
|
294
306
|
// Generate scheduler module — a NodeCronScheduler factory on
|
|
295
307
|
// long-lived runtimes, or null under the Cloudflare adapter where
|
|
296
308
|
// a Cron Trigger drives scheduled work instead.
|
|
309
|
+
//
|
|
310
|
+
// Decide from Astro's command, not Vite's config.command: the
|
|
311
|
+
// Cloudflare adapter builds the worker bundle via a nested Vite
|
|
312
|
+
// *build* pass even during `astro dev`, so viteCommand reports
|
|
313
|
+
// "build" and would wrongly suppress the in-process timer (#1635).
|
|
314
|
+
// Astro's command stays "dev", which is the only case that should
|
|
315
|
+
// fall through to a NodeCronScheduler.
|
|
297
316
|
if (id === RESOLVED_VIRTUAL_SCHEDULER_ID) {
|
|
298
|
-
|
|
317
|
+
const schedulerCommand = astroCommand === "dev" ? "serve" : "build";
|
|
318
|
+
return generateSchedulerModule(astroConfig.adapter?.name, schedulerCommand);
|
|
299
319
|
}
|
|
300
320
|
},
|
|
301
321
|
};
|
|
@@ -340,6 +360,10 @@ export function createViteConfig(
|
|
|
340
360
|
|
|
341
361
|
const adminSourcePath = isDev ? resolveAdminSource(projectRoot) : undefined;
|
|
342
362
|
const useSource = adminSourcePath !== undefined;
|
|
363
|
+
const useSyncExternalStoreShimPath = resolveIntegrationShim("use-sync-external-store.js");
|
|
364
|
+
const useSyncExternalStoreWithSelectorShimPath = resolveIntegrationShim(
|
|
365
|
+
"use-sync-external-store-with-selector.js",
|
|
366
|
+
);
|
|
343
367
|
|
|
344
368
|
return {
|
|
345
369
|
// Astro SSR routes resolve version.ts from source (not tsdown dist),
|
|
@@ -370,19 +394,34 @@ export function createViteConfig(
|
|
|
370
394
|
// dep scanner can't reach it for pre-bundling — so the browser is
|
|
371
395
|
// served raw `module.exports` and hydration fails with
|
|
372
396
|
// `SyntaxError: ... does not provide an export named
|
|
373
|
-
// 'useSyncExternalStore'`. Redirect both shim entry points to
|
|
374
|
-
//
|
|
375
|
-
//
|
|
397
|
+
// 'useSyncExternalStore'`. Redirect both shim entry points to a
|
|
398
|
+
// tiny ESM shim file that re-exports React's built-in hook, and
|
|
399
|
+
// redirect the selector entry points to an ESM wrapper around that
|
|
400
|
+
// hook. The absolute file paths are required because Vite/Rolldown
|
|
401
|
+
// dependency optimization applies aliases without EmDash's virtual
|
|
402
|
+
// module plugin. This also avoids the package main entry's React
|
|
403
|
+
// 18+ dev warning.
|
|
404
|
+
{
|
|
405
|
+
find: "use-sync-external-store/shim/with-selector.js",
|
|
406
|
+
replacement: useSyncExternalStoreWithSelectorShimPath,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
find: "use-sync-external-store/shim/with-selector",
|
|
410
|
+
replacement: useSyncExternalStoreWithSelectorShimPath,
|
|
411
|
+
},
|
|
376
412
|
{
|
|
377
413
|
find: "use-sync-external-store/shim/index.js",
|
|
378
|
-
replacement:
|
|
414
|
+
replacement: useSyncExternalStoreShimPath,
|
|
415
|
+
},
|
|
416
|
+
{
|
|
417
|
+
find: "use-sync-external-store/shim",
|
|
418
|
+
replacement: useSyncExternalStoreShimPath,
|
|
379
419
|
},
|
|
380
|
-
{ find: "use-sync-external-store/shim", replacement: "use-sync-external-store" },
|
|
381
420
|
],
|
|
382
421
|
},
|
|
383
422
|
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- Monorepo has both vite 6 (docs) and vite 7 (core). tsgo resolves correctly.
|
|
384
423
|
plugins: [
|
|
385
|
-
createVirtualModulesPlugin(options),
|
|
424
|
+
createVirtualModulesPlugin(options, command),
|
|
386
425
|
// In dev mode with source alias, compile Lingui macros on the fly
|
|
387
426
|
// and redirect locale .mjs imports to dist/.
|
|
388
427
|
// In production, macros are pre-compiled by tsdown in the admin package.
|
package/src/astro/middleware.ts
CHANGED
|
@@ -355,9 +355,19 @@ function finalizeResponse(
|
|
|
355
355
|
if (astroCookies !== undefined) {
|
|
356
356
|
Reflect.set(res, ASTRO_COOKIES_SYMBOL, astroCookies);
|
|
357
357
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
358
|
+
// Set-if-absent so a host app that sets stricter values on its own routes
|
|
359
|
+
// wins. The middleware registers `order: 'pre'` (#1282), so on the response
|
|
360
|
+
// path it runs *after* host middleware; unconditional `set()` would clobber
|
|
361
|
+
// the host's headers on every public route (#1393). Mirrors the CSP guard.
|
|
362
|
+
if (!res.headers.has("X-Content-Type-Options")) {
|
|
363
|
+
res.headers.set("X-Content-Type-Options", "nosniff");
|
|
364
|
+
}
|
|
365
|
+
if (!res.headers.has("Referrer-Policy")) {
|
|
366
|
+
res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
367
|
+
}
|
|
368
|
+
if (!res.headers.has("Permissions-Policy")) {
|
|
369
|
+
res.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=(), payment=()");
|
|
370
|
+
}
|
|
361
371
|
if (!res.headers.has("Content-Security-Policy")) {
|
|
362
372
|
res.headers.set("X-Frame-Options", "SAMEORIGIN");
|
|
363
373
|
}
|
|
@@ -375,9 +375,10 @@ async function exportTaxonomies(
|
|
|
375
375
|
// Terms in this def's locale.
|
|
376
376
|
const terms = await termRepo.findByName(def.name, { locale: def.locale });
|
|
377
377
|
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
// translation_group -> slug for parent resolution within this locale.
|
|
379
|
+
// `parentId` stores the parent's translation_group, not a row id.
|
|
380
|
+
const groupToSlug = new Map<string, string>();
|
|
381
|
+
for (const term of terms) groupToSlug.set(term.translationGroup ?? term.id, term.slug);
|
|
381
382
|
|
|
382
383
|
// translation_group -> seed id of the anchor term.
|
|
383
384
|
const termGroupToSeedId = new Map<string, string>();
|
|
@@ -396,7 +397,7 @@ async function exportTaxonomies(
|
|
|
396
397
|
description: typeof term.data?.description === "string" ? term.data.description : undefined,
|
|
397
398
|
};
|
|
398
399
|
|
|
399
|
-
if (term.parentId) seedTerm.parent =
|
|
400
|
+
if (term.parentId) seedTerm.parent = groupToSlug.get(term.parentId);
|
|
400
401
|
|
|
401
402
|
if (i18nEnabled && term.locale) {
|
|
402
403
|
seedTerm.locale = term.locale;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
import { sql } from "kysely";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Store the parent's `translation_group` in `taxonomies.parent_id` instead of a
|
|
6
|
+
* locale-bound row id.
|
|
7
|
+
*
|
|
8
|
+
* Before this, a term's `parent_id` pointed at one specific parent *row* (one
|
|
9
|
+
* locale). A child translated in another locale could end up parented to a row
|
|
10
|
+
* in a different locale, silently flattening that locale's tree (#1347). Every
|
|
11
|
+
* other taxonomy i18n relationship already keys off `translation_group`
|
|
12
|
+
* (`content_taxonomies.taxonomy_id`, `_emdash_menu_items.reference_id`), so this
|
|
13
|
+
* aligns parentage with that model: a child links to the parent *concept* and
|
|
14
|
+
* stays nested in whichever locale that parent has been translated into.
|
|
15
|
+
*
|
|
16
|
+
* Backfill rewrites every existing `parent_id` from the referenced parent row's
|
|
17
|
+
* id to that parent's `translation_group`. A `translation_group` normally equals
|
|
18
|
+
* its anchor row's id, so the self-FK on `parent_id` stays valid. We only
|
|
19
|
+
* rewrite when that anchor row still exists; if a parent's anchor was deleted
|
|
20
|
+
* but a sibling translation survives, the existing locale-bound id is left as-is
|
|
21
|
+
* rather than rewritten to a dangling FK value. Such a child then renders as a
|
|
22
|
+
* root rather than nested — an accepted degradation for an already-inconsistent
|
|
23
|
+
* dataset (its `translation_group` already points at a deleted row, which breaks
|
|
24
|
+
* `content_taxonomies` joins too). New deletes can't recreate this state: a
|
|
25
|
+
* parent with children in any locale is undeletable. The correlated subquery is
|
|
26
|
+
* a no-op for rows that already hold a group (an anchor row's id resolves to
|
|
27
|
+
* itself), so the migration is safe to re-run.
|
|
28
|
+
*
|
|
29
|
+
* Dialect-independent: the correlated-subquery `UPDATE` runs on SQLite (incl.
|
|
30
|
+
* D1) and Postgres alike.
|
|
31
|
+
*/
|
|
32
|
+
export async function up(db: Kysely<unknown>): Promise<void> {
|
|
33
|
+
await sql`
|
|
34
|
+
UPDATE taxonomies
|
|
35
|
+
SET parent_id = (
|
|
36
|
+
SELECT p.translation_group FROM taxonomies p WHERE p.id = taxonomies.parent_id
|
|
37
|
+
)
|
|
38
|
+
WHERE parent_id IS NOT NULL
|
|
39
|
+
AND EXISTS (
|
|
40
|
+
SELECT 1 FROM taxonomies p
|
|
41
|
+
WHERE p.id = taxonomies.parent_id
|
|
42
|
+
AND p.translation_group IS NOT NULL
|
|
43
|
+
-- Only rewrite to a translation_group that is itself a live row
|
|
44
|
+
-- id, so the self-FK on parent_id can never dangle.
|
|
45
|
+
AND EXISTS (SELECT 1 FROM taxonomies a WHERE a.id = p.translation_group)
|
|
46
|
+
)
|
|
47
|
+
`.execute(db);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Map each `parent_id` group back to the row id of the parent in the same
|
|
52
|
+
* locale (the pre-migration shape). Rows with no same-locale parent are left as
|
|
53
|
+
* they are rather than nulled, so a rollback never silently detaches a subtree.
|
|
54
|
+
*/
|
|
55
|
+
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
56
|
+
await sql`
|
|
57
|
+
UPDATE taxonomies
|
|
58
|
+
SET parent_id = COALESCE(
|
|
59
|
+
(
|
|
60
|
+
SELECT c.id FROM taxonomies c
|
|
61
|
+
WHERE c.translation_group = taxonomies.parent_id
|
|
62
|
+
AND c.locale = taxonomies.locale
|
|
63
|
+
),
|
|
64
|
+
parent_id
|
|
65
|
+
)
|
|
66
|
+
WHERE parent_id IS NOT NULL
|
|
67
|
+
`.execute(db);
|
|
68
|
+
}
|
|
@@ -46,6 +46,7 @@ import * as m041 from "./041_content_locale_list_index.js";
|
|
|
46
46
|
import * as m042 from "./042_byline_fields.js";
|
|
47
47
|
import * as m043 from "./043_content_references.js";
|
|
48
48
|
import * as m044 from "./044_comment_reactions.js";
|
|
49
|
+
import * as m045 from "./045_taxonomy_parent_group.js";
|
|
49
50
|
|
|
50
51
|
const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
|
|
51
52
|
"001_initial": m001,
|
|
@@ -91,6 +92,7 @@ const MIGRATIONS: Readonly<Record<string, Migration>> = Object.freeze({
|
|
|
91
92
|
"042_byline_fields": m042,
|
|
92
93
|
"043_content_references": m043,
|
|
93
94
|
"044_comment_reactions": m044,
|
|
95
|
+
"045_taxonomy_parent_group": m045,
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
/** Total number of registered migrations. Exported for use in tests. */
|