emdash 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-C0ONzkUP.d.mts → adapters-34EN2eXO.d.mts} +34 -2
- package/dist/adapters-34EN2eXO.d.mts.map +1 -0
- package/dist/after-CBU2vkoO.mjs +96 -0
- package/dist/after-CBU2vkoO.mjs.map +1 -0
- package/dist/{allowed-origins-CCEi9bPI.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
- package/dist/{allowed-origins-CCEi9bPI.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -4
- package/dist/api/route-utils.d.mts.map +1 -1
- package/dist/api/route-utils.mjs +26 -25
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +6 -5
- package/dist/{api-DgPbMlng.mjs → api-DdAsirBe.mjs} +321 -428
- package/dist/api-DdAsirBe.mjs.map +1 -0
- package/dist/{api-tokens-p-iMzvXR.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
- package/dist/{api-tokens-p-iMzvXR.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
- package/dist/{apply-1_6ra7NP.mjs → apply-D5zfeD09.mjs} +139 -102
- package/dist/apply-D5zfeD09.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 +103 -35
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.mjs +9 -9
- package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
- package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
- package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
- package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +13 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +12 -11
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +1915 -223
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +20 -20
- package/dist/astro/routes/api/admin/bylines/index.mjs +20 -20
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -17
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/bulk.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/index.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
- package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
- 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 +4 -4
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +10 -9
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +9 -8
- package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +8 -8
- package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/complete.mjs +14 -13
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/register-options.mjs +13 -12
- package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/logout.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
- package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/me.mjs +10 -9
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +4 -4
- 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 +9 -8
- package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/options.mjs +15 -14
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +13 -12
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/verify.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/complete.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +20 -19
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +17 -16
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +15 -15
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
- 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 +21 -20
- 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 +21 -20
- 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 +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +47 -34
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
- package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
- package/dist/astro/routes/api/dashboard.mjs +12 -10
- package/dist/astro/routes/api/dashboard.mjs.map +1 -1
- package/dist/astro/routes/api/dev/emails.mjs +4 -4
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +22 -20
- package/dist/astro/routes/api/import/probe.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/execute.mjs +18 -18
- 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 +14 -12
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
- package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +15 -15
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +22 -20
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +32 -31
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +6 -6
- package/dist/astro/routes/api/mcp.d.mts.map +1 -1
- package/dist/astro/routes/api/mcp.mjs +174 -72
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +14 -12
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +7 -7
- package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
- package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +16 -15
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
- package/dist/astro/routes/api/media/providers/index.mjs +4 -4
- package/dist/astro/routes/api/media/upload-url.mjs +11 -10
- package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
- package/dist/astro/routes/api/media.mjs +19 -16
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/items.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/translations.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_.mjs +13 -13
- package/dist/astro/routes/api/menus/index.mjs +13 -13
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
- package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
- package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
- package/dist/astro/routes/api/oauth/register.mjs +4 -4
- package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
- package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
- package/dist/astro/routes/api/oauth/token.mjs +7 -7
- package/dist/astro/routes/api/openapi.json.mjs +183 -6
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/index.mjs +14 -13
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +14 -13
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +16 -14
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +16 -14
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +21 -20
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +16 -15
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +21 -20
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
- 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 +52 -46
- 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 +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.d.mts +8 -0
- package/dist/astro/routes/api/schema/collections/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -0
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/schema/index.mjs +14 -11
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.d.mts +1 -0
- package/dist/astro/routes/api/search/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +19 -15
- package/dist/astro/routes/api/search/enable.mjs.map +1 -1
- package/dist/astro/routes/api/search/index.mjs +13 -13
- package/dist/astro/routes/api/search/rebuild.mjs +15 -15
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
- package/dist/astro/routes/api/search/stats.mjs +9 -9
- package/dist/astro/routes/api/search/suggest.mjs +13 -13
- package/dist/astro/routes/api/sections/_slug_.mjs +13 -12
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +13 -12
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/export.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/index.mjs +9 -9
- package/dist/astro/routes/api/settings/email.mjs +6 -6
- package/dist/astro/routes/api/settings.mjs +18 -17
- package/dist/astro/routes/api/settings.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin-verify.mjs +15 -14
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin.mjs +14 -13
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -32
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
- package/dist/astro/routes/api/setup/index.mjs +36 -34
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +5 -5
- package/dist/astro/routes/api/snapshot.mjs +10 -10
- package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +44 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +21 -19
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
- package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
- package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/index.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +8 -8
- package/dist/astro/routes/api/typegen.mjs +6 -6
- 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 +10 -9
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +8 -7
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +4 -4
- package/dist/astro/routes/robots.txt.mjs +11 -10
- package/dist/astro/routes/robots.txt.mjs.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +14 -14
- package/dist/astro/routes/sitemap.xml.mjs +12 -11
- package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
- package/dist/astro/types.d.mts +41 -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-R5iYYrpa.mjs → authorize-1iFJzPaZ.mjs} +2 -2
- package/dist/{authorize-R5iYYrpa.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
- package/dist/{backup-LpQ1V_sR.mjs → backup-LHjIAVNP.mjs} +5 -5
- package/dist/{backup-LpQ1V_sR.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
- package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
- package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
- package/dist/bundle-CBEXrBnb.mjs +663 -0
- package/dist/bundle-CBEXrBnb.mjs.map +1 -0
- package/dist/{byline-fields-T9ZLZMxJ.mjs → byline-fields-C5S8wygO.mjs} +2 -2
- package/dist/{byline-fields-T9ZLZMxJ.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
- package/dist/{byline-C5TAqs8N.mjs → byline-pEcoDps8.mjs} +9 -9
- package/dist/{byline-C5TAqs8N.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
- package/dist/{byline-registry-BCuOp4UF.mjs → byline-registry-DDun8sxh.mjs} +4 -4
- package/dist/{byline-registry-BCuOp4UF.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
- package/dist/{bylines-B8-WGdja.mjs → bylines-BWemdX2k.mjs} +8 -8
- package/dist/{bylines-B8-WGdja.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
- package/dist/{bylines-DGuqBLjV.mjs → bylines-DJfSfXCL.mjs} +5 -5
- package/dist/{bylines-DGuqBLjV.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
- package/dist/cache-CBJaDy_E.mjs +108 -0
- package/dist/cache-CBJaDy_E.mjs.map +1 -0
- package/dist/{challenge-store-BFzgFRog.mjs → challenge-store-CZyFVaad.mjs} +1 -1
- package/dist/{challenge-store-BFzgFRog.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
- package/dist/{chunks-D5dlPeRb.mjs → chunks-DfPax1I0.mjs} +2 -2
- package/dist/{chunks-D5dlPeRb.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
- package/dist/cli/index.mjs +193 -707
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +74 -4
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +25 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/collection-deletion-C8BugSGz.mjs +779 -0
- package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
- package/dist/{comment-DnTxxVHv.mjs → comment-Dxlep6cE.mjs} +3 -3
- package/dist/{comment-DnTxxVHv.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
- package/dist/{comment-reaction-BUlUz2bh.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
- package/dist/{comment-reaction-BUlUz2bh.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
- package/dist/{comments-QPKgK3nk.mjs → comments-CTOuhTSX.mjs} +3 -3
- package/dist/{comments-QPKgK3nk.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
- package/dist/{components-DZkL4Fx0.mjs → components-CQy3lh9x.mjs} +1 -1
- package/dist/{components-DZkL4Fx0.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
- package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
- package/dist/config-C3EBstHi.mjs.map +1 -0
- package/dist/{content-CpfKV9QE.mjs → content-BNfwh7xX.mjs} +667 -183
- package/dist/content-BNfwh7xX.mjs.map +1 -0
- package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
- package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
- package/dist/content-refresh-DNERP4-f.mjs +2560 -0
- package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
- package/dist/{context-kO4YZB03.mjs → context-DRgOvSnW.mjs} +28 -20
- package/dist/context-DRgOvSnW.mjs.map +1 -0
- package/dist/{cron-BlKIMD_e.mjs → cron-Y9aLtgrA.mjs} +1 -1
- package/dist/{cron-BlKIMD_e.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
- package/dist/{dashboard-DoBwPnON.mjs → dashboard-CB8J89rJ.mjs} +45 -11
- package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -4
- package/dist/db/index.mjs +3 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
- package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
- package/dist/{default-C4v9KP56.mjs → default-DdDl3fxv.mjs} +1 -1
- package/dist/{default-C4v9KP56.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
- package/dist/deferred-tasks-BMsa9F64.d.mts +9 -0
- package/dist/deferred-tasks-BMsa9F64.d.mts.map +1 -0
- package/dist/{device-flow-DhUsyJU6.mjs → device-flow-CaioYGK3.mjs} +5 -5
- package/dist/{device-flow-DhUsyJU6.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
- package/dist/{dialect-helpers-DFwyWjTM.mjs → dialect-helpers-BSbfaO3F.mjs} +24 -16
- package/dist/dialect-helpers-BSbfaO3F.mjs.map +1 -0
- package/dist/dist-D_6CKR8z.mjs +810 -0
- package/dist/dist-D_6CKR8z.mjs.map +1 -0
- package/dist/{email-console-C-9Ng8DM.mjs → email-console-BHNjIwiB.mjs} +1 -1
- package/dist/{email-console-C-9Ng8DM.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
- package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
- package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
- package/dist/{error-DmmN74gW.mjs → error-DupyJZE2.mjs} +3 -3
- package/dist/{error-DmmN74gW.mjs.map → error-DupyJZE2.mjs.map} +1 -1
- package/dist/{errors-DtEXIQQV.mjs → errors-CLUPm7aD.mjs} +15 -2
- package/dist/errors-CLUPm7aD.mjs.map +1 -0
- package/dist/{escape-CdW31WsG.mjs → escape-C_aLKP3H.mjs} +1 -1
- package/dist/{escape-CdW31WsG.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
- package/dist/{field-defs-cache-QMVnzTH6.mjs → field-defs-cache-hg6QkjV9.mjs} +5 -5
- package/dist/{field-defs-cache-QMVnzTH6.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
- package/dist/{fts-manager-vncsZIJm.mjs → fts-manager-CZ-5dVwJ.mjs} +111 -61
- package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
- package/dist/{hash-Cb7U1b5M.mjs → hash-Cg0TXJPV.mjs} +1 -1
- package/dist/{hash-Cb7U1b5M.mjs.map → hash-Cg0TXJPV.mjs.map} +1 -1
- package/dist/{hreflang-CZbSx6Bg.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
- package/dist/{hreflang-CZbSx6Bg.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
- package/dist/{hreflang-Bg-zMpkr.mjs → hreflang-DNswoYsi.mjs} +7 -7
- package/dist/{hreflang-Bg-zMpkr.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
- package/dist/{import-PKhLeXvn.mjs → import-B41u8Rot.mjs} +7 -7
- package/dist/{import-PKhLeXvn.mjs.map → import-B41u8Rot.mjs.map} +1 -1
- package/dist/{index-j9rTh3cp.d.mts → index-C3Y-rsyN.d.mts} +286 -93
- package/dist/index-C3Y-rsyN.d.mts.map +1 -0
- package/dist/{index-BeNto_Vs.d.mts → index-Cof4-Fab.d.mts} +5 -10
- package/dist/index-Cof4-Fab.d.mts.map +1 -0
- package/dist/index.d.mts +20 -19
- package/dist/index.mjs +79 -71
- package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
- package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
- package/dist/{load-BwTdWE8B.mjs → load-DmvJrtxS.mjs} +2 -2
- package/dist/{load-BwTdWE8B.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
- package/dist/{loader-C1XOLV5b.mjs → loader-DLBqRiGP.mjs} +120 -64
- package/dist/loader-DLBqRiGP.mjs.map +1 -0
- package/dist/{manifest-schema-bCq54i7F.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
- package/dist/{manifest-schema-bCq54i7F.mjs.map → manifest-schema-BXbUAHod.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 -2
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +12 -12
- package/dist/media/local-runtime.mjs +10 -9
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-DWIzoVFL.mjs → media-BWWWb8kg.mjs} +92 -3
- package/dist/media-BWWWb8kg.mjs.map +1 -0
- package/dist/{media-tQDZEdu7.mjs → media-CzhAn2--.mjs} +2 -2
- package/dist/{media-tQDZEdu7.mjs.map → media-CzhAn2--.mjs.map} +1 -1
- package/dist/{media-allowlist-B7TPD6aq.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
- package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
- package/dist/{media-upload-Bc9Nc5Zf.mjs → media-upload-9yas9zGH.mjs} +11 -9
- package/dist/{media-upload-Bc9Nc5Zf.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
- package/dist/{media-url-BCm5vBn6.mjs → media-url-BvoX88FA.mjs} +1 -1
- package/dist/{media-url-BCm5vBn6.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
- package/dist/{media-usage-DPJcNaAq.mjs → media-usage-De3qvwjc.mjs} +138 -61
- package/dist/media-usage-De3qvwjc.mjs.map +1 -0
- package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
- package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
- package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
- package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
- package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
- package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
- package/dist/{menus-C6qhqP4X.mjs → menus-CC-yn1F9.mjs} +554 -121
- package/dist/menus-CC-yn1F9.mjs.map +1 -0
- package/dist/{menus-BxPLFfIc.mjs → menus-fxvEDYE6.mjs} +5 -5
- package/dist/{menus-BxPLFfIc.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
- package/dist/{mode-fiXRMfeA.mjs → mode-Bpe-tNNy.mjs} +1 -1
- package/dist/{mode-fiXRMfeA.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
- package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
- package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
- package/dist/{oauth-authorization-C9tFinly.mjs → oauth-authorization-BXO1MKej.mjs} +5 -5
- package/dist/{oauth-authorization-C9tFinly.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
- package/dist/{oauth-clients-BwTxE1aE.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
- package/dist/{oauth-clients-BwTxE1aE.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
- package/dist/{oauth-state-store-DaRUwPWO.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
- package/dist/{oauth-state-store-DaRUwPWO.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-CzuzOSlF.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
- package/dist/{oauth-user-lookup-CzuzOSlF.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{object-cache-Bok5j2ae.mjs → object-cache-71tPfV4A.mjs} +106 -5
- package/dist/object-cache-71tPfV4A.mjs.map +1 -0
- package/dist/{options-BlmBHTvX.mjs → options-CBXIHUoT.mjs} +1 -1
- package/dist/{options-BlmBHTvX.mjs.map → options-CBXIHUoT.mjs.map} +1 -1
- package/dist/{options-aeIs8WFu.d.mts → options-DRe1pIBL.d.mts} +4 -3
- package/dist/options-DRe1pIBL.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-BL49yb9D.mjs → parse-DFbuc-Fh.mjs} +2 -2
- package/dist/{parse-BL49yb9D.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
- package/dist/{passkey-config-KMhgFjdQ.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
- package/dist/{passkey-config-KMhgFjdQ.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
- package/dist/{placeholder-Dxo5Zt58.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
- package/dist/{placeholder-Dxo5Zt58.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +8 -1
- 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 +7 -4
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/{portable-text-CNahFfj6.mjs → portable-text-DhKbDAGr.mjs} +1 -1
- package/dist/{portable-text-CNahFfj6.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
- package/dist/{preview-D4Jnbfx7.mjs → preview-PW-nOnq4.mjs} +2 -2
- package/dist/{preview-D4Jnbfx7.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
- package/dist/{public-url-DSGTnJFw.mjs → public-url-CWoFhQT8.mjs} +14 -3
- package/dist/public-url-CWoFhQT8.mjs.map +1 -0
- package/dist/{query-DCiXI7OZ.mjs → query-BgjfiLUQ.mjs} +38 -77
- package/dist/query-BgjfiLUQ.mjs.map +1 -0
- package/dist/{query-1xOcEy18.mjs → query-vgZNwIhh.mjs} +4 -4
- package/dist/{query-1xOcEy18.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
- package/dist/{rate-limit-Vkp9gQ1Y.mjs → rate-limit-L5anykgo.mjs} +2 -2
- package/dist/{rate-limit-Vkp9gQ1Y.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
- package/dist/{redirect-BJ6d1yHh.mjs → redirect-COsS6SWJ.mjs} +3 -3
- package/dist/{redirect-BJ6d1yHh.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
- package/dist/{redirect-CDgfv9vh.mjs → redirect-KBNE3UEs.mjs} +1 -1
- package/dist/{redirect-CDgfv9vh.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
- package/dist/{redirects-Cwkq2H-G.mjs → redirects-CufRQt0Q.mjs} +5 -5
- package/dist/{redirects-Cwkq2H-G.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
- package/dist/{redirects-BOn2DIUs.mjs → redirects-cV9g7V-0.mjs} +149 -16
- package/dist/redirects-cV9g7V-0.mjs.map +1 -0
- package/dist/registry-C581V4TP.mjs +1275 -0
- package/dist/registry-C581V4TP.mjs.map +1 -0
- package/dist/{relation-DJ5DZh7B.mjs → relation-DtmAQH7s.mjs} +3 -3
- package/dist/{relation-DJ5DZh7B.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
- package/dist/{relations-C4duJnwI.mjs → relations-BBApI4e2.mjs} +3 -3
- package/dist/{relations-C4duJnwI.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
- package/dist/{relations-DpTjftCl.mjs → relations-DICSyfsh.mjs} +6 -6
- package/dist/{relations-DpTjftCl.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
- package/dist/{relations-DHFQGX0h.d.mts → relations-DItI3MN9.d.mts} +401 -40
- package/dist/relations-DItI3MN9.d.mts.map +1 -0
- package/dist/request-context.d.mts +3 -0
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{request-meta-DzXYYI-n.mjs → request-meta-DgZJh3zG.mjs} +2 -2
- package/dist/{request-meta-DzXYYI-n.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
- package/dist/{resolve-BUvFE0Lr.mjs → resolve-DOhVEvty.mjs} +2 -2
- package/dist/{resolve-BUvFE0Lr.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
- package/dist/{runner-BsI18UgP.mjs → runner-BiDvq5dv.mjs} +1404 -235
- package/dist/runner-BiDvq5dv.mjs.map +1 -0
- package/dist/{runner-CHdkERbt.d.mts → runner-CgQbELYC.d.mts} +2 -2
- package/dist/{runner-CHdkERbt.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +5 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/{schema-eqZhkR0S.mjs → schema-BFyTg38T.mjs} +3 -3
- package/dist/{schema-eqZhkR0S.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
- package/dist/schema-CeQSNwB3.mjs +390 -0
- package/dist/schema-CeQSNwB3.mjs.map +1 -0
- package/dist/{scopes-Be2d8bSb.mjs → scopes-BVeuhGBE.mjs} +3 -3
- package/dist/{scopes-Be2d8bSb.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
- package/dist/{search-CeO678cp.mjs → search-BpuPKZb0.mjs} +26 -16
- package/dist/search-BpuPKZb0.mjs.map +1 -0
- package/dist/{secrets-870d-7yA.mjs → secrets-dtYs2hDo.mjs} +5 -5
- package/dist/{secrets-870d-7yA.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
- package/dist/{sections-DSuEfirn.mjs → sections-COctk6hf.mjs} +3 -3
- package/dist/{sections-DSuEfirn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +28 -26
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +20 -19
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-C3GDfT0V.mjs → seo-AFpVvTqr.mjs} +6 -4
- package/dist/seo-AFpVvTqr.mjs.map +1 -0
- package/dist/{seo-BAPKiED8.mjs → seo-CvDxfvFb.mjs} +3 -3
- package/dist/{seo-BAPKiED8.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
- package/dist/{service-DyFT-k-R.mjs → service-xMiMSTsR.mjs} +4 -4
- package/dist/{service-DyFT-k-R.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
- package/dist/{session-user-BrK2zz7S.mjs → session-user-BzVemLkw.mjs} +2 -2
- package/dist/{session-user-BrK2zz7S.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
- package/dist/{settings-DgKouY2S.mjs → settings-B372MV0p.mjs} +7 -7
- package/dist/{settings-DgKouY2S.mjs.map → settings-B372MV0p.mjs.map} +1 -1
- package/dist/{settings-CREKyj8I.mjs → settings-BE9aqn_9.mjs} +3 -3
- package/dist/{settings-CREKyj8I.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
- package/dist/{setup-complete-L7bMK2VI.mjs → setup-complete-DF7TAfD5.mjs} +2 -2
- package/dist/{setup-complete-L7bMK2VI.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
- package/dist/{setup-nonce-Ct5tjWcq.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
- package/dist/{setup-nonce-Ct5tjWcq.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
- package/dist/{single-flight-cache-C2exrGAi.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
- package/dist/{single-flight-cache-C2exrGAi.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
- package/dist/site-url-Bowsi9T-.mjs +16 -0
- package/dist/site-url-Bowsi9T-.mjs.map +1 -0
- package/dist/slugify-B994zkXC.mjs +17 -0
- package/dist/slugify-B994zkXC.mjs.map +1 -0
- package/dist/{snapshot-BdpUJKD-.mjs → snapshot-B5h6aPMh.mjs} +2 -2
- package/dist/{snapshot-BdpUJKD-.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
- package/dist/{ssrf-CviKqWmq.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
- package/dist/{ssrf-CviKqWmq.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
- package/dist/{state-xxv6ZTMv.mjs → state-DYwPue40.mjs} +1 -1
- package/dist/{state-xxv6ZTMv.mjs.map → state-DYwPue40.mjs.map} +1 -1
- package/dist/{status-COfycGIU.mjs → status-CWVYpbD8.mjs} +1 -1
- package/dist/{status-COfycGIU.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{taxonomies-B61CRSha.mjs → taxonomies-B0o9ExNV.mjs} +85 -75
- package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
- package/dist/{taxonomies-Dbdki3Un.mjs → taxonomies-Dk3MAklL.mjs} +360 -56
- package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
- package/dist/taxonomy-CPeM8d2o.mjs +484 -0
- package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
- package/dist/{tokens-DVltpO2D.mjs → tokens-C39klX8R.mjs} +2 -2
- package/dist/{tokens-DVltpO2D.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
- package/dist/{transaction-D0FOsb3X.mjs → transaction-NQj4VJ7Z.mjs} +1 -1
- package/dist/{transaction-D0FOsb3X.mjs.map → transaction-NQj4VJ7Z.mjs.map} +1 -1
- package/dist/{transport-BWQo0Foy.mjs → transport--CfOflrp.mjs} +1 -1
- package/dist/{transport-BWQo0Foy.mjs.map → transport--CfOflrp.mjs.map} +1 -1
- package/dist/{transport-DokJOScS.d.mts → transport-B_rfeU-a.d.mts} +1 -1
- package/dist/{transport-DokJOScS.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
- package/dist/{trusted-proxy-CwjQj0YG.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
- package/dist/{trusted-proxy-CwjQj0YG.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
- package/dist/{types-o7xo7VgH.mjs → types-75KIqzL9.mjs} +20 -3
- package/dist/types-75KIqzL9.mjs.map +1 -0
- package/dist/{types-C9P3ysxq.d.mts → types-BPuloofb.d.mts} +1 -1
- package/dist/{types-C9P3ysxq.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
- package/dist/{types-BD40g7zz.mjs → types-BW1syrlw.mjs} +1 -1
- package/dist/{types-BD40g7zz.mjs.map → types-BW1syrlw.mjs.map} +1 -1
- package/dist/{types-jISEalTX.d.mts → types-C3_AbkDP.d.mts} +53 -10
- package/dist/types-C3_AbkDP.d.mts.map +1 -0
- package/dist/{types-84Bque3-.d.mts → types-CAGdePf3.d.mts} +133 -1
- package/dist/types-CAGdePf3.d.mts.map +1 -0
- package/dist/{types-CcSZCcF1.d.mts → types-CFUfyt1s.d.mts} +1 -1
- package/dist/{types-CcSZCcF1.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
- package/dist/{types-DQLZ58GV.d.mts → types-CPx2lIGo.d.mts} +1 -1
- package/dist/{types-DQLZ58GV.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
- package/dist/{types-XrQQ-Aex.mjs → types-CQPWhPxE.mjs} +15 -11
- package/dist/types-CQPWhPxE.mjs.map +1 -0
- package/dist/{types-BQY7VYw6.d.mts → types-Cs-TFLNf.d.mts} +28 -8
- package/dist/types-Cs-TFLNf.d.mts.map +1 -0
- package/dist/types-CtzytVXn.mjs +18 -0
- package/dist/types-CtzytVXn.mjs.map +1 -0
- package/dist/{types-DqZVPILm.d.mts → types-D4_1Ga4L.d.mts} +44 -5
- package/dist/types-D4_1Ga4L.d.mts.map +1 -0
- package/dist/{types-Dr2GLlwu.d.mts → types-DefV3OiO.d.mts} +1 -1
- package/dist/{types-Dr2GLlwu.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
- package/dist/{types-DsAMyD_o.d.mts → types-X0MThr7x.d.mts} +1 -1
- package/dist/{types-DsAMyD_o.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
- package/dist/{upload-attempts-C5yd6Gae.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
- package/dist/{upload-attempts-C5yd6Gae.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
- package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
- package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
- package/dist/{user-BAumEmpA.mjs → user-sHgJ62SP.mjs} +3 -3
- package/dist/{user-BAumEmpA.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
- package/dist/{utils-BbUgmeZG.mjs → utils-D4vD_Y98.mjs} +3 -3
- package/dist/{utils-BbUgmeZG.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
- package/dist/{validate-Bs_wT2ul.mjs → validate-BkqIG_Uj.mjs} +24 -5
- package/dist/validate-BkqIG_Uj.mjs.map +1 -0
- package/dist/{validate-VPnKoIzW.mjs → validate-BpwoDOgt.mjs} +25 -2
- package/dist/validate-BpwoDOgt.mjs.map +1 -0
- package/dist/{validate-CdYcY-Ln.d.mts → validate-DeGtBN84.d.mts} +25 -7
- package/dist/validate-DeGtBN84.d.mts.map +1 -0
- package/dist/{validation-BngR0nit.mjs → validation-CiJb_X5o.mjs} +14 -13
- package/dist/{validation-BngR0nit.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
- package/dist/version-x0NjTScG.mjs +7 -0
- package/dist/{version-Bdfb34kk.mjs.map → version-x0NjTScG.mjs.map} +1 -1
- package/dist/{widgets-L-VndTKD.mjs → widgets-Cwzvww0W.mjs} +3 -3
- package/dist/{widgets-L-VndTKD.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-DueaLFd9.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
- package/dist/{wxr-taxonomies-DueaLFd9.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
- package/dist/{wxr-taxonomies-BexjtNid.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
- package/dist/{wxr-taxonomies-BexjtNid.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
- package/dist/{zod-generator-DMzfga3g.mjs → zod-generator-D11MhxG4.mjs} +53 -6
- package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
- package/package.json +18 -7
- package/src/after.ts +6 -1
- package/src/api/errors.ts +13 -0
- package/src/api/handlers/content.ts +172 -33
- package/src/api/handlers/dashboard.ts +12 -2
- package/src/api/handlers/index.ts +16 -0
- package/src/api/handlers/manifest.ts +2 -0
- package/src/api/handlers/marketplace.ts +14 -0
- package/src/api/handlers/media-allowlist.ts +13 -2
- package/src/api/handlers/media-usage-work.ts +188 -0
- package/src/api/handlers/media-usage.ts +5 -3
- package/src/api/handlers/registry.ts +14 -0
- package/src/api/handlers/revision.ts +19 -5
- package/src/api/handlers/schema.ts +46 -3
- package/src/api/handlers/seo.ts +5 -2
- package/src/api/handlers/snapshot.ts +1 -1
- package/src/api/handlers/taxonomies.ts +446 -83
- package/src/api/media-usage-write-fence.ts +64 -0
- package/src/api/openapi/document.ts +247 -0
- package/src/api/public-url.ts +14 -2
- package/src/api/schemas/common.ts +3 -1
- package/src/api/schemas/content.ts +122 -1
- package/src/api/schemas/media-usage.ts +133 -0
- package/src/api/schemas/schema.ts +91 -3
- package/src/api/schemas/search.ts +2 -0
- package/src/api/schemas/taxonomies.ts +58 -8
- package/src/api/site-url.ts +16 -6
- package/src/api/types.ts +1 -0
- package/src/astro/integration/index.ts +55 -42
- package/src/astro/integration/routes.ts +48 -4
- package/src/astro/integration/runtime.ts +28 -0
- package/src/astro/integration/virtual-modules.ts +27 -1
- package/src/astro/integration/vite-config.ts +16 -1
- package/src/astro/middleware/media-usage-write-fence.ts +30 -0
- package/src/astro/middleware/redirect.ts +14 -12
- package/src/astro/middleware/scoped-db.ts +35 -4
- package/src/astro/middleware.ts +131 -30
- package/src/astro/routes/api/admin/comments/[id]/status.ts +1 -1
- package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
- package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
- package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
- package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +2 -2
- package/src/astro/routes/api/auth/invite/index.ts +2 -2
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -2
- package/src/astro/routes/api/auth/signup/request.ts +2 -2
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
- package/src/astro/routes/api/mcp.ts +1 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
- package/src/astro/routes/api/schema/collections/reorder.ts +31 -0
- package/src/astro/routes/api/schema/index.ts +2 -0
- package/src/astro/routes/api/search/enable.ts +5 -1
- package/src/astro/routes/api/search/rebuild.ts +6 -1
- package/src/astro/routes/api/taxonomies/[name]/reorder.ts +41 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
- package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
- package/src/astro/routes/api/taxonomies/[name].ts +102 -0
- package/src/astro/types.ts +31 -4
- package/src/cleanup.ts +25 -26
- package/src/cli/commands/dev.ts +13 -3
- package/src/cli/commands/doctor.ts +193 -6
- package/src/cli/commands/export-seed.ts +6 -1
- package/src/cli/index.ts +1 -1
- package/src/client/index.ts +119 -0
- package/src/components/Block.astro +12 -0
- package/src/components/CommentForm.astro +1 -1
- package/src/components/Comments.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +260 -32
- package/src/components/OrderedList.astro +14 -0
- package/src/components/PortableText.astro +26 -4
- package/src/components/index.ts +12 -1
- package/src/components/ordered-list.ts +549 -0
- package/src/content/converters/mark-safety.ts +120 -0
- package/src/content/converters/numbered-list.ts +148 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +84 -25
- package/src/content/converters/prosemirror-to-portable-text.ts +35 -14
- package/src/content/converters/types.ts +2 -0
- package/src/content/portable-text-lists.ts +205 -0
- package/src/content-list-query.ts +29 -0
- package/src/database/dialect-helpers.ts +3 -22
- package/src/database/migrations/055_content_translation_group_locale_index.ts +67 -0
- package/src/database/migrations/056_taxonomy_term_sort_order.ts +189 -0
- package/src/database/migrations/057_collection_hidden.ts +24 -0
- package/src/database/migrations/058_collection_sort_order.ts +20 -0
- package/src/database/migrations/059_revision_prune_queue.ts +36 -0
- package/src/database/migrations/060_collection_admin_config.ts +16 -0
- package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
- package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
- package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
- package/src/database/migrations/064_fts_plain_text.ts +253 -0
- package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
- package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
- package/src/database/migrations/067_indexed_content_fields.ts +39 -0
- package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
- package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
- package/src/database/migrations/070_collection_routable.ts +18 -0
- package/src/database/migrations/runner.ts +32 -0
- package/src/database/repositories/content.ts +1051 -273
- package/src/database/repositories/media-usage-work.ts +848 -0
- package/src/database/repositories/media-usage.ts +1502 -138
- package/src/database/repositories/revision.ts +98 -22
- package/src/database/repositories/taxonomy.ts +429 -103
- package/src/database/repositories/types.ts +58 -2
- package/src/database/types.ts +147 -17
- package/src/database/validate.ts +39 -0
- package/src/db/adapters.ts +36 -0
- package/src/db/index.ts +3 -0
- package/src/deferred-tasks.ts +71 -0
- package/src/emdash-runtime.ts +493 -110
- package/src/fields/file.ts +7 -4
- package/src/fields/image.ts +8 -1
- package/src/fields/types.ts +18 -5
- package/src/i18n/config.ts +32 -0
- package/src/i18n/repair-locale-casing.ts +0 -14
- package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
- package/src/import/wxr-taxonomies.ts +7 -47
- package/src/index.ts +27 -2
- package/src/loader.ts +165 -89
- package/src/mcp/server.ts +242 -34
- package/src/media/usage/activation.ts +626 -0
- package/src/media/usage/capture-triggers.ts +543 -0
- package/src/media/usage/cleanup.ts +305 -0
- package/src/media/usage/collection-deletion-processor.ts +356 -0
- package/src/media/usage/collection-deletion.ts +836 -0
- package/src/media/usage/content-fields.ts +40 -3
- package/src/media/usage/content-refresh.ts +356 -47
- package/src/media/usage/content-repair.ts +180 -61
- package/src/media/usage/content-snapshots.ts +85 -105
- package/src/media/usage/gc.ts +50 -0
- package/src/media/usage/projection-fingerprint.ts +129 -0
- package/src/media/usage/reconciliation-processor.ts +372 -0
- package/src/media/usage/reconciliation.ts +1117 -0
- package/src/media/usage/source-key.ts +4 -0
- package/src/media/usage/types.ts +2 -0
- package/src/media/usage/work-processor.ts +248 -0
- package/src/menus/index.ts +246 -141
- package/src/object-cache/index.ts +160 -3
- package/src/plugin-types.ts +8 -0
- package/src/plugins/adapt-sandbox-entry.ts +24 -5
- package/src/plugins/context.ts +24 -7
- package/src/plugins/index.ts +9 -0
- package/src/plugins/routes.ts +77 -1
- package/src/plugins/sandbox/index.ts +10 -1
- package/src/plugins/sandbox/types.ts +78 -1
- package/src/plugins/scheduler/node.ts +13 -1
- package/src/plugins/scheduler/types.ts +2 -0
- package/src/plugins/storage-indexes.ts +50 -11
- package/src/plugins/types.ts +39 -2
- package/src/query.ts +50 -79
- package/src/redirects/cache.ts +79 -19
- package/src/request-context.ts +3 -0
- package/src/scheduled-publish.ts +6 -1
- package/src/scheduler-health.ts +46 -0
- package/src/schema/registry.ts +863 -249
- package/src/schema/types.ts +74 -8
- package/src/schema/url-pattern.ts +33 -0
- package/src/schema/zod-generator.ts +77 -5
- package/src/search/fts-manager.ts +167 -86
- package/src/search/index.ts +2 -0
- package/src/search/query.ts +32 -11
- package/src/search/types.ts +8 -0
- package/src/seed/apply.ts +89 -16
- package/src/seed/types.ts +21 -3
- package/src/seed/validate.ts +52 -2
- package/src/taxonomies/index.ts +88 -82
- package/src/taxonomies/term-counts.ts +23 -12
- package/src/ui-comments.ts +19 -0
- package/src/ui.ts +11 -3
- package/src/utils/slugify.ts +1 -29
- package/src/virtual-modules.d.ts +24 -0
- package/dist/adapters-C0ONzkUP.d.mts.map +0 -1
- package/dist/after-B1IIdH3Y.mjs +0 -29
- package/dist/after-B1IIdH3Y.mjs.map +0 -1
- package/dist/api-DgPbMlng.mjs.map +0 -1
- package/dist/apply-1_6ra7NP.mjs.map +0 -1
- package/dist/cache-B26cufFd.mjs +0 -65
- package/dist/cache-B26cufFd.mjs.map +0 -1
- package/dist/config-DyF3OKg_.mjs.map +0 -1
- package/dist/content-CpfKV9QE.mjs.map +0 -1
- package/dist/content-refresh-DgqUBeTv.mjs +0 -1629
- package/dist/content-refresh-DgqUBeTv.mjs.map +0 -1
- package/dist/context-kO4YZB03.mjs.map +0 -1
- package/dist/dashboard-DoBwPnON.mjs.map +0 -1
- package/dist/dialect-helpers-DFwyWjTM.mjs.map +0 -1
- package/dist/errors-DtEXIQQV.mjs.map +0 -1
- package/dist/fts-manager-vncsZIJm.mjs.map +0 -1
- package/dist/index-BeNto_Vs.d.mts.map +0 -1
- package/dist/index-j9rTh3cp.d.mts.map +0 -1
- package/dist/loader-C1XOLV5b.mjs.map +0 -1
- package/dist/media-DWIzoVFL.mjs.map +0 -1
- package/dist/media-allowlist-B7TPD6aq.mjs.map +0 -1
- package/dist/media-usage-DPJcNaAq.mjs.map +0 -1
- package/dist/menus-C6qhqP4X.mjs.map +0 -1
- package/dist/object-cache-Bok5j2ae.mjs.map +0 -1
- package/dist/options-aeIs8WFu.d.mts.map +0 -1
- package/dist/public-url-DSGTnJFw.mjs.map +0 -1
- package/dist/query-DCiXI7OZ.mjs.map +0 -1
- package/dist/redirects-BOn2DIUs.mjs.map +0 -1
- package/dist/registry-BP1JK2xh.mjs +0 -795
- package/dist/registry-BP1JK2xh.mjs.map +0 -1
- package/dist/relations-DHFQGX0h.d.mts.map +0 -1
- package/dist/runner-BsI18UgP.mjs.map +0 -1
- package/dist/search-CeO678cp.mjs.map +0 -1
- package/dist/seo-C3GDfT0V.mjs.map +0 -1
- package/dist/site-url-8N3kSyVZ.mjs +0 -13
- package/dist/site-url-8N3kSyVZ.mjs.map +0 -1
- package/dist/slugify-C_tqlU4G.mjs +0 -30
- package/dist/slugify-C_tqlU4G.mjs.map +0 -1
- package/dist/taxonomies-B61CRSha.mjs.map +0 -1
- package/dist/taxonomies-Dbdki3Un.mjs.map +0 -1
- package/dist/taxonomy-DvwWAPvA.mjs +0 -304
- package/dist/taxonomy-DvwWAPvA.mjs.map +0 -1
- package/dist/types-84Bque3-.d.mts.map +0 -1
- package/dist/types-BQY7VYw6.d.mts.map +0 -1
- package/dist/types-DqZVPILm.d.mts.map +0 -1
- package/dist/types-XrQQ-Aex.mjs.map +0 -1
- package/dist/types-jISEalTX.d.mts.map +0 -1
- package/dist/types-o7xo7VgH.mjs.map +0 -1
- package/dist/validate-Bs_wT2ul.mjs.map +0 -1
- package/dist/validate-CdYcY-Ln.d.mts.map +0 -1
- package/dist/validate-VPnKoIzW.mjs.map +0 -1
- package/dist/version-Bdfb34kk.mjs +0 -7
- package/dist/zod-generator-DMzfga3g.mjs.map +0 -1
- /package/dist/{api-tokens-CEsW_jCg.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
- /package/dist/{ssrf-BB__xfNM.mjs → ssrf-DpXgKsna.mjs} +0 -0
- /package/dist/{types-CK0LxPUQ.mjs → types-CP1CflWf.mjs} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collection-deletion-C8BugSGz.mjs","names":[],"sources":["../src/media/usage/capture-triggers.ts","../src/media/usage/collection-deletion.ts"],"sourcesContent":["import { sql, type Kysely, type RawBuilder } from \"kysely\";\n\nimport { isPostgres, tableExists } from \"../../database/dialect-helpers.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\n\nconst POSTGRES_TRIGGER_FUNCTION = \"emdash_media_usage_capture_work\";\nconst POSTGRES_IDENTIFIER_LIMIT = 63;\nconst CAPTURE_TRIGGER_VERSION = 1;\nconst OWNED_TRIGGER_PREFIX = \"emdash_mu_\";\nconst WHITESPACE_PATTERN = /\\s+/g;\nconst TRAILING_SEMICOLON_PATTERN = /;$/;\n\nexport interface MediaUsageCaptureIdentity {\n\tcollectionId: string;\n\tcollectionSlug: string;\n}\n\ntype CaptureOperation = \"insert\" | \"update\" | \"delete\";\n\nexport async function installMediaUsageCaptureTriggers(\n\tdb: Kysely<Database>,\n\tidentity: MediaUsageCaptureIdentity,\n\toptions: { replaceExisting?: boolean } = {},\n): Promise<void> {\n\tconst identifiers = await captureIdentifiers(identity);\n\tif (isPostgres(db)) await installPostgresFunction(db);\n\tconst installedNames = await listOwnedCaptureTriggers(db, identifiers.tableName);\n\tif (await hasExactCaptureTriggers(db, identifiers, identity, installedNames)) {\n\t\treturn;\n\t}\n\n\tawait assertCaptureLifecycle(db, identity, [\"installing\"]);\n\tconst expectedNames = new Set(Object.values(identifiers.triggerNames));\n\tconst retainedNames = new Set(\n\t\toptions.replaceExisting === false\n\t\t\t? installedNames.filter((name) => expectedNames.has(name))\n\t\t\t: [],\n\t);\n\tawait removeCaptureTriggers(\n\t\tdb,\n\t\tidentifiers.tableName,\n\t\tinstalledNames.filter((name) => !retainedNames.has(name)),\n\t);\n\n\tfor (const operation of captureOperations) {\n\t\tif (retainedNames.has(identifiers.triggerNames[operation])) continue;\n\t\tif (isPostgres(db)) {\n\t\t\tawait postgresCreateTrigger(\n\t\t\t\tdb,\n\t\t\t\tidentifiers.tableName,\n\t\t\t\tidentifiers.triggerNames[operation],\n\t\t\t\toperation,\n\t\t\t\tidentity,\n\t\t\t);\n\t\t} else {\n\t\t\tawait sqliteCreateTrigger(\n\t\t\t\tdb,\n\t\t\t\tidentifiers.tableName,\n\t\t\t\tidentifiers.triggerNames[operation],\n\t\t\t\toperation,\n\t\t\t\tidentity,\n\t\t\t);\n\t\t}\n\t}\n\n\tawait assertExpectedTriggerSet(db, identifiers, identity);\n}\n\nexport async function verifyMediaUsageCaptureTriggers(\n\tdb: Kysely<Database>,\n\tidentity: MediaUsageCaptureIdentity,\n): Promise<boolean> {\n\tconst identifiers = await captureIdentifiers(identity);\n\tif (!(await tableExists(db, identifiers.tableName))) return false;\n\treturn hasExactCaptureTriggers(\n\t\tdb,\n\t\tidentifiers,\n\t\tidentity,\n\t\tawait listOwnedCaptureTriggers(db, identifiers.tableName),\n\t);\n}\n\nexport async function removeMediaUsageCaptureTriggers(\n\tdb: Kysely<Database>,\n\tidentity: MediaUsageCaptureIdentity,\n): Promise<void> {\n\tconst identifiers = await captureIdentifiers(identity);\n\tif (!(await tableExists(db, identifiers.tableName))) return;\n\tawait assertCaptureLifecycle(db, identity, [\"installing\", \"deleting\"]);\n\tawait removeCaptureTriggers(\n\t\tdb,\n\t\tidentifiers.tableName,\n\t\tawait listOwnedCaptureTriggers(db, identifiers.tableName),\n\t);\n}\n\nconst captureOperations: readonly CaptureOperation[] = [\"insert\", \"update\", \"delete\"];\n\nasync function captureIdentifiers(identity: MediaUsageCaptureIdentity): Promise<{\n\ttableName: string;\n\ttriggerNames: Record<CaptureOperation, string>;\n}> {\n\tvalidateIdentifier(identity.collectionSlug, \"collection slug\");\n\tconst tableName = `ec_${identity.collectionSlug}`;\n\tvalidateIdentifier(tableName, \"content table\");\n\n\tconst digest = await identityDigest(\n\t\t`${CAPTURE_TRIGGER_VERSION}:${identity.collectionId}:${identity.collectionSlug}`,\n\t);\n\tconst triggerNames = {\n\t\tinsert: `emdash_mu_${digest}_ai`,\n\t\tupdate: `emdash_mu_${digest}_au`,\n\t\tdelete: `emdash_mu_${digest}_ad`,\n\t};\n\tfor (const triggerName of Object.values(triggerNames)) {\n\t\tvalidateIdentifier(triggerName, \"media usage trigger\");\n\t\tif (triggerName.length > POSTGRES_IDENTIFIER_LIMIT) {\n\t\t\tthrow new Error(`Media usage trigger name exceeds ${POSTGRES_IDENTIFIER_LIMIT} bytes`);\n\t\t}\n\t}\n\n\treturn { tableName, triggerNames };\n}\n\nasync function identityDigest(value: string): Promise<string> {\n\tconst digest = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(value));\n\treturn Array.from(new Uint8Array(digest).slice(0, 16), (byte) =>\n\t\tbyte.toString(16).padStart(2, \"0\"),\n\t).join(\"\");\n}\n\nasync function removeCaptureTriggers(\n\tdb: Kysely<Database>,\n\ttableName: string,\n\ttriggerNames: readonly string[],\n): Promise<void> {\n\tfor (const triggerName of triggerNames) {\n\t\tvalidateIdentifier(triggerName, \"media usage trigger\");\n\t\tif (isPostgres(db)) {\n\t\t\tawait sql`\n\t\t\t\tDROP TRIGGER IF EXISTS ${sql.ref(triggerName)} ON ${sql.ref(tableName)}\n\t\t\t`.execute(db);\n\t\t} else {\n\t\t\tawait sql`DROP TRIGGER IF EXISTS ${sql.ref(triggerName)}`.execute(db);\n\t\t}\n\t}\n}\n\nasync function listOwnedCaptureTriggers(\n\tdb: Kysely<Database>,\n\ttableName: string,\n): Promise<string[]> {\n\tif (isPostgres(db)) {\n\t\tconst result = await sql<{ name: string }>`\n\t\t\tSELECT trigger.tgname AS name\n\t\t\tFROM pg_trigger AS trigger\n\t\t\tINNER JOIN pg_class AS relation ON relation.oid = trigger.tgrelid\n\t\t\tINNER JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace\n\t\t\tWHERE namespace.nspname = current_schema()\n\t\t\t\tAND relation.relname = ${tableName}\n\t\t\t\tAND NOT trigger.tgisinternal\n\t\t\t\tAND left(trigger.tgname, 10) = ${OWNED_TRIGGER_PREFIX}\n\t\t`.execute(db);\n\t\treturn result.rows.map((row) => row.name);\n\t}\n\n\tconst result = await sql<{ name: string }>`\n\t\tSELECT name\n\t\tFROM sqlite_master\n\t\tWHERE type = 'trigger'\n\t\t\tAND tbl_name = ${tableName}\n\t\t\tAND substr(name, 1, 10) = ${OWNED_TRIGGER_PREFIX}\n\t`.execute(db);\n\treturn result.rows.map((row) => row.name);\n}\n\nasync function hasExactCaptureTriggers(\n\tdb: Kysely<Database>,\n\tidentifiers: {\n\t\ttableName: string;\n\t\ttriggerNames: Record<CaptureOperation, string>;\n\t},\n\tidentity: MediaUsageCaptureIdentity,\n\tinstalledNames: readonly string[],\n): Promise<boolean> {\n\tconst expectedNames = new Set(Object.values(identifiers.triggerNames));\n\tif (\n\t\tinstalledNames.length !== expectedNames.size ||\n\t\tinstalledNames.some((name) => !expectedNames.has(name))\n\t) {\n\t\treturn false;\n\t}\n\n\tif (isPostgres(db)) {\n\t\tconst result = await sql<{\n\t\t\tname: string;\n\t\t\ttrigger_type: number;\n\t\t\tfunction_name: string;\n\t\t\targuments_hex: string;\n\t\t\tenabled: string;\n\t\t\thas_when: boolean;\n\t\t}>`\n\t\t\tSELECT\n\t\t\t\ttrigger.tgname AS name,\n\t\t\t\ttrigger.tgtype AS trigger_type,\n\t\t\t\tprocedure.proname AS function_name,\n\t\t\t\tencode(trigger.tgargs, 'hex') AS arguments_hex,\n\t\t\t\ttrigger.tgenabled AS enabled,\n\t\t\t\ttrigger.tgqual IS NOT NULL AS has_when\n\t\t\tFROM pg_trigger AS trigger\n\t\t\tINNER JOIN pg_class AS relation ON relation.oid = trigger.tgrelid\n\t\t\tINNER JOIN pg_namespace AS namespace ON namespace.oid = relation.relnamespace\n\t\t\tINNER JOIN pg_proc AS procedure ON procedure.oid = trigger.tgfoid\n\t\t\tINNER JOIN pg_namespace AS function_namespace ON function_namespace.oid = procedure.pronamespace\n\t\t\tWHERE namespace.nspname = current_schema()\n\t\t\t\tAND function_namespace.nspname = current_schema()\n\t\t\t\tAND relation.relname = ${identifiers.tableName}\n\t\t\t\tAND NOT trigger.tgisinternal\n\t\t\t\tAND trigger.tgconstraint = 0\n\t\t\t\tAND NOT trigger.tgdeferrable\n\t\t\t\tAND NOT trigger.tginitdeferred\n\t\t\t\tAND trigger.tgattr = ''::int2vector\n\t\t\t\tAND left(trigger.tgname, 10) = ${OWNED_TRIGGER_PREFIX}\n\t\t`.execute(db);\n\t\tconst expectedArguments = triggerArgumentsHex(identity);\n\t\treturn captureOperations.every((operation) => {\n\t\t\tconst row = result.rows.find(\n\t\t\t\t(candidate) => candidate.name === identifiers.triggerNames[operation],\n\t\t\t);\n\t\t\treturn (\n\t\t\t\trow?.function_name === POSTGRES_TRIGGER_FUNCTION &&\n\t\t\t\tNumber(row.trigger_type) === postgresTriggerType(operation) &&\n\t\t\t\trow.arguments_hex === expectedArguments &&\n\t\t\t\trow.enabled === \"O\" &&\n\t\t\t\trow.has_when === false\n\t\t\t);\n\t\t});\n\t}\n\n\tconst result = await sql<{ name: string; definition: string }>`\n\t\tSELECT name, sql AS definition\n\t\tFROM sqlite_master\n\t\tWHERE type = 'trigger'\n\t\t\tAND tbl_name = ${identifiers.tableName}\n\t\t\tAND substr(name, 1, 10) = ${OWNED_TRIGGER_PREFIX}\n\t`.execute(db);\n\treturn captureOperations.every((operation) => {\n\t\tconst row = result.rows.find(\n\t\t\t(candidate) => candidate.name === identifiers.triggerNames[operation],\n\t\t);\n\t\tconst contentId = operation === \"delete\" ? sql`OLD.id` : sql`NEW.id`;\n\t\tconst expected = sqliteTriggerSql(\n\t\t\tidentifiers.tableName,\n\t\t\tidentifiers.triggerNames[operation],\n\t\t\toperationSql(operation),\n\t\t\tcontentId,\n\t\t\tidentity,\n\t\t).compile(db).sql;\n\t\treturn row ? normalizeDdl(row.definition) === normalizeDdl(expected) : false;\n\t});\n}\n\nfunction triggerArgumentsHex(identity: MediaUsageCaptureIdentity): string {\n\tconst bytes = new TextEncoder().encode(`${identity.collectionId}\\0${identity.collectionSlug}\\0`);\n\treturn Array.from(bytes, (byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\nfunction postgresTriggerType(operation: CaptureOperation): number {\n\tswitch (operation) {\n\t\tcase \"insert\":\n\t\t\treturn 5;\n\t\tcase \"update\":\n\t\t\treturn 17;\n\t\tcase \"delete\":\n\t\t\treturn 9;\n\t}\n}\n\nfunction normalizeDdl(definition: string): string {\n\treturn definition.replace(WHITESPACE_PATTERN, \" \").trim().replace(TRAILING_SEMICOLON_PATTERN, \"\");\n}\n\nasync function assertCaptureLifecycle(\n\tdb: Kysely<Database>,\n\tidentity: MediaUsageCaptureIdentity,\n\tallowedStates: readonly string[],\n): Promise<void> {\n\tconst lifecycle = await db\n\t\t.selectFrom(\"_emdash_media_usage_index_status\")\n\t\t.select(\"capture_state\")\n\t\t.where(\"adapter_id\", \"=\", \"content-media\")\n\t\t.where(\"scope_type\", \"=\", \"collection\")\n\t\t.where(\"scope_key\", \"=\", identity.collectionSlug)\n\t\t.where(\"collection_id\", \"=\", identity.collectionId)\n\t\t.executeTakeFirst();\n\tif (!lifecycle?.capture_state || !allowedStates.includes(lifecycle.capture_state)) {\n\t\tthrow new Error(\"Media usage capture trigger changes require a fenced collection lifecycle\");\n\t}\n}\n\nasync function assertExpectedTriggerSet(\n\tdb: Kysely<Database>,\n\tidentifiers: {\n\t\ttableName: string;\n\t\ttriggerNames: Record<CaptureOperation, string>;\n\t},\n\tidentity: MediaUsageCaptureIdentity,\n): Promise<void> {\n\tconst actual = await listOwnedCaptureTriggers(db, identifiers.tableName);\n\tif (!(await hasExactCaptureTriggers(db, identifiers, identity, actual))) {\n\t\tthrow new Error(\"Media usage capture trigger installation is incomplete\");\n\t}\n}\n\nasync function installPostgresFunction(db: Kysely<Database>): Promise<void> {\n\tawait sql`\n\t\tCREATE OR REPLACE FUNCTION ${sql.ref(POSTGRES_TRIGGER_FUNCTION)}()\n\t\tRETURNS trigger\n\t\tLANGUAGE plpgsql\n\t\tAS $function$\n\t\tDECLARE\n\t\t\tv_collection_id text := TG_ARGV[0];\n\t\t\tv_collection_slug text := TG_ARGV[1];\n\t\t\tv_content_id text;\n\t\t\tv_epoch bigint;\n\t\t\tv_now text := to_char(\n\t\t\t\tclock_timestamp() AT TIME ZONE 'UTC',\n\t\t\t\t'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'\n\t\t\t);\n\t\tBEGIN\n\t\t\tIF TG_OP = 'DELETE' THEN\n\t\t\t\tv_content_id := OLD.id;\n\t\t\tELSE\n\t\t\t\tv_content_id := NEW.id;\n\t\t\tEND IF;\n\n\t\t\tUPDATE _emdash_media_usage_index_status\n\t\t\tSET change_epoch = change_epoch + 1,\n\t\t\t\tstatus = CASE WHEN status = 'complete' THEN 'stale' ELSE status END,\n\t\t\t\tcompleted_at = CASE WHEN status = 'complete' THEN NULL ELSE completed_at END,\n\t\t\t\tupdated_at = v_now\n\t\t\tWHERE adapter_id = 'content-media'\n\t\t\t\tAND scope_type = 'collection'\n\t\t\t\tAND scope_key = v_collection_slug\n\t\t\t\tAND collection_id = v_collection_id\n\t\t\t\tAND capture_state = 'active'\n\t\t\t\tAND EXISTS (\n\t\t\t\t\tSELECT 1\n\t\t\t\t\tFROM _emdash_collections AS collection\n\t\t\t\t\tWHERE collection.id = v_collection_id\n\t\t\t\t\t\tAND collection.slug = v_collection_slug\n\t\t\t\t)\n\t\t\tRETURNING change_epoch INTO v_epoch;\n\n\t\t\tIF NOT FOUND THEN\n\t\t\t\tRAISE EXCEPTION USING\n\t\t\t\t\tERRCODE = 'P0001',\n\t\t\t\t\tMESSAGE = 'media usage capture inactive';\n\t\t\tEND IF;\n\n\t\t\tINSERT INTO _emdash_media_usage_work (\n\t\t\t\tcollection_id,\n\t\t\t\tcollection_slug,\n\t\t\t\tcontent_id,\n\t\t\t\tchange_epoch,\n\t\t\t\twork_version,\n\t\t\t\tstate,\n\t\t\t\tattempt_count,\n\t\t\t\tnext_attempt_at,\n\t\t\t\tlease_token,\n\t\t\t\tlease_expires_at,\n\t\t\t\tlast_attempted_at,\n\t\t\t\tlast_error_code,\n\t\t\t\tcreated_at,\n\t\t\t\tupdated_at\n\t\t\t)\n\t\t\tVALUES (\n\t\t\t\tv_collection_id,\n\t\t\t\tv_collection_slug,\n\t\t\t\tv_content_id,\n\t\t\t\tv_epoch,\n\t\t\t\t1,\n\t\t\t\t'pending',\n\t\t\t\t0,\n\t\t\t\tv_now,\n\t\t\t\tNULL,\n\t\t\t\tNULL,\n\t\t\t\tNULL,\n\t\t\t\tNULL,\n\t\t\t\tv_now,\n\t\t\t\tv_now\n\t\t\t)\n\t\t\tON CONFLICT (collection_id, content_id) DO UPDATE SET\n\t\t\t\tcollection_slug = EXCLUDED.collection_slug,\n\t\t\t\tchange_epoch = EXCLUDED.change_epoch,\n\t\t\t\twork_version = _emdash_media_usage_work.work_version + 1,\n\t\t\t\tstate = 'pending',\n\t\t\t\tattempt_count = 0,\n\t\t\t\tnext_attempt_at = EXCLUDED.next_attempt_at,\n\t\t\t\tlease_token = NULL,\n\t\t\t\tlease_expires_at = NULL,\n\t\t\t\tlast_attempted_at = NULL,\n\t\t\t\tlast_error_code = NULL,\n\t\t\t\tupdated_at = EXCLUDED.updated_at;\n\n\t\t\tRETURN NULL;\n\t\tEND;\n\t\t$function$\n\t`.execute(db);\n}\n\nasync function postgresCreateTrigger(\n\tdb: Kysely<Database>,\n\ttableName: string,\n\ttriggerName: string,\n\toperation: CaptureOperation,\n\tidentity: MediaUsageCaptureIdentity,\n): Promise<void> {\n\tawait sql`\n\t\tCREATE TRIGGER ${sql.ref(triggerName)}\n\t\tAFTER ${operationSql(operation)} ON ${sql.ref(tableName)}\n\t\tFOR EACH ROW\n\t\tEXECUTE FUNCTION ${sql.ref(POSTGRES_TRIGGER_FUNCTION)}(\n\t\t\t${sql.lit(identity.collectionId)},\n\t\t\t${sql.lit(identity.collectionSlug)}\n\t\t)\n\t`.execute(db);\n}\n\nasync function sqliteCreateTrigger(\n\tdb: Kysely<Database>,\n\ttableName: string,\n\ttriggerName: string,\n\toperation: CaptureOperation,\n\tidentity: MediaUsageCaptureIdentity,\n): Promise<void> {\n\tconst contentId = operation === \"delete\" ? sql`OLD.id` : sql`NEW.id`;\n\tawait sqliteTriggerSql(\n\t\ttableName,\n\t\ttriggerName,\n\t\toperationSql(operation),\n\t\tcontentId,\n\t\tidentity,\n\t).execute(db);\n}\n\nfunction sqliteTriggerSql(\n\ttableName: string,\n\ttriggerName: string,\n\toperation: RawBuilder<unknown>,\n\tcontentId: RawBuilder<unknown>,\n\tidentity: MediaUsageCaptureIdentity,\n): RawBuilder<unknown> {\n\treturn sql`\n\t\tCREATE TRIGGER ${sql.ref(triggerName)}\n\t\tAFTER ${operation} ON ${sql.ref(tableName)}\n\t\tFOR EACH ROW\n\t\tBEGIN\n\t\t\tUPDATE _emdash_media_usage_index_status\n\t\t\tSET change_epoch = change_epoch + 1,\n\t\t\t\tstatus = CASE WHEN status = 'complete' THEN 'stale' ELSE status END,\n\t\t\t\tcompleted_at = CASE WHEN status = 'complete' THEN NULL ELSE completed_at END,\n\t\t\t\tupdated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')\n\t\t\tWHERE adapter_id = 'content-media'\n\t\t\t\tAND scope_type = 'collection'\n\t\t\t\tAND scope_key = ${sql.lit(identity.collectionSlug)}\n\t\t\t\tAND collection_id = ${sql.lit(identity.collectionId)}\n\t\t\t\tAND capture_state = 'active'\n\t\t\t\tAND EXISTS (\n\t\t\t\t\tSELECT 1\n\t\t\t\t\tFROM _emdash_collections AS collection\n\t\t\t\t\tWHERE collection.id = ${sql.lit(identity.collectionId)}\n\t\t\t\t\t\tAND collection.slug = ${sql.lit(identity.collectionSlug)}\n\t\t\t\t);\n\n\t\t\tSELECT CASE\n\t\t\t\tWHEN changes() <> 1 THEN RAISE(ABORT, 'media usage capture inactive')\n\t\t\tEND;\n\n\t\t\tINSERT INTO _emdash_media_usage_work (\n\t\t\t\tcollection_id,\n\t\t\t\tcollection_slug,\n\t\t\t\tcontent_id,\n\t\t\t\tchange_epoch,\n\t\t\t\twork_version,\n\t\t\t\tstate,\n\t\t\t\tattempt_count,\n\t\t\t\tnext_attempt_at,\n\t\t\t\tlease_token,\n\t\t\t\tlease_expires_at,\n\t\t\t\tlast_attempted_at,\n\t\t\t\tlast_error_code,\n\t\t\t\tcreated_at,\n\t\t\t\tupdated_at\n\t\t\t)\n\t\t\tSELECT\n\t\t\t\t${sql.lit(identity.collectionId)},\n\t\t\t\t${sql.lit(identity.collectionSlug)},\n\t\t\t\t${contentId},\n\t\t\t\tchange_epoch,\n\t\t\t\t1,\n\t\t\t\t'pending',\n\t\t\t\t0,\n\t\t\t\tstrftime('%Y-%m-%dT%H:%M:%fZ', 'now'),\n\t\t\t\tNULL,\n\t\t\t\tNULL,\n\t\t\t\tNULL,\n\t\t\t\tNULL,\n\t\t\t\tstrftime('%Y-%m-%dT%H:%M:%fZ', 'now'),\n\t\t\t\tstrftime('%Y-%m-%dT%H:%M:%fZ', 'now')\n\t\t\tFROM _emdash_media_usage_index_status\n\t\t\tWHERE adapter_id = 'content-media'\n\t\t\t\tAND scope_type = 'collection'\n\t\t\t\tAND scope_key = ${sql.lit(identity.collectionSlug)}\n\t\t\t\tAND collection_id = ${sql.lit(identity.collectionId)}\n\t\t\t\tAND capture_state = 'active'\n\t\t\tON CONFLICT (collection_id, content_id) DO UPDATE SET\n\t\t\t\tcollection_slug = excluded.collection_slug,\n\t\t\t\tchange_epoch = excluded.change_epoch,\n\t\t\t\twork_version = _emdash_media_usage_work.work_version + 1,\n\t\t\t\tstate = 'pending',\n\t\t\t\tattempt_count = 0,\n\t\t\t\tnext_attempt_at = excluded.next_attempt_at,\n\t\t\t\tlease_token = NULL,\n\t\t\t\tlease_expires_at = NULL,\n\t\t\t\tlast_attempted_at = NULL,\n\t\t\t\tlast_error_code = NULL,\n\t\t\t\tupdated_at = excluded.updated_at;\n\t\tEND\n\t`;\n}\n\nfunction operationSql(operation: CaptureOperation): RawBuilder<unknown> {\n\tswitch (operation) {\n\t\tcase \"insert\":\n\t\t\treturn sql`INSERT`;\n\t\tcase \"update\":\n\t\t\treturn sql`UPDATE`;\n\t\tcase \"delete\":\n\t\t\treturn sql`DELETE`;\n\t}\n}\n","import { sql, type Kysely, type RawBuilder, type Selectable, type Transaction } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { isPostgres } from \"../../database/dialect-helpers.js\";\nimport {\n\tdecodeCursor,\n\tencodeCursor,\n\ttype FindManyResult,\n} from \"../../database/repositories/types.js\";\nimport { withTransaction } from \"../../database/transaction.js\";\nimport type { Database, MediaUsageCollectionDeletionTable } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport type {\n\tCollectionDeletionGuardInput,\n\tCollectionDeletionGuardResult,\n} from \"../../db/adapters.js\";\nimport { FTSManager } from \"../../search/fts-manager.js\";\nimport { isMissingTableError } from \"../../utils/db-errors.js\";\nimport { verifyMediaUsageCaptureTriggers } from \"./capture-triggers.js\";\n\nconst ACTIVATION_KEY = \"incremental_capture\";\nconst VIRTUAL_DIALECT_ID = \"virtual:emdash/dialect\";\n\nexport type MediaUsageCollectionDeletionState = \"pending\" | \"retry\" | \"leased\" | \"failed\";\nexport type MediaUsageCollectionDeletionPhase =\n\t| \"fence\"\n\t| \"registry\"\n\t| \"table\"\n\t| \"work\"\n\t| \"sources\"\n\t| \"status\"\n\t| \"finalize\";\n\nexport interface MediaUsageCollectionDeletionRecord {\n\tcollectionId: string;\n\tcollectionSlug: string;\n\tforceDelete: boolean;\n\tstate: MediaUsageCollectionDeletionState;\n\tphase: MediaUsageCollectionDeletionPhase;\n\tworkCursor: string | null;\n\tsourceKey: string | null;\n\toccurrenceCursor: string | null;\n\tattemptCount: number;\n\tnextAttemptAt: string;\n\tleaseToken: string | null;\n\tleaseExpiresAt: string | null;\n\tlastErrorCode: string | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nexport interface MediaUsageCollectionDeletionOperatorItem {\n\tcollectionId: string;\n\tcollectionSlug: string;\n\tstate: MediaUsageCollectionDeletionState;\n\tphase: MediaUsageCollectionDeletionPhase;\n\tattemptCount: number;\n\tnextAttemptAt: string;\n\tleaseExpiresAt: string | null;\n\tlastErrorCode: string | null;\n\tupdatedAt: string;\n}\n\nexport type MediaUsageCollectionDeletionRetryResult =\n\t| { outcome: \"pending\"; changed: boolean; item: MediaUsageCollectionDeletionOperatorItem }\n\t| { outcome: \"lease_active\"; leaseExpiresAt: string }\n\t| { outcome: \"not_found\" }\n\t| { outcome: \"conflict\" };\n\nexport class MediaUsageCollectionDeletionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\tasync createTombstone(input: {\n\t\tcollectionId: string;\n\t\tcollectionSlug: string;\n\t\tforceDelete: boolean;\n\t}): Promise<MediaUsageCollectionDeletionRecord> {\n\t\tassertIdentity(input);\n\t\tconst now = timestampOffset(this.db, 0);\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.values({\n\t\t\t\tcollection_id: input.collectionId,\n\t\t\t\tcollection_slug: input.collectionSlug,\n\t\t\t\tforce_delete: input.forceDelete ? 1 : 0,\n\t\t\t\tstate: \"pending\",\n\t\t\t\tphase: \"fence\",\n\t\t\t\tnext_attempt_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.onConflict((conflict) => conflict.column(\"collection_id\").doNothing())\n\t\t\t.execute();\n\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.executeTakeFirstOrThrow();\n\t\tif (\n\t\t\trow.collection_slug !== input.collectionSlug ||\n\t\t\tBoolean(row.force_delete) !== input.forceDelete\n\t\t) {\n\t\t\tthrow new Error(\"Collection deletion tombstone identity conflicts with existing work\");\n\t\t}\n\t\treturn rowToRecord(row);\n\t}\n\n\tasync claim(input: {\n\t\tcollectionId: string;\n\t\tphase: MediaUsageCollectionDeletionPhase;\n\t\tleaseDurationSeconds: number;\n\t}): Promise<(MediaUsageCollectionDeletionRecord & { leaseToken: string }) | null> {\n\t\tif (!input.collectionId || !isPhase(input.phase)) {\n\t\t\tthrow new Error(\"Collection deletion claim requires an exact identity and phase\");\n\t\t}\n\t\tif (!Number.isSafeInteger(input.leaseDurationSeconds) || input.leaseDurationSeconds < 1) {\n\t\t\tthrow new Error(\"Collection deletion lease duration must be a positive whole number\");\n\t\t}\n\t\tconst leaseToken = ulid();\n\t\tconst row = await this.db\n\t\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.set({\n\t\t\t\tstate: \"leased\",\n\t\t\t\tlease_token: leaseToken,\n\t\t\t\tlease_expires_at: timestampOffset(this.db, input.leaseDurationSeconds),\n\t\t\t\tupdated_at: timestampOffset(this.db, 0),\n\t\t\t})\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where(\"phase\", \"=\", input.phase)\n\t\t\t.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb.and([\n\t\t\t\t\t\teb(\"state\", \"in\", [\"pending\", \"retry\"]),\n\t\t\t\t\t\ttimestampIsDue(this.db, \"next_attempt_at\"),\n\t\t\t\t\t]),\n\t\t\t\t\teb.and([eb(\"state\", \"=\", \"leased\"), timestampIsDue(this.db, \"lease_expires_at\")]),\n\t\t\t\t]),\n\t\t\t)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirst();\n\t\tif (!row) return null;\n\t\treturn { ...rowToRecord(row), leaseToken };\n\t}\n\n\tasync findBySlug(collectionSlug: string): Promise<MediaUsageCollectionDeletionRecord | null> {\n\t\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\t\ttry {\n\t\t\tconst row = await this.db\n\t\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"collection_slug\", \"=\", collectionSlug)\n\t\t\t\t.executeTakeFirst();\n\t\t\treturn row ? rowToRecord(row) : null;\n\t\t} catch (error) {\n\t\t\tif (isMissingTableError(error)) return null;\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tasync findDue(limit: number): Promise<MediaUsageCollectionDeletionRecord[]> {\n\t\tif (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {\n\t\t\tthrow new Error(\"Collection deletion candidate limit must be from 1 to 100\");\n\t\t}\n\t\tconst nextAttemptIsDue = timestampIsDue(this.db, \"next_attempt_at\");\n\t\tconst leaseIsDue = timestampIsDue(this.db, \"lease_expires_at\");\n\t\tconst result = await sql<Selectable<MediaUsageCollectionDeletionTable>>`\n\t\t\tWITH pending_candidates AS (\n\t\t\t\tSELECT * FROM _emdash_media_usage_collection_deletions\n\t\t\t\tWHERE state = 'pending' AND ${nextAttemptIsDue}\n\t\t\t\tORDER BY next_attempt_at, updated_at, collection_id\n\t\t\t\tLIMIT ${limit}\n\t\t\t), retry_candidates AS (\n\t\t\t\tSELECT * FROM _emdash_media_usage_collection_deletions\n\t\t\t\tWHERE state = 'retry' AND ${nextAttemptIsDue}\n\t\t\t\tORDER BY next_attempt_at, updated_at, collection_id\n\t\t\t\tLIMIT ${limit}\n\t\t\t), leased_candidates AS (\n\t\t\t\tSELECT * FROM _emdash_media_usage_collection_deletions\n\t\t\t\tWHERE state = 'leased' AND ${leaseIsDue}\n\t\t\t\tORDER BY lease_expires_at, updated_at, collection_id\n\t\t\t\tLIMIT ${limit}\n\t\t\t), candidates AS (\n\t\t\t\tSELECT * FROM pending_candidates\n\t\t\t\tUNION ALL SELECT * FROM retry_candidates\n\t\t\t\tUNION ALL SELECT * FROM leased_candidates\n\t\t\t)\n\t\t\tSELECT * FROM candidates\n\t\t\tORDER BY CASE WHEN state = 'leased' THEN lease_expires_at ELSE next_attempt_at END,\n\t\t\t\tupdated_at,\n\t\t\t\tcollection_id\n\t\t\tLIMIT ${limit}\n\t\t`.execute(this.db);\n\t\treturn result.rows.map(rowToRecord);\n\t}\n\n\tasync findOperatorPage(options: {\n\t\tstate?: MediaUsageCollectionDeletionState;\n\t\tlimit?: number;\n\t\tcursor?: string;\n\t}): Promise<FindManyResult<MediaUsageCollectionDeletionOperatorItem>> {\n\t\tconst state = options.state ?? \"failed\";\n\t\tif (!isState(state)) throw new Error(\"Invalid collection deletion state\");\n\t\tconst limit = options.limit ?? 50;\n\t\tif (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {\n\t\t\tthrow new Error(\"Collection deletion operator limit must be from 1 to 100\");\n\t\t}\n\t\tconst cursor = options.cursor ? decodeCursor(options.cursor) : null;\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"state\", \"=\", state);\n\t\tif (cursor) {\n\t\t\tquery = query.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"updated_at\", \"<\", cursor.orderValue),\n\t\t\t\t\teb.and([eb(\"updated_at\", \"=\", cursor.orderValue), eb(\"collection_id\", \"<\", cursor.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\t\tconst rows = await query\n\t\t\t.orderBy(\"updated_at\", \"desc\")\n\t\t\t.orderBy(\"collection_id\", \"desc\")\n\t\t\t.limit(limit + 1)\n\t\t\t.execute();\n\t\tconst items = rows.slice(0, limit).map(rowToOperatorItem);\n\t\tconst result: FindManyResult<MediaUsageCollectionDeletionOperatorItem> = { items };\n\t\tif (rows.length > limit && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.updatedAt, last.collectionId);\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync retryOperatorDeletion(input: {\n\t\tcollectionId: string;\n\t}): Promise<MediaUsageCollectionDeletionRetryResult> {\n\t\tif (!input.collectionId) throw new Error(\"Collection deletion retry requires an ID\");\n\t\tconst observed = await this.db\n\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.executeTakeFirst();\n\t\tif (!observed) return { outcome: \"not_found\" };\n\t\tif (observed.state === \"leased\" && observed.lease_expires_at) {\n\t\t\tconst live = await this.db\n\t\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t\t.select(\"lease_expires_at\")\n\t\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t\t.where(liveLease(this.db))\n\t\t\t\t.executeTakeFirst();\n\t\t\tif (live?.lease_expires_at) {\n\t\t\t\treturn { outcome: \"lease_active\", leaseExpiresAt: live.lease_expires_at };\n\t\t\t}\n\t\t}\n\t\tconst now = timestampOffset(this.db, 0);\n\t\tconst reopened = await this.db\n\t\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.set({\n\t\t\t\tstate: \"pending\",\n\t\t\t\tattempt_count: 0,\n\t\t\t\tnext_attempt_at: now,\n\t\t\t\tlease_token: null,\n\t\t\t\tlease_expires_at: null,\n\t\t\t\tlast_error_code: null,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where((eb) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"state\", \"in\", [\"failed\", \"retry\"]),\n\t\t\t\t\teb.and([eb(\"state\", \"=\", \"leased\"), timestampIsDue(this.db, \"lease_expires_at\")]),\n\t\t\t\t]),\n\t\t\t)\n\t\t\t.returningAll()\n\t\t\t.executeTakeFirst();\n\t\tif (reopened) {\n\t\t\treturn { outcome: \"pending\", changed: true, item: rowToOperatorItem(reopened) };\n\t\t}\n\t\treturn { outcome: \"conflict\" };\n\t}\n\n\tasync checkpoint(input: {\n\t\tcollectionId: string;\n\t\tleaseToken: string;\n\t\tfromPhase: MediaUsageCollectionDeletionPhase;\n\t\ttoPhase: MediaUsageCollectionDeletionPhase;\n\t}): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.set({\n\t\t\t\tphase: input.toPhase,\n\t\t\t\tattempt_count: 0,\n\t\t\t\tlast_error_code: null,\n\t\t\t\tupdated_at: timestampOffset(this.db, 0),\n\t\t\t})\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t.where(\"phase\", \"=\", input.fromPhase)\n\t\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t\t.where(liveLease(this.db))\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numUpdatedRows ?? 0) === 1;\n\t}\n\n\tasync release(input: { collectionId: string; leaseToken: string }): Promise<boolean> {\n\t\tconst now = timestampOffset(this.db, 0);\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.set({\n\t\t\t\tstate: \"pending\",\n\t\t\t\tnext_attempt_at: now,\n\t\t\t\tlease_token: null,\n\t\t\t\tlease_expires_at: null,\n\t\t\t\tattempt_count: 0,\n\t\t\t\tlast_error_code: null,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t\t.where(liveLease(this.db))\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numUpdatedRows ?? 0) === 1;\n\t}\n\n\tasync cancelFence(input: { collectionId: string; leaseToken: string }): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t.where(\"phase\", \"=\", \"fence\")\n\t\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t\t.where(liveLease(this.db))\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numDeletedRows ?? 0) === 1;\n\t}\n\n\tasync recordFailure(input: {\n\t\tcollectionId: string;\n\t\tleaseToken: string;\n\t\terrorCode: string;\n\t\tterminal: boolean;\n\t\tretryDelaySeconds: number;\n\t}): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.set({\n\t\t\t\tstate: input.terminal ? \"failed\" : \"retry\",\n\t\t\t\tattempt_count: sql<number>`attempt_count + 1`,\n\t\t\t\tnext_attempt_at: timestampOffset(this.db, input.retryDelaySeconds),\n\t\t\t\tlease_token: null,\n\t\t\t\tlease_expires_at: null,\n\t\t\t\tlast_error_code: input.errorCode,\n\t\t\t\tupdated_at: timestampOffset(this.db, 0),\n\t\t\t})\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t\t.where(liveLease(this.db))\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result.numUpdatedRows ?? 0) === 1;\n\t}\n\n\tasync deleteRegistryAndCheckpoint(input: {\n\t\tcollectionId: string;\n\t\tcollectionSlug: string;\n\t\tleaseToken: string;\n\t}): Promise<boolean> {\n\t\treturn withTransaction(this.db, async (trx) => {\n\t\t\tawait trx\n\t\t\t\t.deleteFrom(\"_emdash_collections\")\n\t\t\t\t.where(\"id\", \"=\", input.collectionId)\n\t\t\t\t.where(\"slug\", \"=\", input.collectionSlug)\n\t\t\t\t.where((eb) =>\n\t\t\t\t\teb.exists(\n\t\t\t\t\t\teb\n\t\t\t\t\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions as deletion\")\n\t\t\t\t\t\t\t.select(\"deletion.collection_id\")\n\t\t\t\t\t\t\t.where(\"deletion.collection_id\", \"=\", input.collectionId)\n\t\t\t\t\t\t\t.where(\"deletion.collection_slug\", \"=\", input.collectionSlug)\n\t\t\t\t\t\t\t.where(\"deletion.state\", \"=\", \"leased\")\n\t\t\t\t\t\t\t.where(\"deletion.phase\", \"=\", \"registry\")\n\t\t\t\t\t\t\t.where(\"deletion.lease_token\", \"=\", input.leaseToken)\n\t\t\t\t\t\t\t.where(liveLease(trx, \"deletion.lease_expires_at\")),\n\t\t\t\t\t),\n\t\t\t\t)\n\t\t\t\t.execute();\n\n\t\t\tconst checkpoint = await trx\n\t\t\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t\t\t.set({ phase: \"table\", updated_at: timestampOffset(trx, 0) })\n\t\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t\t.where(\"collection_slug\", \"=\", input.collectionSlug)\n\t\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t\t.where(\"phase\", \"=\", \"registry\")\n\t\t\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t\t\t.where(liveLease(trx))\n\t\t\t\t.executeTakeFirst();\n\t\t\treturn Number(checkpoint.numUpdatedRows ?? 0) === 1;\n\t\t});\n\t}\n}\n\nexport type ActivatedCollectionDeletionOutcome =\n\t| \"inactive\"\n\t| \"not_found\"\n\t| \"in_progress\"\n\t| \"advanced\"\n\t| \"deleted\"\n\t| \"has_content\";\n\nexport async function deleteActivatedMediaUsageCollection(\n\tdb: Kysely<Database>,\n\tinput: {\n\t\tcollectionSlug: string;\n\t\tcollectionId?: string;\n\t\tforceDelete: boolean;\n\t},\n\toptions: {\n\t\tfrontPhaseLimit?: number;\n\t\tclaimed?: MediaUsageCollectionDeletionRecord & { leaseToken: string };\n\t} = {},\n): Promise<ActivatedCollectionDeletionOutcome> {\n\tconst frontPhaseLimit = options.frontPhaseLimit ?? 3;\n\tif (!Number.isSafeInteger(frontPhaseLimit) || frontPhaseLimit < 1 || frontPhaseLimit > 3) {\n\t\tthrow new Error(\"Collection deletion front-phase limit must be from 1 to 3\");\n\t}\n\tvalidateIdentifier(input.collectionSlug, \"collection slug\");\n\tconst repository = new MediaUsageCollectionDeletionRepository(db);\n\tlet deletion = await repository.findBySlug(input.collectionSlug);\n\tif (deletion && input.collectionId && deletion.collectionId !== input.collectionId) {\n\t\tthrow new Error(\"Collection deletion tombstone identity conflict\");\n\t}\n\tif (!deletion) {\n\t\tlet activation: { state: string } | undefined;\n\t\ttry {\n\t\t\tactivation = await db\n\t\t\t\t.selectFrom(\"_emdash_media_usage_activation\")\n\t\t\t\t.select(\"state\")\n\t\t\t\t.where(\"task_key\", \"=\", ACTIVATION_KEY)\n\t\t\t\t.executeTakeFirst();\n\t\t} catch (error) {\n\t\t\tif (!isMissingTableError(error)) throw error;\n\t\t}\n\t\tif (!activation || activation.state === \"expanded\") return \"inactive\";\n\t\tif (activation.state !== \"active\") {\n\t\t\tthrow new Error(\"Media usage activation must be active before collection deletion\");\n\t\t}\n\t\tif (!input.collectionId) return \"not_found\";\n\t\tawait assertActivatedCollectionDeletionReady(db, {\n\t\t\tcollectionId: input.collectionId,\n\t\t\tcollectionSlug: input.collectionSlug,\n\t\t});\n\t\tdeletion = await repository.createTombstone({\n\t\t\tcollectionId: input.collectionId,\n\t\t\tcollectionSlug: input.collectionSlug,\n\t\t\tforceDelete: input.forceDelete,\n\t\t});\n\t}\n\n\tif (deletion.phase !== \"fence\" && deletion.phase !== \"registry\" && deletion.phase !== \"table\") {\n\t\treturn \"deleted\";\n\t}\n\tconst claim =\n\t\toptions.claimed ??\n\t\t(await repository.claim({\n\t\t\tcollectionId: deletion.collectionId,\n\t\t\tphase: deletion.phase,\n\t\t\tleaseDurationSeconds: 5 * 60,\n\t\t}));\n\tif (!claim) return \"in_progress\";\n\tif (claim.collectionId !== deletion.collectionId || claim.phase !== deletion.phase) {\n\t\tthrow new Error(\"Collection deletion claim identity conflict\");\n\t}\n\tlet phase = claim.phase;\n\tlet processedFrontPhases = 0;\n\tconst lease = { collectionId: claim.collectionId, leaseToken: claim.leaseToken };\n\n\tif (phase === \"fence\") {\n\t\tconst captureState = await findCollectionCaptureState(db, claim);\n\t\tif (captureState !== \"active\" && captureState !== \"deleting\") {\n\t\t\tthrow new Error(\"Activated collection deletion requires a fenced capture lifecycle\");\n\t\t}\n\t\tif (!(await verifyMediaUsageCaptureTriggers(db, claim))) {\n\t\t\tthrow new Error(\"Activated collection deletion requires the exact capture trigger set\");\n\t\t}\n\t\tif (captureState === \"active\") {\n\t\t\tconst result = await executeCollectionDeletionGuard(db, {\n\t\t\t\taction: \"fence\",\n\t\t\t\tcollectionId: claim.collectionId,\n\t\t\t\tcollectionSlug: claim.collectionSlug,\n\t\t\t\tleaseToken: claim.leaseToken,\n\t\t\t\tforceDelete: claim.forceDelete,\n\t\t\t});\n\t\t\tif (result.outcome === \"has_content\") {\n\t\t\t\tif (!(await repository.cancelFence(lease))) {\n\t\t\t\t\tthrow new Error(\"Collection deletion lost its fence while preserving content\");\n\t\t\t\t}\n\t\t\t\treturn \"has_content\";\n\t\t\t}\n\t\t\tif (result.outcome !== \"fenced\") throw new Error(\"Collection deletion lost its fence\");\n\t\t}\n\t\tif (\n\t\t\t!(await repository.checkpoint({\n\t\t\t\t...lease,\n\t\t\t\tfromPhase: \"fence\",\n\t\t\t\ttoPhase: \"registry\",\n\t\t\t}))\n\t\t) {\n\t\t\tthrow new Error(\"Collection deletion lost its fence checkpoint\");\n\t\t}\n\t\tphase = \"registry\";\n\t\tprocessedFrontPhases++;\n\t\tif (processedFrontPhases >= frontPhaseLimit) {\n\t\t\tif (!(await repository.release(lease)))\n\t\t\t\tthrow new Error(\"Collection deletion lost its handoff\");\n\t\t\treturn \"advanced\";\n\t\t}\n\t}\n\n\tif (phase === \"registry\") {\n\t\tif (\n\t\t\t!(await repository.deleteRegistryAndCheckpoint({\n\t\t\t\t...lease,\n\t\t\t\tcollectionSlug: claim.collectionSlug,\n\t\t\t}))\n\t\t) {\n\t\t\tthrow new Error(\"Collection deletion lost its registry checkpoint\");\n\t\t}\n\t\tphase = \"table\";\n\t\tprocessedFrontPhases++;\n\t\tif (processedFrontPhases >= frontPhaseLimit) {\n\t\t\tif (!(await repository.release(lease)))\n\t\t\t\tthrow new Error(\"Collection deletion lost its handoff\");\n\t\t\treturn \"advanced\";\n\t\t}\n\t}\n\n\tif (phase === \"table\") {\n\t\tconst result = await executeCollectionDeletionGuard(db, {\n\t\t\taction: \"drop\",\n\t\t\tcollectionId: claim.collectionId,\n\t\t\tcollectionSlug: claim.collectionSlug,\n\t\t\tleaseToken: claim.leaseToken,\n\t\t});\n\t\tif (result.outcome !== \"dropped\") throw new Error(\"Collection deletion lost its table fence\");\n\t\tif (\n\t\t\t!(await repository.checkpoint({\n\t\t\t\t...lease,\n\t\t\t\tfromPhase: \"table\",\n\t\t\t\ttoPhase: \"work\",\n\t\t\t}))\n\t\t) {\n\t\t\tthrow new Error(\"Collection deletion lost its table checkpoint\");\n\t\t}\n\t}\n\n\tif (!(await repository.release(lease))) {\n\t\tthrow new Error(\"Collection deletion lost its cleanup handoff\");\n\t}\n\treturn \"deleted\";\n}\n\nexport async function isMediaUsageCollectionSlugDeleting(\n\tdb: Kysely<Database>,\n\tcollectionSlug: string,\n): Promise<boolean> {\n\tvalidateIdentifier(collectionSlug, \"collection slug\");\n\ttry {\n\t\tconst row = await db\n\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.select(\"collection_id\")\n\t\t\t.where(\"collection_slug\", \"=\", collectionSlug)\n\t\t\t.executeTakeFirst();\n\t\treturn row !== undefined;\n\t} catch (error) {\n\t\tif (isMissingTableError(error)) return false;\n\t\tthrow error;\n\t}\n}\n\nasync function assertActivatedCollectionDeletionReady(\n\tdb: Kysely<Database>,\n\tidentity: { collectionId: string; collectionSlug: string },\n): Promise<void> {\n\tconst captureState = await findCollectionCaptureState(db, identity);\n\tif (captureState !== \"active\") {\n\t\tthrow new Error(\"Activated collection deletion requires an active capture lifecycle\");\n\t}\n\tif (!(await verifyMediaUsageCaptureTriggers(db, identity))) {\n\t\tthrow new Error(\"Activated collection deletion requires the exact capture trigger set\");\n\t}\n}\n\nasync function findCollectionCaptureState(\n\tdb: Kysely<Database>,\n\tidentity: { collectionId: string; collectionSlug: string },\n): Promise<string | null> {\n\tconst lifecycle = await db\n\t\t.selectFrom(\"_emdash_media_usage_index_status\")\n\t\t.select(\"capture_state\")\n\t\t.where(\"adapter_id\", \"=\", \"content-media\")\n\t\t.where(\"scope_type\", \"=\", \"collection\")\n\t\t.where(\"scope_key\", \"=\", identity.collectionSlug)\n\t\t.where(\"collection_id\", \"=\", identity.collectionId)\n\t\t.executeTakeFirst();\n\treturn lifecycle?.capture_state ?? null;\n}\n\nasync function executeCollectionDeletionGuard(\n\tdb: Kysely<Database>,\n\tinput: CollectionDeletionGuardInput,\n): Promise<CollectionDeletionGuardResult> {\n\tconst executeAdapterGuard = await loadAdapterCollectionDeletionGuard();\n\tif (executeAdapterGuard) {\n\t\tconst { default: config } = await import(\"virtual:emdash/config\");\n\t\treturn executeAdapterGuard(config.database?.config, input);\n\t}\n\treturn executeLocalCollectionDeletionGuard(db, input);\n}\n\nasync function loadAdapterCollectionDeletionGuard(): Promise<\n\timport(\"../../db/adapters.js\").ExecuteCollectionDeletionGuard | undefined\n> {\n\ttry {\n\t\tconst dialect = await import(\"virtual:emdash/dialect\");\n\t\treturn dialect.executeCollectionDeletionGuard;\n\t} catch (error) {\n\t\tif (isVirtualDialectUnavailableError(error)) return undefined;\n\t\tthrow error;\n\t}\n}\n\nexport function isVirtualDialectUnavailableError(error: unknown): boolean {\n\tif (!(error instanceof Error) || !(\"code\" in error)) return false;\n\tif (error.code === \"ERR_MODULE_NOT_FOUND\") return error.message.includes(VIRTUAL_DIALECT_ID);\n\treturn (\n\t\terror.code === \"ERR_UNSUPPORTED_ESM_URL_SCHEME\" &&\n\t\terror.message.includes(\"Received protocol 'virtual:'\")\n\t);\n}\n\nexport async function executeLocalCollectionDeletionGuard(\n\tdb: Kysely<Database>,\n\tinput: CollectionDeletionGuardInput,\n): Promise<CollectionDeletionGuardResult> {\n\tassertGuardInput(input);\n\treturn db.transaction().execute(async (trx) => {\n\t\tif (!(await lockLiveTombstone(trx, input))) return { outcome: \"stale\" };\n\t\tif (input.action === \"fence\") return fenceCollection(trx, input);\n\n\t\tconst fts = new FTSManager(trx);\n\t\tawait fts.dropFtsTable(input.collectionSlug);\n\t\tawait sql`DROP TABLE IF EXISTS ${sql.ref(`ec_${input.collectionSlug}`)}`.execute(trx);\n\t\treturn { outcome: \"dropped\" };\n\t});\n}\n\nasync function fenceCollection(\n\ttrx: Transaction<Database>,\n\tinput: Extract<CollectionDeletionGuardInput, { action: \"fence\" }>,\n): Promise<CollectionDeletionGuardResult> {\n\tconst tableName = `ec_${input.collectionSlug}`;\n\tvalidateIdentifier(tableName, \"content table\");\n\tif (isPostgres(trx)) {\n\t\tawait sql`LOCK TABLE ${sql.ref(tableName)} IN SHARE ROW EXCLUSIVE MODE`.execute(trx);\n\t}\n\n\tif (!input.forceDelete) {\n\t\tconst content = await sql<{ present: number }>`\n\t\t\tSELECT 1 AS present FROM ${sql.ref(tableName)}\n\t\t\tWHERE deleted_at IS NULL\n\t\t\tLIMIT 1\n\t\t`.execute(trx);\n\t\tif (content.rows.length > 0) return { outcome: \"has_content\" };\n\t}\n\n\tconst result = await trx\n\t\t.updateTable(\"_emdash_media_usage_index_status as status\")\n\t\t.set({ capture_state: \"deleting\", updated_at: timestampOffset(trx, 0) })\n\t\t.where(\"status.adapter_id\", \"=\", \"content-media\")\n\t\t.where(\"status.scope_type\", \"=\", \"collection\")\n\t\t.where(\"status.scope_key\", \"=\", input.collectionSlug)\n\t\t.where(\"status.collection_id\", \"=\", input.collectionId)\n\t\t.where(\"status.capture_state\", \"=\", \"active\")\n\t\t.where((eb) =>\n\t\t\teb.exists(\n\t\t\t\teb\n\t\t\t\t\t.selectFrom(\"_emdash_collections as collection\")\n\t\t\t\t\t.select(\"collection.id\")\n\t\t\t\t\t.where(\"collection.id\", \"=\", input.collectionId)\n\t\t\t\t\t.where(\"collection.slug\", \"=\", input.collectionSlug),\n\t\t\t),\n\t\t)\n\t\t.executeTakeFirst();\n\treturn Number(result.numUpdatedRows ?? 0) === 1 ? { outcome: \"fenced\" } : { outcome: \"stale\" };\n}\n\nasync function lockLiveTombstone(\n\ttrx: Transaction<Database>,\n\tinput: CollectionDeletionGuardInput,\n): Promise<boolean> {\n\tconst phase = input.action === \"fence\" ? \"fence\" : \"table\";\n\tif (isPostgres(trx)) {\n\t\tconst row = await trx\n\t\t\t.selectFrom(\"_emdash_media_usage_collection_deletions\")\n\t\t\t.select(\"collection_id\")\n\t\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t\t.where(\"collection_slug\", \"=\", input.collectionSlug)\n\t\t\t.where(\"state\", \"=\", \"leased\")\n\t\t\t.where(\"phase\", \"=\", phase)\n\t\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t\t.where(liveLease(trx))\n\t\t\t.forUpdate()\n\t\t\t.executeTakeFirst();\n\t\treturn row !== undefined;\n\t}\n\n\tconst locked = await trx\n\t\t.updateTable(\"_emdash_media_usage_collection_deletions\")\n\t\t.set({ updated_at: sql<string>`updated_at` })\n\t\t.where(\"collection_id\", \"=\", input.collectionId)\n\t\t.where(\"collection_slug\", \"=\", input.collectionSlug)\n\t\t.where(\"state\", \"=\", \"leased\")\n\t\t.where(\"phase\", \"=\", phase)\n\t\t.where(\"lease_token\", \"=\", input.leaseToken)\n\t\t.where(liveLease(trx))\n\t\t.executeTakeFirst();\n\treturn Number(locked.numUpdatedRows ?? 0) === 1;\n}\n\nfunction assertIdentity(input: { collectionId: string; collectionSlug: string }): void {\n\tif (!input.collectionId) throw new Error(\"Collection deletion requires a collection ID\");\n\tvalidateIdentifier(input.collectionSlug, \"collection slug\");\n}\n\nfunction assertGuardInput(input: CollectionDeletionGuardInput): void {\n\tassertIdentity(input);\n\tif (!input.leaseToken) throw new Error(\"Collection deletion requires a lease token\");\n}\n\nfunction liveLease(db: Kysely<Database>, column = \"lease_expires_at\"): RawBuilder<boolean> {\n\tconst leaseExpiresAt = sql.ref(column);\n\treturn isPostgres(db)\n\t\t? sql<boolean>`${leaseExpiresAt}::timestamptz > clock_timestamp()`\n\t\t: sql<boolean>`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;\n}\n\nfunction timestampIsDue(\n\tdb: Kysely<Database>,\n\tcolumn: \"next_attempt_at\" | \"lease_expires_at\",\n): RawBuilder<boolean> {\n\treturn isPostgres(db)\n\t\t? sql<boolean>`${sql.ref(column)} <= to_char(\n\t\t\tstatement_timestamp() AT TIME ZONE 'UTC',\n\t\t\t'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'\n\t\t)`\n\t\t: sql<boolean>`${sql.ref(column)} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;\n}\n\nfunction timestampOffset(db: Kysely<Database>, offsetSeconds: number): RawBuilder<string> {\n\tif (isPostgres(db)) {\n\t\treturn sql<string>`to_char(\n\t\t\t(clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),\n\t\t\t'YYYY-MM-DD\"T\"HH24:MI:SS.MS\"Z\"'\n\t\t)`;\n\t}\n\treturn sql<string>`strftime(\n\t\t'%Y-%m-%dT%H:%M:%fZ',\n\t\t'now',\n\t\t${`${offsetSeconds >= 0 ? \"+\" : \"\"}${offsetSeconds} seconds`}\n\t)`;\n}\n\nexport function collectionDeletionCurrentTimestamp(db: Kysely<Database>): RawBuilder<string> {\n\treturn timestampOffset(db, 0);\n}\n\nfunction rowToRecord(\n\trow: Selectable<MediaUsageCollectionDeletionTable>,\n): MediaUsageCollectionDeletionRecord {\n\tif (!isState(row.state) || !isPhase(row.phase)) {\n\t\tthrow new Error(\"Invalid media usage collection deletion lifecycle\");\n\t}\n\treturn {\n\t\tcollectionId: row.collection_id,\n\t\tcollectionSlug: row.collection_slug,\n\t\tforceDelete: Boolean(row.force_delete),\n\t\tstate: row.state,\n\t\tphase: row.phase,\n\t\tworkCursor: row.work_cursor,\n\t\tsourceKey: row.source_key,\n\t\toccurrenceCursor: row.occurrence_cursor,\n\t\tattemptCount: row.attempt_count,\n\t\tnextAttemptAt: row.next_attempt_at,\n\t\tleaseToken: row.lease_token,\n\t\tleaseExpiresAt: row.lease_expires_at,\n\t\tlastErrorCode: row.last_error_code,\n\t\tcreatedAt: row.created_at,\n\t\tupdatedAt: row.updated_at,\n\t};\n}\n\nfunction rowToOperatorItem(\n\trow: Selectable<MediaUsageCollectionDeletionTable>,\n): MediaUsageCollectionDeletionOperatorItem {\n\tconst deletion = rowToRecord(row);\n\treturn {\n\t\tcollectionId: deletion.collectionId,\n\t\tcollectionSlug: deletion.collectionSlug,\n\t\tstate: deletion.state,\n\t\tphase: deletion.phase,\n\t\tattemptCount: deletion.attemptCount,\n\t\tnextAttemptAt: deletion.nextAttemptAt,\n\t\tleaseExpiresAt: deletion.leaseExpiresAt,\n\t\tlastErrorCode: deletion.lastErrorCode,\n\t\tupdatedAt: deletion.updatedAt,\n\t};\n}\n\nfunction isState(value: string): value is MediaUsageCollectionDeletionState {\n\treturn value === \"pending\" || value === \"retry\" || value === \"leased\" || value === \"failed\";\n}\n\nfunction isPhase(value: string): value is MediaUsageCollectionDeletionPhase {\n\treturn (\n\t\tvalue === \"fence\" ||\n\t\tvalue === \"registry\" ||\n\t\tvalue === \"table\" ||\n\t\tvalue === \"work\" ||\n\t\tvalue === \"sources\" ||\n\t\tvalue === \"status\" ||\n\t\tvalue === \"finalize\"\n\t);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { l as invalidateCommentObjectCache } from "./object-cache-71tPfV4A.mjs";
|
|
2
|
+
import { o as decodeCursor, s as encodeCursor } from "./types-CQPWhPxE.mjs";
|
|
3
3
|
import { sql } from "kysely";
|
|
4
4
|
import { ulid } from "ulidx";
|
|
5
5
|
|
|
@@ -252,4 +252,4 @@ function safeJsonParse(value) {
|
|
|
252
252
|
|
|
253
253
|
//#endregion
|
|
254
254
|
export { CommentRepository as t };
|
|
255
|
-
//# sourceMappingURL=comment-
|
|
255
|
+
//# sourceMappingURL=comment-Dxlep6cE.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment-DnTxxVHv.mjs","names":[],"sources":["../src/database/repositories/comment.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { invalidateCommentObjectCache } from \"../../object-cache/index.js\";\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n/** Matches LIKE wildcard characters and the escape character itself */\nconst LIKE_ESCAPE_RE = /[%_\\\\]/g;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type CommentStatus = \"pending\" | \"approved\" | \"spam\" | \"trash\";\n\nexport interface Comment {\n\tid: string;\n\tcollection: string;\n\tcontentId: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId: string | null;\n\tbody: string;\n\tstatus: CommentStatus;\n\tipHash: string | null;\n\tuserAgent: string | null;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/** Public-facing comment shape — no private fields */\nexport interface PublicComment {\n\tid: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tisRegisteredUser: boolean;\n\tbody: string;\n\tcreatedAt: string;\n\treplies?: PublicComment[];\n\t/** Aggregate reaction counts (`{ like: 12 }`), attached when requested. */\n\treactions?: Record<string, number>;\n}\n\nexport interface CreateCommentInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tstatus?: CommentStatus;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n\tmoderationMetadata?: Record<string, unknown> | null;\n}\n\nexport interface CommentFindOptions {\n\tstatus?: CommentStatus;\n\tcollection?: string;\n\tsearch?: string;\n\tlimit?: number;\n\tcursor?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class CommentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new comment\n\t */\n\tasync create(input: CreateCommentInput): Promise<Comment> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_comments\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tcollection: input.collection,\n\t\t\t\tcontent_id: input.contentId,\n\t\t\t\tparent_id: input.parentId ?? null,\n\t\t\t\tauthor_name: input.authorName,\n\t\t\t\tauthor_email: input.authorEmail,\n\t\t\t\tauthor_user_id: input.authorUserId ?? null,\n\t\t\t\tbody: input.body,\n\t\t\t\tstatus: input.status ?? \"pending\",\n\t\t\t\tip_hash: input.ipHash ?? null,\n\t\t\t\tuser_agent: input.userAgent ?? null,\n\t\t\t\tmoderation_metadata: input.moderationMetadata\n\t\t\t\t\t? JSON.stringify(input.moderationMetadata)\n\t\t\t\t\t: null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tinvalidateCommentObjectCache();\n\n\t\tconst comment = await this.findById(id);\n\t\tif (!comment) {\n\t\t\tthrow new Error(\"Failed to create comment\");\n\t\t}\n\t\treturn comment;\n\t}\n\n\t/**\n\t * Find comment by ID\n\t */\n\tasync findById(id: string): Promise<Comment | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToComment(row) : null;\n\t}\n\n\t/**\n\t * Find comments for a content item with optional status filter.\n\t * Results are ordered by created_at ASC (oldest first) for display.\n\t */\n\tasync findByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\toptions: { status?: CommentStatus; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (options.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.status);\n\t\t}\n\n\t\t// Cursor pagination (ascending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \">\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.orderBy(\"id\", \"asc\")\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).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Find comments by status (moderation inbox).\n\t * Results are ordered by created_at DESC (newest first).\n\t */\n\tasync findByStatus(\n\t\tstatus: CommentStatus,\n\t\toptions: { collection?: string; search?: string; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db.selectFrom(\"_emdash_comments\").selectAll().where(\"status\", \"=\", status);\n\n\t\tif (options.collection) {\n\t\t\tquery = query.where(\"collection\", \"=\", options.collection);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Escape LIKE wildcards to prevent them acting as SQL pattern characters\n\t\t\tconst escaped = options.search.replace(LIKE_ESCAPE_RE, (ch) => `\\\\${ch}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`author_name LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`author_email LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`body LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Cursor pagination (descending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"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).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Update comment status\n\t */\n\tasync updateStatus(id: string, status: CommentStatus): Promise<Comment | null> {\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Bulk update comment statuses\n\t */\n\tasync bulkUpdateStatus(ids: string[], status: CommentStatus): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn Number(result.numUpdatedRows ?? 0);\n\t}\n\n\t/**\n\t * Hard-delete a single comment. Replies cascade via FK.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Bulk hard-delete comments\n\t */\n\tasync bulkDelete(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(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete all comments for a content item (cascade on content deletion)\n\t */\n\tasync deleteByContent(collection: string, contentId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Count comments for a content item, optionally filtered by status\n\t */\n\tasync countByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\tstatus?: CommentStatus,\n\t): Promise<number> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (status) {\n\t\t\tquery = query.where(\"status\", \"=\", status);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Count comments grouped by status (for inbox badges)\n\t *\n\t * Uses four parallel COUNT queries with WHERE filters to leverage partial indexes\n\t * (idx_comments_pending, idx_comments_approved, idx_comments_spam, idx_comments_trash)\n\t * instead of a full table GROUP BY scan.\n\t */\n\tasync countByStatus(): Promise<Record<CommentStatus, number>> {\n\t\t// Execute four parallel COUNT queries, each using its partial index\n\t\tconst [pending, approved, spam, trash] = await Promise.all([\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"spam\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"trash\")\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn {\n\t\t\tpending: Number(pending?.count ?? 0),\n\t\t\tapproved: Number(approved?.count ?? 0),\n\t\t\tspam: Number(spam?.count ?? 0),\n\t\t\ttrash: Number(trash?.count ?? 0),\n\t\t};\n\t}\n\n\t/**\n\t * Count approved comments from a given email address.\n\t * Used for \"first time commenter\" moderation logic.\n\t */\n\tasync countApprovedByEmail(email: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"author_email\", \"=\", email)\n\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Update the moderation metadata JSON on a comment\n\t */\n\tasync updateModerationMetadata(id: string, metadata: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ moderation_metadata: JSON.stringify(metadata) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t// ---------------------------------------------------------------------------\n\t// Helpers\n\t// ---------------------------------------------------------------------------\n\n\t/**\n\t * Assemble a flat list of comments into a threaded structure (1-level nesting)\n\t */\n\tstatic assembleThreads(comments: Comment[]): Comment[] {\n\t\tconst roots: Comment[] = [];\n\t\tconst childrenMap = new Map<string, Comment[]>();\n\n\t\tfor (const comment of comments) {\n\t\t\tif (comment.parentId) {\n\t\t\t\tconst siblings = childrenMap.get(comment.parentId) ?? [];\n\t\t\t\tsiblings.push(comment);\n\t\t\t\tchildrenMap.set(comment.parentId, siblings);\n\t\t\t} else {\n\t\t\t\troots.push(comment);\n\t\t\t}\n\t\t}\n\n\t\t// Attach children as a non-standard property — callers map to PublicComment.replies\n\t\treturn roots.map((root) => ({\n\t\t\t...root,\n\t\t\t_replies: childrenMap.get(root.id) ?? [],\n\t\t}));\n\t}\n\n\t/**\n\t * Convert a Comment to its public-facing shape\n\t */\n\tstatic toPublicComment(comment: Comment & { _replies?: Comment[] }): PublicComment {\n\t\tconst pub: PublicComment = {\n\t\t\tid: comment.id,\n\t\t\tparentId: comment.parentId,\n\t\t\tauthorName: comment.authorName,\n\t\t\tisRegisteredUser: comment.authorUserId !== null,\n\t\t\tbody: comment.body,\n\t\t\tcreatedAt: comment.createdAt,\n\t\t};\n\n\t\tif (comment._replies && comment._replies.length > 0) {\n\t\t\tpub.replies = comment._replies.map((r) => CommentRepository.toPublicComment(r));\n\t\t}\n\n\t\treturn pub;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- selectAll returns runtime row\n\tprivate rowToComment(row: any): Comment {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tcontentId: row.content_id,\n\t\t\tparentId: row.parent_id,\n\t\t\tauthorName: row.author_name,\n\t\t\tauthorEmail: row.author_email,\n\t\t\tauthorUserId: row.author_user_id,\n\t\t\tbody: row.body,\n\t\t\tstatus: row.status as CommentStatus,\n\t\t\tipHash: row.ip_hash,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tmoderationMetadata: row.moderation_metadata ? safeJsonParse(row.moderation_metadata) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Module helpers\n// ---------------------------------------------------------------------------\n\nfunction safeJsonParse(value: string): Record<string, unknown> | null {\n\ttry {\n\t\treturn JSON.parse(value) as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"comment-Dxlep6cE.mjs","names":[],"sources":["../src/database/repositories/comment.ts"],"sourcesContent":["import { sql, type ExpressionBuilder, type Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { invalidateCommentObjectCache } from \"../../object-cache/index.js\";\nimport type { Database } from \"../types.js\";\nimport { encodeCursor, decodeCursor, type FindManyResult } from \"./types.js\";\n\n/** Matches LIKE wildcard characters and the escape character itself */\nconst LIKE_ESCAPE_RE = /[%_\\\\]/g;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type CommentStatus = \"pending\" | \"approved\" | \"spam\" | \"trash\";\n\nexport interface Comment {\n\tid: string;\n\tcollection: string;\n\tcontentId: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId: string | null;\n\tbody: string;\n\tstatus: CommentStatus;\n\tipHash: string | null;\n\tuserAgent: string | null;\n\tmoderationMetadata: Record<string, unknown> | null;\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\n/** Public-facing comment shape — no private fields */\nexport interface PublicComment {\n\tid: string;\n\tparentId: string | null;\n\tauthorName: string;\n\tisRegisteredUser: boolean;\n\tbody: string;\n\tcreatedAt: string;\n\treplies?: PublicComment[];\n\t/** Aggregate reaction counts (`{ like: 12 }`), attached when requested. */\n\treactions?: Record<string, number>;\n}\n\nexport interface CreateCommentInput {\n\tcollection: string;\n\tcontentId: string;\n\tparentId?: string | null;\n\tauthorName: string;\n\tauthorEmail: string;\n\tauthorUserId?: string | null;\n\tbody: string;\n\tstatus?: CommentStatus;\n\tipHash?: string | null;\n\tuserAgent?: string | null;\n\tmoderationMetadata?: Record<string, unknown> | null;\n}\n\nexport interface CommentFindOptions {\n\tstatus?: CommentStatus;\n\tcollection?: string;\n\tsearch?: string;\n\tlimit?: number;\n\tcursor?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Repository\n// ---------------------------------------------------------------------------\n\nexport class CommentRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Create a new comment\n\t */\n\tasync create(input: CreateCommentInput): Promise<Comment> {\n\t\tconst id = ulid();\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.insertInto(\"_emdash_comments\")\n\t\t\t.values({\n\t\t\t\tid,\n\t\t\t\tcollection: input.collection,\n\t\t\t\tcontent_id: input.contentId,\n\t\t\t\tparent_id: input.parentId ?? null,\n\t\t\t\tauthor_name: input.authorName,\n\t\t\t\tauthor_email: input.authorEmail,\n\t\t\t\tauthor_user_id: input.authorUserId ?? null,\n\t\t\t\tbody: input.body,\n\t\t\t\tstatus: input.status ?? \"pending\",\n\t\t\t\tip_hash: input.ipHash ?? null,\n\t\t\t\tuser_agent: input.userAgent ?? null,\n\t\t\t\tmoderation_metadata: input.moderationMetadata\n\t\t\t\t\t? JSON.stringify(input.moderationMetadata)\n\t\t\t\t\t: null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t})\n\t\t\t.execute();\n\n\t\tinvalidateCommentObjectCache();\n\n\t\tconst comment = await this.findById(id);\n\t\tif (!comment) {\n\t\t\tthrow new Error(\"Failed to create comment\");\n\t\t}\n\t\treturn comment;\n\t}\n\n\t/**\n\t * Find comment by ID\n\t */\n\tasync findById(id: string): Promise<Comment | null> {\n\t\tconst row = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\treturn row ? this.rowToComment(row) : null;\n\t}\n\n\t/**\n\t * Find comments for a content item with optional status filter.\n\t * Results are ordered by created_at ASC (oldest first) for display.\n\t */\n\tasync findByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\toptions: { status?: CommentStatus; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.selectAll()\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (options.status) {\n\t\t\tquery = query.where(\"status\", \"=\", options.status);\n\t\t}\n\n\t\t// Cursor pagination (ascending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \">\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \">\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"asc\")\n\t\t\t.orderBy(\"id\", \"asc\")\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).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Find comments by status (moderation inbox).\n\t * Results are ordered by created_at DESC (newest first).\n\t */\n\tasync findByStatus(\n\t\tstatus: CommentStatus,\n\t\toptions: { collection?: string; search?: string; limit?: number; cursor?: string } = {},\n\t): Promise<FindManyResult<Comment>> {\n\t\tconst limit = Math.min(options.limit || 50, 100);\n\n\t\tlet query = this.db.selectFrom(\"_emdash_comments\").selectAll().where(\"status\", \"=\", status);\n\n\t\tif (options.collection) {\n\t\t\tquery = query.where(\"collection\", \"=\", options.collection);\n\t\t}\n\n\t\tif (options.search) {\n\t\t\t// Escape LIKE wildcards to prevent them acting as SQL pattern characters\n\t\t\tconst escaped = options.search.replace(LIKE_ESCAPE_RE, (ch) => `\\\\${ch}`);\n\t\t\tconst term = `%${escaped}%`;\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\tsql<boolean>`author_name LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`author_email LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t\tsql<boolean>`body LIKE ${term} ESCAPE '\\\\'`,\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\t// Cursor pagination (descending by created_at)\n\t\tif (options.cursor) {\n\t\t\tconst decoded = decodeCursor(options.cursor);\n\t\t\tquery = query.where((eb: ExpressionBuilder<Database, \"_emdash_comments\">) =>\n\t\t\t\teb.or([\n\t\t\t\t\teb(\"created_at\", \"<\", decoded.orderValue),\n\t\t\t\t\teb.and([eb(\"created_at\", \"=\", decoded.orderValue), eb(\"id\", \"<\", decoded.id)]),\n\t\t\t\t]),\n\t\t\t);\n\t\t}\n\n\t\tquery = query\n\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t.orderBy(\"id\", \"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).map((r) => this.rowToComment(r));\n\n\t\tconst result: FindManyResult<Comment> = { items };\n\t\tif (hasMore && items.length > 0) {\n\t\t\tconst last = items.at(-1)!;\n\t\t\tresult.nextCursor = encodeCursor(last.createdAt, last.id);\n\t\t}\n\t\treturn result;\n\t}\n\n\t/**\n\t * Update comment status\n\t */\n\tasync updateStatus(id: string, status: CommentStatus): Promise<Comment | null> {\n\t\tconst now = new Date().toISOString();\n\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn this.findById(id);\n\t}\n\n\t/**\n\t * Bulk update comment statuses\n\t */\n\tasync bulkUpdateStatus(ids: string[], status: CommentStatus): Promise<number> {\n\t\tif (ids.length === 0) return 0;\n\n\t\tconst now = new Date().toISOString();\n\n\t\tconst result = await this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ status, updated_at: now })\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn Number(result.numUpdatedRows ?? 0);\n\t}\n\n\t/**\n\t * Hard-delete a single comment. Replies cascade via FK.\n\t */\n\tasync delete(id: string): Promise<boolean> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn (result.numDeletedRows ?? 0) > 0;\n\t}\n\n\t/**\n\t * Bulk hard-delete comments\n\t */\n\tasync bulkDelete(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(\"_emdash_comments\")\n\t\t\t.where(\"id\", \"in\", ids)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Delete all comments for a content item (cascade on content deletion)\n\t */\n\tasync deleteByContent(collection: string, contentId: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.deleteFrom(\"_emdash_comments\")\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId)\n\t\t\t.executeTakeFirst();\n\n\t\tinvalidateCommentObjectCache();\n\t\treturn Number(result.numDeletedRows ?? 0);\n\t}\n\n\t/**\n\t * Count comments for a content item, optionally filtered by status\n\t */\n\tasync countByContent(\n\t\tcollection: string,\n\t\tcontentId: string,\n\t\tstatus?: CommentStatus,\n\t): Promise<number> {\n\t\tlet query = this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"collection\", \"=\", collection)\n\t\t\t.where(\"content_id\", \"=\", contentId);\n\n\t\tif (status) {\n\t\t\tquery = query.where(\"status\", \"=\", status);\n\t\t}\n\n\t\tconst result = await query.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Count comments grouped by status (for inbox badges)\n\t *\n\t * Uses four parallel COUNT queries with WHERE filters to leverage partial indexes\n\t * (idx_comments_pending, idx_comments_approved, idx_comments_spam, idx_comments_trash)\n\t * instead of a full table GROUP BY scan.\n\t */\n\tasync countByStatus(): Promise<Record<CommentStatus, number>> {\n\t\t// Execute four parallel COUNT queries, each using its partial index\n\t\tconst [pending, approved, spam, trash] = await Promise.all([\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"pending\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"spam\")\n\t\t\t\t.executeTakeFirst(),\n\t\t\tthis.db\n\t\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t\t.where(\"status\", \"=\", \"trash\")\n\t\t\t\t.executeTakeFirst(),\n\t\t]);\n\n\t\treturn {\n\t\t\tpending: Number(pending?.count ?? 0),\n\t\t\tapproved: Number(approved?.count ?? 0),\n\t\t\tspam: Number(spam?.count ?? 0),\n\t\t\ttrash: Number(trash?.count ?? 0),\n\t\t};\n\t}\n\n\t/**\n\t * Count approved comments from a given email address.\n\t * Used for \"first time commenter\" moderation logic.\n\t */\n\tasync countApprovedByEmail(email: string): Promise<number> {\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comments\")\n\t\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t\t.where(\"author_email\", \"=\", email)\n\t\t\t.where(\"status\", \"=\", \"approved\")\n\t\t\t.executeTakeFirst();\n\n\t\treturn Number(result?.count ?? 0);\n\t}\n\n\t/**\n\t * Update the moderation metadata JSON on a comment\n\t */\n\tasync updateModerationMetadata(id: string, metadata: Record<string, unknown>): Promise<void> {\n\t\tawait this.db\n\t\t\t.updateTable(\"_emdash_comments\")\n\t\t\t.set({ moderation_metadata: JSON.stringify(metadata) })\n\t\t\t.where(\"id\", \"=\", id)\n\t\t\t.execute();\n\t}\n\n\t// ---------------------------------------------------------------------------\n\t// Helpers\n\t// ---------------------------------------------------------------------------\n\n\t/**\n\t * Assemble a flat list of comments into a threaded structure (1-level nesting)\n\t */\n\tstatic assembleThreads(comments: Comment[]): Comment[] {\n\t\tconst roots: Comment[] = [];\n\t\tconst childrenMap = new Map<string, Comment[]>();\n\n\t\tfor (const comment of comments) {\n\t\t\tif (comment.parentId) {\n\t\t\t\tconst siblings = childrenMap.get(comment.parentId) ?? [];\n\t\t\t\tsiblings.push(comment);\n\t\t\t\tchildrenMap.set(comment.parentId, siblings);\n\t\t\t} else {\n\t\t\t\troots.push(comment);\n\t\t\t}\n\t\t}\n\n\t\t// Attach children as a non-standard property — callers map to PublicComment.replies\n\t\treturn roots.map((root) => ({\n\t\t\t...root,\n\t\t\t_replies: childrenMap.get(root.id) ?? [],\n\t\t}));\n\t}\n\n\t/**\n\t * Convert a Comment to its public-facing shape\n\t */\n\tstatic toPublicComment(comment: Comment & { _replies?: Comment[] }): PublicComment {\n\t\tconst pub: PublicComment = {\n\t\t\tid: comment.id,\n\t\t\tparentId: comment.parentId,\n\t\t\tauthorName: comment.authorName,\n\t\t\tisRegisteredUser: comment.authorUserId !== null,\n\t\t\tbody: comment.body,\n\t\t\tcreatedAt: comment.createdAt,\n\t\t};\n\n\t\tif (comment._replies && comment._replies.length > 0) {\n\t\t\tpub.replies = comment._replies.map((r) => CommentRepository.toPublicComment(r));\n\t\t}\n\n\t\treturn pub;\n\t}\n\n\t// eslint-disable-next-line @typescript-eslint/no-explicit-any -- selectAll returns runtime row\n\tprivate rowToComment(row: any): Comment {\n\t\treturn {\n\t\t\tid: row.id,\n\t\t\tcollection: row.collection,\n\t\t\tcontentId: row.content_id,\n\t\t\tparentId: row.parent_id,\n\t\t\tauthorName: row.author_name,\n\t\t\tauthorEmail: row.author_email,\n\t\t\tauthorUserId: row.author_user_id,\n\t\t\tbody: row.body,\n\t\t\tstatus: row.status as CommentStatus,\n\t\t\tipHash: row.ip_hash,\n\t\t\tuserAgent: row.user_agent,\n\t\t\tmoderationMetadata: row.moderation_metadata ? safeJsonParse(row.moderation_metadata) : null,\n\t\t\tcreatedAt: row.created_at,\n\t\t\tupdatedAt: row.updated_at,\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Module helpers\n// ---------------------------------------------------------------------------\n\nfunction safeJsonParse(value: string): Record<string, unknown> | null {\n\ttry {\n\t\treturn JSON.parse(value) as Record<string, unknown>;\n\t} catch {\n\t\treturn null;\n\t}\n}\n"],"mappings":";;;;;;;AAQA,MAAM,iBAAiB;AAgEvB,IAAa,oBAAb,MAAa,kBAAkB;CAC9B,YAAY,AAAQ,IAAsB;EAAtB;;;;;CAKpB,MAAM,OAAO,OAA6C;EACzD,MAAM,KAAK,MAAM;EACjB,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAEpC,QAAM,KAAK,GACT,WAAW,mBAAmB,CAC9B,OAAO;GACP;GACA,YAAY,MAAM;GAClB,YAAY,MAAM;GAClB,WAAW,MAAM,YAAY;GAC7B,aAAa,MAAM;GACnB,cAAc,MAAM;GACpB,gBAAgB,MAAM,gBAAgB;GACtC,MAAM,MAAM;GACZ,QAAQ,MAAM,UAAU;GACxB,SAAS,MAAM,UAAU;GACzB,YAAY,MAAM,aAAa;GAC/B,qBAAqB,MAAM,qBACxB,KAAK,UAAU,MAAM,mBAAmB,GACxC;GACH,YAAY;GACZ,YAAY;GACZ,CAAC,CACD,SAAS;AAEX,gCAA8B;EAE9B,MAAM,UAAU,MAAM,KAAK,SAAS,GAAG;AACvC,MAAI,CAAC,QACJ,OAAM,IAAI,MAAM,2BAA2B;AAE5C,SAAO;;;;;CAMR,MAAM,SAAS,IAAqC;EACnD,MAAM,MAAM,MAAM,KAAK,GACrB,WAAW,mBAAmB,CAC9B,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,SAAO,MAAM,KAAK,aAAa,IAAI,GAAG;;;;;;CAOvC,MAAM,cACL,YACA,WACA,UAAuE,EAAE,EACtC;EACnC,MAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI,IAAI;EAEhD,IAAI,QAAQ,KAAK,GACf,WAAW,mBAAmB,CAC9B,WAAW,CACX,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,cAAc,KAAK,UAAU;AAErC,MAAI,QAAQ,OACX,SAAQ,MAAM,MAAM,UAAU,KAAK,QAAQ,OAAO;AAInD,MAAI,QAAQ,QAAQ;GACnB,MAAM,UAAU,aAAa,QAAQ,OAAO;AAC5C,WAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,cAAc,KAAK,QAAQ,WAAW,EACzC,GAAG,IAAI,CAAC,GAAG,cAAc,KAAK,QAAQ,WAAW,EAAE,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC,CAAC,CAC9E,CAAC,CACF;;AAGF,UAAQ,MACN,QAAQ,cAAc,MAAM,CAC5B,QAAQ,MAAM,MAAM,CACpB,MAAM,QAAQ,EAAE;EAElB,MAAM,OAAO,MAAM,MAAM,SAAS;EAClC,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,KAAK,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,KAAK,aAAa,EAAE,CAAC;EAEnE,MAAM,SAAkC,EAAE,OAAO;AACjD,MAAI,WAAW,MAAM,SAAS,GAAG;GAChC,MAAM,OAAO,MAAM,GAAG,GAAG;AACzB,UAAO,aAAa,aAAa,KAAK,WAAW,KAAK,GAAG;;AAE1D,SAAO;;;;;;CAOR,MAAM,aACL,QACA,UAAqF,EAAE,EACpD;EACnC,MAAM,QAAQ,KAAK,IAAI,QAAQ,SAAS,IAAI,IAAI;EAEhD,IAAI,QAAQ,KAAK,GAAG,WAAW,mBAAmB,CAAC,WAAW,CAAC,MAAM,UAAU,KAAK,OAAO;AAE3F,MAAI,QAAQ,WACX,SAAQ,MAAM,MAAM,cAAc,KAAK,QAAQ,WAAW;AAG3D,MAAI,QAAQ,QAAQ;GAGnB,MAAM,OAAO,IADG,QAAQ,OAAO,QAAQ,iBAAiB,OAAO,KAAK,KAAK,CAChD;AACzB,WAAQ,MAAM,OAAO,OACpB,GAAG,GAAG;IACL,GAAY,oBAAoB,KAAK;IACrC,GAAY,qBAAqB,KAAK;IACtC,GAAY,aAAa,KAAK;IAC9B,CAAC,CACF;;AAIF,MAAI,QAAQ,QAAQ;GACnB,MAAM,UAAU,aAAa,QAAQ,OAAO;AAC5C,WAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,cAAc,KAAK,QAAQ,WAAW,EACzC,GAAG,IAAI,CAAC,GAAG,cAAc,KAAK,QAAQ,WAAW,EAAE,GAAG,MAAM,KAAK,QAAQ,GAAG,CAAC,CAAC,CAC9E,CAAC,CACF;;AAGF,UAAQ,MACN,QAAQ,cAAc,OAAO,CAC7B,QAAQ,MAAM,OAAO,CACrB,MAAM,QAAQ,EAAE;EAElB,MAAM,OAAO,MAAM,MAAM,SAAS;EAClC,MAAM,UAAU,KAAK,SAAS;EAC9B,MAAM,QAAQ,KAAK,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,KAAK,aAAa,EAAE,CAAC;EAEnE,MAAM,SAAkC,EAAE,OAAO;AACjD,MAAI,WAAW,MAAM,SAAS,GAAG;GAChC,MAAM,OAAO,MAAM,GAAG,GAAG;AACzB,UAAO,aAAa,aAAa,KAAK,WAAW,KAAK,GAAG;;AAE1D,SAAO;;;;;CAMR,MAAM,aAAa,IAAY,QAAgD;EAC9E,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;AAEpC,QAAM,KAAK,GACT,YAAY,mBAAmB,CAC/B,IAAI;GAAE;GAAQ,YAAY;GAAK,CAAC,CAChC,MAAM,MAAM,KAAK,GAAG,CACpB,SAAS;AAEX,gCAA8B;AAC9B,SAAO,KAAK,SAAS,GAAG;;;;;CAMzB,MAAM,iBAAiB,KAAe,QAAwC;AAC7E,MAAI,IAAI,WAAW,EAAG,QAAO;EAE7B,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;EAEpC,MAAM,SAAS,MAAM,KAAK,GACxB,YAAY,mBAAmB,CAC/B,IAAI;GAAE;GAAQ,YAAY;GAAK,CAAC,CAChC,MAAM,MAAM,MAAM,IAAI,CACtB,kBAAkB;AAEpB,gCAA8B;AAC9B,SAAO,OAAO,OAAO,kBAAkB,EAAE;;;;;CAM1C,MAAM,OAAO,IAA8B;EAC1C,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,mBAAmB,CAC9B,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,gCAA8B;AAC9B,UAAQ,OAAO,kBAAkB,KAAK;;;;;CAMvC,MAAM,WAAW,KAAgC;AAChD,MAAI,IAAI,WAAW,EAAG,QAAO;EAE7B,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,mBAAmB,CAC9B,MAAM,MAAM,MAAM,IAAI,CACtB,kBAAkB;AAEpB,gCAA8B;AAC9B,SAAO,OAAO,OAAO,kBAAkB,EAAE;;;;;CAM1C,MAAM,gBAAgB,YAAoB,WAAoC;EAC7E,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,mBAAmB,CAC9B,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,cAAc,KAAK,UAAU,CACnC,kBAAkB;AAEpB,gCAA8B;AAC9B,SAAO,OAAO,OAAO,kBAAkB,EAAE;;;;;CAM1C,MAAM,eACL,YACA,WACA,QACkB;EAClB,IAAI,QAAQ,KAAK,GACf,WAAW,mBAAmB,CAC9B,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,cAAc,KAAK,WAAW,CACpC,MAAM,cAAc,KAAK,UAAU;AAErC,MAAI,OACH,SAAQ,MAAM,MAAM,UAAU,KAAK,OAAO;EAG3C,MAAM,SAAS,MAAM,MAAM,kBAAkB;AAC7C,SAAO,OAAO,QAAQ,SAAS,EAAE;;;;;;;;;CAUlC,MAAM,gBAAwD;EAE7D,MAAM,CAAC,SAAS,UAAU,MAAM,SAAS,MAAM,QAAQ,IAAI;GAC1D,KAAK,GACH,WAAW,mBAAmB,CAC9B,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,UAAU,KAAK,UAAU,CAC/B,kBAAkB;GACpB,KAAK,GACH,WAAW,mBAAmB,CAC9B,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,UAAU,KAAK,WAAW,CAChC,kBAAkB;GACpB,KAAK,GACH,WAAW,mBAAmB,CAC9B,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,UAAU,KAAK,OAAO,CAC5B,kBAAkB;GACpB,KAAK,GACH,WAAW,mBAAmB,CAC9B,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,UAAU,KAAK,QAAQ,CAC7B,kBAAkB;GACpB,CAAC;AAEF,SAAO;GACN,SAAS,OAAO,SAAS,SAAS,EAAE;GACpC,UAAU,OAAO,UAAU,SAAS,EAAE;GACtC,MAAM,OAAO,MAAM,SAAS,EAAE;GAC9B,OAAO,OAAO,OAAO,SAAS,EAAE;GAChC;;;;;;CAOF,MAAM,qBAAqB,OAAgC;EAC1D,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,mBAAmB,CAC9B,QAAQ,OAAO,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,QAAQ,CAAC,CAC7C,MAAM,gBAAgB,KAAK,MAAM,CACjC,MAAM,UAAU,KAAK,WAAW,CAChC,kBAAkB;AAEpB,SAAO,OAAO,QAAQ,SAAS,EAAE;;;;;CAMlC,MAAM,yBAAyB,IAAY,UAAkD;AAC5F,QAAM,KAAK,GACT,YAAY,mBAAmB,CAC/B,IAAI,EAAE,qBAAqB,KAAK,UAAU,SAAS,EAAE,CAAC,CACtD,MAAM,MAAM,KAAK,GAAG,CACpB,SAAS;;;;;CAUZ,OAAO,gBAAgB,UAAgC;EACtD,MAAM,QAAmB,EAAE;EAC3B,MAAM,8BAAc,IAAI,KAAwB;AAEhD,OAAK,MAAM,WAAW,SACrB,KAAI,QAAQ,UAAU;GACrB,MAAM,WAAW,YAAY,IAAI,QAAQ,SAAS,IAAI,EAAE;AACxD,YAAS,KAAK,QAAQ;AACtB,eAAY,IAAI,QAAQ,UAAU,SAAS;QAE3C,OAAM,KAAK,QAAQ;AAKrB,SAAO,MAAM,KAAK,UAAU;GAC3B,GAAG;GACH,UAAU,YAAY,IAAI,KAAK,GAAG,IAAI,EAAE;GACxC,EAAE;;;;;CAMJ,OAAO,gBAAgB,SAA4D;EAClF,MAAM,MAAqB;GAC1B,IAAI,QAAQ;GACZ,UAAU,QAAQ;GAClB,YAAY,QAAQ;GACpB,kBAAkB,QAAQ,iBAAiB;GAC3C,MAAM,QAAQ;GACd,WAAW,QAAQ;GACnB;AAED,MAAI,QAAQ,YAAY,QAAQ,SAAS,SAAS,EACjD,KAAI,UAAU,QAAQ,SAAS,KAAK,MAAM,kBAAkB,gBAAgB,EAAE,CAAC;AAGhF,SAAO;;CAIR,AAAQ,aAAa,KAAmB;AACvC,SAAO;GACN,IAAI,IAAI;GACR,YAAY,IAAI;GAChB,WAAW,IAAI;GACf,UAAU,IAAI;GACd,YAAY,IAAI;GAChB,aAAa,IAAI;GACjB,cAAc,IAAI;GAClB,MAAM,IAAI;GACV,QAAQ,IAAI;GACZ,QAAQ,IAAI;GACZ,WAAW,IAAI;GACf,oBAAoB,IAAI,sBAAsB,cAAc,IAAI,oBAAoB,GAAG;GACvF,WAAW,IAAI;GACf,WAAW,IAAI;GACf;;;AAQH,SAAS,cAAc,OAA+C;AACrE,KAAI;AACH,SAAO,KAAK,MAAM,MAAM;SACjB;AACP,SAAO"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-
|
|
1
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-DfPax1I0.mjs";
|
|
2
2
|
import { ulid } from "ulidx";
|
|
3
3
|
|
|
4
4
|
//#region src/database/repositories/comment-reaction.ts
|
|
@@ -82,4 +82,4 @@ var CommentReactionRepository = class {
|
|
|
82
82
|
|
|
83
83
|
//#endregion
|
|
84
84
|
export { CommentReactionRepository as t };
|
|
85
|
-
//# sourceMappingURL=comment-reaction-
|
|
85
|
+
//# sourceMappingURL=comment-reaction-D6qfHKBc.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comment-reaction-
|
|
1
|
+
{"version":3,"file":"comment-reaction-D6qfHKBc.mjs","names":[],"sources":["../src/database/repositories/comment-reaction.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport { SQL_BATCH_SIZE, chunks } from \"../../utils/chunks.js\";\nimport type { Database } from \"../types.js\";\n\n/** Per-comment reaction counts: `{ like: 12, love: 3 }`. */\nexport type ReactionCounts = Record<string, number>;\n\nexport interface ToggleReactionInput {\n\tcommentId: string;\n\treaction: string;\n\tvoterHash: string;\n}\n\n/**\n * Repository for comment reactions (likes / emoji).\n *\n * Reactions are deduped per (comment, voter, reaction) by a unique index, so\n * a second toggle of the same reaction by the same voter removes it.\n */\nexport class CommentReactionRepository {\n\tconstructor(private db: Kysely<Database>) {}\n\n\t/**\n\t * Toggle a reaction for a voter on a comment.\n\t *\n\t * @returns `{ reacted: true }` if the reaction was added, `{ reacted: false }`\n\t * if an existing reaction was removed.\n\t */\n\tasync toggle(input: ToggleReactionInput): Promise<{ reacted: boolean }> {\n\t\t// Idempotent add: insert, or no-op if this voter already has this\n\t\t// reaction. Using ON CONFLICT (rather than read-then-write) avoids a\n\t\t// TOCTOU race where two concurrent toggles both see \"no row\" and the\n\t\t// second INSERT violates the unique index — the old path surfaced that\n\t\t// as a 500. The unique index doubles as the conflict target.\n\t\tconst inserted = await this.db\n\t\t\t.insertInto(\"_emdash_comment_reactions\")\n\t\t\t.values({\n\t\t\t\tid: ulid(),\n\t\t\t\tcomment_id: input.commentId,\n\t\t\t\treaction: input.reaction,\n\t\t\t\tvoter_hash: input.voterHash,\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t})\n\t\t\t.onConflict((oc) => oc.columns([\"comment_id\", \"voter_hash\", \"reaction\"]).doNothing())\n\t\t\t.executeTakeFirst();\n\n\t\tif ((inserted.numInsertedOrUpdatedRows ?? 0n) > 0n) {\n\t\t\treturn { reacted: true };\n\t\t}\n\n\t\t// The reaction already existed → toggle it off.\n\t\tawait this.db\n\t\t\t.deleteFrom(\"_emdash_comment_reactions\")\n\t\t\t.where(\"comment_id\", \"=\", input.commentId)\n\t\t\t.where(\"voter_hash\", \"=\", input.voterHash)\n\t\t\t.where(\"reaction\", \"=\", input.reaction)\n\t\t\t.execute();\n\t\treturn { reacted: false };\n\t}\n\n\t/**\n\t * Aggregate reaction counts for a set of comments.\n\t *\n\t * @returns a Map keyed by comment id; comments with no reactions are absent.\n\t */\n\tasync countsForComments(commentIds: string[]): Promise<Map<string, ReactionCounts>> {\n\t\tconst result = new Map<string, ReactionCounts>();\n\t\tif (commentIds.length === 0) return result;\n\n\t\tfor (const batch of chunks(commentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t\t.select([\"comment_id\", \"reaction\"])\n\t\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t\t.where(\"comment_id\", \"in\", batch)\n\t\t\t\t.groupBy([\"comment_id\", \"reaction\"])\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst counts = result.get(row.comment_id) ?? {};\n\t\t\t\tcounts[row.reaction] = Number(row.count);\n\t\t\t\tresult.set(row.comment_id, counts);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Which reactions a given voter has set, per comment.\n\t *\n\t * @returns a Map keyed by comment id whose values are the reaction names the\n\t * voter has active on that comment.\n\t */\n\tasync viewerReactions(commentIds: string[], voterHash: string): Promise<Map<string, string[]>> {\n\t\tconst result = new Map<string, string[]>();\n\t\tif (commentIds.length === 0) return result;\n\n\t\tfor (const batch of chunks(commentIds, SQL_BATCH_SIZE)) {\n\t\t\tconst rows = await this.db\n\t\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t\t.select([\"comment_id\", \"reaction\"])\n\t\t\t\t.where(\"comment_id\", \"in\", batch)\n\t\t\t\t.where(\"voter_hash\", \"=\", voterHash)\n\t\t\t\t.execute();\n\n\t\t\tfor (const row of rows) {\n\t\t\t\tconst list = result.get(row.comment_id) ?? [];\n\t\t\t\tlist.push(row.reaction);\n\t\t\t\tresult.set(row.comment_id, list);\n\t\t\t}\n\t\t}\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Count a voter's reactions within a recent time window (for rate limiting).\n\t */\n\tasync countRecentByVoter(voterHash: string, windowMinutes = 10): Promise<number> {\n\t\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\t\tconst result = await this.db\n\t\t\t.selectFrom(\"_emdash_comment_reactions\")\n\t\t\t.select((eb) => eb.fn.count<number>(\"id\").as(\"count\"))\n\t\t\t.where(\"voter_hash\", \"=\", voterHash)\n\t\t\t.where(\"created_at\", \">\", cutoff)\n\t\t\t.executeTakeFirst();\n\t\treturn Number(result?.count ?? 0);\n\t}\n}\n"],"mappings":";;;;;;;;;;AAqBA,IAAa,4BAAb,MAAuC;CACtC,YAAY,AAAQ,IAAsB;EAAtB;;;;;;;;CAQpB,MAAM,OAAO,OAA2D;AAkBvE,QAZiB,MAAM,KAAK,GAC1B,WAAW,4BAA4B,CACvC,OAAO;GACP,IAAI,MAAM;GACV,YAAY,MAAM;GAClB,UAAU,MAAM;GAChB,YAAY,MAAM;GAClB,6BAAY,IAAI,MAAM,EAAC,aAAa;GACpC,CAAC,CACD,YAAY,OAAO,GAAG,QAAQ;GAAC;GAAc;GAAc;GAAW,CAAC,CAAC,WAAW,CAAC,CACpF,kBAAkB,EAEN,4BAA4B,MAAM,GAC/C,QAAO,EAAE,SAAS,MAAM;AAIzB,QAAM,KAAK,GACT,WAAW,4BAA4B,CACvC,MAAM,cAAc,KAAK,MAAM,UAAU,CACzC,MAAM,cAAc,KAAK,MAAM,UAAU,CACzC,MAAM,YAAY,KAAK,MAAM,SAAS,CACtC,SAAS;AACX,SAAO,EAAE,SAAS,OAAO;;;;;;;CAQ1B,MAAM,kBAAkB,YAA4D;EACnF,MAAM,yBAAS,IAAI,KAA6B;AAChD,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,OAAK,MAAM,SAAS,OAAO,YAAY,eAAe,EAAE;GACvD,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,4BAA4B,CACvC,OAAO,CAAC,cAAc,WAAW,CAAC,CAClC,QAAQ,OAAO,GAAG,GAAG,MAAc,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrD,MAAM,cAAc,MAAM,MAAM,CAChC,QAAQ,CAAC,cAAc,WAAW,CAAC,CACnC,SAAS;AAEX,QAAK,MAAM,OAAO,MAAM;IACvB,MAAM,SAAS,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;AAC/C,WAAO,IAAI,YAAY,OAAO,IAAI,MAAM;AACxC,WAAO,IAAI,IAAI,YAAY,OAAO;;;AAIpC,SAAO;;;;;;;;CASR,MAAM,gBAAgB,YAAsB,WAAmD;EAC9F,MAAM,yBAAS,IAAI,KAAuB;AAC1C,MAAI,WAAW,WAAW,EAAG,QAAO;AAEpC,OAAK,MAAM,SAAS,OAAO,YAAY,eAAe,EAAE;GACvD,MAAM,OAAO,MAAM,KAAK,GACtB,WAAW,4BAA4B,CACvC,OAAO,CAAC,cAAc,WAAW,CAAC,CAClC,MAAM,cAAc,MAAM,MAAM,CAChC,MAAM,cAAc,KAAK,UAAU,CACnC,SAAS;AAEX,QAAK,MAAM,OAAO,MAAM;IACvB,MAAM,OAAO,OAAO,IAAI,IAAI,WAAW,IAAI,EAAE;AAC7C,SAAK,KAAK,IAAI,SAAS;AACvB,WAAO,IAAI,IAAI,YAAY,KAAK;;;AAIlC,SAAO;;;;;CAMR,MAAM,mBAAmB,WAAmB,gBAAgB,IAAqB;EAChF,MAAM,0BAAS,IAAI,KAAK,KAAK,KAAK,GAAG,gBAAgB,KAAK,IAAK,EAAC,aAAa;EAC7E,MAAM,SAAS,MAAM,KAAK,GACxB,WAAW,4BAA4B,CACvC,QAAQ,OAAO,GAAG,GAAG,MAAc,KAAK,CAAC,GAAG,QAAQ,CAAC,CACrD,MAAM,cAAc,KAAK,UAAU,CACnC,MAAM,cAAc,KAAK,OAAO,CAChC,kBAAkB;AACpB,SAAO,OAAO,QAAQ,SAAS,EAAE"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as CommentRepository } from "./comment-
|
|
1
|
+
import { i as InvalidCursorError } from "./types-CQPWhPxE.mjs";
|
|
2
|
+
import { t as CommentRepository } from "./comment-Dxlep6cE.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/api/handlers/comments.ts
|
|
5
5
|
async function handleCommentList(db, collection, contentId, options = {}) {
|
|
@@ -201,4 +201,4 @@ async function hashIp(ip, salt) {
|
|
|
201
201
|
|
|
202
202
|
//#endregion
|
|
203
203
|
export { handleCommentGet as a, hashIp as c, handleCommentDelete as i, handleCommentBulk as n, handleCommentInbox as o, handleCommentCounts as r, handleCommentList as s, checkRateLimit as t };
|
|
204
|
-
//# sourceMappingURL=comments-
|
|
204
|
+
//# sourceMappingURL=comments-CTOuhTSX.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comments-QPKgK3nk.mjs","names":[],"sources":["../src/api/handlers/comments.ts"],"sourcesContent":["/**\n * Comment handlers — business logic for comment API routes.\n *\n * Standalone functions that return ApiResult<T>. Routes are thin wrappers.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport type { Comment, CommentStatus, PublicComment } from \"../../database/repositories/comment.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Public: List approved comments for content\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n\toptions: { limit?: number; cursor?: string; threaded?: boolean } = {},\n): Promise<ApiResult<{ items: PublicComment[]; nextCursor?: string; total: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\t// Get total approved count\n\t\tconst total = await repo.countByContent(collection, contentId, \"approved\");\n\n\t\tlet publicItems: PublicComment[];\n\t\tlet nextCursor: string | undefined;\n\n\t\tif (options.threaded) {\n\t\t\t// Threaded mode: fetch all approved comments (capped) so threading\n\t\t\t// doesn't lose children that would fall on later pages.\n\t\t\tconst MAX_THREADED = 500;\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: MAX_THREADED,\n\t\t\t});\n\t\t\tconst threaded = CommentRepository.assembleThreads(result.items);\n\t\t\tpublicItems = threaded.map((c) => CommentRepository.toPublicComment(c));\n\t\t\t// No cursor for threaded mode — all comments returned at once\n\t\t} else {\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: options.limit,\n\t\t\t\tcursor: options.cursor,\n\t\t\t});\n\t\t\tpublicItems = result.items.map((c) => CommentRepository.toPublicComment(c));\n\t\t\tnextCursor = result.nextCursor;\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: publicItems,\n\t\t\t\tnextCursor,\n\t\t\t\ttotal,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Moderation inbox\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentInbox(\n\tdb: Kysely<Database>,\n\toptions: {\n\t\tstatus?: CommentStatus;\n\t\tcollection?: string;\n\t\tsearch?: string;\n\t\tlimit?: number;\n\t\tcursor?: string;\n\t} = {},\n): Promise<ApiResult<{ items: Comment[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst status = options.status ?? \"pending\";\n\n\t\tconst result = await repo.findByStatus(status, {\n\t\t\tcollection: options.collection,\n\t\t\tsearch: options.search,\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment inbox error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_INBOX_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Status counts for inbox badges\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentCounts(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<Record<CommentStatus, number>>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst counts = await repo.countByStatus();\n\t\treturn { success: true, data: counts };\n\t} catch (error) {\n\t\tconsole.error(\"Comment counts error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_COUNTS_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment counts\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Get single comment detail\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst comment = await repo.findById(id);\n\n\t\tif (!comment) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: comment };\n\t} catch (error) {\n\t\tconsole.error(\"Comment get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Change comment status\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentStatusChange(\n\tdb: Kysely<Database>,\n\tid: string,\n\tstatus: CommentStatus,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst updated = await repo.updateStatus(id, status);\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: updated };\n\t} catch (error) {\n\t\tconsole.error(\"Comment status change error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_STATUS_ERROR\",\n\t\t\t\tmessage: \"Failed to update comment status\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Hard delete comment\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Bulk operations\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentBulk(\n\tdb: Kysely<Database>,\n\tids: string[],\n\taction: \"approve\" | \"spam\" | \"trash\" | \"delete\",\n): Promise<ApiResult<{ affected: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\tlet affected: number;\n\t\tif (action === \"delete\") {\n\t\t\taffected = await repo.bulkDelete(ids);\n\t\t} else {\n\t\t\tconst statusMap: Record<string, CommentStatus> = {\n\t\t\t\tapprove: \"approved\",\n\t\t\t\tspam: \"spam\",\n\t\t\t\ttrash: \"trash\",\n\t\t\t};\n\t\t\taffected = await repo.bulkUpdateStatus(ids, statusMap[action]);\n\t\t}\n\n\t\treturn { success: true, data: { affected } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment bulk error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_BULK_ERROR\",\n\t\t\t\tmessage: \"Failed to perform bulk operation\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Anti-spam: Rate limiting\n// ---------------------------------------------------------------------------\n\n/**\n * Check if an IP has exceeded the comment rate limit.\n * Uses ip_hash in the comments table — no separate counter storage.\n */\nexport async function checkRateLimit(\n\tdb: Kysely<Database>,\n\tipHash: string,\n\tmaxPerWindow: number = 5,\n\twindowMinutes: number = 10,\n): Promise<boolean> {\n\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\n\t// Count recent comments from this IP\n\tconst result = await db\n\t\t.selectFrom(\"_emdash_comments\")\n\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t.where(\"ip_hash\", \"=\", ipHash)\n\t\t.where(\"created_at\", \">\", cutoff)\n\t\t.executeTakeFirst();\n\n\tconst count = Number(result?.count ?? 0);\n\treturn count >= maxPerWindow;\n}\n\n/**\n * Hash an IP address for storage (never store cleartext IPs).\n *\n * Uses full SHA-256 with a site-specific salt to prevent rainbow-table\n * recovery of IPs. The salt must be provided by the caller — typically\n * via `resolveSecretsCached(db).ipSalt` from `#config/secrets.js`. The\n * salt is generated and persisted on first need so it's stable across\n * requests within a deployment but unique per install.\n */\nexport async function hashIp(ip: string, salt: string): Promise<string> {\n\tconst data = `ip:${salt}:${ip}`;\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(data));\n\treturn Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"comments-CTOuhTSX.mjs","names":[],"sources":["../src/api/handlers/comments.ts"],"sourcesContent":["/**\n * Comment handlers — business logic for comment API routes.\n *\n * Standalone functions that return ApiResult<T>. Routes are thin wrappers.\n */\n\nimport type { Kysely } from \"kysely\";\n\nimport { CommentRepository } from \"../../database/repositories/comment.js\";\nimport type { Comment, CommentStatus, PublicComment } from \"../../database/repositories/comment.js\";\nimport { InvalidCursorError } from \"../../database/repositories/types.js\";\nimport type { Database } from \"../../database/types.js\";\nimport type { ApiResult } from \"../types.js\";\n\n// ---------------------------------------------------------------------------\n// Public: List approved comments for content\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentList(\n\tdb: Kysely<Database>,\n\tcollection: string,\n\tcontentId: string,\n\toptions: { limit?: number; cursor?: string; threaded?: boolean } = {},\n): Promise<ApiResult<{ items: PublicComment[]; nextCursor?: string; total: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\t// Get total approved count\n\t\tconst total = await repo.countByContent(collection, contentId, \"approved\");\n\n\t\tlet publicItems: PublicComment[];\n\t\tlet nextCursor: string | undefined;\n\n\t\tif (options.threaded) {\n\t\t\t// Threaded mode: fetch all approved comments (capped) so threading\n\t\t\t// doesn't lose children that would fall on later pages.\n\t\t\tconst MAX_THREADED = 500;\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: MAX_THREADED,\n\t\t\t});\n\t\t\tconst threaded = CommentRepository.assembleThreads(result.items);\n\t\t\tpublicItems = threaded.map((c) => CommentRepository.toPublicComment(c));\n\t\t\t// No cursor for threaded mode — all comments returned at once\n\t\t} else {\n\t\t\tconst result = await repo.findByContent(collection, contentId, {\n\t\t\t\tstatus: \"approved\",\n\t\t\t\tlimit: options.limit,\n\t\t\t\tcursor: options.cursor,\n\t\t\t});\n\t\t\tpublicItems = result.items.map((c) => CommentRepository.toPublicComment(c));\n\t\t\tnextCursor = result.nextCursor;\n\t\t}\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: publicItems,\n\t\t\t\tnextCursor,\n\t\t\t\ttotal,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment list error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_LIST_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Moderation inbox\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentInbox(\n\tdb: Kysely<Database>,\n\toptions: {\n\t\tstatus?: CommentStatus;\n\t\tcollection?: string;\n\t\tsearch?: string;\n\t\tlimit?: number;\n\t\tcursor?: string;\n\t} = {},\n): Promise<ApiResult<{ items: Comment[]; nextCursor?: string }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst status = options.status ?? \"pending\";\n\n\t\tconst result = await repo.findByStatus(status, {\n\t\t\tcollection: options.collection,\n\t\t\tsearch: options.search,\n\t\t\tlimit: options.limit,\n\t\t\tcursor: options.cursor,\n\t\t});\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\titems: result.items,\n\t\t\t\tnextCursor: result.nextCursor,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tif (error instanceof InvalidCursorError) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"INVALID_CURSOR\", message: error.message },\n\t\t\t};\n\t\t}\n\t\tconsole.error(\"Comment inbox error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_INBOX_ERROR\",\n\t\t\t\tmessage: \"Failed to list comments\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Status counts for inbox badges\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentCounts(\n\tdb: Kysely<Database>,\n): Promise<ApiResult<Record<CommentStatus, number>>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst counts = await repo.countByStatus();\n\t\treturn { success: true, data: counts };\n\t} catch (error) {\n\t\tconsole.error(\"Comment counts error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_COUNTS_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment counts\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Get single comment detail\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentGet(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst comment = await repo.findById(id);\n\n\t\tif (!comment) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: comment };\n\t} catch (error) {\n\t\tconsole.error(\"Comment get error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_GET_ERROR\",\n\t\t\t\tmessage: \"Failed to get comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Change comment status\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentStatusChange(\n\tdb: Kysely<Database>,\n\tid: string,\n\tstatus: CommentStatus,\n): Promise<ApiResult<Comment>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst updated = await repo.updateStatus(id, status);\n\n\t\tif (!updated) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: updated };\n\t} catch (error) {\n\t\tconsole.error(\"Comment status change error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_STATUS_ERROR\",\n\t\t\t\tmessage: \"Failed to update comment status\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Hard delete comment\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentDelete(\n\tdb: Kysely<Database>,\n\tid: string,\n): Promise<ApiResult<{ deleted: true }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\t\tconst deleted = await repo.delete(id);\n\n\t\tif (!deleted) {\n\t\t\treturn {\n\t\t\t\tsuccess: false,\n\t\t\t\terror: { code: \"NOT_FOUND\", message: `Comment not found: ${id}` },\n\t\t\t};\n\t\t}\n\n\t\treturn { success: true, data: { deleted: true } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment delete error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_DELETE_ERROR\",\n\t\t\t\tmessage: \"Failed to delete comment\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Admin: Bulk operations\n// ---------------------------------------------------------------------------\n\nexport async function handleCommentBulk(\n\tdb: Kysely<Database>,\n\tids: string[],\n\taction: \"approve\" | \"spam\" | \"trash\" | \"delete\",\n): Promise<ApiResult<{ affected: number }>> {\n\ttry {\n\t\tconst repo = new CommentRepository(db);\n\n\t\tlet affected: number;\n\t\tif (action === \"delete\") {\n\t\t\taffected = await repo.bulkDelete(ids);\n\t\t} else {\n\t\t\tconst statusMap: Record<string, CommentStatus> = {\n\t\t\t\tapprove: \"approved\",\n\t\t\t\tspam: \"spam\",\n\t\t\t\ttrash: \"trash\",\n\t\t\t};\n\t\t\taffected = await repo.bulkUpdateStatus(ids, statusMap[action]);\n\t\t}\n\n\t\treturn { success: true, data: { affected } };\n\t} catch (error) {\n\t\tconsole.error(\"Comment bulk error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"COMMENT_BULK_ERROR\",\n\t\t\t\tmessage: \"Failed to perform bulk operation\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// Anti-spam: Rate limiting\n// ---------------------------------------------------------------------------\n\n/**\n * Check if an IP has exceeded the comment rate limit.\n * Uses ip_hash in the comments table — no separate counter storage.\n */\nexport async function checkRateLimit(\n\tdb: Kysely<Database>,\n\tipHash: string,\n\tmaxPerWindow: number = 5,\n\twindowMinutes: number = 10,\n): Promise<boolean> {\n\tconst cutoff = new Date(Date.now() - windowMinutes * 60 * 1000).toISOString();\n\n\t// Count recent comments from this IP\n\tconst result = await db\n\t\t.selectFrom(\"_emdash_comments\")\n\t\t.select((eb) => eb.fn.count(\"id\").as(\"count\"))\n\t\t.where(\"ip_hash\", \"=\", ipHash)\n\t\t.where(\"created_at\", \">\", cutoff)\n\t\t.executeTakeFirst();\n\n\tconst count = Number(result?.count ?? 0);\n\treturn count >= maxPerWindow;\n}\n\n/**\n * Hash an IP address for storage (never store cleartext IPs).\n *\n * Uses full SHA-256 with a site-specific salt to prevent rainbow-table\n * recovery of IPs. The salt must be provided by the caller — typically\n * via `resolveSecretsCached(db).ipSalt` from `#config/secrets.js`. The\n * salt is generated and persisted on first need so it's stable across\n * requests within a deployment but unique per install.\n */\nexport async function hashIp(ip: string, salt: string): Promise<string> {\n\tconst data = `ip:${salt}:${ip}`;\n\tconst buf = await crypto.subtle.digest(\"SHA-256\", new TextEncoder().encode(data));\n\treturn Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"components-
|
|
1
|
+
{"version":3,"file":"components-CQy3lh9x.mjs","names":[],"sources":["../src/widgets/components.ts"],"sourcesContent":["import type { WidgetComponentDef } from \"./types.js\";\n\n/**\n * Core widget components registry\n * These are built-in widgets that ship with EmDash\n */\nexport const coreWidgetComponents: WidgetComponentDef[] = [\n\t{\n\t\tid: \"core:recent-posts\",\n\t\tlabel: \"Recent Posts\",\n\t\tdescription: \"Display a list of recent posts\",\n\t\tprops: {\n\t\t\tcount: {\n\t\t\t\ttype: \"number\",\n\t\t\t\tlabel: \"Number of posts\",\n\t\t\t\tdefault: 5,\n\t\t\t},\n\t\t\tshowThumbnails: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show thumbnails\",\n\t\t\t\tdefault: false,\n\t\t\t},\n\t\t\tshowDate: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show date\",\n\t\t\t\tdefault: true,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:categories\",\n\t\tlabel: \"Categories\",\n\t\tdescription: \"Display category list\",\n\t\tprops: {\n\t\t\tshowCount: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show post count\",\n\t\t\t\tdefault: true,\n\t\t\t},\n\t\t\thierarchical: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show hierarchy\",\n\t\t\t\tdefault: true,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:tags\",\n\t\tlabel: \"Tags\",\n\t\tdescription: \"Display tag cloud\",\n\t\tprops: {\n\t\t\tshowCount: {\n\t\t\t\ttype: \"boolean\",\n\t\t\t\tlabel: \"Show count\",\n\t\t\t\tdefault: false,\n\t\t\t},\n\t\t\tlimit: {\n\t\t\t\ttype: \"number\",\n\t\t\t\tlabel: \"Maximum tags\",\n\t\t\t\tdefault: 20,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:search\",\n\t\tlabel: \"Search\",\n\t\tdescription: \"Search form\",\n\t\tprops: {\n\t\t\tplaceholder: {\n\t\t\t\ttype: \"string\",\n\t\t\t\tlabel: \"Placeholder text\",\n\t\t\t\tdefault: \"Search...\",\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tid: \"core:archives\",\n\t\tlabel: \"Archives\",\n\t\tdescription: \"Monthly/yearly archives\",\n\t\tprops: {\n\t\t\ttype: {\n\t\t\t\ttype: \"select\",\n\t\t\t\tlabel: \"Group by\",\n\t\t\t\tdefault: \"monthly\",\n\t\t\t\toptions: [\n\t\t\t\t\t{ value: \"monthly\", label: \"Monthly\" },\n\t\t\t\t\t{ value: \"yearly\", label: \"Yearly\" },\n\t\t\t\t],\n\t\t\t},\n\t\t\tlimit: {\n\t\t\t\ttype: \"number\",\n\t\t\t\tlabel: \"Limit\",\n\t\t\t\tdefault: 12,\n\t\t\t},\n\t\t},\n\t},\n];\n\n/**\n * Get all widget component definitions (core + plugin-registered)\n * For now, only returns core components. Plugin widgets will be added later.\n */\nexport function getWidgetComponents(): WidgetComponentDef[] {\n\treturn [...coreWidgetComponents];\n}\n"],"mappings":";;;;;AAMA,MAAa,uBAA6C;CACzD;EACC,IAAI;EACJ,OAAO;EACP,aAAa;EACb,OAAO;GACN,OAAO;IACN,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD,gBAAgB;IACf,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD,UAAU;IACT,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD;EACD;CACD;EACC,IAAI;EACJ,OAAO;EACP,aAAa;EACb,OAAO;GACN,WAAW;IACV,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD,cAAc;IACb,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD;EACD;CACD;EACC,IAAI;EACJ,OAAO;EACP,aAAa;EACb,OAAO;GACN,WAAW;IACV,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD,OAAO;IACN,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD;EACD;CACD;EACC,IAAI;EACJ,OAAO;EACP,aAAa;EACb,OAAO,EACN,aAAa;GACZ,MAAM;GACN,OAAO;GACP,SAAS;GACT,EACD;EACD;CACD;EACC,IAAI;EACJ,OAAO;EACP,aAAa;EACb,OAAO;GACN,MAAM;IACL,MAAM;IACN,OAAO;IACP,SAAS;IACT,SAAS,CACR;KAAE,OAAO;KAAW,OAAO;KAAW,EACtC;KAAE,OAAO;KAAU,OAAO;KAAU,CACpC;IACD;GACD,OAAO;IACN,MAAM;IACN,OAAO;IACP,SAAS;IACT;GACD;EACD;CACD;;;;;AAMD,SAAgB,sBAA4C;AAC3D,QAAO,CAAC,GAAG,qBAAqB"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
//#region src/i18n/config.ts
|
|
2
|
+
const LOCALE_CODE_PATTERN = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
|
|
2
3
|
const I18N_CONFIG_KEY = Symbol.for("emdash:i18n-config");
|
|
3
4
|
function configStore() {
|
|
4
5
|
return globalThis;
|
|
@@ -21,6 +22,21 @@ function getI18nConfig() {
|
|
|
21
22
|
function resolveConfiguredLocale(locale) {
|
|
22
23
|
return getI18nConfig()?.locales.find((configured) => configured.toLowerCase() === locale.toLowerCase()) ?? locale;
|
|
23
24
|
}
|
|
25
|
+
/** Whether a string matches the locale-code subset accepted by EmDash. */
|
|
26
|
+
function isValidLocaleCode(locale) {
|
|
27
|
+
return LOCALE_CODE_PATTERN.test(locale);
|
|
28
|
+
}
|
|
29
|
+
/** Resolve the locale for a new content row. */
|
|
30
|
+
function resolveContentCreateLocale(locale, config = getI18nConfig()) {
|
|
31
|
+
if (locale !== void 0) {
|
|
32
|
+
if (typeof locale !== "string") throw new Error("Invalid locale code: expected a string");
|
|
33
|
+
if (!isValidLocaleCode(locale)) throw new Error(`Invalid locale code: "${locale}"`);
|
|
34
|
+
const configured = config?.locales.find((candidate) => candidate.toLowerCase() === locale.toLowerCase());
|
|
35
|
+
if (config && !configured) throw new Error(`Locale "${locale}" is not configured for this site`);
|
|
36
|
+
return configured ?? locale;
|
|
37
|
+
}
|
|
38
|
+
return config?.defaultLocale ?? "en";
|
|
39
|
+
}
|
|
24
40
|
/**
|
|
25
41
|
* Check if i18n is enabled.
|
|
26
42
|
* Returns true when multiple locales are configured.
|
|
@@ -52,5 +68,5 @@ function getFallbackChain(locale) {
|
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
//#endregion
|
|
55
|
-
export {
|
|
56
|
-
//# sourceMappingURL=config-
|
|
71
|
+
export { resolveConfiguredLocale as a, isI18nEnabled as i, getFallbackChain as n, resolveContentCreateLocale as o, getI18nConfig as r, setI18nConfig as s, LOCALE_CODE_PATTERN as t };
|
|
72
|
+
//# sourceMappingURL=config-C3EBstHi.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-C3EBstHi.mjs","names":[],"sources":["../src/i18n/config.ts"],"sourcesContent":["/**\n * EmDash i18n Configuration\n *\n * Reads locale configuration from the virtual module (sourced from Astro config).\n * Initialized during runtime startup, then available via getI18nConfig().\n */\n\nexport interface I18nConfig {\n\tdefaultLocale: string;\n\tlocales: string[];\n\tfallback?: Record<string, string>;\n\tprefixDefaultLocale?: boolean;\n}\n\nexport const LOCALE_CODE_PATTERN = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;\n\nconst I18N_CONFIG_KEY = Symbol.for(\"emdash:i18n-config\");\n\nfunction configStore(): Record<symbol, I18nConfig | null | undefined> {\n\treturn globalThis;\n}\n\n/**\n * Initialize i18n config from virtual module data.\n * Called during runtime initialization.\n */\nexport function setI18nConfig(config: I18nConfig | null): void {\n\tconfigStore()[I18N_CONFIG_KEY] = config;\n}\n\n/**\n * Get the current i18n config.\n * Returns null if i18n is not configured.\n */\nexport function getI18nConfig(): I18nConfig | null {\n\treturn configStore()[I18N_CONFIG_KEY] ?? null;\n}\n\n/** Match a locale to the exact casing used by the site configuration. */\nexport function resolveConfiguredLocale(locale: string): string {\n\tconst config = getI18nConfig();\n\treturn (\n\t\tconfig?.locales.find((configured) => configured.toLowerCase() === locale.toLowerCase()) ??\n\t\tlocale\n\t);\n}\n\n/** Whether a string matches the locale-code subset accepted by EmDash. */\nexport function isValidLocaleCode(locale: string): boolean {\n\treturn LOCALE_CODE_PATTERN.test(locale);\n}\n\n/** Resolve the locale for a new content row. */\nexport function resolveContentCreateLocale(\n\tlocale: unknown,\n\tconfig: I18nConfig | null = getI18nConfig(),\n): string {\n\tif (locale !== undefined) {\n\t\tif (typeof locale !== \"string\") {\n\t\t\tthrow new Error(\"Invalid locale code: expected a string\");\n\t\t}\n\t\tif (!isValidLocaleCode(locale)) {\n\t\t\tthrow new Error(`Invalid locale code: \"${locale}\"`);\n\t\t}\n\n\t\tconst configured = config?.locales.find(\n\t\t\t(candidate) => candidate.toLowerCase() === locale.toLowerCase(),\n\t\t);\n\t\tif (config && !configured) {\n\t\t\tthrow new Error(`Locale \"${locale}\" is not configured for this site`);\n\t\t}\n\t\treturn configured ?? locale;\n\t}\n\n\treturn config?.defaultLocale ?? \"en\";\n}\n\n/**\n * Check if i18n is enabled.\n * Returns true when multiple locales are configured.\n */\nexport function isI18nEnabled(): boolean {\n\tconst config = getI18nConfig();\n\treturn config != null && config.locales.length > 1;\n}\n\n/**\n * Resolve fallback locale chain for a given locale.\n * Returns array of locales to try, from most preferred to least.\n * Always ends with defaultLocale.\n */\nexport function getFallbackChain(locale: string): string[] {\n\tconst config = getI18nConfig();\n\tif (!config) return [locale];\n\n\tconst chain: string[] = [locale];\n\tlet current = locale;\n\tconst visited = new Set<string>([locale]);\n\n\twhile (config.fallback?.[current]) {\n\t\t// eslint-disable-next-line typescript/no-unnecessary-type-assertion -- noUncheckedIndexedAccess\n\t\tconst next = config.fallback[current]!;\n\t\tif (visited.has(next)) break; // prevent cycles\n\t\tchain.push(next);\n\t\tvisited.add(next);\n\t\tcurrent = next;\n\t}\n\n\t// Always end with defaultLocale if not already in chain\n\tif (!visited.has(config.defaultLocale)) {\n\t\tchain.push(config.defaultLocale);\n\t}\n\n\treturn chain;\n}\n"],"mappings":";AAcA,MAAa,sBAAsB;AAEnC,MAAM,kBAAkB,OAAO,IAAI,qBAAqB;AAExD,SAAS,cAA6D;AACrE,QAAO;;;;;;AAOR,SAAgB,cAAc,QAAiC;AAC9D,cAAa,CAAC,mBAAmB;;;;;;AAOlC,SAAgB,gBAAmC;AAClD,QAAO,aAAa,CAAC,oBAAoB;;;AAI1C,SAAgB,wBAAwB,QAAwB;AAE/D,QADe,eAAe,EAErB,QAAQ,MAAM,eAAe,WAAW,aAAa,KAAK,OAAO,aAAa,CAAC,IACvF;;;AAKF,SAAgB,kBAAkB,QAAyB;AAC1D,QAAO,oBAAoB,KAAK,OAAO;;;AAIxC,SAAgB,2BACf,QACA,SAA4B,eAAe,EAClC;AACT,KAAI,WAAW,QAAW;AACzB,MAAI,OAAO,WAAW,SACrB,OAAM,IAAI,MAAM,yCAAyC;AAE1D,MAAI,CAAC,kBAAkB,OAAO,CAC7B,OAAM,IAAI,MAAM,yBAAyB,OAAO,GAAG;EAGpD,MAAM,aAAa,QAAQ,QAAQ,MACjC,cAAc,UAAU,aAAa,KAAK,OAAO,aAAa,CAC/D;AACD,MAAI,UAAU,CAAC,WACd,OAAM,IAAI,MAAM,WAAW,OAAO,mCAAmC;AAEtE,SAAO,cAAc;;AAGtB,QAAO,QAAQ,iBAAiB;;;;;;AAOjC,SAAgB,gBAAyB;CACxC,MAAM,SAAS,eAAe;AAC9B,QAAO,UAAU,QAAQ,OAAO,QAAQ,SAAS;;;;;;;AAQlD,SAAgB,iBAAiB,QAA0B;CAC1D,MAAM,SAAS,eAAe;AAC9B,KAAI,CAAC,OAAQ,QAAO,CAAC,OAAO;CAE5B,MAAM,QAAkB,CAAC,OAAO;CAChC,IAAI,UAAU;CACd,MAAM,UAAU,IAAI,IAAY,CAAC,OAAO,CAAC;AAEzC,QAAO,OAAO,WAAW,UAAU;EAElC,MAAM,OAAO,OAAO,SAAS;AAC7B,MAAI,QAAQ,IAAI,KAAK,CAAE;AACvB,QAAM,KAAK,KAAK;AAChB,UAAQ,IAAI,KAAK;AACjB,YAAU;;AAIX,KAAI,CAAC,QAAQ,IAAI,OAAO,cAAc,CACrC,OAAM,KAAK,OAAO,cAAc;AAGjC,QAAO"}
|