emdash 0.24.1 → 0.25.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.d.mts +2 -2
- package/dist/api/route-utils.mjs +9 -8
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/api/schemas/index.mjs +2 -2
- package/dist/{api-d_0wi9fW.mjs → api-D2_sbavn.mjs} +16 -12
- package/dist/api-D2_sbavn.mjs.map +1 -0
- package/dist/{apply-BE9UaWX0.mjs → apply-Bk2mAuLn.mjs} +11 -10
- package/dist/apply-Bk2mAuLn.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +2 -2
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +66 -37
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +4 -4
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +4 -4
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +1 -1
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +2 -2
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +5 -5
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +7 -7
- package/dist/astro/routes/api/admin/bylines/index.mjs +7 -7
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/bulk.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/counts.mjs +2 -2
- package/dist/astro/routes/api/admin/comments/index.mjs +5 -5
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +3 -3
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +2 -2
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +2 -2
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +21 -21
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +20 -20
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +21 -21
- package/dist/astro/routes/api/admin/plugins/updates.mjs +20 -20
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +2 -2
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +20 -20
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +4 -4
- package/dist/astro/routes/api/auth/dev-bypass.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/index.mjs +4 -4
- package/dist/astro/routes/api/auth/invite/register-options.mjs +4 -4
- package/dist/astro/routes/api/auth/logout.mjs +1 -1
- package/dist/astro/routes/api/auth/magic-link/send.mjs +5 -5
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +1 -1
- package/dist/astro/routes/api/auth/me.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/passkey/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/complete.mjs +4 -4
- package/dist/astro/routes/api/auth/signup/request.mjs +5 -5
- package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +4 -4
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +8 -8
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/index.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/trash.mjs +5 -5
- package/dist/astro/routes/api/dashboard.mjs +5 -5
- package/dist/astro/routes/api/dev/emails.mjs +1 -1
- package/dist/astro/routes/api/import/probe.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +2 -2
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +5 -5
- package/dist/astro/routes/api/import/wordpress/execute.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/media.d.mts +13 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.mjs +12 -8
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +6 -6
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +7 -7
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +5 -5
- package/dist/astro/routes/api/manifest.mjs +2 -2
- package/dist/astro/routes/api/mcp.mjs +20 -20
- package/dist/astro/routes/api/media/_id_/confirm.d.mts.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +43 -7
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +5 -5
- package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
- package/dist/astro/routes/api/media/providers/index.mjs +2 -2
- package/dist/astro/routes/api/media/upload-url.mjs +7 -7
- package/dist/astro/routes/api/media.mjs +21 -20
- package/dist/astro/routes/api/media.mjs.map +1 -1
- 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 +2 -2
- package/dist/astro/routes/api/oauth/device/code.mjs +3 -3
- package/dist/astro/routes/api/oauth/device/token.mjs +3 -3
- package/dist/astro/routes/api/oauth/register.mjs +1 -1
- package/dist/astro/routes/api/oauth/token/refresh.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/revoke.mjs +2 -2
- package/dist/astro/routes/api/oauth/token.mjs +1 -1
- package/dist/astro/routes/api/openapi.json.mjs +2 -2
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +5 -5
- package/dist/astro/routes/api/redirects/404s/summary.mjs +5 -5
- package/dist/astro/routes/api/redirects/_id_.mjs +6 -6
- package/dist/astro/routes/api/redirects/index.mjs +6 -6
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +2 -2
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +20 -20
- package/dist/astro/routes/api/schema/collections/index.mjs +20 -20
- package/dist/astro/routes/api/schema/index.mjs +3 -3
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +20 -20
- package/dist/astro/routes/api/schema/orphans/index.mjs +20 -20
- 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 +4 -4
- package/dist/astro/routes/api/search/suggest.mjs +6 -6
- package/dist/astro/routes/api/sections/_slug_.mjs +7 -7
- package/dist/astro/routes/api/sections/index.mjs +7 -7
- package/dist/astro/routes/api/settings/email.mjs +3 -3
- package/dist/astro/routes/api/settings.mjs +9 -9
- package/dist/astro/routes/api/setup/admin-verify.mjs +4 -4
- package/dist/astro/routes/api/setup/admin.mjs +4 -4
- package/dist/astro/routes/api/setup/dev-bypass.mjs +7 -7
- package/dist/astro/routes/api/setup/dev-reset.mjs +1 -1
- package/dist/astro/routes/api/setup/index.mjs +10 -10
- package/dist/astro/routes/api/setup/status.mjs +1 -1
- package/dist/astro/routes/api/snapshot.mjs +2 -2
- 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 +2 -2
- package/dist/astro/routes/api/typegen.mjs +3 -3
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/_name_.mjs +4 -4
- package/dist/astro/routes/api/widget-areas/index.mjs +7 -7
- package/dist/astro/routes/api/widget-components.mjs +1 -1
- package/dist/astro/routes/robots.txt.mjs +3 -3
- 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 +7 -7
- package/dist/{authorize-B__u-iZJ.mjs → authorize-CuxZ0i2t.mjs} +2 -2
- package/dist/{authorize-B__u-iZJ.mjs.map → authorize-CuxZ0i2t.mjs.map} +1 -1
- package/dist/{byline-fields-Ca56gaT6.d.mts → byline-fields-KD3JFRuH.d.mts} +24 -6
- package/dist/byline-fields-KD3JFRuH.d.mts.map +1 -0
- package/dist/{byline-fields-B-KXkg5R.mjs → byline-fields-eyJMblSr.mjs} +2 -2
- package/dist/{byline-fields-B-KXkg5R.mjs.map → byline-fields-eyJMblSr.mjs.map} +1 -1
- package/dist/{bylines-BMtJnPnG.mjs → bylines-CPc_q2Vd.mjs} +4 -4
- package/dist/{bylines-BMtJnPnG.mjs.map → bylines-CPc_q2Vd.mjs.map} +1 -1
- package/dist/{cache-BUJe2w6Z.mjs → cache-C35VWD5D.mjs} +1 -1
- package/dist/{cache-BUJe2w6Z.mjs.map → cache-C35VWD5D.mjs.map} +1 -1
- package/dist/cli/index.mjs +8 -8
- package/dist/{content-PoVGKjp0.mjs → content-B43-YrIq.mjs} +7 -5
- package/dist/content-B43-YrIq.mjs.map +1 -0
- package/dist/{context-Dvws41_A.mjs → context-BW3N7wyt.mjs} +12 -6
- package/dist/context-BW3N7wyt.mjs.map +1 -0
- package/dist/{dashboard-DsYU8JKS.mjs → dashboard-FIe0S512.mjs} +3 -3
- package/dist/{dashboard-DsYU8JKS.mjs.map → dashboard-FIe0S512.mjs.map} +1 -1
- package/dist/{db-errors-UXB1CK42.mjs → db-errors-CK46D-ly.mjs} +1 -1
- package/dist/{db-errors-UXB1CK42.mjs.map → db-errors-CK46D-ly.mjs.map} +1 -1
- package/dist/{placeholder-B0s2mWu5.mjs → enrich-BQ0mxJRs.mjs} +61 -10
- package/dist/enrich-BQ0mxJRs.mjs.map +1 -0
- package/dist/{error-lq_fL-RS.mjs → error-DXnK2wF4.mjs} +2 -1
- package/dist/error-DXnK2wF4.mjs.map +1 -0
- package/dist/{index-BroXT-vD.d.mts → index-Cz6_JBL3.d.mts} +21 -8
- package/dist/index-Cz6_JBL3.d.mts.map +1 -0
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +28 -28
- package/dist/{loader-BR2SgfEc.mjs → loader-DrpofRon.mjs} +2 -2
- package/dist/{loader-BR2SgfEc.mjs.map → loader-DrpofRon.mjs.map} +1 -1
- package/dist/{manifest-schema-BWKnQYQF.mjs → manifest-schema-BOfPXse0.mjs} +2 -1
- package/dist/manifest-schema-BOfPXse0.mjs.map +1 -0
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +45 -2
- package/dist/media/index.d.mts.map +1 -1
- package/dist/media/index.mjs +5 -3
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +6 -6
- package/dist/media/local-runtime.d.mts.map +1 -1
- package/dist/media/local-runtime.mjs +15 -3
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-CTjJoJTh.mjs → media-BjKxjzk8.mjs} +3 -1
- package/dist/media-BjKxjzk8.mjs.map +1 -0
- package/dist/{media-allowlist-B77I5_bw.mjs → media-allowlist-Du8t1u6a.mjs} +2 -2
- package/dist/{media-allowlist-B77I5_bw.mjs.map → media-allowlist-Du8t1u6a.mjs.map} +1 -1
- package/dist/{menus-GvRSpgIA.mjs → menus-CxqJ-z-N.mjs} +46 -4
- package/dist/menus-CxqJ-z-N.mjs.map +1 -0
- package/dist/{mime-BTzvzJ8M.mjs → mime-CCEzze7W.mjs} +1 -1
- package/dist/{mime-BTzvzJ8M.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
- package/dist/{normalize-ClbNIenm.mjs → normalize-CeglrBT9.mjs} +11 -2
- package/dist/normalize-CeglrBT9.mjs.map +1 -0
- package/dist/{options-CqECG0gM.d.mts → options-BvClpP5Q.d.mts} +2 -2
- package/dist/{options-CqECG0gM.d.mts.map → options-BvClpP5Q.d.mts.map} +1 -1
- package/dist/page/index.d.mts +1 -1
- package/dist/{parse-DHI873qU.mjs → parse-DqMWHQ8x.mjs} +2 -2
- package/dist/{parse-DHI873qU.mjs.map → parse-DqMWHQ8x.mjs.map} +1 -1
- package/dist/{placeholder-QoI4gEzP.d.mts → placeholder-Cuce9U-m.d.mts} +20 -5
- package/dist/placeholder-Cuce9U-m.d.mts.map +1 -0
- package/dist/plugin-types.d.mts +2 -1
- package/dist/plugin-types.d.mts.map +1 -1
- package/dist/plugin-utils.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-DZdlS6Cu.mjs → query-34pfm02Q.mjs} +11 -11
- package/dist/{query-DZdlS6Cu.mjs.map → query-34pfm02Q.mjs.map} +1 -1
- package/dist/{rate-limit-D1Y6VFVP.mjs → rate-limit-CBQK4ygJ.mjs} +2 -2
- package/dist/{rate-limit-D1Y6VFVP.mjs.map → rate-limit-CBQK4ygJ.mjs.map} +1 -1
- package/dist/{redirects-x9eFPeyi.mjs → redirects-Dcbuisoj.mjs} +2 -1
- package/dist/redirects-Dcbuisoj.mjs.map +1 -0
- package/dist/{registry-CJkCubOJ.mjs → registry-3aOAS4My.mjs} +11 -1
- package/dist/registry-3aOAS4My.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{schema-DMjXuvNU.mjs → schema-BGwx8AQY.mjs} +3 -3
- package/dist/{schema-DMjXuvNU.mjs.map → schema-BGwx8AQY.mjs.map} +1 -1
- package/dist/{search-D1wKk3lc.mjs → search-Gl747zMP.mjs} +2 -2
- package/dist/{search-D1wKk3lc.mjs.map → search-Gl747zMP.mjs.map} +1 -1
- package/dist/{sections-CCMic8dB.mjs → sections-C-umuuLj.mjs} +2 -2
- package/dist/{sections-CCMic8dB.mjs.map → sections-C-umuuLj.mjs.map} +1 -1
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +6 -6
- package/dist/seo/index.d.mts +1 -1
- package/dist/{seo-3QrZ1EPT.mjs → seo-BNzkiK_W.mjs} +1 -1
- package/dist/{seo-3QrZ1EPT.mjs.map → seo-BNzkiK_W.mjs.map} +1 -1
- package/dist/{seo-DCEF-AVr.mjs → seo-BkhuuaaE.mjs} +25 -3
- package/dist/seo-BkhuuaaE.mjs.map +1 -0
- package/dist/{settings-D9rvaLuY.mjs → settings-C4tXRYiG.mjs} +3 -3
- package/dist/{settings-D9rvaLuY.mjs.map → settings-C4tXRYiG.mjs.map} +1 -1
- package/dist/{settings-C3Yi5Co3.mjs → settings-fPSR6dJa.mjs} +2 -2
- package/dist/{settings-C3Yi5Co3.mjs.map → settings-fPSR6dJa.mjs.map} +1 -1
- package/dist/{taxonomies-DhLaNJji.mjs → taxonomies-DbAtL6m4.mjs} +2 -2
- package/dist/{taxonomies-DhLaNJji.mjs.map → taxonomies-DbAtL6m4.mjs.map} +1 -1
- package/dist/{taxonomies-CurpLtNC.mjs → taxonomies-n8w72lLQ.mjs} +4 -4
- package/dist/{taxonomies-CurpLtNC.mjs.map → taxonomies-n8w72lLQ.mjs.map} +1 -1
- package/dist/{types-CTLJYMhN.d.mts → types-CNlaBFzx.d.mts} +11 -1
- package/dist/{types-CTLJYMhN.d.mts.map → types-CNlaBFzx.d.mts.map} +1 -1
- package/dist/{types-BWh2MjmA.d.mts → types-ETmO_jQr.d.mts} +2 -2
- package/dist/{types-BWh2MjmA.d.mts.map → types-ETmO_jQr.d.mts.map} +1 -1
- package/dist/{types-TTx8uP7V.d.mts → types-h2Cwbzi2.d.mts} +6 -3
- package/dist/{types-TTx8uP7V.d.mts.map → types-h2Cwbzi2.d.mts.map} +1 -1
- package/dist/types-tM44hEcf.mjs.map +1 -1
- package/dist/{validate-DOFrIb-c.d.mts → validate-Va_An3qb.d.mts} +2 -2
- package/dist/{validate-DOFrIb-c.d.mts.map → validate-Va_An3qb.d.mts.map} +1 -1
- package/dist/{validation-CY1gk4Ar.mjs → validation-B6DKPTtU.mjs} +3 -3
- package/dist/{validation-CY1gk4Ar.mjs.map → validation-B6DKPTtU.mjs.map} +1 -1
- package/dist/version-DHdCA24u.mjs +7 -0
- package/dist/{version-C0JPFUG8.mjs.map → version-DHdCA24u.mjs.map} +1 -1
- package/dist/{widgets-Dri0spI4.mjs → widgets-C2RbFN0G.mjs} +2 -2
- package/dist/{widgets-Dri0spI4.mjs.map → widgets-C2RbFN0G.mjs.map} +1 -1
- package/package.json +6 -6
- package/src/api/errors.ts +1 -0
- package/src/api/handlers/content.ts +4 -4
- package/src/api/handlers/seo.ts +30 -2
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/middleware.ts +14 -1
- package/src/astro/routes/api/import/wordpress/media.ts +9 -3
- package/src/astro/routes/api/media/[id]/confirm.ts +62 -2
- package/src/astro/routes/api/media.ts +17 -19
- package/src/components/EmDashImage.astro +4 -7
- package/src/components/Image.astro +14 -4
- package/src/components/InlinePortableTextEditor.tsx +48 -1
- package/src/database/repositories/content.ts +8 -5
- package/src/database/repositories/media.ts +9 -1
- package/src/emdash-runtime.ts +39 -1
- package/src/media/enrich.ts +78 -0
- package/src/media/index.ts +1 -0
- package/src/media/local-runtime.ts +19 -0
- package/src/media/normalize.ts +20 -1
- package/src/media/placeholder.ts +29 -9
- package/src/media/types.ts +14 -0
- package/src/plugin-types.ts +2 -0
- package/src/plugins/context.ts +8 -0
- package/src/plugins/hooks.ts +45 -0
- package/src/plugins/index.ts +1 -0
- package/src/plugins/manager.ts +11 -0
- package/src/plugins/manifest-schema.ts +1 -0
- package/src/plugins/types.ts +7 -0
- package/src/schema/registry.ts +25 -0
- package/src/schema/types.ts +10 -0
- package/src/seed/apply.ts +1 -0
- package/dist/api-d_0wi9fW.mjs.map +0 -1
- package/dist/apply-BE9UaWX0.mjs.map +0 -1
- package/dist/byline-fields-Ca56gaT6.d.mts.map +0 -1
- package/dist/content-PoVGKjp0.mjs.map +0 -1
- package/dist/context-Dvws41_A.mjs.map +0 -1
- package/dist/error-lq_fL-RS.mjs.map +0 -1
- package/dist/index-BroXT-vD.d.mts.map +0 -1
- package/dist/manifest-schema-BWKnQYQF.mjs.map +0 -1
- package/dist/media-CTjJoJTh.mjs.map +0 -1
- package/dist/menus-GvRSpgIA.mjs.map +0 -1
- package/dist/normalize-ClbNIenm.mjs.map +0 -1
- package/dist/placeholder-B0s2mWu5.mjs.map +0 -1
- package/dist/placeholder-QoI4gEzP.d.mts.map +0 -1
- package/dist/redirects-x9eFPeyi.mjs.map +0 -1
- package/dist/registry-CJkCubOJ.mjs.map +0 -1
- package/dist/seo-DCEF-AVr.mjs.map +0 -1
- package/dist/version-C0JPFUG8.mjs +0 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types-tM44hEcf.mjs","names":[],"sources":["../src/schema/types.ts"],"sourcesContent":["/**\n * Schema Registry Types\n *\n * These types represent the schema definitions stored in D1.\n * They are the source of truth for all collections and fields.\n */\n\n/**\n * Supported field types\n */\nexport type FieldType =\n\t| \"string\"\n\t| \"text\"\n\t| \"url\"\n\t| \"number\"\n\t| \"integer\"\n\t| \"boolean\"\n\t| \"datetime\"\n\t| \"select\"\n\t| \"multiSelect\"\n\t| \"portableText\"\n\t| \"image\"\n\t| \"file\"\n\t| \"reference\"\n\t| \"json\"\n\t| \"slug\"\n\t| \"repeater\";\n\n/**\n * Array of all field types for validation\n */\nexport const FIELD_TYPES: readonly FieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n\t\"repeater\",\n] as const;\n\n/**\n * SQLite column types that map from field types\n */\nexport type ColumnType = \"TEXT\" | \"REAL\" | \"INTEGER\" | \"JSON\";\n\n/**\n * Map field types to their SQLite column types\n */\nexport const FIELD_TYPE_TO_COLUMN: Record<FieldType, ColumnType> = {\n\tstring: \"TEXT\",\n\ttext: \"TEXT\",\n\tnumber: \"REAL\",\n\tinteger: \"INTEGER\",\n\tboolean: \"INTEGER\",\n\tdatetime: \"TEXT\",\n\tselect: \"TEXT\",\n\tmultiSelect: \"JSON\",\n\tportableText: \"JSON\",\n\timage: \"TEXT\",\n\tfile: \"TEXT\",\n\treference: \"TEXT\",\n\tjson: \"JSON\",\n\tslug: \"TEXT\",\n\turl: \"TEXT\",\n\trepeater: \"JSON\",\n};\n\n/**\n * Features a collection can support\n */\nexport type CollectionSupport =\n\t| \"drafts\"\n\t| \"revisions\"\n\t| \"preview\"\n\t| \"scheduling\"\n\t| \"search\"\n\t| \"seo\";\n\n/**\n * Sources for how a collection was created\n */\nexport type CollectionSource =\n\t| `template:${string}`\n\t| `import:${string}`\n\t| \"manual\"\n\t| \"discovered\"\n\t| \"seed\";\n\n/**\n * Validation rules for a field\n */\n/** Sub-field definition for repeater fields */\nexport interface RepeaterSubField {\n\tslug: string;\n\ttype:\n\t\t| \"string\"\n\t\t| \"text\"\n\t\t| \"url\"\n\t\t| \"number\"\n\t\t| \"integer\"\n\t\t| \"boolean\"\n\t\t| \"datetime\"\n\t\t| \"select\"\n\t\t| \"image\";\n\tlabel: string;\n\trequired?: boolean;\n\toptions?: string[]; // For select sub-fields\n}\n\n/** Allowed types for repeater sub-fields (no nesting, no complex types) */\nexport const REPEATER_SUB_FIELD_TYPES = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"image\",\n] as const;\n\nexport interface FieldValidation {\n\trequired?: boolean;\n\tmin?: number;\n\tmax?: number;\n\tminLength?: number;\n\tmaxLength?: number;\n\tpattern?: string;\n\toptions?: string[]; // For select/multiSelect\n\tsubFields?: RepeaterSubField[]; // For repeater fields\n\tminItems?: number; // For repeater fields\n\tmaxItems?: number; // For repeater fields\n\tallowedMimeTypes?: string[];\n}\n\n/**\n * Widget options for field rendering\n */\nexport interface FieldWidgetOptions {\n\trows?: number; // For textarea\n\tshowPreview?: boolean; // For image/file\n\tcollection?: string; // For reference - which collection to reference\n\tallowMultiple?: boolean; // For reference\n\t[key: string]: unknown;\n}\n\n/**\n * A collection definition\n */\nexport interface Collection {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports: CollectionSupport[];\n\tsource?: CollectionSource;\n\t/** Whether this collection has SEO metadata fields enabled */\n\thasSeo: boolean;\n\t/** URL pattern with {slug} placeholder (e.g. \"/{slug}\", \"/blog/{slug}\") */\n\turlPattern?: string;\n\t/** Whether comments are enabled for this collection */\n\tcommentsEnabled: boolean;\n\t/** Moderation strategy: \"all\" | \"first_time\" | \"none\" */\n\tcommentsModeration: \"all\" | \"first_time\" | \"none\";\n\t/** Auto-close comments after N days. 0 = never close. */\n\tcommentsClosedAfterDays: number;\n\t/** Auto-approve comments from authenticated CMS users */\n\tcommentsAutoApproveUsers: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * A field definition\n */\nexport interface Field {\n\tid: string;\n\tcollectionId: string;\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\tcolumnType: ColumnType;\n\trequired: boolean;\n\tunique: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder: number;\n\tsearchable: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable: boolean;\n\tcreatedAt: string;\n}\n\n/**\n * Input for creating a collection\n */\nexport interface CreateCollectionInput {\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\tsource?: CollectionSource;\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n}\n\n/**\n * Input for updating a collection\n */\nexport interface UpdateCollectionInput {\n\tlabel?: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n\tcommentsModeration?: \"all\" | \"first_time\" | \"none\";\n\tcommentsClosedAfterDays?: number;\n\tcommentsAutoApproveUsers?: boolean;\n}\n\n/**\n * Input for creating a field\n */\nexport interface CreateFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * Input for updating a field\n */\nexport interface UpdateFieldInput {\n\tlabel?: string;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * A collection with its fields\n */\nexport interface CollectionWithFields extends Collection {\n\tfields: Field[];\n}\n\n/**\n * Reserved field slugs that cannot be used.\n *\n * Includes names reserved for runtime hydration (`terms`, `bylines`, `byline`)\n * so user-defined fields never shadow the auto-hydrated values on entry.data.\n */\nexport const RESERVED_FIELD_SLUGS = [\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t// Runtime-hydrated fields\n\t\"terms\",\n\t\"bylines\",\n\t\"byline\",\n];\n\n/**\n * Reserved collection slugs that cannot be used\n */\nexport const RESERVED_COLLECTION_SLUGS = [\n\t\"content\",\n\t\"media\",\n\t\"users\",\n\t\"revisions\",\n\t\"taxonomies\",\n\t\"options\",\n\t\"audit_logs\",\n];\n\n/**\n * Byline custom fields (Discussion #1174).\n *\n * Sites declare site-specific byline metadata (`job_title`, `pronouns`,\n * `twitter_handle`, `company`, …) without touching emdash core. Definitions\n * live in `_emdash_byline_fields`; values in either\n * `_emdash_byline_field_values` (translatable, keyed by `byline_id`) or\n * `_emdash_byline_field_group_values` (non-translatable, keyed by\n * `translation_group`). The per-field `translatable` flag decides which\n * value table is used. See migration 041.\n */\n\n/**\n * The five v1 field types supported on byline custom fields. Deliberately\n * narrower than the content `FieldType` union — bylines don't need\n * `portableText`, `reference`, `image`, etc. v2 may extend this; v1 keeps\n * the storage and UI surfaces small.\n */\nexport type BylineFieldType = \"string\" | \"text\" | \"url\" | \"boolean\" | \"select\";\n\nexport const BYLINE_FIELD_TYPES: readonly BylineFieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"boolean\",\n\t\"select\",\n] as const;\n\n/**\n * Validation rules for a byline custom field. v1 only exposes `options`\n * (the choice list for `select` fields). The shape mirrors the content-\n * field convention so the admin UI patterns transfer.\n */\nexport interface BylineFieldValidation {\n\t/** Choices for `select`-type fields. Ignored for other types. */\n\toptions?: string[];\n}\n\n/**\n * Runtime shape of a registered byline custom field. Stored in\n * `_emdash_byline_fields` (see migration 041).\n */\nexport interface BylineFieldDefinition {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired: boolean;\n\t/**\n\t * Whether values are stored per-locale (`true`, in\n\t * `_emdash_byline_field_values` keyed by `byline_id`) or shared across\n\t * every locale variant of the same byline identity (`false`, in\n\t * `_emdash_byline_field_group_values` keyed by `translation_group`).\n\t * Defaults to `true` at the DB level.\n\t */\n\ttranslatable: boolean;\n\tvalidation: BylineFieldValidation | null;\n\tsortOrder: number;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * Input for creating a byline custom field. `slug` and `type` are not\n * updatable post-create — changing either would invalidate stored values.\n */\nexport interface CreateBylineFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Input for updating a byline custom field. `slug` and `type` are\n * intentionally not present — see `CreateBylineFieldInput`.\n */\nexport interface UpdateBylineFieldInput {\n\tlabel?: string;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Runtime value type for a byline custom field. The narrow union mirrors\n * what the five v1 field types can produce: `string`/`text`/`url`/`select`\n * → string, `boolean` → boolean, plus `null` for cleared values.\n */\nexport type CustomFieldValue = string | boolean | null;\n\n/**\n * Reserved byline-field slugs. Two reasons a slug ends up here:\n *\n * 1. **Column collision.** Slugs that match a fixed column on\n * `_emdash_bylines` (migrations 031 + 040) would shadow that column\n * on hydration. The first 12 entries cover this.\n * 2. **Route collision.** Static file routes under\n * `/_emdash/api/admin/byline-fields/` take precedence over the\n * `[slug].ts` dynamic route in Astro, so a custom field whose slug\n * matches a sibling static file (e.g. `reorder.ts`) is unreachable\n * via single-field CRUD — the static route handles only its own\n * method (POST for `reorder`) and 405s everything else.\n * `reorder` is the only such sibling today; new sibling routes\n * (e.g. a hypothetical `import.ts`) must be added here.\n * `[slug]/usage.ts` lives a level deeper so a slug of `usage` does\n * not collide — it resolves cleanly to `[slug].ts`.\n *\n * Enforced at the registry layer (Phase 2) and the admin API zod layer\n * (Phase 4) so non-HTTP callers (seeds, scripts) get the same guarantee.\n */\nexport const RESERVED_BYLINE_FIELD_SLUGS = [\n\t// 1. Column-collision slugs (matches `_emdash_bylines` fixed columns).\n\t\"id\",\n\t\"slug\",\n\t\"display_name\",\n\t\"bio\",\n\t\"avatar_media_id\",\n\t\"website_url\",\n\t\"user_id\",\n\t\"is_guest\",\n\t\"locale\",\n\t\"translation_group\",\n\t\"created_at\",\n\t\"updated_at\",\n\t// 2. Route-collision slugs (matches static sibling files of `[slug].ts`).\n\t\"reorder\",\n] as const;\n"],"mappings":";;;;AA+BA,MAAa,cAAoC;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;AAUD,MAAa,uBAAsD;CAClE,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;CACR,aAAa;CACb,cAAc;CACd,OAAO;CACP,MAAM;CACN,WAAW;CACX,MAAM;CACN,MAAM;CACN,KAAK;CACL,UAAU;CACV;;;;;;;AAyND,MAAa,uBAAuB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;;;;AAKD,MAAa,4BAA4B;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAsBD,MAAa,qBAAiD;CAC7D;CACA;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;AAyFD,MAAa,8BAA8B;CAE1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA"}
|
|
1
|
+
{"version":3,"file":"types-tM44hEcf.mjs","names":[],"sources":["../src/schema/types.ts"],"sourcesContent":["/**\n * Schema Registry Types\n *\n * These types represent the schema definitions stored in D1.\n * They are the source of truth for all collections and fields.\n */\n\n/**\n * Supported field types\n */\nexport type FieldType =\n\t| \"string\"\n\t| \"text\"\n\t| \"url\"\n\t| \"number\"\n\t| \"integer\"\n\t| \"boolean\"\n\t| \"datetime\"\n\t| \"select\"\n\t| \"multiSelect\"\n\t| \"portableText\"\n\t| \"image\"\n\t| \"file\"\n\t| \"reference\"\n\t| \"json\"\n\t| \"slug\"\n\t| \"repeater\";\n\n/**\n * Array of all field types for validation\n */\nexport const FIELD_TYPES: readonly FieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"multiSelect\",\n\t\"portableText\",\n\t\"image\",\n\t\"file\",\n\t\"reference\",\n\t\"json\",\n\t\"slug\",\n\t\"repeater\",\n] as const;\n\n/**\n * SQLite column types that map from field types\n */\nexport type ColumnType = \"TEXT\" | \"REAL\" | \"INTEGER\" | \"JSON\";\n\n/**\n * Map field types to their SQLite column types\n */\nexport const FIELD_TYPE_TO_COLUMN: Record<FieldType, ColumnType> = {\n\tstring: \"TEXT\",\n\ttext: \"TEXT\",\n\tnumber: \"REAL\",\n\tinteger: \"INTEGER\",\n\tboolean: \"INTEGER\",\n\tdatetime: \"TEXT\",\n\tselect: \"TEXT\",\n\tmultiSelect: \"JSON\",\n\tportableText: \"JSON\",\n\timage: \"TEXT\",\n\tfile: \"TEXT\",\n\treference: \"TEXT\",\n\tjson: \"JSON\",\n\tslug: \"TEXT\",\n\turl: \"TEXT\",\n\trepeater: \"JSON\",\n};\n\n/**\n * Features a collection can support\n */\nexport type CollectionSupport =\n\t| \"drafts\"\n\t| \"revisions\"\n\t| \"preview\"\n\t| \"scheduling\"\n\t| \"search\"\n\t| \"seo\";\n\n/**\n * Sources for how a collection was created\n */\nexport type CollectionSource =\n\t| `template:${string}`\n\t| `import:${string}`\n\t| \"manual\"\n\t| \"discovered\"\n\t| \"seed\";\n\n/**\n * Validation rules for a field\n */\n/** Sub-field definition for repeater fields */\nexport interface RepeaterSubField {\n\tslug: string;\n\ttype:\n\t\t| \"string\"\n\t\t| \"text\"\n\t\t| \"url\"\n\t\t| \"number\"\n\t\t| \"integer\"\n\t\t| \"boolean\"\n\t\t| \"datetime\"\n\t\t| \"select\"\n\t\t| \"image\";\n\tlabel: string;\n\trequired?: boolean;\n\toptions?: string[]; // For select sub-fields\n}\n\n/** Allowed types for repeater sub-fields (no nesting, no complex types) */\nexport const REPEATER_SUB_FIELD_TYPES = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"number\",\n\t\"integer\",\n\t\"boolean\",\n\t\"datetime\",\n\t\"select\",\n\t\"image\",\n] as const;\n\nexport interface FieldValidation {\n\trequired?: boolean;\n\tmin?: number;\n\tmax?: number;\n\tminLength?: number;\n\tmaxLength?: number;\n\tpattern?: string;\n\toptions?: string[]; // For select/multiSelect\n\tsubFields?: RepeaterSubField[]; // For repeater fields\n\tminItems?: number; // For repeater fields\n\tmaxItems?: number; // For repeater fields\n\tallowedMimeTypes?: string[];\n}\n\n/**\n * Widget options for field rendering\n */\nexport interface FieldWidgetOptions {\n\trows?: number; // For textarea\n\tshowPreview?: boolean; // For image/file\n\tcollection?: string; // For reference - which collection to reference\n\tallowMultiple?: boolean; // For reference\n\t[key: string]: unknown;\n}\n\n/**\n * A collection definition\n */\nexport interface Collection {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports: CollectionSupport[];\n\tsource?: CollectionSource;\n\t/** Whether this collection has SEO metadata fields enabled */\n\thasSeo: boolean;\n\t/** URL pattern with {slug} placeholder (e.g. \"/{slug}\", \"/blog/{slug}\") */\n\turlPattern?: string;\n\t/** Whether comments are enabled for this collection */\n\tcommentsEnabled: boolean;\n\t/** Moderation strategy: \"all\" | \"first_time\" | \"none\" */\n\tcommentsModeration: \"all\" | \"first_time\" | \"none\";\n\t/** Auto-close comments after N days. 0 = never close. */\n\tcommentsClosedAfterDays: number;\n\t/** Auto-approve comments from authenticated CMS users */\n\tcommentsAutoApproveUsers: boolean;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * A field definition\n */\nexport interface Field {\n\tid: string;\n\tcollectionId: string;\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\tcolumnType: ColumnType;\n\trequired: boolean;\n\tunique: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder: number;\n\tsearchable: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable: boolean;\n\tcreatedAt: string;\n}\n\n/**\n * Input for creating a collection\n */\nexport interface CreateCollectionInput {\n\tslug: string;\n\tlabel: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\tsource?: CollectionSource;\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n}\n\n/**\n * Input for updating a collection\n */\nexport interface UpdateCollectionInput {\n\tlabel?: string;\n\tlabelSingular?: string;\n\tdescription?: string;\n\ticon?: string;\n\tsupports?: CollectionSupport[];\n\turlPattern?: string;\n\thasSeo?: boolean;\n\tcommentsEnabled?: boolean;\n\tcommentsModeration?: \"all\" | \"first_time\" | \"none\";\n\tcommentsClosedAfterDays?: number;\n\tcommentsAutoApproveUsers?: boolean;\n}\n\n/**\n * Input for creating a field\n */\nexport interface CreateFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: FieldType;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * Input for updating a field\n */\nexport interface UpdateFieldInput {\n\tlabel?: string;\n\t/**\n\t * Change the field's type. Only type changes that keep the same underlying\n\t * column type (per `FIELD_TYPE_TO_COLUMN`) are allowed — e.g. `string` to\n\t * `slug` (both TEXT). A change that would alter the column affinity (e.g.\n\t * `text` TEXT to `portableText` JSON) is rejected, because there is no\n\t * in-place column migration and silently rewriting the metadata would\n\t * desync `column_type` from the real `ec_*` column. Omit to keep the\n\t * current type.\n\t */\n\ttype?: FieldType;\n\trequired?: boolean;\n\tunique?: boolean;\n\tdefaultValue?: unknown;\n\tvalidation?: FieldValidation | null;\n\twidget?: string;\n\toptions?: FieldWidgetOptions;\n\tsortOrder?: number;\n\t/** Whether this field should be indexed for search */\n\tsearchable?: boolean;\n\t/** Whether this field is translatable (default true). Non-translatable fields are synced across locales. */\n\ttranslatable?: boolean;\n}\n\n/**\n * A collection with its fields\n */\nexport interface CollectionWithFields extends Collection {\n\tfields: Field[];\n}\n\n/**\n * Reserved field slugs that cannot be used.\n *\n * Includes names reserved for runtime hydration (`terms`, `bylines`, `byline`)\n * so user-defined fields never shadow the auto-hydrated values on entry.data.\n */\nexport const RESERVED_FIELD_SLUGS = [\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t// Runtime-hydrated fields\n\t\"terms\",\n\t\"bylines\",\n\t\"byline\",\n];\n\n/**\n * Reserved collection slugs that cannot be used\n */\nexport const RESERVED_COLLECTION_SLUGS = [\n\t\"content\",\n\t\"media\",\n\t\"users\",\n\t\"revisions\",\n\t\"taxonomies\",\n\t\"options\",\n\t\"audit_logs\",\n];\n\n/**\n * Byline custom fields (Discussion #1174).\n *\n * Sites declare site-specific byline metadata (`job_title`, `pronouns`,\n * `twitter_handle`, `company`, …) without touching emdash core. Definitions\n * live in `_emdash_byline_fields`; values in either\n * `_emdash_byline_field_values` (translatable, keyed by `byline_id`) or\n * `_emdash_byline_field_group_values` (non-translatable, keyed by\n * `translation_group`). The per-field `translatable` flag decides which\n * value table is used. See migration 041.\n */\n\n/**\n * The five v1 field types supported on byline custom fields. Deliberately\n * narrower than the content `FieldType` union — bylines don't need\n * `portableText`, `reference`, `image`, etc. v2 may extend this; v1 keeps\n * the storage and UI surfaces small.\n */\nexport type BylineFieldType = \"string\" | \"text\" | \"url\" | \"boolean\" | \"select\";\n\nexport const BYLINE_FIELD_TYPES: readonly BylineFieldType[] = [\n\t\"string\",\n\t\"text\",\n\t\"url\",\n\t\"boolean\",\n\t\"select\",\n] as const;\n\n/**\n * Validation rules for a byline custom field. v1 only exposes `options`\n * (the choice list for `select` fields). The shape mirrors the content-\n * field convention so the admin UI patterns transfer.\n */\nexport interface BylineFieldValidation {\n\t/** Choices for `select`-type fields. Ignored for other types. */\n\toptions?: string[];\n}\n\n/**\n * Runtime shape of a registered byline custom field. Stored in\n * `_emdash_byline_fields` (see migration 041).\n */\nexport interface BylineFieldDefinition {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired: boolean;\n\t/**\n\t * Whether values are stored per-locale (`true`, in\n\t * `_emdash_byline_field_values` keyed by `byline_id`) or shared across\n\t * every locale variant of the same byline identity (`false`, in\n\t * `_emdash_byline_field_group_values` keyed by `translation_group`).\n\t * Defaults to `true` at the DB level.\n\t */\n\ttranslatable: boolean;\n\tvalidation: BylineFieldValidation | null;\n\tsortOrder: number;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/**\n * Input for creating a byline custom field. `slug` and `type` are not\n * updatable post-create — changing either would invalidate stored values.\n */\nexport interface CreateBylineFieldInput {\n\tslug: string;\n\tlabel: string;\n\ttype: BylineFieldType;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Input for updating a byline custom field. `slug` and `type` are\n * intentionally not present — see `CreateBylineFieldInput`.\n */\nexport interface UpdateBylineFieldInput {\n\tlabel?: string;\n\trequired?: boolean;\n\ttranslatable?: boolean;\n\tvalidation?: BylineFieldValidation | null;\n\tsortOrder?: number;\n}\n\n/**\n * Runtime value type for a byline custom field. The narrow union mirrors\n * what the five v1 field types can produce: `string`/`text`/`url`/`select`\n * → string, `boolean` → boolean, plus `null` for cleared values.\n */\nexport type CustomFieldValue = string | boolean | null;\n\n/**\n * Reserved byline-field slugs. Two reasons a slug ends up here:\n *\n * 1. **Column collision.** Slugs that match a fixed column on\n * `_emdash_bylines` (migrations 031 + 040) would shadow that column\n * on hydration. The first 12 entries cover this.\n * 2. **Route collision.** Static file routes under\n * `/_emdash/api/admin/byline-fields/` take precedence over the\n * `[slug].ts` dynamic route in Astro, so a custom field whose slug\n * matches a sibling static file (e.g. `reorder.ts`) is unreachable\n * via single-field CRUD — the static route handles only its own\n * method (POST for `reorder`) and 405s everything else.\n * `reorder` is the only such sibling today; new sibling routes\n * (e.g. a hypothetical `import.ts`) must be added here.\n * `[slug]/usage.ts` lives a level deeper so a slug of `usage` does\n * not collide — it resolves cleanly to `[slug].ts`.\n *\n * Enforced at the registry layer (Phase 2) and the admin API zod layer\n * (Phase 4) so non-HTTP callers (seeds, scripts) get the same guarantee.\n */\nexport const RESERVED_BYLINE_FIELD_SLUGS = [\n\t// 1. Column-collision slugs (matches `_emdash_bylines` fixed columns).\n\t\"id\",\n\t\"slug\",\n\t\"display_name\",\n\t\"bio\",\n\t\"avatar_media_id\",\n\t\"website_url\",\n\t\"user_id\",\n\t\"is_guest\",\n\t\"locale\",\n\t\"translation_group\",\n\t\"created_at\",\n\t\"updated_at\",\n\t// 2. Route-collision slugs (matches static sibling files of `[slug].ts`).\n\t\"reorder\",\n] as const;\n"],"mappings":";;;;AA+BA,MAAa,cAAoC;CAChD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;;;;AAUD,MAAa,uBAAsD;CAClE,QAAQ;CACR,MAAM;CACN,QAAQ;CACR,SAAS;CACT,SAAS;CACT,UAAU;CACV,QAAQ;CACR,aAAa;CACb,cAAc;CACd,OAAO;CACP,MAAM;CACN,WAAW;CACX,MAAM;CACN,MAAM;CACN,KAAK;CACL,UAAU;CACV;;;;;;;AAmOD,MAAa,uBAAuB;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;;;;AAKD,MAAa,4BAA4B;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AAsBD,MAAa,qBAAiD;CAC7D;CACA;CACA;CACA;CACA;CACA;;;;;;;;;;;;;;;;;;;;;AAyFD,MAAa,8BAA8B;CAE1C;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { d as FieldType } from "./types-
|
|
1
|
+
import { d as FieldType } from "./types-CNlaBFzx.mjs";
|
|
2
2
|
import { t as Database } from "./types-B-gIxFCE.mjs";
|
|
3
3
|
import { i as SiteSettings } from "./types-BG8_XTLj.mjs";
|
|
4
4
|
import { d as Storage } from "./types-DrYMm9s5.mjs";
|
|
@@ -374,4 +374,4 @@ declare function loadUserSeed(): Promise<SeedFile | null>;
|
|
|
374
374
|
declare function validateSeed(data: unknown): ValidationResult;
|
|
375
375
|
//#endregion
|
|
376
376
|
export { SeedTaxonomyTerm as _, applySeed as a, ValidationResult as b, SeedCollection as c, SeedFile as d, SeedMenu as f, SeedTaxonomy as g, SeedSection as h, defaultSeed as i, SeedContentEntry as l, SeedRedirect as m, loadSeed as n, SeedApplyOptions as o, SeedMenuItem as p, loadUserSeed as r, SeedApplyResult as s, validateSeed as t, SeedField as u, SeedWidget as v, SeedWidgetArea as y };
|
|
377
|
-
//# sourceMappingURL=validate-
|
|
377
|
+
//# sourceMappingURL=validate-Va_An3qb.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate-
|
|
1
|
+
{"version":3,"file":"validate-Va_An3qb.d.mts","names":[],"sources":["../src/seed/types.ts","../src/seed/apply.ts","../src/seed/default.ts","../src/seed/load.ts","../src/seed/validate.ts"],"mappings":";;;;;;;;;;UAciB,QAAA;EA8BH;EA5Bb,OAAA;EAkCY;EA/BZ,OAAA;EAqCW;;;;;;;EA5BX,aAAA;EAAA;EAGA,IAAA;IACC,IAAA;IACA,WAAA;IACA,MAAA;EAAA;EAIU;EAAX,QAAA,GAAW,OAAA,CAAQ,YAAA;EAGnB;EAAA,WAAA,GAAc,cAAA;EAGd;EAAA,UAAA,GAAa,YAAA;EAGb;EAAA,KAAA,GAAQ,QAAA;EAGR;EAAA,SAAA,GAAY,YAAA;EAGZ;EAAA,WAAA,GAAc,cAAA;EAGd;EAAA,QAAA,GAAW,WAAA;EAGX;EAAA,OAAA,GAAU,UAAA;EAGV;EAAA,OAAA,GAAU,MAAA,SAAe,gBAAA;AAAA;;;AAM1B;UAAiB,cAAA;EAChB,IAAA;EACA,KAAA;EACA,aAAA;EACA,WAAA;EACA,IAAA;EACA,QAAA;EACA,UAAA;EADA;EAGA,eAAA;EACA,MAAA,EAAQ,SAAA;AAAA;;;;UAMQ,SAAA;EAChB,IAAA;EACA,KAAA;EACA,IAAA,EAAM,SAAA;EACN,QAAA;EACA,MAAA;EACA,UAAA;EACA,YAAA;EACA,UAAA,GAAa,MAAA;EACb,MAAA;EACA,OAAA,GAAU,MAAA;AAAA;;;;;UAOM,YAAA;EATH;EAWb,EAAA;EACA,IAAA;EACA,KAAA;EACA,aAAA;EACA,YAAA;EACA,WAAA;EACA,MAAA;EACA,aAAA;EACA,KAAA,GAAQ,gBAAA;AAAA;;;;UAMQ,gBAAA;EAThB;EAWA,EAAA;EACA,IAAA;EACA,KAAA;EACA,WAAA;EACA,MAAA;EACA,MAAA;EACA,aAAA;AAAA;;;;UAMgB,QAAA;EAVhB;EAYA,EAAA;EACA,IAAA;EACA,KAAA;EACA,MAAA;EACA,aAAA;EACA,KAAA,EAAO,YAAA;AAAA;;;;UAMS,YAAA;EAVhB;EAYA,EAAA;EACA,IAAA;EACA,KAAA;EACA,GAAA;EACA,GAAA;EACA,UAAA;EACA,MAAA;EACA,SAAA;EACA,UAAA;EACA,MAAA;EACA,aAAA;EACA,QAAA,GAAW,YAAA;AAAA;;;;UAMK,YAAA;EAChB,MAAA;EACA,WAAA;EACA,IAAA;EACA,OAAA;EACA,SAAA;AAAA;;;;UAMgB,cAAA;EAChB,IAAA;EACA,KAAA;EACA,WAAA;EACA,OAAA,EAAS,UAAA;AAAA;;;;UAMO,UAAA;EAChB,IAAA;EACA,KAAA;EAGA,OAAA,GAAU,KAAA;IAAQ,KAAA;IAAe,IAAA;IAAA,CAAgB,GAAA;EAAA;EAGjD,QAAA;EAGA,WAAA;EACA,KAAA,GAAQ,MAAA;AAAA;;AAZT;;UAkBiB,WAAA;EAChB,IAAA;EACA,KAAA;EACA,WAAA;EAhBA;EAkBA,QAAA;EAlBkB;EAoBlB,OAAA,EAAS,KAAA;IAAQ,KAAA;IAAe,IAAA;IAAA,CAAgB,GAAA;EAAA;EAbxC;EAeR,MAAA;AAAA;AATD;;;AAAA,UAeiB,UAAA;EAdhB;EAgBA,EAAA;EACA,IAAA;EACA,WAAA;EACA,GAAA;EACA,UAAA;EACA,OAAA;EAfgC;;;;;AAQjC;;EAeC,MAAA,GAAS,gBAAA;AAAA;AAAA,UAGO,gBAAA;EAfhB;EAiBA,UAAA;EAfA;EAiBA,GAAA;EAfA;EAiBA,QAAA;EATS;EAWT,QAAA;EACA,KAAA;EACA,MAAA;AAAA;;;;UAMgB,gBAAA;EAVhB;EAYA,EAAA;EATA;EAYA,IAAA;EAXM;EAcN,MAAA;EARgB;EAWhB,IAAA,EAAM,MAAA;;EAGN,UAAA,GAAa,MAAA;EAAA;EAGb,OAAA,GAAU,gBAAA;EAAgB;EAG1B,MAAA;EAlBA;;;;EAwBA,aAAA;AAAA;AAAA,UAGgB,gBAAA;EAZhB;EAcA,MAAA;EACA,SAAA;AAAA;;;AAHD;UASiB,gBAAA;;EAEhB,cAAA;EARS;EAWT,UAAA;EALgC;EAQhC,aAAA;EAMiB;;;;EAAjB,OAAA,GAAU,OAAA;EAAA;;;;AAkBX;;;;;;EANC,iBAAA;AAAA;;;;UAMgB,eAAA;EAChB,WAAA;IAAe,OAAA;IAAiB,OAAA;IAAiB,OAAA;EAAA;EACjD,MAAA;IAAU,OAAA;IAAiB,OAAA;IAAiB,OAAA;EAAA;EAC5C,UAAA;IAAc,OAAA;IAAiB,KAAA;EAAA;EAC/B,OAAA;IAAW,OAAA;IAAiB,OAAA;IAAiB,OAAA;EAAA;EAC7C,KAAA;IAAS,OAAA;IAAiB,KAAA;EAAA;EAC1B,SAAA;IAAa,OAAA;IAAiB,OAAA;IAAiB,OAAA;EAAA;EAC/C,WAAA;IAAe,OAAA;IAAiB,OAAA;EAAA;EAChC,QAAA;IAAY,OAAA;IAAiB,OAAA;IAAiB,OAAA;EAAA;EAC9C,QAAA;IAAY,OAAA;EAAA;EACZ,OAAA;IAAW,OAAA;IAAiB,OAAA;IAAiB,OAAA;EAAA;EAC7C,KAAA;IAAS,OAAA;IAAiB,OAAA;EAAA;AAAA;;;;UAMV,gBAAA;EAChB,KAAA;EACA,MAAA;EACA,QAAA;AAAA;;;;;;;;;;;;;iBCtRqB,SAAA,CACrB,EAAA,EAAI,MAAA,CAAO,QAAA,GACX,IAAA,EAAM,QAAA,EACN,OAAA,GAAS,gBAAA,GACP,OAAA,CAAQ,eAAA;;;cC1DE,WAAA,EAAa,QAAA;;;;;;iBCcJ,QAAA,CAAA,GAAY,OAAA,CAAQ,QAAA;;;;iBAQpB,YAAA,CAAA,GAAgB,OAAA,CAAQ,QAAA;;;;AHjB9C;;;;;iBIuBgB,YAAA,CAAa,IAAA,YAAgB,gBAAA"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
2
2
|
import "./dialect-helpers-DRI5pyY3.mjs";
|
|
3
3
|
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-BHgcHh2C.mjs";
|
|
4
|
-
import { n as isMissingTableError } from "./db-errors-
|
|
4
|
+
import { n as isMissingTableError } from "./db-errors-CK46D-ly.mjs";
|
|
5
5
|
import "./fts-manager-DW_NR-Fs.mjs";
|
|
6
|
-
import { n as SchemaRegistry } from "./registry-
|
|
6
|
+
import { n as SchemaRegistry } from "./registry-3aOAS4My.mjs";
|
|
7
7
|
import { n as generateZodSchema } from "./zod-generator-xEHODMJf.mjs";
|
|
8
8
|
import { sql } from "kysely";
|
|
9
9
|
|
|
@@ -142,4 +142,4 @@ async function validateContentData(db, collection, data, options = {}) {
|
|
|
142
142
|
|
|
143
143
|
//#endregion
|
|
144
144
|
export { validateContentData };
|
|
145
|
-
//# sourceMappingURL=validation-
|
|
145
|
+
//# sourceMappingURL=validation-B6DKPTtU.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation-CY1gk4Ar.mjs","names":[],"sources":["../src/api/handlers/validation.ts"],"sourcesContent":["/**\n * Field-level validation for content create / update.\n *\n * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)\n * into the handler boundary so REST and MCP both get the same enforcement:\n *\n * - required fields must be present and non-empty\n * - select / multiSelect values must match the configured options\n * - reference fields must resolve to a real, non-trashed target\n *\n * Errors surface as `{ code: \"VALIDATION_ERROR\", message }` with all\n * offending fields listed in one message so callers can fix everything in\n * a single round trip.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { Field } from \"../../schema/types.js\";\nimport { generateZodSchema } from \"../../schema/zod-generator.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\n\ntype ValidationResult =\n\t| { ok: true }\n\t| { ok: false; error: { code: \"VALIDATION_ERROR\" | \"COLLECTION_NOT_FOUND\"; message: string } };\n\n/** Treat `undefined`, `null`, and `\"\"` as \"not set\". */\nfunction isMissing(value: unknown): boolean {\n\treturn value === undefined || value === null || value === \"\";\n}\n\n/**\n * Resolve the target collection slug for a reference field.\n *\n * Schema-defined reference fields (the static `reference()` factory in\n * `fields/reference.ts`) put the target in `options.collection`. The MCP\n * `schema_create_field` tool also puts it there. Tests and some admin paths\n * stash it inside `validation.collection` directly; we accept both.\n */\nfunction getReferenceTargetCollection(field: Field): string | undefined {\n\tconst fromOptions = field.options?.collection;\n\tif (typeof fromOptions === \"string\" && fromOptions.length > 0) return fromOptions;\n\tconst validation = field.validation;\n\tif (validation && \"collection\" in validation) {\n\t\tconst fromValidation: unknown = (validation as { collection?: unknown }).collection;\n\t\tif (typeof fromValidation === \"string\" && fromValidation.length > 0) return fromValidation;\n\t}\n\treturn undefined;\n}\n\n/**\n * Format a Zod issue path into a human-readable field reference, e.g.\n * `tags`, `tags.1`, `image.alt`.\n */\nfunction formatIssuePath(path: ReadonlyArray<PropertyKey>): string {\n\tif (path.length === 0) return \"(root)\";\n\treturn path.map((seg) => String(seg)).join(\".\");\n}\n\n/**\n * Validate `data` against the collection's field definitions.\n *\n * `partial: true` switches Zod into partial mode so updates can include\n * only the fields being changed without tripping required-field errors on\n * fields the caller didn't touch. Required fields that ARE present in\n * partial-mode data still get the empty-string check below.\n */\nexport async function validateContentData(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n\toptions: { partial?: boolean } = {},\n): Promise<ValidationResult> {\n\tconst registry = new SchemaRegistry(db);\n\tconst collectionWithFields = await registry.getCollectionWithFields(collection);\n\tif (!collectionWithFields) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst issues: string[] = [];\n\n\t// Detect unknown keys explicitly so callers get a useful error rather\n\t// than silently dropped data. Leading-underscore keys (e.g. `_slug`,\n\t// `_rev`) are reserved for internal handler/runtime use and aren't real\n\t// fields; skip them.\n\tconst knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));\n\tfor (const key of Object.keys(data)) {\n\t\tif (key.startsWith(\"_\")) continue;\n\t\tif (!knownFields.has(key)) {\n\t\t\tissues.push(`${key}: unknown field on collection '${collection}'`);\n\t\t}\n\t}\n\n\t// Zod handles type, enum, length and missing-required (in non-partial\n\t// mode) checks. Empty-string handling for required string fields is\n\t// done as a separate pass below since Zod's `z.string()` accepts \"\".\n\tconst baseSchema = generateZodSchema(collectionWithFields);\n\tconst schema = options.partial ? baseSchema.partial() : baseSchema;\n\tconst parsed = schema.safeParse(data);\n\tif (!parsed.success) {\n\t\tfor (const issue of parsed.error.issues) {\n\t\t\tissues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);\n\t\t}\n\t}\n\n\t// Empty-string-on-required check. In create mode (partial=false) Zod\n\t// already catches missing/null for required fields, but `z.string()`\n\t// happily accepts \"\". In update mode (partial=true) the field is only\n\t// checked if it's present in `data`.\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (!field.required) continue;\n\t\tconst present = Object.hasOwn(data, field.slug);\n\t\tif (options.partial && !present) continue;\n\t\tif (data[field.slug] === \"\") {\n\t\t\tissues.push(`${field.slug}: required (empty value not allowed)`);\n\t\t}\n\t}\n\n\t// Reference target existence. Only check fields that:\n\t// - have a value (non-missing) in `data`\n\t// - have a resolvable target collection\n\t// - in partial mode: are present in `data`\n\t// Batch one IN-query per target collection to keep round-trips low.\n\tconst refsByTarget = new Map<string, { field: string; id: string }[]>();\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (field.type !== \"reference\") continue;\n\t\tif (options.partial && !Object.hasOwn(data, field.slug)) continue;\n\t\tconst value = data[field.slug];\n\t\tif (isMissing(value)) continue;\n\t\tif (typeof value !== \"string\") continue; // Zod will have flagged this already\n\t\tconst target = getReferenceTargetCollection(field);\n\t\tif (!target) continue;\n\t\tconst list = refsByTarget.get(target) ?? [];\n\t\tlist.push({ field: field.slug, id: value });\n\t\trefsByTarget.set(target, list);\n\t}\n\n\tfor (const [target, refs] of refsByTarget) {\n\t\t// Validate the target collection slug before interpolating into raw\n\t\t// SQL — defense-in-depth even though slugs are already validated at\n\t\t// schema-create time.\n\t\ttry {\n\t\t\tvalidateIdentifier(target, \"reference target collection\");\n\t\t} catch {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: invalid reference target collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst ids = [...new Set(refs.map((r) => r.id))];\n\t\tconst tableName = `ec_${target}`;\n\n\t\t// Chunk the IN clause to stay below D1's bind-parameter limit. One\n\t\t// reference per request is the common case today; chunking makes the\n\t\t// helper safe if a future multiSelect-of-references is added.\n\t\tconst found = new Set<string>();\n\t\tlet targetTableMissing = false;\n\t\tfor (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{ id: string }>`\n\t\t\t\t\tSELECT id FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(idChunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(db);\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tfound.add(row.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Missing table = the target collection table doesn't exist\n\t\t\t\t// (orphan reference). Treat all those references as missing.\n\t\t\t\t// Any other DB error (permissions, connection, syntax) must\n\t\t\t\t// propagate — silently dropping data integrity errors as\n\t\t\t\t// \"not found\" is exactly the bug F5 fixes.\n\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\ttargetTableMissing = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\tif (targetTableMissing) {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const ref of refs) {\n\t\t\tif (!found.has(ref.id)) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length === 0) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\terror: {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: issues.join(\"; \"),\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"validation-B6DKPTtU.mjs","names":[],"sources":["../src/api/handlers/validation.ts"],"sourcesContent":["/**\n * Field-level validation for content create / update.\n *\n * Wires the existing `generateZodSchema()` pipeline (`schema/zod-generator.ts`)\n * into the handler boundary so REST and MCP both get the same enforcement:\n *\n * - required fields must be present and non-empty\n * - select / multiSelect values must match the configured options\n * - reference fields must resolve to a real, non-trashed target\n *\n * Errors surface as `{ code: \"VALIDATION_ERROR\", message }` with all\n * offending fields listed in one message so callers can fix everything in\n * a single round trip.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport { SchemaRegistry } from \"../../schema/registry.js\";\nimport type { Field } from \"../../schema/types.js\";\nimport { generateZodSchema } from \"../../schema/zod-generator.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\n\ntype ValidationResult =\n\t| { ok: true }\n\t| { ok: false; error: { code: \"VALIDATION_ERROR\" | \"COLLECTION_NOT_FOUND\"; message: string } };\n\n/** Treat `undefined`, `null`, and `\"\"` as \"not set\". */\nfunction isMissing(value: unknown): boolean {\n\treturn value === undefined || value === null || value === \"\";\n}\n\n/**\n * Resolve the target collection slug for a reference field.\n *\n * Schema-defined reference fields (the static `reference()` factory in\n * `fields/reference.ts`) put the target in `options.collection`. The MCP\n * `schema_create_field` tool also puts it there. Tests and some admin paths\n * stash it inside `validation.collection` directly; we accept both.\n */\nfunction getReferenceTargetCollection(field: Field): string | undefined {\n\tconst fromOptions = field.options?.collection;\n\tif (typeof fromOptions === \"string\" && fromOptions.length > 0) return fromOptions;\n\tconst validation = field.validation;\n\tif (validation && \"collection\" in validation) {\n\t\tconst fromValidation: unknown = (validation as { collection?: unknown }).collection;\n\t\tif (typeof fromValidation === \"string\" && fromValidation.length > 0) return fromValidation;\n\t}\n\treturn undefined;\n}\n\n/**\n * Format a Zod issue path into a human-readable field reference, e.g.\n * `tags`, `tags.1`, `image.alt`.\n */\nfunction formatIssuePath(path: ReadonlyArray<PropertyKey>): string {\n\tif (path.length === 0) return \"(root)\";\n\treturn path.map((seg) => String(seg)).join(\".\");\n}\n\n/**\n * Validate `data` against the collection's field definitions.\n *\n * `partial: true` switches Zod into partial mode so updates can include\n * only the fields being changed without tripping required-field errors on\n * fields the caller didn't touch. Required fields that ARE present in\n * partial-mode data still get the empty-string check below.\n */\nexport async function validateContentData(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tdata: Record<string, unknown>,\n\toptions: { partial?: boolean } = {},\n): Promise<ValidationResult> {\n\tconst registry = new SchemaRegistry(db);\n\tconst collectionWithFields = await registry.getCollectionWithFields(collection);\n\tif (!collectionWithFields) {\n\t\treturn {\n\t\t\tok: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COLLECTION_NOT_FOUND\",\n\t\t\t\tmessage: `Collection '${collection}' not found`,\n\t\t\t},\n\t\t};\n\t}\n\n\tconst issues: string[] = [];\n\n\t// Detect unknown keys explicitly so callers get a useful error rather\n\t// than silently dropped data. Leading-underscore keys (e.g. `_slug`,\n\t// `_rev`) are reserved for internal handler/runtime use and aren't real\n\t// fields; skip them.\n\tconst knownFields = new Set(collectionWithFields.fields.map((f) => f.slug));\n\tfor (const key of Object.keys(data)) {\n\t\tif (key.startsWith(\"_\")) continue;\n\t\tif (!knownFields.has(key)) {\n\t\t\tissues.push(`${key}: unknown field on collection '${collection}'`);\n\t\t}\n\t}\n\n\t// Zod handles type, enum, length and missing-required (in non-partial\n\t// mode) checks. Empty-string handling for required string fields is\n\t// done as a separate pass below since Zod's `z.string()` accepts \"\".\n\tconst baseSchema = generateZodSchema(collectionWithFields);\n\tconst schema = options.partial ? baseSchema.partial() : baseSchema;\n\tconst parsed = schema.safeParse(data);\n\tif (!parsed.success) {\n\t\tfor (const issue of parsed.error.issues) {\n\t\t\tissues.push(`${formatIssuePath(issue.path)}: ${issue.message}`);\n\t\t}\n\t}\n\n\t// Empty-string-on-required check. In create mode (partial=false) Zod\n\t// already catches missing/null for required fields, but `z.string()`\n\t// happily accepts \"\". In update mode (partial=true) the field is only\n\t// checked if it's present in `data`.\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (!field.required) continue;\n\t\tconst present = Object.hasOwn(data, field.slug);\n\t\tif (options.partial && !present) continue;\n\t\tif (data[field.slug] === \"\") {\n\t\t\tissues.push(`${field.slug}: required (empty value not allowed)`);\n\t\t}\n\t}\n\n\t// Reference target existence. Only check fields that:\n\t// - have a value (non-missing) in `data`\n\t// - have a resolvable target collection\n\t// - in partial mode: are present in `data`\n\t// Batch one IN-query per target collection to keep round-trips low.\n\tconst refsByTarget = new Map<string, { field: string; id: string }[]>();\n\tfor (const field of collectionWithFields.fields) {\n\t\tif (field.type !== \"reference\") continue;\n\t\tif (options.partial && !Object.hasOwn(data, field.slug)) continue;\n\t\tconst value = data[field.slug];\n\t\tif (isMissing(value)) continue;\n\t\tif (typeof value !== \"string\") continue; // Zod will have flagged this already\n\t\tconst target = getReferenceTargetCollection(field);\n\t\tif (!target) continue;\n\t\tconst list = refsByTarget.get(target) ?? [];\n\t\tlist.push({ field: field.slug, id: value });\n\t\trefsByTarget.set(target, list);\n\t}\n\n\tfor (const [target, refs] of refsByTarget) {\n\t\t// Validate the target collection slug before interpolating into raw\n\t\t// SQL — defense-in-depth even though slugs are already validated at\n\t\t// schema-create time.\n\t\ttry {\n\t\t\tvalidateIdentifier(target, \"reference target collection\");\n\t\t} catch {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: invalid reference target collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst ids = [...new Set(refs.map((r) => r.id))];\n\t\tconst tableName = `ec_${target}`;\n\n\t\t// Chunk the IN clause to stay below D1's bind-parameter limit. One\n\t\t// reference per request is the common case today; chunking makes the\n\t\t// helper safe if a future multiSelect-of-references is added.\n\t\tconst found = new Set<string>();\n\t\tlet targetTableMissing = false;\n\t\tfor (const idChunk of chunks(ids, SQL_BATCH_SIZE)) {\n\t\t\ttry {\n\t\t\t\tconst rows = await sql<{ id: string }>`\n\t\t\t\t\tSELECT id FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(idChunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(db);\n\t\t\t\tfor (const row of rows.rows) {\n\t\t\t\t\tfound.add(row.id);\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\t// Missing table = the target collection table doesn't exist\n\t\t\t\t// (orphan reference). Treat all those references as missing.\n\t\t\t\t// Any other DB error (permissions, connection, syntax) must\n\t\t\t\t// propagate — silently dropping data integrity errors as\n\t\t\t\t// \"not found\" is exactly the bug F5 fixes.\n\t\t\t\tif (isMissingTableError(error)) {\n\t\t\t\t\ttargetTableMissing = true;\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\t\tif (targetTableMissing) {\n\t\t\tfor (const ref of refs) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\tfor (const ref of refs) {\n\t\t\tif (!found.has(ref.id)) {\n\t\t\t\tissues.push(`${ref.field}: target '${ref.id}' not found in collection '${target}'`);\n\t\t\t}\n\t\t}\n\t}\n\n\tif (issues.length === 0) return { ok: true };\n\treturn {\n\t\tok: false,\n\t\terror: {\n\t\t\tcode: \"VALIDATION_ERROR\",\n\t\t\tmessage: issues.join(\"; \"),\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-
|
|
1
|
+
{"version":3,"file":"version-DHdCA24u.mjs","names":[],"sources":["../src/version.ts"],"sourcesContent":["/**\n * Build-time version constants, replaced by tsdown/Vite `define`.\n * Falls back to \"dev\" when running uncompiled (tests, dev).\n */\n\ndeclare const __EMDASH_VERSION__: string;\ndeclare const __EMDASH_COMMIT__: string;\n\nexport const VERSION: string =\n\ttypeof __EMDASH_VERSION__ !== \"undefined\" ? __EMDASH_VERSION__ : \"dev\";\n\nexport const COMMIT: string = typeof __EMDASH_COMMIT__ !== \"undefined\" ? __EMDASH_COMMIT__ : \"dev\";\n"],"mappings":";AAQA,MAAa;AAGb,MAAa"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as requestCached } from "./request-cache-KCNHp_RJ.mjs";
|
|
2
|
-
import { a as getDb } from "./loader-
|
|
2
|
+
import { a as getDb } from "./loader-DrpofRon.mjs";
|
|
3
3
|
import { t as getWidgetComponents$1 } from "./components-CYt4uVK9.mjs";
|
|
4
4
|
|
|
5
5
|
//#region src/widgets/index.ts
|
|
@@ -106,4 +106,4 @@ function rowToWidget(row) {
|
|
|
106
106
|
|
|
107
107
|
//#endregion
|
|
108
108
|
export { rowToWidget as i, getWidgetAreas as n, getWidgetComponents as r, getWidgetArea as t };
|
|
109
|
-
//# sourceMappingURL=widgets-
|
|
109
|
+
//# sourceMappingURL=widgets-C2RbFN0G.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"widgets-
|
|
1
|
+
{"version":3,"file":"widgets-C2RbFN0G.mjs","names":["getComponentRegistry"],"sources":["../src/widgets/index.ts"],"sourcesContent":["import { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { getWidgetComponents as getComponentRegistry } from \"./components.js\";\nimport type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from \"./types.js\";\n\nexport type {\n\tWidget,\n\tWidgetArea,\n\tWidgetType,\n\tWidgetComponentDef,\n\tPropDef,\n\tCreateWidgetAreaInput,\n\tCreateWidgetInput,\n\tUpdateWidgetInput,\n\tReorderWidgetsInput,\n} from \"./types.js\";\n\n/**\n * Get a widget area by name, with all its widgets.\n *\n * Single query with a left join rather than area-then-widgets so the\n * common case costs one round-trip. An area with no widgets yields one\n * row with null widget columns, which we skip when mapping.\n */\nexport async function getWidgetArea(name: string): Promise<WidgetArea | null> {\n\treturn requestCached(`widget-area:${name}`, async () => {\n\t\tconst db = await getDb();\n\t\tconst rows = await db\n\t\t\t.selectFrom(\"_emdash_widget_areas as a\")\n\t\t\t.leftJoin(\"_emdash_widgets as w\", \"w.area_id\", \"a.id\")\n\t\t\t.select([\n\t\t\t\t\"a.id as a_id\",\n\t\t\t\t\"a.name as a_name\",\n\t\t\t\t\"a.label as a_label\",\n\t\t\t\t\"a.description as a_description\",\n\t\t\t\t\"w.id as w_id\",\n\t\t\t\t\"w.type as w_type\",\n\t\t\t\t\"w.title as w_title\",\n\t\t\t\t\"w.content as w_content\",\n\t\t\t\t\"w.menu_name as w_menu_name\",\n\t\t\t\t\"w.component_id as w_component_id\",\n\t\t\t\t\"w.component_props as w_component_props\",\n\t\t\t\t\"w.area_id as w_area_id\",\n\t\t\t\t\"w.sort_order as w_sort_order\",\n\t\t\t\t\"w.created_at as w_created_at\",\n\t\t\t])\n\t\t\t.where(\"a.name\", \"=\", name)\n\t\t\t.orderBy(\"w.sort_order\", \"asc\")\n\t\t\t.execute();\n\n\t\tconst first = rows[0];\n\t\tif (!first) return null;\n\t\tconst widgets: Widget[] = [];\n\t\tfor (const row of rows) {\n\t\t\tif (row.w_id === null) continue; // area has no widgets (left-join null row)\n\t\t\t// Left-join makes every w_* column nullable in the type; at runtime\n\t\t\t// they're all non-null once w_id is (we match on widgets.area_id, so\n\t\t\t// a widget row always has the not-null columns filled). Cast is the\n\t\t\t// price of that structural fact.\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- left-join row is non-null when w_id is set; see above\n\t\t\tconst widgetRow = {\n\t\t\t\tid: row.w_id,\n\t\t\t\ttype: row.w_type,\n\t\t\t\ttitle: row.w_title,\n\t\t\t\tcontent: row.w_content,\n\t\t\t\tmenu_name: row.w_menu_name,\n\t\t\t\tcomponent_id: row.w_component_id,\n\t\t\t\tcomponent_props: row.w_component_props,\n\t\t\t\tarea_id: row.w_area_id,\n\t\t\t\tsort_order: row.w_sort_order,\n\t\t\t\tcreated_at: row.w_created_at,\n\t\t\t} as WidgetRow;\n\t\t\twidgets.push(rowToWidget(widgetRow));\n\t\t}\n\n\t\treturn {\n\t\t\tid: first.a_id,\n\t\t\tname: first.a_name,\n\t\t\tlabel: first.a_label,\n\t\t\tdescription: first.a_description ?? undefined,\n\t\t\twidgets,\n\t\t};\n\t});\n}\n\n/**\n * Get all widget areas with their widgets\n */\nexport async function getWidgetAreas(): Promise<WidgetArea[]> {\n\tconst db = await getDb();\n\t// Get all areas\n\tconst areaRows = await db.selectFrom(\"_emdash_widget_areas\").selectAll().execute();\n\n\t// Get all widgets\n\tconst widgetRows = await db\n\t\t.selectFrom(\"_emdash_widgets\")\n\t\t.selectAll()\n\t\t.$castTo<WidgetRow>()\n\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t.execute();\n\n\t// Group widgets by area\n\tconst widgetsByArea = new Map<string, Widget[]>();\n\tfor (const row of widgetRows) {\n\t\tif (!widgetsByArea.has(row.area_id)) {\n\t\t\twidgetsByArea.set(row.area_id, []);\n\t\t}\n\t\twidgetsByArea.get(row.area_id)!.push(rowToWidget(row));\n\t}\n\n\t// Combine\n\treturn areaRows.map((areaRow) => ({\n\t\tid: areaRow.id,\n\t\tname: areaRow.name,\n\t\tlabel: areaRow.label,\n\t\tdescription: areaRow.description ?? undefined,\n\t\twidgets: widgetsByArea.get(areaRow.id) || [],\n\t}));\n}\n\n/**\n * Get available widget components (for admin UI)\n */\nexport function getWidgetComponents(): WidgetComponentDef[] {\n\treturn getComponentRegistry();\n}\n\n/**\n * Convert a widget row to the API type\n */\nexport function rowToWidget(row: WidgetRow): Widget {\n\tconst widget: Widget = {\n\t\tid: row.id,\n\t\ttype: row.type,\n\t\ttitle: row.title ?? undefined,\n\t};\n\n\t// Type-specific fields\n\tif (row.type === \"content\" && row.content) {\n\t\ttry {\n\t\t\twidget.content = JSON.parse(row.content);\n\t\t} catch {\n\t\t\t// Invalid JSON, ignore\n\t\t}\n\t}\n\n\tif (row.type === \"menu\" && row.menu_name) {\n\t\twidget.menuName = row.menu_name;\n\t}\n\n\tif (row.type === \"component\" && row.component_id) {\n\t\twidget.componentId = row.component_id;\n\t\tif (row.component_props) {\n\t\t\ttry {\n\t\t\t\twidget.componentProps = JSON.parse(row.component_props);\n\t\t\t} catch {\n\t\t\t\t// Invalid JSON, ignore\n\t\t\t}\n\t\t}\n\t}\n\n\treturn widget;\n}\n"],"mappings":";;;;;;;;;;;;AAwBA,eAAsB,cAAc,MAA0C;AAC7E,QAAO,cAAc,eAAe,QAAQ,YAAY;EAEvD,MAAM,OAAO,OADF,MAAM,OAAO,EAEtB,WAAW,4BAA4B,CACvC,SAAS,wBAAwB,aAAa,OAAO,CACrD,OAAO;GACP;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA;GACA,CAAC,CACD,MAAM,UAAU,KAAK,KAAK,CAC1B,QAAQ,gBAAgB,MAAM,CAC9B,SAAS;EAEX,MAAM,QAAQ,KAAK;AACnB,MAAI,CAAC,MAAO,QAAO;EACnB,MAAM,UAAoB,EAAE;AAC5B,OAAK,MAAM,OAAO,MAAM;AACvB,OAAI,IAAI,SAAS,KAAM;GAMvB,MAAM,YAAY;IACjB,IAAI,IAAI;IACR,MAAM,IAAI;IACV,OAAO,IAAI;IACX,SAAS,IAAI;IACb,WAAW,IAAI;IACf,cAAc,IAAI;IAClB,iBAAiB,IAAI;IACrB,SAAS,IAAI;IACb,YAAY,IAAI;IAChB,YAAY,IAAI;IAChB;AACD,WAAQ,KAAK,YAAY,UAAU,CAAC;;AAGrC,SAAO;GACN,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,OAAO,MAAM;GACb,aAAa,MAAM,iBAAiB;GACpC;GACA;GACA;;;;;AAMH,eAAsB,iBAAwC;CAC7D,MAAM,KAAK,MAAM,OAAO;CAExB,MAAM,WAAW,MAAM,GAAG,WAAW,uBAAuB,CAAC,WAAW,CAAC,SAAS;CAGlF,MAAM,aAAa,MAAM,GACvB,WAAW,kBAAkB,CAC7B,WAAW,CACX,SAAoB,CACpB,QAAQ,cAAc,MAAM,CAC5B,SAAS;CAGX,MAAM,gCAAgB,IAAI,KAAuB;AACjD,MAAK,MAAM,OAAO,YAAY;AAC7B,MAAI,CAAC,cAAc,IAAI,IAAI,QAAQ,CAClC,eAAc,IAAI,IAAI,SAAS,EAAE,CAAC;AAEnC,gBAAc,IAAI,IAAI,QAAQ,CAAE,KAAK,YAAY,IAAI,CAAC;;AAIvD,QAAO,SAAS,KAAK,aAAa;EACjC,IAAI,QAAQ;EACZ,MAAM,QAAQ;EACd,OAAO,QAAQ;EACf,aAAa,QAAQ,eAAe;EACpC,SAAS,cAAc,IAAI,QAAQ,GAAG,IAAI,EAAE;EAC5C,EAAE;;;;;AAMJ,SAAgB,sBAA4C;AAC3D,QAAOA,uBAAsB;;;;;AAM9B,SAAgB,YAAY,KAAwB;CACnD,MAAM,SAAiB;EACtB,IAAI,IAAI;EACR,MAAM,IAAI;EACV,OAAO,IAAI,SAAS;EACpB;AAGD,KAAI,IAAI,SAAS,aAAa,IAAI,QACjC,KAAI;AACH,SAAO,UAAU,KAAK,MAAM,IAAI,QAAQ;SACjC;AAKT,KAAI,IAAI,SAAS,UAAU,IAAI,UAC9B,QAAO,WAAW,IAAI;AAGvB,KAAI,IAAI,SAAS,eAAe,IAAI,cAAc;AACjD,SAAO,cAAc,IAAI;AACzB,MAAI,IAAI,gBACP,KAAI;AACH,UAAO,iBAAiB,KAAK,MAAM,IAAI,gBAAgB;UAChD;;AAMV,QAAO"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "emdash",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.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.
|
|
233
|
+
"@emdash-cms/admin": "0.25.1",
|
|
234
|
+
"@emdash-cms/auth": "0.25.1",
|
|
235
|
+
"@emdash-cms/gutenberg-to-portable-text": "0.25.1",
|
|
235
236
|
"@emdash-cms/plugin-types": "0.1.0",
|
|
236
|
-
"@emdash-cms/registry-client": "0.3.2"
|
|
237
|
-
"@emdash-cms/admin": "0.24.1"
|
|
237
|
+
"@emdash-cms/registry-client": "0.3.2"
|
|
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.25.1"
|
|
271
271
|
},
|
|
272
272
|
"repository": {
|
|
273
273
|
"type": "git",
|
package/src/api/errors.ts
CHANGED
|
@@ -72,6 +72,7 @@ export const ErrorCode = {
|
|
|
72
72
|
COLLECTION_NOT_FOUND: "COLLECTION_NOT_FOUND",
|
|
73
73
|
TABLE_NOT_FOUND: "TABLE_NOT_FOUND",
|
|
74
74
|
FIELD_EXISTS: "FIELD_EXISTS",
|
|
75
|
+
FIELD_TYPE_COLUMN_CHANGE: "FIELD_TYPE_COLUMN_CHANGE",
|
|
75
76
|
RESERVED_SLUG: "RESERVED_SLUG",
|
|
76
77
|
INVALID_SLUG: "INVALID_SLUG",
|
|
77
78
|
CREATE_FAILED: "CREATE_FAILED",
|
|
@@ -1087,15 +1087,15 @@ export async function handleContentRestore(
|
|
|
1087
1087
|
db: Kysely<Database>,
|
|
1088
1088
|
collection: string,
|
|
1089
1089
|
id: string,
|
|
1090
|
-
): Promise<ApiResult<{ restored: true }>> {
|
|
1090
|
+
): Promise<ApiResult<{ restored: true; item: ContentItem }>> {
|
|
1091
1091
|
try {
|
|
1092
|
-
const
|
|
1092
|
+
const item = await withTransaction(db, async (trx) => {
|
|
1093
1093
|
const repo = new ContentRepository(trx);
|
|
1094
1094
|
const resolvedId = (await resolveIdIncludingTrashed(repo, collection, id)) ?? id;
|
|
1095
1095
|
return repo.restore(collection, resolvedId);
|
|
1096
1096
|
});
|
|
1097
1097
|
|
|
1098
|
-
if (!
|
|
1098
|
+
if (!item) {
|
|
1099
1099
|
return {
|
|
1100
1100
|
success: false,
|
|
1101
1101
|
error: {
|
|
@@ -1107,7 +1107,7 @@ export async function handleContentRestore(
|
|
|
1107
1107
|
|
|
1108
1108
|
return {
|
|
1109
1109
|
success: true,
|
|
1110
|
-
data: { restored: true },
|
|
1110
|
+
data: { restored: true, item },
|
|
1111
1111
|
};
|
|
1112
1112
|
} catch (error) {
|
|
1113
1113
|
console.error("Content restore error:", error);
|
package/src/api/handlers/seo.ts
CHANGED
|
@@ -56,6 +56,34 @@ export interface SitemapDataResponse {
|
|
|
56
56
|
/** Maximum entries per sitemap (per spec) */
|
|
57
57
|
const SITEMAP_MAX_ENTRIES = 50_000;
|
|
58
58
|
|
|
59
|
+
/** Matches a trailing timezone designator (`Z` or `±HH`, `±HHMM`, `±HH:MM`). */
|
|
60
|
+
const TZ_SUFFIX_RE = /([zZ]|[+-]\d{2}(:?\d{2})?)$/;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Normalize a stored timestamp to W3C Datetime (ISO 8601) for sitemaps.
|
|
64
|
+
*
|
|
65
|
+
* `updated_at` is not guaranteed to be ISO: the column default is
|
|
66
|
+
* `datetime('now')` on SQLite and `CURRENT_TIMESTAMP` on Postgres, both of
|
|
67
|
+
* which store a space-separated `YYYY-MM-DD HH:MM:SS` string (and imported
|
|
68
|
+
* content can carry other shapes). The sitemap `<lastmod>` field requires
|
|
69
|
+
* W3C Datetime, and Google Search Console rejects the space-separated form
|
|
70
|
+
* as "Invalid date". Normalize defensively, assuming UTC when no offset is
|
|
71
|
+
* present (matches SQLite's `datetime('now')`). Valid date strings are
|
|
72
|
+
* normalized to UTC ISO 8601; unparseable values are returned as-is.
|
|
73
|
+
*/
|
|
74
|
+
function toW3CDate(value: string): string {
|
|
75
|
+
if (!value) return value;
|
|
76
|
+
let normalized = value.trim();
|
|
77
|
+
if (normalized.includes(" ") && !normalized.includes("T")) {
|
|
78
|
+
normalized = normalized.replace(" ", "T");
|
|
79
|
+
}
|
|
80
|
+
if (!TZ_SUFFIX_RE.test(normalized)) {
|
|
81
|
+
normalized += "Z";
|
|
82
|
+
}
|
|
83
|
+
const parsed = Date.parse(normalized);
|
|
84
|
+
return Number.isNaN(parsed) ? value : new Date(parsed).toISOString();
|
|
85
|
+
}
|
|
86
|
+
|
|
59
87
|
/**
|
|
60
88
|
* Collect all published, indexable content across SEO-enabled collections
|
|
61
89
|
* for sitemap generation, grouped by collection.
|
|
@@ -133,7 +161,7 @@ export async function handleSitemapData(
|
|
|
133
161
|
entries.push({
|
|
134
162
|
id: row.id,
|
|
135
163
|
slug: row.slug,
|
|
136
|
-
updatedAt: row.updated_at,
|
|
164
|
+
updatedAt: toW3CDate(row.updated_at),
|
|
137
165
|
locale: row.locale,
|
|
138
166
|
translationGroup: row.translation_group,
|
|
139
167
|
image: row.seo_image ?? null,
|
|
@@ -144,7 +172,7 @@ export async function handleSitemapData(
|
|
|
144
172
|
collection: col.slug,
|
|
145
173
|
urlPattern: col.url_pattern,
|
|
146
174
|
// Rows are ordered by updated_at DESC, so first row is the latest
|
|
147
|
-
lastmod: rows.rows[0].updated_at,
|
|
175
|
+
lastmod: toW3CDate(rows.rows[0].updated_at),
|
|
148
176
|
entries,
|
|
149
177
|
});
|
|
150
178
|
} catch (err) {
|
|
@@ -125,6 +125,7 @@ export const createFieldBody = z
|
|
|
125
125
|
export const updateFieldBody = z
|
|
126
126
|
.object({
|
|
127
127
|
label: z.string().min(1).optional(),
|
|
128
|
+
type: fieldTypeValues.optional(),
|
|
128
129
|
required: z.boolean().optional(),
|
|
129
130
|
unique: z.boolean().optional(),
|
|
130
131
|
defaultValue: z.unknown().optional(),
|
package/src/astro/middleware.ts
CHANGED
|
@@ -490,6 +490,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
490
490
|
const sessionUser =
|
|
491
491
|
context.isPrerendered || !hasSessionCookie ? null : await resolveSessionUser(context.session);
|
|
492
492
|
|
|
493
|
+
// Credentialed API requests (API tokens `ec_pat_*`, OAuth tokens
|
|
494
|
+
// `ec_oat_*`, and other Bearer credentials) carry no `astro-session`
|
|
495
|
+
// cookie, so `sessionUser` is null for them -- yet they still expect
|
|
496
|
+
// read-your-writes. The auth middleware that resolves the token runs
|
|
497
|
+
// *after* this one, so `locals.user` isn't populated here; detect the
|
|
498
|
+
// credential directly on the request. Request-scoped adapters use this to
|
|
499
|
+
// keep such requests on the primary/uncached connection (not a lagging
|
|
500
|
+
// read replica or the Hyperdrive query cache).
|
|
501
|
+
const hasBearerAuth = (request.headers.get("authorization") ?? "")
|
|
502
|
+
.toLowerCase()
|
|
503
|
+
.startsWith("bearer ");
|
|
504
|
+
|
|
493
505
|
if (!isEmDashRoute && !isPublicRuntimeRoute && !hasEditCookie && !hasPreviewToken) {
|
|
494
506
|
if (!sessionUser && !playgroundDb) {
|
|
495
507
|
const timings: Array<{ name: string; dur: number; desc?: string }> = [];
|
|
@@ -668,6 +680,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
668
680
|
// Content handlers
|
|
669
681
|
handleContentList: runtime.handleContentList.bind(runtime),
|
|
670
682
|
handleContentGet: runtime.handleContentGet.bind(runtime),
|
|
683
|
+
handleContentAuthors: runtime.handleContentAuthors.bind(runtime),
|
|
671
684
|
handleContentCreate: runtime.handleContentCreate.bind(runtime),
|
|
672
685
|
handleContentUpdate: runtime.handleContentUpdate.bind(runtime),
|
|
673
686
|
handleContentDelete: runtime.handleContentDelete.bind(runtime),
|
|
@@ -775,7 +788,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
775
788
|
// per-request state (e.g. a D1 bookmark cookie for read-your-writes).
|
|
776
789
|
const scoped = createRequestScopedDb({
|
|
777
790
|
config: config?.database?.config,
|
|
778
|
-
isAuthenticated: !!sessionUser,
|
|
791
|
+
isAuthenticated: !!sessionUser || hasBearerAuth,
|
|
779
792
|
isWrite: request.method !== "GET" && request.method !== "HEAD",
|
|
780
793
|
cookies: context.cookies,
|
|
781
794
|
url,
|
|
@@ -20,6 +20,7 @@ import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
|
20
20
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
21
21
|
import { wpMediaImportBody } from "#api/schemas.js";
|
|
22
22
|
import { validateExternalUrl, ssrfSafeFetch, SsrfError } from "#import/ssrf.js";
|
|
23
|
+
import { enrichImageMetadata } from "#media/enrich.js";
|
|
23
24
|
import type { EmDashHandlers } from "#types";
|
|
24
25
|
|
|
25
26
|
import type { AttachmentInfo } from "./analyze.js";
|
|
@@ -125,7 +126,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
125
126
|
}
|
|
126
127
|
};
|
|
127
128
|
|
|
128
|
-
async function importMediaWithProgress(
|
|
129
|
+
export async function importMediaWithProgress(
|
|
129
130
|
attachments: AttachmentInfo[],
|
|
130
131
|
db: NonNullable<EmDashHandlers["db"]>,
|
|
131
132
|
storage: NonNullable<EmDashHandlers["storage"]>,
|
|
@@ -273,6 +274,9 @@ async function importMediaWithProgress(
|
|
|
273
274
|
contentType,
|
|
274
275
|
});
|
|
275
276
|
|
|
277
|
+
// Derive dimensions + LQIP placeholders (no-op for non-images).
|
|
278
|
+
const enriched = await enrichImageMetadata(new Uint8Array(buffer), contentType);
|
|
279
|
+
|
|
276
280
|
// Create media record with content hash
|
|
277
281
|
const mediaItem = await repo.create({
|
|
278
282
|
filename: attachment.filename || `media-${attachment.id}${ext}`,
|
|
@@ -280,8 +284,10 @@ async function importMediaWithProgress(
|
|
|
280
284
|
size,
|
|
281
285
|
storageKey,
|
|
282
286
|
contentHash,
|
|
283
|
-
width:
|
|
284
|
-
height:
|
|
287
|
+
width: enriched.width,
|
|
288
|
+
height: enriched.height,
|
|
289
|
+
blurhash: enriched.blurhash,
|
|
290
|
+
dominantColor: enriched.dominantColor,
|
|
285
291
|
});
|
|
286
292
|
|
|
287
293
|
// Build the new URL
|
|
@@ -14,10 +14,20 @@ import { requireOwnerPerm, requirePerm } from "#api/authorize.js";
|
|
|
14
14
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
15
15
|
import { isParseError, parseOptionalBody } from "#api/parse.js";
|
|
16
16
|
import { mediaConfirmBody } from "#api/schemas.js";
|
|
17
|
+
import { enrichImageMetadata } from "#media/enrich.js";
|
|
17
18
|
import type { MediaItem } from "#types";
|
|
18
19
|
|
|
19
20
|
export const prerender = false;
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Max raw bytes to buffer for server-side LQIP generation at confirm time. The
|
|
24
|
+
* signed-URL upload flow exists so large files bypass server buffering — re-reading
|
|
25
|
+
* the whole object into a Worker's 128 MB heap to compute a blurhash would OOM
|
|
26
|
+
* on the very uploads that flow was designed for. LQIP is progressive
|
|
27
|
+
* enhancement: large images simply ship without a server-generated placeholder.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_PLACEHOLDER_DOWNLOAD_BYTES = 8 * 1024 * 1024;
|
|
30
|
+
|
|
21
31
|
/**
|
|
22
32
|
* Add URL to media item (relative URL for portability)
|
|
23
33
|
*/
|
|
@@ -81,11 +91,61 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
|
|
94
|
+
// For images, read the just-uploaded bytes back from storage once to
|
|
95
|
+
// generate LQIP placeholders (and server-side dimensions as a fallback).
|
|
96
|
+
// The signed-URL flow uploads directly to storage, so this confirm is the
|
|
97
|
+
// only point at which the server sees the bytes. Best-effort: a decode
|
|
98
|
+
// failure must not block the upload from being marked ready. We also cap
|
|
99
|
+
// the download size — buffering a large original into a Worker heap to
|
|
100
|
+
// compute a 32px blurhash would OOM on the uploads the signed-URL path
|
|
101
|
+
// exists to support, so oversized files skip the server-side placeholder.
|
|
102
|
+
let blurhash: string | undefined;
|
|
103
|
+
let dominantColor: string | undefined;
|
|
104
|
+
let width = body.width;
|
|
105
|
+
let height = body.height;
|
|
106
|
+
if (emdash.storage && existing.mimeType.startsWith("image/")) {
|
|
107
|
+
const knownSize = body.size ?? existing.size ?? undefined;
|
|
108
|
+
const tooLarge = knownSize != null && knownSize > MAX_PLACEHOLDER_DOWNLOAD_BYTES;
|
|
109
|
+
if (!tooLarge) {
|
|
110
|
+
try {
|
|
111
|
+
const { body: stream } = await emdash.storage.download(existing.storageKey);
|
|
112
|
+
const bytes = new Uint8Array(await new Response(stream).arrayBuffer());
|
|
113
|
+
// Defense-in-depth for the unknown-size case: even though we
|
|
114
|
+
// already buffered it, refuse the decode so we don't also pay
|
|
115
|
+
// the (larger) RGBA allocation.
|
|
116
|
+
if (bytes.byteLength > MAX_PLACEHOLDER_DOWNLOAD_BYTES) {
|
|
117
|
+
console.warn(
|
|
118
|
+
`[media] confirm skipping placeholder: object ${existing.storageKey} is ${bytes.byteLength} bytes (> ${MAX_PLACEHOLDER_DOWNLOAD_BYTES})`,
|
|
119
|
+
);
|
|
120
|
+
} else {
|
|
121
|
+
const enriched = await enrichImageMetadata(bytes, existing.mimeType, {
|
|
122
|
+
knownDimensions:
|
|
123
|
+
body.width != null && body.height != null
|
|
124
|
+
? { width: body.width, height: body.height }
|
|
125
|
+
: undefined,
|
|
126
|
+
});
|
|
127
|
+
blurhash = enriched.blurhash;
|
|
128
|
+
dominantColor = enriched.dominantColor;
|
|
129
|
+
width = width ?? enriched.width;
|
|
130
|
+
height = height ?? enriched.height;
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error("[media] confirm placeholder generation failed:", error);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
console.warn(
|
|
137
|
+
`[media] confirm skipping placeholder: object ${existing.storageKey} reported size ${knownSize} bytes (> ${MAX_PLACEHOLDER_DOWNLOAD_BYTES})`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
84
142
|
// Confirm the upload
|
|
85
143
|
const item = await repo.confirmUpload(id, {
|
|
86
144
|
size: body.size,
|
|
87
|
-
width
|
|
88
|
-
height
|
|
145
|
+
width,
|
|
146
|
+
height,
|
|
147
|
+
blurhash,
|
|
148
|
+
dominantColor,
|
|
89
149
|
});
|
|
90
150
|
|
|
91
151
|
if (!item) {
|
|
@@ -16,8 +16,8 @@ import { GLOBAL_UPLOAD_ALLOWLIST, resolveFieldAllowlist } from "#api/handlers/me
|
|
|
16
16
|
import { isParseError, parseQuery } from "#api/parse.js";
|
|
17
17
|
import { DEFAULT_MAX_UPLOAD_SIZE, formatFileSize, mediaListQuery } from "#api/schemas.js";
|
|
18
18
|
import { MediaRepository } from "#db/repositories/media.js";
|
|
19
|
+
import { enrichImageMetadata } from "#media/enrich.js";
|
|
19
20
|
import { matchesMimeAllowlist, normalizeMime } from "#media/mime.js";
|
|
20
|
-
import { generatePlaceholder } from "#media/placeholder.js";
|
|
21
21
|
import { computeContentHash } from "#utils/hash.js";
|
|
22
22
|
|
|
23
23
|
import type { MediaItem } from "../../types.js";
|
|
@@ -163,34 +163,32 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
163
163
|
const width = widthStr ? parseInt(widthStr, 10) : undefined;
|
|
164
164
|
const height = heightStr ? parseInt(heightStr, 10) : undefined;
|
|
165
165
|
|
|
166
|
-
//
|
|
167
|
-
// If the client sent a thumbnail
|
|
168
|
-
//
|
|
166
|
+
// Derive dimensions + LQIP placeholders via the shared helper.
|
|
167
|
+
// If the client sent a downscaled thumbnail, decode that for the blurhash
|
|
168
|
+
// (avoids OOM on large originals on memory-constrained runtimes).
|
|
169
169
|
const thumbnailEntry = formData.get("thumbnail");
|
|
170
170
|
const thumbnail = thumbnailEntry instanceof File ? thumbnailEntry : null;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} else {
|
|
178
|
-
const clientDims = width && height ? { width, height } : undefined;
|
|
179
|
-
placeholder = await generatePlaceholder(buffer, file.type, clientDims);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
171
|
+
const enriched = await enrichImageMetadata(buffer, file.type, {
|
|
172
|
+
knownDimensions: width != null && height != null ? { width, height } : undefined,
|
|
173
|
+
placeholder: thumbnail
|
|
174
|
+
? { bytes: new Uint8Array(await thumbnail.arrayBuffer()), contentType: thumbnail.type }
|
|
175
|
+
: undefined,
|
|
176
|
+
});
|
|
182
177
|
|
|
183
178
|
// Create media record
|
|
184
179
|
const result = await emdash.handleMediaCreate({
|
|
185
180
|
filename: file.name,
|
|
186
181
|
mimeType: normalizeMime(file.type),
|
|
187
182
|
size: file.size,
|
|
188
|
-
|
|
189
|
-
|
|
183
|
+
// Client dimensions win over server header dimensions: the browser's
|
|
184
|
+
// naturalWidth/Height apply EXIF orientation, while image-size reports
|
|
185
|
+
// raw (pre-orientation) header dims — swapped for 90°/270° JPEGs.
|
|
186
|
+
width: width ?? enriched.width,
|
|
187
|
+
height: height ?? enriched.height,
|
|
190
188
|
storageKey,
|
|
191
189
|
contentHash,
|
|
192
|
-
blurhash:
|
|
193
|
-
dominantColor:
|
|
190
|
+
blurhash: enriched.blurhash,
|
|
191
|
+
dominantColor: enriched.dominantColor,
|
|
194
192
|
authorId: user?.id,
|
|
195
193
|
});
|
|
196
194
|
|
|
@@ -158,15 +158,12 @@ if (img) {
|
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
-
// Build placeholder background style
|
|
161
|
+
// Build placeholder background style. Prefer the first-class MediaValue fields;
|
|
162
|
+
// fall back to `meta` for snapshots stored before LQIP was promoted off `meta`.
|
|
162
163
|
const blurhash =
|
|
163
|
-
|
|
164
|
-
? (image?.meta?.blurhash as string | undefined)
|
|
165
|
-
: undefined;
|
|
164
|
+
img?.blurhash ?? (img?.meta?.blurhash as string | undefined);
|
|
166
165
|
const dominantColor =
|
|
167
|
-
|
|
168
|
-
? (image?.meta?.dominantColor as string | undefined)
|
|
169
|
-
: undefined;
|
|
166
|
+
img?.dominantColor ?? (img?.meta?.dominantColor as string | undefined);
|
|
170
167
|
|
|
171
168
|
let placeholderStyle = "";
|
|
172
169
|
if (blurhash) {
|