emdash 0.16.1 → 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-DWLnr6-k.d.mts → byline-fields-BNy7Ng1U.d.mts} +151 -23
- 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-D_p_jIP1.d.mts → index-CjKdMZ3U.d.mts} +38 -16
- 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-ITD3PlQd.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-B7TFEvFf.mjs +0 -118
- package/dist/bylines-B7TFEvFf.mjs.map +0 -1
- package/dist/bylines-DWLnr6-k.d.mts.map +0 -1
- package/dist/content-8voQNTXX.mjs.map +0 -1
- package/dist/error-ChfADBuu.mjs.map +0 -1
- package/dist/index-D_p_jIP1.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-ITD3PlQd.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
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { sql, type Kysely, type Selectable } from "kysely";
|
|
2
2
|
import { ulid } from "ulidx";
|
|
3
3
|
|
|
4
|
+
import { getBylineFieldDefs } from "../../bylines/field-defs-cache.js";
|
|
5
|
+
import {
|
|
6
|
+
clearRequestCacheEntry,
|
|
7
|
+
peekRequestCache,
|
|
8
|
+
setRequestCacheEntry,
|
|
9
|
+
} from "../../request-cache.js";
|
|
10
|
+
import type { BylineFieldDefinition, CustomFieldValue } from "../../schema/types.js";
|
|
4
11
|
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
5
12
|
import { listTablesLike } from "../dialect-helpers.js";
|
|
6
13
|
import { withTransaction } from "../transaction.js";
|
|
@@ -8,6 +15,7 @@ import type { BylineTable, Database } from "../types.js";
|
|
|
8
15
|
import { validateIdentifier } from "../validate.js";
|
|
9
16
|
import {
|
|
10
17
|
decodeCursor,
|
|
18
|
+
EmDashValidationError,
|
|
11
19
|
encodeCursor,
|
|
12
20
|
type BylineSummary,
|
|
13
21
|
type ContentBylineCredit,
|
|
@@ -16,6 +24,17 @@ import {
|
|
|
16
24
|
|
|
17
25
|
type BylineRow = Selectable<BylineTable>;
|
|
18
26
|
|
|
27
|
+
/**
|
|
28
|
+
* A byline row optionally augmented with the avatar's media columns, folded in
|
|
29
|
+
* by the `LEFT JOIN media` in the content-credit hydration queries. The plain
|
|
30
|
+
* `selectAll()` finders produce rows without these keys, so they're optional
|
|
31
|
+
* and `rowToByline` defaults them to null.
|
|
32
|
+
*/
|
|
33
|
+
type BylineRowWithAvatar = BylineRow & {
|
|
34
|
+
avatar_storage_key?: string | null;
|
|
35
|
+
avatar_alt?: string | null;
|
|
36
|
+
};
|
|
37
|
+
|
|
19
38
|
export interface CreateBylineInput {
|
|
20
39
|
slug: string;
|
|
21
40
|
displayName: string;
|
|
@@ -36,6 +55,16 @@ export interface CreateBylineInput {
|
|
|
36
55
|
* throws. Mirrors `TaxonomyRepository.create`.
|
|
37
56
|
*/
|
|
38
57
|
translationOf?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Byline custom-field values to seed on the new row (Phase 6 of
|
|
60
|
+
* Discussion #1174). Same semantics as `UpdateBylineInput.customFields`:
|
|
61
|
+
* keys must match registered slugs in `_emdash_byline_fields`, values
|
|
62
|
+
* are validated against the field's type, and writes route to
|
|
63
|
+
* `_emdash_byline_field_values` (translatable) or
|
|
64
|
+
* `_emdash_byline_field_group_values` (group-shared). Validation runs
|
|
65
|
+
* before the row insert so a bad value can't leave a bare byline behind.
|
|
66
|
+
*/
|
|
67
|
+
customFields?: Record<string, unknown>;
|
|
39
68
|
}
|
|
40
69
|
|
|
41
70
|
export interface UpdateBylineInput {
|
|
@@ -46,6 +75,24 @@ export interface UpdateBylineInput {
|
|
|
46
75
|
websiteUrl?: string | null;
|
|
47
76
|
userId?: string | null;
|
|
48
77
|
isGuest?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Byline custom-field values to write (Phase 3 of Discussion #1174).
|
|
80
|
+
*
|
|
81
|
+
* Each key must match a registered slug in `_emdash_byline_fields`;
|
|
82
|
+
* unknown keys throw `EmDashValidationError`. Per-field writes route
|
|
83
|
+
* to `_emdash_byline_field_values` (when the field's `translatable`
|
|
84
|
+
* flag is true) or `_emdash_byline_field_group_values` (when false).
|
|
85
|
+
* A value of `null` clears the row.
|
|
86
|
+
*
|
|
87
|
+
* Values are validated against the field's type:
|
|
88
|
+
* - `string` / `text` / `url` accept a `string`
|
|
89
|
+
* - `boolean` accepts a `boolean`
|
|
90
|
+
* - `select` accepts a `string` that appears in `validation.options`
|
|
91
|
+
*
|
|
92
|
+
* Writes are idempotent (`INSERT … ON CONFLICT DO UPDATE`), so
|
|
93
|
+
* retrying the same update produces the same DB state.
|
|
94
|
+
*/
|
|
95
|
+
customFields?: Record<string, unknown>;
|
|
49
96
|
}
|
|
50
97
|
|
|
51
98
|
export interface ContentBylineInput {
|
|
@@ -53,13 +100,15 @@ export interface ContentBylineInput {
|
|
|
53
100
|
roleLabel?: string | null;
|
|
54
101
|
}
|
|
55
102
|
|
|
56
|
-
function rowToByline(row:
|
|
103
|
+
function rowToByline(row: BylineRowWithAvatar): BylineSummary {
|
|
57
104
|
return {
|
|
58
105
|
id: row.id,
|
|
59
106
|
slug: row.slug,
|
|
60
107
|
displayName: row.display_name,
|
|
61
108
|
bio: row.bio,
|
|
62
109
|
avatarMediaId: row.avatar_media_id,
|
|
110
|
+
avatarStorageKey: row.avatar_storage_key ?? null,
|
|
111
|
+
avatarAlt: row.avatar_alt ?? null,
|
|
63
112
|
websiteUrl: row.website_url,
|
|
64
113
|
userId: row.user_id,
|
|
65
114
|
isGuest: row.is_guest === 1,
|
|
@@ -70,6 +119,124 @@ function rowToByline(row: BylineRow): BylineSummary {
|
|
|
70
119
|
};
|
|
71
120
|
}
|
|
72
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Merge a single decoded value into a `BylineSummary.customFields` map.
|
|
124
|
+
* Centralised so the merge semantics (null storage, JSON.parse failure
|
|
125
|
+
* handling) live in one place across both translatable and group-shared
|
|
126
|
+
* paths.
|
|
127
|
+
*
|
|
128
|
+
* A stored row with `value = NULL` (representing an explicit null) is
|
|
129
|
+
* surfaced as `null` in `customFields`. A row with a malformed JSON
|
|
130
|
+
* payload is dropped silently with a `console.warn` — a corrupted
|
|
131
|
+
* payload shouldn't break the entire byline hydration; the field-defs
|
|
132
|
+
* cache will let admins replace the value, and the warning makes the
|
|
133
|
+
* issue debuggable. (Storage path uses `JSON.stringify`, so the only
|
|
134
|
+
* way to get malformed JSON is direct DB tampering or a future
|
|
135
|
+
* migration bug.)
|
|
136
|
+
*/
|
|
137
|
+
function assignCustomFieldValue(
|
|
138
|
+
summary: BylineSummary,
|
|
139
|
+
field: BylineFieldDefinition,
|
|
140
|
+
stored: string | null,
|
|
141
|
+
): void {
|
|
142
|
+
const target = summary.customFields ?? {};
|
|
143
|
+
if (stored === null) {
|
|
144
|
+
target[field.slug] = null;
|
|
145
|
+
} else {
|
|
146
|
+
try {
|
|
147
|
+
// eslint-disable-next-line typescript/no-unsafe-type-assertion -- coerceFieldValue ran at write time, see field-defs-cache.ts
|
|
148
|
+
target[field.slug] = JSON.parse(stored) as CustomFieldValue;
|
|
149
|
+
} catch {
|
|
150
|
+
console.warn(
|
|
151
|
+
`[BylineRepository] dropping malformed JSON for byline=${summary.id} ` +
|
|
152
|
+
`field=${field.slug}: ${stored.slice(0, 60)}`,
|
|
153
|
+
);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
summary.customFields = target;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Coerce a raw write-path value to `CustomFieldValue`, throwing
|
|
162
|
+
* `EmDashValidationError` on type mismatch. `null` clears the field
|
|
163
|
+
* (DELETE in the write path).
|
|
164
|
+
*
|
|
165
|
+
* TODO: `field.required` is not enforced. The admin UI exposes the
|
|
166
|
+
* toggle but the backend accepts missing values; design pass needed
|
|
167
|
+
* on the enforcement model.
|
|
168
|
+
*/
|
|
169
|
+
function coerceFieldValue(field: BylineFieldDefinition, raw: unknown): CustomFieldValue {
|
|
170
|
+
if (raw === null) return null;
|
|
171
|
+
|
|
172
|
+
switch (field.type) {
|
|
173
|
+
case "string":
|
|
174
|
+
case "text": {
|
|
175
|
+
if (typeof raw !== "string") {
|
|
176
|
+
throw new EmDashValidationError(
|
|
177
|
+
`Byline field "${field.slug}" expects a string value (received ${typeof raw})`,
|
|
178
|
+
{ slug: field.slug, type: field.type, received: typeof raw },
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return raw;
|
|
182
|
+
}
|
|
183
|
+
case "url": {
|
|
184
|
+
if (typeof raw !== "string") {
|
|
185
|
+
throw new EmDashValidationError(
|
|
186
|
+
`Byline field "${field.slug}" expects a string value (received ${typeof raw})`,
|
|
187
|
+
{ slug: field.slug, type: field.type, received: typeof raw },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
// Empty string round-trips as a clear from the admin UI; any
|
|
191
|
+
// non-empty value must be a valid http(s) URL. The scheme
|
|
192
|
+
// allowlist mirrors `httpUrl` in `api/schemas/common.ts` —
|
|
193
|
+
// `new URL` alone would accept `javascript:`/`data:` etc.
|
|
194
|
+
if (raw === "") return raw;
|
|
195
|
+
let parsed: URL;
|
|
196
|
+
try {
|
|
197
|
+
parsed = new URL(raw);
|
|
198
|
+
} catch {
|
|
199
|
+
throw new EmDashValidationError(
|
|
200
|
+
`Byline field "${field.slug}" expects a valid URL (received "${raw}")`,
|
|
201
|
+
{ slug: field.slug, type: field.type, received: raw },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
205
|
+
throw new EmDashValidationError(
|
|
206
|
+
`Byline field "${field.slug}" must use http or https scheme (received "${parsed.protocol}")`,
|
|
207
|
+
{ slug: field.slug, type: field.type, received: raw, protocol: parsed.protocol },
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
return raw;
|
|
211
|
+
}
|
|
212
|
+
case "boolean": {
|
|
213
|
+
if (typeof raw !== "boolean") {
|
|
214
|
+
throw new EmDashValidationError(
|
|
215
|
+
`Byline field "${field.slug}" expects a boolean value (received ${typeof raw})`,
|
|
216
|
+
{ slug: field.slug, type: field.type, received: typeof raw },
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return raw;
|
|
220
|
+
}
|
|
221
|
+
case "select": {
|
|
222
|
+
if (typeof raw !== "string") {
|
|
223
|
+
throw new EmDashValidationError(
|
|
224
|
+
`Byline field "${field.slug}" expects a string value (received ${typeof raw})`,
|
|
225
|
+
{ slug: field.slug, type: field.type, received: typeof raw },
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
const options = field.validation?.options ?? [];
|
|
229
|
+
if (!options.includes(raw)) {
|
|
230
|
+
throw new EmDashValidationError(
|
|
231
|
+
`Byline field "${field.slug}" value "${raw}" is not one of the registered choices`,
|
|
232
|
+
{ slug: field.slug, value: raw, options },
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
return raw;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
73
240
|
/**
|
|
74
241
|
* Byline repository for content credits.
|
|
75
242
|
*
|
|
@@ -91,13 +258,248 @@ function rowToByline(row: BylineRow): BylineSummary {
|
|
|
91
258
|
export class BylineRepository {
|
|
92
259
|
constructor(private db: Kysely<Database>) {}
|
|
93
260
|
|
|
261
|
+
// ============================================
|
|
262
|
+
// Custom-field hydration (Phase 3 of #1174)
|
|
263
|
+
// ============================================
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Merge `customFields` onto each `BylineSummary` produced from the
|
|
267
|
+
* given rows. Two batched queries total — one against
|
|
268
|
+
* `_emdash_byline_field_values` (keyed by `byline_id`), one against
|
|
269
|
+
* `_emdash_byline_field_group_values` (keyed by `translation_group`)
|
|
270
|
+
* — both chunked at `SQL_BATCH_SIZE` for D1's bound-parameter cap.
|
|
271
|
+
*
|
|
272
|
+
* When zero fields are registered, every row gets `customFields = {}`
|
|
273
|
+
* with no value-table reads (the field-defs cache returns `[]`).
|
|
274
|
+
* Group-shared values are looked up via the row's `translation_group`,
|
|
275
|
+
* so every locale sibling of the same byline identity sees the same
|
|
276
|
+
* non-translatable value without re-reading per row.
|
|
277
|
+
*
|
|
278
|
+
* **Duplicate-row handling.** Callers (notably `getContentBylinesMany`
|
|
279
|
+
* for list views with repeated authors) can pass the same byline row
|
|
280
|
+
* multiple times. We assign values by *iterating both `rows` and
|
|
281
|
+
* `summaries` in lockstep by index*, not by deduping into a Map keyed
|
|
282
|
+
* on byline id. A Map approach silently drops earlier duplicates' merge
|
|
283
|
+
* step (last writer wins, earlier instances keep their initial `{}`).
|
|
284
|
+
* Iterating by index gives every duplicate its own merged copy.
|
|
285
|
+
*
|
|
286
|
+
* Hydration is *strict per row* — values are merged onto whichever
|
|
287
|
+
* `BylineRow` produced them. Fallback semantics (e.g. "if no value
|
|
288
|
+
* for this locale, show the default-locale value") are not the
|
|
289
|
+
* repository's concern; consumers layer them on top if wanted, the
|
|
290
|
+
* same way `BylineRepository` doesn't resolve locale fallback for
|
|
291
|
+
* the base byline lookup.
|
|
292
|
+
*/
|
|
293
|
+
private async withCustomFields(rows: BylineRow[]): Promise<BylineSummary[]> {
|
|
294
|
+
const summaries = rows.map(rowToByline);
|
|
295
|
+
// Always populate `customFields = {}` (PR plan AC #6) — even when
|
|
296
|
+
// no fields are registered, every BylineSummary carries the empty
|
|
297
|
+
// object. A fresh object per summary so duplicate rows don't share
|
|
298
|
+
// state.
|
|
299
|
+
for (const summary of summaries) {
|
|
300
|
+
summary.customFields = {};
|
|
301
|
+
}
|
|
302
|
+
await this.applyCustomFieldsTo(summaries);
|
|
303
|
+
return summaries;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
private async withCustomFieldsOne(row: BylineRow | undefined): Promise<BylineSummary | null> {
|
|
307
|
+
if (!row) return null;
|
|
308
|
+
const [result] = await this.withCustomFields([row]);
|
|
309
|
+
return result ?? null;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Hydrate `customFields` on each `BylineSummary`, mutating in place.
|
|
314
|
+
*
|
|
315
|
+
* The public entry point for callers that fetch byline rows in
|
|
316
|
+
* multiple passes (e.g. `getBylinesForEntries`, which buckets by
|
|
317
|
+
* locale and calls `getContentBylinesMany` per bucket) and want a
|
|
318
|
+
* single batched hydration over the union of bylines, not one per
|
|
319
|
+
* pass. Use with the `skipHydration` option on the read methods to
|
|
320
|
+
* defer customFields work to a single call here.
|
|
321
|
+
*
|
|
322
|
+
* Two batched queries total (translatable + group-shared) regardless
|
|
323
|
+
* of how many bylines, locales, or translation_groups are in the
|
|
324
|
+
* input — meets the Phase 3 query-count envelope for mixed-locale
|
|
325
|
+
* list views even when sibling locales reference disjoint
|
|
326
|
+
* translation_groups.
|
|
327
|
+
*
|
|
328
|
+
* Replaces any existing `customFields` on each summary with a freshly
|
|
329
|
+
* fetched map. Callers that want to merge rather than replace should
|
|
330
|
+
* not use this entry point.
|
|
331
|
+
*/
|
|
332
|
+
async hydrateBylineCustomFields(summaries: BylineSummary[]): Promise<void> {
|
|
333
|
+
for (const summary of summaries) {
|
|
334
|
+
summary.customFields = {};
|
|
335
|
+
}
|
|
336
|
+
await this.applyCustomFieldsTo(summaries);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Shared merge engine for `withCustomFields` and
|
|
341
|
+
* `hydrateBylineCustomFields`. Reads field defs (cached), batches the
|
|
342
|
+
* translatable + group-shared fetches, and walks `summaries` directly
|
|
343
|
+
* to apply values.
|
|
344
|
+
*
|
|
345
|
+
* Iterates `summaries` (not a `summaryById` map) so duplicate
|
|
346
|
+
* `BylineSummary` objects sharing the same `id` — e.g. the same
|
|
347
|
+
* author credited to multiple entries — each get their own merged
|
|
348
|
+
* values. The previous Map-based dedup silently dropped earlier
|
|
349
|
+
* duplicates' merge step.
|
|
350
|
+
*/
|
|
351
|
+
private async applyCustomFieldsTo(summaries: BylineSummary[]): Promise<void> {
|
|
352
|
+
if (summaries.length === 0) return;
|
|
353
|
+
|
|
354
|
+
const defs = await getBylineFieldDefs(this.db);
|
|
355
|
+
if (defs.length === 0) return;
|
|
356
|
+
|
|
357
|
+
const fieldById = new Map(defs.map((d) => [d.id, d]));
|
|
358
|
+
|
|
359
|
+
// Translatable values, batched by byline_id (unique per locale, so
|
|
360
|
+
// IDs across different locale buckets don't collide — one batched
|
|
361
|
+
// query covers everything).
|
|
362
|
+
const translatableByByline = new Map<string, Map<string, string | null>>();
|
|
363
|
+
const bylineIds = [...new Set(summaries.map((s) => s.id))];
|
|
364
|
+
for (const chunk of chunks(bylineIds, SQL_BATCH_SIZE)) {
|
|
365
|
+
const trRows = await this.db
|
|
366
|
+
.selectFrom("_emdash_byline_field_values")
|
|
367
|
+
.select(["byline_id", "field_id", "value"])
|
|
368
|
+
.where("byline_id", "in", chunk)
|
|
369
|
+
.execute();
|
|
370
|
+
for (const trRow of trRows) {
|
|
371
|
+
let fieldMap = translatableByByline.get(trRow.byline_id);
|
|
372
|
+
if (!fieldMap) {
|
|
373
|
+
fieldMap = new Map();
|
|
374
|
+
translatableByByline.set(trRow.byline_id, fieldMap);
|
|
375
|
+
}
|
|
376
|
+
fieldMap.set(trRow.field_id, trRow.value);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Group-shared values, batched over the union of translation_groups,
|
|
381
|
+
// with per-group request-cache priming so subsequent calls within
|
|
382
|
+
// the same request share the lookup. Together with the
|
|
383
|
+
// `hydrateBylineCustomFields` + `skipHydration` flow in
|
|
384
|
+
// `getBylinesForEntries`, this keeps mixed-locale list views to
|
|
385
|
+
// **one** group-shared query per request, even for disjoint
|
|
386
|
+
// translation_groups across locale buckets.
|
|
387
|
+
const groups = [
|
|
388
|
+
...new Set(
|
|
389
|
+
summaries
|
|
390
|
+
.map((s) => s.translationGroup)
|
|
391
|
+
.filter((g): g is string => typeof g === "string" && g.length > 0),
|
|
392
|
+
),
|
|
393
|
+
];
|
|
394
|
+
const groupByGroup = await this.loadGroupValuesByIds(groups);
|
|
395
|
+
|
|
396
|
+
// Each loop gates on `field.translatable` so a row in the wrong
|
|
397
|
+
// owner table (e.g. left over from a translatable flip) can't
|
|
398
|
+
// leak into hydration.
|
|
399
|
+
for (const summary of summaries) {
|
|
400
|
+
const trValues = translatableByByline.get(summary.id);
|
|
401
|
+
if (trValues) {
|
|
402
|
+
for (const [fieldId, value] of trValues) {
|
|
403
|
+
const field = fieldById.get(fieldId);
|
|
404
|
+
if (!field || !field.translatable) continue;
|
|
405
|
+
assignCustomFieldValue(summary, field, value);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (summary.translationGroup) {
|
|
410
|
+
const grpValues = groupByGroup.get(summary.translationGroup);
|
|
411
|
+
if (grpValues) {
|
|
412
|
+
for (const [fieldId, value] of grpValues) {
|
|
413
|
+
const field = fieldById.get(fieldId);
|
|
414
|
+
if (!field || field.translatable) continue;
|
|
415
|
+
assignCustomFieldValue(summary, field, value);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Resolve the group-shared custom-field values for a set of
|
|
424
|
+
* translation_groups, sharing work across hydration calls within the
|
|
425
|
+
* same request via per-group `requestCached` entries.
|
|
426
|
+
*
|
|
427
|
+
* The non-translatable storage table (`_emdash_byline_field_group_values`)
|
|
428
|
+
* is keyed by `translation_group`, which is locale-agnostic. Combining
|
|
429
|
+
* this method with `skipHydration` on `getContentBylinesMany` and a
|
|
430
|
+
* single `hydrateBylineCustomFields` call (see
|
|
431
|
+
* `getBylinesForEntries`) keeps mixed-locale list hydration to **one**
|
|
432
|
+
* batched group-shared SQL per request — even with disjoint
|
|
433
|
+
* translation_groups across locale buckets. Solo callers (`findById`,
|
|
434
|
+
* `findMany`, etc.) still get the same per-call batching they had
|
|
435
|
+
* before; the cache simply means a second call in the same request
|
|
436
|
+
* for an overlapping group is free.
|
|
437
|
+
*
|
|
438
|
+
* Cache key: `byline-field-group-values:${groupId}` — one entry per
|
|
439
|
+
* group. Writes use `setRequestCacheEntry` (idempotent, doesn't
|
|
440
|
+
* overwrite); `BylineRepository.update` calls `clearRequestCacheEntry`
|
|
441
|
+
* after a group-shared write to keep the cache fresh within the same
|
|
442
|
+
* request.
|
|
443
|
+
*/
|
|
444
|
+
private async loadGroupValuesByIds(
|
|
445
|
+
groups: string[],
|
|
446
|
+
): Promise<Map<string, Map<string, string | null>>> {
|
|
447
|
+
const result = new Map<string, Map<string, string | null>>();
|
|
448
|
+
if (groups.length === 0) return result;
|
|
449
|
+
|
|
450
|
+
// First pass: pull any already-cached groups from the request scope.
|
|
451
|
+
const missing: string[] = [];
|
|
452
|
+
for (const g of groups) {
|
|
453
|
+
const cached = peekRequestCache<Map<string, string | null>>(`byline-field-group-values:${g}`);
|
|
454
|
+
if (cached) {
|
|
455
|
+
result.set(g, await cached);
|
|
456
|
+
} else {
|
|
457
|
+
missing.push(g);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (missing.length === 0) return result;
|
|
462
|
+
|
|
463
|
+
// Second pass: one batched SQL for the union of all missing groups
|
|
464
|
+
// (chunked for D1's bound-parameter cap). Initialise empty maps for
|
|
465
|
+
// missing groups so the primed cache covers "this group has no
|
|
466
|
+
// values" — preventing a re-fetch on subsequent calls.
|
|
467
|
+
const fetched = new Map<string, Map<string, string | null>>();
|
|
468
|
+
for (const g of missing) fetched.set(g, new Map());
|
|
469
|
+
for (const chunk of chunks(missing, SQL_BATCH_SIZE)) {
|
|
470
|
+
const grpRows = await this.db
|
|
471
|
+
.selectFrom("_emdash_byline_field_group_values")
|
|
472
|
+
.select(["translation_group", "field_id", "value"])
|
|
473
|
+
.where("translation_group", "in", chunk)
|
|
474
|
+
.execute();
|
|
475
|
+
for (const grpRow of grpRows) {
|
|
476
|
+
const fieldMap = fetched.get(grpRow.translation_group);
|
|
477
|
+
if (!fieldMap) continue;
|
|
478
|
+
fieldMap.set(grpRow.field_id, grpRow.value);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
for (const g of missing) {
|
|
483
|
+
const m = fetched.get(g);
|
|
484
|
+
if (!m) continue;
|
|
485
|
+
setRequestCacheEntry(`byline-field-group-values:${g}`, m);
|
|
486
|
+
result.set(g, m);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ============================================
|
|
493
|
+
// Reads
|
|
494
|
+
// ============================================
|
|
495
|
+
|
|
94
496
|
async findById(id: string): Promise<BylineSummary | null> {
|
|
95
497
|
const row = await this.db
|
|
96
498
|
.selectFrom("_emdash_bylines")
|
|
97
499
|
.selectAll()
|
|
98
500
|
.where("id", "=", id)
|
|
99
501
|
.executeTakeFirst();
|
|
100
|
-
return
|
|
502
|
+
return this.withCustomFieldsOne(row);
|
|
101
503
|
}
|
|
102
504
|
|
|
103
505
|
/**
|
|
@@ -109,7 +511,7 @@ export class BylineRepository {
|
|
|
109
511
|
let query = this.db.selectFrom("_emdash_bylines").selectAll().where("slug", "=", slug);
|
|
110
512
|
if (options?.locale !== undefined) query = query.where("locale", "=", options.locale);
|
|
111
513
|
const row = await query.orderBy("locale", "asc").executeTakeFirst();
|
|
112
|
-
return
|
|
514
|
+
return this.withCustomFieldsOne(row);
|
|
113
515
|
}
|
|
114
516
|
|
|
115
517
|
/**
|
|
@@ -122,7 +524,7 @@ export class BylineRepository {
|
|
|
122
524
|
let query = this.db.selectFrom("_emdash_bylines").selectAll().where("user_id", "=", userId);
|
|
123
525
|
if (options?.locale !== undefined) query = query.where("locale", "=", options.locale);
|
|
124
526
|
const row = await query.orderBy("locale", "asc").executeTakeFirst();
|
|
125
|
-
return
|
|
527
|
+
return this.withCustomFieldsOne(row);
|
|
126
528
|
}
|
|
127
529
|
|
|
128
530
|
async findMany(options?: {
|
|
@@ -176,7 +578,8 @@ export class BylineRepository {
|
|
|
176
578
|
}
|
|
177
579
|
|
|
178
580
|
const rows = await query.execute();
|
|
179
|
-
const
|
|
581
|
+
const pageRows = rows.slice(0, limit);
|
|
582
|
+
const items = await this.withCustomFields(pageRows);
|
|
180
583
|
const result: FindManyResult<BylineSummary> = { items };
|
|
181
584
|
|
|
182
585
|
if (rows.length > limit) {
|
|
@@ -211,15 +614,120 @@ export class BylineRepository {
|
|
|
211
614
|
.where("translation_group", "=", translationGroup)
|
|
212
615
|
.orderBy("locale", "asc")
|
|
213
616
|
.execute();
|
|
214
|
-
return
|
|
617
|
+
return this.withCustomFields(rows);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Validate a `customFields` input map into a write list before any row
|
|
622
|
+
* write — throws `EmDashValidationError` on unknown slugs, type
|
|
623
|
+
* mismatches, or select-choice misses.
|
|
624
|
+
*/
|
|
625
|
+
private async resolveCustomFieldWrites(
|
|
626
|
+
customFields: Record<string, unknown> | undefined,
|
|
627
|
+
): Promise<Array<{ field: BylineFieldDefinition; value: CustomFieldValue }>> {
|
|
628
|
+
if (!customFields || Object.keys(customFields).length === 0) return [];
|
|
629
|
+
const defs = await getBylineFieldDefs(this.db);
|
|
630
|
+
const bySlug = new Map(defs.map((d) => [d.slug, d]));
|
|
631
|
+
const writes: Array<{ field: BylineFieldDefinition; value: CustomFieldValue }> = [];
|
|
632
|
+
for (const [slug, raw] of Object.entries(customFields)) {
|
|
633
|
+
const field = bySlug.get(slug);
|
|
634
|
+
if (!field) {
|
|
635
|
+
throw new EmDashValidationError(`Unknown byline custom field "${slug}"`, {
|
|
636
|
+
slug,
|
|
637
|
+
registered: defs.map((d) => d.slug),
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
writes.push({ field, value: coerceFieldValue(field, raw) });
|
|
641
|
+
}
|
|
642
|
+
return writes;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Write a validated custom-field list against a byline row inside the
|
|
647
|
+
* caller's transaction. Per-field writes route to
|
|
648
|
+
* `_emdash_byline_field_values` (translatable) or
|
|
649
|
+
* `_emdash_byline_field_group_values` (group-shared); `null` clears.
|
|
650
|
+
* Returns `true` when any group-shared row was touched so the caller
|
|
651
|
+
* can invalidate the per-request cache post-commit.
|
|
652
|
+
*/
|
|
653
|
+
private async applyCustomFieldWritesInTrx(
|
|
654
|
+
trx: Kysely<Database>,
|
|
655
|
+
bylineId: string,
|
|
656
|
+
translationGroup: string,
|
|
657
|
+
writes: Array<{ field: BylineFieldDefinition; value: CustomFieldValue }>,
|
|
658
|
+
now: string,
|
|
659
|
+
): Promise<boolean> {
|
|
660
|
+
if (writes.length === 0) return false;
|
|
661
|
+
let touchedGroupShared = false;
|
|
662
|
+
for (const { field, value } of writes) {
|
|
663
|
+
if (!field.translatable) touchedGroupShared = true;
|
|
664
|
+
if (field.translatable) {
|
|
665
|
+
if (value === null) {
|
|
666
|
+
await trx
|
|
667
|
+
.deleteFrom("_emdash_byline_field_values")
|
|
668
|
+
.where("byline_id", "=", bylineId)
|
|
669
|
+
.where("field_id", "=", field.id)
|
|
670
|
+
.execute();
|
|
671
|
+
} else {
|
|
672
|
+
const encoded = JSON.stringify(value);
|
|
673
|
+
await trx
|
|
674
|
+
.insertInto("_emdash_byline_field_values")
|
|
675
|
+
.values({
|
|
676
|
+
byline_id: bylineId,
|
|
677
|
+
field_id: field.id,
|
|
678
|
+
value: encoded,
|
|
679
|
+
created_at: now,
|
|
680
|
+
updated_at: now,
|
|
681
|
+
})
|
|
682
|
+
.onConflict((oc) =>
|
|
683
|
+
oc.columns(["byline_id", "field_id"]).doUpdateSet({
|
|
684
|
+
value: encoded,
|
|
685
|
+
updated_at: now,
|
|
686
|
+
}),
|
|
687
|
+
)
|
|
688
|
+
.execute();
|
|
689
|
+
}
|
|
690
|
+
} else {
|
|
691
|
+
if (value === null) {
|
|
692
|
+
await trx
|
|
693
|
+
.deleteFrom("_emdash_byline_field_group_values")
|
|
694
|
+
.where("translation_group", "=", translationGroup)
|
|
695
|
+
.where("field_id", "=", field.id)
|
|
696
|
+
.execute();
|
|
697
|
+
} else {
|
|
698
|
+
const encoded = JSON.stringify(value);
|
|
699
|
+
await trx
|
|
700
|
+
.insertInto("_emdash_byline_field_group_values")
|
|
701
|
+
.values({
|
|
702
|
+
translation_group: translationGroup,
|
|
703
|
+
field_id: field.id,
|
|
704
|
+
value: encoded,
|
|
705
|
+
created_at: now,
|
|
706
|
+
updated_at: now,
|
|
707
|
+
})
|
|
708
|
+
.onConflict((oc) =>
|
|
709
|
+
oc.columns(["translation_group", "field_id"]).doUpdateSet({
|
|
710
|
+
value: encoded,
|
|
711
|
+
updated_at: now,
|
|
712
|
+
}),
|
|
713
|
+
)
|
|
714
|
+
.execute();
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return touchedGroupShared;
|
|
215
719
|
}
|
|
216
720
|
|
|
217
721
|
async create(input: CreateBylineInput): Promise<BylineSummary> {
|
|
218
722
|
const id = ulid();
|
|
219
723
|
const now = new Date().toISOString();
|
|
220
724
|
|
|
221
|
-
//
|
|
222
|
-
//
|
|
725
|
+
// Validate customFields before opening the transaction so a bad
|
|
726
|
+
// value surfaces as VALIDATION_ERROR without aborting an insert.
|
|
727
|
+
const customFieldWrites = await this.resolveCustomFieldWrites(input.customFields);
|
|
728
|
+
|
|
729
|
+
// translationOf joins the source's group; otherwise mint a fresh
|
|
730
|
+
// group = id (matches migration 040's backfill pattern).
|
|
223
731
|
let translationGroup: string = id;
|
|
224
732
|
if (input.translationOf) {
|
|
225
733
|
const source = await this.findById(input.translationOf);
|
|
@@ -227,25 +735,44 @@ export class BylineRepository {
|
|
|
227
735
|
translationGroup = source.translationGroup ?? source.id;
|
|
228
736
|
}
|
|
229
737
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
738
|
+
// Wrap insert + custom-field writes in one transaction so a
|
|
739
|
+
// partial failure rolls both back on Node/PG. D1 still has its
|
|
740
|
+
// own no-transactions limitation — recovery for that path lives
|
|
741
|
+
// in `handleBylineCreate`.
|
|
742
|
+
let touchedGroupShared = false;
|
|
743
|
+
await withTransaction(this.db, async (trx) => {
|
|
744
|
+
await trx
|
|
745
|
+
.insertInto("_emdash_bylines")
|
|
746
|
+
.values({
|
|
747
|
+
id,
|
|
748
|
+
slug: input.slug,
|
|
749
|
+
display_name: input.displayName,
|
|
750
|
+
bio: input.bio ?? null,
|
|
751
|
+
avatar_media_id: input.avatarMediaId ?? null,
|
|
752
|
+
website_url: input.websiteUrl ?? null,
|
|
753
|
+
user_id: input.userId ?? null,
|
|
754
|
+
is_guest: input.isGuest ? 1 : 0,
|
|
755
|
+
created_at: now,
|
|
756
|
+
updated_at: now,
|
|
757
|
+
// Omit `locale` so the DB DEFAULT (configured defaultLocale)
|
|
758
|
+
// applies — matches TaxonomyRepository.create.
|
|
759
|
+
...(input.locale !== undefined ? { locale: input.locale } : {}),
|
|
760
|
+
translation_group: translationGroup,
|
|
761
|
+
})
|
|
762
|
+
.execute();
|
|
763
|
+
|
|
764
|
+
touchedGroupShared = await this.applyCustomFieldWritesInTrx(
|
|
765
|
+
trx,
|
|
233
766
|
id,
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
// When omitted the DB DEFAULT (configured defaultLocale) is used —
|
|
244
|
-
// keeps behaviour consistent with TaxonomyRepository.create.
|
|
245
|
-
...(input.locale !== undefined ? { locale: input.locale } : {}),
|
|
246
|
-
translation_group: translationGroup,
|
|
247
|
-
})
|
|
248
|
-
.execute();
|
|
767
|
+
translationGroup,
|
|
768
|
+
customFieldWrites,
|
|
769
|
+
now,
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
if (touchedGroupShared) {
|
|
774
|
+
clearRequestCacheEntry(`byline-field-group-values:${translationGroup}`);
|
|
775
|
+
}
|
|
249
776
|
|
|
250
777
|
const byline = await this.findById(id);
|
|
251
778
|
if (!byline) {
|
|
@@ -258,9 +785,12 @@ export class BylineRepository {
|
|
|
258
785
|
const existing = await this.findById(id);
|
|
259
786
|
if (!existing) return null;
|
|
260
787
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
788
|
+
// Validate customFields before opening the transaction so a bad
|
|
789
|
+
// value surfaces as VALIDATION_ERROR without aborting an update.
|
|
790
|
+
const customFieldWrites = await this.resolveCustomFieldWrites(input.customFields);
|
|
791
|
+
|
|
792
|
+
const now = new Date().toISOString();
|
|
793
|
+
const updates: Record<string, unknown> = { updated_at: now };
|
|
264
794
|
|
|
265
795
|
if (input.slug !== undefined) updates.slug = input.slug;
|
|
266
796
|
if (input.displayName !== undefined) updates.display_name = input.displayName;
|
|
@@ -270,19 +800,58 @@ export class BylineRepository {
|
|
|
270
800
|
if (input.userId !== undefined) updates.user_id = input.userId;
|
|
271
801
|
if (input.isGuest !== undefined) updates.is_guest = input.isGuest ? 1 : 0;
|
|
272
802
|
|
|
273
|
-
|
|
803
|
+
const group = existing.translationGroup ?? existing.id;
|
|
804
|
+
// Wrap row update + custom-field writes in one transaction so a
|
|
805
|
+
// partial failure rolls both back on Node/PG. The post-commit
|
|
806
|
+
// invalidation below clears the per-request cache that the
|
|
807
|
+
// top-of-method `findById` populated for this group.
|
|
808
|
+
let touchedGroupShared = false;
|
|
809
|
+
await withTransaction(this.db, async (trx) => {
|
|
810
|
+
await trx.updateTable("_emdash_bylines").set(updates).where("id", "=", id).execute();
|
|
811
|
+
touchedGroupShared = await this.applyCustomFieldWritesInTrx(
|
|
812
|
+
trx,
|
|
813
|
+
id,
|
|
814
|
+
group,
|
|
815
|
+
customFieldWrites,
|
|
816
|
+
now,
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (touchedGroupShared) {
|
|
821
|
+
clearRequestCacheEntry(`byline-field-group-values:${group}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
274
824
|
return await this.findById(id);
|
|
275
825
|
}
|
|
276
826
|
|
|
277
827
|
/**
|
|
278
828
|
* Delete a byline row. When this row is the last sibling in its
|
|
279
|
-
* translation group, also drops every junction row pointing at the group
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
* the
|
|
829
|
+
* translation group, also drops every junction row pointing at the group,
|
|
830
|
+
* clears `primary_byline_id` references, and removes the byline's
|
|
831
|
+
* non-translatable custom-field values. When other siblings remain in
|
|
832
|
+
* the group, junctions, `primary_byline_id` pointers, and group-shared
|
|
833
|
+
* custom-field values stay intact — the credit (and its shared metadata)
|
|
834
|
+
* lives on at other locales.
|
|
835
|
+
*
|
|
836
|
+
* **Application-level cascade.** The byline domain has standardised on
|
|
837
|
+
* app-level cascade rather than trusting FK ON DELETE CASCADE, partly
|
|
838
|
+
* because migration 040 had to strip its own FK to support the
|
|
839
|
+
* translation_group remap (#1021), and partly so cleanup doesn't
|
|
840
|
+
* depend on `PRAGMA foreign_keys = ON` (set in production via
|
|
841
|
+
* `connection.ts:60`, but easy to bypass in tests, scripts, and
|
|
842
|
+
* one-off tools). Every byline-related deletion table is cleared
|
|
843
|
+
* explicitly here:
|
|
283
844
|
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
845
|
+
* - `_emdash_byline_field_values` (per-byline translatable values) —
|
|
846
|
+
* migration 041 declares FK ON DELETE CASCADE on `byline_id`; the
|
|
847
|
+
* explicit DELETE removes the dependency on that pragma.
|
|
848
|
+
* - `_emdash_content_bylines` — migration 040 dropped its FK.
|
|
849
|
+
* - `ec_*.primary_byline_id` — never had an FK.
|
|
850
|
+
* - `_emdash_byline_field_group_values` (translation-group-keyed) —
|
|
851
|
+
* keyed by a text column with no FK to bylines, so app-level cleanup
|
|
852
|
+
* is the only path.
|
|
853
|
+
*
|
|
854
|
+
* The FKs that remain (migration 041) serve as defense-in-depth.
|
|
286
855
|
*/
|
|
287
856
|
async delete(id: string): Promise<boolean> {
|
|
288
857
|
const existing = await this.findById(id);
|
|
@@ -291,6 +860,14 @@ export class BylineRepository {
|
|
|
291
860
|
const group = existing.translationGroup ?? existing.id;
|
|
292
861
|
|
|
293
862
|
await withTransaction(this.db, async (trx) => {
|
|
863
|
+
// Per-row translatable custom-field values. Done BEFORE the
|
|
864
|
+
// byline row delete so the application-level cleanup is
|
|
865
|
+
// observable in the transaction log even if FK enforcement is
|
|
866
|
+
// off; migration 041's FK ON DELETE CASCADE would catch any
|
|
867
|
+
// row we miss, but the explicit DELETE is what the rest of
|
|
868
|
+
// the byline domain expects to see.
|
|
869
|
+
await trx.deleteFrom("_emdash_byline_field_values").where("byline_id", "=", id).execute();
|
|
870
|
+
|
|
294
871
|
await trx.deleteFrom("_emdash_bylines").where("id", "=", id).execute();
|
|
295
872
|
|
|
296
873
|
// Count remaining siblings in the translation group. If none
|
|
@@ -307,6 +884,19 @@ export class BylineRepository {
|
|
|
307
884
|
// Last sibling gone: cascade in application code.
|
|
308
885
|
await trx.deleteFrom("_emdash_content_bylines").where("byline_id", "=", group).execute();
|
|
309
886
|
|
|
887
|
+
// Group-shared custom-field values are keyed by translation_group
|
|
888
|
+
// (no FK to bylines), so they don't cascade with the byline row.
|
|
889
|
+
// Clean them up explicitly so deleting the last sibling of an
|
|
890
|
+
// identity doesn't leave orphan group values pointing at a
|
|
891
|
+
// vanished translation group. Per-row translatable values
|
|
892
|
+
// (`_emdash_byline_field_values` keyed by byline_id) already
|
|
893
|
+
// cascaded when each sibling row was deleted, so no extra
|
|
894
|
+
// cleanup is needed for that table.
|
|
895
|
+
await trx
|
|
896
|
+
.deleteFrom("_emdash_byline_field_group_values")
|
|
897
|
+
.where("translation_group", "=", group)
|
|
898
|
+
.execute();
|
|
899
|
+
|
|
310
900
|
const tableNames = await listTablesLike(trx, "ec_%");
|
|
311
901
|
for (const tableName of tableNames) {
|
|
312
902
|
validateIdentifier(tableName, "content table");
|
|
@@ -336,6 +926,7 @@ export class BylineRepository {
|
|
|
336
926
|
let query = this.db
|
|
337
927
|
.selectFrom("_emdash_content_bylines as cb")
|
|
338
928
|
.innerJoin("_emdash_bylines as b", "b.translation_group", "cb.byline_id")
|
|
929
|
+
.leftJoin("media as m", "m.id", "b.avatar_media_id")
|
|
339
930
|
.select([
|
|
340
931
|
"cb.sort_order as sort_order",
|
|
341
932
|
"cb.role_label as role_label",
|
|
@@ -344,6 +935,8 @@ export class BylineRepository {
|
|
|
344
935
|
"b.display_name as display_name",
|
|
345
936
|
"b.bio as bio",
|
|
346
937
|
"b.avatar_media_id as avatar_media_id",
|
|
938
|
+
"m.storage_key as avatar_storage_key",
|
|
939
|
+
"m.alt as avatar_alt",
|
|
347
940
|
"b.website_url as website_url",
|
|
348
941
|
"b.user_id as user_id",
|
|
349
942
|
"b.is_guest as is_guest",
|
|
@@ -358,11 +951,42 @@ export class BylineRepository {
|
|
|
358
951
|
if (options?.locale !== undefined) query = query.where("b.locale", "=", options.locale);
|
|
359
952
|
|
|
360
953
|
const rows = await query.execute();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
954
|
+
// Reconstruct byline rows to feed `withCustomFields`. The JOIN selects
|
|
955
|
+
// the `BylineRow` columns under the `b.` alias plus the avatar media
|
|
956
|
+
// columns from the `media` LEFT JOIN; carry both through so
|
|
957
|
+
// `rowToByline` can populate `avatarStorageKey`/`avatarAlt` (otherwise
|
|
958
|
+
// the join runs but its values are dropped here).
|
|
959
|
+
const bylineRows: BylineRowWithAvatar[] = rows.map((row) => ({
|
|
960
|
+
id: row.id,
|
|
961
|
+
slug: row.slug,
|
|
962
|
+
display_name: row.display_name,
|
|
963
|
+
bio: row.bio,
|
|
964
|
+
avatar_media_id: row.avatar_media_id,
|
|
965
|
+
avatar_storage_key: row.avatar_storage_key,
|
|
966
|
+
avatar_alt: row.avatar_alt,
|
|
967
|
+
website_url: row.website_url,
|
|
968
|
+
user_id: row.user_id,
|
|
969
|
+
is_guest: row.is_guest,
|
|
970
|
+
created_at: row.created_at,
|
|
971
|
+
updated_at: row.updated_at,
|
|
972
|
+
locale: row.locale,
|
|
973
|
+
translation_group: row.translation_group,
|
|
365
974
|
}));
|
|
975
|
+
const hydrated = await this.withCustomFields(bylineRows);
|
|
976
|
+
return rows.map((row, i) => {
|
|
977
|
+
const byline = hydrated[i];
|
|
978
|
+
if (!byline) {
|
|
979
|
+
// Defensive: hydrated and rows are produced in lock-step;
|
|
980
|
+
// this branch is unreachable unless `withCustomFields`
|
|
981
|
+
// breaks its contract.
|
|
982
|
+
throw new Error("getContentBylines: hydration row count mismatch");
|
|
983
|
+
}
|
|
984
|
+
return {
|
|
985
|
+
byline,
|
|
986
|
+
sortOrder: row.sort_order,
|
|
987
|
+
roleLabel: row.role_label,
|
|
988
|
+
};
|
|
989
|
+
});
|
|
366
990
|
}
|
|
367
991
|
|
|
368
992
|
/**
|
|
@@ -414,11 +1038,20 @@ export class BylineRepository {
|
|
|
414
1038
|
* When callers need per-entry-locale filtering (e.g. a list endpoint
|
|
415
1039
|
* returning entries at mixed locales), they should group the input ids by
|
|
416
1040
|
* the entry's locale and call this method once per group.
|
|
1041
|
+
*
|
|
1042
|
+
* When the caller will issue multiple `getContentBylinesMany` calls in
|
|
1043
|
+
* one request (e.g. per locale bucket) and wants a *single* batched
|
|
1044
|
+
* customFields hydration over the union of returned bylines, pass
|
|
1045
|
+
* `skipHydration: true` on each call and finish with
|
|
1046
|
+
* `hydrateBylineCustomFields(allBylines)`. The returned bylines carry
|
|
1047
|
+
* `customFields = {}` until that hydration call runs — matching the
|
|
1048
|
+
* "always populated" invariant from AC #6 — so callers that forget to
|
|
1049
|
+
* hydrate get an empty map rather than `undefined`.
|
|
417
1050
|
*/
|
|
418
1051
|
async getContentBylinesMany(
|
|
419
1052
|
collectionSlug: string,
|
|
420
1053
|
contentIds: string[],
|
|
421
|
-
options?: { locale?: string },
|
|
1054
|
+
options?: { locale?: string; skipHydration?: boolean },
|
|
422
1055
|
): Promise<Map<string, ContentBylineCredit[]>> {
|
|
423
1056
|
const result = new Map<string, ContentBylineCredit[]>();
|
|
424
1057
|
if (contentIds.length === 0) return result;
|
|
@@ -428,6 +1061,7 @@ export class BylineRepository {
|
|
|
428
1061
|
let query = this.db
|
|
429
1062
|
.selectFrom("_emdash_content_bylines as cb")
|
|
430
1063
|
.innerJoin("_emdash_bylines as b", "b.translation_group", "cb.byline_id")
|
|
1064
|
+
.leftJoin("media as m", "m.id", "b.avatar_media_id")
|
|
431
1065
|
.select([
|
|
432
1066
|
"cb.content_id as content_id",
|
|
433
1067
|
"cb.sort_order as sort_order",
|
|
@@ -437,6 +1071,8 @@ export class BylineRepository {
|
|
|
437
1071
|
"b.display_name as display_name",
|
|
438
1072
|
"b.bio as bio",
|
|
439
1073
|
"b.avatar_media_id as avatar_media_id",
|
|
1074
|
+
"m.storage_key as avatar_storage_key",
|
|
1075
|
+
"m.alt as avatar_alt",
|
|
440
1076
|
"b.website_url as website_url",
|
|
441
1077
|
"b.user_id as user_id",
|
|
442
1078
|
"b.is_guest as is_guest",
|
|
@@ -451,11 +1087,45 @@ export class BylineRepository {
|
|
|
451
1087
|
if (options?.locale !== undefined) query = query.where("b.locale", "=", options.locale);
|
|
452
1088
|
|
|
453
1089
|
const rows = await query.execute();
|
|
1090
|
+
// Carry the avatar media columns from the LEFT JOIN through the
|
|
1091
|
+
// reshape so `rowToByline` can populate avatarStorageKey/avatarAlt.
|
|
1092
|
+
const bylineRows: BylineRowWithAvatar[] = rows.map((row) => ({
|
|
1093
|
+
id: row.id,
|
|
1094
|
+
slug: row.slug,
|
|
1095
|
+
display_name: row.display_name,
|
|
1096
|
+
bio: row.bio,
|
|
1097
|
+
avatar_media_id: row.avatar_media_id,
|
|
1098
|
+
avatar_storage_key: row.avatar_storage_key,
|
|
1099
|
+
avatar_alt: row.avatar_alt,
|
|
1100
|
+
website_url: row.website_url,
|
|
1101
|
+
user_id: row.user_id,
|
|
1102
|
+
is_guest: row.is_guest,
|
|
1103
|
+
created_at: row.created_at,
|
|
1104
|
+
updated_at: row.updated_at,
|
|
1105
|
+
locale: row.locale,
|
|
1106
|
+
translation_group: row.translation_group,
|
|
1107
|
+
}));
|
|
454
1108
|
|
|
455
|
-
|
|
1109
|
+
// When `skipHydration` is set, return BylineSummary objects with
|
|
1110
|
+
// `customFields = {}`. The caller is responsible for batching
|
|
1111
|
+
// `hydrateBylineCustomFields` across multiple
|
|
1112
|
+
// `getContentBylinesMany` calls. Otherwise hydrate per-call —
|
|
1113
|
+
// the historical behaviour for solo callers.
|
|
1114
|
+
let bylines: BylineSummary[];
|
|
1115
|
+
if (options?.skipHydration === true) {
|
|
1116
|
+
bylines = bylineRows.map(rowToByline);
|
|
1117
|
+
for (const b of bylines) b.customFields = {};
|
|
1118
|
+
} else {
|
|
1119
|
+
bylines = await this.withCustomFields(bylineRows);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1123
|
+
const row = rows[i];
|
|
1124
|
+
const byline = bylines[i];
|
|
1125
|
+
if (!row || !byline) continue;
|
|
456
1126
|
const contentId = row.content_id;
|
|
457
1127
|
const credit: ContentBylineCredit = {
|
|
458
|
-
byline
|
|
1128
|
+
byline,
|
|
459
1129
|
sortOrder: row.sort_order,
|
|
460
1130
|
roleLabel: row.role_label,
|
|
461
1131
|
};
|
|
@@ -474,24 +1144,62 @@ export class BylineRepository {
|
|
|
474
1144
|
/**
|
|
475
1145
|
* Batch-fetch byline profiles linked to user IDs in a single query.
|
|
476
1146
|
* Strict-locale variant of `findByUserId`.
|
|
1147
|
+
*
|
|
1148
|
+
* `skipHydration: true` returns bylines with `customFields = {}` so
|
|
1149
|
+
* callers issuing multiple `findByUserIds` calls in one request (e.g.
|
|
1150
|
+
* the per-locale-bucket author-fallback path in `getBylinesForEntries`)
|
|
1151
|
+
* can defer customFields hydration to a single batched
|
|
1152
|
+
* `hydrateBylineCustomFields` call across the union — keeping the
|
|
1153
|
+
* Phase 3 query-count envelope at "+1 group-shared query per
|
|
1154
|
+
* hydration pass" even when buckets fetch disjoint author bylines.
|
|
477
1155
|
*/
|
|
478
1156
|
async findByUserIds(
|
|
479
1157
|
userIds: string[],
|
|
480
|
-
options?: { locale?: string },
|
|
1158
|
+
options?: { locale?: string; skipHydration?: boolean },
|
|
481
1159
|
): Promise<Map<string, BylineSummary>> {
|
|
482
1160
|
const result = new Map<string, BylineSummary>();
|
|
483
1161
|
if (userIds.length === 0) return result;
|
|
484
1162
|
|
|
485
1163
|
for (const chunk of chunks(userIds, SQL_BATCH_SIZE)) {
|
|
486
|
-
|
|
487
|
-
|
|
1164
|
+
// LEFT JOIN media so author-inferred bylines (the fallback path in
|
|
1165
|
+
// `getBylinesForEntries`) carry the same render-ready avatar storage
|
|
1166
|
+
// key as explicitly-credited bylines do.
|
|
1167
|
+
let query = this.db
|
|
1168
|
+
.selectFrom("_emdash_bylines as b")
|
|
1169
|
+
.leftJoin("media as m", "m.id", "b.avatar_media_id")
|
|
1170
|
+
.select([
|
|
1171
|
+
"b.id as id",
|
|
1172
|
+
"b.slug as slug",
|
|
1173
|
+
"b.display_name as display_name",
|
|
1174
|
+
"b.bio as bio",
|
|
1175
|
+
"b.avatar_media_id as avatar_media_id",
|
|
1176
|
+
"m.storage_key as avatar_storage_key",
|
|
1177
|
+
"m.alt as avatar_alt",
|
|
1178
|
+
"b.website_url as website_url",
|
|
1179
|
+
"b.user_id as user_id",
|
|
1180
|
+
"b.is_guest as is_guest",
|
|
1181
|
+
"b.created_at as created_at",
|
|
1182
|
+
"b.updated_at as updated_at",
|
|
1183
|
+
"b.locale as locale",
|
|
1184
|
+
"b.translation_group as translation_group",
|
|
1185
|
+
])
|
|
1186
|
+
.where("b.user_id", "in", chunk);
|
|
1187
|
+
if (options?.locale !== undefined) query = query.where("b.locale", "=", options.locale);
|
|
488
1188
|
|
|
489
1189
|
const rows = await query.execute();
|
|
1190
|
+
let bylines: BylineSummary[];
|
|
1191
|
+
if (options?.skipHydration === true) {
|
|
1192
|
+
bylines = rows.map(rowToByline);
|
|
1193
|
+
for (const b of bylines) b.customFields = {};
|
|
1194
|
+
} else {
|
|
1195
|
+
bylines = await this.withCustomFields(rows);
|
|
1196
|
+
}
|
|
490
1197
|
|
|
491
|
-
for (
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1198
|
+
for (let i = 0; i < rows.length; i++) {
|
|
1199
|
+
const row = rows[i];
|
|
1200
|
+
const summary = bylines[i];
|
|
1201
|
+
if (!row || !summary || !row.user_id) continue;
|
|
1202
|
+
result.set(row.user_id, summary);
|
|
495
1203
|
}
|
|
496
1204
|
}
|
|
497
1205
|
return result;
|