emdash 0.37.0 → 0.38.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/{activation-K0kGBWuJ.mjs → activation-DyNTe_Tl.mjs} +3 -3
- package/dist/{activation-K0kGBWuJ.mjs.map → activation-DyNTe_Tl.mjs.map} +1 -1
- package/dist/{adapters-BDQwg236.d.mts → adapters-DCEgOBXi.d.mts} +1 -1
- package/dist/{adapters-BDQwg236.d.mts.map → adapters-DCEgOBXi.d.mts.map} +1 -1
- package/dist/{allowed-origins-BGglilcs.mjs → allowed-origins-GFUVavRj.mjs} +2 -2
- package/dist/{allowed-origins-BGglilcs.mjs.map → allowed-origins-GFUVavRj.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +23 -22
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +5 -5
- package/dist/{api-Dz6rgoa3.mjs → api-BvxJmnFY.mjs} +141 -87
- package/dist/api-BvxJmnFY.mjs.map +1 -0
- package/dist/{api-tokens-BSsQACNe.mjs → api-tokens-xpGqMHrC.mjs} +2 -2
- package/dist/{api-tokens-BSsQACNe.mjs.map → api-tokens-xpGqMHrC.mjs.map} +1 -1
- package/dist/{apply-B4SIOxR1.mjs → apply-DLmM3R7f.mjs} +25 -21
- package/dist/apply-DLmM3R7f.mjs.map +1 -0
- package/dist/{config-it29yDLA.mjs → artifact-fetch-P2JXVwOq.mjs} +8 -199
- package/dist/artifact-fetch-P2JXVwOq.mjs.map +1 -0
- package/dist/astro/index.d.mts +12 -12
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +37 -11
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.mjs +6 -6
- package/dist/astro/middleware/media-usage-write-fence.mjs +3 -3
- package/dist/astro/middleware/redirect.mjs +7 -5
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +3 -3
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +10 -10
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +154 -125
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +6 -6
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +6 -6
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +4 -4
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +9 -9
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +9 -9
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +9 -9
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +16 -16
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +16 -16
- package/dist/astro/routes/api/admin/bylines/index.mjs +16 -16
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +11 -11
- package/dist/astro/routes/api/admin/comments/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/comments/bulk.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/counts.mjs +3 -3
- 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/media-usage/activation.mjs +13 -13
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +15 -15
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +15 -15
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +13 -13
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +16 -16
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +15 -15
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +15 -15
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +3 -3
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +3 -3
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -53
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +56 -53
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +54 -51
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +54 -51
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +55 -52
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +55 -52
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +54 -51
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +54 -51
- 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 +55 -52
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +54 -51
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +55 -52
- 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 +56 -53
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +5 -4
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +56 -53
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +55 -52
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +54 -51
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +54 -51
- 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 +2 -2
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +54 -51
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +1 -1
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +3 -3
- package/dist/astro/routes/api/admin/users/index.mjs +6 -6
- package/dist/astro/routes/api/auth/dev-bypass.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/accept.mjs +1 -1
- package/dist/astro/routes/api/auth/invite/complete.mjs +11 -11
- package/dist/astro/routes/api/auth/invite/index.mjs +8 -8
- package/dist/astro/routes/api/auth/invite/register-options.mjs +10 -10
- package/dist/astro/routes/api/auth/logout.mjs +2 -2
- package/dist/astro/routes/api/auth/magic-link/send.mjs +10 -10
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +2 -2
- package/dist/astro/routes/api/auth/me.mjs +7 -7
- package/dist/astro/routes/api/auth/mode.mjs +2 -2
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +6 -6
- package/dist/astro/routes/api/auth/passkey/index.mjs +1 -1
- package/dist/astro/routes/api/auth/passkey/options.mjs +12 -12
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +10 -10
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +11 -11
- package/dist/astro/routes/api/auth/passkey/verify.mjs +11 -11
- package/dist/astro/routes/api/auth/signup/complete.mjs +11 -11
- package/dist/astro/routes/api/auth/signup/request.mjs +10 -10
- package/dist/astro/routes/api/auth/signup/verify.mjs +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +12 -12
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +13 -8
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/lock.d.mts +10 -0
- package/dist/astro/routes/api/content/_collection_/_id_/lock.d.mts.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs +66 -0
- package/dist/astro/routes/api/content/_collection_/_id_/lock.mjs.map +1 -0
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +11 -11
- package/dist/astro/routes/api/content/_collection_/_id_/publish.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +14 -9
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +19 -17
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +19 -17
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +3 -3
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +15 -8
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +15 -14
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +13 -8
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +23 -11
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +2 -2
- package/dist/astro/routes/api/content/_collection_/index.mjs +14 -9
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/trash.mjs +7 -7
- package/dist/astro/routes/api/dashboard.mjs +6 -6
- package/dist/astro/routes/api/dev/emails.mjs +2 -2
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +16 -16
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/execute.mjs +14 -13
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
- package/dist/astro/routes/api/import/wordpress/media.mjs +11 -11
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +10 -10
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +13 -13
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +16 -16
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +27 -26
- 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 +71 -55
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +10 -10
- package/dist/astro/routes/api/media/_id_/replace.mjs +8 -8
- package/dist/astro/routes/api/media/_id_/upload.mjs +5 -5
- package/dist/astro/routes/api/media/_id_/usage.mjs +18 -18
- package/dist/astro/routes/api/media/_id_.mjs +17 -17
- package/dist/astro/routes/api/media/file/_...key_.mjs +1 -1
- package/dist/astro/routes/api/media/folders/_id_.mjs +8 -8
- package/dist/astro/routes/api/media/folders/index.mjs +8 -8
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +2 -2
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +2 -2
- package/dist/astro/routes/api/media/providers/index.mjs +2 -2
- package/dist/astro/routes/api/media/upload-url.mjs +9 -9
- package/dist/astro/routes/api/media.mjs +21 -21
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/items.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_/translations.mjs +9 -9
- package/dist/astro/routes/api/menus/_name_.mjs +9 -9
- package/dist/astro/routes/api/menus/index.mjs +9 -9
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +5 -5
- package/dist/astro/routes/api/oauth/device/code.mjs +8 -8
- package/dist/astro/routes/api/oauth/device/token.mjs +7 -7
- package/dist/astro/routes/api/oauth/register.mjs +2 -2
- package/dist/astro/routes/api/oauth/token/refresh.mjs +5 -5
- package/dist/astro/routes/api/oauth/token/revoke.mjs +5 -5
- package/dist/astro/routes/api/oauth/token.mjs +5 -5
- package/dist/astro/routes/api/openapi.json.mjs +84 -18
- 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 +12 -11
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +12 -11
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +13 -12
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +13 -12
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +19 -17
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +14 -12
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +19 -17
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +2 -2
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +3 -3
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +54 -51
- 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 +54 -51
- 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 +54 -51
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +54 -51
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +54 -51
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.mjs +54 -51
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +13 -11
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +54 -51
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +54 -51
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +12 -11
- package/dist/astro/routes/api/search/enable.mjs.map +1 -1
- package/dist/astro/routes/api/search/index.mjs +11 -10
- package/dist/astro/routes/api/search/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/rebuild.mjs +12 -11
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
- package/dist/astro/routes/api/search/stats.mjs +7 -6
- package/dist/astro/routes/api/search/stats.mjs.map +1 -1
- package/dist/astro/routes/api/search/suggest.mjs +11 -10
- package/dist/astro/routes/api/search/suggest.mjs.map +1 -1
- package/dist/astro/routes/api/sections/_slug_.mjs +11 -10
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +11 -10
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -6
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +6 -5
- package/dist/astro/routes/api/settings/backups/archives/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/export.mjs +7 -6
- package/dist/astro/routes/api/settings/backups/export.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/index.mjs +8 -7
- package/dist/astro/routes/api/settings/backups/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/email.mjs +4 -4
- package/dist/astro/routes/api/settings.mjs +14 -14
- package/dist/astro/routes/api/setup/admin-verify.mjs +12 -12
- package/dist/astro/routes/api/setup/admin.mjs +11 -11
- package/dist/astro/routes/api/setup/dev-bypass.mjs +33 -31
- 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 +33 -31
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +3 -3
- package/dist/astro/routes/api/snapshot.mjs +8 -7
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +15 -14
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/index.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +5 -5
- package/dist/astro/routes/api/typegen.mjs +4 -4
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- 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 +8 -8
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +12 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +12 -12
- package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -7
- package/dist/astro/routes/api/widget-areas/index.mjs +12 -12
- package/dist/astro/routes/api/widget-components.mjs +2 -2
- package/dist/astro/routes/robots.txt.mjs +6 -6
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +11 -10
- package/dist/astro/routes/sitemap-_collection_.xml.mjs.map +1 -1
- package/dist/astro/routes/sitemap.xml.mjs +7 -7
- package/dist/astro/types.d.mts +32 -17
- 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-Bkf_UHhD.mjs → authorize-C2QHlWSS.mjs} +2 -2
- package/dist/{authorize-Bkf_UHhD.mjs.map → authorize-C2QHlWSS.mjs.map} +1 -1
- package/dist/{backup-rwVAl9NI.mjs → backup-oS8ft5Cm.mjs} +5 -5
- package/dist/{backup-rwVAl9NI.mjs.map → backup-oS8ft5Cm.mjs.map} +1 -1
- package/dist/{bundle-C8S2Zl6U.mjs → bundle-CL0JjIPx.mjs} +4 -4
- package/dist/{bundle-C8S2Zl6U.mjs.map → bundle-CL0JjIPx.mjs.map} +1 -1
- package/dist/{byline-DuhF7NWl.mjs → byline-Vdi85CsZ.mjs} +6 -6
- package/dist/{byline-DuhF7NWl.mjs.map → byline-Vdi85CsZ.mjs.map} +1 -1
- package/dist/{byline-fields-BovY4KYM.mjs → byline-fields-DPVRFMz2.mjs} +2 -2
- package/dist/{byline-fields-BovY4KYM.mjs.map → byline-fields-DPVRFMz2.mjs.map} +1 -1
- package/dist/{byline-registry-Bpu9-MVs.mjs → byline-registry-BxDGIO0I.mjs} +2 -2
- package/dist/{byline-registry-Bpu9-MVs.mjs.map → byline-registry-BxDGIO0I.mjs.map} +1 -1
- package/dist/{bylines-CJ5nd6Lw.mjs → bylines-BX8QhcTk.mjs} +7 -7
- package/dist/{bylines-CJ5nd6Lw.mjs.map → bylines-BX8QhcTk.mjs.map} +1 -1
- package/dist/{bylines-RwBAeOF1.mjs → bylines-w-E1IEQd.mjs} +3 -3
- package/dist/{bylines-RwBAeOF1.mjs.map → bylines-w-E1IEQd.mjs.map} +1 -1
- package/dist/{cache-BF5Kvn59.mjs → cache--7HKu5Xm.mjs} +4 -4
- package/dist/{cache-BF5Kvn59.mjs.map → cache--7HKu5Xm.mjs.map} +1 -1
- package/dist/{capture-triggers-CNW1q3HK.mjs → capture-triggers-CeAsC76Z.mjs} +2 -2
- package/dist/{capture-triggers-CNW1q3HK.mjs.map → capture-triggers-CeAsC76Z.mjs.map} +1 -1
- package/dist/{challenge-store-DQN_Yrck.mjs → challenge-store-BIOak-tp.mjs} +1 -1
- package/dist/{challenge-store-DQN_Yrck.mjs.map → challenge-store-BIOak-tp.mjs.map} +1 -1
- package/dist/{chrome-tags-D8fiZG94.mjs → chrome-tags-DzDQBCjX.mjs} +1 -1
- package/dist/{chrome-tags-D8fiZG94.mjs.map → chrome-tags-DzDQBCjX.mjs.map} +1 -1
- package/dist/{chunks-C4szrD8o.mjs → chunks-VG_tyqnP.mjs} +2 -2
- package/dist/{chunks-C4szrD8o.mjs.map → chunks-VG_tyqnP.mjs.map} +1 -1
- package/dist/cli/index.mjs +66 -38
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +17 -7
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +17 -12
- package/dist/client/index.mjs.map +1 -1
- package/dist/{collection-deletion-Bj07NHbj.mjs → collection-deletion-BCwaWTDw.mjs} +4 -4
- package/dist/{collection-deletion-Bj07NHbj.mjs.map → collection-deletion-BCwaWTDw.mjs.map} +1 -1
- package/dist/collection-slugs-cache-7lTmNzeA.mjs +85 -0
- package/dist/collection-slugs-cache-7lTmNzeA.mjs.map +1 -0
- package/dist/{comment-reaction-BhrvSHml.mjs → comment-reaction-CDyvPQRj.mjs} +2 -2
- package/dist/{comment-reaction-BhrvSHml.mjs.map → comment-reaction-CDyvPQRj.mjs.map} +1 -1
- package/dist/{comments-Ct4ensnf.mjs → comments-VzmVLyaC.mjs} +1 -1
- package/dist/{comments-Ct4ensnf.mjs.map → comments-VzmVLyaC.mjs.map} +1 -1
- package/dist/{components-DJbo1z0S.mjs → components-CL6Wkdk-.mjs} +1 -1
- package/dist/{components-DJbo1z0S.mjs.map → components-CL6Wkdk-.mjs.map} +1 -1
- package/dist/config--eWjQJE5.mjs +217 -0
- package/dist/config--eWjQJE5.mjs.map +1 -0
- package/dist/{index-BJNMCWmu.d.mts → config-CGAhyOjP.d.mts} +138 -47
- package/dist/config-CGAhyOjP.d.mts.map +1 -0
- package/dist/{config-B9eZ340f.d.mts → config-ChFzshGd.d.mts} +1 -1
- package/dist/{config-B9eZ340f.d.mts.map → config-ChFzshGd.d.mts.map} +1 -1
- package/dist/{content-DjvM0T92.mjs → content-CNfMWuhY.mjs} +121 -5
- package/dist/content-CNfMWuhY.mjs.map +1 -0
- package/dist/{content-list-query-Btqd4uV2.d.mts → content-list-query-B8d4UQp9.d.mts} +1 -1
- package/dist/{content-list-query-Btqd4uV2.d.mts.map → content-list-query-B8d4UQp9.d.mts.map} +1 -1
- package/dist/{content-refresh-Bwp-8yGy.mjs → content-refresh-GTMyi2Xy.mjs} +8 -8
- package/dist/{content-refresh-Bwp-8yGy.mjs.map → content-refresh-GTMyi2Xy.mjs.map} +1 -1
- package/dist/{content-repair-Czdp7jz5.mjs → content-repair-CIMt5C0U.mjs} +4 -4
- package/dist/{content-repair-Czdp7jz5.mjs.map → content-repair-CIMt5C0U.mjs.map} +1 -1
- package/dist/{context-CX6gxOLW.mjs → context-C9PB8vGd.mjs} +265 -25
- package/dist/context-C9PB8vGd.mjs.map +1 -0
- package/dist/{cron-C6o7VppJ.mjs → cron-DHAy9oyq.mjs} +1 -1
- package/dist/{cron-C6o7VppJ.mjs.map → cron-DHAy9oyq.mjs.map} +1 -1
- package/dist/{dashboard-C2kTeeRI.mjs → dashboard-BNSvAN8A.mjs} +4 -4
- package/dist/{dashboard-C2kTeeRI.mjs.map → dashboard-BNSvAN8A.mjs.map} +1 -1
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql-migrations.d.mts +1 -1
- package/dist/db/libsql-migrations.mjs +5 -5
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +1 -1
- package/dist/db/postgres-migrations.mjs +5 -5
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +1 -1
- package/dist/db/sqlite-migrations.mjs +5 -5
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.mjs +1 -1
- package/dist/{default-Clra32mw.mjs → default-ByZ0JqZo.mjs} +1 -1
- package/dist/{default-Clra32mw.mjs.map → default-ByZ0JqZo.mjs.map} +1 -1
- package/dist/{deferred-tasks-CvDETTJO.d.mts → deferred-tasks-Cu4NSJpa.d.mts} +1 -1
- package/dist/{deferred-tasks-CvDETTJO.d.mts.map → deferred-tasks-Cu4NSJpa.d.mts.map} +1 -1
- package/dist/{dev-typegen-DzOPfsNj.mjs → dev-typegen-DHT7lcMU.mjs} +1 -1
- package/dist/{dev-typegen-DzOPfsNj.mjs.map → dev-typegen-DHT7lcMU.mjs.map} +1 -1
- package/dist/{device-flow-D2oCWvLJ.mjs → device-flow-Cf6bhCCT.mjs} +4 -4
- package/dist/{device-flow-D2oCWvLJ.mjs.map → device-flow-Cf6bhCCT.mjs.map} +1 -1
- package/dist/{dialect-helpers-BSbfaO3F.mjs → dialect-helpers-BcQvkHgn.mjs} +83 -2
- package/dist/dialect-helpers-BcQvkHgn.mjs.map +1 -0
- package/dist/{direct-executor-Bfa5_ToE.mjs → direct-executor-yAyUGSqB.mjs} +3 -3
- package/dist/{direct-executor-Bfa5_ToE.mjs.map → direct-executor-yAyUGSqB.mjs.map} +1 -1
- package/dist/{dist-89xCrGqi.mjs → dist-BAsh8dbf.mjs} +1 -1
- package/dist/{dist-89xCrGqi.mjs.map → dist-BAsh8dbf.mjs.map} +1 -1
- package/dist/{email-console-Dzb6uGYa.mjs → email-console-DNup1Fku.mjs} +1 -1
- package/dist/{email-console-Dzb6uGYa.mjs.map → email-console-DNup1Fku.mjs.map} +1 -1
- package/dist/{enrich-BX0IDp4z.mjs → enrich-t2Xvjdqa.mjs} +3 -3
- package/dist/{enrich-BX0IDp4z.mjs.map → enrich-t2Xvjdqa.mjs.map} +1 -1
- package/dist/entry-lock-BEr8gDkG.mjs +163 -0
- package/dist/entry-lock-BEr8gDkG.mjs.map +1 -0
- package/dist/{relations-DnRK4YIT.d.mts → entry-lock-CGAtrwAR.d.mts} +92 -37
- package/dist/entry-lock-CGAtrwAR.d.mts.map +1 -0
- package/dist/{redirects-BLNOWQTn.mjs → entry-lock-DIYHAaSw.mjs} +57 -14
- package/dist/entry-lock-DIYHAaSw.mjs.map +1 -0
- package/dist/entry-locks-BEcX7ET0.mjs +167 -0
- package/dist/entry-locks-BEcX7ET0.mjs.map +1 -0
- package/dist/{error-DFrQjppn.mjs → error-BDcFyoRt.mjs} +2 -2
- package/dist/{error-DFrQjppn.mjs.map → error-BDcFyoRt.mjs.map} +1 -1
- package/dist/{errors-DsVoDUjJ.mjs → errors-DvMdNrgf.mjs} +8 -1
- package/dist/errors-DvMdNrgf.mjs.map +1 -0
- package/dist/{escape-HmHTC1Is.mjs → escape-B3asJC6W.mjs} +1 -1
- package/dist/{escape-HmHTC1Is.mjs.map → escape-B3asJC6W.mjs.map} +1 -1
- package/dist/{field-defs-cache-BM4SIRqH.mjs → field-defs-cache-Cf5FHK0M.mjs} +5 -5
- package/dist/{field-defs-cache-BM4SIRqH.mjs.map → field-defs-cache-Cf5FHK0M.mjs.map} +1 -1
- package/dist/{fts-manager-Do1BlW2r.mjs → fts-manager-iddym8cd.mjs} +4 -4
- package/dist/{fts-manager-Do1BlW2r.mjs.map → fts-manager-iddym8cd.mjs.map} +1 -1
- package/dist/{hash-3JJPlHF2.mjs → hash-BRweX6jz.mjs} +1 -1
- package/dist/{hash-3JJPlHF2.mjs.map → hash-BRweX6jz.mjs.map} +1 -1
- package/dist/{hook-result-DCh2WUpe.d.mts → hook-result-BrshYHLx.d.mts} +1 -1
- package/dist/{hook-result-DCh2WUpe.d.mts.map → hook-result-BrshYHLx.d.mts.map} +1 -1
- package/dist/{hreflang-B6hbAuuO.d.mts → hreflang-CQpu5bMy.d.mts} +2 -2
- package/dist/{hreflang-B6hbAuuO.d.mts.map → hreflang-CQpu5bMy.d.mts.map} +1 -1
- package/dist/{hreflang-DQbPZliA.mjs → hreflang-DshnweTC.mjs} +9 -8
- package/dist/{hreflang-DQbPZliA.mjs.map → hreflang-DshnweTC.mjs.map} +1 -1
- package/dist/{identity-DksvEazf.mjs → identity-CjGKDq8J.mjs} +3 -3
- package/dist/{identity-DksvEazf.mjs.map → identity-CjGKDq8J.mjs.map} +1 -1
- package/dist/{import-CJpaBBBT.mjs → import-D96g2otw.mjs} +5 -5
- package/dist/{import-CJpaBBBT.mjs.map → import-D96g2otw.mjs.map} +1 -1
- package/dist/{index-Du8K7bWa.d.mts → index-1ECYpfiF.d.mts} +3 -3
- package/dist/{index-Du8K7bWa.d.mts.map → index-1ECYpfiF.d.mts.map} +1 -1
- package/dist/index.d.mts +22 -22
- package/dist/index.mjs +81 -78
- package/dist/{init-lock-DJkX6Hto.mjs → init-lock-CFNBe_iH.mjs} +1 -1
- package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-CFNBe_iH.mjs.map} +1 -1
- package/dist/{load-CawHjTOH.mjs → load-DPtY9cZo.mjs} +2 -2
- package/dist/{load-CawHjTOH.mjs.map → load-DPtY9cZo.mjs.map} +1 -1
- package/dist/{loader-Z8vl7zTq.mjs → loader-WJi6GFRn.mjs} +46 -8
- package/dist/loader-WJi6GFRn.mjs.map +1 -0
- package/dist/{manifest-builder-COtzuca2.mjs → manifest-builder-BfNlGQIA.mjs} +2 -2
- package/dist/{manifest-builder-COtzuca2.mjs.map → manifest-builder-BfNlGQIA.mjs.map} +1 -1
- package/dist/{manifest-schema-BNWwR8Vd.mjs → manifest-schema-CILooI5J.mjs} +1 -1
- package/dist/{manifest-schema-BNWwR8Vd.mjs.map → manifest-schema-CILooI5J.mjs.map} +1 -1
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +3 -3
- package/dist/media/local-runtime.d.mts +12 -12
- package/dist/media/local-runtime.mjs +5 -5
- package/dist/{media-BHjus86Y.mjs → media-C1EIZKh1.mjs} +1 -1
- package/dist/{media-BHjus86Y.mjs.map → media-C1EIZKh1.mjs.map} +1 -1
- package/dist/{media-allowlist-CoTBWoDp.mjs → media-allowlist-elXJgb_K.mjs} +2 -2
- package/dist/{media-allowlist-CoTBWoDp.mjs.map → media-allowlist-elXJgb_K.mjs.map} +1 -1
- package/dist/{media-folders-BGkATs8n.mjs → media-folders-DASfX_DH.mjs} +1 -1
- package/dist/{media-folders-BGkATs8n.mjs.map → media-folders-DASfX_DH.mjs.map} +1 -1
- package/dist/{media-upload-BCidR-EY.mjs → media-upload-C7_AQJ5v.mjs} +8 -8
- package/dist/{media-upload-BCidR-EY.mjs.map → media-upload-C7_AQJ5v.mjs.map} +1 -1
- package/dist/{media-url-DHoPr0hq.mjs → media-url-8dVl1loq.mjs} +19 -2
- package/dist/media-url-8dVl1loq.mjs.map +1 -0
- package/dist/{media-usage-CdY5jhSR.mjs → media-usage-C1EswMTq.mjs} +9 -9
- package/dist/{media-usage-CdY5jhSR.mjs.map → media-usage-C1EswMTq.mjs.map} +1 -1
- package/dist/{media-usage-activation-BgeMtK5C.mjs → media-usage-activation-CrHNltlL.mjs} +3 -3
- package/dist/{media-usage-activation-BgeMtK5C.mjs.map → media-usage-activation-CrHNltlL.mjs.map} +1 -1
- package/dist/{media-usage-work-D6D9sMCA.mjs → media-usage-work-C5B1zT1c.mjs} +2 -2
- package/dist/{media-usage-work-D6D9sMCA.mjs.map → media-usage-work-C5B1zT1c.mjs.map} +1 -1
- package/dist/{media-usage-work-CnGCdHxx.mjs → media-usage-work-IkQx_Yz6.mjs} +4 -4
- package/dist/{media-usage-work-CnGCdHxx.mjs.map → media-usage-work-IkQx_Yz6.mjs.map} +1 -1
- package/dist/{media-usage-write-fence-hjeD4u7s.mjs → media-usage-write-fence-B9anjOSg.mjs} +3 -3
- package/dist/{media-usage-write-fence-hjeD4u7s.mjs.map → media-usage-write-fence-B9anjOSg.mjs.map} +1 -1
- package/dist/{menus-6qSwTNxG.mjs → menus-D8mC4eaN.mjs} +115 -136
- package/dist/menus-D8mC4eaN.mjs.map +1 -0
- package/dist/{menus-uvPLXMZf.mjs → menus-UdwjHZJC.mjs} +11 -3
- package/dist/{menus-uvPLXMZf.mjs.map → menus-UdwjHZJC.mjs.map} +1 -1
- package/dist/migrations/index.d.mts +1 -1
- package/dist/migrations/index.mjs +4 -4
- package/dist/{mime-CUIG6x84.mjs → mime-DYpsOAny.mjs} +1 -1
- package/dist/{mime-CUIG6x84.mjs.map → mime-DYpsOAny.mjs.map} +1 -1
- package/dist/{mode-DN_WneRx.mjs → mode-Dm4BHhRH.mjs} +1 -1
- package/dist/{mode-DN_WneRx.mjs.map → mode-Dm4BHhRH.mjs.map} +1 -1
- package/dist/{node-sqlite-compat-BuJXIeMz.mjs → node-sqlite-compat-2sezm-Zd.mjs} +1 -1
- package/dist/{node-sqlite-compat-BuJXIeMz.mjs.map → node-sqlite-compat-2sezm-Zd.mjs.map} +1 -1
- package/dist/{normalize-C2xNjTSG.mjs → normalize-4nkKGsuW.mjs} +1 -1
- package/dist/{normalize-C2xNjTSG.mjs.map → normalize-4nkKGsuW.mjs.map} +1 -1
- package/dist/{oauth-authorization-Dj6Kwc21.mjs → oauth-authorization-BxE_lURD.mjs} +4 -4
- package/dist/{oauth-authorization-Dj6Kwc21.mjs.map → oauth-authorization-BxE_lURD.mjs.map} +1 -1
- package/dist/{oauth-clients-FD7I0J8m.mjs → oauth-clients-DJIdOdq0.mjs} +1 -1
- package/dist/{oauth-clients-FD7I0J8m.mjs.map → oauth-clients-DJIdOdq0.mjs.map} +1 -1
- package/dist/{oauth-state-store-DkmDOmHs.mjs → oauth-state-store-CsdxP57n.mjs} +1 -1
- package/dist/{oauth-state-store-DkmDOmHs.mjs.map → oauth-state-store-CsdxP57n.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-CvIwGpYd.mjs → oauth-user-lookup-HzzW-dkF.mjs} +1 -1
- package/dist/{oauth-user-lookup-CvIwGpYd.mjs.map → oauth-user-lookup-HzzW-dkF.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{options-BHPeIbBd.mjs → options-D1FBiAv1.mjs} +60 -5
- package/dist/options-D1FBiAv1.mjs.map +1 -0
- package/dist/{options-CYb4y7hM.d.mts → options-D8Y0VHc7.d.mts} +9 -3
- package/dist/options-D8Y0VHc7.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +12 -0
- package/dist/page/index.mjs.map +1 -1
- package/dist/{parse-Ckpg-Qfp.mjs → parse-WRg5C3NX.mjs} +2 -2
- package/dist/{parse-Ckpg-Qfp.mjs.map → parse-WRg5C3NX.mjs.map} +1 -1
- package/dist/{passkey-config-O_lj3y0k.mjs → passkey-config-qIdO-wF6.mjs} +1 -1
- package/dist/{passkey-config-O_lj3y0k.mjs.map → passkey-config-qIdO-wF6.mjs.map} +1 -1
- package/dist/{patterns-D9iDO3j8.mjs → patterns-DC8CLptu.mjs} +1 -1
- package/dist/{patterns-D9iDO3j8.mjs.map → patterns-DC8CLptu.mjs.map} +1 -1
- package/dist/{placeholder-4X9BrRrp.d.mts → placeholder-COrvARyD.d.mts} +1 -1
- package/dist/{placeholder-4X9BrRrp.d.mts.map → placeholder-COrvARyD.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +3 -3
- package/dist/plugin-types.d.mts.map +1 -1
- package/dist/plugin-utils.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{policy-Dhtwntrt.mjs → policy-XCfb-IE9.mjs} +2 -2
- package/dist/{policy-Dhtwntrt.mjs.map → policy-XCfb-IE9.mjs.map} +1 -1
- package/dist/{portable-text-U85hOafW.mjs → portable-text-BS3d5n-0.mjs} +1 -1
- package/dist/{portable-text-U85hOafW.mjs.map → portable-text-BS3d5n-0.mjs.map} +1 -1
- package/dist/{preview-B_vJLvf8.mjs → preview-BxwsGgGp.mjs} +2 -2
- package/dist/{preview-B_vJLvf8.mjs.map → preview-BxwsGgGp.mjs.map} +1 -1
- package/dist/{protocol-JdD2YhY9.d.mts → protocol-CLn58-nw.d.mts} +2 -2
- package/dist/{protocol-JdD2YhY9.d.mts.map → protocol-CLn58-nw.d.mts.map} +1 -1
- package/dist/{public-url-CarhZJgf.mjs → public-url-FWWAvnre.mjs} +1 -1
- package/dist/{public-url-CarhZJgf.mjs.map → public-url-FWWAvnre.mjs.map} +1 -1
- package/dist/{query-BZKZcG5b.mjs → query-DygOte9M.mjs} +25 -16
- package/dist/query-DygOte9M.mjs.map +1 -0
- package/dist/{query-BndaFxxp.mjs → query-gtXT03Wc.mjs} +4 -4
- package/dist/{query-BndaFxxp.mjs.map → query-gtXT03Wc.mjs.map} +1 -1
- package/dist/{rate-limit-BZmATC6W.mjs → rate-limit-CQUPm0dh.mjs} +2 -2
- package/dist/{rate-limit-BZmATC6W.mjs.map → rate-limit-CQUPm0dh.mjs.map} +1 -1
- package/dist/{redirect-CzqnLxK-.mjs → redirect-D1cWafi6.mjs} +1 -1
- package/dist/{redirect-CzqnLxK-.mjs.map → redirect-D1cWafi6.mjs.map} +1 -1
- package/dist/{redirect-CZkbozu9.mjs → redirect-DId44BS1.mjs} +19 -6
- package/dist/redirect-DId44BS1.mjs.map +1 -0
- package/dist/{redirects-CqzNM0KM.mjs → redirects-DN-ZE33W.mjs} +5 -5
- package/dist/{redirects-CqzNM0KM.mjs.map → redirects-DN-ZE33W.mjs.map} +1 -1
- package/dist/{registry-AZAsRsor.mjs → registry-DTqaqxed.mjs} +26 -10
- package/dist/registry-DTqaqxed.mjs.map +1 -0
- package/dist/{relation-04DfKRTO.mjs → relation-DxgCHOBw.mjs} +2 -2
- package/dist/{relation-04DfKRTO.mjs.map → relation-DxgCHOBw.mjs.map} +1 -1
- package/dist/{relations-CcFwDJdJ.mjs → relations-KKDSLUwS.mjs} +4 -4
- package/dist/{relations-CcFwDJdJ.mjs.map → relations-KKDSLUwS.mjs.map} +1 -1
- package/dist/{relations-C4htk5sb.mjs → relations-rOmbndUJ.mjs} +3 -3
- package/dist/{relations-C4htk5sb.mjs.map → relations-rOmbndUJ.mjs.map} +1 -1
- package/dist/{request-cache-DH9zdCOD.mjs → request-cache-DAZZmesc.mjs} +1 -1
- package/dist/{request-cache-DH9zdCOD.mjs.map → request-cache-DAZZmesc.mjs.map} +1 -1
- package/dist/request-context.d.mts +1 -1
- package/dist/{request-meta-DYxLlnmz.mjs → request-meta-BgjUQLQ-.mjs} +2 -2
- package/dist/{request-meta-DYxLlnmz.mjs.map → request-meta-BgjUQLQ-.mjs.map} +1 -1
- package/dist/{resolve-prZ3mP7l.mjs → resolve-Cy0aWSRj.mjs} +37 -4
- package/dist/resolve-Cy0aWSRj.mjs.map +1 -0
- package/dist/{runner-YKfIMnaF.d.mts → runner-CfwysRM0.d.mts} +2 -2
- package/dist/{runner-YKfIMnaF.d.mts.map → runner-CfwysRM0.d.mts.map} +1 -1
- package/dist/{runner-BLIDKpmA.mjs → runner-nEzvnogG.mjs} +419 -295
- package/dist/runner-nEzvnogG.mjs.map +1 -0
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +3 -2
- package/dist/runtime.mjs.map +1 -1
- package/dist/{schema-CGnBeLiW.mjs → schema-CRu38Pr5.mjs} +3 -3
- package/dist/{schema-CGnBeLiW.mjs.map → schema-CRu38Pr5.mjs.map} +1 -1
- package/dist/{schema-DYr-Vd-L.mjs → schema-DifxndJx.mjs} +4 -4
- package/dist/{schema-DYr-Vd-L.mjs.map → schema-DifxndJx.mjs.map} +1 -1
- package/dist/{scopes-BhWvBavj.mjs → scopes-D5A1OZbq.mjs} +3 -3
- package/dist/{scopes-BhWvBavj.mjs.map → scopes-D5A1OZbq.mjs.map} +1 -1
- package/dist/{search-C-qDk4kx.mjs → search-DEPFSBFq.mjs} +4 -4
- package/dist/{search-C-qDk4kx.mjs.map → search-DEPFSBFq.mjs.map} +1 -1
- package/dist/{secrets-rEtHt-6z.mjs → secrets-CdfzLdoO.mjs} +3 -3
- package/dist/{secrets-rEtHt-6z.mjs.map → secrets-CdfzLdoO.mjs.map} +1 -1
- package/dist/{sections-B_nYvlwn.mjs → sections-3X52kmIR.mjs} +2 -2
- package/dist/{sections-B_nYvlwn.mjs.map → sections-3X52kmIR.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +26 -24
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.d.mts.map +1 -1
- package/dist/seo/index.mjs +18 -19
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-926NRIgt.mjs → seo-Bw68KxRc.mjs} +2 -2
- package/dist/{seo-926NRIgt.mjs.map → seo-Bw68KxRc.mjs.map} +1 -1
- package/dist/{seo-CVWkdv6x.mjs → seo-C4AmbsMU.mjs} +3 -2
- package/dist/seo-C4AmbsMU.mjs.map +1 -0
- package/dist/{service-CJl3ZAYs.mjs → service-J_-Vgo7R.mjs} +2 -2
- package/dist/{service-CJl3ZAYs.mjs.map → service-J_-Vgo7R.mjs.map} +1 -1
- package/dist/{session-user-BlMgqCHS.mjs → session-user-CbnMMwk6.mjs} +1 -1
- package/dist/{session-user-BlMgqCHS.mjs.map → session-user-CbnMMwk6.mjs.map} +1 -1
- package/dist/{settings-Cwm6o2jY.mjs → settings-AnEFYICn.mjs} +3 -3
- package/dist/{settings-Cwm6o2jY.mjs.map → settings-AnEFYICn.mjs.map} +1 -1
- package/dist/{settings--wobrkkC.mjs → settings-DcMjREe5.mjs} +6 -6
- package/dist/{settings--wobrkkC.mjs.map → settings-DcMjREe5.mjs.map} +1 -1
- package/dist/{setup-complete-DQbClIN2.mjs → setup-complete-BClXXTvr.mjs} +2 -2
- package/dist/{setup-complete-DQbClIN2.mjs.map → setup-complete-BClXXTvr.mjs.map} +1 -1
- package/dist/{setup-nonce-DZCFS2sn.mjs → setup-nonce-BheRa4RG.mjs} +1 -1
- package/dist/{setup-nonce-DZCFS2sn.mjs.map → setup-nonce-BheRa4RG.mjs.map} +1 -1
- package/dist/{single-flight-cache-BPuGPdcN.mjs → single-flight-cache-jaswDzmQ.mjs} +2 -2
- package/dist/{single-flight-cache-BPuGPdcN.mjs.map → single-flight-cache-jaswDzmQ.mjs.map} +1 -1
- package/dist/{site-url-BrxA_E9H.mjs → site-url-Bqf-ea3N.mjs} +3 -3
- package/dist/{site-url-BrxA_E9H.mjs.map → site-url-Bqf-ea3N.mjs.map} +1 -1
- package/dist/{snapshot--OCDTqdb.mjs → snapshot-C4CtXntb.mjs} +51 -35
- package/dist/snapshot-C4CtXntb.mjs.map +1 -0
- package/dist/{ssrf-5sIM5jzl.mjs → ssrf-gl4gFNhw.mjs} +148 -91
- package/dist/ssrf-gl4gFNhw.mjs.map +1 -0
- package/dist/{state-BpFVsJZB.mjs → state-1F6IOhdQ.mjs} +1 -1
- package/dist/{state-BpFVsJZB.mjs.map → state-1F6IOhdQ.mjs.map} +1 -1
- package/dist/{status-B777-dXz.mjs → status-Cenfk_r5.mjs} +1 -1
- package/dist/{status-B777-dXz.mjs.map → status-Cenfk_r5.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/{target-BmR0iTwz.mjs → target-CRzeTs8G.mjs} +1 -1
- package/dist/{target-BmR0iTwz.mjs.map → target-CRzeTs8G.mjs.map} +1 -1
- package/dist/{taxonomies-DkAkMupr.mjs → taxonomies-BOdxuZpV.mjs} +11 -4
- package/dist/taxonomies-BOdxuZpV.mjs.map +1 -0
- package/dist/{taxonomies-Wfy_Lg12.mjs → taxonomies-NcFDq5Gg.mjs} +23 -14
- package/dist/taxonomies-NcFDq5Gg.mjs.map +1 -0
- package/dist/{taxonomy-tJepplLL.mjs → taxonomy-DP1vny4K.mjs} +11 -3
- package/dist/{taxonomy-tJepplLL.mjs.map → taxonomy-DP1vny4K.mjs.map} +1 -1
- package/dist/{tokens-BCO1oruF.mjs → tokens-CDk8ox9p.mjs} +1 -1
- package/dist/{tokens-BCO1oruF.mjs.map → tokens-CDk8ox9p.mjs.map} +1 -1
- package/dist/{transport-xh6YKW2G.d.mts → transport-BVn4tVb9.d.mts} +1 -1
- package/dist/{transport-xh6YKW2G.d.mts.map → transport-BVn4tVb9.d.mts.map} +1 -1
- package/dist/{transport-Dr3qtwXC.mjs → transport-DP0JI-8i.mjs} +1 -1
- package/dist/{transport-Dr3qtwXC.mjs.map → transport-DP0JI-8i.mjs.map} +1 -1
- package/dist/{trusted-proxy-BR5F1Otp.mjs → trusted-proxy-BTM6UxqD.mjs} +1 -1
- package/dist/{trusted-proxy-BR5F1Otp.mjs.map → trusted-proxy-BTM6UxqD.mjs.map} +1 -1
- package/dist/{types-CXFkwEGC.d.mts → types-2n848aVW.d.mts} +1 -1
- package/dist/{types-CXFkwEGC.d.mts.map → types-2n848aVW.d.mts.map} +1 -1
- package/dist/{types-75KIqzL9.mjs → types-BMXnRIrX.mjs} +4 -2
- package/dist/types-BMXnRIrX.mjs.map +1 -0
- package/dist/{types-B6GrPic4.d.mts → types-BouXpOsa.d.mts} +115 -4
- package/dist/types-BouXpOsa.d.mts.map +1 -0
- package/dist/{types-3qNFPlci.mjs → types-ByG2coLw.mjs} +1 -1
- package/dist/{types-3qNFPlci.mjs.map → types-ByG2coLw.mjs.map} +1 -1
- package/dist/types-CQPWhPxE.mjs.map +1 -1
- package/dist/{types-CDtkNSFQ.d.mts → types-DEfNJ__A.d.mts} +1 -1
- package/dist/{types-CDtkNSFQ.d.mts.map → types-DEfNJ__A.d.mts.map} +1 -1
- package/dist/{types-xDMtUFnO.d.mts → types-DKcKSbtB.d.mts} +23 -7
- package/dist/types-DKcKSbtB.d.mts.map +1 -0
- package/dist/{types-C00oyCZp.d.mts → types-DRYs7uTL.d.mts} +1 -1
- package/dist/{types-C00oyCZp.d.mts.map → types-DRYs7uTL.d.mts.map} +1 -1
- package/dist/{types-DczELMop.d.mts → types-DaEg7KX2.d.mts} +1 -1
- package/dist/{types-DczELMop.d.mts.map → types-DaEg7KX2.d.mts.map} +1 -1
- package/dist/{types-BuHW4xDE.d.mts → types-LU8utpP_.d.mts} +6 -3
- package/dist/types-LU8utpP_.d.mts.map +1 -0
- package/dist/{types-CPeXo1wF.d.mts → types-VpYfPUxN.d.mts} +1 -1
- package/dist/{types-CPeXo1wF.d.mts.map → types-VpYfPUxN.d.mts.map} +1 -1
- package/dist/{types-DM3vioBY.mjs → types-c9GP7Dj5.mjs} +1 -1
- package/dist/{types-DM3vioBY.mjs.map → types-c9GP7Dj5.mjs.map} +1 -1
- package/dist/{types-FV23hEoW.d.mts → types-tJWDK__M.d.mts} +14 -1
- package/dist/types-tJWDK__M.d.mts.map +1 -0
- package/dist/{upload-attempts-BsqoQ1fn.mjs → upload-attempts-C6S2y-lp.mjs} +1 -1
- package/dist/{upload-attempts-BsqoQ1fn.mjs.map → upload-attempts-C6S2y-lp.mjs.map} +1 -1
- package/dist/{url-pattern-D4c2czpJ.mjs → url-pattern-UYtxVTuj.mjs} +1 -1
- package/dist/{url-pattern-D4c2czpJ.mjs.map → url-pattern-UYtxVTuj.mjs.map} +1 -1
- package/dist/{user-BTZ40aq6.mjs → user-DIsIXogT.mjs} +2 -2
- package/dist/{user-BTZ40aq6.mjs.map → user-DIsIXogT.mjs.map} +1 -1
- package/dist/{utils-BKvNjjhY.mjs → utils-BicP5Qbg.mjs} +2 -2
- package/dist/{utils-BKvNjjhY.mjs.map → utils-BicP5Qbg.mjs.map} +1 -1
- package/dist/{validate-DE_LRwo9.mjs → validate-D8VPn4Hv.mjs} +8 -3
- package/dist/validate-D8VPn4Hv.mjs.map +1 -0
- package/dist/{validate-DUP27OYD.d.mts → validate-Df6Dvwbw.d.mts} +12 -7
- package/dist/validate-Df6Dvwbw.d.mts.map +1 -0
- package/dist/{validation-BMImFJ11.mjs → validation-CzMJn9Ku.mjs} +13 -11
- package/dist/{validation-BMImFJ11.mjs.map → validation-CzMJn9Ku.mjs.map} +1 -1
- package/dist/version-CK67_SXX.mjs +7 -0
- package/dist/{version-Bii3suaO.mjs.map → version-CK67_SXX.mjs.map} +1 -1
- package/dist/{widgets-D3gJ10xU.mjs → widgets-RFVAxQl3.mjs} +5 -5
- package/dist/{widgets-D3gJ10xU.mjs.map → widgets-RFVAxQl3.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-B2Og_6rU.d.mts → wxr-taxonomies-BX4yQL9s.d.mts} +1 -1
- package/dist/{wxr-taxonomies-B2Og_6rU.d.mts.map → wxr-taxonomies-BX4yQL9s.d.mts.map} +1 -1
- package/dist/{wxr-taxonomies-Cxngsjwo.mjs → wxr-taxonomies-Csdp7yde.mjs} +4 -4
- package/dist/{wxr-taxonomies-Cxngsjwo.mjs.map → wxr-taxonomies-Csdp7yde.mjs.map} +1 -1
- package/dist/{zod-generator-BuYSQJB7.mjs → zod-generator-CNRGdnBY.mjs} +3 -3
- package/dist/{zod-generator-BuYSQJB7.mjs.map → zod-generator-CNRGdnBY.mjs.map} +1 -1
- package/package.json +12 -10
- package/src/api/errors.ts +7 -0
- package/src/api/handlers/content.ts +66 -79
- package/src/api/handlers/entry-lock.ts +206 -0
- package/src/api/handlers/manifest.ts +1 -0
- package/src/api/handlers/marketplace.ts +28 -20
- package/src/api/handlers/menus.ts +9 -0
- package/src/api/handlers/registry.ts +13 -8
- package/src/api/handlers/revision.ts +2 -1
- package/src/api/handlers/seo.ts +10 -1
- package/src/api/handlers/snapshot.ts +84 -72
- package/src/api/handlers/taxonomies.ts +9 -0
- package/src/api/openapi/document.ts +111 -6
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/entry-lock.ts +48 -0
- package/src/api/schemas/index.ts +1 -0
- package/src/api/schemas/schema.ts +10 -1
- package/src/api/types.ts +2 -0
- package/src/astro/integration/index.ts +9 -6
- package/src/astro/integration/routes.ts +5 -0
- package/src/astro/integration/runtime.ts +13 -5
- package/src/astro/integration/vite-config.ts +30 -0
- package/src/astro/middleware.ts +16 -0
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id]/lock.ts +133 -0
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +11 -1
- package/src/astro/routes/api/content/[collection]/[id]/schedule.ts +20 -0
- package/src/astro/routes/api/content/[collection]/[id]/unpublish.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +27 -2
- package/src/astro/routes/api/content/[collection]/index.ts +3 -0
- package/src/astro/routes/sitemap-[collection].xml.ts +4 -0
- package/src/astro/types.ts +13 -3
- package/src/cli/commands/content.ts +31 -5
- package/src/cli/commands/export-seed.ts +2 -0
- package/src/client/index.ts +34 -7
- package/src/components/EmDashBodyEnd.astro +4 -1
- package/src/components/EmDashBodyStart.astro +4 -1
- package/src/components/EmDashHead.astro +35 -16
- package/src/components/Gallery.astro +18 -4
- package/src/components/InlineEditor.astro +3 -1
- package/src/components/InlinePortableTextEditor.tsx +106 -11
- package/src/components/PortableText.astro +4 -0
- package/src/components/Table.astro +147 -62
- package/src/config/errors.ts +13 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +26 -2
- package/src/content/converters/prosemirror-to-portable-text.ts +35 -6
- package/src/content/converters/types.ts +10 -0
- package/src/database/dialect-helpers.ts +125 -0
- package/src/database/migrations/075_entry_edit_locks.ts +44 -0
- package/src/database/migrations/076_collection_nav_group.ts +22 -0
- package/src/database/migrations/077_plugin_storage_revisions.ts +89 -0
- package/src/database/migrations/runner.ts +6 -0
- package/src/database/repositories/content.ts +170 -1
- package/src/database/repositories/entry-locks.ts +257 -0
- package/src/database/repositories/options.ts +69 -5
- package/src/database/repositories/plugin-storage.ts +159 -0
- package/src/database/repositories/redirect.ts +17 -6
- package/src/database/repositories/taxonomy.ts +8 -0
- package/src/database/repositories/types.ts +3 -0
- package/src/database/types.ts +15 -1
- package/src/emdash-runtime.ts +117 -50
- package/src/i18n/resolve.ts +44 -4
- package/src/index.ts +14 -0
- package/src/loader.ts +35 -4
- package/src/mcp/server.ts +23 -5
- package/src/media/responsive.ts +23 -0
- package/src/media/usage/content-refresh.ts +2 -1
- package/src/menus/index.ts +17 -21
- package/src/page/seo-contributions.ts +78 -1
- package/src/page/seo-panel.ts +47 -0
- package/src/plugin-types.ts +9 -0
- package/src/plugins/conditional-storage.ts +30 -0
- package/src/plugins/context.ts +49 -17
- package/src/plugins/hooks.ts +7 -1
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manager.ts +5 -2
- package/src/plugins/sandbox/types.ts +16 -2
- package/src/plugins/storage-query.ts +45 -0
- package/src/plugins/storage-update.ts +96 -0
- package/src/plugins/types.ts +114 -0
- package/src/query.ts +29 -3
- package/src/registry/config.ts +130 -27
- package/src/registry/types.ts +5 -6
- package/src/schema/collection-slugs-cache.ts +157 -0
- package/src/schema/registry.ts +19 -0
- package/src/schema/types.ts +25 -6
- package/src/security/ssrf.ts +167 -141
- package/src/seed/apply.ts +4 -0
- package/src/seed/types.ts +7 -2
- package/src/seed/validate.ts +18 -1
- package/src/seo/hreflang.ts +1 -0
- package/src/seo/index.ts +5 -10
- package/src/seo/media-url.ts +26 -0
- package/src/taxonomies/term-counts.ts +22 -5
- package/dist/api-Dz6rgoa3.mjs.map +0 -1
- package/dist/apply-B4SIOxR1.mjs.map +0 -1
- package/dist/config-it29yDLA.mjs.map +0 -1
- package/dist/content-DjvM0T92.mjs.map +0 -1
- package/dist/context-CX6gxOLW.mjs.map +0 -1
- package/dist/dialect-helpers-BSbfaO3F.mjs.map +0 -1
- package/dist/errors-DsVoDUjJ.mjs.map +0 -1
- package/dist/index-BJNMCWmu.d.mts.map +0 -1
- package/dist/loader-Z8vl7zTq.mjs.map +0 -1
- package/dist/media-url-DHoPr0hq.mjs.map +0 -1
- package/dist/menus-6qSwTNxG.mjs.map +0 -1
- package/dist/options-BHPeIbBd.mjs.map +0 -1
- package/dist/options-CYb4y7hM.d.mts.map +0 -1
- package/dist/query-BZKZcG5b.mjs.map +0 -1
- package/dist/redirect-CZkbozu9.mjs.map +0 -1
- package/dist/redirects-BLNOWQTn.mjs.map +0 -1
- package/dist/registry-AZAsRsor.mjs.map +0 -1
- package/dist/relations-DnRK4YIT.d.mts.map +0 -1
- package/dist/resolve-prZ3mP7l.mjs.map +0 -1
- package/dist/runner-BLIDKpmA.mjs.map +0 -1
- package/dist/seo-CVWkdv6x.mjs.map +0 -1
- package/dist/snapshot--OCDTqdb.mjs.map +0 -1
- package/dist/ssrf-5sIM5jzl.mjs.map +0 -1
- package/dist/taxonomies-DkAkMupr.mjs.map +0 -1
- package/dist/taxonomies-Wfy_Lg12.mjs.map +0 -1
- package/dist/types-75KIqzL9.mjs.map +0 -1
- package/dist/types-B6GrPic4.d.mts.map +0 -1
- package/dist/types-BuHW4xDE.d.mts.map +0 -1
- package/dist/types-FV23hEoW.d.mts.map +0 -1
- package/dist/types-xDMtUFnO.d.mts.map +0 -1
- package/dist/validate-DE_LRwo9.mjs.map +0 -1
- package/dist/validate-DUP27OYD.d.mts.map +0 -1
- package/dist/version-Bii3suaO.mjs +0 -7
- /package/dist/{api-tokens-BfYts-sq.mjs → api-tokens-B_lfpmH3.mjs} +0 -0
- /package/dist/{ssrf-Dg3cIT-b.mjs → ssrf-TQhaljuI.mjs} +0 -0
- /package/dist/{types-DfuTR6dL.mjs → types-A2cN6a4a.mjs} +0 -0
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"content-DjvM0T92.mjs","names":[],"sources":["../src/search/match.ts","../src/database/repositories/revision.ts","../src/database/repositories/content.ts"],"sourcesContent":["/**\n * FTS5 match-expression builder for structured (non-user-syntax) queries.\n *\n * Unlike `escapeQuery` in `query.ts` (which powers the public search API and\n * deliberately passes through FTS5 operators like AND/OR/NOT), this builder\n * treats the input as plain words: every term is double-quoted with interior\n * quotes escaped, so the result can never produce an FTS5 syntax error. Used\n * by the admin content-list filter, where the input is a filter box, not a\n * search-syntax field.\n */\n\nconst WHITESPACE_RE = /\\s+/;\nconst DOUBLE_QUOTE_RE = /\"/g;\nconst GLOB_SPECIAL_RE = /[[\\]*?]/g;\n\n/**\n * Build a prefix-matching FTS5 MATCH expression from free-form input.\n *\n * `hello wor` becomes `\"hello\"* \"wor\"*` — implicit AND with per-term prefix\n * matching. Returns `\"\"` when the input contains no usable terms; callers\n * must fall back to their non-FTS path in that case.\n */\nexport function buildFtsPrefixMatch(input: string): string {\n\tconst terms = input\n\t\t.trim()\n\t\t.split(WHITESPACE_RE)\n\t\t.map((term) => term.replace(DOUBLE_QUOTE_RE, '\"\"'))\n\t\t.filter((term) => term.length > 0);\n\n\tif (terms.length === 0) return \"\";\n\treturn terms.map((term) => `\"${term}\"*`).join(\" \");\n}\n\n/**\n * Build a GLOB prefix pattern from free-form input, treating GLOB\n * metacharacters (`* ? [ ]`) literally by wrapping each in a character\n * class (GLOB has no ESCAPE clause).\n *\n * GLOB (unlike default LIKE) is case-sensitive, so with a lowercased\n * pattern it matches slugs (lowercase by construction) while staying\n * servable by the ordinary BINARY-collated slug index — SQLite's GLOB\n * optimization turns a `prefix*` pattern into an index range scan.\n */\nexport function buildSlugGlobPrefix(input: string): string {\n\tconst escaped = input\n\t\t.trim()\n\t\t.toLowerCase()\n\t\t.replace(GLOB_SPECIAL_RE, (c) => `[${c}]`);\n\treturn `${escaped}*`;\n}\n","import { sql, type Kysely } from \"kysely\";\nimport { monotonicFactory } from \"ulidx\";\n\nimport type { Database, RevisionTable } from \"../types.js\";\nimport { validateIdentifier } from \"../validate.js\";\n\nconst monotonic = monotonicFactory();\n\nexport interface Revision {\n\tid: string;\n\tcollection: string;\n\tentryId: string;\n\tdata: Record<string, unknown>;\n\tauthorId: string | null;\n\tcreatedAt: string;\n}\n\nexport interface CreateRevisionInput {\n\tcollection: string;\n\tentryId: string;\n\tdata: Record<string, unknown>;\n\tauthorId?: string;\n}\n\n/**\n * Revision repository for version history\n *\n * Each revision stores a JSON snapshot of the content at a point in time.\n * Used when collection has `supports: [\"revisions\"]` enabled.\n */\nexport class RevisionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new revision\n\t */\n\tasync create(input: CreateRevisionInput): Promise<Revision> {\n\t\tconst id = monotonic();\n\n\t\tconst row: Omit<RevisionTable, \"created_at\"> = {\n\t\t\tid,\n\t\t\tcollection: input.collection,\n\t\t\tentry_id: input.entryId,\n\t\t\tdata: JSON.stringify(input.data),\n\t\t\tauthor_id: input.authorId ?? null,\n\t\t};\n\n\t\tawait this.db.insertInto(\"revisions\").values(row).execute();\n\n\t\tconst revision = await this.findById(id);\n\t\tif (!revision) {\n\t\t\tthrow new Error(\"Failed to create revision\");\n\t\t}\n\n\t\ttry {\n\t\t\tawait this.db\n\t\t\t\t.insertInto(\"_emdash_revision_prune_queue\")\n\t\t\t\t.values({\n\t\t\t\t\tcollection: input.collection,\n\t\t\t\t\tentry_id: input.entryId,\n\t\t\t\t\trevision_id: id,\n\t\t\t\t})\n\t\t\t\t.onConflict((conflict) =>\n\t\t\t\t\tconflict.columns([\"collection\", \"entry_id\"]).doUpdateSet({ revision_id: id }),\n\t\t\t\t)\n\t\t\t\t.execute();\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[revisions] Failed to queue revision pruning for ${input.collection}/${input.entryId}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\n\t\treturn revision;\n\t}\n\n\t/**\n\t * Find revision by ID\n\t */\n\tasync findById(id: string): Promise<Revision | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRevision(row) : null;\n\t}\n\n\t/**\n\t * Get all revisions for an entry (newest first)\n\t *\n\t * Orders by monotonic ULID (descending). The monotonic factory\n\t * guarantees strictly increasing IDs even within the same millisecond.\n\t */\n\tasync findByEntry(\n\t\tcollection: string,\n\t\tentryId: string,\n\t\toptions: { limit?: number } = {},\n\t): Promise<Revision[]> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"id\", \"desc\");\n\n\t\tif (options.limit) {\n\t\t\tquery = query.limit(options.limit);\n\t\t}\n\n\t\tconst rows = await query.execute();\n\t\treturn rows.map((row) => this.rowToRevision(row));\n\t}\n\n\t/**\n\t * Get the most recent revision for an entry\n\t */\n\tasync findLatest(collection: string, entryId: string): Promise<Revision | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"id\", \"desc\")\n\t\t\t.limit(1)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToRevision(row) : null;\n\t}\n\n\t/**\n\t * Count revisions for an entry\n\t */\n\tasync countByEntry(collection: string, entryId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Delete all revisions for an entry (use when entry is deleted)\n\t */\n\tasync deleteByEntry(collection: string, entryId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"revisions\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.executeTakeFirst();\n\n\t\ttry {\n\t\t\tawait this.db\n\t\t\t\t.deleteFrom(\"_emdash_revision_prune_queue\")\n\t\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t\t.execute();\n\t\t} catch (error) {\n\t\t\tconsole.error(\n\t\t\t\t`[revisions] Failed to clear queued revision pruning for ${collection}/${entryId}:`,\n\t\t\t\terror,\n\t\t\t);\n\t\t}\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete old revisions, keeping the most recent N\n\t */\n\tasync pruneOldRevisions(\n\t\tcollection: string,\n\t\tentryId: string,\n\t\tkeepCount: number,\n\t\tthroughRevisionId?: string,\n\t): Promise<number> {\n\t\tvalidateIdentifier(collection, \"collection\");\n\t\tconst tableName = `ec_${collection}`;\n\t\tlet keepQuery = this.db\n\t\t\t.selectFrom(\"revisions\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"desc\") // ULID tiebreaker\n\t\t\t.limit(keepCount);\n\n\t\tif (throughRevisionId) {\n\t\t\tkeepQuery = keepQuery.where(\"id\", \"<=\", throughRevisionId);\n\t\t}\n\n\t\tconst keep = await keepQuery.execute();\n\n\t\tconst keepIds = keep.map((r) => r.id);\n\n\t\tif (keepIds.length === 0) return 0;\n\t\tconst revisionBoundary = throughRevisionId ? sql`AND id <= ${throughRevisionId}` : sql``;\n\n\t\tconst result = await sql`\n\t\t\tDELETE FROM revisions\n\t\t\tWHERE collection = ${collection}\n\t\t\tAND entry_id = ${entryId}\n\t\t\t${revisionBoundary}\n\t\t\tAND id NOT IN (${sql.join(keepIds.map((id) => sql`${id}`))})\n\t\t\tAND NOT EXISTS (\n\t\t\t\tSELECT 1 FROM ${sql.ref(tableName)} AS content\n\t\t\t\tWHERE content.live_revision_id = revisions.id\n\t\t\t\tOR content.draft_revision_id = revisions.id\n\t\t\t)\n\t\t`.execute(this.db);\n\n\t\treturn Number(result.numAffectedRows ?? 0);\n\t}\n\n\tasync pruneQueuedEntry(\n\t\tcollection: string,\n\t\tentryId: string,\n\t\tqueuedRevisionId: string,\n\t\tkeepCount: number,\n\t): Promise<number> {\n\t\tconst pruned = await this.pruneOldRevisions(collection, entryId, keepCount, queuedRevisionId);\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_revision_prune_queue\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"entry_id\", \"=\", entryId)\n\t\t\t.where(\"revision_id\", \"=\", queuedRevisionId)\n\t\t\t.execute();\n\t\treturn pruned;\n\t}\n\n\tasync deleteIfUnreferenced(\n\t\tcollection: string,\n\t\tentryId: string,\n\t\trevisionId: string,\n\t): Promise<boolean> {\n\t\tvalidateIdentifier(collection, \"collection\");\n\t\tconst tableName = `ec_${collection}`;\n\t\tconst result = await sql`\n\t\t\tDELETE FROM revisions\n\t\t\tWHERE id = ${revisionId}\n\t\t\tAND collection = ${collection}\n\t\t\tAND entry_id = ${entryId}\n\t\t\tAND NOT EXISTS (\n\t\t\t\tSELECT 1 FROM ${sql.ref(tableName)} AS content\n\t\t\t\tWHERE content.live_revision_id = revisions.id\n\t\t\t\tOR content.draft_revision_id = revisions.id\n\t\t\t)\n\t\t`.execute(this.db);\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Convert database row to Revision object\n\t */\n\tprivate rowToRevision(row: {\n\t\tid: string;\n\t\tcollection: string;\n\t\tentry_id: string;\n\t\tdata: string;\n\t\tauthor_id: string | null;\n\t\tcreated_at: string;\n\t}): Revision {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tentryId: row.entry_id,\n\t\t\tdata: JSON.parse(row.data),\n\t\t\tauthorId: row.author_id,\n\t\t\tcreatedAt: row.created_at,\n\t\t};\n\t}\n}\n","import { sql, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport type { ContentFieldFilterValue, ContentFieldFilters } from \"../../content-list-query.js\";\nimport { invalidateCollectionCache } from \"../../object-cache/index.js\";\nimport { isIndexableFieldType, type FieldType } from \"../../schema/types.js\";\nimport { buildFtsPrefixMatch, buildSlugGlobPrefix } from \"../../search/match.js\";\nimport { chunks, SQL_BATCH_SIZE } from \"../../utils/chunks.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\nimport { slugify } from \"../../utils/slugify.js\";\nimport type { Database } from \"../types.js\";\nimport { validateIdentifier } from \"../validate.js\";\nimport { RevisionRepository } from \"./revision.js\";\nimport type {\n\tCreateContentInput,\n\tUpdateContentInput,\n\tFindManyOptions,\n\tFindManyResult,\n\tContentItem,\n\tContentDateField,\n\tContentBylineFilter,\n} from \"./types.js\";\nimport {\n\tContentCollectionNotFoundError,\n\tContentMutationConflictError,\n\tEmDashValidationError,\n\tInvalidCursorError,\n\tScheduledNotDueError,\n\tencodeCursor,\n\tdecodeCursor,\n} from \"./types.js\";\n\n// Regex pattern for ULID validation\nconst ULID_PATTERN = /^[0-9A-Z]{26}$/;\nconst SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;\nconst MAX_DRAFT_STAGE_ATTEMPTS = 32;\n\n// LIKE wildcards that must be escaped so user search input is matched literally.\nconst LIKE_WILDCARD_RE = /[\\\\%_]/g;\nconst MAX_INDEXED_FIELD_FILTERS = 20;\nconst MAX_IN_FILTER_VALUES = SQL_BATCH_SIZE;\nconst MAX_FILTER_STRING_LENGTH = 2048;\n\ntype NormalizedFilterScalar = string | number;\nexport type ContentRevisionPrecondition = Pick<ContentItem, \"version\" | \"updatedAt\">;\n\nfunction assertRevisionPrecondition(\n\titem: ContentItem,\n\texpected: ContentRevisionPrecondition | undefined,\n): void {\n\tif (expected && (item.version !== expected.version || item.updatedAt !== expected.updatedAt)) {\n\t\tthrow new ContentMutationConflictError();\n\t}\n}\n\ntype ResolvedFieldFilter =\n\t| { column: string; kind: \"null\" }\n\t| { column: string; kind: \"exact\"; value: NormalizedFilterScalar }\n\t| { column: string; kind: \"in\"; values: NormalizedFilterScalar[] }\n\t| {\n\t\t\tcolumn: string;\n\t\t\tkind: \"range\";\n\t\t\tbounds: Partial<Record<\"gt\" | \"gte\" | \"lt\" | \"lte\", NormalizedFilterScalar>>;\n\t };\n\nfunction nullableColumnMatch(column: string, value: string | null): ReturnType<typeof sql> {\n\tvalidateIdentifier(column, \"content column\");\n\treturn value === null ? sql`${sql.ref(column)} IS NULL` : sql`${sql.ref(column)} = ${value}`;\n}\n\nfunction sameStoredValue(left: unknown, right: unknown): boolean {\n\treturn Object.is(serializeValue(left), serializeValue(right));\n}\n\nfunction matchesPublication(\n\tobserved: ContentItem,\n\texisting: ContentItem,\n\trevision: { data: Record<string, unknown> },\n\trevisionId: string,\n\tslug: string | null,\n\tpublishedAt: string,\n\tupdatedAt: string,\n): boolean {\n\tif (\n\t\tobserved.version !== existing.version + 1 ||\n\t\tobserved.status !== \"published\" ||\n\t\tobserved.slug !== slug ||\n\t\tobserved.liveRevisionId !== revisionId ||\n\t\tobserved.draftRevisionId !== null ||\n\t\tobserved.scheduledAt !== null ||\n\t\tobserved.publishedAt !== publishedAt ||\n\t\tobserved.updatedAt !== updatedAt\n\t) {\n\t\treturn false;\n\t}\n\n\treturn Object.entries(revision.data).every(\n\t\t([key, value]) =>\n\t\t\tSYSTEM_COLUMNS.has(key) || key.startsWith(\"_\") || sameStoredValue(observed.data[key], value),\n\t);\n}\n\nfunction matchesLifecyclePublication(\n\tobserved: ContentItem,\n\texisting: ContentItem,\n\tliveRevisionId: string,\n\tpublishedAt: string,\n\tupdatedAt: string,\n): boolean {\n\tif (\n\t\tobserved.version !== existing.version + 1 ||\n\t\tobserved.status !== \"published\" ||\n\t\tobserved.slug !== existing.slug ||\n\t\tobserved.liveRevisionId !== liveRevisionId ||\n\t\tobserved.draftRevisionId !== null ||\n\t\tobserved.scheduledAt !== null ||\n\t\tobserved.publishedAt !== publishedAt ||\n\t\tobserved.updatedAt !== updatedAt\n\t) {\n\t\treturn false;\n\t}\n\n\treturn Object.entries(existing.data).every(([key, value]) =>\n\t\tsameStoredValue(observed.data[key], value),\n\t);\n}\n\nfunction matchesPublicationFence(observed: ContentItem, existing: ContentItem): boolean {\n\treturn (\n\t\tobserved.version === existing.version &&\n\t\tobserved.status === existing.status &&\n\t\tobserved.liveRevisionId === existing.liveRevisionId &&\n\t\tobserved.draftRevisionId === existing.draftRevisionId &&\n\t\tobserved.scheduledAt === existing.scheduledAt\n\t);\n}\n\nexport function isConfirmedStatementFailure(error: unknown): boolean {\n\tif (typeof error !== \"object\" || error === null || !(\"code\" in error)) return false;\n\tconst code = (error as { code?: unknown }).code;\n\treturn (\n\t\ttypeof code === \"string\" &&\n\t\t(code === \"ERR_SQLITE_ERROR\" || code.startsWith(\"SQLITE_\") || SQLSTATE_PATTERN.test(code))\n\t);\n}\n\ninterface ResolvedOrderField {\n\tcolumn: string;\n\tindexedCustomField: boolean;\n}\n\ntype IndexedOrderValue = string | number | null;\n\ninterface IndexedFieldCursorPayload {\n\tversion: 1;\n\tfield: string;\n\tvalue: IndexedOrderValue;\n}\n\nfunction encodeIndexedFieldCursor(field: string, value: IndexedOrderValue, id: string): string {\n\tconst payload: IndexedFieldCursorPayload = { version: 1, field, value };\n\treturn encodeCursor(JSON.stringify(payload), id);\n}\n\nfunction decodeIndexedFieldCursor(\n\tcursor: string,\n\tfield: string,\n): { value: IndexedOrderValue; id: string } {\n\tconst { orderValue, id } = decodeCursor(cursor);\n\tlet payload: unknown;\n\ttry {\n\t\tpayload = JSON.parse(orderValue);\n\t} catch {\n\t\tthrow new InvalidCursorError(cursor);\n\t}\n\n\tif (payload === null || typeof payload !== \"object\") {\n\t\tthrow new InvalidCursorError(cursor);\n\t}\n\tconst candidate = payload as Partial<IndexedFieldCursorPayload>;\n\tconst validValue =\n\t\tcandidate.value === null ||\n\t\ttypeof candidate.value === \"string\" ||\n\t\ttypeof candidate.value === \"number\";\n\tif (candidate.version !== 1 || candidate.field !== field || !validValue) {\n\t\tthrow new InvalidCursorError(cursor);\n\t}\n\n\treturn { value: candidate.value as IndexedOrderValue, id };\n}\n\n/**\n * Whitelist mapping a public date-filter field to its physical column. Keeping\n * this separate from `mapOrderField` makes the filterable set explicit and\n * prevents filtering on arbitrary columns.\n */\nconst DATE_FILTER_COLUMNS: Record<ContentDateField, \"created_at\" | \"updated_at\" | \"published_at\"> =\n\t{\n\t\tcreatedAt: \"created_at\",\n\t\tupdatedAt: \"updated_at\",\n\t\tpublishedAt: \"published_at\",\n\t};\n\n/**\n * Built-in sort fields → their physical columns. A closed set that blocks\n * sorting by arbitrary columns; per-collection fields are allowed\n * separately via `mapOrderField`'s `sortableExtras`.\n */\nconst ORDER_FIELD_COLUMNS: Record<string, string> = {\n\tcreatedAt: \"created_at\",\n\tupdatedAt: \"updated_at\",\n\tpublishedAt: \"published_at\",\n\tscheduledAt: \"scheduled_at\",\n\tdeletedAt: \"deleted_at\",\n\ttitle: \"title\",\n\tname: \"name\",\n\tslug: \"slug\",\n\tstatus: \"status\",\n\tlocale: \"locale\",\n};\n\n/** True when `field` maps to a system column and needs no per-collection resolution. */\nexport function isSystemOrderField(field: string): boolean {\n\treturn field in ORDER_FIELD_COLUMNS;\n}\n\n/**\n * System columns that exist in every ec_* table\n */\nconst SYSTEM_COLUMNS = new Set([\n\t\"id\",\n\t\"slug\",\n\t\"status\",\n\t\"author_id\",\n\t\"primary_byline_id\",\n\t\"created_at\",\n\t\"updated_at\",\n\t\"published_at\",\n\t\"scheduled_at\",\n\t\"deleted_at\",\n\t\"version\",\n\t\"live_revision_id\",\n\t\"draft_revision_id\",\n\t\"locale\",\n\t\"translation_group\",\n]);\n\n/**\n * Get the table name for a collection type\n */\nfunction getTableName(type: string): string {\n\tvalidateIdentifier(type, \"collection type\");\n\treturn `ec_${type}`;\n}\n\n/**\n * Serialize a value for database storage\n * Objects/arrays are JSON-stringified\n * Booleans are converted to 0/1 for SQLite\n */\nfunction serializeValue(value: unknown): unknown {\n\tif (value === null || value === undefined) {\n\t\treturn null;\n\t}\n\tif (typeof value === \"boolean\") {\n\t\treturn value ? 1 : 0;\n\t}\n\tif (typeof value === \"object\") {\n\t\treturn JSON.stringify(value);\n\t}\n\treturn value;\n}\n\nfunction writableContentData(data: Record<string, unknown>): Record<string, unknown> {\n\tconst writable: Record<string, unknown> = {};\n\tfor (const [key, value] of Object.entries(data)) {\n\t\tif (SYSTEM_COLUMNS.has(key)) continue;\n\t\tvalidateIdentifier(key, \"content field name\");\n\t\twritable[key] = value;\n\t}\n\treturn writable;\n}\n\n/**\n * Deserialize a value from database storage\n * Attempts to parse JSON strings that look like objects/arrays\n */\nfunction deserializeValue(value: unknown): unknown {\n\tif (typeof value === \"string\") {\n\t\t// Try to parse if it looks like JSON\n\t\tif (value.startsWith(\"{\") || value.startsWith(\"[\")) {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(value);\n\t\t\t} catch {\n\t\t\t\treturn value;\n\t\t\t}\n\t\t}\n\t}\n\treturn value;\n}\n\n/** Pattern for escaping special regex characters */\nconst REGEX_ESCAPE_PATTERN = /[.*+?^${}()|[\\]\\\\]/g;\n\n/**\n * Escape special regex characters in a string for use in `new RegExp()`\n */\nfunction escapeRegExp(s: string): string {\n\treturn s.replace(REGEX_ESCAPE_PATTERN, \"\\\\$&\");\n}\n\n/**\n * Repository for content CRUD operations\n *\n * Content is stored in per-collection tables (ec_posts, ec_pages, etc.)\n * Each field becomes a real column in the table.\n */\nexport class ContentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new content item\n\t */\n\tasync create(input: CreateContentInput): Promise<ContentItem> {\n\t\tconst id = input.id ?? ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tconst {\n\t\t\ttype,\n\t\t\tslug,\n\t\t\tdata,\n\t\t\tstatus = \"draft\",\n\t\t\tauthorId,\n\t\t\tprimaryBylineId,\n\t\t\tlocale,\n\t\t\ttranslationOf,\n\t\t\tpublishedAt,\n\t\t\tcreatedAt,\n\t\t} = input;\n\n\t\t// Validate required fields\n\t\tif (!type) {\n\t\t\tthrow new EmDashValidationError(\"Content type is required\");\n\t\t}\n\n\t\tconst tableName = getTableName(type);\n\n\t\t// Resolve translation_group: if translationOf is set, look up the source item's group\n\t\tlet translationGroup: string = id; // default: self-reference\n\t\tif (translationOf) {\n\t\t\tconst source = await this.findById(type, translationOf);\n\t\t\tif (!source) {\n\t\t\t\tthrow new EmDashValidationError(\"Translation source content not found\");\n\t\t\t}\n\t\t\ttranslationGroup = source.translationGroup || source.id;\n\t\t}\n\n\t\t// Build column names and values\n\t\tconst columns: string[] = [\n\t\t\t\"id\",\n\t\t\t\"slug\",\n\t\t\t\"status\",\n\t\t\t\"author_id\",\n\t\t\t\"primary_byline_id\",\n\t\t\t\"created_at\",\n\t\t\t\"updated_at\",\n\t\t\t\"published_at\",\n\t\t\t\"version\",\n\t\t\t\"locale\",\n\t\t\t\"translation_group\",\n\t\t];\n\t\tconst values: unknown[] = [\n\t\t\tid,\n\t\t\tslug || null,\n\t\t\tstatus,\n\t\t\tauthorId || null,\n\t\t\tprimaryBylineId ?? null,\n\t\t\tcreatedAt || now,\n\t\t\tnow,\n\t\t\tpublishedAt || null,\n\t\t\t1,\n\t\t\tlocale || \"en\",\n\t\t\ttranslationGroup,\n\t\t];\n\n\t\t// Add data fields as columns (skip system columns to prevent injection via data)\n\t\tif (data && typeof data === \"object\") {\n\t\t\tfor (const [key, value] of Object.entries(data)) {\n\t\t\t\tif (!SYSTEM_COLUMNS.has(key)) {\n\t\t\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\t\t\tcolumns.push(key);\n\t\t\t\t\tvalues.push(serializeValue(value));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Build dynamic INSERT using raw SQL\n\t\tconst columnRefs = columns.map((c) => sql.ref(c));\n\t\tconst valuePlaceholders = values.map((v) => (v === null ? sql`NULL` : sql`${v}`));\n\n\t\tawait sql`\n\t\t\tINSERT INTO ${sql.ref(tableName)} (${sql.join(columnRefs, sql`, `)})\n\t\t\tVALUES (${sql.join(valuePlaceholders, sql`, `)})\n\t\t`.execute(this.db);\n\n\t\tinvalidateCollectionCache(type);\n\n\t\t// Fetch and return the created item\n\t\tconst item = await this.findById(type, id);\n\t\tif (!item) {\n\t\t\tthrow new Error(\"Failed to create content\");\n\t\t}\n\t\treturn item;\n\t}\n\n\t/**\n\t * Generate a unique slug for a content item within a collection.\n\t *\n\t * Checks the collection table for existing slugs that match `baseSlug`\n\t * (optionally scoped to a locale) and appends a numeric suffix (`-1`,\n\t * `-2`, etc.) on collision to guarantee uniqueness.\n\t *\n\t * Returns null when slug normalization cannot produce a value.\n\t */\n\tasync generateUniqueSlug(type: string, text: string, locale?: string): Promise<string | null> {\n\t\tconst baseSlug = slugify(text);\n\t\tif (!baseSlug) return null;\n\n\t\tconst tableName = getTableName(type);\n\n\t\t// Check if the base slug is available\n\t\tconst existing = locale\n\t\t\t? await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug}\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tif (existing.rows.length === 0) {\n\t\t\treturn baseSlug;\n\t\t}\n\n\t\t// Find all slugs matching the pattern `baseSlug` or `baseSlug-N`\n\t\tconst pattern = `${baseSlug}-%`;\n\t\tconst candidates = locale\n\t\t\t? await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE (slug = ${baseSlug} OR slug LIKE ${pattern})\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<{ slug: string }>`\n\t\t\t\t\tSELECT slug FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${baseSlug} OR slug LIKE ${pattern}\n\t\t\t\t`.execute(this.db);\n\n\t\t// Find the highest numeric suffix in use\n\t\tlet maxSuffix = 0;\n\t\tconst suffixPattern = new RegExp(`^${escapeRegExp(baseSlug)}-(\\\\d+)$`);\n\t\tfor (const row of candidates.rows) {\n\t\t\tconst match = suffixPattern.exec(row.slug);\n\t\t\tif (match) {\n\t\t\t\tconst n = parseInt(match[1], 10);\n\t\t\t\tif (n > maxSuffix) maxSuffix = n;\n\t\t\t}\n\t\t}\n\n\t\treturn `${baseSlug}-${maxSuffix + 1}`;\n\t}\n\n\t/**\n\t * Duplicate a content item\n\t * Creates a new draft copy with \"(Copy)\" appended to the title.\n\t * A slug is auto-generated from the new title by the handler layer.\n\t */\n\tasync duplicate(type: string, id: string, authorId?: string): Promise<ContentItem> {\n\t\t// Fetch the original item\n\t\tconst original = await this.findById(type, id);\n\t\tif (!original) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Prepare the new data\n\t\tconst newData = { ...original.data };\n\n\t\t// Append \"(Copy)\" to title if present\n\t\tif (typeof newData.title === \"string\") {\n\t\t\tnewData.title = `${newData.title} (Copy)`;\n\t\t} else if (typeof newData.name === \"string\") {\n\t\t\tnewData.name = `${newData.name} (Copy)`;\n\t\t}\n\n\t\t// Auto-generate a unique slug from the new title/name\n\t\tconst slugSource =\n\t\t\ttypeof newData.title === \"string\"\n\t\t\t\t? newData.title\n\t\t\t\t: typeof newData.name === \"string\"\n\t\t\t\t\t? newData.name\n\t\t\t\t\t: null;\n\n\t\tconst slug = slugSource\n\t\t\t? await this.generateUniqueSlug(type, slugSource, original.locale ?? undefined)\n\t\t\t: null;\n\n\t\t// Create the duplicate as a draft — use override authorId if provided (caller owns the copy)\n\t\treturn this.create({\n\t\t\ttype,\n\t\t\tslug,\n\t\t\tdata: newData,\n\t\t\tstatus: \"draft\",\n\t\t\tauthorId: authorId || original.authorId || undefined,\n\t\t\tlocale: original.locale ?? undefined,\n\t\t});\n\t}\n\n\t/**\n\t * Find content by ID\n\t */\n\tasync findById(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by id, including trashed (soft-deleted) items.\n\t * Used by restore endpoint for ownership checks.\n\t */\n\tasync findByIdIncludingTrashed(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by ID or slug. Tries ID first if it looks like a ULID,\n\t * otherwise tries slug. Falls back to the other if the first lookup misses.\n\t */\n\tasync findByIdOrSlug(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\treturn this._findByIdOrSlug(type, identifier, false, locale);\n\t}\n\n\t/**\n\t * Find content by ID or slug, including trashed (soft-deleted) items.\n\t * Used by restore/permanent-delete endpoints.\n\t */\n\tasync findByIdOrSlugIncludingTrashed(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\treturn this._findByIdOrSlug(type, identifier, true, locale);\n\t}\n\n\tprivate async _findByIdOrSlug(\n\t\ttype: string,\n\t\tidentifier: string,\n\t\tincludeTrashed: boolean,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\t// ULIDs are 26 uppercase alphanumeric chars\n\t\tconst looksLikeUlid = ULID_PATTERN.test(identifier);\n\n\t\tconst findById = includeTrashed\n\t\t\t? (t: string, id: string) => this.findByIdIncludingTrashed(t, id)\n\t\t\t: (t: string, id: string) => this.findById(t, id);\n\t\tconst findBySlug = includeTrashed\n\t\t\t? (t: string, s: string) => this.findBySlugIncludingTrashed(t, s, locale)\n\t\t\t: (t: string, s: string) => this.findBySlug(t, s, locale);\n\n\t\ttry {\n\t\t\tif (looksLikeUlid) {\n\t\t\t\t// Try ID first, fall back to slug\n\t\t\t\tconst byId = await findById(type, identifier);\n\t\t\t\tif (byId) return byId;\n\t\t\t\treturn await findBySlug(type, identifier);\n\t\t\t}\n\t\t\t// Try slug first, fall back to ID\n\t\t\tconst bySlug = await findBySlug(type, identifier);\n\t\t\tif (bySlug) return bySlug;\n\t\t\treturn await findById(type, identifier);\n\t\t} catch (error) {\n\t\t\t// A collection dropped out from under a still-referencing caller (e.g. a\n\t\t\t// relation whose collection was deleted without cascading) leaves the\n\t\t\t// ec_* table missing. Treat that as \"not found\", matching\n\t\t\t// findManyByIdOrSlug and findTranslationsForGroups, so callers surface a\n\t\t\t// structured NOT_FOUND instead of a 500.\n\t\t\tif (isMissingTableError(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * Find content by slug\n\t */\n\tasync findBySlug(type: string, slug: string, locale?: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = locale\n\t\t\t? await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find content by slug, including trashed (soft-deleted) items.\n\t * Used by restore/permanent-delete endpoints.\n\t */\n\tasync findBySlugIncludingTrashed(\n\t\ttype: string,\n\t\tslug: string,\n\t\tlocale?: string,\n\t): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = locale\n\t\t\t? await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tAND locale = ${locale}\n\t\t\t\t`.execute(this.db)\n\t\t\t: await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug = ${slug}\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t\tLIMIT 1\n\t\t\t\t`.execute(this.db);\n\n\t\tconst row = result.rows[0];\n\t\tif (!row) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn this.mapRow(type, row);\n\t}\n\n\t/**\n\t * Find many content items with filtering and pagination\n\t */\n\tasync findMany(\n\t\ttype: string,\n\t\toptions: FindManyOptions = {},\n\t): Promise<FindManyResult<ContentItem>> {\n\t\tconst tableName = getTableName(type);\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\t// Determine ordering\n\t\tconst orderField = options.orderBy?.field || \"createdAt\";\n\t\tconst orderDirection = options.orderBy?.direction || \"desc\";\n\t\tconst resolvedOrderField = await this.resolveOrderField(\n\t\t\ttype,\n\t\t\torderField,\n\t\t\toptions.sortableExtras,\n\t\t);\n\t\tconst dbField = resolvedOrderField.column;\n\t\tconst resolvedFieldFilters = await this.resolveFieldFilters(type, options.where?.fieldFilters);\n\n\t\t// Validate order direction to prevent injection\n\t\tconst safeOrderDirection = orderDirection.toLowerCase() === \"asc\" ? \"ASC\" : \"DESC\";\n\n\t\t// Build query with parameterized values (no string interpolation)\n\t\t// Note: Dynamic content tables have deleted_at column, cast needed for Kysely\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.selectAll()\n\t\t\t.where(\"deleted_at\" as never, \"is\", null);\n\n\t\t// Apply filters with parameterized queries\n\t\tif (options.where?.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.where.status);\n\t\t}\n\n\t\tif (options.where?.authorId) {\n\t\t\tquery = query.where(\"author_id\", \"=\", options.where.authorId);\n\t\t}\n\n\t\tif (options.where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", options.where.locale);\n\t\t}\n\n\t\tquery = this.applySearchFilter(query, options.where, type);\n\t\tquery = this.applyDateFilter(query, options.where);\n\t\tquery = this.applyBylineFilter(query, options.where, type);\n\t\tquery = this.applyFieldFilters(query, resolvedFieldFilters);\n\n\t\t// Handle cursor pagination — decodeCursor throws InvalidCursorError\n\t\t// on malformed input; let it propagate so handlers surface a\n\t\t// structured INVALID_CURSOR rather than silently returning page 1.\n\t\tif (options.cursor) {\n\t\t\tif (resolvedOrderField.indexedCustomField) {\n\t\t\t\tconst { value, id: cursorId } = decodeIndexedFieldCursor(options.cursor, orderField);\n\t\t\t\tconst isPresent = sql<boolean>`${sql.ref(dbField)} IS NOT NULL`;\n\t\t\t\tconst falseLiteral = sql<boolean>`FALSE`;\n\t\t\t\tconst trueLiteral = sql<boolean>`TRUE`;\n\t\t\t\tif (safeOrderDirection === \"ASC\" && value === null) {\n\t\t\t\t\tquery = query.where(sql<boolean>`\n\t\t\t\t\t\t(${isPresent}) > ${falseLiteral}\n\t\t\t\t\t\tOR ((${isPresent}) = ${falseLiteral} AND ${sql.ref(\"id\")} > ${cursorId})\n\t\t\t\t\t`);\n\t\t\t\t} else if (safeOrderDirection === \"DESC\" && value === null) {\n\t\t\t\t\tquery = query.where(sql<boolean>`\n\t\t\t\t\t\t(${isPresent}) = ${falseLiteral} AND ${sql.ref(\"id\")} < ${cursorId}\n\t\t\t\t\t`);\n\t\t\t\t} else if (safeOrderDirection === \"ASC\") {\n\t\t\t\t\tquery = query.where(sql<boolean>`\n\t\t\t\t\t\t(${isPresent}, ${sql.ref(dbField)}, ${sql.ref(\"id\")})\n\t\t\t\t\t\t\t> (${trueLiteral}, ${value}, ${cursorId})\n\t\t\t\t\t`);\n\t\t\t\t} else {\n\t\t\t\t\tquery = query.where(sql<boolean>`\n\t\t\t\t\t\t(${isPresent}, ${sql.ref(dbField)}, ${sql.ref(\"id\")})\n\t\t\t\t\t\t\t< (${trueLiteral}, ${value}, ${cursorId})\n\t\t\t\t\t`);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tconst { orderValue, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\t\tif (safeOrderDirection === \"DESC\") {\n\t\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\t\teb.or([\n\t\t\t\t\t\t\teb(dbField as any, \"<\", orderValue),\n\t\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t} else {\n\t\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\t\teb.or([\n\t\t\t\t\t\t\teb(dbField as any, \">\", orderValue),\n\t\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \">\", cursorId)]),\n\t\t\t\t\t\t]),\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Apply ordering and limit\n\t\tconst indexedOrderFilter = resolvedOrderField.indexedCustomField\n\t\t\t? resolvedFieldFilters.find((filter) => filter.column === dbField)\n\t\t\t: undefined;\n\t\tif (resolvedOrderField.indexedCustomField && !indexedOrderFilter) {\n\t\t\tquery = query.orderBy(\n\t\t\t\tsql<boolean>`${sql.ref(dbField)} IS NOT NULL`,\n\t\t\t\tsafeOrderDirection === \"ASC\" ? \"asc\" : \"desc\",\n\t\t\t);\n\t\t}\n\t\tif (indexedOrderFilter?.kind !== \"null\") {\n\t\t\tquery = query.orderBy(dbField as any, safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\");\n\t\t}\n\t\tquery = query.orderBy(\"id\", safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\").limit(limit + 1);\n\n\t\t// Run the page fetch and the unbounded count together — the UI needs\n\t\t// both to render a stable denominator (kept on every page intentionally),\n\t\t// and issuing them in parallel on SQLite is essentially free.\n\t\t//\n\t\t// Settled rather than raced: a collection whose table is missing rejects\n\t\t// both, and `Promise.all` returns on the first. The loser stays in flight\n\t\t// holding a pooled connection, so a Postgres pool destroyed in that window\n\t\t// never finishes closing.\n\t\tconst [rowsResult, countResult] = await Promise.allSettled([\n\t\t\tquery.execute(),\n\t\t\tthis.countWithResolvedFilters(type, options.where, resolvedFieldFilters),\n\t\t]);\n\t\tif (rowsResult.status === \"rejected\") throw rowsResult.reason;\n\t\tif (countResult.status === \"rejected\") throw countResult.reason;\n\t\tconst rows = rowsResult.value;\n\t\tconst total = countResult.value;\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit);\n\n\t\tconst mappedResult: FindManyResult<ContentItem> = {\n\t\t\titems: items.map((row) => this.mapRow(type, row as Record<string, unknown>)),\n\t\t\ttotal,\n\t\t};\n\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastRow = items.at(-1) as Record<string, unknown>;\n\t\t\tconst lastOrderValue = lastRow[dbField];\n\t\t\tif (resolvedOrderField.indexedCustomField) {\n\t\t\t\tif (\n\t\t\t\t\tlastOrderValue !== null &&\n\t\t\t\t\ttypeof lastOrderValue !== \"string\" &&\n\t\t\t\t\ttypeof lastOrderValue !== \"number\"\n\t\t\t\t) {\n\t\t\t\t\tthrow new EmDashValidationError(`Invalid indexed value for order field: ${orderField}`);\n\t\t\t\t}\n\t\t\t\tmappedResult.nextCursor = encodeIndexedFieldCursor(\n\t\t\t\t\torderField,\n\t\t\t\t\tlastOrderValue,\n\t\t\t\t\tString(lastRow.id),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tconst orderStr =\n\t\t\t\t\ttypeof lastOrderValue === \"string\" || typeof lastOrderValue === \"number\"\n\t\t\t\t\t\t? String(lastOrderValue)\n\t\t\t\t\t\t: \"\";\n\t\t\t\tmappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));\n\t\t\t}\n\t\t}\n\n\t\treturn mappedResult;\n\t}\n\n\tprivate normalizeScheduledAt(value: string | null): string | null {\n\t\tif (value === null) return null;\n\t\tconst scheduledDate = new Date(value);\n\t\tif (isNaN(scheduledDate.getTime())) {\n\t\t\tthrow new EmDashValidationError(\"Invalid scheduled date\");\n\t\t}\n\t\treturn scheduledDate.toISOString();\n\t}\n\n\t/**\n\t * Update content\n\t */\n\tasync update(type: string, id: string, input: UpdateContentInput): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Every update advances the optimistic-concurrency version. updated_at\n\t\t// advances only when a content-row column actually changes.\n\t\tconst updates: Record<string, unknown> = {};\n\n\t\tif (input.status !== undefined) {\n\t\t\tupdates.status = input.status;\n\t\t}\n\n\t\tif (input.slug !== undefined) {\n\t\t\tupdates.slug = input.slug;\n\t\t}\n\n\t\tif (input.publishedAt !== undefined) {\n\t\t\tupdates.published_at = input.publishedAt;\n\t\t}\n\n\t\tif (input.scheduledAt !== undefined) {\n\t\t\tupdates.scheduled_at = this.normalizeScheduledAt(input.scheduledAt);\n\t\t}\n\n\t\tif (input.authorId !== undefined) {\n\t\t\tupdates.author_id = input.authorId;\n\t\t}\n\n\t\tif (input.primaryBylineId !== undefined) {\n\t\t\tupdates.primary_byline_id = input.primaryBylineId;\n\t\t}\n\n\t\t// Update data fields (skip system columns to prevent injection via data)\n\t\tif (input.data !== undefined && typeof input.data === \"object\") {\n\t\t\tfor (const [key, value] of Object.entries(writableContentData(input.data))) {\n\t\t\t\tupdates[key] = serializeValue(value);\n\t\t\t}\n\t\t}\n\n\t\tconst hasColumnWrites = Object.keys(updates).length > 0;\n\t\tif (hasColumnWrites) {\n\t\t\tupdates.updated_at = now;\n\t\t}\n\t\tupdates.version = sql`version + 1`;\n\n\t\tawait this.db\n\t\t\t.updateTable(tableName as keyof Database)\n\t\t\t.set(updates)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.execute();\n\n\t\tif (hasColumnWrites) invalidateCollectionCache(type);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Update plugin-authored fields without letting content columns diverge\n\t * from the revision pointers that publication promotes.\n\t */\n\tasync updateDraftAware(\n\t\ttype: string,\n\t\tid: string,\n\t\tinput: UpdateContentInput,\n\t): Promise<ContentItem> {\n\t\tconst data = input.data ? writableContentData(input.data) : {};\n\t\tconst stagedSlug = typeof input.slug === \"string\" ? input.slug : undefined;\n\t\tconst hasDraftUpdate = Object.keys(data).length > 0 || stagedSlug !== undefined;\n\n\t\tif (!hasDraftUpdate) {\n\t\t\treturn this.update(type, id, { ...input, data });\n\t\t}\n\n\t\tconst collectionRows = await this.db\n\t\t\t.selectFrom(\"_emdash_collections as collection\")\n\t\t\t.leftJoin(\"_emdash_fields as field\", \"field.collection_id\", \"collection.id\")\n\t\t\t.select([\"collection.supports\", \"field.slug as fieldSlug\"])\n\t\t\t.where(\"collection.slug\", \"=\", type)\n\t\t\t.execute();\n\t\tconst supportsRaw = collectionRows[0]?.supports;\n\t\tconst supports: unknown = supportsRaw ? JSON.parse(supportsRaw) : [];\n\t\tif (!Array.isArray(supports) || !supports.includes(\"revisions\")) {\n\t\t\treturn this.update(type, id, { ...input, data });\n\t\t}\n\n\t\tconst fieldSlugs = new Set(collectionRows.map((row) => row.fieldSlug).filter(Boolean));\n\t\tfor (const field of Object.keys(data)) {\n\t\t\tif (!fieldSlugs.has(field)) {\n\t\t\t\tthrow new EmDashValidationError(`Unknown field '${field}' in collection '${type}'`);\n\t\t\t}\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tlet existing = await this.findById(type, id);\n\n\t\tfor (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {\n\t\t\tlet baseData = existing.data;\n\t\t\tif (existing.draftRevisionId) {\n\t\t\t\tconst draft = await revisionRepo.findById(existing.draftRevisionId);\n\t\t\t\tif (draft) baseData = draft.data;\n\t\t\t}\n\n\t\t\tconst mergedData = { ...baseData, ...data };\n\t\t\tif (stagedSlug !== undefined) mergedData._slug = stagedSlug;\n\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\tcollection: type,\n\t\t\t\tentryId: id,\n\t\t\t\tdata: mergedData,\n\t\t\t\t...(input.authorId ? { authorId: input.authorId } : {}),\n\t\t\t});\n\n\t\t\tlet staged: boolean;\n\t\t\ttry {\n\t\t\t\tstaged = await this.replaceDraftRevisionForUpdate(type, id, revision.id, existing, input);\n\t\t\t} catch (error) {\n\t\t\t\tawait this.deleteUnstagedRevision(revisionRepo, type, id, revision.id);\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\tif (staged) {\n\t\t\t\tconst updated = await this.findById(type, id);\n\t\t\t\tif (!updated) throw new Error(\"Content not found\");\n\t\t\t\tconst draftData: Record<string, unknown> = {};\n\t\t\t\tfor (const [key, value] of Object.entries(mergedData)) {\n\t\t\t\t\tif (!key.startsWith(\"_\")) draftData[key] = value;\n\t\t\t\t}\n\t\t\t\treturn { ...updated, data: { ...updated.data, ...draftData } };\n\t\t\t}\n\n\t\t\tawait this.deleteUnstagedRevision(revisionRepo, type, id, revision.id);\n\t\t\texisting = await this.findById(type, id);\n\t\t}\n\n\t\tif (!existing) throw new Error(\"Content not found\");\n\t\tthrow new ContentMutationConflictError();\n\t}\n\n\tprivate async deleteUnstagedRevision(\n\t\trevisionRepo: RevisionRepository,\n\t\ttype: string,\n\t\tid: string,\n\t\trevisionId: string,\n\t): Promise<void> {\n\t\ttry {\n\t\t\tawait revisionRepo.deleteIfUnreferenced(type, id, revisionId);\n\t\t} catch (error) {\n\t\t\tconsole.error(`[content] Failed to clean up unstaged revision ${revisionId}:`, error);\n\t\t}\n\t}\n\n\tprivate async replaceDraftRevisionForUpdate(\n\t\ttype: string,\n\t\tid: string,\n\t\trevisionId: string,\n\t\texpected: ContentItem,\n\t\tinput: UpdateContentInput,\n\t): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\t\tconst assignments = [sql`draft_revision_id = ${revisionId}`];\n\t\tlet liveMetadataChanged = false;\n\n\t\tif (input.status !== undefined) {\n\t\t\tassignments.push(sql`status = ${input.status}`);\n\t\t\tliveMetadataChanged = true;\n\t\t}\n\t\tif (input.slug === null) {\n\t\t\tassignments.push(sql`slug = NULL`);\n\t\t\tliveMetadataChanged = true;\n\t\t}\n\t\tif (input.publishedAt !== undefined) {\n\t\t\tassignments.push(sql`published_at = ${input.publishedAt}`);\n\t\t\tliveMetadataChanged = true;\n\t\t}\n\t\tif (input.scheduledAt !== undefined) {\n\t\t\tassignments.push(sql`scheduled_at = ${this.normalizeScheduledAt(input.scheduledAt)}`);\n\t\t\tliveMetadataChanged = true;\n\t\t}\n\t\tif (input.authorId !== undefined) {\n\t\t\tassignments.push(sql`author_id = ${input.authorId}`);\n\t\t\tliveMetadataChanged = true;\n\t\t}\n\t\tif (input.primaryBylineId !== undefined) {\n\t\t\tassignments.push(sql`primary_byline_id = ${input.primaryBylineId}`);\n\t\t\tliveMetadataChanged = true;\n\t\t}\n\t\tif (liveMetadataChanged) assignments.push(sql`updated_at = ${new Date().toISOString()}`);\n\t\tassignments.push(sql`version = version + 1`);\n\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET ${sql.join(assignments, sql`, `)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t\tAND version = ${expected.version}\n\t\t\tAND ${nullableColumnMatch(\"live_revision_id\", expected.liveRevisionId)}\n\t\t\tAND ${nullableColumnMatch(\"draft_revision_id\", expected.draftRevisionId)}\n\t\t\tAND EXISTS (\n\t\t\t\tSELECT 1 FROM revisions\n\t\t\t\tWHERE revisions.id = ${revisionId}\n\t\t\t\tAND revisions.collection = ${type}\n\t\t\t\tAND revisions.entry_id = ${id}\n\t\t\t)\n\t\t`.execute(this.db);\n\n\t\tconst changed = (result.numAffectedRows ?? 0n) > 0n;\n\t\tif (changed && liveMetadataChanged) invalidateCollectionCache(type);\n\t\treturn changed;\n\t}\n\n\t/**\n\t * Delete content (soft delete - moves to trash)\n\t */\n\tasync delete(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET deleted_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tconst changed = (result.numAffectedRows ?? 0n) > 0n;\n\t\tif (changed) {\n\t\t\tinvalidateCollectionCache(type);\n\t\t}\n\t\treturn changed;\n\t}\n\n\t/**\n\t * Restore content from trash\n\t */\n\tasync restore(type: string, id: string): Promise<ContentItem | null> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET deleted_at = NULL\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NOT NULL\n\t\t\tRETURNING *\n\t\t`.execute(this.db);\n\n\t\tconst restored = result.rows[0];\n\t\tif (!restored) return null;\n\n\t\tinvalidateCollectionCache(type);\n\t\treturn this.mapRow(type, restored);\n\t}\n\n\t/**\n\t * Permanently delete content (cannot be undone)\n\t */\n\t/**\n\t * Permanently delete a soft-deleted content row.\n\t *\n\t * Returns `true` only when a soft-deleted (trashed) row was removed.\n\t * Returns `false` when no row exists OR when the row exists but is live —\n\t * the caller is responsible for distinguishing these cases (typically via\n\t * a follow-up `findByIdOrSlugIncludingTrashed` to surface NOT_FOUND vs\n\t * NOT_TRASHED). The `AND deleted_at IS NOT NULL` clause is the safety net\n\t * that prevents permanent delete from bypassing the trash workflow.\n\t */\n\tasync permanentDelete(type: string, id: string): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql`\n\t\t\tDELETE FROM ${sql.ref(tableName)}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NOT NULL\n\t\t`.execute(this.db);\n\n\t\tconst changed = (result.numAffectedRows ?? 0n) > 0n;\n\t\tif (changed) invalidateCollectionCache(type);\n\t\treturn changed;\n\t}\n\n\t/**\n\t * Find trashed content items\n\t */\n\tasync findTrashed(\n\t\ttype: string,\n\t\toptions: Omit<FindManyOptions, \"where\"> & { where?: { locale?: string } } = {},\n\t): Promise<FindManyResult<ContentItem & { deletedAt: string }>> {\n\t\tconst tableName = getTableName(type);\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\t// Determine ordering - default to most recently deleted\n\t\tconst orderField = options.orderBy?.field || \"deletedAt\";\n\t\tconst orderDirection = options.orderBy?.direction || \"desc\";\n\t\tconst dbField = this.mapOrderField(orderField);\n\n\t\tconst safeOrderDirection = orderDirection.toLowerCase() === \"asc\" ? \"ASC\" : \"DESC\";\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.selectAll()\n\t\t\t.where(\"deleted_at\" as never, \"is not\", null);\n\n\t\tif (options.where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", options.where.locale);\n\t\t}\n\n\t\t// Handle cursor pagination — decodeCursor throws on invalid input.\n\t\tif (options.cursor) {\n\t\t\tconst { orderValue, id: cursorId } = decodeCursor(options.cursor);\n\n\t\t\tif (safeOrderDirection === \"DESC\") {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \"<\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \"<\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(dbField as any, \">\", orderValue),\n\t\t\t\t\t\teb.and([eb(dbField as any, \"=\", orderValue), eb(\"id\", \">\", cursorId)]),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(dbField as any, safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.orderBy(\"id\", safeOrderDirection === \"ASC\" ? \"asc\" : \"desc\")\n\t\t\t.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit);\n\n\t\tconst mappedResult: FindManyResult<ContentItem & { deletedAt: string }> = {\n\t\t\titems: items.map((row) => {\n\t\t\t\tconst record = row as Record<string, unknown>;\n\t\t\t\treturn {\n\t\t\t\t\t...this.mapRow(type, record),\n\t\t\t\t\tdeletedAt: typeof record.deleted_at === \"string\" ? record.deleted_at : \"\",\n\t\t\t\t};\n\t\t\t}),\n\t\t};\n\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst lastRow = items.at(-1) as Record<string, unknown>;\n\t\t\tconst lastOrderValue = lastRow[dbField];\n\t\t\tconst orderStr =\n\t\t\t\ttypeof lastOrderValue === \"string\" || typeof lastOrderValue === \"number\"\n\t\t\t\t\t? String(lastOrderValue)\n\t\t\t\t\t: \"\";\n\t\t\tmappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));\n\t\t}\n\n\t\treturn mappedResult;\n\t}\n\n\t/**\n\t * Count trashed content items\n\t */\n\tasync countTrashed(type: string, options: { locale?: string } = {}): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"deleted_at\" as never, \"is not\", null);\n\n\t\tif (options.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", options.locale);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Apply the optional `q` filter.\n\t *\n\t * When the handler sets `useFts` (collection has a healthy FTS5 index\n\t * covering the display columns; SQLite only), the filter is served from\n\t * the index: a token-prefix MATCH against `_emdash_fts_<slug>` OR'd with\n\t * an index-served `slug GLOB 'term*'` prefix (the slug is not in the FTS\n\t * index). Both sides are index-backed, so SQLite's OR optimization avoids\n\t * the full-table scan the LIKE fallback needs (#1517). The trade-off is\n\t * search semantics: token-prefix matching instead of arbitrary substring.\n\t *\n\t * Fallback (Postgres, search disabled, or no usable terms): case-\n\t * insensitive substring LIKE across the handler-resolved `searchColumns`\n\t * (OR'd). User input is treated literally (LIKE wildcards escaped) and\n\t * `lower()` is applied on both sides for SQLite/Postgres parity.\n\t */\n\tprivate applySearchFilter<QB extends { where: (cb: (eb: any) => unknown) => QB }>(\n\t\tquery: QB,\n\t\twhere: { q?: string; searchColumns?: string[]; useFts?: boolean } | undefined,\n\t\ttype: string,\n\t): QB {\n\t\tconst term = where?.q?.trim();\n\t\tconst columns = where?.searchColumns;\n\t\tif (!term || !columns || columns.length === 0) return query;\n\n\t\tif (where.useFts) {\n\t\t\tconst match = buildFtsPrefixMatch(term);\n\t\t\tif (match) {\n\t\t\t\tvalidateIdentifier(type, \"collection slug\");\n\t\t\t\tconst ftsTable = `_emdash_fts_${type}`;\n\t\t\t\tconst slugPrefix = buildSlugGlobPrefix(term);\n\t\t\t\treturn query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\tsql<boolean>`id IN (SELECT id FROM ${sql.ref(ftsTable)} WHERE ${sql.ref(ftsTable)} MATCH ${match})`,\n\t\t\t\t\t\tsql<boolean>`slug GLOB ${slugPrefix}`,\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\t\t\t// No usable terms (e.g. quotes only) — fall through to LIKE.\n\t\t}\n\n\t\tconst escaped = term.replace(LIKE_WILDCARD_RE, (c) => `\\\\${c}`);\n\t\tconst pattern = `%${escaped}%`;\n\n\t\treturn query.where((eb) =>\n\t\t\teb.or(\n\t\t\t\tcolumns.map((col) => {\n\t\t\t\t\tvalidateIdentifier(col, \"search column\");\n\t\t\t\t\treturn sql<boolean>`lower(CAST(${sql.ref(col)} AS TEXT)) LIKE lower(${pattern}) ESCAPE '\\\\'`;\n\t\t\t\t}),\n\t\t\t),\n\t\t);\n\t}\n\n\t/**\n\t * Apply the optional inclusive date-range filter. The field is mapped\n\t * through `DATE_FILTER_COLUMNS` (a closed whitelist), and bounds compare\n\t * lexicographically against the stored ISO 8601 timestamps. A `publishedAt`\n\t * range naturally excludes never-published rows (their column is NULL).\n\t */\n\tprivate applyDateFilter<QB extends { where: (cb: (eb: any) => unknown) => QB }>(\n\t\tquery: QB,\n\t\twhere?: { dateFilter?: { field: string; from?: string; to?: string } },\n\t): QB {\n\t\tconst filter = where?.dateFilter;\n\t\tif (!filter) return query;\n\t\tconst column = DATE_FILTER_COLUMNS[filter.field as ContentDateField];\n\t\tif (!column) {\n\t\t\tthrow new EmDashValidationError(`Invalid date filter field: ${filter.field}`);\n\t\t}\n\t\tconst { from, to } = filter;\n\t\tif (!from && !to) return query;\n\n\t\tlet next = query;\n\t\tif (from) next = next.where((eb) => eb(column as any, \">=\", from));\n\t\tif (to) next = next.where((eb) => eb(column as any, \"<=\", to));\n\t\treturn next;\n\t}\n\n\t/**\n\t * Apply the optional byline filter as a correlated (NOT) EXISTS against\n\t * `_emdash_content_bylines`.\n\t *\n\t * Correlating from the content table preserves the outer sort index so\n\t * `LIMIT` can short-circuit. `mode: \"none\"` tests the junction rather than\n\t * `primary_byline_id` because the two are written in the same call but\n\t * are not atomically consistent, so the junction is authoritative.\n\t *\n\t * Whether a credit *renders* is locale-scoped; whether one *exists* is\n\t * not. Both are needed: the first decides what the filter matches, the\n\t * second decides whether the author fallback applies at all.\n\t */\n\tprivate applyBylineFilter<QB extends { where: (cb: (eb: any) => unknown) => QB }>(\n\t\tquery: QB,\n\t\twhere: { bylineFilter?: ContentBylineFilter } | undefined,\n\t\ttype: string,\n\t): QB {\n\t\tconst filter = where?.bylineFilter;\n\t\tif (!filter) return query;\n\t\tconst tableName = getTableName(type);\n\t\tconst idColumn = `${tableName}.id`;\n\t\tconst authorColumn = `${tableName}.author_id`;\n\t\tconst localeColumn = `${tableName}.locale`;\n\n\t\t// An explicit credit that actually renders — optionally within a given\n\t\t// set of translation groups. The junction stores a group, but a credit\n\t\t// resolves only where that group has a byline row at the locale the\n\t\t// list is scoped to, so this repeats the join `getContentBylinesMany`\n\t\t// makes. `locale` falls back to each entry's own when the list spans\n\t\t// locales.\n\t\tconst creditRenders = (eb: any, bylineIds?: string[]) => {\n\t\t\tlet sub = eb\n\t\t\t\t.selectFrom(\"_emdash_content_bylines as cb\")\n\t\t\t\t.innerJoin(\"_emdash_bylines as b\", \"b.translation_group\", \"cb.byline_id\")\n\t\t\t\t.select(\"cb.id\")\n\t\t\t\t.where(\"cb.collection_slug\", \"=\", type)\n\t\t\t\t.whereRef(\"cb.content_id\", \"=\", idColumn);\n\t\t\tsub = filter.locale\n\t\t\t\t? sub.where(\"b.locale\", \"=\", filter.locale)\n\t\t\t\t: sub.whereRef(\"b.locale\", \"=\", localeColumn);\n\t\t\tif (bylineIds) sub = sub.where(\"cb.byline_id\", \"in\", bylineIds);\n\t\t\treturn eb.exists(sub);\n\t\t};\n\n\t\t// Whether the entry carries an explicit credit at all, at any locale.\n\t\t// Deliberately not locale-scoped: the author fallback is suppressed by\n\t\t// the presence of a junction row, even one that renders nothing here\n\t\t// (`hydrateBylinesMany` gates inference on `primaryBylineId`).\n\t\tconst hasExplicitCredit = (eb: any) =>\n\t\t\teb.exists(\n\t\t\t\teb\n\t\t\t\t\t.selectFrom(\"_emdash_content_bylines as cb\")\n\t\t\t\t\t.select(\"cb.id\")\n\t\t\t\t\t.where(\"cb.collection_slug\", \"=\", type)\n\t\t\t\t\t.whereRef(\"cb.content_id\", \"=\", idColumn),\n\t\t\t);\n\n\t\t// The entry's author owns a byline row — optionally within a given set\n\t\t// of translation groups — at the locale the list is scoped to. Matching\n\t\t// the locale is what keeps the filter agreeing with the list: an\n\t\t// inferred credit renders only when the author's byline has a row at\n\t\t// that locale (`hydrateBylinesMany` -> `findByUserIds`), and byline\n\t\t// translations start life with a null `user_id`, so a group translated\n\t\t// into the locale but not re-linked resolves to no credit. `locale`\n\t\t// falls back to each entry's own when the list spans locales.\n\t\tconst authorHasByline = (eb: any, bylineIds?: string[]) => {\n\t\t\tlet sub = eb\n\t\t\t\t.selectFrom(\"_emdash_bylines as b\")\n\t\t\t\t.select(\"b.id\")\n\t\t\t\t.whereRef(\"b.user_id\", \"=\", authorColumn);\n\t\t\tsub = filter.locale\n\t\t\t\t? sub.where(\"b.locale\", \"=\", filter.locale)\n\t\t\t\t: sub.whereRef(\"b.locale\", \"=\", localeColumn);\n\t\t\tif (bylineIds) sub = sub.where(\"b.translation_group\", \"in\", bylineIds);\n\t\t\treturn eb.exists(sub);\n\t\t};\n\n\t\tif (filter.mode === \"none\") {\n\t\t\treturn query.where((eb: any) => {\n\t\t\t\tconst uncredited = eb.not(creditRenders(eb));\n\t\t\t\t// With inference on, \"no byline\" means none is rendered, so an\n\t\t\t\t// entry that falls through to an author byline is excluded too.\n\t\t\t\treturn filter.includeInferred\n\t\t\t\t\t? eb.and([uncredited, eb.or([hasExplicitCredit(eb), eb.not(authorHasByline(eb))])])\n\t\t\t\t\t: uncredited;\n\t\t\t});\n\t\t}\n\n\t\tconst bylineIds = filter.bylineIds ?? [];\n\t\tif (bylineIds.length === 0) {\n\t\t\t// A filter that resolved to no ids must match nothing rather than\n\t\t\t// silently degrade to \"no filter\" and return the whole collection.\n\t\t\treturn query.where(() => sql<boolean>`1 = 0`);\n\t\t}\n\n\t\treturn query.where((eb: any) => {\n\t\t\tif (!filter.includeInferred) return creditRenders(eb, bylineIds);\n\t\t\t// Inference applies only where no explicit credit exists, so an\n\t\t\t// entry credited to someone else never matches on its author.\n\t\t\treturn eb.or([\n\t\t\t\tcreditRenders(eb, bylineIds),\n\t\t\t\teb.and([eb.not(hasExplicitCredit(eb)), authorHasByline(eb, bylineIds)]),\n\t\t\t]);\n\t\t});\n\t}\n\n\t/**\n\t * Count content items\n\t */\n\tasync count(type: string, where?: FindManyOptions[\"where\"]): Promise<number> {\n\t\tconst resolvedFieldFilters = await this.resolveFieldFilters(type, where?.fieldFilters);\n\t\treturn this.countWithResolvedFilters(type, where, resolvedFieldFilters);\n\t}\n\n\tprivate async countWithResolvedFilters(\n\t\ttype: string,\n\t\twhere: FindManyOptions[\"where\"] | undefined,\n\t\tresolvedFieldFilters: ResolvedFieldFilter[],\n\t): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"deleted_at\" as never, \"is\", null);\n\n\t\tif (where?.status) {\n\t\t\tquery = query.where(\"status\", \"=\", where.status);\n\t\t}\n\n\t\tif (where?.authorId) {\n\t\t\tquery = query.where(\"author_id\", \"=\", where.authorId);\n\t\t}\n\n\t\tif (where?.locale) {\n\t\t\tquery = query.where(\"locale\" as any, \"=\", where.locale);\n\t\t}\n\n\t\tquery = this.applySearchFilter(query, where, type);\n\t\tquery = this.applyDateFilter(query, where);\n\t\tquery = this.applyBylineFilter(query, where, type);\n\t\tquery = this.applyFieldFilters(query, resolvedFieldFilters);\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count || 0);\n\t}\n\n\t/**\n\t * Distinct, non-null `author_id` values across the collection's live\n\t * (non-trashed) content. Used to populate the admin author filter with\n\t * only the users who have actually authored entries, rather than the\n\t * full user directory (which requires admin privileges to read).\n\t */\n\tasync findDistinctAuthorIds(type: string): Promise<string[]> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select(\"author_id\")\n\t\t\t.distinct()\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.where(\"author_id\" as never, \"is not\", null)\n\t\t\t.execute();\n\n\t\treturn rows.map((row) => row.author_id).filter((id): id is string => id !== null);\n\t}\n\n\t// get overall statistics for a content type in a single query\n\tasync getStats(\n\t\ttype: string,\n\t\tnow = new Date(),\n\t): Promise<{\n\t\ttotal: number;\n\t\tpublished: number;\n\t\tdraft: number;\n\t\tscheduled: number;\n\t\toverdueScheduled: number;\n\t}> {\n\t\tconst tableName = getTableName(type);\n\t\tconst nowIso = now.toISOString();\n\n\t\tconst result = await this.db\n\t\t\t.selectFrom(tableName as keyof Database)\n\t\t\t.select((eb) => [\n\t\t\t\teb.fn.count(\"id\").as(\"total\"),\n\t\t\t\teb.fn.sum(eb.case().when(\"status\", \"=\", \"published\").then(1).else(0).end()).as(\"published\"),\n\t\t\t\teb.fn.sum(eb.case().when(\"status\", \"=\", \"draft\").then(1).else(0).end()).as(\"draft\"),\n\t\t\t\tsql<number>`SUM(CASE WHEN scheduled_at IS NOT NULL THEN 1 ELSE 0 END)`.as(\"scheduled\"),\n\t\t\t\tsql<number>`SUM(CASE WHEN scheduled_at IS NOT NULL AND scheduled_at <= ${nowIso} THEN 1 ELSE 0 END)`.as(\n\t\t\t\t\t\"overdue_scheduled\",\n\t\t\t\t),\n\t\t\t])\n\t\t\t.where(\"deleted_at\" as never, \"is\", null)\n\t\t\t.executeTakeFirst();\n\n\t\treturn {\n\t\t\ttotal: Number(result?.total || 0),\n\t\t\tpublished: Number(result?.published || 0),\n\t\t\tdraft: Number(result?.draft || 0),\n\t\t\tscheduled: Number(result?.scheduled || 0),\n\t\t\toverdueScheduled: Number(result?.overdue_scheduled || 0),\n\t\t};\n\t}\n\n\t/**\n\t * Schedule content for future publishing\n\t *\n\t * Sets status to 'scheduled' and stores the scheduled publish time.\n\t * The content will be auto-published when the scheduled time is reached.\n\t */\n\tasync schedule(type: string, id: string, scheduledAt: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Validate scheduledAt is in the future\n\t\tconst scheduledDate = new Date(scheduledAt);\n\t\tif (isNaN(scheduledDate.getTime())) {\n\t\t\tthrow new EmDashValidationError(\"Invalid scheduled date\");\n\t\t}\n\t\tif (scheduledDate <= new Date()) {\n\t\t\tthrow new EmDashValidationError(\"Scheduled date must be in the future\");\n\t\t}\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Published posts keep their status — the schedule applies to the\n\t\t// pending draft, not the currently-live revision. Unpublished posts\n\t\t// transition to 'scheduled' so they aren't visible before the time.\n\t\tconst newStatus = existing.status === \"published\" ? \"published\" : \"scheduled\";\n\n\t\t// The due query compares ISO strings, so every stored schedule uses the same UTC form.\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET status = ${newStatus},\n\t\t\t\tscheduled_at = ${scheduledDate.toISOString()},\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tinvalidateCollectionCache(type);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Unschedule content\n\t *\n\t * Clears the scheduled time. Published posts stay published;\n\t * draft/scheduled posts revert to 'draft'.\n\t */\n\tasync unschedule(type: string, id: string): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\t// Published posts keep their status — just clear the pending schedule.\n\t\t// Draft/scheduled posts revert to 'draft'.\n\t\tconst newStatus = existing.status === \"published\" ? \"published\" : \"draft\";\n\n\t\tawait sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET status = ${newStatus},\n\t\t\t\tscheduled_at = NULL,\n\t\t\t\tupdated_at = ${now}\n\t\t\tWHERE id = ${id}\n\t\t\tAND scheduled_at IS NOT NULL\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\tinvalidateCollectionCache(type);\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Find content that is ready to be published\n\t *\n\t * Returns all content where scheduled_at <= now, regardless of status.\n\t * This covers both draft-scheduled posts (status='scheduled') and\n\t * published posts with scheduled draft changes (status='published').\n\t *\n\t * `limit` (optional) caps how many due rows are returned, oldest-due first.\n\t * The scheduled-publishing sweep passes a limit so a large backlog can't\n\t * fan out unbounded publish/webhook work in a single tick (and blow a Worker\n\t * invocation's CPU/subrequest budget); the remainder drains on later ticks.\n\t */\n\tasync findReadyToPublish(type: string, limit?: number): Promise<ContentItem[]> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\t// Embed an empty fragment when unbounded so callers that want every due\n\t\t// row (manual flows, tests) keep the original behaviour.\n\t\tconst limitClause =\n\t\t\ttypeof limit === \"number\" && Number.isInteger(limit) && limit > 0\n\t\t\t\t? sql`LIMIT ${limit}`\n\t\t\t\t: sql``;\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t\tAND scheduled_at <= ${now}\n\t\t\tAND deleted_at IS NULL\n\t\t\tORDER BY scheduled_at ASC\n\t\t\t${limitClause}\n\t\t`.execute(this.db);\n\n\t\treturn result.rows.map((row) => this.mapRow(type, row));\n\t}\n\n\t/**\n\t * Find all translations in a translation group\n\t */\n\tasync findTranslations(type: string, translationGroup: string): Promise<ContentItem[]> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\tWHERE translation_group = ${translationGroup}\n\t\t\tAND deleted_at IS NULL\n\t\t\tORDER BY locale ASC\n\t\t`.execute(this.db);\n\n\t\treturn result.rows.map((row) => this.mapRow(type, row));\n\t}\n\n\t/**\n\t * Batch variant of {@link findTranslations}: every (non-deleted) locale\n\t * variant for any of `translationGroups`, in one `WHERE translation_group IN\n\t * (...)` query chunked at `SQL_BATCH_SIZE` for D1's bind-parameter limit.\n\t * Lets callers resolve many edge groups without an N+1 per group. The caller\n\t * groups the flat result by `translationGroup` itself.\n\t *\n\t * `translation_group` leads the sort so the ordering follows\n\t * `idx_{table}_del_tg_locale` past its `deleted_at` equality; callers group by\n\t * `translationGroup`, so the per-group locale order they rely on is preserved.\n\t *\n\t * `publishedOnly` restricts the result to `status = 'published'` — reference\n\t * reads pass this for callers without `content:read_drafts` so draft/scheduled\n\t * entries never leak through an edge traversal.\n\t *\n\t * A reference edge stores only a collection slug (no SQL FK), so the table may\n\t * have been dropped since the edge was written. That is a tolerated dangling\n\t * state, not an error: a missing table resolves to no rows, mirroring how the\n\t * content read handlers treat `isMissingTableError`.\n\t */\n\tasync findTranslationsForGroups(\n\t\ttype: string,\n\t\ttranslationGroups: string[],\n\t\toptions: { publishedOnly?: boolean } = {},\n\t): Promise<ContentItem[]> {\n\t\tif (translationGroups.length === 0) return [];\n\t\tconst tableName = getTableName(type);\n\t\tconst publishedFilter = options.publishedOnly ? sql`AND status = 'published'` : sql``;\n\n\t\tconst items: ContentItem[] = [];\n\t\ttry {\n\t\t\tfor (const chunk of chunks(translationGroups, SQL_BATCH_SIZE)) {\n\t\t\t\tconst result = await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE translation_group IN (${sql.join(chunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\t${publishedFilter}\n\t\t\t\t\tORDER BY translation_group ASC, locale ASC\n\t\t\t\t`.execute(this.db);\n\t\t\t\tfor (const row of result.rows) items.push(this.mapRow(type, row));\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return [];\n\t\t\tthrow error;\n\t\t}\n\t\treturn items;\n\t}\n\n\t/**\n\t * Batch variant of {@link findByIdOrSlug}: resolve many identifiers (each an\n\t * id OR a slug) within `type` in a constant number of queries — one `WHERE id\n\t * IN (...)` and one `WHERE slug IN (...)`, each chunked at `SQL_BATCH_SIZE`.\n\t * Returns a map from the input identifier to its resolved item; identifiers\n\t * that match nothing are absent. Used on write paths that accept a list of\n\t * references, so a single request doesn't fan out to an N+1 of point lookups.\n\t *\n\t * Resolution mirrors {@link findByIdOrSlug}: a ULID-shaped identifier prefers\n\t * the id match and falls back to slug; anything else prefers the slug match\n\t * and falls back to id. Slug matches collapse to the lowest-locale variant\n\t * (`ORDER BY locale ASC`), matching the slug-without-locale lookup.\n\t */\n\tasync findManyByIdOrSlug(type: string, identifiers: string[]): Promise<Map<string, ContentItem>> {\n\t\tconst resolved = new Map<string, ContentItem>();\n\t\tconst unique = [...new Set(identifiers)];\n\t\tif (unique.length === 0) return resolved;\n\n\t\tconst tableName = getTableName(type);\n\t\tconst byId = new Map<string, ContentItem>();\n\t\tconst bySlug = new Map<string, ContentItem>();\n\n\t\ttry {\n\t\t\tfor (const chunk of chunks(unique, SQL_BATCH_SIZE)) {\n\t\t\t\tconst idRows = await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE id IN (${sql.join(chunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t`.execute(this.db);\n\t\t\t\tfor (const row of idRows.rows) {\n\t\t\t\t\tconst item = this.mapRow(type, row);\n\t\t\t\t\tbyId.set(item.id, item);\n\t\t\t\t}\n\n\t\t\t\tconst slugRows = await sql<Record<string, unknown>>`\n\t\t\t\t\tSELECT * FROM ${sql.ref(tableName)}\n\t\t\t\t\tWHERE slug IN (${sql.join(chunk)})\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\tORDER BY locale ASC\n\t\t\t\t`.execute(this.db);\n\t\t\t\tfor (const row of slugRows.rows) {\n\t\t\t\t\tconst item = this.mapRow(type, row);\n\t\t\t\t\t// First write wins → lowest locale, matching findBySlug without a locale.\n\t\t\t\t\tif (item.slug != null && !bySlug.has(item.slug)) bySlug.set(item.slug, item);\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (error) {\n\t\t\t// A collection dropped after a relation was created leaves the relation\n\t\t\t// pointing at a missing table. Treat it like an empty collection (no\n\t\t\t// matches) so callers surface a structured NOT_FOUND, not a 500 —\n\t\t\t// mirroring findTranslationsForGroups.\n\t\t\tif (isMissingTableError(error)) return resolved;\n\t\t\tthrow error;\n\t\t}\n\n\t\tfor (const identifier of unique) {\n\t\t\tconst looksLikeUlid = ULID_PATTERN.test(identifier);\n\t\t\tconst item = looksLikeUlid\n\t\t\t\t? (byId.get(identifier) ?? bySlug.get(identifier))\n\t\t\t\t: (bySlug.get(identifier) ?? byId.get(identifier));\n\t\t\tif (item) resolved.set(identifier, item);\n\t\t}\n\t\treturn resolved;\n\t}\n\n\t/**\n\t * Publish the current draft\n\t *\n\t * Promotes draft_revision_id to live_revision_id and clears draft pointer.\n\t * Syncs the draft revision's data into the content table columns so the\n\t * content table always reflects the published version.\n\t * If no draft revision exists, creates one from current data and publishes it.\n\t * When `promoteRevision` is false, publishes the current content-table data\n\t * by changing lifecycle metadata only.\n\t *\n\t * `publishedAt` (optional) overrides the publication timestamp. If omitted,\n\t * the existing `published_at` is preserved (idempotent re-publish keeps the\n\t * original date) and falls back to the current time on first publish. Pass\n\t * an explicit value to backdate a publish (e.g. when migrating content from\n\t * another CMS).\n\t *\n\t * `requireDue` gates the final update on the row still being due.\n\t * `expectedScheduledAt` additionally fences changes made after a sweep\n\t * selected the row but before publication preparation began.\n\t */\n\tasync publish(\n\t\ttype: string,\n\t\tid: string,\n\t\tpublishedAt?: string,\n\t\trequireDue = false,\n\t\texpectedScheduledAt?: string,\n\t\tpromoteRevision = true,\n\t\trequireSlug = true,\n\t\texpectedRevision?: ContentRevisionPrecondition,\n\t): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\t\tassertRevisionPrecondition(existing, expectedRevision);\n\t\tif (\n\t\t\trequireDue &&\n\t\t\texpectedScheduledAt !== undefined &&\n\t\t\texisting.scheduledAt !== expectedScheduledAt\n\t\t) {\n\t\t\tthrow new ScheduledNotDueError();\n\t\t}\n\t\tif (!promoteRevision && requireSlug && !existing.slug?.trim()) {\n\t\t\tthrow new EmDashValidationError(\"Cannot publish routable content without a slug\");\n\t\t}\n\n\t\tif (!promoteRevision) {\n\t\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\t\tlet provisionalRevisionId: string | null = null;\n\t\t\ttry {\n\t\t\t\tlet liveRevisionId = existing.liveRevisionId;\n\t\t\t\tif (!liveRevisionId) {\n\t\t\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\t\t\tcollection: type,\n\t\t\t\t\t\tentryId: id,\n\t\t\t\t\t\tdata: existing.data,\n\t\t\t\t\t});\n\t\t\t\t\tliveRevisionId = revision.id;\n\t\t\t\t\tprovisionalRevisionId = revision.id;\n\t\t\t\t}\n\n\t\t\t\tconst intendedPublishedAt = publishedAt ?? existing.publishedAt ?? now;\n\t\t\t\tconst duePredicate = requireDue\n\t\t\t\t\t? sql`AND scheduled_at IS NOT NULL AND scheduled_at <= ${now}`\n\t\t\t\t\t: sql``;\n\t\t\t\tlet published = false;\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await sql`\n\t\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\t\tSET live_revision_id = ${liveRevisionId},\n\t\t\t\t\t\t\tdraft_revision_id = NULL,\n\t\t\t\t\t\t\tstatus = 'published',\n\t\t\t\t\t\t\tscheduled_at = NULL,\n\t\t\t\t\t\t\tpublished_at = ${intendedPublishedAt},\n\t\t\t\t\t\t\tupdated_at = ${now},\n\t\t\t\t\t\t\tversion = version + 1\n\t\t\t\t\t\tWHERE id = ${id}\n\t\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\t\tAND version = ${existing.version}\n\t\t\t\t\t\tAND updated_at = ${existing.updatedAt}\n\t\t\t\t\t\tAND status = ${existing.status}\n\t\t\t\t\t\tAND ${nullableColumnMatch(\"live_revision_id\", existing.liveRevisionId)}\n\t\t\t\t\t\tAND ${nullableColumnMatch(\"draft_revision_id\", existing.draftRevisionId)}\n\t\t\t\t\t\tAND ${nullableColumnMatch(\"scheduled_at\", existing.scheduledAt)}\n\t\t\t\t\t\t${duePredicate}\n\t\t\t\t\t`.execute(this.db);\n\t\t\t\t\tpublished = (result.numAffectedRows ?? 0n) > 0n;\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (isConfirmedStatementFailure(error)) throw error;\n\t\t\t\t\tlet observed: ContentItem | null;\n\t\t\t\t\ttry {\n\t\t\t\t\t\tobserved = await this.findById(type, id);\n\t\t\t\t\t} catch (reconciliationError) {\n\t\t\t\t\t\tthrow new Error(\"Unable to confirm whether content publication completed\", {\n\t\t\t\t\t\t\tcause: reconciliationError,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tif (!observed) {\n\t\t\t\t\t\tthrow new Error(\"Unable to confirm whether content publication completed\", {\n\t\t\t\t\t\t\tcause: error,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t\tpublished = matchesLifecyclePublication(\n\t\t\t\t\t\tobserved,\n\t\t\t\t\t\texisting,\n\t\t\t\t\t\tliveRevisionId,\n\t\t\t\t\t\tintendedPublishedAt,\n\t\t\t\t\t\tnow,\n\t\t\t\t\t);\n\t\t\t\t\tif (!published && matchesPublicationFence(observed, existing)) throw error;\n\t\t\t\t\tif (!published) {\n\t\t\t\t\t\tthrow new Error(\"Unable to confirm whether content publication completed\", {\n\t\t\t\t\t\t\tcause: error,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif (!published) {\n\t\t\t\t\tthrow requireDue ? new ScheduledNotDueError() : new ContentMutationConflictError();\n\t\t\t\t}\n\n\t\t\t\tinvalidateCollectionCache(type);\n\t\t\t\tconst updated = await this.findById(type, id);\n\t\t\t\tif (!updated) throw new Error(\"Content not found\");\n\t\t\t\treturn updated;\n\t\t\t} catch (error) {\n\t\t\t\tif (provisionalRevisionId) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait revisionRepo.deleteIfUnreferenced(type, id, provisionalRevisionId);\n\t\t\t\t\t} catch (cleanupError) {\n\t\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t\t`[content] Failed to clean up provisional revision ${provisionalRevisionId}:`,\n\t\t\t\t\t\t\tcleanupError,\n\t\t\t\t\t\t);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tlet provisionalRevisionId: string | null = null;\n\t\ttry {\n\t\t\tlet revisionToPublish = existing.draftRevisionId || existing.liveRevisionId;\n\n\t\t\tif (!revisionToPublish) {\n\t\t\t\tconst revision = await revisionRepo.create({\n\t\t\t\t\tcollection: type,\n\t\t\t\t\tentryId: id,\n\t\t\t\t\tdata: existing.data,\n\t\t\t\t});\n\t\t\t\trevisionToPublish = revision.id;\n\t\t\t\tprovisionalRevisionId = revision.id;\n\t\t\t}\n\n\t\t\tconst revision = await revisionRepo.findById(revisionToPublish);\n\t\t\tif (!revision || revision.collection !== type || revision.entryId !== id) {\n\t\t\t\tthrow new EmDashValidationError(\"Revision does not belong to the specified content item\");\n\t\t\t}\n\n\t\t\tconst stagedSlug = typeof revision.data._slug === \"string\" ? revision.data._slug : null;\n\t\t\tconst intendedSlug = stagedSlug ?? existing.slug;\n\t\t\tif (requireSlug && !intendedSlug?.trim()) {\n\t\t\t\tthrow new EmDashValidationError(\"Cannot publish routable content without a slug\");\n\t\t\t}\n\t\t\tconst intendedPublishedAt = publishedAt ?? existing.publishedAt ?? now;\n\t\t\tif (stagedSlug !== null && stagedSlug !== existing.slug && existing.locale !== null) {\n\t\t\t\tconst conflict = await this.findBySlugIncludingTrashed(type, stagedSlug, existing.locale);\n\t\t\t\tif (conflict && conflict.id !== id) {\n\t\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t\t`Cannot publish: slug '${stagedSlug}' is already used by another entry` +\n\t\t\t\t\t\t\t` in this collection (id: ${conflict.id}). Choose a different slug.`,\n\t\t\t\t\t\t{ code: \"SLUG_CONFLICT\" },\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst assignments: ReturnType<typeof sql>[] = [];\n\t\t\tif (stagedSlug !== null) assignments.push(sql`slug = ${stagedSlug}`);\n\t\t\tfor (const [key, value] of Object.entries(revision.data)) {\n\t\t\t\tif (SYSTEM_COLUMNS.has(key) || key.startsWith(\"_\")) continue;\n\t\t\t\tvalidateIdentifier(key, \"content field name\");\n\t\t\t\tassignments.push(sql`${sql.ref(key)} = ${serializeValue(value)}`);\n\t\t\t}\n\t\t\tassignments.push(\n\t\t\t\tsql`live_revision_id = ${revisionToPublish}`,\n\t\t\t\tsql`draft_revision_id = NULL`,\n\t\t\t\tsql`status = 'published'`,\n\t\t\t\tsql`scheduled_at = NULL`,\n\t\t\t\tsql`published_at = ${intendedPublishedAt}`,\n\t\t\t\tsql`updated_at = ${now}`,\n\t\t\t\tsql`version = version + 1`,\n\t\t\t);\n\n\t\t\tconst duePredicate = requireDue\n\t\t\t\t? sql`AND scheduled_at IS NOT NULL AND scheduled_at <= ${now}`\n\t\t\t\t: sql``;\n\t\t\tlet promoted = false;\n\t\t\ttry {\n\t\t\t\tconst result = await sql`\n\t\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\t\tSET ${sql.join(assignments, sql`, `)}\n\t\t\t\t\tWHERE id = ${id}\n\t\t\t\t\tAND deleted_at IS NULL\n\t\t\t\t\tAND version = ${existing.version}\n\t\t\t\t\tAND updated_at = ${existing.updatedAt}\n\t\t\t\t\tAND status = ${existing.status}\n\t\t\t\t\tAND ${nullableColumnMatch(\"live_revision_id\", existing.liveRevisionId)}\n\t\t\t\t\tAND ${nullableColumnMatch(\"draft_revision_id\", existing.draftRevisionId)}\n\t\t\t\t\tAND ${nullableColumnMatch(\"scheduled_at\", existing.scheduledAt)}\n\t\t\t\t\t${duePredicate}\n\t\t\t\t\tAND EXISTS (\n\t\t\t\t\t\tSELECT 1 FROM revisions\n\t\t\t\t\t\tWHERE revisions.id = ${revisionToPublish}\n\t\t\t\t\t\tAND revisions.collection = ${type}\n\t\t\t\t\t\tAND revisions.entry_id = ${id}\n\t\t\t\t\t)\n\t\t\t\t`.execute(this.db);\n\t\t\t\tpromoted = (result.numAffectedRows ?? 0n) > 0n;\n\t\t\t} catch (error) {\n\t\t\t\tif (isConfirmedStatementFailure(error)) throw error;\n\t\t\t\tlet observed: ContentItem | null;\n\t\t\t\ttry {\n\t\t\t\t\tobserved = await this.findById(type, id);\n\t\t\t\t} catch (reconciliationError) {\n\t\t\t\t\tthrow new Error(\"Unable to confirm whether content publication completed\", {\n\t\t\t\t\t\tcause: reconciliationError,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tif (!observed) {\n\t\t\t\t\tthrow new Error(\"Unable to confirm whether content publication completed\", {\n\t\t\t\t\t\tcause: error,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tpromoted = matchesPublication(\n\t\t\t\t\tobserved,\n\t\t\t\t\texisting,\n\t\t\t\t\trevision,\n\t\t\t\t\trevisionToPublish,\n\t\t\t\t\tintendedSlug,\n\t\t\t\t\tintendedPublishedAt,\n\t\t\t\t\tnow,\n\t\t\t\t);\n\t\t\t\tif (!promoted && matchesPublicationFence(observed, existing)) throw error;\n\t\t\t\tif (!promoted) {\n\t\t\t\t\tthrow new Error(\"Unable to confirm whether content publication completed\", {\n\t\t\t\t\t\tcause: error,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!promoted) {\n\t\t\t\tthrow requireDue ? new ScheduledNotDueError() : new ContentMutationConflictError();\n\t\t\t}\n\n\t\t\tinvalidateCollectionCache(type);\n\t\t\tconst updated = await this.findById(type, id);\n\t\t\tif (!updated) {\n\t\t\t\tthrow new Error(\"Content not found\");\n\t\t\t}\n\n\t\t\treturn updated;\n\t\t} catch (error) {\n\t\t\tif (provisionalRevisionId) {\n\t\t\t\ttry {\n\t\t\t\t\tawait revisionRepo.deleteIfUnreferenced(type, id, provisionalRevisionId);\n\t\t\t\t} catch (cleanupError) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[content] Failed to clean up provisional revision ${provisionalRevisionId}:`,\n\t\t\t\t\t\tcleanupError,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * Unpublish content\n\t *\n\t * Removes live pointer but preserves the draft and publication date. If no\n\t * draft exists, creates one from the live version so the content isn't lost.\n\t */\n\tasync unpublish(\n\t\ttype: string,\n\t\tid: string,\n\t\texpectedRevision?: ContentRevisionPrecondition,\n\t): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\t\tconst now = new Date().toISOString();\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\t\tassertRevisionPrecondition(existing, expectedRevision);\n\t\tif (existing.status === \"draft\" && !existing.liveRevisionId) return existing;\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tlet provisionalRevisionId: string | null = null;\n\t\ttry {\n\t\t\tlet draftRevisionId = existing.draftRevisionId;\n\t\t\tif (!draftRevisionId && existing.liveRevisionId) {\n\t\t\t\tconst liveRevision = await revisionRepo.findById(existing.liveRevisionId);\n\t\t\t\tif (liveRevision) {\n\t\t\t\t\tconst draft = await revisionRepo.create({\n\t\t\t\t\t\tcollection: type,\n\t\t\t\t\t\tentryId: id,\n\t\t\t\t\t\tdata: liveRevision.data,\n\t\t\t\t\t});\n\t\t\t\t\tdraftRevisionId = draft.id;\n\t\t\t\t\tprovisionalRevisionId = draft.id;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await sql`\n\t\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\t\tSET live_revision_id = NULL,\n\t\t\t\t\tdraft_revision_id = ${draftRevisionId},\n\t\t\t\t\tstatus = 'draft',\n\t\t\t\t\tupdated_at = ${now},\n\t\t\t\t\tversion = version + 1\n\t\t\t\tWHERE id = ${id}\n\t\t\t\tAND deleted_at IS NULL\n\t\t\t\tAND version = ${existing.version}\n\t\t\t\tAND updated_at = ${existing.updatedAt}\n\t\t\t\tAND status = ${existing.status}\n\t\t\t\tAND ${nullableColumnMatch(\"live_revision_id\", existing.liveRevisionId)}\n\t\t\t\tAND ${nullableColumnMatch(\"draft_revision_id\", existing.draftRevisionId)}\n\t\t\t\tAND ${nullableColumnMatch(\"scheduled_at\", existing.scheduledAt)}\n\t\t\t`.execute(this.db);\n\t\t\tif ((result.numAffectedRows ?? 0n) === 0n) throw new ContentMutationConflictError();\n\n\t\t\tinvalidateCollectionCache(type);\n\t\t\tconst updated = await this.findById(type, id);\n\t\t\tif (!updated) throw new Error(\"Content not found\");\n\t\t\treturn updated;\n\t\t} catch (error) {\n\t\t\tif (provisionalRevisionId) {\n\t\t\t\ttry {\n\t\t\t\t\tawait revisionRepo.deleteIfUnreferenced(type, id, provisionalRevisionId);\n\t\t\t\t} catch (cleanupError) {\n\t\t\t\t\tconsole.error(\n\t\t\t\t\t\t`[content] Failed to clean up provisional revision ${provisionalRevisionId}:`,\n\t\t\t\t\t\tcleanupError,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\t/**\n\t * Set the draft revision pointer for a content item.\n\t *\n\t * Used by seed/import paths that stage a new revision's data before\n\t * promoting it to live via `publish()`.\n\t *\n\t * Validates that the content item exists and is not soft-deleted, that\n\t * the revision exists, and that the revision belongs to the same\n\t * collection and entry. Without these checks, a caller could leave the\n\t * content row pointing at a missing or unrelated revision.\n\t */\n\tasync setDraftRevision(type: string, id: string, revisionId: string): Promise<void> {\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\n\t\tconst revisionRepo = new RevisionRepository(this.db);\n\t\tconst revision = await revisionRepo.findById(revisionId);\n\t\tif (!revision) {\n\t\t\tthrow new EmDashValidationError(\"Revision not found\");\n\t\t}\n\n\t\tif (revision.collection !== type || revision.entryId !== id) {\n\t\t\tthrow new EmDashValidationError(\"Revision does not belong to the specified content item\");\n\t\t}\n\n\t\tif (!(await this.replaceDraftRevision(type, id, revisionId, existing))) {\n\t\t\tthrow new ContentMutationConflictError();\n\t\t}\n\t}\n\n\tasync replaceDraftRevision(\n\t\ttype: string,\n\t\tid: string,\n\t\trevisionId: string,\n\t\texpected: Pick<ContentItem, \"version\" | \"liveRevisionId\" | \"draftRevisionId\">,\n\t): Promise<boolean> {\n\t\tconst tableName = getTableName(type);\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET draft_revision_id = ${revisionId},\n\t\t\t\tversion = version + 1\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t\tAND version = ${expected.version}\n\t\t\tAND ${nullableColumnMatch(\"live_revision_id\", expected.liveRevisionId)}\n\t\t\tAND ${nullableColumnMatch(\"draft_revision_id\", expected.draftRevisionId)}\n\t\t\tAND EXISTS (\n\t\t\t\tSELECT 1 FROM revisions\n\t\t\t\tWHERE revisions.id = ${revisionId}\n\t\t\t\tAND revisions.collection = ${type}\n\t\t\t\tAND revisions.entry_id = ${id}\n\t\t\t)\n\t\t`.execute(this.db);\n\t\treturn (result.numAffectedRows ?? 0n) > 0n;\n\t}\n\n\t/**\n\t * Discard pending draft changes\n\t *\n\t * Clears draft_revision_id. The content table columns already hold the\n\t * published version, so no data sync is needed.\n\t */\n\tasync discardDraft(\n\t\ttype: string,\n\t\tid: string,\n\t\texpectedRevision?: ContentRevisionPrecondition,\n\t): Promise<ContentItem> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst existing = await this.findById(type, id);\n\t\tif (!existing) {\n\t\t\tthrow new EmDashValidationError(\"Content item not found\");\n\t\t}\n\t\tassertRevisionPrecondition(existing, expectedRevision);\n\n\t\tif (!existing.draftRevisionId) {\n\t\t\t// No draft to discard\n\t\t\treturn existing;\n\t\t}\n\n\t\t// Discarding a draft restores the state from before the draft was\n\t\t// staged — nothing about the live entry changed in between, so\n\t\t// updated_at stays at its pre-draft value (#2143).\n\t\tconst result = await sql`\n\t\t\tUPDATE ${sql.ref(tableName)}\n\t\t\tSET draft_revision_id = NULL,\n\t\t\t\tversion = version + 1\n\t\t\tWHERE id = ${id}\n\t\t\tAND deleted_at IS NULL\n\t\t\tAND version = ${existing.version}\n\t\t\tAND updated_at = ${existing.updatedAt}\n\t\t\tAND status = ${existing.status}\n\t\t\tAND ${nullableColumnMatch(\"live_revision_id\", existing.liveRevisionId)}\n\t\t\tAND ${nullableColumnMatch(\"draft_revision_id\", existing.draftRevisionId)}\n\t\t\tAND ${nullableColumnMatch(\"scheduled_at\", existing.scheduledAt)}\n\t\t`.execute(this.db);\n\t\tif ((result.numAffectedRows ?? 0n) === 0n) throw new ContentMutationConflictError();\n\n\t\tconst updated = await this.findById(type, id);\n\t\tif (!updated) {\n\t\t\tthrow new Error(\"Content not found\");\n\t\t}\n\n\t\treturn updated;\n\t}\n\n\t/**\n\t * Count content items with a pending schedule.\n\t * Includes both draft-scheduled (status='scheduled') and published\n\t * posts with scheduled draft changes (status='published', scheduled_at set).\n\t */\n\tasync countScheduled(type: string): Promise<number> {\n\t\tconst tableName = getTableName(type);\n\n\t\tconst result = await sql<{ count: number }>`\n\t\t\tSELECT COUNT(id) as count FROM ${sql.ref(tableName)}\n\t\t\tWHERE scheduled_at IS NOT NULL\n\t\t\tAND deleted_at IS NULL\n\t\t`.execute(this.db);\n\n\t\treturn Number(result.rows[0]?.count || 0);\n\t}\n\n\t/**\n\t * Map database row to ContentItem\n\t * Extracts system columns and puts content fields in data\n\t * Excludes null values from data to match input semantics\n\t */\n\tprivate mapRow(type: string, row: Record<string, unknown>): ContentItem {\n\t\tconst data: Record<string, unknown> = {};\n\n\t\tfor (const [key, value] of Object.entries(row)) {\n\t\t\tif (!SYSTEM_COLUMNS.has(key) && value !== null) {\n\t\t\t\tdata[key] = deserializeValue(value);\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tid: row.id as string,\n\t\t\ttype,\n\t\t\tslug: row.slug as string | null,\n\t\t\tstatus: row.status as string,\n\t\t\tdata,\n\t\t\tauthorId: row.author_id as string | null,\n\t\t\tprimaryBylineId: (row.primary_byline_id as string | null) ?? null,\n\t\t\tcreatedAt: row.created_at as string,\n\t\t\tupdatedAt: row.updated_at as string,\n\t\t\tpublishedAt: row.published_at as string | null,\n\t\t\tscheduledAt: row.scheduled_at as string | null,\n\t\t\tliveRevisionId: (row.live_revision_id as string | null) ?? null,\n\t\t\tdraftRevisionId: (row.draft_revision_id as string | null) ?? null,\n\t\t\tversion: typeof row.version === \"number\" ? row.version : 1,\n\t\t\tlocale: (row.locale as string) ?? null,\n\t\t\ttranslationGroup: (row.translation_group as string) ?? null,\n\t\t};\n\t}\n\n\tprivate normalizeFilterScalar(\n\t\tfield: string,\n\t\ttype: FieldType,\n\t\tvalue: unknown,\n\t): NormalizedFilterScalar {\n\t\tif (type === \"number\" || type === \"integer\") {\n\t\t\tif (typeof value !== \"number\" || !Number.isFinite(value)) {\n\t\t\t\tthrow new EmDashValidationError(`Filter for field \"${field}\" must use a finite number`);\n\t\t\t}\n\t\t\tif (type === \"integer\" && !Number.isInteger(value)) {\n\t\t\t\tthrow new EmDashValidationError(`Filter for field \"${field}\" must use an integer`);\n\t\t\t}\n\t\t\treturn value;\n\t\t}\n\n\t\tif (type === \"boolean\") {\n\t\t\tif (typeof value !== \"boolean\") {\n\t\t\t\tthrow new EmDashValidationError(`Filter for field \"${field}\" must use a boolean`);\n\t\t\t}\n\t\t\treturn value ? 1 : 0;\n\t\t}\n\n\t\tif (typeof value !== \"string\") {\n\t\t\tthrow new EmDashValidationError(`Filter for field \"${field}\" must use a string`);\n\t\t}\n\t\tif (value.length > MAX_FILTER_STRING_LENGTH) {\n\t\t\tthrow new EmDashValidationError(\n\t\t\t\t`Filter value for field \"${field}\" exceeds ${MAX_FILTER_STRING_LENGTH} characters`,\n\t\t\t);\n\t\t}\n\t\treturn value;\n\t}\n\n\tprivate normalizeFieldFilter(\n\t\tfield: string,\n\t\ttype: FieldType,\n\t\tvalue: ContentFieldFilterValue,\n\t): ResolvedFieldFilter {\n\t\tif (value === null) return { column: field, kind: \"null\" };\n\t\tif (typeof value !== \"object\") {\n\t\t\treturn {\n\t\t\t\tcolumn: field,\n\t\t\t\tkind: \"exact\",\n\t\t\t\tvalue: this.normalizeFilterScalar(field, type, value),\n\t\t\t};\n\t\t}\n\t\tif (Array.isArray(value)) {\n\t\t\tthrow new EmDashValidationError(`Invalid filter for field \"${field}\"`);\n\t\t}\n\n\t\tconst record = value as Record<string, unknown>;\n\t\tconst keys = Object.keys(record);\n\t\tif (keys.length === 1 && keys[0] === \"in\") {\n\t\t\tif (!Array.isArray(record.in) || record.in.length === 0) {\n\t\t\t\tthrow new EmDashValidationError(`IN filter for field \"${field}\" must not be empty`);\n\t\t\t}\n\t\t\tif (record.in.length > MAX_IN_FILTER_VALUES) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`IN filter for field \"${field}\" exceeds ${MAX_IN_FILTER_VALUES} values`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn {\n\t\t\t\tcolumn: field,\n\t\t\t\tkind: \"in\",\n\t\t\t\tvalues: record.in.map((entry) => this.normalizeFilterScalar(field, type, entry)),\n\t\t\t};\n\t\t}\n\n\t\tconst rangeKeys = new Set([\"gt\", \"gte\", \"lt\", \"lte\"]);\n\t\tif (keys.length === 0 || keys.some((key) => !rangeKeys.has(key))) {\n\t\t\tthrow new EmDashValidationError(`Invalid filter operator for field \"${field}\"`);\n\t\t}\n\t\tif (type === \"boolean\") {\n\t\t\tthrow new EmDashValidationError(`Boolean field \"${field}\" does not support range filters`);\n\t\t}\n\n\t\tconst bounds: Partial<Record<\"gt\" | \"gte\" | \"lt\" | \"lte\", NormalizedFilterScalar>> = {};\n\t\tfor (const key of keys as Array<\"gt\" | \"gte\" | \"lt\" | \"lte\">) {\n\t\t\tif (record[key] === undefined) continue;\n\t\t\tbounds[key] = this.normalizeFilterScalar(field, type, record[key]);\n\t\t}\n\t\tif (Object.keys(bounds).length === 0) {\n\t\t\tthrow new EmDashValidationError(`Range filter for field \"${field}\" has no bounds`);\n\t\t}\n\t\treturn { column: field, kind: \"range\", bounds };\n\t}\n\n\tprivate async collectionExists(type: string): Promise<boolean> {\n\t\tconst collection = await this.db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.where(\"slug\", \"=\", type)\n\t\t\t.select(\"id\")\n\t\t\t.executeTakeFirst();\n\t\treturn collection !== undefined;\n\t}\n\n\tprivate async resolveFieldFilters(\n\t\ttype: string,\n\t\tfilters: ContentFieldFilters | undefined,\n\t): Promise<ResolvedFieldFilter[]> {\n\t\tconst resolvedFilters = filters ?? {};\n\t\tconst fields = Object.keys(resolvedFilters);\n\t\tif (fields.length === 0) return [];\n\t\tif (fields.length > MAX_INDEXED_FIELD_FILTERS) {\n\t\t\tif (!(await this.collectionExists(type))) return [];\n\t\t\tthrow new EmDashValidationError(\n\t\t\t\t`Content list queries support at most ${MAX_INDEXED_FIELD_FILTERS} indexed field filters`,\n\t\t\t);\n\t\t}\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_emdash_fields as field\")\n\t\t\t.innerJoin(\"_emdash_collections as collection\", \"collection.id\", \"field.collection_id\")\n\t\t\t.where(\"collection.slug\", \"=\", type)\n\t\t\t.where(\"field.slug\", \"in\", fields)\n\t\t\t.where(\"field.indexed\", \"=\", 1)\n\t\t\t.select([\"field.slug\", \"field.type\"])\n\t\t\t.execute();\n\t\tconst metadata = new Map(rows.map((row) => [row.slug, row.type as FieldType]));\n\n\t\tif (metadata.size === 0 && !(await this.collectionExists(type))) return [];\n\n\t\tfor (const field of fields) {\n\t\t\ttry {\n\t\t\t\tvalidateIdentifier(field, \"content filter field\");\n\t\t\t} catch {\n\t\t\t\tthrow new EmDashValidationError(`Invalid content filter field: ${field}`);\n\t\t\t}\n\t\t}\n\n\t\tconst normalized = fields.map((field) => {\n\t\t\tconst fieldType = metadata.get(field);\n\t\t\tif (!fieldType || !isIndexableFieldType(fieldType)) {\n\t\t\t\tthrow new EmDashValidationError(\n\t\t\t\t\t`Cannot filter by field \"${field}\". Custom fields must be indexed before filtering.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn this.normalizeFieldFilter(field, fieldType, resolvedFilters[field]);\n\t\t});\n\t\tconst operandCount = normalized.reduce((total, filter) => {\n\t\t\tif (filter.kind === \"null\") return total;\n\t\t\tif (filter.kind === \"exact\") return total + 1;\n\t\t\tif (filter.kind === \"in\") return total + filter.values.length;\n\t\t\treturn total + Object.keys(filter.bounds).length;\n\t\t}, 0);\n\t\tif (operandCount > SQL_BATCH_SIZE) {\n\t\t\tthrow new EmDashValidationError(\n\t\t\t\t`Indexed field filters have a total operand budget of ${SQL_BATCH_SIZE}`,\n\t\t\t);\n\t\t}\n\t\treturn normalized;\n\t}\n\n\tprivate applyFieldFilters<QB extends { where: (cb: (eb: any) => unknown) => QB }>(\n\t\tquery: QB,\n\t\tfilters: ResolvedFieldFilter[],\n\t): QB {\n\t\tlet next = query;\n\t\tfor (const filter of filters) {\n\t\t\tconst column = sql.ref(filter.column);\n\t\t\tconst isPresent = sql<boolean>`${column} IS NOT NULL`;\n\t\t\tif (filter.kind === \"null\") {\n\t\t\t\tnext = next.where(() => sql<boolean>`(${isPresent}) = FALSE AND ${column} IS NULL`);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (filter.kind === \"exact\") {\n\t\t\t\tnext = next.where(\n\t\t\t\t\t() => sql<boolean>`(${isPresent}) = TRUE AND ${column} = ${filter.value}`,\n\t\t\t\t);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (filter.kind === \"in\") {\n\t\t\t\tconst values = sql.join(\n\t\t\t\t\tfilter.values.map((value) => sql`${value}`),\n\t\t\t\t\tsql`, `,\n\t\t\t\t);\n\t\t\t\tnext = next.where(() => sql<boolean>`(${isPresent}) = TRUE AND ${column} IN (${values})`);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tnext = next.where(() => sql<boolean>`(${isPresent}) = TRUE`);\n\t\t\tif (filter.bounds.gt !== undefined) {\n\t\t\t\tnext = next.where(() => sql<boolean>`${column} > ${filter.bounds.gt}`);\n\t\t\t}\n\t\t\tif (filter.bounds.gte !== undefined) {\n\t\t\t\tnext = next.where(() => sql<boolean>`${column} >= ${filter.bounds.gte}`);\n\t\t\t}\n\t\t\tif (filter.bounds.lt !== undefined) {\n\t\t\t\tnext = next.where(() => sql<boolean>`${column} < ${filter.bounds.lt}`);\n\t\t\t}\n\t\t\tif (filter.bounds.lte !== undefined) {\n\t\t\t\tnext = next.where(() => sql<boolean>`${column} <= ${filter.bounds.lte}`);\n\t\t\t}\n\t\t}\n\t\treturn next;\n\t}\n\n\t/**\n\t * Map order field names to database columns.\n\t * Only allows known fields to prevent column enumeration via crafted orderBy values.\n\t */\n\tprivate mapOrderField(field: string, sortableExtras: readonly string[] = []): string {\n\t\tconst mapped = ORDER_FIELD_COLUMNS[field];\n\t\tif (mapped) return mapped;\n\n\t\t// A collection's configured titleField/dateField are allowed as\n\t\t// sort columns. The caller passes the collection's *actual* values (resolved\n\t\t// server-side, never client-supplied), so this stays a closed set per\n\t\t// request and doesn't reopen the column-enumeration hole. The slug is a\n\t\t// validated identifier that maps directly to the column.\n\t\tif (sortableExtras.includes(field)) {\n\t\t\tvalidateIdentifier(field, \"order field\");\n\t\t\treturn field;\n\t\t}\n\n\t\tthrow new EmDashValidationError(`Invalid order field: ${field}`);\n\t}\n\n\tprivate async resolveOrderField(\n\t\ttype: string,\n\t\tfield: string,\n\t\tsortableExtras: readonly string[] = [],\n\t): Promise<ResolvedOrderField> {\n\t\ttry {\n\t\t\treturn { column: this.mapOrderField(field, sortableExtras), indexedCustomField: false };\n\t\t} catch (error) {\n\t\t\tif (!(error instanceof EmDashValidationError)) throw error;\n\t\t}\n\n\t\tconst customField = await this.db\n\t\t\t.selectFrom(\"_emdash_collections as collection\")\n\t\t\t.leftJoin(\"_emdash_fields as field\", (join) =>\n\t\t\t\tjoin\n\t\t\t\t\t.onRef(\"field.collection_id\", \"=\", \"collection.id\")\n\t\t\t\t\t.on(\"field.slug\", \"=\", field)\n\t\t\t\t\t.on(\"field.indexed\", \"=\", 1),\n\t\t\t)\n\t\t\t.where(\"collection.slug\", \"=\", type)\n\t\t\t.select([\"collection.id as collectionId\", \"field.slug as fieldSlug\"])\n\t\t\t.executeTakeFirst();\n\n\t\tif (!customField) {\n\t\t\tthrow new ContentCollectionNotFoundError(type);\n\t\t}\n\n\t\tif (!customField.fieldSlug) {\n\t\t\tthrow new EmDashValidationError(\n\t\t\t\t`Invalid order field: ${field}. Custom fields must be indexed before sorting.`,\n\t\t\t);\n\t\t}\n\n\t\tvalidateIdentifier(customField.fieldSlug, \"content order field\");\n\t\treturn { column: customField.fieldSlug, indexedCustomField: true };\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"context-CX6gxOLW.mjs","names":[],"sources":["../src/plugins/storage-query.ts","../src/database/repositories/plugin-storage.ts","../src/plugins/context.ts"],"sourcesContent":["/**\n * Plugin Storage Query Validation and Building\n *\n * Validates that queries only use indexed fields and builds SQL WHERE clauses.\n *\n * @see PLUGIN-SYSTEM.md § Plugin Storage > Query Validation\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { pluginDataExtractExpr, pluginDataOrderExpr } from \"../database/dialect-helpers.js\";\nimport type { WhereClause, WhereValue, RangeFilter, InFilter, StartsWithFilter } from \"./types.js\";\n\n/**\n * Error thrown when querying non-indexed fields\n */\nexport class StorageQueryError extends Error {\n\tconstructor(\n\t\tmessage: string,\n\t\tpublic field?: string,\n\t\tpublic suggestion?: string,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"StorageQueryError\";\n\t}\n}\n\n/**\n * Check if a value is a range filter\n */\nexport function isRangeFilter(value: WhereValue): value is RangeFilter {\n\tif (typeof value !== \"object\" || value === null) return false;\n\treturn \"gt\" in value || \"gte\" in value || \"lt\" in value || \"lte\" in value;\n}\n\n/**\n * Check if a value is an IN filter\n */\nexport function isInFilter(value: WhereValue): value is InFilter {\n\tif (typeof value !== \"object\" || value === null) return false;\n\treturn \"in\" in value && Array.isArray(value.in);\n}\n\n/**\n * Check if a value is a startsWith filter\n */\nexport function isStartsWithFilter(value: WhereValue): value is StartsWithFilter {\n\tif (typeof value !== \"object\" || value === null) return false;\n\treturn \"startsWith\" in value && typeof value.startsWith === \"string\";\n}\n\n/**\n * Escape LIKE pattern metacharacters so a startsWith prefix matches\n * literally. Without this, `%` and `_` in the prefix act as wildcards\n * (e.g. `{ startsWith: \"50%\" }` would match \"50x off\").\n */\nexport function escapeLikePattern(value: string): string {\n\treturn value.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll(\"%\", \"\\\\%\").replaceAll(\"_\", \"\\\\_\");\n}\n\n/**\n * Get the set of indexed fields from index declarations\n */\nexport function getIndexedFields(indexes: Array<string | string[]>): Set<string> {\n\tconst fields = new Set<string>();\n\tfor (const index of indexes) {\n\t\tif (Array.isArray(index)) {\n\t\t\tfor (const field of index) {\n\t\t\t\tfields.add(field);\n\t\t\t}\n\t\t} else {\n\t\t\tfields.add(index);\n\t\t}\n\t}\n\treturn fields;\n}\n\n/**\n * Validate that all fields in a where clause are indexed\n */\nexport function validateWhereClause(\n\twhere: WhereClause,\n\tindexedFields: Set<string>,\n\tpluginId: string,\n\tcollection: string,\n): void {\n\tfor (const field of Object.keys(where)) {\n\t\tif (!indexedFields.has(field)) {\n\t\t\tthrow new StorageQueryError(\n\t\t\t\t`Cannot query on non-indexed field '${field}'.`,\n\t\t\t\tfield,\n\t\t\t\t`Add '${field}' to storage.${collection}.indexes in plugin '${pluginId}' to enable this query.`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * Validate orderBy fields are indexed\n */\nexport function validateOrderByClause(\n\torderBy: Record<string, \"asc\" | \"desc\">,\n\tindexedFields: Set<string>,\n\tpluginId: string,\n\tcollection: string,\n): void {\n\tfor (const field of Object.keys(orderBy)) {\n\t\tif (!indexedFields.has(field)) {\n\t\t\tthrow new StorageQueryError(\n\t\t\t\t`Cannot order by non-indexed field '${field}'.`,\n\t\t\t\tfield,\n\t\t\t\t`Add '${field}' to storage.${collection}.indexes in plugin '${pluginId}' to enable ordering by this field.`,\n\t\t\t);\n\t\t}\n\t}\n}\n\n/**\n * SQL expression for extracting a queryable field from the `_plugin_storage.data`\n * column.\n *\n * Delegates to `pluginDataExtractExpr`, which validates the field name before\n * interpolation and applies the dialect-correct extraction: a `::jsonb` cast on\n * Postgres (the `data` column is `text`) plus an optional type-guarded\n * `::numeric` cast so numeric comparisons don't fall back to lexical text\n * ordering.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function jsonExtract(\n\tdb: Kysely<any>,\n\tfield: string,\n\toptions?: { numeric?: boolean },\n): string {\n\treturn pluginDataExtractExpr(db, field, options);\n}\n\n/**\n * SQL expression for ordering by a `_plugin_storage.data` field.\n *\n * Delegates to `pluginDataOrderExpr`, which orders over the jsonb-native value\n * on Postgres so numeric fields sort numerically (not lexically) while staying\n * total across heterogeneous data. SQLite keeps `json_extract` (already numeric).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function jsonOrderExtract(db: Kysely<any>, field: string): string {\n\treturn pluginDataOrderExpr(db, field);\n}\n\n/**\n * Build a WHERE clause condition for a single field\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildCondition(\n\tdb: Kysely<any>,\n\tfield: string,\n\tvalue: WhereValue,\n): { sql: string; params: unknown[] } {\n\t// Numeric-vs-text is decided per condition from the JS type of the bound\n\t// value. On Postgres a text extract compared to a bound number would sort\n\t// lexically (`'9' >= '10'` is TRUE); a type-guarded `::numeric` cast on the\n\t// extract fixes it. String/boolean operands keep text comparison. SQLite is\n\t// unaffected — `json_extract` already returns a typed value.\n\tconst extractFor = (numeric: boolean): string => jsonExtract(db, field, { numeric });\n\n\tif (value === null) {\n\t\treturn { sql: `${extractFor(false)} IS NULL`, params: [] };\n\t}\n\n\tif (typeof value === \"number\") {\n\t\treturn { sql: `${extractFor(true)} = ?`, params: [value] };\n\t}\n\n\tif (typeof value === \"string\") {\n\t\treturn { sql: `${extractFor(false)} = ?`, params: [value] };\n\t}\n\n\tif (typeof value === \"boolean\") {\n\t\t// JSON booleans are stored as true/false strings\n\t\treturn { sql: `${extractFor(false)} = ?`, params: [value] };\n\t}\n\n\tif (isInFilter(value)) {\n\t\tconst numeric = value.in.length > 0 && value.in.every((v) => typeof v === \"number\");\n\t\tconst placeholders = value.in.map(() => \"?\").join(\", \");\n\t\treturn {\n\t\t\tsql: `${extractFor(numeric)} IN (${placeholders})`,\n\t\t\tparams: value.in,\n\t\t};\n\t}\n\n\tif (isStartsWithFilter(value)) {\n\t\t// ESCAPE '\\' works on both SQLite and PostgreSQL. startsWith is a string\n\t\t// operation, so always compare as text.\n\t\treturn {\n\t\t\tsql: `${extractFor(false)} LIKE ? ESCAPE '\\\\'`,\n\t\t\tparams: [`${escapeLikePattern(value.startsWith)}%`],\n\t\t};\n\t}\n\n\tif (isRangeFilter(value)) {\n\t\tconst conditions: string[] = [];\n\t\tconst params: unknown[] = [];\n\n\t\t// Each bound is cast to numeric only when its own operand is a number, so\n\t\t// a mixed range (e.g. a string lower bound) stays correct per side.\n\t\tconst pushBound = (op: string, bound: string | number): void => {\n\t\t\tconditions.push(`${extractFor(typeof bound === \"number\")} ${op} ?`);\n\t\t\tparams.push(bound);\n\t\t};\n\n\t\tif (value.gt !== undefined) pushBound(\">\", value.gt);\n\t\tif (value.gte !== undefined) pushBound(\">=\", value.gte);\n\t\tif (value.lt !== undefined) pushBound(\"<\", value.lt);\n\t\tif (value.lte !== undefined) pushBound(\"<=\", value.lte);\n\n\t\treturn {\n\t\t\tsql: conditions.join(\" AND \"),\n\t\t\tparams,\n\t\t};\n\t}\n\n\tthrow new StorageQueryError(`Unknown filter type for field '${field}'`);\n}\n\n/**\n * Build a complete WHERE clause from a WhereClause object\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildWhereClause(\n\tdb: Kysely<any>,\n\twhere: WhereClause,\n): {\n\tsql: string;\n\tparams: unknown[];\n} {\n\tconst conditions: string[] = [];\n\tconst params: unknown[] = [];\n\n\tfor (const [field, value] of Object.entries(where)) {\n\t\tconst condition = buildCondition(db, field, value);\n\t\tconditions.push(condition.sql);\n\t\tparams.push(...condition.params);\n\t}\n\n\tif (conditions.length === 0) {\n\t\treturn { sql: \"\", params: [] };\n\t}\n\n\treturn {\n\t\tsql: conditions.join(\" AND \"),\n\t\tparams,\n\t};\n}\n\n/**\n * Build ORDER BY clause\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildOrderByClause(\n\tdb: Kysely<any>,\n\torderBy: Record<string, \"asc\" | \"desc\">,\n): string {\n\tconst clauses: string[] = [];\n\n\tfor (const [field, direction] of Object.entries(orderBy)) {\n\t\tclauses.push(`${jsonOrderExtract(db, field)} ${direction.toUpperCase()}`);\n\t}\n\n\tif (clauses.length === 0) {\n\t\treturn \"\";\n\t}\n\n\treturn `ORDER BY ${clauses.join(\", \")}`;\n}\n","/**\n * Plugin Storage Repository\n *\n * Provides a document store API for plugin data storage.\n * Uses a single _plugin_storage table with JSON documents and expression indexes.\n *\n * @see PLUGIN-SYSTEM.md § Plugin Storage > Full API Reference\n */\n\nimport type { Kysely, RawBuilder } from \"kysely\";\nimport { sql } from \"kysely\";\n\nimport {\n\tbuildWhereClause,\n\tvalidateWhereClause,\n\tvalidateOrderByClause,\n\tgetIndexedFields,\n\tjsonOrderExtract,\n\tStorageQueryError,\n} from \"../../plugins/storage-query.js\";\nimport type {\n\tStorageCollection,\n\tQueryOptions,\n\tPaginatedResult,\n\tWhereClause,\n} from \"../../plugins/types.js\";\nimport { withTransaction } from \"../transaction.js\";\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor } from \"./types.js\";\n\n/**\n * Interleave a `?`-placeholder SQL string with its params into a single\n * boolean raw expression. Used as a WHERE predicate directly — wrapping it\n * in `(...) = 1` breaks on Postgres, which has a strict boolean type (#920).\n */\nfunction rawWhereExpr(sqlText: string, params: unknown[]): RawBuilder<boolean> {\n\tconst parts: ReturnType<typeof sql>[] = [];\n\tlet paramIndex = 0;\n\tconst sqlParts = sqlText.split(\"?\");\n\tfor (let i = 0; i < sqlParts.length; i++) {\n\t\tif (i > 0) {\n\t\t\tparts.push(sql`${params[paramIndex++]}`);\n\t\t}\n\t\tif (sqlParts[i]) {\n\t\t\tparts.push(sql.raw(sqlParts[i]));\n\t\t}\n\t}\n\treturn sql<boolean>`(${sql.join(parts, sql.raw(\"\"))})`;\n}\n\n/**\n * Plugin Storage Repository\n *\n * Implements the StorageCollection interface for a specific plugin and collection.\n */\n/**\n * Rank a sort expression so NULLs get a deterministic position.\n *\n * A missing key in a schemaless document extracts as NULL, which sorts\n * differently per dialect (SQLite first, Postgres last) and makes every\n * comparison against it UNKNOWN. Sorting on this rank ahead of the value puts\n * NULLs in the same place on both dialects and keeps the seek operands\n * non-NULL.\n */\nfunction nullRank(expr: RawBuilder<unknown>): RawBuilder<number> {\n\treturn sql`(case when ${expr} is null then 1 else 0 end)`;\n}\n\nexport class PluginStorageRepository<T = unknown> implements StorageCollection<T> {\n\tprivate indexedFields: Set<string>;\n\n\tconstructor(\n\t\tprivate db: Kysely<Database>,\n\t\tprivate pluginId: string,\n\t\tprivate collection: string,\n\t\tindexes: Array<string | string[]>,\n\t) {\n\t\tthis.indexedFields = getIndexedFields(indexes);\n\t}\n\n\t/**\n\t * Get a document by ID\n\t */\n\tasync get(id: string): Promise<T | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select(\"data\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\tif (!row) return null;\n\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T\n\t\treturn JSON.parse(row.data) as T;\n\t}\n\n\t/**\n\t * Store a document\n\t */\n\tasync put(id: string, data: T): Promise<void> {\n\t\tconst now = new Date().toISOString();\n\t\tconst jsonData = JSON.stringify(data);\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_plugin_storage\")\n\t\t\t.values({\n\t\t\t\tplugin_id: this.pluginId,\n\t\t\t\tcollection: this.collection,\n\t\t\t\tid,\n\t\t\t\tdata: jsonData,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.onConflict((oc) =>\n\t\t\t\toc.columns([\"plugin_id\", \"collection\", \"id\"]).doUpdateSet({\n\t\t\t\t\tdata: jsonData,\n\t\t\t\t\tupdated_at: now,\n\t\t\t\t}),\n\t\t\t)\n\t\t\t.execute();\n\t}\n\n\t/**\n\t * Delete a document\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_plugin_storage\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Check if a document exists\n\t */\n\tasync exists(id: string): Promise<boolean> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select(\"id\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn !!row;\n\t}\n\n\t/**\n\t * Get multiple documents by ID\n\t */\n\tasync getMany(ids: string[]): Promise<Map<string, T>> {\n\t\tif (ids.length === 0) return new Map();\n\n\t\tconst rows = await this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select([\"id\", \"data\"])\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.execute();\n\n\t\tconst result = new Map<string, T>();\n\t\tfor (const row of rows) {\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T\n\t\t\tresult.set(row.id, JSON.parse(row.data) as T);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Store multiple documents\n\t */\n\tasync putMany(items: Array<{ id: string; data: T }>): Promise<void> {\n\t\tif (items.length === 0) return;\n\n\t\tconst now = new Date().toISOString();\n\n\t\t// SQLite doesn't support batch upserts well, so we do them one at a time\n\t\t// In a transaction for atomicity\n\t\tawait withTransaction(this.db, async (trx) => {\n\t\t\tfor (const item of items) {\n\t\t\t\tconst jsonData = JSON.stringify(item.data);\n\t\t\t\tawait trx\n\t\t\t\t\t.insertInto(\"_plugin_storage\")\n\t\t\t\t\t.values({\n\t\t\t\t\t\tplugin_id: this.pluginId,\n\t\t\t\t\t\tcollection: this.collection,\n\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\tdata: jsonData,\n\t\t\t\t\t\tcreated_at: now,\n\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t})\n\t\t\t\t\t.onConflict((oc) =>\n\t\t\t\t\t\toc.columns([\"plugin_id\", \"collection\", \"id\"]).doUpdateSet({\n\t\t\t\t\t\t\tdata: jsonData,\n\t\t\t\t\t\t\tupdated_at: now,\n\t\t\t\t\t\t}),\n\t\t\t\t\t)\n\t\t\t\t\t.execute();\n\t\t\t}\n\t\t});\n\t}\n\n\t/**\n\t * Delete multiple documents\n\t */\n\tasync deleteMany(ids: string[]): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_plugin_storage\")\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection)\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Query documents with filters\n\t */\n\tasync query(options: QueryOptions = {}): Promise<PaginatedResult<{ id: string; data: T }>> {\n\t\tconst { where = {}, orderBy = {}, cursor } = options;\n\t\tconst limit = Math.min(options.limit ?? 50, 100);\n\n\t\t// Validate that all queried fields are indexed\n\t\tvalidateWhereClause(where, this.indexedFields, this.pluginId, this.collection);\n\t\tif (Object.keys(orderBy).length > 0) {\n\t\t\tvalidateOrderByClause(orderBy, this.indexedFields, this.pluginId, this.collection);\n\t\t}\n\n\t\t// Build base query\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select([\"id\", \"data\", \"created_at\"])\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection);\n\n\t\t// Add JSON extraction WHERE conditions\n\t\tconst whereResult = buildWhereClause(this.db, where);\n\t\tif (whereResult.sql) {\n\t\t\tquery = query.where(rawWhereExpr(whereResult.sql, whereResult.params));\n\t\t}\n\n\t\tconst orderEntries = Object.entries(orderBy);\n\t\tconst orderDirections = new Set(orderEntries.map(([, direction]) => direction));\n\t\tconst descending = orderEntries.length > 0 && orderEntries[0][1] === \"desc\";\n\n\t\t// A missing key in a schemaless document extracts as NULL, and NULL sorts\n\t\t// differently per dialect (SQLite first, Postgres last) and makes any\n\t\t// comparison against it UNKNOWN. Rank nulls explicitly so both the sort and\n\t\t// the seek agree on where they sit, on either dialect, and so no comparison\n\t\t// operand is ever NULL.\n\t\tconst sortExpr = (field: string): RawBuilder<unknown> =>\n\t\t\tsql`${sql.raw(jsonOrderExtract(this.db, field))}`;\n\n\t\t// Handle cursor-based pagination — throws on invalid cursor.\n\t\tif (cursor) {\n\t\t\tconst decoded = decodeCursor(cursor);\n\t\t\tif (orderEntries.length === 0) {\n\t\t\t\tquery = query.where(({ eb }) =>\n\t\t\t\t\teb(sql`(created_at, id)`, \">\", sql`(${decoded.orderValue}, ${decoded.id})`),\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tif (orderDirections.size > 1) {\n\t\t\t\t\tthrow new StorageQueryError(\n\t\t\t\t\t\t\"Cursor pagination requires every orderBy field to share one direction.\",\n\t\t\t\t\t\tObject.keys(orderBy).join(\", \"),\n\t\t\t\t\t\t\"Sort every field the same way, or page without a cursor.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst op = descending ? sql`<` : sql`>`;\n\t\t\t\t// The cursor row's sort values, recomputed with the same expression\n\t\t\t\t// rather than carried in the cursor, so nothing is bound as a literal\n\t\t\t\t// and neither dialect has to coerce a JSON value to compare it.\n\t\t\t\tconst cursorExpr = (field: string): RawBuilder<unknown> =>\n\t\t\t\t\tsql`(select ${sql.raw(jsonOrderExtract(this.db, field))} from _plugin_storage where plugin_id = ${this.pluginId} and collection = ${this.collection} and id = ${decoded.id})`;\n\n\t\t\t\tconst fields = orderEntries.map(([field]) => {\n\t\t\t\t\tconst own = sortExpr(field);\n\t\t\t\t\tconst theirs = cursorExpr(field);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tequal: sql`(${nullRank(own)} = ${nullRank(theirs)} and (${nullRank(own)} = 1 or ${own} = ${theirs}))`,\n\t\t\t\t\t\tafter: sql`(${nullRank(own)} ${op} ${nullRank(theirs)} or (${nullRank(own)} = ${nullRank(theirs)} and ${nullRank(own)} = 0 and ${own} ${op} ${theirs}))`,\n\t\t\t\t\t};\n\t\t\t\t});\n\n\t\t\t\t// Lexicographic seek: ties on every earlier field, then strictly after\n\t\t\t\t// on this one; finally all fields tied and strictly after on id.\n\t\t\t\tconst terms = fields.map(\n\t\t\t\t\t(_, k) =>\n\t\t\t\t\t\tsql`(${sql.join([...fields.slice(0, k).map((f) => f.equal), fields[k].after], sql` and `)})`,\n\t\t\t\t);\n\t\t\t\tterms.push(\n\t\t\t\t\tsql`(${sql.join([...fields.map((f) => f.equal), sql`id ${op} ${decoded.id}`], sql` and `)})`,\n\t\t\t\t);\n\t\t\t\tquery = query.where(sql<boolean>`(${sql.join(terms, sql` or `)})`);\n\t\t\t}\n\t\t}\n\n\t\t// Build ORDER BY using sql template\n\t\tif (orderEntries.length > 0) {\n\t\t\tfor (const [field, direction] of orderEntries) {\n\t\t\t\tconst dir = direction === \"desc\" ? sql`desc` : sql`asc`;\n\t\t\t\tconst expr = sortExpr(field);\n\t\t\t\t// Null rank first so NULL placement is identical on both dialects.\n\t\t\t\tquery = query.orderBy(sql`${nullRank(expr)} ${dir}`);\n\t\t\t\t// Order over the jsonb-native value on Postgres so numeric fields sort\n\t\t\t\t// numerically, not lexically. See pluginDataOrderExpr.\n\t\t\t\tquery = query.orderBy(sql`${expr} ${dir}`);\n\t\t\t}\n\t\t\t// Total order, so a page boundary can never fall inside a group of ties.\n\t\t\tquery = query.orderBy(\"id\", descending ? \"desc\" : \"asc\");\n\t\t} else {\n\t\t\t// Default ordering for consistent pagination\n\t\t\tquery = query.orderBy(\"created_at\", \"asc\").orderBy(\"id\", \"asc\");\n\t\t}\n\n\t\t// Apply limit (fetch one extra to detect if there's more)\n\t\tquery = query.limit(limit + 1);\n\n\t\tconst rows = await query.execute();\n\n\t\tconst hasMore = rows.length > limit;\n\t\tconst items = rows.slice(0, limit).map((row) => ({\n\t\t\tid: row.id,\n\t\t\t// eslint-disable-next-line typescript/no-unsafe-type-assertion -- JSON.parse returns any; generic callers provide T\n\t\t\tdata: JSON.parse(row.data) as T,\n\t\t}));\n\n\t\t// Generate cursor for next page if there are more results\n\t\tlet nextCursor: string | undefined;\n\t\tif (hasMore) {\n\t\t\tconst lastItem = rows[limit - 1];\n\t\t\tif (lastItem) {\n\t\t\t\tnextCursor = encodeCursor(lastItem.created_at, lastItem.id);\n\t\t\t}\n\t\t}\n\n\t\treturn { items, cursor: nextCursor, hasMore };\n\t}\n\n\t/**\n\t * Count documents matching a filter\n\t */\n\tasync count(where?: WhereClause): Promise<number> {\n\t\tif (where && Object.keys(where).length > 0) {\n\t\t\tvalidateWhereClause(where, this.indexedFields, this.pluginId, this.collection);\n\t\t}\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_plugin_storage\")\n\t\t\t.select(sql<number>`COUNT(*)`.as(\"count\"))\n\t\t\t.where(\"plugin_id\", \"=\", this.pluginId)\n\t\t\t.where(\"collection\", \"=\", this.collection);\n\n\t\t// Add JSON extraction WHERE conditions\n\t\tif (where && Object.keys(where).length > 0) {\n\t\t\tconst whereResult = buildWhereClause(this.db, where);\n\t\t\tif (whereResult.sql) {\n\t\t\t\tquery = query.where(rawWhereExpr(whereResult.sql, whereResult.params));\n\t\t\t}\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\t// Number() because the pg driver returns COUNT(*) (bigint) as a string.\n\t\treturn Number(result?.count ?? 0);\n\t}\n}\n\n/**\n * Create a scoped storage accessor for a plugin\n */\nexport function createPluginStorageAccessor(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tstorageConfig: Record<\n\t\tstring,\n\t\t{ indexes: Array<string | string[]>; uniqueIndexes?: Array<string | string[]> }\n\t>,\n): Record<string, StorageCollection> {\n\tconst accessor: Record<string, StorageCollection> = {};\n\n\tfor (const [collectionName, config] of Object.entries(storageConfig)) {\n\t\tconst allIndexes = [...config.indexes, ...(config.uniqueIndexes ?? [])];\n\t\taccessor[collectionName] = new PluginStorageRepository(\n\t\t\tdb,\n\t\t\tpluginId,\n\t\t\tcollectionName,\n\t\t\tallIndexes,\n\t\t);\n\t}\n\n\treturn accessor;\n}\n\n/**\n * Delete all storage data for a plugin\n */\nexport async function deleteAllPluginStorage(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_plugin_storage\")\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n\n/**\n * Delete all storage data for a plugin collection\n */\nexport async function deletePluginCollection(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollection: string,\n): Promise<number> {\n\tconst result = await db\n\t\t.deleteFrom(\"_plugin_storage\")\n\t\t.where(\"plugin_id\", \"=\", pluginId)\n\t\t.where(\"collection\", \"=\", collection)\n\t\t.executeTakeFirst();\n\n\treturn Number(result.numDeletedRows ?? 0);\n}\n","/**\n * Plugin Context v2\n *\n * Creates the unified context object provided to plugins in all hooks and routes.\n *\n */\n\nimport type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { ContentRepository } from \"../database/repositories/content.js\";\nimport { MediaRepository } from \"../database/repositories/media.js\";\nimport { OptionsRepository } from \"../database/repositories/options.js\";\nimport { PluginStorageRepository } from \"../database/repositories/plugin-storage.js\";\nimport { SeoRepository } from \"../database/repositories/seo.js\";\nimport { TaxonomyRepository, type Taxonomy } from \"../database/repositories/taxonomy.js\";\nimport { UserRepository } from \"../database/repositories/user.js\";\nimport { withTransaction } from \"../database/transaction.js\";\nimport type { Database } from \"../database/types.js\";\nimport { resolveContentCreateLocale } from \"../i18n/config.js\";\nimport {\n\tresolveAndValidateExternalUrl,\n\tSsrfError,\n\tstripCredentialHeaders,\n} from \"../import/ssrf.js\";\nimport { enrichImageMetadata } from \"../media/enrich.js\";\nimport { markContentMediaUsageCollectionStaleSafely } from \"../media/usage/content-refresh.js\";\nimport { invalidateSiteSettingsCache } from \"../settings/index.js\";\nimport type { Storage } from \"../storage/types.js\";\nimport { CronAccessImpl } from \"./cron.js\";\nimport type { EmailPipeline } from \"./email.js\";\nimport type {\n\tResolvedPlugin,\n\tPluginContext,\n\tPluginStorageConfig,\n\tStorageCollection,\n\tKVAccess,\n\tCronAccess,\n\tEmailAccess,\n\tContentAccess,\n\tContentAccessWithWrite,\n\tMediaAccess,\n\tMediaAccessWithWrite,\n\tHttpAccess,\n\tLogAccess,\n\tSiteInfo,\n\tUserAccess,\n\tUserInfo,\n\tContentItem,\n\tContentCreateOptions,\n\tContentItemSeoInput,\n\tContentWriteInput,\n\tMediaItem,\n\tPaginatedResult,\n\tQueryOptions,\n\tContentListOptions,\n\tMediaListOptions,\n\tTaxonomyAccess,\n\tTaxonomyDefInfo,\n\tTaxonomyTermInfo,\n\tTaxonomyReadOptions,\n} from \"./types.js\";\n\n// =============================================================================\n// KV Access\n// =============================================================================\n\n/**\n * Create KV accessor for a plugin\n * All keys are automatically prefixed with the plugin ID\n */\nexport function createKVAccess(optionsRepo: OptionsRepository, pluginId: string): KVAccess {\n\tconst prefix = `plugin:${pluginId}:`;\n\n\treturn {\n\t\tasync get<T>(key: string): Promise<T | null> {\n\t\t\treturn optionsRepo.get<T>(`${prefix}${key}`);\n\t\t},\n\n\t\tasync set(key: string, value: unknown): Promise<void> {\n\t\t\tawait optionsRepo.set(`${prefix}${key}`, value);\n\t\t},\n\n\t\tasync delete(key: string): Promise<boolean> {\n\t\t\treturn optionsRepo.delete(`${prefix}${key}`);\n\t\t},\n\n\t\tasync list(keyPrefix?: string): Promise<Array<{ key: string; value: unknown }>> {\n\t\t\tconst fullPrefix = `${prefix}${keyPrefix ?? \"\"}`;\n\t\t\tconst entriesMap = await optionsRepo.getByPrefix(fullPrefix);\n\t\t\tconst result: Array<{ key: string; value: unknown }> = [];\n\t\t\tfor (const [fullKey, value] of entriesMap) {\n\t\t\t\tresult.push({\n\t\t\t\t\tkey: fullKey.slice(prefix.length),\n\t\t\t\t\tvalue,\n\t\t\t\t});\n\t\t\t}\n\t\t\treturn result;\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Storage Access\n// =============================================================================\n\n/**\n * Create storage collection accessor for a plugin\n * Wraps PluginStorageRepository with the v2 interface (no async iterators)\n */\nfunction createStorageCollection<T>(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tcollectionName: string,\n\tindexes: Array<string | string[]>,\n): StorageCollection<T> {\n\tconst repo = new PluginStorageRepository<T>(db, pluginId, collectionName, indexes);\n\n\treturn {\n\t\tget: (id) => repo.get(id),\n\t\tput: (id, data) => repo.put(id, data),\n\t\tdelete: (id) => repo.delete(id),\n\t\texists: (id) => repo.exists(id),\n\t\tgetMany: (ids) => repo.getMany(ids),\n\t\tputMany: (items) => repo.putMany(items),\n\t\tdeleteMany: (ids) => repo.deleteMany(ids),\n\t\tcount: (where) => repo.count(where),\n\n\t\t// Query returns PaginatedResult instead of the old format\n\t\tasync query(options?: QueryOptions): Promise<PaginatedResult<{ id: string; data: T }>> {\n\t\t\tconst result = await repo.query({\n\t\t\t\twhere: options?.where,\n\t\t\t\torderBy: options?.orderBy,\n\t\t\t\tlimit: options?.limit,\n\t\t\t\tcursor: options?.cursor,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\titems: result.items,\n\t\t\t\tcursor: result.cursor,\n\t\t\t\thasMore: result.hasMore,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Create storage accessor with all declared collections\n */\nexport function createStorageAccess<T extends PluginStorageConfig>(\n\tdb: Kysely<Database>,\n\tpluginId: string,\n\tstorageConfig: T,\n): Record<string, StorageCollection> {\n\tconst storage: Record<string, StorageCollection> = {};\n\n\tfor (const [collectionName, config] of Object.entries(storageConfig)) {\n\t\tconst allIndexes = [...config.indexes, ...(config.uniqueIndexes ?? [])];\n\t\tstorage[collectionName] = createStorageCollection(db, pluginId, collectionName, allIndexes);\n\t}\n\n\treturn storage;\n}\n\n// =============================================================================\n// Content Access\n// =============================================================================\n\n/**\n * Extract `seo` from a plugin-supplied content write input and return both\n * parts. Mutates nothing — returns a new field map without the `seo` key.\n */\nfunction splitSeoFromInput(input: ContentWriteInput): {\n\tfields: Record<string, unknown>;\n\tseo: ContentItemSeoInput | undefined;\n} {\n\tconst { seo, ...fields } = input;\n\t// Reject non-object seo values rather than silently dropping them.\n\tif (seo !== undefined && (seo === null || typeof seo !== \"object\" || Array.isArray(seo))) {\n\t\tthrow new Error(\"content.seo must be an object\");\n\t}\n\treturn { fields, seo };\n}\n\n/**\n * Reject writing SEO to a collection that does not have it enabled.\n * Matches the REST API behavior (VALIDATION_ERROR).\n */\nasync function assertSeoEnabled(\n\tseoRepo: SeoRepository,\n\tcollection: string,\n\tseo: ContentItemSeoInput | undefined,\n): Promise<boolean> {\n\tconst hasSeo = await seoRepo.isEnabled(collection);\n\tif (seo !== undefined && !hasSeo) {\n\t\tthrow new Error(\n\t\t\t`Collection \"${collection}\" does not have SEO enabled. ` +\n\t\t\t\t`Remove the seo field or enable SEO on this collection.`,\n\t\t);\n\t}\n\treturn hasSeo;\n}\n\n/**\n * Parse the `collections` JSON column into a string array (`[]` on anything\n * else). Mirrors the guards in the Cloudflare/workerd bridges so an\n * in-process plugin degrades on malformed data instead of crashing.\n */\nfunction parseCollectionsColumn(value: string | null): string[] {\n\tif (!value) return [];\n\ttry {\n\t\tconst parsed: unknown = JSON.parse(value);\n\t\treturn Array.isArray(parsed)\n\t\t\t? parsed.filter((item): item is string => typeof item === \"string\")\n\t\t\t: [];\n\t} catch {\n\t\treturn [];\n\t}\n}\n\n/** Map a repository `Taxonomy` row to the plugin-facing term shape. */\nfunction taxonomyToTermInfo(term: Taxonomy): TaxonomyTermInfo {\n\treturn {\n\t\tid: term.id,\n\t\ttaxonomy: term.name,\n\t\tslug: term.slug,\n\t\tlabel: term.label,\n\t\tparentId: term.parentId,\n\t\tdata: term.data,\n\t\tlocale: term.locale,\n\t\ttranslationGroup: term.translationGroup,\n\t};\n}\n\n/**\n * Create read-only content access\n */\nexport function createContentAccess(db: Kysely<Database>): ContentAccess {\n\tconst contentRepo = new ContentRepository(db);\n\tconst seoRepo = new SeoRepository(db);\n\n\treturn {\n\t\tasync get(collection: string, id: string): Promise<ContentItem | null> {\n\t\t\tconst item = await contentRepo.findById(collection, id);\n\t\t\tif (!item) return null;\n\n\t\t\tconst result: ContentItem = {\n\t\t\t\tid: item.id,\n\t\t\t\ttype: item.type,\n\t\t\t\tslug: item.slug,\n\t\t\t\tstatus: item.status,\n\t\t\t\tdata: item.data,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\tlocale: item.locale,\n\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\tscheduledAt: item.scheduledAt,\n\t\t\t};\n\n\t\t\tif (await seoRepo.isEnabled(collection)) {\n\t\t\t\tresult.seo = await seoRepo.get(collection, item.id);\n\t\t\t}\n\n\t\t\treturn result;\n\t\t},\n\n\t\tasync list(\n\t\t\tcollection: string,\n\t\t\toptions?: ContentListOptions,\n\t\t): Promise<PaginatedResult<ContentItem>> {\n\t\t\t// Convert orderBy format if provided\n\t\t\tlet orderBy: { field: string; direction: \"asc\" | \"desc\" } | undefined;\n\t\t\tif (options?.orderBy) {\n\t\t\t\tconst entries = Object.entries(options.orderBy);\n\t\t\t\tconst first = entries[0];\n\t\t\t\tif (first) {\n\t\t\t\t\torderBy = { field: first[0], direction: first[1] };\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst result = await contentRepo.findMany(collection, {\n\t\t\t\tlimit: options?.limit ?? 50,\n\t\t\t\tcursor: options?.cursor,\n\t\t\t\torderBy,\n\t\t\t\twhere: options?.where,\n\t\t\t});\n\n\t\t\tconst items: ContentItem[] = result.items.map((item) => ({\n\t\t\t\tid: item.id,\n\t\t\t\ttype: item.type,\n\t\t\t\tslug: item.slug,\n\t\t\t\tstatus: item.status,\n\t\t\t\tdata: item.data,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\tlocale: item.locale,\n\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\tscheduledAt: item.scheduledAt,\n\t\t\t}));\n\n\t\t\tif (items.length > 0 && (await seoRepo.isEnabled(collection))) {\n\t\t\t\tconst seoMap = await seoRepo.getMany(\n\t\t\t\t\tcollection,\n\t\t\t\t\titems.map((i) => i.id),\n\t\t\t\t);\n\t\t\t\tfor (const item of items) {\n\t\t\t\t\tconst seo = seoMap.get(item.id);\n\t\t\t\t\tif (seo) item.seo = seo;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\titems,\n\t\t\t\tcursor: result.nextCursor,\n\t\t\t\thasMore: !!result.nextCursor,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Create read-only taxonomy access (gated on `taxonomies:read`).\n */\nexport function createTaxonomyAccess(db: Kysely<Database>): TaxonomyAccess {\n\tconst taxonomyRepo = new TaxonomyRepository(db);\n\n\treturn {\n\t\tasync getAll(options?: TaxonomyReadOptions): Promise<TaxonomyDefInfo[]> {\n\t\t\tlet query = db.selectFrom(\"_emdash_taxonomy_defs\").selectAll();\n\t\t\tif (options?.locale !== undefined) query = query.where(\"locale\", \"=\", options.locale);\n\t\t\tconst rows = await query.orderBy(\"name\", \"asc\").execute();\n\t\t\treturn rows.map((row) => ({\n\t\t\t\tname: row.name,\n\t\t\t\tlabel: row.label,\n\t\t\t\tlabelSingular: row.label_singular,\n\t\t\t\thierarchical: row.hierarchical === 1,\n\t\t\t\tcollections: parseCollectionsColumn(row.collections),\n\t\t\t\tlocale: row.locale,\n\t\t\t}));\n\t\t},\n\n\t\tasync getTerms(taxonomy: string, options?: TaxonomyReadOptions): Promise<TaxonomyTermInfo[]> {\n\t\t\tconst terms = await taxonomyRepo.findByName(taxonomy, { locale: options?.locale });\n\t\t\treturn terms.map(taxonomyToTermInfo);\n\t\t},\n\n\t\tasync getEntryTerms(\n\t\t\tcollection: string,\n\t\t\tentryId: string,\n\t\t\toptions?: TaxonomyReadOptions & { taxonomy?: string },\n\t\t): Promise<TaxonomyTermInfo[]> {\n\t\t\tconst terms = await taxonomyRepo.getTermsForEntry(\n\t\t\t\tcollection,\n\t\t\t\tentryId,\n\t\t\t\toptions?.taxonomy,\n\t\t\t\toptions?.locale,\n\t\t\t);\n\t\t\treturn terms.map(taxonomyToTermInfo);\n\t\t},\n\t};\n}\n\n/**\n * Create full content access with write operations.\n *\n * `create` and `update` accept a reserved `seo` key in their `data`\n * argument. When present, it is routed to the core SEO panel\n * (`_emdash_seo`) via `SeoRepository.upsert`, in the same transaction as\n * the content write. The returned `ContentItem.seo` reflects the resulting\n * SEO state for SEO-enabled collections.\n */\nexport function createContentAccessWithWrite(\n\tdb: Kysely<Database>,\n\tbeforeContentWrite?: () => Promise<void>,\n): ContentAccessWithWrite {\n\tconst readAccess = createContentAccess(db);\n\n\treturn {\n\t\t...readAccess,\n\n\t\tasync create(\n\t\t\tcollection: string,\n\t\t\tdata: ContentWriteInput,\n\t\t\toptions?: ContentCreateOptions,\n\t\t): Promise<ContentItem> {\n\t\t\tconst locale = resolveContentCreateLocale(options?.locale);\n\t\t\tawait beforeContentWrite?.();\n\t\t\tconst { fields, seo } = splitSeoFromInput(data);\n\t\t\tlet contentMutated = false;\n\n\t\t\ttry {\n\t\t\t\tconst created = await withTransaction(db, async (trx) => {\n\t\t\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\t\t\tconst trxSeoRepo = new SeoRepository(trx);\n\n\t\t\t\t\tconst hasSeo = await assertSeoEnabled(trxSeoRepo, collection, seo);\n\n\t\t\t\t\tconst item = await trxContentRepo.create({\n\t\t\t\t\t\ttype: collection,\n\t\t\t\t\t\tdata: fields,\n\t\t\t\t\t\tlocale,\n\t\t\t\t\t});\n\t\t\t\t\tcontentMutated = true;\n\n\t\t\t\t\tconst result: ContentItem = {\n\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\ttype: item.type,\n\t\t\t\t\t\tslug: item.slug,\n\t\t\t\t\t\tstatus: item.status,\n\t\t\t\t\t\tdata: item.data,\n\t\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t\t\tscheduledAt: item.scheduledAt,\n\t\t\t\t\t};\n\n\t\t\t\t\tif (hasSeo) {\n\t\t\t\t\t\tresult.seo =\n\t\t\t\t\t\t\tseo !== undefined\n\t\t\t\t\t\t\t\t? await trxSeoRepo.upsert(collection, item.id, seo)\n\t\t\t\t\t\t\t\t: await trxSeoRepo.get(collection, item.id);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn result;\n\t\t\t\t});\n\t\t\t\tawait markContentMediaUsageCollectionStaleSafely(db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t\treturn created;\n\t\t\t} catch (error) {\n\t\t\t\tif (contentMutated) {\n\t\t\t\t\tawait markContentMediaUsageCollectionStaleSafely(db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\tasync update(collection: string, id: string, data: ContentWriteInput): Promise<ContentItem> {\n\t\t\tawait beforeContentWrite?.();\n\t\t\tconst { fields, seo } = splitSeoFromInput(data);\n\t\t\tconst hasFieldUpdates = Object.keys(fields).length > 0;\n\t\t\tlet contentMutated = false;\n\n\t\t\ttry {\n\t\t\t\tconst updated = await withTransaction(db, async (trx) => {\n\t\t\t\t\tconst trxContentRepo = new ContentRepository(trx);\n\t\t\t\t\tconst trxSeoRepo = new SeoRepository(trx);\n\n\t\t\t\t\tconst hasSeo = await assertSeoEnabled(trxSeoRepo, collection, seo);\n\n\t\t\t\t\t// Pass the `data` payload to ContentRepository.updateDraftAware only when\n\t\t\t\t\t// there are field updates — passing an empty object would still\n\t\t\t\t\t// bump updated_at/version, but we want a seo-only call to touch\n\t\t\t\t\t// only the SEO table. updateDraftAware delegates no-op writes to\n\t\t\t\t\t// ContentRepository.update.\n\t\t\t\t\tconst item = hasFieldUpdates\n\t\t\t\t\t\t? await trxContentRepo.updateDraftAware(collection, id, { data: fields })\n\t\t\t\t\t\t: await (async () => {\n\t\t\t\t\t\t\t\tconst existing = await trxContentRepo.findById(collection, id);\n\t\t\t\t\t\t\t\tif (!existing) throw new Error(\"Content not found\");\n\t\t\t\t\t\t\t\treturn existing;\n\t\t\t\t\t\t\t})();\n\t\t\t\t\tif (hasFieldUpdates) contentMutated = true;\n\n\t\t\t\t\tconst result: ContentItem = {\n\t\t\t\t\t\tid: item.id,\n\t\t\t\t\t\ttype: item.type,\n\t\t\t\t\t\tslug: item.slug,\n\t\t\t\t\t\tstatus: item.status,\n\t\t\t\t\t\tdata: item.data,\n\t\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t\t\tupdatedAt: item.updatedAt,\n\t\t\t\t\t\tlocale: item.locale,\n\t\t\t\t\t\tpublishedAt: item.publishedAt,\n\t\t\t\t\t\tscheduledAt: item.scheduledAt,\n\t\t\t\t\t};\n\n\t\t\t\t\tif (hasSeo) {\n\t\t\t\t\t\tresult.seo =\n\t\t\t\t\t\t\tseo !== undefined\n\t\t\t\t\t\t\t\t? await trxSeoRepo.upsert(collection, item.id, seo)\n\t\t\t\t\t\t\t\t: await trxSeoRepo.get(collection, item.id);\n\t\t\t\t\t}\n\n\t\t\t\t\treturn result;\n\t\t\t\t});\n\t\t\t\tif (hasFieldUpdates) {\n\t\t\t\t\tawait markContentMediaUsageCollectionStaleSafely(db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t\t}\n\t\t\t\treturn updated;\n\t\t\t} catch (error) {\n\t\t\t\tif (contentMutated) {\n\t\t\t\t\tawait markContentMediaUsageCollectionStaleSafely(db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t},\n\n\t\tasync delete(collection: string, id: string): Promise<boolean> {\n\t\t\tawait beforeContentWrite?.();\n\t\t\tconst contentRepo = new ContentRepository(db);\n\t\t\tconst deleted = await contentRepo.delete(collection, id);\n\t\t\tif (deleted) {\n\t\t\t\tawait markContentMediaUsageCollectionStaleSafely(db, collection, \"CONTENT_USAGE_STALE\");\n\t\t\t}\n\t\t\treturn deleted;\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Media Access\n// =============================================================================\n\n/**\n * Create read-only media access\n */\nexport function createMediaAccess(db: Kysely<Database>): MediaAccess {\n\tconst mediaRepo = new MediaRepository(db);\n\n\treturn {\n\t\tasync get(id: string): Promise<MediaItem | null> {\n\t\t\tconst item = await mediaRepo.findById(id);\n\t\t\tif (!item) return null;\n\n\t\t\treturn {\n\t\t\t\tid: item.id,\n\t\t\t\tfilename: item.filename,\n\t\t\t\tmimeType: item.mimeType,\n\t\t\t\tsize: item.size,\n\t\t\t\t// Construct URL from storage key (or use a sensible default path)\n\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t};\n\t\t},\n\n\t\tasync list(options?: MediaListOptions): Promise<PaginatedResult<MediaItem>> {\n\t\t\tconst result = await mediaRepo.findMany({\n\t\t\t\tlimit: options?.limit ?? 50,\n\t\t\t\tcursor: options?.cursor,\n\t\t\t\tmimeType: options?.mimeType,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\titems: result.items.map((item) => ({\n\t\t\t\t\tid: item.id,\n\t\t\t\t\tfilename: item.filename,\n\t\t\t\t\tmimeType: item.mimeType,\n\t\t\t\t\tsize: item.size,\n\t\t\t\t\turl: `/media/${item.id}/${item.filename}`,\n\t\t\t\t\tcreatedAt: item.createdAt,\n\t\t\t\t})),\n\t\t\t\tcursor: result.nextCursor,\n\t\t\t\thasMore: !!result.nextCursor,\n\t\t\t};\n\t\t},\n\t};\n}\n\n/**\n * Create full media access with write operations.\n *\n * `getUploadUrlFn` is optional: when omitted, `getUploadUrl()` is derived from\n * `storage` (create a pending record + a signed PUT URL), mirroring the REST\n * `/_emdash/api/media/upload-url` endpoint. `upload()` only needs `storage`.\n * If storage is not provided, both throw at call time.\n */\nexport function createMediaAccessWithWrite(\n\tdb: Kysely<Database>,\n\tgetUploadUrlFn:\n\t\t| ((filename: string, contentType: string) => Promise<{ uploadUrl: string; mediaId: string }>)\n\t\t| undefined,\n\tstorage?: Storage,\n): MediaAccessWithWrite {\n\tconst mediaRepo = new MediaRepository(db);\n\tconst readAccess = createMediaAccess(db);\n\n\tconst getUploadUrl =\n\t\tgetUploadUrlFn ??\n\t\t(async (filename: string, contentType: string) => {\n\t\t\tif (!storage) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Media getUploadUrl() requires a storage backend. Configure storage in PluginContextFactoryOptions.\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst basename = filename.split(\"/\").pop() ?? filename;\n\t\t\tconst dotIdx = basename.lastIndexOf(\".\");\n\t\t\tconst ext = dotIdx > 0 ? basename.slice(dotIdx).toLowerCase() : \"\";\n\t\t\tconst storageKey = `${ulid()}${ext}`;\n\n\t\t\tconst media = await mediaRepo.createPending({\n\t\t\t\tfilename: basename,\n\t\t\t\tmimeType: contentType,\n\t\t\t\tstorageKey,\n\t\t\t});\n\n\t\t\tconst signed = await storage.getSignedUploadUrl({\n\t\t\t\tkey: storageKey,\n\t\t\t\tcontentType,\n\t\t\t\texpiresIn: 3600,\n\t\t\t});\n\n\t\t\treturn { uploadUrl: signed.url, mediaId: media.id };\n\t\t});\n\n\treturn {\n\t\t...readAccess,\n\n\t\tgetUploadUrl,\n\n\t\tasync upload(\n\t\t\tfilename: string,\n\t\t\tcontentType: string,\n\t\t\tbytes: ArrayBuffer,\n\t\t): Promise<{ mediaId: string; storageKey: string; url: string }> {\n\t\t\tif (!storage) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Media upload() requires a storage backend. Configure storage in PluginContextFactoryOptions.\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\t// Generate a storage key with a unique prefix\n\t\t\tconst keyPrefix = ulid();\n\t\t\t// Extract extension from basename (ignore path separators)\n\t\t\tconst basename = filename.split(\"/\").pop() ?? filename;\n\t\t\tconst dotIdx = basename.lastIndexOf(\".\");\n\t\t\tconst ext = dotIdx > 0 ? basename.slice(dotIdx).toLowerCase() : \"\";\n\t\t\tconst storageKey = `${keyPrefix}${ext}`;\n\n\t\t\t// Upload to storage first\n\t\t\tawait storage.upload({\n\t\t\t\tkey: storageKey,\n\t\t\t\tbody: new Uint8Array(bytes),\n\t\t\t\tcontentType,\n\t\t\t});\n\n\t\t\t// Derive dimensions + LQIP placeholders (no-op for non-images).\n\t\t\tconst enriched = await enrichImageMetadata(new Uint8Array(bytes), contentType);\n\n\t\t\t// Create DB record — clean up storage on failure\n\t\t\tlet media;\n\t\t\ttry {\n\t\t\t\tmedia = await mediaRepo.create({\n\t\t\t\t\tfilename: basename,\n\t\t\t\t\tmimeType: contentType,\n\t\t\t\t\tsize: bytes.byteLength,\n\t\t\t\t\tstorageKey,\n\t\t\t\t\tstatus: \"ready\",\n\t\t\t\t\twidth: enriched.width,\n\t\t\t\t\theight: enriched.height,\n\t\t\t\t\tblurhash: enriched.blurhash,\n\t\t\t\t\tdominantColor: enriched.dominantColor,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\ttry {\n\t\t\t\t\tawait storage.delete(storageKey);\n\t\t\t\t} catch {\n\t\t\t\t\t// Best-effort cleanup\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tmediaId: media.id,\n\t\t\t\tstorageKey,\n\t\t\t\turl: `/_emdash/api/media/file/${storageKey}`,\n\t\t\t};\n\t\t},\n\n\t\tasync delete(id: string): Promise<boolean> {\n\t\t\tconst deleted = await mediaRepo.delete(id);\n\t\t\t// Plugins can delete media that's referenced by site settings\n\t\t\t// (`logo`, `favicon`, `seo.defaultOgImage`); the worker-scoped\n\t\t\t// resolved-URL cache must be dropped or it will keep serving\n\t\t\t// 404s. Matches the invalidation in\n\t\t\t// `EmDashRuntime.handleMediaDelete`.\n\t\t\tif (deleted) {\n\t\t\t\tinvalidateSiteSettingsCache();\n\t\t\t}\n\t\t\treturn deleted;\n\t\t},\n\t};\n}\n\n// =============================================================================\n// HTTP Access\n// =============================================================================\n\n/** Maximum number of redirects to follow in plugin HTTP access */\nconst MAX_PLUGIN_REDIRECTS = 5;\n\n/**\n * Check if a hostname matches any pattern in the allowed list.\n * Patterns: \"*\" matches all, \"*.example.com\" matches subdomains AND bare \"example.com\",\n * \"api.example.com\" matches exactly.\n */\nfunction isHostAllowed(host: string, allowedHosts: string[]): boolean {\n\treturn allowedHosts.some((pattern) => {\n\t\tif (pattern === \"*\") return true;\n\t\tif (pattern.startsWith(\"*.\")) {\n\t\t\tconst suffix = pattern.slice(1); // \".example.com\"\n\t\t\t// Match subdomains (foo.example.com) and bare domain (example.com)\n\t\t\treturn host.endsWith(suffix) || host === pattern.slice(2);\n\t\t}\n\t\treturn host === pattern;\n\t});\n}\n\n/**\n * Create HTTP access with host validation.\n *\n * Uses redirect: \"manual\" to re-validate each redirect target against\n * the allowedHosts list, preventing redirects to unauthorized hosts.\n */\nexport function createHttpAccess(pluginId: string, allowedHosts: string[]): HttpAccess {\n\treturn {\n\t\tasync fetch(url: string, init?: RequestInit): Promise<Response> {\n\t\t\t// Deny by default — plugins must declare allowed hosts\n\t\t\tif (allowedHosts.length === 0) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Plugin \"${pluginId}\" has no allowed hosts configured. ` +\n\t\t\t\t\t\t`Add hosts to the plugin's allowedHosts array to enable HTTP requests.`,\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tlet currentUrl = url;\n\t\t\tlet currentInit = init;\n\n\t\t\tfor (let i = 0; i <= MAX_PLUGIN_REDIRECTS; i++) {\n\t\t\t\tconst hostname = new URL(currentUrl).hostname;\n\t\t\t\tif (!isHostAllowed(hostname, allowedHosts)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Plugin \"${pluginId}\" is not allowed to fetch from host \"${hostname}\". ` +\n\t\t\t\t\t\t\t`Allowed hosts: ${allowedHosts.join(\", \")}`,\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst response = await globalThis.fetch(currentUrl, {\n\t\t\t\t\t...currentInit,\n\t\t\t\t\tredirect: \"manual\",\n\t\t\t\t});\n\n\t\t\t\t// Not a redirect -- return directly\n\t\t\t\tif (response.status < 300 || response.status >= 400) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Extract redirect target\n\t\t\t\tconst location = response.headers.get(\"Location\");\n\t\t\t\tif (!location) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Resolve relative redirects; strip credentials on cross-origin hops\n\t\t\t\tconst previousOrigin = new URL(currentUrl).origin;\n\t\t\t\tcurrentUrl = new URL(location, currentUrl).href;\n\t\t\t\tconst nextOrigin = new URL(currentUrl).origin;\n\n\t\t\t\tif (previousOrigin !== nextOrigin && currentInit) {\n\t\t\t\t\tcurrentInit = stripCredentialHeaders(currentInit);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthrow new Error(`Plugin \"${pluginId}\": too many redirects (max ${MAX_PLUGIN_REDIRECTS})`);\n\t\t},\n\t};\n}\n\n/**\n * Create unrestricted HTTP access (for plugins with network:fetch:any capability).\n * No host validation, but applies SSRF protection on redirect targets to\n * prevent plugins from being tricked into reaching internal services.\n */\nexport function createUnrestrictedHttpAccess(pluginId: string): HttpAccess {\n\treturn {\n\t\tasync fetch(url: string, init?: RequestInit): Promise<Response> {\n\t\t\tlet currentUrl = url;\n\t\t\tlet currentInit = init;\n\n\t\t\tfor (let i = 0; i <= MAX_PLUGIN_REDIRECTS; i++) {\n\t\t\t\t// Validate each URL against SSRF rules (private IPs, metadata\n\t\t\t\t// endpoints, wildcard DNS, resolved-IP private ranges).\n\t\t\t\ttry {\n\t\t\t\t\tawait resolveAndValidateExternalUrl(currentUrl);\n\t\t\t\t} catch (e) {\n\t\t\t\t\tconst msg = e instanceof SsrfError ? e.message : \"SSRF validation failed\";\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Plugin \"${pluginId}\": blocked fetch to \"${new URL(currentUrl).hostname}\": ${msg}`,\n\t\t\t\t\t\t{ cause: e },\n\t\t\t\t\t);\n\t\t\t\t}\n\n\t\t\t\tconst response = await globalThis.fetch(currentUrl, {\n\t\t\t\t\t...currentInit,\n\t\t\t\t\tredirect: \"manual\",\n\t\t\t\t});\n\n\t\t\t\t// Not a redirect -- return directly\n\t\t\t\tif (response.status < 300 || response.status >= 400) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Extract redirect target\n\t\t\t\tconst location = response.headers.get(\"Location\");\n\t\t\t\tif (!location) {\n\t\t\t\t\treturn response;\n\t\t\t\t}\n\n\t\t\t\t// Resolve relative redirects; strip credentials on cross-origin hops\n\t\t\t\tconst previousOrigin = new URL(currentUrl).origin;\n\t\t\t\tcurrentUrl = new URL(location, currentUrl).href;\n\t\t\t\tconst nextOrigin = new URL(currentUrl).origin;\n\n\t\t\t\tif (previousOrigin !== nextOrigin && currentInit) {\n\t\t\t\t\tcurrentInit = stripCredentialHeaders(currentInit);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tthrow new Error(`Plugin \"${pluginId}\": too many redirects (max ${MAX_PLUGIN_REDIRECTS})`);\n\t\t},\n\t};\n}\n\n/**\n * Create blocked HTTP access (for plugins without network:request capability)\n */\nexport function createBlockedHttpAccess(pluginId: string): HttpAccess {\n\treturn {\n\t\tasync fetch(): Promise<never> {\n\t\t\tthrow new Error(\n\t\t\t\t`Plugin \"${pluginId}\" does not have the \"network:request\" capability. ` +\n\t\t\t\t\t`Add \"network:request\" to the plugin's capabilities to enable HTTP requests.`,\n\t\t\t);\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Log Access\n// =============================================================================\n\n/**\n * Create logger for a plugin\n */\nexport function createLogAccess(pluginId: string): LogAccess {\n\tconst prefix = `[plugin:${pluginId}]`;\n\n\treturn {\n\t\tdebug(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.debug(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.debug(prefix, message);\n\t\t\t}\n\t\t},\n\n\t\tinfo(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.info(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.info(prefix, message);\n\t\t\t}\n\t\t},\n\n\t\twarn(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.warn(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.warn(prefix, message);\n\t\t\t}\n\t\t},\n\n\t\terror(message: string, data?: unknown): void {\n\t\t\tif (data !== undefined) {\n\t\t\t\tconsole.error(prefix, message, data);\n\t\t\t} else {\n\t\t\t\tconsole.error(prefix, message);\n\t\t\t}\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Site Info\n// =============================================================================\n\nconst TRAILING_SLASH_RE = /\\/$/;\n\n/**\n * Options for creating site info\n */\nexport interface SiteInfoOptions {\n\t/** Site name from options table */\n\tsiteName?: string;\n\t/** Site URL from options table or Astro config */\n\tsiteUrl?: string;\n\t/** Site locale from options table */\n\tlocale?: string;\n\t/** Astro's `trailingSlash` config (from `virtual:emdash/config`). */\n\ttrailingSlash?: \"always\" | \"never\" | \"ignore\";\n}\n\n/**\n * Create site info from config and settings.\n *\n * Resolution order for URL:\n * 1. options table (emdash:site_url)\n * 2. Astro `site` config\n * 3. fallback to empty string\n */\nexport function createSiteInfo(options: SiteInfoOptions): SiteInfo {\n\treturn {\n\t\tname: options.siteName ?? \"\",\n\t\turl: (options.siteUrl ?? \"\").replace(TRAILING_SLASH_RE, \"\"), // strip trailing slash\n\t\tlocale: options.locale ?? \"en\",\n\t\ttrailingSlash: options.trailingSlash ?? \"ignore\", // Astro's default\n\t};\n}\n\n/**\n * Create a URL helper that generates absolute URLs from relative paths.\n * Validates that path starts with \"/\" and rejects protocol-relative paths (\"//\").\n */\nexport function createUrlHelper(siteUrl: string): (path: string) => string {\n\tconst base = siteUrl.replace(TRAILING_SLASH_RE, \"\"); // strip trailing slash\n\n\treturn (path: string): string => {\n\t\tif (!path.startsWith(\"/\")) {\n\t\t\tthrow new Error(`URL path must start with \"/\", got: \"${path}\"`);\n\t\t}\n\t\tif (path.startsWith(\"//\")) {\n\t\t\tthrow new Error(`URL path must not be protocol-relative, got: \"${path}\"`);\n\t\t}\n\t\treturn `${base}${path}`;\n\t};\n}\n\n// =============================================================================\n// User Access\n// =============================================================================\n\n/**\n * Convert a UserRepository user to the plugin-facing UserInfo shape.\n * Strips sensitive fields (avatarUrl, emailVerified, data).\n */\nfunction toUserInfo(user: {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\trole: number;\n\tcreatedAt: string;\n}): UserInfo {\n\treturn {\n\t\tid: user.id,\n\t\temail: user.email,\n\t\tname: user.name,\n\t\trole: user.role,\n\t\tcreatedAt: user.createdAt,\n\t};\n}\n\n/**\n * Create read-only user access for plugins.\n * Excludes sensitive fields (password hashes, sessions, passkeys, avatar URL, data).\n */\nexport function createUserAccess(db: Kysely<Database>): UserAccess {\n\tconst userRepo = new UserRepository(db);\n\n\treturn {\n\t\tasync get(id: string): Promise<UserInfo | null> {\n\t\t\tconst user = await userRepo.findById(id);\n\t\t\tif (!user) return null;\n\t\t\treturn toUserInfo(user);\n\t\t},\n\n\t\tasync getByEmail(email: string): Promise<UserInfo | null> {\n\t\t\tconst user = await userRepo.findByEmail(email);\n\t\t\tif (!user) return null;\n\t\t\treturn toUserInfo(user);\n\t\t},\n\n\t\tasync list(opts?: {\n\t\t\trole?: number;\n\t\t\tlimit?: number;\n\t\t\tcursor?: string;\n\t\t}): Promise<{ items: UserInfo[]; nextCursor?: string }> {\n\t\t\tconst result = await userRepo.findMany({\n\t\t\t\trole: opts?.role as 10 | 20 | 30 | 40 | 50 | undefined,\n\t\t\t\tcursor: opts?.cursor,\n\t\t\t\tlimit: opts?.limit,\n\t\t\t});\n\n\t\t\treturn {\n\t\t\t\titems: result.items.map(toUserInfo),\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t};\n\t\t},\n\t};\n}\n\n// =============================================================================\n// Plugin Context Factory\n// =============================================================================\n\nexport interface PluginContextFactoryOptions {\n\tdb: Kysely<Database>;\n\tbeforeContentWrite?: () => Promise<void>;\n\t/**\n\t * Resolver for the database connection, preferred over `db` when present.\n\t * Called per `createContext()` so connection-backed adapters (e.g. Postgres\n\t * over Hyperdrive) get the current request/event-scoped connection from ALS\n\t * rather than a snapshot of the per-isolate singleton — reusing the\n\t * singleton's socket from a later event trips workerd's cross-request I/O\n\t * guard. When omitted, `db` is used directly (correct for stateless\n\t * adapters like D1 and Node SQLite). `db` remains required as the fallback.\n\t */\n\tgetDb?: () => Kysely<Database>;\n\t/**\n\t * Storage backend for direct media uploads.\n\t * If not provided, upload() will throw.\n\t */\n\tstorage?: Storage;\n\t/**\n\t * Explicit provider for `ctx.media.getUploadUrl()`. Optional: when omitted\n\t * but `storage` is configured, the factory derives a working `getUploadUrl()`\n\t * (and `upload()`) from storage. Only when neither `getUploadUrl` nor\n\t * `storage` is present do media write operations become unavailable.\n\t */\n\tgetUploadUrl?: (\n\t\tfilename: string,\n\t\tcontentType: string,\n\t) => Promise<{ uploadUrl: string; mediaId: string }>;\n\t/**\n\t * Site information for ctx.site and ctx.url().\n\t * If not provided, site info will have empty defaults.\n\t */\n\tsiteInfo?: SiteInfoOptions;\n\t/**\n\t * Callback to notify the cron scheduler that the next due time may have changed.\n\t * If not provided, ctx.cron will not be available.\n\t */\n\tcronReschedule?: () => void;\n\t/**\n\t * Email pipeline instance for ctx.email.\n\t * If not provided (or no provider configured), ctx.email will be undefined.\n\t */\n\temailPipeline?: EmailPipeline;\n\t/**\n\t * Pre-resolved list of trusted proxy header names (from the runtime\n\t * `EmDashConfig.trustedProxyHeaders` or the env var). Plugin route\n\t * handlers pass this to `extractRequestMeta` so plugins see the same\n\t * client IP the core auth path does.\n\t */\n\ttrustedProxyHeaders?: string[];\n}\n\n/**\n * Factory for creating plugin contexts\n */\nexport class PluginContextFactory {\n\tprivate resolveDb: () => Kysely<Database>;\n\tprivate beforeContentWrite?: () => Promise<void>;\n\tprivate storage?: Storage;\n\tprivate getUploadUrl?: (\n\t\tfilename: string,\n\t\tcontentType: string,\n\t) => Promise<{ uploadUrl: string; mediaId: string }>;\n\tprivate site: SiteInfo;\n\tprivate urlHelper: (path: string) => string;\n\tprivate cronReschedule?: () => void;\n\tprivate emailPipeline?: EmailPipeline;\n\t/**\n\t * Plugin IDs already warned about a missing media-write backend, so the\n\t * warning fires once per factory instead of on every hook/route context\n\t * creation (which would spam logs for hook-participating plugins).\n\t */\n\tprivate warnedMissingMediaBackend = new Set<string>();\n\n\tconstructor(options: PluginContextFactoryOptions) {\n\t\tconst fixedDb = options.db;\n\t\tthis.resolveDb = options.getDb ?? (() => fixedDb);\n\t\tthis.beforeContentWrite = options.beforeContentWrite;\n\t\tthis.storage = options.storage;\n\t\tthis.getUploadUrl = options.getUploadUrl;\n\t\tthis.site = createSiteInfo(options.siteInfo ?? {});\n\t\tthis.urlHelper = createUrlHelper(this.site.url);\n\t\tthis.cronReschedule = options.cronReschedule;\n\t\tthis.emailPipeline = options.emailPipeline;\n\t}\n\n\t/**\n\t * Create the unified plugin context\n\t */\n\tcreateContext(plugin: ResolvedPlugin): PluginContext {\n\t\tconst capabilities = new Set(plugin.capabilities);\n\n\t\t// Resolve the connection once per context. For stateless adapters this\n\t\t// is the singleton; for connection-backed adapters it's the current\n\t\t// request/event-scoped connection from ALS. All repos below are built\n\t\t// from this local `db` so a hook never queries a stale singleton socket.\n\t\tconst db = this.resolveDb();\n\t\tconst optionsRepo = new OptionsRepository(db);\n\n\t\t// Always available\n\t\tconst kv = createKVAccess(optionsRepo, plugin.id);\n\t\tconst log = createLogAccess(plugin.id);\n\t\tconst storage = createStorageAccess(db, plugin.id, plugin.storage);\n\n\t\t// Capability-gated: content\n\t\t// Note: capabilities reach this point already normalized to the\n\t\t// canonical names by definePlugin / adaptSandboxEntry. Deprecated\n\t\t// names (\"read:content\", \"write:content\") never appear here.\n\t\tlet content: ContentAccess | ContentAccessWithWrite | undefined;\n\t\tif (capabilities.has(\"content:write\")) {\n\t\t\tcontent = createContentAccessWithWrite(db, this.beforeContentWrite);\n\t\t} else if (capabilities.has(\"content:read\")) {\n\t\t\tcontent = createContentAccess(db);\n\t\t}\n\n\t\t// Capability-gated: taxonomies (read-only)\n\t\tlet taxonomies: TaxonomyAccess | undefined;\n\t\tif (capabilities.has(\"taxonomies:read\")) {\n\t\t\ttaxonomies = createTaxonomyAccess(db);\n\t\t}\n\n\t\t// Capability-gated: media\n\t\t// `upload()` only needs `storage`; `getUploadUrl()` is derived from\n\t\t// storage when no explicit provider is wired. Granting write access on\n\t\t// either avoids silently degrading media:write to read-only — the bug\n\t\t// where the runtime threads `storage` but not `getUploadUrl`.\n\t\tlet media: MediaAccess | MediaAccessWithWrite | undefined;\n\t\tif (capabilities.has(\"media:write\")) {\n\t\t\tif (this.getUploadUrl || this.storage) {\n\t\t\t\tmedia = createMediaAccessWithWrite(db, this.getUploadUrl, this.storage);\n\t\t\t} else {\n\t\t\t\tif (!this.warnedMissingMediaBackend.has(plugin.id)) {\n\t\t\t\t\tthis.warnedMissingMediaBackend.add(plugin.id);\n\t\t\t\t\tlog.warn(\n\t\t\t\t\t\t\"declares the media:write capability but no storage backend is configured; upload() is unavailable.\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tif (capabilities.has(\"media:read\")) {\n\t\t\t\t\tmedia = createMediaAccess(db);\n\t\t\t\t}\n\t\t\t}\n\t\t} else if (capabilities.has(\"media:read\")) {\n\t\t\tmedia = createMediaAccess(db);\n\t\t}\n\n\t\t// Capability-gated: http\n\t\tlet http: HttpAccess | undefined;\n\t\tif (capabilities.has(\"network:request:unrestricted\")) {\n\t\t\thttp = createUnrestrictedHttpAccess(plugin.id);\n\t\t} else if (capabilities.has(\"network:request\")) {\n\t\t\thttp = createHttpAccess(plugin.id, plugin.allowedHosts);\n\t\t}\n\n\t\t// Capability-gated: users\n\t\tlet users: UserAccess | undefined;\n\t\tif (capabilities.has(\"users:read\")) {\n\t\t\tusers = createUserAccess(db);\n\t\t}\n\n\t\t// Cron access — always available (scoped to plugin), but only if\n\t\t// the runtime provided a reschedule callback (i.e. cron is wired up).\n\t\tlet cron: CronAccess | undefined;\n\t\tif (this.cronReschedule) {\n\t\t\tcron = new CronAccessImpl(db, plugin.id, this.cronReschedule);\n\t\t}\n\n\t\t// Email access — requires email:send capability AND a configured provider\n\t\tlet email: EmailAccess | undefined;\n\t\tif (capabilities.has(\"email:send\") && this.emailPipeline?.isAvailable()) {\n\t\t\tconst pipeline = this.emailPipeline;\n\t\t\tconst pluginId = plugin.id;\n\t\t\temail = {\n\t\t\t\tsend: (message) => pipeline.send(message, pluginId),\n\t\t\t};\n\t\t}\n\n\t\treturn {\n\t\t\tplugin: {\n\t\t\t\tid: plugin.id,\n\t\t\t\tversion: plugin.version,\n\t\t\t},\n\t\t\tstorage,\n\t\t\tkv,\n\t\t\tcontent,\n\t\t\ttaxonomies,\n\t\t\tmedia,\n\t\t\thttp,\n\t\t\tlog,\n\t\t\tsite: this.site,\n\t\t\turl: this.urlHelper,\n\t\t\tusers,\n\t\t\tcron,\n\t\t\temail,\n\t\t};\n\t}\n}\n\n/**\n * Create a plugin context for a resolved plugin\n */\nexport function createPluginContext(\n\toptions: PluginContextFactoryOptions,\n\tplugin: ResolvedPlugin,\n): PluginContext {\n\tconst factory = new PluginContextFactory(options);\n\treturn factory.createContext(plugin);\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"dialect-helpers-BSbfaO3F.mjs","names":[],"sources":["../src/database/dialect-helpers.ts"],"sourcesContent":["/**\n * Dialect-specific SQL helpers\n *\n * Every function takes a Kysely `db` instance and detects the dialect from\n * the adapter class. No module-level state, no globals, no heuristics —\n * the adapter is the source of truth.\n *\n * This is NOT an ORM abstraction — just targeted helpers for the ~15 places\n * that use raw dialect-specific SQL. Most Kysely schema builder code already\n * works cross-dialect.\n */\n\nimport type { ColumnDataType, Kysely, RawBuilder } from \"kysely\";\nimport { PostgresAdapter, sql } from \"kysely\";\n\nimport type { DatabaseDialectType } from \"../db/adapters.js\";\nimport { validateIdentifier, validateJsonFieldName } from \"./validate.js\";\n\nexport type { DatabaseDialectType };\n\n/**\n * Detect dialect type from a Kysely instance via the adapter class.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function detectDialect(db: Kysely<any>): DatabaseDialectType {\n\tif (db.getExecutor().adapter instanceof PostgresAdapter) return \"postgres\";\n\treturn \"sqlite\";\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function isSqlite(db: Kysely<any>): boolean {\n\treturn detectDialect(db) === \"sqlite\";\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function isPostgres(db: Kysely<any>): boolean {\n\treturn detectDialect(db) === \"postgres\";\n}\n\n/**\n * Declared by an adapter whose backend caps the number of terms in a compound\n * SELECT (`UNION ALL`, `INTERSECT`, `EXCEPT`). SQLite's own\n * SQLITE_LIMIT_COMPOUND_SELECT default is 500 — high enough that no query\n * EmDash builds approaches it — but Cloudflare D1 sets it to 5 and rejects\n * anything larger with \"too many terms in compound SELECT\".\n */\nexport interface CompoundSelectLimitedAdapter {\n\t/** Maximum terms per compound SELECT. Must be a positive integer. */\n\treadonly compoundSelectLimit: number;\n}\n\n/**\n * The backend's compound-SELECT ceiling, or null when it has none worth\n * splitting statements for. Only the adapter knows: the limit is a property of\n * the SQLite build behind the dialect, not of the SQL flavour, so two \"sqlite\"\n * dialects can answer differently.\n *\n * A declared ceiling must be a positive integer — callers batch by it, and\n * every other value silently misbehaves rather than failing: 0 and negatives\n * never advance the batch cursor, fractions overlap batches and double-count,\n * NaN yields an empty batch. A malformed declaration throws here, where the\n * message can name the adapter.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function compoundSelectLimit(db: Kysely<any>): number | null {\n\tconst adapter: object = db.getExecutor().adapter;\n\tif (!(\"compoundSelectLimit\" in adapter)) return null;\n\n\tconst limit: unknown = adapter.compoundSelectLimit;\n\tif (typeof limit !== \"number\" || !Number.isInteger(limit) || limit < 1) {\n\t\tthrow new Error(\n\t\t\t`${adapter.constructor.name} declares compoundSelectLimit ${String(limit)}; it must be a positive integer.`,\n\t\t);\n\t}\n\treturn limit;\n}\n\n/**\n * Default timestamp expression for column defaults.\n * Wrapped in parens for use in CREATE TABLE ... DEFAULT (...).\n *\n * sqlite: (datetime('now'))\n * postgres: CURRENT_TIMESTAMP\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function currentTimestamp(db: Kysely<any>): RawBuilder<string> {\n\tif (isPostgres(db)) {\n\t\treturn sql`CURRENT_TIMESTAMP`;\n\t}\n\treturn sql`(datetime('now'))`;\n}\n\n/**\n * Timestamp expression for use in WHERE clauses and SET expressions.\n * No wrapping parens.\n *\n * sqlite: datetime('now')\n * postgres: CURRENT_TIMESTAMP\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function currentTimestampValue(db: Kysely<any>): RawBuilder<string> {\n\tif (isPostgres(db)) {\n\t\treturn sql`CURRENT_TIMESTAMP`;\n\t}\n\treturn sql`datetime('now')`;\n}\n\n/**\n * Build WHERE clause for status filtering on a content table.\n * Scheduled content becomes public only after the publication sweep commits\n * the row with a literal `published` status.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function buildStatusCondition(\n\t_db: Kysely<any>,\n\tstatus: string,\n\ttablePrefix?: string,\n): ReturnType<typeof sql> {\n\tconst statusField = tablePrefix ? `${tablePrefix}.status` : \"status\";\n\treturn sql`${sql.ref(statusField)} = ${status}`;\n}\n\n/**\n * Check if a table exists in the database.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport async function tableExists(db: Kysely<any>, tableName: string): Promise<boolean> {\n\tif (isPostgres(db)) {\n\t\t// Scope to the active schema (matches indexExists/columnExists below).\n\t\t// Hardcoding 'public' breaks non-public-schema Postgres deployments.\n\t\tconst result = await sql<{ exists: boolean }>`\n\t\t\tSELECT EXISTS(\n\t\t\t\tSELECT 1 FROM information_schema.tables\n\t\t\t\tWHERE table_schema = current_schema() AND table_name = ${tableName}\n\t\t\t) as exists\n\t\t`.execute(db);\n\t\treturn result.rows[0]?.exists === true;\n\t}\n\n\tconst result = await sql<{ name: string }>`\n\t\tSELECT name FROM sqlite_master\n\t\tWHERE type = 'table' AND name = ${tableName}\n\t`.execute(db);\n\treturn result.rows.length > 0;\n}\n\n/**\n * Check if an index exists in the database.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport async function indexExists(db: Kysely<any>, indexName: string): Promise<boolean> {\n\tif (isPostgres(db)) {\n\t\tconst result = await sql<{ exists: boolean }>`\n\t\t\tSELECT EXISTS(\n\t\t\t\tSELECT 1 FROM pg_indexes\n\t\t\t\tWHERE schemaname = current_schema() AND indexname = ${indexName}\n\t\t\t) as exists\n\t\t`.execute(db);\n\t\treturn result.rows[0]?.exists === true;\n\t}\n\n\tconst result = await sql<{ name: string }>`\n\t\tSELECT name FROM sqlite_master\n\t\tWHERE type = 'index' AND name = ${indexName}\n\t`.execute(db);\n\treturn result.rows.length > 0;\n}\n\n/**\n * Check if a column exists in the database.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport async function columnExists(\n\tdb: Kysely<any>,\n\ttableName: string,\n\tcolumnName: string,\n): Promise<boolean> {\n\tif (isPostgres(db)) {\n\t\tconst result = await sql<{ exists: boolean }>`\n\t\t\tSELECT EXISTS(\n\t\t\t\tSELECT 1 FROM information_schema.columns\n\t\t\t\tWHERE table_schema = current_schema()\n\t\t\t\t\tAND table_name = ${tableName}\n\t\t\t\t\tAND column_name = ${columnName}\n\t\t\t) as exists\n\t\t`.execute(db);\n\t\treturn result.rows[0]?.exists === true;\n\t}\n\n\tconst result = await sql<{ name: string }>`\n\t\tSELECT name FROM pragma_table_info(${tableName})\n\t\tWHERE name = ${columnName}\n\t`.execute(db);\n\treturn result.rows.length > 0;\n}\n\n/**\n * List tables matching a LIKE pattern.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport async function listTablesLike(db: Kysely<any>, pattern: string): Promise<string[]> {\n\tif (isPostgres(db)) {\n\t\t// Scope to the connection's active schema rather than hardcoding\n\t\t// 'public'. A Postgres deployment using a non-public schema (per-tenant\n\t\t// or shared-cluster setups), or per-test schemas, otherwise sees tables\n\t\t// from the wrong schema — or none at all. Mirrors migration 038.\n\t\tconst result = await sql<{ table_name: string }>`\n\t\t\tSELECT table_name FROM information_schema.tables\n\t\t\tWHERE table_schema = current_schema() AND table_name LIKE ${pattern}\n\t\t`.execute(db);\n\t\treturn result.rows.map((r) => r.table_name);\n\t}\n\n\tconst result = await sql<{ name: string }>`\n\t\tSELECT name FROM sqlite_master\n\t\tWHERE type = 'table' AND name LIKE ${pattern}\n\t`.execute(db);\n\treturn result.rows.map((r) => r.name);\n}\n\n/**\n * Column type for binary data.\n *\n * sqlite: blob\n * postgres: bytea\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function binaryType(db: Kysely<any>): ColumnDataType {\n\tif (isPostgres(db)) {\n\t\treturn \"bytea\";\n\t}\n\treturn \"blob\";\n}\n\n/**\n * SQL expression for extracting a field from a JSON column stored as text.\n *\n * sqlite: json_extract(column, '$.path')\n * postgres: (column)::jsonb->>'path'\n *\n * The Postgres cast is required because JSON columns (e.g.\n * `_plugin_storage.data`) are `text`, and `text ->> unknown` is not an\n * operator. The cast is immutable, so the same expression works in\n * expression indexes — queries and indexes must build it through this\n * helper so the planner can match them.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function jsonExtractExpr(db: Kysely<any>, column: string, path: string): string {\n\tvalidateIdentifier(column, \"JSON column name\");\n\tvalidateJsonFieldName(path, \"JSON path\");\n\tif (isPostgres(db)) {\n\t\treturn `(${column})::jsonb->>'${path}'`;\n\t}\n\treturn `json_extract(${column}, '$.${path}')`;\n}\n\n/**\n * SQL expression for extracting a queryable field from the plugin-storage\n * `data` column.\n *\n * `_plugin_storage.data` is `text`, so Postgres extraction goes through the\n * `(data)::jsonb->>'field'` cast (#1898) — otherwise `text ->> unknown` is not\n * an operator. But the extracted value is still `text`, so a numeric comparison\n * (`stock >= 10`) compares lexically (`'9' >= '10'` is TRUE) and silently\n * over-counts / oversells; pass `{ numeric: true }` for a numeric comparison.\n *\n * The numeric form is a **type-guarded** cast, not a bare `::numeric`. A bare\n * cast throws `invalid input syntax for type numeric` the moment a single\n * scanned row stores a non-number in that field (documents are schemaless),\n * aborting the whole query — and it would diverge from SQLite, which silently\n * coerces. Guarding with `jsonb_typeof`/`json_type` makes the comparison total\n * and parity-correct on both dialects: a non-number stored value yields `NULL`\n * (no match) instead of an error.\n *\n * The field name is validated before interpolation, so the casts wrap only a\n * safe identifier and add no injection surface.\n *\n * sqlite text: json_extract(data, '$.field')\n * sqlite numeric: CASE WHEN json_type(data, '$.field') IN ('integer', 'real')\n * THEN json_extract(data, '$.field') END\n * postgres text: (data)::jsonb->>'field'\n * postgres numeric: CASE WHEN jsonb_typeof((data)::jsonb->'field') = 'number'\n * THEN ((data)::jsonb->>'field')::numeric END\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function pluginDataExtractExpr(\n\tdb: Kysely<any>,\n\tfield: string,\n\toptions?: { numeric?: boolean },\n): string {\n\tvalidateJsonFieldName(field, \"plugin storage field name\");\n\tif (isPostgres(db)) {\n\t\tconst text = `(data)::jsonb->>'${field}'`;\n\t\tif (!options?.numeric) return text;\n\t\treturn `CASE WHEN jsonb_typeof((data)::jsonb->'${field}') = 'number' THEN (${text})::numeric END`;\n\t}\n\tconst extract = `json_extract(data, '$.${field}')`;\n\tif (!options?.numeric) return extract;\n\treturn `CASE WHEN json_type(data, '$.${field}') IN ('integer', 'real') THEN ${extract} END`;\n}\n\n/**\n * SQL expression for ordering plugin-storage rows by a `data` field.\n *\n * `ORDER BY` has no bound operand to infer numeric-vs-text from, so extracting\n * as text (`->>'field'`) would sort a numeric field lexically on Postgres\n * (`[10, 100, 9]`) while SQLite's `json_extract` sorts it numerically — a\n * cross-dialect divergence. Ordering over the jsonb-native value (`->'field'`,\n * single arrow) fixes this: jsonb btree ordering is numeric among numbers,\n * lexical among strings, and total across heterogeneous values (never throws).\n * SQLite's `json_extract` already orders numerically, so it is unchanged.\n *\n * sqlite: json_extract(data, '$.field')\n * postgres: (data)::jsonb->'field'\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance\nexport function pluginDataOrderExpr(db: Kysely<any>, field: string): string {\n\tvalidateJsonFieldName(field, \"plugin storage order field name\");\n\tif (isPostgres(db)) {\n\t\treturn `(data)::jsonb->'${field}'`;\n\t}\n\treturn `json_extract(data, '$.${field}')`;\n}\n"],"mappings":"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"}
|