emdash 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-C4yd_UJR.d.mts → adapters-C5AWLJSD.d.mts} +1 -1
- package/dist/{adapters-C4yd_UJR.d.mts.map → adapters-C5AWLJSD.d.mts.map} +1 -1
- package/dist/{allowed-origins-D0fFk9a6.mjs → allowed-origins-CyYLEJkp.mjs} +2 -2
- package/dist/{allowed-origins-D0fFk9a6.mjs.map → allowed-origins-CyYLEJkp.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +16 -16
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +3 -3
- package/dist/{api-BNKqxyFX.mjs → api-Dmz40c2V.mjs} +44 -22
- package/dist/api-Dmz40c2V.mjs.map +1 -0
- package/dist/{api-tokens-ucpcNXDt.mjs → api-tokens-VrXNiNvV.mjs} +2 -2
- package/dist/{api-tokens-ucpcNXDt.mjs.map → api-tokens-VrXNiNvV.mjs.map} +1 -1
- package/dist/{apply-BOPaD-s9.mjs → apply-CgamLmed.mjs} +93 -31
- package/dist/apply-CgamLmed.mjs.map +1 -0
- package/dist/astro/index.d.mts +10 -10
- package/dist/astro/index.mjs +19 -3
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +9 -9
- package/dist/astro/middleware/auth.mjs +6 -6
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +9 -5
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +2 -2
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +66 -65
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +5 -5
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +5 -5
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.d.mts +8 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +23 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_.d.mts +10 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +55 -0
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/index.d.mts +9 -0
- package/dist/astro/routes/api/admin/byline-fields/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +43 -0
- package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/reorder.d.mts +8 -0
- package/dist/astro/routes/api/admin/byline-fields/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +27 -0
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/admin/bylines/_id_/index.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +27 -28
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +13 -12
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/index.mjs +15 -13
- package/dist/astro/routes/api/admin/bylines/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +10 -10
- package/dist/astro/routes/api/admin/comments/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/counts.mjs +5 -5
- package/dist/astro/routes/api/admin/comments/index.mjs +8 -8
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +4 -4
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +4 -4
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +35 -34
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +35 -34
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +35 -34
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +35 -34
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +34 -33
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +34 -33
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +3 -3
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +34 -33
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +2 -2
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +5 -5
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
- package/dist/astro/routes/api/admin/users/index.mjs +5 -5
- package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -5
- package/dist/astro/routes/api/auth/invite/accept.mjs +2 -2
- package/dist/astro/routes/api/auth/invite/complete.mjs +9 -9
- package/dist/astro/routes/api/auth/invite/index.mjs +6 -6
- package/dist/astro/routes/api/auth/invite/register-options.mjs +8 -8
- package/dist/astro/routes/api/auth/logout.mjs +3 -3
- package/dist/astro/routes/api/auth/magic-link/send.mjs +8 -8
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +3 -3
- package/dist/astro/routes/api/auth/me.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/me.mjs +18 -11
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +1 -1
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +3 -3
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +5 -5
- package/dist/astro/routes/api/auth/passkey/index.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/options.mjs +10 -10
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +8 -8
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +9 -9
- package/dist/astro/routes/api/auth/passkey/verify.mjs +9 -9
- package/dist/astro/routes/api/auth/signup/complete.mjs +9 -9
- package/dist/astro/routes/api/auth/signup/request.mjs +8 -8
- package/dist/astro/routes/api/auth/signup/verify.mjs +2 -2
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +9 -9
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +18 -13
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +9 -7
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/index.mjs +6 -6
- package/dist/astro/routes/api/content/_collection_/trash.mjs +6 -6
- package/dist/astro/routes/api/dashboard.mjs +7 -7
- package/dist/astro/routes/api/dev/emails.mjs +3 -3
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +10 -10
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +4 -4
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +9 -9
- package/dist/astro/routes/api/import/wordpress/execute.mjs +11 -10
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +9 -9
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +8 -8
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +10 -10
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +13 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +4 -4
- package/dist/astro/routes/api/mcp.mjs +34 -30
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +6 -6
- package/dist/astro/routes/api/media/_id_.mjs +6 -6
- package/dist/astro/routes/api/media/file/_...key_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +3 -3
- package/dist/astro/routes/api/media/providers/index.mjs +3 -3
- package/dist/astro/routes/api/media/upload-url.mjs +8 -8
- package/dist/astro/routes/api/media.d.mts.map +1 -1
- package/dist/astro/routes/api/media.mjs +13 -12
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/items.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_/translations.mjs +7 -7
- package/dist/astro/routes/api/menus/_name_.mjs +7 -7
- package/dist/astro/routes/api/menus/index.mjs +7 -7
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/code.mjs +9 -9
- package/dist/astro/routes/api/oauth/device/token.mjs +8 -8
- package/dist/astro/routes/api/oauth/register.mjs +3 -3
- package/dist/astro/routes/api/oauth/token/refresh.mjs +6 -6
- package/dist/astro/routes/api/oauth/token/revoke.mjs +6 -6
- package/dist/astro/routes/api/oauth/token.mjs +6 -6
- package/dist/astro/routes/api/openapi.json.mjs +10 -7
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +4 -4
- package/dist/astro/routes/api/redirects/404s/index.mjs +8 -8
- package/dist/astro/routes/api/redirects/404s/summary.mjs +8 -8
- package/dist/astro/routes/api/redirects/_id_.mjs +9 -9
- package/dist/astro/routes/api/redirects/index.mjs +9 -9
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +3 -3
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +34 -33
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +34 -33
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +34 -33
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +34 -33
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +34 -33
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +6 -6
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +34 -33
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +34 -33
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +9 -9
- package/dist/astro/routes/api/search/index.mjs +8 -8
- package/dist/astro/routes/api/search/rebuild.mjs +9 -9
- package/dist/astro/routes/api/search/stats.mjs +6 -6
- package/dist/astro/routes/api/search/suggest.mjs +8 -8
- package/dist/astro/routes/api/sections/_slug_.mjs +8 -8
- package/dist/astro/routes/api/sections/index.mjs +8 -8
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +11 -11
- package/dist/astro/routes/api/setup/admin-verify.mjs +10 -10
- package/dist/astro/routes/api/setup/admin.mjs +9 -9
- package/dist/astro/routes/api/setup/dev-bypass.mjs +24 -23
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +2 -2
- package/dist/astro/routes/api/setup/index.mjs +24 -23
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +4 -4
- package/dist/astro/routes/api/snapshot.mjs +5 -5
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +12 -12
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +12 -12
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +12 -12
- package/dist/astro/routes/api/taxonomies/index.mjs +12 -12
- package/dist/astro/routes/api/themes/preview.mjs +5 -5
- package/dist/astro/routes/api/typegen.mjs +5 -5
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +6 -6
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +8 -8
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +8 -8
- package/dist/astro/routes/api/widget-areas/_name_.mjs +5 -5
- package/dist/astro/routes/api/widget-areas/index.mjs +8 -8
- package/dist/astro/routes/api/widget-components.mjs +3 -3
- package/dist/astro/routes/robots.txt.mjs +6 -6
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +8 -8
- package/dist/astro/routes/sitemap.xml.mjs +7 -7
- package/dist/astro/types.d.mts +13 -12
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-Bn4S4DUT.mjs → authorize-_wWM_44T.mjs} +2 -2
- package/dist/{authorize-Bn4S4DUT.mjs.map → authorize-_wWM_44T.mjs.map} +1 -1
- package/dist/byline-BrIVWLm-.mjs +925 -0
- package/dist/byline-BrIVWLm-.mjs.map +1 -0
- package/dist/{bylines-B2_XmnSU.d.mts → byline-fields-BNy7Ng1U.d.mts} +154 -26
- package/dist/byline-fields-BNy7Ng1U.d.mts.map +1 -0
- package/dist/byline-fields-DC3Wkk-U.mjs +123 -0
- package/dist/byline-fields-DC3Wkk-U.mjs.map +1 -0
- package/dist/byline-fields-Dr-xcb6S.mjs +238 -0
- package/dist/byline-fields-Dr-xcb6S.mjs.map +1 -0
- package/dist/byline-registry-CxK5g559.mjs +406 -0
- package/dist/byline-registry-CxK5g559.mjs.map +1 -0
- package/dist/{bylines-n6nykUyI.mjs → bylines-C_POWmGT.mjs} +25 -11
- package/dist/{bylines-n6nykUyI.mjs.map → bylines-C_POWmGT.mjs.map} +1 -1
- package/dist/bylines-sqExMElV.mjs +204 -0
- package/dist/bylines-sqExMElV.mjs.map +1 -0
- package/dist/{cache-BcI1yUjR.mjs → cache-wsDkA8ru.mjs} +2 -2
- package/dist/{cache-BcI1yUjR.mjs.map → cache-wsDkA8ru.mjs.map} +1 -1
- package/dist/{challenge-store-Dng1SxKT.mjs → challenge-store-DGwuCc4R.mjs} +1 -1
- package/dist/{challenge-store-Dng1SxKT.mjs.map → challenge-store-DGwuCc4R.mjs.map} +1 -1
- package/dist/{chunks-cYG4SnIP.mjs → chunks-BAYkM-CF.mjs} +2 -2
- package/dist/{chunks-cYG4SnIP.mjs.map → chunks-BAYkM-CF.mjs.map} +1 -1
- package/dist/cli/index.mjs +29 -23
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +2 -1
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +4 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/{comment-C76G-9tz.mjs → comment-Cd29aktf.mjs} +2 -2
- package/dist/{comment-C76G-9tz.mjs.map → comment-Cd29aktf.mjs.map} +1 -1
- package/dist/{comments-CCxFFGY1.mjs → comments-B7ufhkxN.mjs} +3 -3
- package/dist/{comments-CCxFFGY1.mjs.map → comments-B7ufhkxN.mjs.map} +1 -1
- package/dist/{components-Dx3DM0gg.mjs → components-CTfpu3PZ.mjs} +1 -1
- package/dist/{components-Dx3DM0gg.mjs.map → components-CTfpu3PZ.mjs.map} +1 -1
- package/dist/{content-8voQNTXX.mjs → content-BbqKo3Kc.mjs} +22 -3
- package/dist/content-BbqKo3Kc.mjs.map +1 -0
- package/dist/{context-B7qiYrz2.mjs → context-BsF1rhoI.mjs} +9 -9
- package/dist/{context-B7qiYrz2.mjs.map → context-BsF1rhoI.mjs.map} +1 -1
- package/dist/{cron-Bd3b3iuj.mjs → cron-DZovZUnC.mjs} +1 -1
- package/dist/{cron-Bd3b3iuj.mjs.map → cron-DZovZUnC.mjs.map} +1 -1
- package/dist/{dashboard-BeaFSPpx.mjs → dashboard-BwIX9r-X.mjs} +4 -4
- package/dist/{dashboard-BeaFSPpx.mjs.map → dashboard-BwIX9r-X.mjs.map} +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-BiYqoX-n.mjs → db-errors-CtzxKBxe.mjs} +1 -1
- package/dist/{db-errors-BiYqoX-n.mjs.map → db-errors-CtzxKBxe.mjs.map} +1 -1
- package/dist/{default-BvTAYCzx.mjs → default-xLFNSsZ9.mjs} +1 -1
- package/dist/{default-BvTAYCzx.mjs.map → default-xLFNSsZ9.mjs.map} +1 -1
- package/dist/{device-flow-B9oG8PwP.mjs → device-flow-ptLrVINd.mjs} +4 -4
- package/dist/{device-flow-B9oG8PwP.mjs.map → device-flow-ptLrVINd.mjs.map} +1 -1
- package/dist/{email-console-CubRll9q.mjs → email-console-DHT2Fbpj.mjs} +1 -1
- package/dist/{email-console-CubRll9q.mjs.map → email-console-DHT2Fbpj.mjs.map} +1 -1
- package/dist/{error-ChfADBuu.mjs → error-npZWBSb7.mjs} +7 -3
- package/dist/error-npZWBSb7.mjs.map +1 -0
- package/dist/{escape-Cg6kMELH.mjs → escape-bIyGoW5W.mjs} +1 -1
- package/dist/{escape-Cg6kMELH.mjs.map → escape-bIyGoW5W.mjs.map} +1 -1
- package/dist/{fts-manager-C_b-4x8u.mjs → fts-manager-DmUAk-kQ.mjs} +2 -2
- package/dist/{fts-manager-C_b-4x8u.mjs.map → fts-manager-DmUAk-kQ.mjs.map} +1 -1
- package/dist/{hash-DlUxGhQS.mjs → hash-9w3pd3-m.mjs} +1 -1
- package/dist/{hash-DlUxGhQS.mjs.map → hash-9w3pd3-m.mjs.map} +1 -1
- package/dist/{import-DG80rC_I.mjs → import-Dh8bWmyq.mjs} +3 -3
- package/dist/{import-DG80rC_I.mjs.map → import-Dh8bWmyq.mjs.map} +1 -1
- package/dist/{index-BPZFAcgE.d.mts → index-CjKdMZ3U.d.mts} +39 -17
- package/dist/index-CjKdMZ3U.d.mts.map +1 -0
- package/dist/{index-CC42STEm.d.mts → index-D60_SzHG.d.mts} +3 -3
- package/dist/{index-CC42STEm.d.mts.map → index-D60_SzHG.d.mts.map} +1 -1
- package/dist/index.d.mts +17 -17
- package/dist/index.mjs +55 -54
- package/dist/{load-CLFRjk9r.mjs → load-DsoLq7ex.mjs} +2 -2
- package/dist/{load-CLFRjk9r.mjs.map → load-DsoLq7ex.mjs.map} +1 -1
- package/dist/{loader-D-vIJjfY.mjs → loader-CJ6lWO0d.mjs} +75 -19
- package/dist/loader-CJ6lWO0d.mjs.map +1 -0
- package/dist/{manifest-schema-Czqf0TLu.mjs → manifest-schema-Cj-YrzrF.mjs} +1 -1
- package/dist/{manifest-schema-Czqf0TLu.mjs.map → manifest-schema-Cj-YrzrF.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +2 -2
- package/dist/media/local-runtime.d.mts +11 -11
- package/dist/media/local-runtime.mjs +5 -5
- package/dist/{media-allowlist-BNloC69x.mjs → media-allowlist-CMcoYIjQ.mjs} +2 -2
- package/dist/{media-allowlist-BNloC69x.mjs.map → media-allowlist-CMcoYIjQ.mjs.map} +1 -1
- package/dist/{media-CKQd8AYU.mjs → media-jk_HzzOl.mjs} +7 -2
- package/dist/media-jk_HzzOl.mjs.map +1 -0
- package/dist/{menus-arUNspyU.mjs → menus-B-5-3aon.mjs} +2 -2
- package/dist/{menus-arUNspyU.mjs.map → menus-B-5-3aon.mjs.map} +1 -1
- package/dist/{menus-C-nWT5Tu.mjs → menus-CyMO6GBx.mjs} +27 -11
- package/dist/menus-CyMO6GBx.mjs.map +1 -0
- package/dist/{mime-KV5TqkMN.mjs → mime-CCEzze7W.mjs} +1 -1
- package/dist/{mime-KV5TqkMN.mjs.map → mime-CCEzze7W.mjs.map} +1 -1
- package/dist/{mode-CaaiebZI.mjs → mode-BjlXswIw.mjs} +1 -1
- package/dist/{mode-CaaiebZI.mjs.map → mode-BjlXswIw.mjs.map} +1 -1
- package/dist/{normalize-CN5kRSMC.mjs → normalize-DVV8nbrL.mjs} +1 -1
- package/dist/{normalize-CN5kRSMC.mjs.map → normalize-DVV8nbrL.mjs.map} +1 -1
- package/dist/{oauth-authorization-CTMeVfvj.mjs → oauth-authorization-DvBAL75d.mjs} +4 -4
- package/dist/{oauth-authorization-CTMeVfvj.mjs.map → oauth-authorization-DvBAL75d.mjs.map} +1 -1
- package/dist/{oauth-clients-eJCbkVSG.mjs → oauth-clients-8mPDStMv.mjs} +1 -1
- package/dist/{oauth-clients-eJCbkVSG.mjs.map → oauth-clients-8mPDStMv.mjs.map} +1 -1
- package/dist/{oauth-state-store-vOSdOeGe.mjs → oauth-state-store-BJ7YtrfD.mjs} +1 -1
- package/dist/{oauth-state-store-vOSdOeGe.mjs.map → oauth-state-store-BJ7YtrfD.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-3JwsVw6N.mjs → oauth-user-lookup-BdDSDvjF.mjs} +1 -1
- package/dist/{oauth-user-lookup-3JwsVw6N.mjs.map → oauth-user-lookup-BdDSDvjF.mjs.map} +1 -1
- package/dist/{options-DhV-gwJb.d.mts → options-tb7DJROi.d.mts} +3 -3
- package/dist/{options-DhV-gwJb.d.mts.map → options-tb7DJROi.d.mts.map} +1 -1
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-DHbXfvxO.mjs → parse-4zO5Y2DL.mjs} +2 -2
- package/dist/{parse-DHbXfvxO.mjs.map → parse-4zO5Y2DL.mjs.map} +1 -1
- package/dist/{passkey-config-BloQOT3y.mjs → passkey-config-BDVM86Tj.mjs} +1 -1
- package/dist/{passkey-config-BloQOT3y.mjs.map → passkey-config-BDVM86Tj.mjs.map} +1 -1
- package/dist/{placeholder-KCkkCtgQ.d.mts → placeholder-B9lUUEmj.d.mts} +1 -1
- package/dist/{placeholder-KCkkCtgQ.d.mts.map → placeholder-B9lUUEmj.d.mts.map} +1 -1
- package/dist/{placeholder-LqmHqvBw.mjs → placeholder-BZxr8W1j.mjs} +1 -1
- package/dist/{placeholder-LqmHqvBw.mjs.map → placeholder-BZxr8W1j.mjs.map} +1 -1
- package/dist/plugin-types.d.mts +1 -1
- package/dist/plugin-utils.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.d.mts +9 -9
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{preview-D4z0WONU.mjs → preview-BfuRkVKW.mjs} +2 -2
- package/dist/{preview-D4z0WONU.mjs.map → preview-BfuRkVKW.mjs.map} +1 -1
- package/dist/{public-url-CUWWFME2.mjs → public-url-egRHCy1m.mjs} +1 -1
- package/dist/{public-url-CUWWFME2.mjs.map → public-url-egRHCy1m.mjs.map} +1 -1
- package/dist/{query-7m6-l0f_.mjs → query-CuvjwhrE.mjs} +12 -12
- package/dist/{query-7m6-l0f_.mjs.map → query-CuvjwhrE.mjs.map} +1 -1
- package/dist/{rate-limit-D8RAXN8b.mjs → rate-limit-D6VQqBk_.mjs} +2 -2
- package/dist/{rate-limit-D8RAXN8b.mjs.map → rate-limit-D6VQqBk_.mjs.map} +1 -1
- package/dist/{redirect-CjfDGrTd.mjs → redirect-BZUJltlj.mjs} +2 -2
- package/dist/{redirect-CjfDGrTd.mjs.map → redirect-BZUJltlj.mjs.map} +1 -1
- package/dist/{redirect-BINiRYq4.mjs → redirect-Cw3JTlmj.mjs} +1 -1
- package/dist/{redirect-BINiRYq4.mjs.map → redirect-Cw3JTlmj.mjs.map} +1 -1
- package/dist/{redirects-COMLwsV5.mjs → redirects-C0L9JUk4.mjs} +19 -6
- package/dist/redirects-C0L9JUk4.mjs.map +1 -0
- package/dist/{redirects-CowoEHdE.mjs → redirects-DnYuqsEf.mjs} +3 -3
- package/dist/{redirects-CowoEHdE.mjs.map → redirects-DnYuqsEf.mjs.map} +1 -1
- package/dist/{registry-Cyp-dx6J.mjs → registry-Dn6gsx3L.mjs} +13 -5
- package/dist/{registry-Cyp-dx6J.mjs.map → registry-Dn6gsx3L.mjs.map} +1 -1
- package/dist/{request-cache-dzCt8TZB.mjs → request-cache-BYMs-BGX.mjs} +23 -2
- package/dist/{request-cache-dzCt8TZB.mjs.map → request-cache-BYMs-BGX.mjs.map} +1 -1
- package/dist/{request-meta-C_Cjii-T.mjs → request-meta-7ByVLxB-.mjs} +2 -2
- package/dist/{request-meta-C_Cjii-T.mjs.map → request-meta-7ByVLxB-.mjs.map} +1 -1
- package/dist/{resolve-D6sM-SgF.mjs → resolve-BqYMVG0D.mjs} +1 -1
- package/dist/{resolve-D6sM-SgF.mjs.map → resolve-BqYMVG0D.mjs.map} +1 -1
- package/dist/{runner-DSQBurMS.d.mts → runner-DM1yR5qd.d.mts} +2 -2
- package/dist/{runner-DSQBurMS.d.mts.map → runner-DM1yR5qd.d.mts.map} +1 -1
- package/dist/{runner-Drnvs96u.mjs → runner-eAgyIkeg.mjs} +284 -158
- package/dist/runner-eAgyIkeg.mjs.map +1 -0
- package/dist/runtime.d.mts +10 -10
- package/dist/runtime.mjs +2 -2
- package/dist/{schema-CI9mYPX3.mjs → schema--mYZX4D7.mjs} +5 -5
- package/dist/{schema-CI9mYPX3.mjs.map → schema--mYZX4D7.mjs.map} +1 -1
- package/dist/{search-DKz_mGBP.mjs → search-C6U_NvZI.mjs} +4 -4
- package/dist/{search-DKz_mGBP.mjs.map → search-C6U_NvZI.mjs.map} +1 -1
- package/dist/{secrets-rPdhEBkD.mjs → secrets-YYbTgB1w.mjs} +1 -1
- package/dist/{secrets-rPdhEBkD.mjs.map → secrets-YYbTgB1w.mjs.map} +1 -1
- package/dist/{sections-DBbCDIAT.mjs → sections-Ba-rJLKb.mjs} +3 -3
- package/dist/{sections-DBbCDIAT.mjs.map → sections-Ba-rJLKb.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +18 -17
- package/dist/seo/index.d.mts +1 -1
- package/dist/{seo-BGCyDlkb.mjs → seo-BTzb5ksq.mjs} +2 -2
- package/dist/{seo-BGCyDlkb.mjs.map → seo-BTzb5ksq.mjs.map} +1 -1
- package/dist/{seo-Dq707mNQ.mjs → seo-DfjLvu8i.mjs} +1 -1
- package/dist/{seo-Dq707mNQ.mjs.map → seo-DfjLvu8i.mjs.map} +1 -1
- package/dist/{service-B0H7U1Y9.mjs → service-Cn-kIfZn.mjs} +3 -3
- package/dist/{service-B0H7U1Y9.mjs.map → service-Cn-kIfZn.mjs.map} +1 -1
- package/dist/{settings-DfwNyQkf.mjs → settings-C65OSm41.mjs} +3 -3
- package/dist/{settings-DfwNyQkf.mjs.map → settings-C65OSm41.mjs.map} +1 -1
- package/dist/{settings-BSXRtTzk.mjs → settings-ChlQbwU0.mjs} +4 -4
- package/dist/{settings-BSXRtTzk.mjs.map → settings-ChlQbwU0.mjs.map} +1 -1
- package/dist/{setup-complete-MzzN9u0b.mjs → setup-complete-VoEZfasi.mjs} +1 -1
- package/dist/{setup-complete-MzzN9u0b.mjs.map → setup-complete-VoEZfasi.mjs.map} +1 -1
- package/dist/{setup-nonce-DXuriHsg.mjs → setup-nonce-Bm0uKqmf.mjs} +1 -1
- package/dist/{setup-nonce-DXuriHsg.mjs.map → setup-nonce-Bm0uKqmf.mjs.map} +1 -1
- package/dist/{site-url-xkhw1tcz.mjs → site-url-Cm8-sJy7.mjs} +1 -1
- package/dist/{site-url-xkhw1tcz.mjs.map → site-url-Cm8-sJy7.mjs.map} +1 -1
- package/dist/{ssrf-MZ-zrG6-.mjs → ssrf-BsVGIE0Z.mjs} +1 -1
- package/dist/{ssrf-MZ-zrG6-.mjs.map → ssrf-BsVGIE0Z.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{taxonomies-CcvrMLbR.mjs → taxonomies-CgpzAU6F.mjs} +8 -8
- package/dist/{taxonomies-CcvrMLbR.mjs.map → taxonomies-CgpzAU6F.mjs.map} +1 -1
- package/dist/{taxonomies-4vx0nmMr.mjs → taxonomies-D72gTOg_.mjs} +4 -4
- package/dist/{taxonomies-4vx0nmMr.mjs.map → taxonomies-D72gTOg_.mjs.map} +1 -1
- package/dist/{taxonomy-zqGQUqgu.mjs → taxonomy-BBK-UAEo.mjs} +3 -3
- package/dist/{taxonomy-zqGQUqgu.mjs.map → taxonomy-BBK-UAEo.mjs.map} +1 -1
- package/dist/{tokens-N8otWMmj.mjs → tokens-Bx2afeT-.mjs} +1 -1
- package/dist/{tokens-N8otWMmj.mjs.map → tokens-Bx2afeT-.mjs.map} +1 -1
- package/dist/{transport-B6CHddbu.mjs → transport--Ck3RBin.mjs} +1 -1
- package/dist/{transport-B6CHddbu.mjs.map → transport--Ck3RBin.mjs.map} +1 -1
- package/dist/{transport-C2MGqtL6.d.mts → transport-OnMNbsIA.d.mts} +1 -1
- package/dist/{transport-C2MGqtL6.d.mts.map → transport-OnMNbsIA.d.mts.map} +1 -1
- package/dist/{trusted-proxy-97pajC2f.mjs → trusted-proxy-B4AfnoAp.mjs} +1 -1
- package/dist/{trusted-proxy-97pajC2f.mjs.map → trusted-proxy-B4AfnoAp.mjs.map} +1 -1
- package/dist/types-D8bhH891.mjs +125 -0
- package/dist/{types-DSZl1Dsv.mjs.map → types-D8bhH891.mjs.map} +1 -1
- package/dist/{types-DGHWRQgr.d.mts → types-DMwSpvcw.d.mts} +2 -2
- package/dist/{types-DGHWRQgr.d.mts.map → types-DMwSpvcw.d.mts.map} +1 -1
- package/dist/{types-bYmRn_Uy.d.mts → types-DWnN7weG.d.mts} +1 -1
- package/dist/{types-bYmRn_Uy.d.mts.map → types-DWnN7weG.d.mts.map} +1 -1
- package/dist/{types-Dgo6y-Ut.d.mts → types-DX6v9KzJ.d.mts} +1 -1
- package/dist/{types-Dgo6y-Ut.d.mts.map → types-DX6v9KzJ.d.mts.map} +1 -1
- package/dist/{types-DaqNzqVt.d.mts → types-DawhLFwy.d.mts} +35 -1
- package/dist/{types-DaqNzqVt.d.mts.map → types-DawhLFwy.d.mts.map} +1 -1
- package/dist/{types-CpUuGcd5.d.mts → types-DbCWhHet.d.mts} +8 -2
- package/dist/{types-CpUuGcd5.d.mts.map → types-DbCWhHet.d.mts.map} +1 -1
- package/dist/{types-Cd9UCu3t.mjs → types-DpFmlNyB.mjs} +1 -1
- package/dist/{types-Cd9UCu3t.mjs.map → types-DpFmlNyB.mjs.map} +1 -1
- package/dist/{types-D599-ruj.d.mts → types-Qa7-HJJC.d.mts} +1 -1
- package/dist/{types-D599-ruj.d.mts.map → types-Qa7-HJJC.d.mts.map} +1 -1
- package/dist/{types-B0bmgwMG.mjs → types-SF1DwGf2.mjs} +2 -2
- package/dist/types-SF1DwGf2.mjs.map +1 -0
- package/dist/{types-DaYDYW6g.d.mts → types-i8_uzhMD.d.mts} +40 -2
- package/dist/types-i8_uzhMD.d.mts.map +1 -0
- package/dist/{types-CkDSF81F.d.mts → types-kwqCOUxj.d.mts} +1 -1
- package/dist/{types-CkDSF81F.d.mts.map → types-kwqCOUxj.d.mts.map} +1 -1
- package/dist/{user-hUSOaIJy.mjs → user-X4rtyO4Y.mjs} +2 -2
- package/dist/{user-hUSOaIJy.mjs.map → user-X4rtyO4Y.mjs.map} +1 -1
- package/dist/{utils-C3wTAP-P.mjs → utils-C4Ih4DML.mjs} +1 -1
- package/dist/{utils-C3wTAP-P.mjs.map → utils-C4Ih4DML.mjs.map} +1 -1
- package/dist/{validate-IGltez8n.mjs → validate-DactmcJG.mjs} +23 -3
- package/dist/validate-DactmcJG.mjs.map +1 -0
- package/dist/{validate-DQtHw9NT.d.mts → validate-Dy6nkNls.d.mts} +25 -5
- package/dist/{validate-DQtHw9NT.d.mts.map → validate-Dy6nkNls.d.mts.map} +1 -1
- package/dist/{validation-Bmymau7y.mjs → validation-BYA4i85b.mjs} +6 -6
- package/dist/{validation-Bmymau7y.mjs.map → validation-BYA4i85b.mjs.map} +1 -1
- package/dist/version-FGcv0ooe.mjs +7 -0
- package/dist/{version-BTc87L3L.mjs.map → version-FGcv0ooe.mjs.map} +1 -1
- package/dist/{widgets-yHQa4c6c.mjs → widgets-DG-1jxnz.mjs} +3 -3
- package/dist/{widgets-yHQa4c6c.mjs.map → widgets-DG-1jxnz.mjs.map} +1 -1
- package/dist/{zod-generator-B80aap1J.mjs → zod-generator-BNAObjSt.mjs} +3 -3
- package/dist/{zod-generator-B80aap1J.mjs.map → zod-generator-BNAObjSt.mjs.map} +1 -1
- package/package.json +7 -7
- package/src/api/errors.ts +7 -0
- package/src/api/handlers/byline-fields.ts +212 -0
- package/src/api/handlers/bylines.ts +126 -5
- package/src/api/handlers/content.ts +43 -2
- package/src/api/handlers/media.ts +2 -0
- package/src/api/openapi/document.ts +3 -0
- package/src/api/schemas/byline-fields.ts +188 -0
- package/src/api/schemas/bylines.ts +42 -0
- package/src/api/schemas/content.ts +2 -0
- package/src/api/schemas/index.ts +1 -0
- package/src/api/schemas/media.ts +2 -0
- package/src/astro/integration/routes.ts +27 -0
- package/src/astro/middleware/redirect.ts +5 -1
- package/src/astro/routes/api/admin/byline-fields/[slug]/usage.ts +36 -0
- package/src/astro/routes/api/admin/byline-fields/[slug].ts +92 -0
- package/src/astro/routes/api/admin/byline-fields/index.ts +66 -0
- package/src/astro/routes/api/admin/byline-fields/reorder.ts +39 -0
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +23 -21
- package/src/astro/routes/api/admin/bylines/index.ts +1 -0
- package/src/astro/routes/api/auth/me.ts +21 -10
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +15 -3
- package/src/astro/routes/api/content/[collection]/[id].ts +3 -1
- package/src/astro/routes/api/media.ts +1 -0
- package/src/astro/types.ts +1 -0
- package/src/bylines/field-defs-cache.ts +138 -0
- package/src/bylines/index.ts +37 -4
- package/src/cli/commands/content.ts +4 -2
- package/src/client/index.ts +4 -1
- package/src/components/InlinePortableTextEditor.tsx +69 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +7 -0
- package/src/content/converters/prosemirror-to-portable-text.ts +16 -0
- package/src/content/converters/types.ts +10 -0
- package/src/database/migrations/041_content_locale_list_index.ts +47 -0
- package/src/database/migrations/042_byline_fields.ts +157 -0
- package/src/database/migrations/runner.ts +4 -0
- package/src/database/repositories/byline.ts +758 -50
- package/src/database/repositories/content.ts +43 -3
- package/src/database/repositories/media.ts +14 -0
- package/src/database/repositories/types.ts +38 -0
- package/src/database/types.ts +44 -0
- package/src/emdash-runtime.ts +4 -1
- package/src/index.ts +1 -0
- package/src/loader.ts +98 -10
- package/src/mcp/server.ts +10 -1
- package/src/request-cache.ts +23 -0
- package/src/schema/byline-registry.ts +671 -0
- package/src/schema/registry.ts +14 -0
- package/src/schema/types.ts +133 -0
- package/src/seed/apply.ts +101 -14
- package/src/seed/types.ts +21 -0
- package/src/seed/validate.ts +39 -0
- package/dist/api-BNKqxyFX.mjs.map +0 -1
- package/dist/apply-BOPaD-s9.mjs.map +0 -1
- package/dist/byline-BDylH_m4.mjs +0 -404
- package/dist/byline-BDylH_m4.mjs.map +0 -1
- package/dist/bylines-B2_XmnSU.d.mts.map +0 -1
- package/dist/bylines-B7TFEvFf.mjs +0 -118
- package/dist/bylines-B7TFEvFf.mjs.map +0 -1
- package/dist/content-8voQNTXX.mjs.map +0 -1
- package/dist/error-ChfADBuu.mjs.map +0 -1
- package/dist/index-BPZFAcgE.d.mts.map +0 -1
- package/dist/loader-D-vIJjfY.mjs.map +0 -1
- package/dist/media-CKQd8AYU.mjs.map +0 -1
- package/dist/menus-C-nWT5Tu.mjs.map +0 -1
- package/dist/redirects-COMLwsV5.mjs.map +0 -1
- package/dist/runner-Drnvs96u.mjs.map +0 -1
- package/dist/setup-Cf_TyOv5.mjs +0 -137
- package/dist/setup-Cf_TyOv5.mjs.map +0 -1
- package/dist/types-B0bmgwMG.mjs.map +0 -1
- package/dist/types-DSZl1Dsv.mjs +0 -83
- package/dist/types-DaYDYW6g.d.mts.map +0 -1
- package/dist/validate-IGltez8n.mjs.map +0 -1
- package/dist/version-BTc87L3L.mjs +0 -7
- /package/dist/{api-tokens-iPIHAY8N.mjs → api-tokens-B6VgoE6M.mjs} +0 -0
- /package/dist/{ssrf-BIcd-aXW.mjs → ssrf-BvgVcfNQ.mjs} +0 -0
- /package/dist/{types-1NNkmTIn.mjs → types-Cj2S6FuC.mjs} +0 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
2
|
+
import { t as withTransaction } from "./transaction-NQj4VJ7Z.mjs";
|
|
3
|
+
import { i as RESERVED_BYLINE_FIELD_SLUGS, t as BYLINE_FIELD_TYPES } from "./types-D8bhH891.mjs";
|
|
4
|
+
import { sql } from "kysely";
|
|
5
|
+
import { ulid } from "ulidx";
|
|
6
|
+
|
|
7
|
+
//#region src/schema/byline-registry.ts
|
|
8
|
+
const RESERVED_SET = new Set(RESERVED_BYLINE_FIELD_SLUGS);
|
|
9
|
+
const TYPE_SET = new Set(BYLINE_FIELD_TYPES);
|
|
10
|
+
const VERSION_KEY = "byline_fields_version";
|
|
11
|
+
/** Hard cap on the choices array for a `select`-type field. */
|
|
12
|
+
const MAX_SELECT_OPTIONS = 200;
|
|
13
|
+
/** Hard cap on a slug — mirrors `SchemaRegistry.validateSlug`. */
|
|
14
|
+
const MAX_SLUG_LENGTH = 63;
|
|
15
|
+
/** Hard cap on a label. Bigger than slugs because labels are display strings. */
|
|
16
|
+
const MAX_LABEL_LENGTH = 200;
|
|
17
|
+
/**
|
|
18
|
+
* Error thrown for byline-schema validation failures. Mirrors
|
|
19
|
+
* `SchemaError` in `registry.ts` so the admin API layer can map a small
|
|
20
|
+
* set of codes to HTTP statuses without inspecting messages.
|
|
21
|
+
*
|
|
22
|
+
* Codes:
|
|
23
|
+
* - `INVALID_SLUG` — slug fails identifier rules or length cap
|
|
24
|
+
* - `RESERVED_SLUG` — slug collides with a fixed `_emdash_bylines` column
|
|
25
|
+
* - `INVALID_TYPE` — type is not one of the five v1 field types
|
|
26
|
+
* - `INVALID_LABEL` — label missing or exceeds length cap
|
|
27
|
+
* - `INVALID_VALIDATION` — validation payload malformed (e.g. `select` with
|
|
28
|
+
* no `options`, duplicates in `options`)
|
|
29
|
+
* - `FIELD_EXISTS` — slug already registered
|
|
30
|
+
* - `FIELD_NOT_FOUND` — slug not registered
|
|
31
|
+
* - `TRANSLATABLE_LOCKED` — attempt to flip `translatable` while stored
|
|
32
|
+
* values reference the field
|
|
33
|
+
* - `REORDER_MISMATCH` — reorder input doesn't match the registered set
|
|
34
|
+
*/
|
|
35
|
+
var BylineSchemaError = class extends Error {
|
|
36
|
+
constructor(message, code, details) {
|
|
37
|
+
super(message);
|
|
38
|
+
this.code = code;
|
|
39
|
+
this.details = details;
|
|
40
|
+
this.name = "BylineSchemaError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Translate a `BylineSchemaError` code to a shared `ErrorCode` for the
|
|
45
|
+
* admin API. HTTP status is then derived by `mapErrorStatus` — this
|
|
46
|
+
* function deliberately doesn't carry one, so the API/handler boundary
|
|
47
|
+
* matches the rest of the codebase (handlers return `ApiResult<T>` with
|
|
48
|
+
* a code, the route layer maps to status via `unwrapResult`).
|
|
49
|
+
*
|
|
50
|
+
* Every code on the right-hand side of `case ... return ...` is defined
|
|
51
|
+
* in `ErrorCode` (`api/errors.ts`). `INVALID_LABEL` and
|
|
52
|
+
* `INVALID_VALIDATION` are intentionally folded into the `default`
|
|
53
|
+
* branch (→ `VALIDATION_ERROR`) so no ad-hoc codes leak out — the
|
|
54
|
+
* registry's domain code names them but the HTTP surface should not.
|
|
55
|
+
*
|
|
56
|
+
* `RESERVED_SLUG` / `INVALID_SLUG` typically don't reach this layer for
|
|
57
|
+
* HTTP callers — the zod schema rejects them first with a clean
|
|
58
|
+
* `VALIDATION_ERROR`. They're still listed so non-HTTP callers (and the
|
|
59
|
+
* test layer) get consistent mapping.
|
|
60
|
+
*
|
|
61
|
+
* `FIELD_NOT_FOUND` is normalised to the shared `NOT_FOUND` code so the
|
|
62
|
+
* admin client can branch on one constant across resource types.
|
|
63
|
+
*/
|
|
64
|
+
function mapBylineSchemaError(error) {
|
|
65
|
+
switch (error.code) {
|
|
66
|
+
case "FIELD_NOT_FOUND": return {
|
|
67
|
+
code: "NOT_FOUND",
|
|
68
|
+
message: error.message,
|
|
69
|
+
details: error.details
|
|
70
|
+
};
|
|
71
|
+
case "FIELD_EXISTS":
|
|
72
|
+
case "TRANSLATABLE_LOCKED":
|
|
73
|
+
case "REORDER_MISMATCH":
|
|
74
|
+
case "INVALID_SLUG":
|
|
75
|
+
case "RESERVED_SLUG":
|
|
76
|
+
case "INVALID_TYPE": return {
|
|
77
|
+
code: error.code,
|
|
78
|
+
message: error.message,
|
|
79
|
+
details: error.details
|
|
80
|
+
};
|
|
81
|
+
default: return {
|
|
82
|
+
code: "VALIDATION_ERROR",
|
|
83
|
+
message: error.message,
|
|
84
|
+
details: error.details
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Registry for byline custom fields (Discussion #1174).
|
|
90
|
+
*
|
|
91
|
+
* Owns CRUD over `_emdash_byline_fields` and the
|
|
92
|
+
* `options.byline_fields_version` counter that drives cache
|
|
93
|
+
* invalidation in `bylines/field-defs-cache.ts`.
|
|
94
|
+
*
|
|
95
|
+
* **Dirty-bit bookend.** Every mutation runs `markVersionDirty` before
|
|
96
|
+
* the schema write and `markVersionClean` after, as standalone writes
|
|
97
|
+
* (not inside `withTransaction`) so concurrent isolates observe the
|
|
98
|
+
* dirty mark *before* the mutation lands. Parity carries meaning:
|
|
99
|
+
* odd = mutation in flight or crashed mid-flight, even = stable.
|
|
100
|
+
* The cache bypasses the global holder while odd.
|
|
101
|
+
*
|
|
102
|
+
* `markVersionDirty` is parity-aware (idempotent on odd) so a
|
|
103
|
+
* crashed prior attempt doesn't invert the bit.
|
|
104
|
+
* `markVersionClean` always advances to a new even value (+2 from
|
|
105
|
+
* even, +1 from odd) so concurrent mutators can't collapse on the
|
|
106
|
+
* same key and pin a stale cache snapshot. Idempotent-retry exits
|
|
107
|
+
* (`FIELD_EXISTS` / `FIELD_NOT_FOUND` / no-op update) call
|
|
108
|
+
* `markVersionClean` too — same code path doubles as crash recovery
|
|
109
|
+
* and false-clean recovery.
|
|
110
|
+
*
|
|
111
|
+
* The residual race: a reader caching between two concurrent
|
|
112
|
+
* `markVersionClean` calls sees a partial-set snapshot until the
|
|
113
|
+
* second clean lands. Bounded by the inter-clean window (~ms).
|
|
114
|
+
* Schema mutations are admin-only and rare; acceptable for now.
|
|
115
|
+
* A CAS-on-bump or dialect-specific lock is tracked as follow-up.
|
|
116
|
+
*
|
|
117
|
+
* **`deleteField` cascade.** Migration 041 already declares
|
|
118
|
+
* `ON DELETE CASCADE` on both value tables. The explicit deletes
|
|
119
|
+
* here are defense-in-depth against FK-pragma misconfig and mirror
|
|
120
|
+
* `BylineRepository.delete`'s app-level cascade for the bylines
|
|
121
|
+
* domain.
|
|
122
|
+
*
|
|
123
|
+
* Reserved-slug rejection runs at the API layer (zod) *and* here so
|
|
124
|
+
* non-HTTP callers (seeds, scripts) can't bypass the check.
|
|
125
|
+
*/
|
|
126
|
+
var BylineSchemaRegistry = class {
|
|
127
|
+
constructor(db) {
|
|
128
|
+
this.db = db;
|
|
129
|
+
}
|
|
130
|
+
async listFields() {
|
|
131
|
+
return (await this.db.selectFrom("_emdash_byline_fields").selectAll().orderBy("sort_order", "asc").orderBy("created_at", "asc").execute()).map((row) => mapFieldRow(row));
|
|
132
|
+
}
|
|
133
|
+
async getField(slug) {
|
|
134
|
+
const row = await this.db.selectFrom("_emdash_byline_fields").selectAll().where("slug", "=", slug).executeTakeFirst();
|
|
135
|
+
return row ? mapFieldRow(row) : null;
|
|
136
|
+
}
|
|
137
|
+
async getFieldById(id) {
|
|
138
|
+
const row = await this.db.selectFrom("_emdash_byline_fields").selectAll().where("id", "=", id).executeTakeFirst();
|
|
139
|
+
return row ? mapFieldRow(row) : null;
|
|
140
|
+
}
|
|
141
|
+
async createField(input) {
|
|
142
|
+
this.validateSlug(input.slug);
|
|
143
|
+
this.validateLabel(input.label);
|
|
144
|
+
this.validateType(input.type);
|
|
145
|
+
const validation = this.normaliseValidation(input.type, input.validation ?? null);
|
|
146
|
+
if (await this.getField(input.slug)) {
|
|
147
|
+
await this.markVersionClean();
|
|
148
|
+
throw new BylineSchemaError(`Byline field "${input.slug}" already exists`, "FIELD_EXISTS", { slug: input.slug });
|
|
149
|
+
}
|
|
150
|
+
const id = ulid();
|
|
151
|
+
const sortOrder = input.sortOrder ?? await this.nextSortOrder();
|
|
152
|
+
await this.markVersionDirty();
|
|
153
|
+
await withTransaction(this.db, async (trx) => {
|
|
154
|
+
await trx.insertInto("_emdash_byline_fields").values({
|
|
155
|
+
id,
|
|
156
|
+
slug: input.slug,
|
|
157
|
+
label: input.label,
|
|
158
|
+
type: input.type,
|
|
159
|
+
required: input.required ? 1 : 0,
|
|
160
|
+
translatable: input.translatable === false ? 0 : 1,
|
|
161
|
+
validation: validation ? JSON.stringify(validation) : null,
|
|
162
|
+
sort_order: sortOrder
|
|
163
|
+
}).execute();
|
|
164
|
+
});
|
|
165
|
+
await this.markVersionClean();
|
|
166
|
+
const created = await this.getFieldById(id);
|
|
167
|
+
if (!created) throw new BylineSchemaError("Failed to load created field", "FIELD_NOT_FOUND", { id });
|
|
168
|
+
return created;
|
|
169
|
+
}
|
|
170
|
+
async updateField(slug, input) {
|
|
171
|
+
const field = await this.getField(slug);
|
|
172
|
+
if (!field) {
|
|
173
|
+
await this.markVersionClean();
|
|
174
|
+
throw new BylineSchemaError(`Byline field "${slug}" not found`, "FIELD_NOT_FOUND", { slug });
|
|
175
|
+
}
|
|
176
|
+
const updates = {};
|
|
177
|
+
if (input.label !== void 0) {
|
|
178
|
+
this.validateLabel(input.label);
|
|
179
|
+
updates.label = input.label;
|
|
180
|
+
}
|
|
181
|
+
if (input.required !== void 0) updates.required = input.required ? 1 : 0;
|
|
182
|
+
if (input.validation !== void 0) {
|
|
183
|
+
const validation = this.normaliseValidation(field.type, input.validation);
|
|
184
|
+
updates.validation = validation ? JSON.stringify(validation) : null;
|
|
185
|
+
}
|
|
186
|
+
if (input.translatable !== void 0 && input.translatable !== field.translatable) {
|
|
187
|
+
const usage = await this.countFieldValues(field.id);
|
|
188
|
+
if (usage > 0) throw new BylineSchemaError(`Cannot change "translatable" on field "${slug}" while ${usage} value row(s) exist. Delete the values (or the field) and re-create with the new setting.`, "TRANSLATABLE_LOCKED", {
|
|
189
|
+
slug,
|
|
190
|
+
valueCount: usage
|
|
191
|
+
});
|
|
192
|
+
updates.translatable = input.translatable ? 1 : 0;
|
|
193
|
+
}
|
|
194
|
+
if (input.sortOrder !== void 0) updates.sort_order = input.sortOrder;
|
|
195
|
+
if (Object.keys(updates).length === 0) {
|
|
196
|
+
await this.markVersionClean();
|
|
197
|
+
return field;
|
|
198
|
+
}
|
|
199
|
+
updates.updated_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
200
|
+
await this.markVersionDirty();
|
|
201
|
+
await withTransaction(this.db, async (trx) => {
|
|
202
|
+
await trx.updateTable("_emdash_byline_fields").set(updates).where("id", "=", field.id).execute();
|
|
203
|
+
});
|
|
204
|
+
await this.markVersionClean();
|
|
205
|
+
const updated = await this.getFieldById(field.id);
|
|
206
|
+
if (!updated) throw new BylineSchemaError("Failed to load updated field", "FIELD_NOT_FOUND", { slug });
|
|
207
|
+
return updated;
|
|
208
|
+
}
|
|
209
|
+
async deleteField(slug) {
|
|
210
|
+
const field = await this.getField(slug);
|
|
211
|
+
if (!field) {
|
|
212
|
+
await this.markVersionClean();
|
|
213
|
+
throw new BylineSchemaError(`Byline field "${slug}" not found`, "FIELD_NOT_FOUND", { slug });
|
|
214
|
+
}
|
|
215
|
+
await this.markVersionDirty();
|
|
216
|
+
await withTransaction(this.db, async (trx) => {
|
|
217
|
+
await trx.deleteFrom("_emdash_byline_field_values").where("field_id", "=", field.id).execute();
|
|
218
|
+
await trx.deleteFrom("_emdash_byline_field_group_values").where("field_id", "=", field.id).execute();
|
|
219
|
+
await trx.deleteFrom("_emdash_byline_fields").where("id", "=", field.id).execute();
|
|
220
|
+
});
|
|
221
|
+
await this.markVersionClean();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Reorder fields by slug. The input must be the *exact* set of
|
|
225
|
+
* currently registered slugs — no adds, no drops, no duplicates. This
|
|
226
|
+
* keeps the operation invertible (any reorder is followed by a reverse
|
|
227
|
+
* reorder) and removes a class of "did I forget a field?" bugs at the
|
|
228
|
+
* API layer.
|
|
229
|
+
*/
|
|
230
|
+
async reorderFields(slugs) {
|
|
231
|
+
if (new Set(slugs).size !== slugs.length) throw new BylineSchemaError("Reorder input contains duplicate slugs", "REORDER_MISMATCH", { slugs });
|
|
232
|
+
const registeredSlugs = (await this.listFields()).map((f) => f.slug).toSorted();
|
|
233
|
+
const inputSlugs = slugs.toSorted();
|
|
234
|
+
if (registeredSlugs.length !== inputSlugs.length) throw new BylineSchemaError(`Reorder input has ${inputSlugs.length} slug(s); ${registeredSlugs.length} registered`, "REORDER_MISMATCH", {
|
|
235
|
+
registered: registeredSlugs,
|
|
236
|
+
input: inputSlugs
|
|
237
|
+
});
|
|
238
|
+
for (let i = 0; i < registeredSlugs.length; i++) if (registeredSlugs[i] !== inputSlugs[i]) throw new BylineSchemaError("Reorder input does not match the registered field set", "REORDER_MISMATCH", {
|
|
239
|
+
registered: registeredSlugs,
|
|
240
|
+
input: inputSlugs
|
|
241
|
+
});
|
|
242
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
243
|
+
await this.markVersionDirty();
|
|
244
|
+
await withTransaction(this.db, async (trx) => {
|
|
245
|
+
for (let i = 0; i < slugs.length; i++) {
|
|
246
|
+
const slug = slugs[i];
|
|
247
|
+
if (slug === void 0) continue;
|
|
248
|
+
await trx.updateTable("_emdash_byline_fields").set({
|
|
249
|
+
sort_order: i,
|
|
250
|
+
updated_at: now
|
|
251
|
+
}).where("slug", "=", slug).execute();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
await this.markVersionClean();
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Per-table usage counts for a field, plus the sum. Backs the
|
|
258
|
+
* destructive-delete confirm dialog in the admin UI (Phase 5).
|
|
259
|
+
*
|
|
260
|
+
* Both counts are surfaced separately for diagnostic value: a
|
|
261
|
+
* non-zero count on the table that doesn't match the field's current
|
|
262
|
+
* `translatable` flag indicates historical drift (e.g. a flip from
|
|
263
|
+
* an older code path). Today the registry rejects such flips with
|
|
264
|
+
* `TRANSLATABLE_LOCKED`, so any drift originates pre-Phase-2.
|
|
265
|
+
*
|
|
266
|
+
* Throws `FIELD_NOT_FOUND` when the slug doesn't resolve — callers
|
|
267
|
+
* shouldn't get back zero counts for a missing field.
|
|
268
|
+
*/
|
|
269
|
+
async getFieldUsage(slug) {
|
|
270
|
+
const field = await this.getField(slug);
|
|
271
|
+
if (!field) throw new BylineSchemaError(`Byline field "${slug}" not found`, "FIELD_NOT_FOUND", { slug });
|
|
272
|
+
const tr = await this.db.selectFrom("_emdash_byline_field_values").select(({ fn }) => [fn.count("field_id").as("count")]).where("field_id", "=", field.id).executeTakeFirst();
|
|
273
|
+
const grp = await this.db.selectFrom("_emdash_byline_field_group_values").select(({ fn }) => [fn.count("field_id").as("count")]).where("field_id", "=", field.id).executeTakeFirst();
|
|
274
|
+
const translatableValueCount = Number(tr?.count ?? 0);
|
|
275
|
+
const groupValueCount = Number(grp?.count ?? 0);
|
|
276
|
+
return {
|
|
277
|
+
translatableValueCount,
|
|
278
|
+
groupValueCount,
|
|
279
|
+
totalAffectedRows: translatableValueCount + groupValueCount
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Read the persisted version counter. Used by the field-defs cache
|
|
284
|
+
* (Phase 3) to detect invalidation. Returns `0` when the row is
|
|
285
|
+
* missing — covers the "tests that didn't run migration 041" case
|
|
286
|
+
* without throwing.
|
|
287
|
+
*/
|
|
288
|
+
async getVersion() {
|
|
289
|
+
const row = await this.db.selectFrom("options").select("value").where("name", "=", VERSION_KEY).executeTakeFirst();
|
|
290
|
+
if (!row) return 0;
|
|
291
|
+
const parsed = Number.parseInt(row.value, 10);
|
|
292
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Force the version counter to an odd integer ("dirty"). Idempotent
|
|
296
|
+
* on odd so a crashed prior attempt can't invert parity. Upsert (not
|
|
297
|
+
* UPDATE) so a missing row still flips parity — `getVersion` returns
|
|
298
|
+
* 0 on missing, which is even, so a bare UPDATE would leave the
|
|
299
|
+
* cache pinned on a stale snapshot. See the class JSDoc.
|
|
300
|
+
*
|
|
301
|
+
* `options.value` qualified: PG's `ON CONFLICT DO UPDATE` puts both
|
|
302
|
+
* the target and `EXCLUDED.value` in scope; bare `value` is ambiguous.
|
|
303
|
+
*/
|
|
304
|
+
async markVersionDirty() {
|
|
305
|
+
await sql`
|
|
306
|
+
INSERT INTO options (name, value)
|
|
307
|
+
VALUES (${VERSION_KEY}, '1')
|
|
308
|
+
ON CONFLICT(name) DO UPDATE SET value = CASE
|
|
309
|
+
WHEN CAST(options.value AS INTEGER) % 2 = 0
|
|
310
|
+
THEN CAST(CAST(options.value AS INTEGER) + 1 AS TEXT)
|
|
311
|
+
ELSE options.value
|
|
312
|
+
END
|
|
313
|
+
`.execute(this.db);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Force the version counter to a **new** even integer (+2 from even,
|
|
317
|
+
* +1 from odd). Always-advance — never a no-op — so two concurrent
|
|
318
|
+
* mutators can't collapse on the same even key and pin a stale cache
|
|
319
|
+
* snapshot. See the class JSDoc for the concurrent-collapse rationale.
|
|
320
|
+
*
|
|
321
|
+
* `options.value` qualified — see `markVersionDirty`.
|
|
322
|
+
*/
|
|
323
|
+
async markVersionClean() {
|
|
324
|
+
await sql`
|
|
325
|
+
INSERT INTO options (name, value)
|
|
326
|
+
VALUES (${VERSION_KEY}, '2')
|
|
327
|
+
ON CONFLICT(name) DO UPDATE SET value = CASE
|
|
328
|
+
WHEN CAST(options.value AS INTEGER) % 2 = 0
|
|
329
|
+
THEN CAST(CAST(options.value AS INTEGER) + 2 AS TEXT)
|
|
330
|
+
ELSE CAST(CAST(options.value AS INTEGER) + 1 AS TEXT)
|
|
331
|
+
END
|
|
332
|
+
`.execute(this.db);
|
|
333
|
+
}
|
|
334
|
+
async nextSortOrder() {
|
|
335
|
+
const max = (await this.db.selectFrom("_emdash_byline_fields").select(({ fn }) => [fn.max("sort_order").as("max")]).executeTakeFirst())?.max ?? null;
|
|
336
|
+
return max === null ? 0 : max + 1;
|
|
337
|
+
}
|
|
338
|
+
async countFieldValues(fieldId) {
|
|
339
|
+
const tr = await this.db.selectFrom("_emdash_byline_field_values").select(({ fn }) => [fn.count("field_id").as("count")]).where("field_id", "=", fieldId).executeTakeFirst();
|
|
340
|
+
const grp = await this.db.selectFrom("_emdash_byline_field_group_values").select(({ fn }) => [fn.count("field_id").as("count")]).where("field_id", "=", fieldId).executeTakeFirst();
|
|
341
|
+
return Number(tr?.count ?? 0) + Number(grp?.count ?? 0);
|
|
342
|
+
}
|
|
343
|
+
validateSlug(slug) {
|
|
344
|
+
if (!slug || typeof slug !== "string") throw new BylineSchemaError("Byline field slug is required", "INVALID_SLUG", { slug });
|
|
345
|
+
if (slug.length > MAX_SLUG_LENGTH) throw new BylineSchemaError(`Byline field slug must be ${MAX_SLUG_LENGTH} characters or less`, "INVALID_SLUG", { slug });
|
|
346
|
+
try {
|
|
347
|
+
validateIdentifier(slug, "byline field slug");
|
|
348
|
+
} catch (error) {
|
|
349
|
+
throw new BylineSchemaError(error instanceof Error ? error.message : "Invalid byline field slug", "INVALID_SLUG", { slug });
|
|
350
|
+
}
|
|
351
|
+
if (RESERVED_SET.has(slug)) throw new BylineSchemaError(`Byline field slug "${slug}" is reserved`, "RESERVED_SLUG", { slug });
|
|
352
|
+
}
|
|
353
|
+
validateLabel(label) {
|
|
354
|
+
if (!label || typeof label !== "string") throw new BylineSchemaError("Byline field label is required", "INVALID_LABEL", { label });
|
|
355
|
+
if (label.length > MAX_LABEL_LENGTH) throw new BylineSchemaError(`Byline field label must be ${MAX_LABEL_LENGTH} characters or less`, "INVALID_LABEL", { length: label.length });
|
|
356
|
+
}
|
|
357
|
+
validateType(type) {
|
|
358
|
+
if (!TYPE_SET.has(type)) throw new BylineSchemaError(`Byline field type "${type}" is not supported. Valid types: ${[...TYPE_SET].join(", ")}`, "INVALID_TYPE", { type });
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Normalise + validate a validation payload for a given field type.
|
|
362
|
+
*
|
|
363
|
+
* - `select`: `options` is required, must be a non-empty array of unique
|
|
364
|
+
* non-empty strings, capped at `MAX_SELECT_OPTIONS`.
|
|
365
|
+
* - any other type: `options` is silently dropped if present (a future
|
|
366
|
+
* field type might use it, but v1 doesn't).
|
|
367
|
+
*
|
|
368
|
+
* Returns `null` when the resulting validation object is empty, so the
|
|
369
|
+
* storage column stays NULL rather than carrying `'{}'`.
|
|
370
|
+
*/
|
|
371
|
+
normaliseValidation(type, validation) {
|
|
372
|
+
if (type === "select") {
|
|
373
|
+
const options = validation?.options;
|
|
374
|
+
if (!Array.isArray(options) || options.length === 0) throw new BylineSchemaError(`Byline field of type "select" requires non-empty "validation.options"`, "INVALID_VALIDATION", { type });
|
|
375
|
+
if (options.length > MAX_SELECT_OPTIONS) throw new BylineSchemaError(`Byline field "select" cannot have more than ${MAX_SELECT_OPTIONS} options`, "INVALID_VALIDATION", { count: options.length });
|
|
376
|
+
const seen = /* @__PURE__ */ new Set();
|
|
377
|
+
for (const option of options) {
|
|
378
|
+
if (typeof option !== "string" || option.length === 0) throw new BylineSchemaError(`Byline field "select" options must be non-empty strings`, "INVALID_VALIDATION", { option });
|
|
379
|
+
if (seen.has(option)) throw new BylineSchemaError(`Byline field "select" options must be unique`, "INVALID_VALIDATION", { option });
|
|
380
|
+
seen.add(option);
|
|
381
|
+
}
|
|
382
|
+
return { options };
|
|
383
|
+
}
|
|
384
|
+
if (validation == null) return null;
|
|
385
|
+
const { options: _drop, ...rest } = validation;
|
|
386
|
+
return Object.keys(rest).length === 0 ? null : rest;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
function mapFieldRow(row) {
|
|
390
|
+
return {
|
|
391
|
+
id: row.id,
|
|
392
|
+
slug: row.slug,
|
|
393
|
+
label: row.label,
|
|
394
|
+
type: row.type,
|
|
395
|
+
required: row.required === 1,
|
|
396
|
+
translatable: row.translatable === 1,
|
|
397
|
+
validation: row.validation ? JSON.parse(row.validation) : null,
|
|
398
|
+
sortOrder: row.sort_order,
|
|
399
|
+
createdAt: row.created_at,
|
|
400
|
+
updatedAt: row.updated_at
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
//#endregion
|
|
405
|
+
export { BylineSchemaRegistry as n, mapBylineSchemaError as r, BylineSchemaError as t };
|
|
406
|
+
//# sourceMappingURL=byline-registry-CxK5g559.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"byline-registry-CxK5g559.mjs","names":[],"sources":["../src/schema/byline-registry.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport { sql } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { withTransaction } from \"../database/transaction.js\";\nimport type { BylineFieldTable, Database } from \"../database/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport {\n\tBYLINE_FIELD_TYPES,\n\tRESERVED_BYLINE_FIELD_SLUGS,\n\ttype BylineFieldDefinition,\n\ttype BylineFieldType,\n\ttype BylineFieldValidation,\n\ttype CreateBylineFieldInput,\n\ttype UpdateBylineFieldInput,\n} from \"./types.js\";\n\nconst RESERVED_SET: ReadonlySet<string> = new Set(RESERVED_BYLINE_FIELD_SLUGS);\nconst TYPE_SET: ReadonlySet<string> = new Set(BYLINE_FIELD_TYPES);\n\nconst VERSION_KEY = \"byline_fields_version\";\n\n/** Hard cap on the choices array for a `select`-type field. */\nconst MAX_SELECT_OPTIONS = 200;\n/** Hard cap on a slug — mirrors `SchemaRegistry.validateSlug`. */\nconst MAX_SLUG_LENGTH = 63;\n/** Hard cap on a label. Bigger than slugs because labels are display strings. */\nconst MAX_LABEL_LENGTH = 200;\n\n/**\n * Error thrown for byline-schema validation failures. Mirrors\n * `SchemaError` in `registry.ts` so the admin API layer can map a small\n * set of codes to HTTP statuses without inspecting messages.\n *\n * Codes:\n * - `INVALID_SLUG` — slug fails identifier rules or length cap\n * - `RESERVED_SLUG` — slug collides with a fixed `_emdash_bylines` column\n * - `INVALID_TYPE` — type is not one of the five v1 field types\n * - `INVALID_LABEL` — label missing or exceeds length cap\n * - `INVALID_VALIDATION` — validation payload malformed (e.g. `select` with\n * no `options`, duplicates in `options`)\n * - `FIELD_EXISTS` — slug already registered\n * - `FIELD_NOT_FOUND` — slug not registered\n * - `TRANSLATABLE_LOCKED` — attempt to flip `translatable` while stored\n * values reference the field\n * - `REORDER_MISMATCH` — reorder input doesn't match the registered set\n */\nexport class BylineSchemaError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic code: string,\n\t\tpublic details?: Record<string, unknown>,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"BylineSchemaError\";\n\t}\n}\n\n/**\n * Translate a `BylineSchemaError` code to a shared `ErrorCode` for the\n * admin API. HTTP status is then derived by `mapErrorStatus` — this\n * function deliberately doesn't carry one, so the API/handler boundary\n * matches the rest of the codebase (handlers return `ApiResult<T>` with\n * a code, the route layer maps to status via `unwrapResult`).\n *\n * Every code on the right-hand side of `case ... return ...` is defined\n * in `ErrorCode` (`api/errors.ts`). `INVALID_LABEL` and\n * `INVALID_VALIDATION` are intentionally folded into the `default`\n * branch (→ `VALIDATION_ERROR`) so no ad-hoc codes leak out — the\n * registry's domain code names them but the HTTP surface should not.\n *\n * `RESERVED_SLUG` / `INVALID_SLUG` typically don't reach this layer for\n * HTTP callers — the zod schema rejects them first with a clean\n * `VALIDATION_ERROR`. They're still listed so non-HTTP callers (and the\n * test layer) get consistent mapping.\n *\n * `FIELD_NOT_FOUND` is normalised to the shared `NOT_FOUND` code so the\n * admin client can branch on one constant across resource types.\n */\nexport function mapBylineSchemaError(error: BylineSchemaError): {\n\tcode: string;\n\tmessage: string;\n\tdetails?: Record<string, unknown>;\n} {\n\tswitch (error.code) {\n\t\tcase \"FIELD_NOT_FOUND\":\n\t\t\treturn { code: \"NOT_FOUND\", message: error.message, details: error.details };\n\t\tcase \"FIELD_EXISTS\":\n\t\tcase \"TRANSLATABLE_LOCKED\":\n\t\tcase \"REORDER_MISMATCH\":\n\t\tcase \"INVALID_SLUG\":\n\t\tcase \"RESERVED_SLUG\":\n\t\tcase \"INVALID_TYPE\":\n\t\t\treturn { code: error.code, message: error.message, details: error.details };\n\t\tdefault:\n\t\t\t// Catches INVALID_LABEL, INVALID_VALIDATION, and any future\n\t\t\t// registry codes we forget to wire up explicitly.\n\t\t\treturn { code: \"VALIDATION_ERROR\", message: error.message, details: error.details };\n\t}\n}\n\n/**\n * Registry for byline custom fields (Discussion #1174).\n *\n * Owns CRUD over `_emdash_byline_fields` and the\n * `options.byline_fields_version` counter that drives cache\n * invalidation in `bylines/field-defs-cache.ts`.\n *\n * **Dirty-bit bookend.** Every mutation runs `markVersionDirty` before\n * the schema write and `markVersionClean` after, as standalone writes\n * (not inside `withTransaction`) so concurrent isolates observe the\n * dirty mark *before* the mutation lands. Parity carries meaning:\n * odd = mutation in flight or crashed mid-flight, even = stable.\n * The cache bypasses the global holder while odd.\n *\n * `markVersionDirty` is parity-aware (idempotent on odd) so a\n * crashed prior attempt doesn't invert the bit.\n * `markVersionClean` always advances to a new even value (+2 from\n * even, +1 from odd) so concurrent mutators can't collapse on the\n * same key and pin a stale cache snapshot. Idempotent-retry exits\n * (`FIELD_EXISTS` / `FIELD_NOT_FOUND` / no-op update) call\n * `markVersionClean` too — same code path doubles as crash recovery\n * and false-clean recovery.\n *\n * The residual race: a reader caching between two concurrent\n * `markVersionClean` calls sees a partial-set snapshot until the\n * second clean lands. Bounded by the inter-clean window (~ms).\n * Schema mutations are admin-only and rare; acceptable for now.\n * A CAS-on-bump or dialect-specific lock is tracked as follow-up.\n *\n * **`deleteField` cascade.** Migration 041 already declares\n * `ON DELETE CASCADE` on both value tables. The explicit deletes\n * here are defense-in-depth against FK-pragma misconfig and mirror\n * `BylineRepository.delete`'s app-level cascade for the bylines\n * domain.\n *\n * Reserved-slug rejection runs at the API layer (zod) *and* here so\n * non-HTTP callers (seeds, scripts) can't bypass the check.\n */\nexport class BylineSchemaRegistry {\n\tconstructor(private db: Kysely<Database>) {}\n\n\tasync listFields(): Promise<BylineFieldDefinition[]> {\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_fields\")\n\t\t\t.selectAll()\n\t\t\t.orderBy(\"sort_order\", \"asc\")\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.execute();\n\t\treturn rows.map((row) => mapFieldRow(row));\n\t}\n\n\tasync getField(slug: string): Promise<BylineFieldDefinition | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_fields\")\n\t\t\t.selectAll()\n\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? mapFieldRow(row) : null;\n\t}\n\n\tasync getFieldById(id: string): Promise<BylineFieldDefinition | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_fields\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\t\treturn row ? mapFieldRow(row) : null;\n\t}\n\n\tasync createField(input: CreateBylineFieldInput): Promise<BylineFieldDefinition> {\n\t\tthis.validateSlug(input.slug);\n\t\tthis.validateLabel(input.label);\n\t\tthis.validateType(input.type);\n\t\tconst validation = this.normaliseValidation(input.type, input.validation ?? null);\n\n\t\tconst existing = await this.getField(input.slug);\n\t\tif (existing) {\n\t\t\t// Idempotent retry exit — see class JSDoc.\n\t\t\tawait this.markVersionClean();\n\t\t\tthrow new BylineSchemaError(`Byline field \"${input.slug}\" already exists`, \"FIELD_EXISTS\", {\n\t\t\t\tslug: input.slug,\n\t\t\t});\n\t\t}\n\n\t\tconst id = ulid();\n\t\tconst sortOrder = input.sortOrder ?? (await this.nextSortOrder());\n\n\t\tawait this.markVersionDirty();\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.insertInto(\"_emdash_byline_fields\")\n\t\t\t\t.values({\n\t\t\t\t\tid,\n\t\t\t\t\tslug: input.slug,\n\t\t\t\t\tlabel: input.label,\n\t\t\t\t\ttype: input.type,\n\t\t\t\t\trequired: input.required ? 1 : 0,\n\t\t\t\t\ttranslatable: input.translatable === false ? 0 : 1,\n\t\t\t\t\tvalidation: validation ? JSON.stringify(validation) : null,\n\t\t\t\t\tsort_order: sortOrder,\n\t\t\t\t})\n\t\t\t\t.execute();\n\t\t});\n\t\tawait this.markVersionClean();\n\n\t\tconst created = await this.getFieldById(id);\n\t\tif (!created) {\n\t\t\t// Should be unreachable on a working DB — but a typed error\n\t\t\t// beats letting the route returning null on a successful path.\n\t\t\tthrow new BylineSchemaError(\"Failed to load created field\", \"FIELD_NOT_FOUND\", {\n\t\t\t\tid,\n\t\t\t});\n\t\t}\n\t\treturn created;\n\t}\n\n\tasync updateField(slug: string, input: UpdateBylineFieldInput): Promise<BylineFieldDefinition> {\n\t\tconst field = await this.getField(slug);\n\t\tif (!field) {\n\t\t\t// Idempotent retry exit — see class JSDoc.\n\t\t\tawait this.markVersionClean();\n\t\t\tthrow new BylineSchemaError(`Byline field \"${slug}\" not found`, \"FIELD_NOT_FOUND\", {\n\t\t\t\tslug,\n\t\t\t});\n\t\t}\n\n\t\tconst updates: Partial<{\n\t\t\tlabel: string;\n\t\t\trequired: number;\n\t\t\ttranslatable: number;\n\t\t\tvalidation: string | null;\n\t\t\tsort_order: number;\n\t\t\tupdated_at: string;\n\t\t}> = {};\n\n\t\tif (input.label !== undefined) {\n\t\t\tthis.validateLabel(input.label);\n\t\t\tupdates.label = input.label;\n\t\t}\n\n\t\tif (input.required !== undefined) {\n\t\t\tupdates.required = input.required ? 1 : 0;\n\t\t}\n\n\t\tif (input.validation !== undefined) {\n\t\t\t// Validation payload is normalised against the *current* field\n\t\t\t// type — `type` is not updatable, so it's safe to use `field.type`.\n\t\t\tconst validation = this.normaliseValidation(field.type, input.validation);\n\t\t\tupdates.validation = validation ? JSON.stringify(validation) : null;\n\t\t}\n\n\t\tif (input.translatable !== undefined && input.translatable !== field.translatable) {\n\t\t\t// Flipping `translatable` would orphan any values already stored\n\t\t\t// in the table matching the *current* flag. Reject when any\n\t\t\t// value rows reference this field — admins can delete the field\n\t\t\t// (cascading the values) and re-create it with the new flag if\n\t\t\t// they want a clean re-start. Migrating values across tables is\n\t\t\t// out of scope (Discussion #1174 doesn't authorise it).\n\t\t\tconst usage = await this.countFieldValues(field.id);\n\t\t\tif (usage > 0) {\n\t\t\t\tthrow new BylineSchemaError(\n\t\t\t\t\t`Cannot change \"translatable\" on field \"${slug}\" while ${usage} value row(s) exist. ` +\n\t\t\t\t\t\t`Delete the values (or the field) and re-create with the new setting.`,\n\t\t\t\t\t\"TRANSLATABLE_LOCKED\",\n\t\t\t\t\t{ slug, valueCount: usage },\n\t\t\t\t);\n\t\t\t}\n\t\t\tupdates.translatable = input.translatable ? 1 : 0;\n\t\t}\n\n\t\tif (input.sortOrder !== undefined) {\n\t\t\tupdates.sort_order = input.sortOrder;\n\t\t}\n\n\t\tif (Object.keys(updates).length === 0) {\n\t\t\t// No-op update — still advance the clean marker in case\n\t\t\t// we're recovering a crashed prior attempt.\n\t\t\tawait this.markVersionClean();\n\t\t\treturn field;\n\t\t}\n\n\t\tupdates.updated_at = new Date().toISOString();\n\n\t\tawait this.markVersionDirty();\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.updateTable(\"_emdash_byline_fields\")\n\t\t\t\t.set(updates)\n\t\t\t\t.where(\"id\", \"=\", field.id)\n\t\t\t\t.execute();\n\t\t});\n\t\tawait this.markVersionClean();\n\n\t\tconst updated = await this.getFieldById(field.id);\n\t\tif (!updated) {\n\t\t\tthrow new BylineSchemaError(\"Failed to load updated field\", \"FIELD_NOT_FOUND\", {\n\t\t\t\tslug,\n\t\t\t});\n\t\t}\n\t\treturn updated;\n\t}\n\n\tasync deleteField(slug: string): Promise<void> {\n\t\tconst field = await this.getField(slug);\n\t\tif (!field) {\n\t\t\t// Idempotent retry exit — see class JSDoc.\n\t\t\tawait this.markVersionClean();\n\t\t\tthrow new BylineSchemaError(`Byline field \"${slug}\" not found`, \"FIELD_NOT_FOUND\", {\n\t\t\t\tslug,\n\t\t\t});\n\t\t}\n\n\t\t// Delete order matters on D1 (no tx): value rows first, definition\n\t\t// row last, so a crash leaves the definition recoverable on retry\n\t\t// rather than orphan values pointing at a vanished id.\n\t\tawait this.markVersionDirty();\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.deleteFrom(\"_emdash_byline_field_values\")\n\t\t\t\t.where(\"field_id\", \"=\", field.id)\n\t\t\t\t.execute();\n\t\t\tawait trx\n\t\t\t\t.deleteFrom(\"_emdash_byline_field_group_values\")\n\t\t\t\t.where(\"field_id\", \"=\", field.id)\n\t\t\t\t.execute();\n\t\t\tawait trx.deleteFrom(\"_emdash_byline_fields\").where(\"id\", \"=\", field.id).execute();\n\t\t});\n\t\tawait this.markVersionClean();\n\t}\n\n\t/**\n\t * Reorder fields by slug. The input must be the *exact* set of\n\t * currently registered slugs — no adds, no drops, no duplicates. This\n\t * keeps the operation invertible (any reorder is followed by a reverse\n\t * reorder) and removes a class of \"did I forget a field?\" bugs at the\n\t * API layer.\n\t */\n\tasync reorderFields(slugs: string[]): Promise<void> {\n\t\tif (new Set(slugs).size !== slugs.length) {\n\t\t\tthrow new BylineSchemaError(\"Reorder input contains duplicate slugs\", \"REORDER_MISMATCH\", {\n\t\t\t\tslugs,\n\t\t\t});\n\t\t}\n\n\t\tconst registered = await this.listFields();\n\t\tconst registeredSlugs = registered.map((f) => f.slug).toSorted();\n\t\tconst inputSlugs = slugs.toSorted();\n\n\t\tif (registeredSlugs.length !== inputSlugs.length) {\n\t\t\tthrow new BylineSchemaError(\n\t\t\t\t`Reorder input has ${inputSlugs.length} slug(s); ${registeredSlugs.length} registered`,\n\t\t\t\t\"REORDER_MISMATCH\",\n\t\t\t\t{ registered: registeredSlugs, input: inputSlugs },\n\t\t\t);\n\t\t}\n\t\tfor (let i = 0; i < registeredSlugs.length; i++) {\n\t\t\tif (registeredSlugs[i] !== inputSlugs[i]) {\n\t\t\t\tthrow new BylineSchemaError(\n\t\t\t\t\t\"Reorder input does not match the registered field set\",\n\t\t\t\t\t\"REORDER_MISMATCH\",\n\t\t\t\t\t{ registered: registeredSlugs, input: inputSlugs },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tconst now = new Date().toISOString();\n\t\tawait this.markVersionDirty();\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tfor (let i = 0; i < slugs.length; i++) {\n\t\t\t\tconst slug = slugs[i];\n\t\t\t\tif (slug === undefined) continue;\n\t\t\t\tawait trx\n\t\t\t\t\t.updateTable(\"_emdash_byline_fields\")\n\t\t\t\t\t.set({ sort_order: i, updated_at: now })\n\t\t\t\t\t.where(\"slug\", \"=\", slug)\n\t\t\t\t\t.execute();\n\t\t\t}\n\t\t});\n\t\tawait this.markVersionClean();\n\t}\n\n\t/**\n\t * Per-table usage counts for a field, plus the sum. Backs the\n\t * destructive-delete confirm dialog in the admin UI (Phase 5).\n\t *\n\t * Both counts are surfaced separately for diagnostic value: a\n\t * non-zero count on the table that doesn't match the field's current\n\t * `translatable` flag indicates historical drift (e.g. a flip from\n\t * an older code path). Today the registry rejects such flips with\n\t * `TRANSLATABLE_LOCKED`, so any drift originates pre-Phase-2.\n\t *\n\t * Throws `FIELD_NOT_FOUND` when the slug doesn't resolve — callers\n\t * shouldn't get back zero counts for a missing field.\n\t */\n\tasync getFieldUsage(slug: string): Promise<{\n\t\ttranslatableValueCount: number;\n\t\tgroupValueCount: number;\n\t\ttotalAffectedRows: number;\n\t}> {\n\t\tconst field = await this.getField(slug);\n\t\tif (!field) {\n\t\t\tthrow new BylineSchemaError(`Byline field \"${slug}\" not found`, \"FIELD_NOT_FOUND\", {\n\t\t\t\tslug,\n\t\t\t});\n\t\t}\n\t\tconst tr = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_field_values\")\n\t\t\t.select(({ fn }) => [fn.count<number>(\"field_id\").as(\"count\")])\n\t\t\t.where(\"field_id\", \"=\", field.id)\n\t\t\t.executeTakeFirst();\n\t\tconst grp = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_field_group_values\")\n\t\t\t.select(({ fn }) => [fn.count<number>(\"field_id\").as(\"count\")])\n\t\t\t.where(\"field_id\", \"=\", field.id)\n\t\t\t.executeTakeFirst();\n\t\tconst translatableValueCount = Number(tr?.count ?? 0);\n\t\tconst groupValueCount = Number(grp?.count ?? 0);\n\t\treturn {\n\t\t\ttranslatableValueCount,\n\t\t\tgroupValueCount,\n\t\t\ttotalAffectedRows: translatableValueCount + groupValueCount,\n\t\t};\n\t}\n\n\t/**\n\t * Read the persisted version counter. Used by the field-defs cache\n\t * (Phase 3) to detect invalidation. Returns `0` when the row is\n\t * missing — covers the \"tests that didn't run migration 041\" case\n\t * without throwing.\n\t */\n\tasync getVersion(): Promise<number> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"options\")\n\t\t\t.select(\"value\")\n\t\t\t.where(\"name\", \"=\", VERSION_KEY)\n\t\t\t.executeTakeFirst();\n\t\tif (!row) return 0;\n\t\tconst parsed = Number.parseInt(row.value, 10);\n\t\treturn Number.isFinite(parsed) ? parsed : 0;\n\t}\n\n\t// ============================================\n\t// Private helpers\n\t// ============================================\n\n\t/**\n\t * Force the version counter to an odd integer (\"dirty\"). Idempotent\n\t * on odd so a crashed prior attempt can't invert parity. Upsert (not\n\t * UPDATE) so a missing row still flips parity — `getVersion` returns\n\t * 0 on missing, which is even, so a bare UPDATE would leave the\n\t * cache pinned on a stale snapshot. See the class JSDoc.\n\t *\n\t * `options.value` qualified: PG's `ON CONFLICT DO UPDATE` puts both\n\t * the target and `EXCLUDED.value` in scope; bare `value` is ambiguous.\n\t */\n\tprivate async markVersionDirty(): Promise<void> {\n\t\tawait sql`\n\t\t\tINSERT INTO options (name, value)\n\t\t\tVALUES (${VERSION_KEY}, '1')\n\t\t\tON CONFLICT(name) DO UPDATE SET value = CASE\n\t\t\t\tWHEN CAST(options.value AS INTEGER) % 2 = 0\n\t\t\t\t\tTHEN CAST(CAST(options.value AS INTEGER) + 1 AS TEXT)\n\t\t\t\tELSE options.value\n\t\t\tEND\n\t\t`.execute(this.db);\n\t}\n\n\t/**\n\t * Force the version counter to a **new** even integer (+2 from even,\n\t * +1 from odd). Always-advance — never a no-op — so two concurrent\n\t * mutators can't collapse on the same even key and pin a stale cache\n\t * snapshot. See the class JSDoc for the concurrent-collapse rationale.\n\t *\n\t * `options.value` qualified — see `markVersionDirty`.\n\t */\n\tprivate async markVersionClean(): Promise<void> {\n\t\tawait sql`\n\t\t\tINSERT INTO options (name, value)\n\t\t\tVALUES (${VERSION_KEY}, '2')\n\t\t\tON CONFLICT(name) DO UPDATE SET value = CASE\n\t\t\t\tWHEN CAST(options.value AS INTEGER) % 2 = 0\n\t\t\t\t\tTHEN CAST(CAST(options.value AS INTEGER) + 2 AS TEXT)\n\t\t\t\tELSE CAST(CAST(options.value AS INTEGER) + 1 AS TEXT)\n\t\t\tEND\n\t\t`.execute(this.db);\n\t}\n\n\tprivate async nextSortOrder(): Promise<number> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_fields\")\n\t\t\t.select(({ fn }) => [fn.max<number | null>(\"sort_order\").as(\"max\")])\n\t\t\t.executeTakeFirst();\n\t\tconst max = row?.max ?? null;\n\t\treturn max === null ? 0 : max + 1;\n\t}\n\n\tprivate async countFieldValues(fieldId: string): Promise<number> {\n\t\t// Count both per-locale and group-shared values. A field can only\n\t\t// store in one table at a time (translatable picks), but historic\n\t\t// rows might exist in the other if a prior version of this code\n\t\t// allowed the flip — count both to be safe.\n\t\tconst tr = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_field_values\")\n\t\t\t.select(({ fn }) => [fn.count<number>(\"field_id\").as(\"count\")])\n\t\t\t.where(\"field_id\", \"=\", fieldId)\n\t\t\t.executeTakeFirst();\n\t\tconst grp = await this.db\n\t\t\t.selectFrom(\"_emdash_byline_field_group_values\")\n\t\t\t.select(({ fn }) => [fn.count<number>(\"field_id\").as(\"count\")])\n\t\t\t.where(\"field_id\", \"=\", fieldId)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(tr?.count ?? 0) + Number(grp?.count ?? 0);\n\t}\n\n\tprivate validateSlug(slug: string): void {\n\t\tif (!slug || typeof slug !== \"string\") {\n\t\t\tthrow new BylineSchemaError(\"Byline field slug is required\", \"INVALID_SLUG\", { slug });\n\t\t}\n\t\tif (slug.length > MAX_SLUG_LENGTH) {\n\t\t\tthrow new BylineSchemaError(\n\t\t\t\t`Byline field slug must be ${MAX_SLUG_LENGTH} characters or less`,\n\t\t\t\t\"INVALID_SLUG\",\n\t\t\t\t{ slug },\n\t\t\t);\n\t\t}\n\t\t// `validateIdentifier` enforces /^[a-z][a-z0-9_]*$/ — rejects\n\t\t// camelCase, PascalCase, hyphens, leading digits, and identifiers\n\t\t// over 128 characters. We hit the 63-char cap above first, which\n\t\t// matches the content-collection slug cap.\n\t\ttry {\n\t\t\tvalidateIdentifier(slug, \"byline field slug\");\n\t\t} catch (error) {\n\t\t\tthrow new BylineSchemaError(\n\t\t\t\terror instanceof Error ? error.message : \"Invalid byline field slug\",\n\t\t\t\t\"INVALID_SLUG\",\n\t\t\t\t{ slug },\n\t\t\t);\n\t\t}\n\t\tif (RESERVED_SET.has(slug)) {\n\t\t\tthrow new BylineSchemaError(`Byline field slug \"${slug}\" is reserved`, \"RESERVED_SLUG\", {\n\t\t\t\tslug,\n\t\t\t});\n\t\t}\n\t}\n\n\tprivate validateLabel(label: string): void {\n\t\tif (!label || typeof label !== \"string\") {\n\t\t\tthrow new BylineSchemaError(\"Byline field label is required\", \"INVALID_LABEL\", {\n\t\t\t\tlabel,\n\t\t\t});\n\t\t}\n\t\tif (label.length > MAX_LABEL_LENGTH) {\n\t\t\tthrow new BylineSchemaError(\n\t\t\t\t`Byline field label must be ${MAX_LABEL_LENGTH} characters or less`,\n\t\t\t\t\"INVALID_LABEL\",\n\t\t\t\t{ length: label.length },\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate validateType(type: BylineFieldType): void {\n\t\tif (!TYPE_SET.has(type)) {\n\t\t\tthrow new BylineSchemaError(\n\t\t\t\t`Byline field type \"${type}\" is not supported. Valid types: ${[...TYPE_SET].join(\", \")}`,\n\t\t\t\t\"INVALID_TYPE\",\n\t\t\t\t{ type },\n\t\t\t);\n\t\t}\n\t}\n\n\t/**\n\t * Normalise + validate a validation payload for a given field type.\n\t *\n\t * - `select`: `options` is required, must be a non-empty array of unique\n\t * non-empty strings, capped at `MAX_SELECT_OPTIONS`.\n\t * - any other type: `options` is silently dropped if present (a future\n\t * field type might use it, but v1 doesn't).\n\t *\n\t * Returns `null` when the resulting validation object is empty, so the\n\t * storage column stays NULL rather than carrying `'{}'`.\n\t */\n\tprivate normaliseValidation(\n\t\ttype: BylineFieldType,\n\t\tvalidation: BylineFieldValidation | null,\n\t): BylineFieldValidation | null {\n\t\tif (type === \"select\") {\n\t\t\tconst options = validation?.options;\n\t\t\tif (!Array.isArray(options) || options.length === 0) {\n\t\t\t\tthrow new BylineSchemaError(\n\t\t\t\t\t`Byline field of type \"select\" requires non-empty \"validation.options\"`,\n\t\t\t\t\t\"INVALID_VALIDATION\",\n\t\t\t\t\t{ type },\n\t\t\t\t);\n\t\t\t}\n\t\t\tif (options.length > MAX_SELECT_OPTIONS) {\n\t\t\t\tthrow new BylineSchemaError(\n\t\t\t\t\t`Byline field \"select\" cannot have more than ${MAX_SELECT_OPTIONS} options`,\n\t\t\t\t\t\"INVALID_VALIDATION\",\n\t\t\t\t\t{ count: options.length },\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst seen = new Set<string>();\n\t\t\tfor (const option of options) {\n\t\t\t\tif (typeof option !== \"string\" || option.length === 0) {\n\t\t\t\t\tthrow new BylineSchemaError(\n\t\t\t\t\t\t`Byline field \"select\" options must be non-empty strings`,\n\t\t\t\t\t\t\"INVALID_VALIDATION\",\n\t\t\t\t\t\t{ option },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (seen.has(option)) {\n\t\t\t\t\tthrow new BylineSchemaError(\n\t\t\t\t\t\t`Byline field \"select\" options must be unique`,\n\t\t\t\t\t\t\"INVALID_VALIDATION\",\n\t\t\t\t\t\t{ option },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tseen.add(option);\n\t\t\t}\n\t\t\treturn { options };\n\t\t}\n\n\t\tif (validation == null) return null;\n\t\t// Non-select: drop `options` if present. Strip nothing else — future\n\t\t// field types might extend the shape and we don't want to lose\n\t\t// payload silently. Today's `BylineFieldValidation` is `{ options? }`\n\t\t// only, so this branch is a pass-through; left explicit for clarity.\n\t\tconst { options: _drop, ...rest } = validation;\n\t\treturn Object.keys(rest).length === 0 ? null : (rest as BylineFieldValidation);\n\t}\n}\n\nfunction mapFieldRow(row: {\n\tid: string;\n\tslug: string;\n\tlabel: string;\n\ttype: string;\n\trequired: number;\n\ttranslatable: number;\n\tvalidation: string | null;\n\tsort_order: number;\n\tcreated_at: string;\n\tupdated_at: string;\n}): BylineFieldDefinition {\n\treturn {\n\t\tid: row.id,\n\t\tslug: row.slug,\n\t\tlabel: row.label,\n\t\t// `type` is stored as TEXT but `createField` rejects anything outside\n\t\t// `BYLINE_FIELD_TYPES` before inserting. The assertion narrows on\n\t\t// that write-time guarantee.\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- validated at write\n\t\ttype: row.type as BylineFieldType,\n\t\trequired: row.required === 1,\n\t\ttranslatable: row.translatable === 1,\n\t\t// `validation` is JSON-encoded `BylineFieldValidation | null`, written\n\t\t// only through `normaliseValidation`. The cast matches the\n\t\t// `JSON.parse(...) as T` pattern in `OptionsRepository`.\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- validated at write\n\t\tvalidation: row.validation ? (JSON.parse(row.validation) as BylineFieldValidation) : null,\n\t\tsortOrder: row.sort_order,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\n// Re-export the table type for callers that want to spell it explicitly.\n// Most callers should rely on the Database interface; this is convenience\n// for tests that hand-roll Kysely queries.\nexport type { BylineFieldTable };\n"],"mappings":"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"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { i as __exportAll } from "./runner-
|
|
1
|
+
import { i as __exportAll } from "./runner-eAgyIkeg.mjs";
|
|
2
2
|
import { t as validateIdentifier } from "./validate-VPnKoIzW.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { n as
|
|
6
|
-
import { r as getDb } from "./loader-
|
|
7
|
-
import { i as resolveLocaleChain } from "./resolve-
|
|
3
|
+
import { r as requestCached } from "./request-cache-BYMs-BGX.mjs";
|
|
4
|
+
import { t as BylineRepository } from "./byline-BrIVWLm-.mjs";
|
|
5
|
+
import { n as isMissingTableError } from "./db-errors-CtzxKBxe.mjs";
|
|
6
|
+
import { r as getDb } from "./loader-CJ6lWO0d.mjs";
|
|
7
|
+
import { i as resolveLocaleChain } from "./resolve-BqYMVG0D.mjs";
|
|
8
8
|
import { sql } from "kysely";
|
|
9
9
|
|
|
10
10
|
//#region src/bylines/index.ts
|
|
@@ -110,8 +110,12 @@ async function getBylinesForEntries(collection, entries) {
|
|
|
110
110
|
}
|
|
111
111
|
const explicitByEntry = /* @__PURE__ */ new Map();
|
|
112
112
|
const entriesNeedingAuthorCheck = [];
|
|
113
|
+
const hydrationTargets = [];
|
|
113
114
|
for (const [locale, bucket] of buckets) {
|
|
114
|
-
const localeOpt = locale ? {
|
|
115
|
+
const localeOpt = locale ? {
|
|
116
|
+
locale,
|
|
117
|
+
skipHydration: true
|
|
118
|
+
} : { skipHydration: true };
|
|
115
119
|
const bucketIds = bucket.map((e) => e.id);
|
|
116
120
|
let bylinesMap;
|
|
117
121
|
try {
|
|
@@ -120,7 +124,10 @@ async function getBylinesForEntries(collection, entries) {
|
|
|
120
124
|
if (isMissingTableError(error)) return result;
|
|
121
125
|
throw error;
|
|
122
126
|
}
|
|
123
|
-
for (const [id, list] of bylinesMap)
|
|
127
|
+
for (const [id, list] of bylinesMap) {
|
|
128
|
+
explicitByEntry.set(id, list);
|
|
129
|
+
for (const credit of list) hydrationTargets.push(credit.byline);
|
|
130
|
+
}
|
|
124
131
|
for (const entry of bucket) {
|
|
125
132
|
if (bylinesMap.has(entry.id) && bylinesMap.get(entry.id).length > 0) continue;
|
|
126
133
|
if (entry.authorId) entriesNeedingAuthorCheck.push(entry);
|
|
@@ -137,7 +144,10 @@ async function getBylinesForEntries(collection, entries) {
|
|
|
137
144
|
else authorBuckets.set(key, [entry]);
|
|
138
145
|
}
|
|
139
146
|
for (const [locale, bucket] of authorBuckets) {
|
|
140
|
-
const localeOpt = locale ? {
|
|
147
|
+
const localeOpt = locale ? {
|
|
148
|
+
locale,
|
|
149
|
+
skipHydration: true
|
|
150
|
+
} : { skipHydration: true };
|
|
141
151
|
const authorIds = bucket.map((e) => e.authorId).filter((id) => id !== null);
|
|
142
152
|
const uniqueAuthorIds = [...new Set(authorIds)];
|
|
143
153
|
if (uniqueAuthorIds.length === 0) continue;
|
|
@@ -145,10 +155,14 @@ async function getBylinesForEntries(collection, entries) {
|
|
|
145
155
|
for (const entry of bucket) {
|
|
146
156
|
if (!entry.authorId) continue;
|
|
147
157
|
const f = authorBylineMap.get(entry.authorId);
|
|
148
|
-
if (f)
|
|
158
|
+
if (f) {
|
|
159
|
+
fallbackByEntry.set(entry.id, f);
|
|
160
|
+
hydrationTargets.push(f);
|
|
161
|
+
}
|
|
149
162
|
}
|
|
150
163
|
}
|
|
151
164
|
}
|
|
165
|
+
if (hydrationTargets.length > 0) await repo.hydrateBylineCustomFields(hydrationTargets);
|
|
152
166
|
for (const { id } of entries) {
|
|
153
167
|
const explicit = explicitByEntry.get(id);
|
|
154
168
|
if (explicit && explicit.length > 0) {
|
|
@@ -171,4 +185,4 @@ async function getBylinesForEntries(collection, entries) {
|
|
|
171
185
|
|
|
172
186
|
//#endregion
|
|
173
187
|
export { invalidateBylineCache as i, getByline as n, getBylineBySlug as r, bylines_exports as t };
|
|
174
|
-
//# sourceMappingURL=bylines-
|
|
188
|
+
//# sourceMappingURL=bylines-C_POWmGT.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bylines-n6nykUyI.mjs","names":[],"sources":["../src/bylines/index.ts"],"sourcesContent":["/**\n * Runtime API for bylines\n *\n * Provides functions to query byline profiles and byline credits\n * associated with content entries. Follows the same pattern as\n * the taxonomies runtime API.\n *\n * i18n model (migration 040): byline rows are per-locale and share a\n * `translation_group`. Credits on `_emdash_content_bylines.byline_id` store\n * the translation_group, so a single credit spans every locale of a byline.\n *\n * Hydration is strict per locale: a credit at locale X renders iff a byline\n * row exists at locale X within the credited translation_group. There is no\n * read-time fallback. Mirrors `getEntryTerms` and the convention in PR #916.\n * Locale is passed in by callers — `query.ts` resolves it from the entry's\n * own `data.locale` for the runtime path.\n */\n\nimport { sql } from \"kysely\";\n\nimport { BylineRepository } from \"../database/repositories/byline.js\";\nimport type { BylineSummary, ContentBylineCredit } from \"../database/repositories/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\n\n/**\n * No-op — kept for API compatibility.\n *\n * Used to invalidate a worker-lifetime \"has any byline?\" probe. That\n * probe added a query on every cold isolate to save one query on sites\n * with zero bylines (i.e. the wrong tradeoff), so we dropped it. The\n * batch byline join below returns an empty map for empty sites at the\n * same cost as the probe, without the pre-check.\n */\nexport function invalidateBylineCache(): void {\n\t// Intentionally empty.\n}\n\n/**\n * Get a byline by ID.\n *\n * @example\n * ```ts\n * import { getByline } from \"emdash\";\n *\n * const byline = await getByline(\"01HXYZ...\");\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getByline(id: string): Promise<BylineSummary | null> {\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\treturn repo.findById(id);\n}\n\n/**\n * Get a byline by slug.\n *\n * Standalone identity lookup (e.g. rendering an author profile page). Walks\n * the configured locale fallback chain — same pattern as `getMenu` and\n * `getTerm`, see PR #916. Returns the first match found, walking\n * `[requestedLocale, ...fallbacks, defaultLocale]` in order.\n *\n * Note: this is intentionally different from credit hydration on a content\n * entry (`getEntryBylines`), which is strict per locale with no fallback.\n * The distinction: identity lookups answer \"give me this byline\", and\n * falling back to another locale's display name is acceptable. Credit\n * hydration answers \"what should render on this entry\", where falling back\n * silently surfaces a stale-locale name and contradicts editorial intent.\n *\n * @example\n * ```ts\n * import { getBylineBySlug } from \"emdash\";\n *\n * const byline = await getBylineBySlug(\"jane-doe\", { locale: \"de-de\" });\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getBylineBySlug(\n\tslug: string,\n\toptions?: { locale?: string },\n): Promise<BylineSummary | null> {\n\tconst chain = resolveLocaleChain(options?.locale);\n\tconst cacheKey = `byline-by-slug:${slug}:${chain.length > 0 ? chain.join(\",\") : \"*\"}`;\n\treturn requestCached(cacheKey, async () => {\n\t\tconst db = await getDb();\n\t\tconst repo = new BylineRepository(db);\n\n\t\tif (chain.length === 0) {\n\t\t\t// No i18n or no resolved locale — fall back to the repo's\n\t\t\t// \"lowest-locale-code\" deterministic match.\n\t\t\treturn repo.findBySlug(slug);\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await repo.findBySlug(slug, { locale });\n\t\t\tif (row) return row;\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * Get byline credits for a single content entry.\n *\n * Strict per locale (post-migration 040): a credit renders iff a byline row\n * exists at the requested locale within the credited translation_group.\n * Callers wanting fallback behaviour apply it themselves. When `locale` is\n * omitted, returns every locale variant of every credit on the entry —\n * useful for admin tooling, not for end-user rendering.\n *\n * Internal: not re-exported from the `emdash` package entry point. Every\n * entry returned by `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated by `hydrateEntryBylines` (which uses the batch\n * helper `getBylinesForEntries` directly). Site code should read those\n * fields rather than calling this function.\n */\nexport async function getEntryBylines(\n\tcollection: string,\n\tentryId: string,\n\toptions?: { locale?: string },\n): Promise<ContentBylineCredit[]> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\tconst localeOpt = options?.locale !== undefined ? { locale: options.locale } : undefined;\n\tconst explicit = await repo.getContentBylines(collection, entryId, localeOpt);\n\tif (explicit.length > 0) {\n\t\treturn explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t}\n\n\t// `primary_byline_id` is the explicit-credit sentinel: non-null\n\t// suppresses author fallback even when the credit doesn't resolve\n\t// at this locale.\n\tconst ctx = await getEntryContext(db, collection, entryId);\n\tif (ctx.primaryBylineId) return [];\n\n\tif (ctx.authorId) {\n\t\tconst fallback = await repo.findByUserId(ctx.authorId, localeOpt);\n\t\tif (fallback) {\n\t\t\treturn [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t}\n\t}\n\n\treturn [];\n}\n\n/**\n * Entry reference for batch byline lookups. Passing `authorId`,\n * `primaryBylineId`, and `locale` in directly avoids a per-entry\n * `SELECT` against the content table during hydration.\n *\n * `primaryBylineId` is the explicit-credit sentinel — non-null suppresses\n * author fallback. `locale` drives the strict per-locale join.\n */\nexport interface BylineEntry {\n\tid: string;\n\tauthorId: string | null;\n\tprimaryBylineId?: string | null;\n\tlocale?: string | null;\n}\n\n/**\n * Batch-fetch byline credits for multiple content entries.\n *\n * Per-entry strict-locale hydration: entries are bucketed by `entry.locale`\n * and each bucket gets a single batched call to the strict-locale repo\n * method. Items with no `locale` field (legacy / single-locale installs)\n * share an unscoped bucket.\n *\n * Internal: consumed by `hydrateEntryBylines` in `query.ts` so that every\n * entry returned from `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated. Site code should rely on that eager hydration\n * rather than calling this directly -- this function is not re-exported\n * from the `emdash` package entry point.\n *\n * @param collection - The collection slug (e.g., \"posts\")\n * @param entries - Entry id + authorId + locale (each entry resolves at its own locale)\n * @returns Map from entry ID to array of byline credits\n */\nexport async function getBylinesForEntries(\n\tcollection: string,\n\tentries: BylineEntry[],\n): Promise<Map<string, ContentBylineCredit[]>> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst result = new Map<string, ContentBylineCredit[]>();\n\n\tfor (const { id } of entries) {\n\t\tresult.set(id, []);\n\t}\n\n\tif (entries.length === 0) {\n\t\treturn result;\n\t}\n\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\t// Bucket entries by locale so each bucket fires a single strict-locale\n\t// `getContentBylinesMany` call. Items with no locale field share a\n\t// bucket keyed by null (no `WHERE locale = ?` applied — legacy\n\t// pre-i18n shape).\n\tconst buckets = new Map<string | null, BylineEntry[]>();\n\tfor (const entry of entries) {\n\t\tconst key = entry.locale ?? null;\n\t\tconst bucket = buckets.get(key);\n\t\tif (bucket) bucket.push(entry);\n\t\telse buckets.set(key, [entry]);\n\t}\n\n\t// Sites with no bylines get an empty map back at the same cost as the\n\t// previous \"has any bylines\" probe, without the extra round-trip.\n\t// Pre-migration databases (bylines table missing) fall through to the\n\t// `isMissingTableError` catch below and return empty.\n\tconst explicitByEntry = new Map<string, ContentBylineCredit[]>();\n\tconst entriesNeedingAuthorCheck: BylineEntry[] = [];\n\tfor (const [locale, bucket] of buckets) {\n\t\tconst localeOpt = locale ? { locale } : undefined;\n\t\tconst bucketIds = bucket.map((e) => e.id);\n\t\tlet bylinesMap;\n\t\ttry {\n\t\t\tbylinesMap = await repo.getContentBylinesMany(collection, bucketIds, localeOpt);\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return result;\n\t\t\tthrow error;\n\t\t}\n\t\tfor (const [id, list] of bylinesMap) explicitByEntry.set(id, list);\n\n\t\tfor (const entry of bucket) {\n\t\t\tconst hasResolved = bylinesMap.has(entry.id) && bylinesMap.get(entry.id)!.length > 0;\n\t\t\tif (hasResolved) continue;\n\t\t\tif (entry.authorId) entriesNeedingAuthorCheck.push(entry);\n\t\t}\n\t}\n\n\t// Only entries without an explicit credit (primaryBylineId null) are\n\t// eligible for author fallback.\n\tconst fallbackByEntry = new Map<string, BylineSummary>();\n\tif (entriesNeedingAuthorCheck.length > 0) {\n\t\tconst authorBuckets = new Map<string | null, BylineEntry[]>();\n\t\tfor (const entry of entriesNeedingAuthorCheck) {\n\t\t\tif (entry.primaryBylineId) continue;\n\t\t\tconst key = entry.locale ?? null;\n\t\t\tconst bucket = authorBuckets.get(key);\n\t\t\tif (bucket) bucket.push(entry);\n\t\t\telse authorBuckets.set(key, [entry]);\n\t\t}\n\n\t\tfor (const [locale, bucket] of authorBuckets) {\n\t\t\tconst localeOpt = locale ? { locale } : undefined;\n\t\t\tconst authorIds = bucket.map((e) => e.authorId).filter((id): id is string => id !== null);\n\t\t\tconst uniqueAuthorIds = [...new Set(authorIds)];\n\t\t\tif (uniqueAuthorIds.length === 0) continue;\n\t\t\tconst authorBylineMap = await repo.findByUserIds(uniqueAuthorIds, localeOpt);\n\t\t\tfor (const entry of bucket) {\n\t\t\t\tif (!entry.authorId) continue;\n\t\t\t\tconst f = authorBylineMap.get(entry.authorId);\n\t\t\t\tif (f) fallbackByEntry.set(entry.id, f);\n\t\t\t}\n\t\t}\n\t}\n\n\tfor (const { id } of entries) {\n\t\tconst explicit = explicitByEntry.get(id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\tresult.set(\n\t\t\t\tid,\n\t\t\t\texplicit.map((c) => ({ ...c, source: \"explicit\" as const })),\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fallback = fallbackByEntry.get(id);\n\t\tif (fallback) {\n\t\t\tresult.set(id, [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }]);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/** Reads `author_id` + `primary_byline_id` for one entry in a single query. */\nasync function getEntryContext(\n\tdb: Awaited<ReturnType<typeof getDb>>,\n\tcollection: string,\n\tentryId: string,\n): Promise<{ authorId: string | null; primaryBylineId: string | null }> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst tableName = `ec_${collection}`;\n\n\tconst result = await sql<{\n\t\tauthor_id: string | null;\n\t\tprimary_byline_id: string | null;\n\t}>`\n\t\tSELECT author_id, primary_byline_id FROM ${sql.ref(tableName)}\n\t\tWHERE id = ${entryId}\n\t\tLIMIT 1\n\t`.execute(db);\n\n\tconst row = result.rows[0];\n\treturn {\n\t\tauthorId: row?.author_id ?? null,\n\t\tprimaryBylineId: row?.primary_byline_id ?? null,\n\t};\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bylines-C_POWmGT.mjs","names":[],"sources":["../src/bylines/index.ts"],"sourcesContent":["/**\n * Runtime API for bylines\n *\n * Provides functions to query byline profiles and byline credits\n * associated with content entries. Follows the same pattern as\n * the taxonomies runtime API.\n *\n * i18n model (migration 040): byline rows are per-locale and share a\n * `translation_group`. Credits on `_emdash_content_bylines.byline_id` store\n * the translation_group, so a single credit spans every locale of a byline.\n *\n * Hydration is strict per locale: a credit at locale X renders iff a byline\n * row exists at locale X within the credited translation_group. There is no\n * read-time fallback. Mirrors `getEntryTerms` and the convention in PR #916.\n * Locale is passed in by callers — `query.ts` resolves it from the entry's\n * own `data.locale` for the runtime path.\n */\n\nimport { sql } from \"kysely\";\n\nimport { BylineRepository } from \"../database/repositories/byline.js\";\nimport type { BylineSummary, ContentBylineCredit } from \"../database/repositories/types.js\";\nimport { validateIdentifier } from \"../database/validate.js\";\nimport { resolveLocaleChain } from \"../i18n/resolve.js\";\nimport { getDb } from \"../loader.js\";\nimport { requestCached } from \"../request-cache.js\";\nimport { isMissingTableError } from \"../utils/db-errors.js\";\n\n/**\n * No-op — kept for API compatibility.\n *\n * Used to invalidate a worker-lifetime \"has any byline?\" probe. That\n * probe added a query on every cold isolate to save one query on sites\n * with zero bylines (i.e. the wrong tradeoff), so we dropped it. The\n * batch byline join below returns an empty map for empty sites at the\n * same cost as the probe, without the pre-check.\n */\nexport function invalidateBylineCache(): void {\n\t// Intentionally empty.\n}\n\n/**\n * Get a byline by ID.\n *\n * @example\n * ```ts\n * import { getByline } from \"emdash\";\n *\n * const byline = await getByline(\"01HXYZ...\");\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getByline(id: string): Promise<BylineSummary | null> {\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\treturn repo.findById(id);\n}\n\n/**\n * Get a byline by slug.\n *\n * Standalone identity lookup (e.g. rendering an author profile page). Walks\n * the configured locale fallback chain — same pattern as `getMenu` and\n * `getTerm`, see PR #916. Returns the first match found, walking\n * `[requestedLocale, ...fallbacks, defaultLocale]` in order.\n *\n * Note: this is intentionally different from credit hydration on a content\n * entry (`getEntryBylines`), which is strict per locale with no fallback.\n * The distinction: identity lookups answer \"give me this byline\", and\n * falling back to another locale's display name is acceptable. Credit\n * hydration answers \"what should render on this entry\", where falling back\n * silently surfaces a stale-locale name and contradicts editorial intent.\n *\n * @example\n * ```ts\n * import { getBylineBySlug } from \"emdash\";\n *\n * const byline = await getBylineBySlug(\"jane-doe\", { locale: \"de-de\" });\n * if (byline) {\n * console.log(byline.displayName);\n * }\n * ```\n */\nexport async function getBylineBySlug(\n\tslug: string,\n\toptions?: { locale?: string },\n): Promise<BylineSummary | null> {\n\tconst chain = resolveLocaleChain(options?.locale);\n\tconst cacheKey = `byline-by-slug:${slug}:${chain.length > 0 ? chain.join(\",\") : \"*\"}`;\n\treturn requestCached(cacheKey, async () => {\n\t\tconst db = await getDb();\n\t\tconst repo = new BylineRepository(db);\n\n\t\tif (chain.length === 0) {\n\t\t\t// No i18n or no resolved locale — fall back to the repo's\n\t\t\t// \"lowest-locale-code\" deterministic match.\n\t\t\treturn repo.findBySlug(slug);\n\t\t}\n\n\t\tfor (const locale of chain) {\n\t\t\tconst row = await repo.findBySlug(slug, { locale });\n\t\t\tif (row) return row;\n\t\t}\n\t\treturn null;\n\t});\n}\n\n/**\n * Get byline credits for a single content entry.\n *\n * Strict per locale (post-migration 040): a credit renders iff a byline row\n * exists at the requested locale within the credited translation_group.\n * Callers wanting fallback behaviour apply it themselves. When `locale` is\n * omitted, returns every locale variant of every credit on the entry —\n * useful for admin tooling, not for end-user rendering.\n *\n * Internal: not re-exported from the `emdash` package entry point. Every\n * entry returned by `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated by `hydrateEntryBylines` (which uses the batch\n * helper `getBylinesForEntries` directly). Site code should read those\n * fields rather than calling this function.\n */\nexport async function getEntryBylines(\n\tcollection: string,\n\tentryId: string,\n\toptions?: { locale?: string },\n): Promise<ContentBylineCredit[]> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\tconst localeOpt = options?.locale !== undefined ? { locale: options.locale } : undefined;\n\tconst explicit = await repo.getContentBylines(collection, entryId, localeOpt);\n\tif (explicit.length > 0) {\n\t\treturn explicit.map((c) => ({ ...c, source: \"explicit\" as const }));\n\t}\n\n\t// `primary_byline_id` is the explicit-credit sentinel: non-null\n\t// suppresses author fallback even when the credit doesn't resolve\n\t// at this locale.\n\tconst ctx = await getEntryContext(db, collection, entryId);\n\tif (ctx.primaryBylineId) return [];\n\n\tif (ctx.authorId) {\n\t\tconst fallback = await repo.findByUserId(ctx.authorId, localeOpt);\n\t\tif (fallback) {\n\t\t\treturn [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }];\n\t\t}\n\t}\n\n\treturn [];\n}\n\n/**\n * Entry reference for batch byline lookups. Passing `authorId`,\n * `primaryBylineId`, and `locale` in directly avoids a per-entry\n * `SELECT` against the content table during hydration.\n *\n * `primaryBylineId` is the explicit-credit sentinel — non-null suppresses\n * author fallback. `locale` drives the strict per-locale join.\n */\nexport interface BylineEntry {\n\tid: string;\n\tauthorId: string | null;\n\tprimaryBylineId?: string | null;\n\tlocale?: string | null;\n}\n\n/**\n * Batch-fetch byline credits for multiple content entries.\n *\n * Per-entry strict-locale hydration: entries are bucketed by `entry.locale`\n * and each bucket gets a single batched call to the strict-locale repo\n * method. Items with no `locale` field (legacy / single-locale installs)\n * share an unscoped bucket.\n *\n * Internal: consumed by `hydrateEntryBylines` in `query.ts` so that every\n * entry returned from `getEmDashCollection` / `getEmDashEntry` already has\n * `data.bylines` populated. Site code should rely on that eager hydration\n * rather than calling this directly -- this function is not re-exported\n * from the `emdash` package entry point.\n *\n * @param collection - The collection slug (e.g., \"posts\")\n * @param entries - Entry id + authorId + locale (each entry resolves at its own locale)\n * @returns Map from entry ID to array of byline credits\n */\nexport async function getBylinesForEntries(\n\tcollection: string,\n\tentries: BylineEntry[],\n): Promise<Map<string, ContentBylineCredit[]>> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst result = new Map<string, ContentBylineCredit[]>();\n\n\tfor (const { id } of entries) {\n\t\tresult.set(id, []);\n\t}\n\n\tif (entries.length === 0) {\n\t\treturn result;\n\t}\n\n\tconst db = await getDb();\n\tconst repo = new BylineRepository(db);\n\n\t// Bucket entries by locale so each bucket fires a single strict-locale\n\t// `getContentBylinesMany` call. Items with no locale field share a\n\t// bucket keyed by null (no `WHERE locale = ?` applied — legacy\n\t// pre-i18n shape).\n\tconst buckets = new Map<string | null, BylineEntry[]>();\n\tfor (const entry of entries) {\n\t\tconst key = entry.locale ?? null;\n\t\tconst bucket = buckets.get(key);\n\t\tif (bucket) bucket.push(entry);\n\t\telse buckets.set(key, [entry]);\n\t}\n\n\t// Sites with no bylines get an empty map back at the same cost as the\n\t// previous \"has any bylines\" probe, without the extra round-trip.\n\t// Pre-migration databases (bylines table missing) fall through to the\n\t// `isMissingTableError` catch below and return empty.\n\t//\n\t// Each bucket's `getContentBylinesMany` call uses `skipHydration: true`\n\t// so the per-bucket fetches return bylines with `customFields = {}`.\n\t// We then hydrate the union of returned bylines in a SINGLE batched\n\t// pass via `hydrateBylineCustomFields`. This keeps mixed-locale list\n\t// hydration at one batched group-shared query (and one batched\n\t// translatable query) per request, even when locale buckets reference\n\t// disjoint translation_groups — the strict reading of the Phase 3\n\t// query-count envelope.\n\tconst explicitByEntry = new Map<string, ContentBylineCredit[]>();\n\tconst entriesNeedingAuthorCheck: BylineEntry[] = [];\n\tconst hydrationTargets: BylineSummary[] = [];\n\tfor (const [locale, bucket] of buckets) {\n\t\tconst localeOpt = locale ? { locale, skipHydration: true } : { skipHydration: true };\n\t\tconst bucketIds = bucket.map((e) => e.id);\n\t\tlet bylinesMap;\n\t\ttry {\n\t\t\tbylinesMap = await repo.getContentBylinesMany(collection, bucketIds, localeOpt);\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return result;\n\t\t\tthrow error;\n\t\t}\n\t\tfor (const [id, list] of bylinesMap) {\n\t\t\texplicitByEntry.set(id, list);\n\t\t\tfor (const credit of list) hydrationTargets.push(credit.byline);\n\t\t}\n\n\t\tfor (const entry of bucket) {\n\t\t\tconst hasResolved = bylinesMap.has(entry.id) && bylinesMap.get(entry.id)!.length > 0;\n\t\t\tif (hasResolved) continue;\n\t\t\tif (entry.authorId) entriesNeedingAuthorCheck.push(entry);\n\t\t}\n\t}\n\n\t// Only entries without an explicit credit (primaryBylineId null) are\n\t// eligible for author fallback.\n\tconst fallbackByEntry = new Map<string, BylineSummary>();\n\tif (entriesNeedingAuthorCheck.length > 0) {\n\t\tconst authorBuckets = new Map<string | null, BylineEntry[]>();\n\t\tfor (const entry of entriesNeedingAuthorCheck) {\n\t\t\tif (entry.primaryBylineId) continue;\n\t\t\tconst key = entry.locale ?? null;\n\t\t\tconst bucket = authorBuckets.get(key);\n\t\t\tif (bucket) bucket.push(entry);\n\t\t\telse authorBuckets.set(key, [entry]);\n\t\t}\n\n\t\tfor (const [locale, bucket] of authorBuckets) {\n\t\t\tconst localeOpt: { locale?: string; skipHydration: true } = locale\n\t\t\t\t? { locale, skipHydration: true }\n\t\t\t\t: { skipHydration: true };\n\t\t\tconst authorIds = bucket.map((e) => e.authorId).filter((id): id is string => id !== null);\n\t\t\tconst uniqueAuthorIds = [...new Set(authorIds)];\n\t\t\tif (uniqueAuthorIds.length === 0) continue;\n\t\t\t// `skipHydration: true` returns bylines with `customFields = {}`\n\t\t\t// so the fallback path participates in the single batched\n\t\t\t// `hydrateBylineCustomFields` call below — keeping the query\n\t\t\t// envelope at \"+1 group-shared query per hydration pass\" even\n\t\t\t// when author bylines across locale buckets reference disjoint\n\t\t\t// translation_groups.\n\t\t\tconst authorBylineMap = await repo.findByUserIds(uniqueAuthorIds, localeOpt);\n\t\t\tfor (const entry of bucket) {\n\t\t\t\tif (!entry.authorId) continue;\n\t\t\t\tconst f = authorBylineMap.get(entry.authorId);\n\t\t\t\tif (f) {\n\t\t\t\t\tfallbackByEntry.set(entry.id, f);\n\t\t\t\t\thydrationTargets.push(f);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Single batched hydration over every byline returned from both the\n\t// per-bucket explicit-credit fetches AND the per-bucket author-\n\t// fallback fetches. One translatable query + one group-shared query\n\t// for the whole pass, regardless of bucket count or whether\n\t// translation_groups overlap across locales.\n\tif (hydrationTargets.length > 0) {\n\t\tawait repo.hydrateBylineCustomFields(hydrationTargets);\n\t}\n\n\tfor (const { id } of entries) {\n\t\tconst explicit = explicitByEntry.get(id);\n\t\tif (explicit && explicit.length > 0) {\n\t\t\tresult.set(\n\t\t\t\tid,\n\t\t\t\texplicit.map((c) => ({ ...c, source: \"explicit\" as const })),\n\t\t\t);\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst fallback = fallbackByEntry.get(id);\n\t\tif (fallback) {\n\t\t\tresult.set(id, [{ byline: fallback, sortOrder: 0, roleLabel: null, source: \"inferred\" }]);\n\t\t}\n\t}\n\n\treturn result;\n}\n\n/** Reads `author_id` + `primary_byline_id` for one entry in a single query. */\nasync function getEntryContext(\n\tdb: Awaited<ReturnType<typeof getDb>>,\n\tcollection: string,\n\tentryId: string,\n): Promise<{ authorId: string | null; primaryBylineId: string | null }> {\n\tvalidateIdentifier(collection, \"collection\");\n\tconst tableName = `ec_${collection}`;\n\n\tconst result = await sql<{\n\t\tauthor_id: string | null;\n\t\tprimary_byline_id: string | null;\n\t}>`\n\t\tSELECT author_id, primary_byline_id FROM ${sql.ref(tableName)}\n\t\tWHERE id = ${entryId}\n\t\tLIMIT 1\n\t`.execute(db);\n\n\tconst row = result.rows[0];\n\treturn {\n\t\tauthorId: row?.author_id ?? null,\n\t\tprimaryBylineId: row?.primary_byline_id ?? null,\n\t};\n}\n"],"mappings":"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"}
|