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,2560 @@
|
|
|
1
|
+
import { t as validateIdentifier } from "./validate-BpwoDOgt.mjs";
|
|
2
|
+
import { p as tableExists, s as isPostgres } from "./dialect-helpers-BSbfaO3F.mjs";
|
|
3
|
+
import { i as isI18nEnabled } from "./config-C3EBstHi.mjs";
|
|
4
|
+
import { n as chunks, t as SQL_BATCH_SIZE } from "./chunks-DfPax1I0.mjs";
|
|
5
|
+
import { i as InvalidCursorError, o as decodeCursor, s as encodeCursor } from "./types-CQPWhPxE.mjs";
|
|
6
|
+
import { t as withTransaction } from "./transaction-NQj4VJ7Z.mjs";
|
|
7
|
+
import { t as INTERNAL_MEDIA_PREFIX } from "./normalize-Bc-s_r-a.mjs";
|
|
8
|
+
import { n as normalizeMime } from "./mime-DfIVjNkr.mjs";
|
|
9
|
+
import { sql } from "kysely";
|
|
10
|
+
import { ulid } from "ulidx";
|
|
11
|
+
|
|
12
|
+
//#region src/media/usage/projection-fingerprint.ts
|
|
13
|
+
const MEDIA_USAGE_PROJECTION_FINGERPRINT_VERSION = 1;
|
|
14
|
+
const FINGERPRINT_PREFIX = `media-usage-projection:v${MEDIA_USAGE_PROJECTION_FINGERPRINT_VERSION}:sha256:`;
|
|
15
|
+
const FINGERPRINT_PATTERN = new RegExp(`^${FINGERPRINT_PREFIX}[a-f0-9]{64}$`);
|
|
16
|
+
async function buildMediaUsageProjectionFingerprint(input) {
|
|
17
|
+
if (!input.collectionId) throw new Error("Media usage projection fingerprints require a collection identity");
|
|
18
|
+
const canonicalOccurrences = input.occurrences.map((occurrence) => ({
|
|
19
|
+
fieldSlug: occurrence.fieldSlug,
|
|
20
|
+
fieldPath: occurrence.fieldPath,
|
|
21
|
+
occurrenceIndex: occurrence.occurrenceIndex ?? 0,
|
|
22
|
+
referenceType: occurrence.referenceType,
|
|
23
|
+
mediaId: occurrence.mediaId,
|
|
24
|
+
provider: occurrence.provider,
|
|
25
|
+
providerAssetId: occurrence.providerAssetId,
|
|
26
|
+
mediaKind: occurrence.mediaKind ?? null,
|
|
27
|
+
mimeType: occurrence.mimeType ?? null
|
|
28
|
+
})).map((occurrence) => ({
|
|
29
|
+
occurrence,
|
|
30
|
+
key: canonicalJson(occurrence)
|
|
31
|
+
})).toSorted((a, b) => compareCanonicalStrings(a.key, b.key)).map(({ occurrence }) => occurrence);
|
|
32
|
+
return buildCanonicalSha256Fingerprint(FINGERPRINT_PREFIX, {
|
|
33
|
+
fingerprintVersion: MEDIA_USAGE_PROJECTION_FINGERPRINT_VERSION,
|
|
34
|
+
collectionId: input.collectionId,
|
|
35
|
+
extractionSchema: normalizeExtractionFields(input.extractionFields),
|
|
36
|
+
source: {
|
|
37
|
+
sourceKey: input.source.sourceKey,
|
|
38
|
+
sourceType: input.source.sourceType,
|
|
39
|
+
collectionSlug: input.source.collectionSlug ?? null,
|
|
40
|
+
contentId: input.source.contentId ?? null,
|
|
41
|
+
sourceVariant: input.source.sourceVariant,
|
|
42
|
+
locale: input.source.locale ?? null,
|
|
43
|
+
translationGroup: input.source.translationGroup ?? null,
|
|
44
|
+
contentSlug: input.source.contentSlug ?? null,
|
|
45
|
+
contentTitle: input.source.contentTitle ?? null,
|
|
46
|
+
contentStatus: input.source.contentStatus ?? null,
|
|
47
|
+
contentScheduledAt: input.source.contentScheduledAt ?? null,
|
|
48
|
+
contentDeletedAt: input.source.contentDeletedAt ?? null,
|
|
49
|
+
revisionId: input.source.revisionId ?? null,
|
|
50
|
+
schemaVersion: input.source.schemaVersion ?? 1,
|
|
51
|
+
sourceCompleteness: input.source.sourceCompleteness ?? "complete"
|
|
52
|
+
},
|
|
53
|
+
occurrences: canonicalOccurrences
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function buildCanonicalSha256Fingerprint(prefix, payload) {
|
|
57
|
+
const encodedPayload = new TextEncoder().encode(canonicalJson(payload));
|
|
58
|
+
const digest = await crypto.subtle.digest("SHA-256", encodedPayload);
|
|
59
|
+
return {
|
|
60
|
+
fingerprint: `${prefix}${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`,
|
|
61
|
+
byteLength: encodedPayload.byteLength
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function normalizeExtractionFields(fields) {
|
|
65
|
+
return fields.map((field) => {
|
|
66
|
+
if (field.type !== "repeater") return {
|
|
67
|
+
slug: field.slug,
|
|
68
|
+
type: field.type
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
slug: field.slug,
|
|
72
|
+
type: field.type,
|
|
73
|
+
subFields: (field.validation?.subFields ?? []).map((subField) => ({
|
|
74
|
+
slug: subField.slug,
|
|
75
|
+
type: subField.type
|
|
76
|
+
})).toSorted((a, b) => compareCanonicalStrings(a.slug, b.slug))
|
|
77
|
+
};
|
|
78
|
+
}).toSorted((a, b) => compareCanonicalStrings(String(a.slug), String(b.slug)));
|
|
79
|
+
}
|
|
80
|
+
function compareCanonicalStrings(a, b) {
|
|
81
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
function isMediaUsageProjectionFingerprint(value) {
|
|
84
|
+
return typeof value === "string" && FINGERPRINT_PATTERN.test(value);
|
|
85
|
+
}
|
|
86
|
+
function canonicalJson(value) {
|
|
87
|
+
return JSON.stringify(canonicalize(value));
|
|
88
|
+
}
|
|
89
|
+
function canonicalize(value) {
|
|
90
|
+
if (value === void 0) return null;
|
|
91
|
+
if (typeof value === "bigint") return value.toString();
|
|
92
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : null;
|
|
93
|
+
if (Array.isArray(value)) return value.map((item) => canonicalize(item));
|
|
94
|
+
if (!isRecord$3(value)) return value;
|
|
95
|
+
const canonical = {};
|
|
96
|
+
for (const key of Object.keys(value).toSorted()) canonical[key] = canonicalize(value[key]);
|
|
97
|
+
return canonical;
|
|
98
|
+
}
|
|
99
|
+
function isRecord$3(value) {
|
|
100
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/database/repositories/media-usage.ts
|
|
105
|
+
const OCCURRENCE_BIND_COLUMNS = 13;
|
|
106
|
+
const MEDIA_USAGE_GENERATION_WRITE_LEASE_MS = 3600 * 1e3;
|
|
107
|
+
const OCCURRENCE_INSERT_BATCH_SIZE = Math.max(1, Math.floor(SQL_BATCH_SIZE / OCCURRENCE_BIND_COLUMNS));
|
|
108
|
+
function cleanupDeleteBatchSize(cleanupLease) {
|
|
109
|
+
return cleanupLease ? SQL_BATCH_SIZE - 3 : SQL_BATCH_SIZE;
|
|
110
|
+
}
|
|
111
|
+
function canIssueCleanupStatement(canIssueStatement) {
|
|
112
|
+
return canIssueStatement?.() ?? true;
|
|
113
|
+
}
|
|
114
|
+
function cleanupDurationSeconds(value) {
|
|
115
|
+
if (!Number.isSafeInteger(value) || value < 0) throw new Error("Media usage cleanup duration must be a non-negative whole number of seconds");
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
const CONTENT_SOURCE_ELIGIBILITY = sql`(
|
|
119
|
+
s.source_variant = 'draft_overlay'
|
|
120
|
+
OR (
|
|
121
|
+
s.source_variant = 'columns'
|
|
122
|
+
AND (
|
|
123
|
+
s.content_status = 'published'
|
|
124
|
+
OR NOT EXISTS (
|
|
125
|
+
SELECT 1
|
|
126
|
+
FROM _emdash_media_usage_sources AS overlay
|
|
127
|
+
WHERE overlay.source_type = 'content'
|
|
128
|
+
AND overlay.collection_slug = s.collection_slug
|
|
129
|
+
AND overlay.content_id = s.content_id
|
|
130
|
+
AND overlay.source_variant = 'draft_overlay'
|
|
131
|
+
AND ${contentSourceMatchesActiveCollection("overlay", "s.collection_id")}
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
)`;
|
|
136
|
+
function contentSourceMatchesActiveCollection(source, currentCollectionId) {
|
|
137
|
+
return sql`(
|
|
138
|
+
NOT EXISTS (
|
|
139
|
+
SELECT 1
|
|
140
|
+
FROM _emdash_media_usage_activation AS activation
|
|
141
|
+
WHERE activation.task_key = 'incremental_capture'
|
|
142
|
+
AND activation.state = 'active'
|
|
143
|
+
)
|
|
144
|
+
OR (
|
|
145
|
+
${sql.ref(`${source}.collection_id`)} = ${sql.ref(currentCollectionId)}
|
|
146
|
+
AND ${sql.ref(`${source}.identity_version`)} = 1
|
|
147
|
+
)
|
|
148
|
+
)`;
|
|
149
|
+
}
|
|
150
|
+
/** Persistence-only repository for the internal media usage projection tables. */
|
|
151
|
+
var MediaUsageRepository = class {
|
|
152
|
+
constructor(db) {
|
|
153
|
+
this.db = db;
|
|
154
|
+
}
|
|
155
|
+
async replaceSource(source, occurrences) {
|
|
156
|
+
const generation = ulid();
|
|
157
|
+
if (!await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
158
|
+
await withTransaction(this.db, async (trx) => {
|
|
159
|
+
if (!await this.lockCanonicalSourceCollection(trx, source)) throw new Error(`Media usage collection is no longer current for ${source.sourceKey}`);
|
|
160
|
+
await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
|
|
161
|
+
if (!await this.upsertSource(trx, source, generation, now, leaseToken)) throw new Error(`Media usage generation lease expired for ${source.sourceKey}`);
|
|
162
|
+
});
|
|
163
|
+
})) throw new Error(`Media usage collection is no longer current for ${source.sourceKey}`);
|
|
164
|
+
const replaced = await this.findSource(source.sourceKey);
|
|
165
|
+
if (!replaced) throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
|
|
166
|
+
return replaced;
|
|
167
|
+
}
|
|
168
|
+
async replaceSourceIfCurrent(source, occurrences, expectedCurrentGeneration) {
|
|
169
|
+
if (expectedCurrentGeneration !== null && await this.projectionMatchesCurrentGeneration(source, expectedCurrentGeneration)) return {
|
|
170
|
+
replaced: false,
|
|
171
|
+
unchanged: true,
|
|
172
|
+
source: null
|
|
173
|
+
};
|
|
174
|
+
const generation = ulid();
|
|
175
|
+
let replaced = false;
|
|
176
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
177
|
+
const row = this.buildSourceRow(source, generation, now);
|
|
178
|
+
await withTransaction(this.db, async (trx) => {
|
|
179
|
+
if (!await this.lockCanonicalSourceCollection(trx, source)) return;
|
|
180
|
+
await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
|
|
181
|
+
if (expectedCurrentGeneration === null) {
|
|
182
|
+
replaced = await this.insertSourceIfAbsent(trx, row, leaseToken);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
replaced = await this.updateSourceIfGeneration(trx, row, expectedCurrentGeneration, leaseToken);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
return {
|
|
189
|
+
replaced,
|
|
190
|
+
unchanged: false,
|
|
191
|
+
source: replaced ? null : await this.findSource(source.sourceKey)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
async findSource(sourceKey) {
|
|
195
|
+
const row = await this.db.selectFrom("_emdash_media_usage_sources").selectAll().where("source_key", "=", sourceKey).executeTakeFirst();
|
|
196
|
+
return row ? rowToSource(row) : null;
|
|
197
|
+
}
|
|
198
|
+
async findSources(sourceKeys) {
|
|
199
|
+
const uniqueSourceKeys = [...new Set(sourceKeys)];
|
|
200
|
+
const sources = /* @__PURE__ */ new Map();
|
|
201
|
+
if (uniqueSourceKeys.length === 0) return sources;
|
|
202
|
+
for (const sourceKeyBatch of chunks(uniqueSourceKeys, SQL_BATCH_SIZE)) {
|
|
203
|
+
const rows = await this.db.selectFrom("_emdash_media_usage_sources").selectAll().where("source_key", "in", sourceKeyBatch).execute();
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
const source = rowToSource(row);
|
|
206
|
+
sources.set(source.sourceKey, source);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return sources;
|
|
210
|
+
}
|
|
211
|
+
async measureSourceGenerationDeletion(sourceKey, generation, maxOccurrences) {
|
|
212
|
+
if (!Number.isSafeInteger(maxOccurrences) || maxOccurrences < 0) throw new Error("Media usage deletion measurement requires a non-negative row limit");
|
|
213
|
+
const payload = sql`
|
|
214
|
+
COALESCE(field_slug, '') || COALESCE(field_path, '') ||
|
|
215
|
+
COALESCE(reference_type, '') || COALESCE(media_id, '') ||
|
|
216
|
+
COALESCE(provider, '') || COALESCE(provider_asset_id, '') ||
|
|
217
|
+
COALESCE(media_kind, '') || COALESCE(mime_type, '')
|
|
218
|
+
`;
|
|
219
|
+
const occurrenceBytes = isPostgres(this.db) ? sql`octet_length(${payload})` : sql`length(CAST(${payload} AS BLOB))`;
|
|
220
|
+
const rows = await this.db.selectFrom("_emdash_media_usage").select(occurrenceBytes.as("occurrence_bytes")).where("source_key", "=", sourceKey).where("generation", "=", generation).limit(maxOccurrences + 1).execute();
|
|
221
|
+
return {
|
|
222
|
+
occurrenceCount: rows.length,
|
|
223
|
+
occurrenceBytes: rows.reduce((total, row) => total + Number(row.occurrence_bytes), 0),
|
|
224
|
+
exceedsOccurrenceLimit: rows.length > maxOccurrences
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async replaceSourceIfMatching(source, occurrences, expectedSource) {
|
|
228
|
+
if (expectedSource !== null && await this.projectionMatchesExpectedSource(source, expectedSource)) return {
|
|
229
|
+
replaced: false,
|
|
230
|
+
unchanged: true,
|
|
231
|
+
source: null
|
|
232
|
+
};
|
|
233
|
+
const generation = ulid();
|
|
234
|
+
let replaced = false;
|
|
235
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
236
|
+
const row = this.buildSourceRow(source, generation, now);
|
|
237
|
+
await withTransaction(this.db, async (trx) => {
|
|
238
|
+
if (!await this.lockCanonicalSourceCollection(trx, source)) return;
|
|
239
|
+
await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
|
|
240
|
+
if (expectedSource === null) {
|
|
241
|
+
replaced = await this.insertSourceIfAbsent(trx, row, leaseToken);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
replaced = await this.updateSourceIfMatching(trx, row, expectedSource, leaseToken);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
return {
|
|
248
|
+
replaced,
|
|
249
|
+
unchanged: false,
|
|
250
|
+
source: replaced ? null : await this.findSource(source.sourceKey)
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async markSourceAttempted(source) {
|
|
254
|
+
if (source.collectionId !== void 0 && source.collectionId !== null) {
|
|
255
|
+
const expectedSource = await this.findSource(source.sourceKey);
|
|
256
|
+
if (!(await this.markSourceAttemptedIfMatching(source, expectedSource)).attempted) throw new Error(`Canonical media usage source ${source.sourceKey} is no longer current`);
|
|
257
|
+
const attempted = await this.findSource(source.sourceKey);
|
|
258
|
+
if (!attempted) throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
|
|
259
|
+
return attempted;
|
|
260
|
+
}
|
|
261
|
+
const generation = ulid();
|
|
262
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
263
|
+
const row = this.buildAttemptedSourceRow(source, generation, now);
|
|
264
|
+
const updates = this.attemptedSourceUpdateSet(source, row);
|
|
265
|
+
if (((await this.db.insertInto("_emdash_media_usage_sources").values(row).onConflict((oc) => oc.column("source_key").doUpdateSet(updates)).executeTakeFirst()).numInsertedOrUpdatedRows ?? 0n) <= 0n) throw new Error(`Media usage generation lease expired for ${source.sourceKey}`);
|
|
266
|
+
});
|
|
267
|
+
const attempted = await this.findSource(source.sourceKey);
|
|
268
|
+
if (!attempted) throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
|
|
269
|
+
return attempted;
|
|
270
|
+
}
|
|
271
|
+
async markSourceAttemptedIfMatching(source, expectedSource) {
|
|
272
|
+
const generation = ulid();
|
|
273
|
+
let attempted = false;
|
|
274
|
+
if (expectedSource === null) await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
275
|
+
const row = this.buildAttemptedSourceRow(source, generation, now);
|
|
276
|
+
await withTransaction(this.db, async (trx) => {
|
|
277
|
+
if (!await this.lockCanonicalSourceCollection(trx, source)) return;
|
|
278
|
+
attempted = await this.persistSourceIfWriteLease(trx, row, leaseToken, sql`ON CONFLICT (source_key) DO NOTHING`);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
else {
|
|
282
|
+
const row = this.buildAttemptedSourceRow(source, generation, (/* @__PURE__ */ new Date()).toISOString());
|
|
283
|
+
await withTransaction(this.db, async (trx) => {
|
|
284
|
+
if (!await this.lockCanonicalSourceCollection(trx, source)) return;
|
|
285
|
+
attempted = await this.updateAttemptedSourceIfMatching(trx, source, row, expectedSource);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
attempted,
|
|
290
|
+
source: attempted ? null : await this.findSource(source.sourceKey)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
async findActiveEntryCountsByMediaIds(mediaIds) {
|
|
294
|
+
const uniqueMediaIds = [...new Set(mediaIds)];
|
|
295
|
+
const counts = new Map(uniqueMediaIds.map((mediaId) => [mediaId, 0]));
|
|
296
|
+
for (const mediaIdBatch of chunks(uniqueMediaIds, SQL_BATCH_SIZE)) {
|
|
297
|
+
const visibleEntries = this.currentContentMediaUsageBaseQuery().select([
|
|
298
|
+
"u.media_id as media_id",
|
|
299
|
+
"s.collection_slug as collection_slug",
|
|
300
|
+
"s.content_id as content_id"
|
|
301
|
+
]).where("u.media_id", "in", mediaIdBatch).where((eb) => eb.not(eb.exists(eb.selectFrom("_emdash_media_usage_sources as deleted_source").select("deleted_source.source_key").where("deleted_source.source_type", "=", "content").whereRef("deleted_source.collection_slug", "=", "s.collection_slug").whereRef("deleted_source.content_id", "=", "s.content_id").where("deleted_source.source_variant", "in", ["columns", "draft_overlay"]).where(contentSourceMatchesActiveCollection("deleted_source", "collection.id")).where("deleted_source.content_deleted_at", "is not", null)))).distinct().as("visible_entries");
|
|
302
|
+
const rows = await this.db.selectFrom(visibleEntries).select("media_id").select((eb) => eb.fn.countAll().as("usage_count")).groupBy("media_id").execute();
|
|
303
|
+
for (const row of rows) if (row.media_id !== null) counts.set(row.media_id, Number(row.usage_count));
|
|
304
|
+
}
|
|
305
|
+
return counts;
|
|
306
|
+
}
|
|
307
|
+
async findCollectionIndexStatusScopes(identity) {
|
|
308
|
+
return (await this.db.selectFrom("_emdash_collections as collection").leftJoin("_emdash_media_usage_index_status as status", (join) => join.on("status.adapter_id", "=", identity.adapterId).on("status.scope_type", "=", identity.scopeType).onRef("status.scope_key", "=", "collection.slug")).select([
|
|
309
|
+
"collection.slug as collection_slug",
|
|
310
|
+
"status.status as status",
|
|
311
|
+
"status.schema_version as schema_version",
|
|
312
|
+
"status.reconciliation_required as reconciliation_required"
|
|
313
|
+
]).orderBy("collection.slug", "asc").execute()).map((row) => ({
|
|
314
|
+
collectionSlug: row.collection_slug,
|
|
315
|
+
status: row.status,
|
|
316
|
+
schemaVersion: row.schema_version === null ? null : Number(row.schema_version),
|
|
317
|
+
reconciliationRequired: row.reconciliation_required !== null && Number(row.reconciliation_required) !== 0
|
|
318
|
+
}));
|
|
319
|
+
}
|
|
320
|
+
async findCurrentEntryUsagePageByMediaId(mediaId, options = {}) {
|
|
321
|
+
const requestedLimit = Math.floor(options.limit ?? 50);
|
|
322
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(1, requestedLimit), 100) : 50;
|
|
323
|
+
const cursor = options.cursor ? decodeCursor(options.cursor) : null;
|
|
324
|
+
if (cursor && (cursor.orderValue.length === 0 || cursor.id.length === 0)) throw new InvalidCursorError(options.cursor ?? "");
|
|
325
|
+
let matchedGroups = this.currentContentMediaUsageBaseQuery().select([
|
|
326
|
+
"collection.id as collection_id",
|
|
327
|
+
"s.collection_slug as collection_slug",
|
|
328
|
+
"s.content_id as content_id"
|
|
329
|
+
]).where("u.media_id", "=", mediaId).distinct();
|
|
330
|
+
if (cursor) matchedGroups = matchedGroups.where((eb) => eb.or([eb("s.collection_slug", ">", cursor.orderValue), eb.and([eb("s.collection_slug", "=", cursor.orderValue), eb("s.content_id", ">", cursor.id)])]));
|
|
331
|
+
matchedGroups = matchedGroups.orderBy("s.collection_slug", "asc").orderBy("s.content_id", "asc").limit(limit + 1);
|
|
332
|
+
const rows = await this.db.with("matched_groups", () => matchedGroups).with("page_groups", (db) => db.selectFrom("matched_groups").selectAll().orderBy("collection_slug", "asc").orderBy("content_id", "asc").limit(limit)).with("entry_state", (db) => db.selectFrom("page_groups as page").crossJoin("_emdash_media_usage_sources as state").select([
|
|
333
|
+
"page.collection_id",
|
|
334
|
+
"page.collection_slug",
|
|
335
|
+
"page.content_id"
|
|
336
|
+
]).select((eb) => eb.fn.max("state.content_deleted_at").as("entry_deleted_at")).whereRef("page.collection_slug", "=", "state.collection_slug").whereRef("page.content_id", "=", "state.content_id").where("state.source_type", "=", "content").where("state.source_variant", "in", ["columns", "draft_overlay"]).where(contentSourceMatchesActiveCollection("state", "page.collection_id")).groupBy([
|
|
337
|
+
"page.collection_id",
|
|
338
|
+
"page.collection_slug",
|
|
339
|
+
"page.content_id"
|
|
340
|
+
])).selectFrom("entry_state as page").crossJoin("_emdash_media_usage_sources as s").crossJoin("_emdash_media_usage as u").whereRef("page.collection_slug", "=", "s.collection_slug").whereRef("page.content_id", "=", "s.content_id").where(contentSourceMatchesActiveCollection("s", "page.collection_id")).whereRef("s.source_key", "=", "u.source_key").whereRef("s.current_generation", "=", "u.generation").select(currentUsageSelect).select("page.entry_deleted_at").select(sql`CASE
|
|
341
|
+
WHEN (SELECT COUNT(*) FROM matched_groups) > ${limit} THEN 1
|
|
342
|
+
ELSE 0
|
|
343
|
+
END`.as("has_more")).where("u.media_id", "=", mediaId).where("s.source_type", "=", "content").where("s.collection_slug", "is not", null).where("s.content_id", "is not", null).where("s.source_variant", "in", ["columns", "draft_overlay"]).where(CONTENT_SOURCE_ELIGIBILITY).orderBy("s.collection_slug", "asc").orderBy("s.content_id", "asc").orderBy("s.source_variant", "asc").orderBy("s.source_key", "asc").orderBy("u.field_path", "asc").orderBy("u.occurrence_index", "asc").orderBy("u.id", "asc").execute();
|
|
344
|
+
const items = groupUsageRows(rows);
|
|
345
|
+
const result = { items };
|
|
346
|
+
if (Number(rows[0]?.has_more ?? 0) === 1 && items.length > 0) {
|
|
347
|
+
const last = items.at(-1);
|
|
348
|
+
result.nextCursor = encodeCursor(last.collectionSlug, last.contentId);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
async findCurrentUsageByMediaId(mediaId) {
|
|
353
|
+
return (await this.db.selectFrom("_emdash_media_usage_sources as s").innerJoin("_emdash_media_usage as u", (join) => join.onRef("u.source_key", "=", "s.source_key").onRef("u.generation", "=", "s.current_generation")).select(currentUsageSelect).where("u.media_id", "=", mediaId).orderBy("s.source_key", "asc").orderBy("u.field_path", "asc").orderBy("u.occurrence_index", "asc").execute()).map(rowToUsageRecord);
|
|
354
|
+
}
|
|
355
|
+
async findCurrentUsageByProviderAsset(provider, providerAssetId) {
|
|
356
|
+
return (await this.db.selectFrom("_emdash_media_usage_sources as s").innerJoin("_emdash_media_usage as u", (join) => join.onRef("u.source_key", "=", "s.source_key").onRef("u.generation", "=", "s.current_generation")).select(currentUsageSelect).where("u.provider", "=", provider).where("u.provider_asset_id", "=", providerAssetId).orderBy("s.source_key", "asc").orderBy("u.field_path", "asc").orderBy("u.occurrence_index", "asc").execute()).map(rowToUsageRecord);
|
|
357
|
+
}
|
|
358
|
+
async findCurrentUsagePageByMediaId(mediaId, options = {}) {
|
|
359
|
+
return this.findCurrentUsagePage((query) => query.where("u.media_id", "=", mediaId), options);
|
|
360
|
+
}
|
|
361
|
+
async findCurrentUsagePageByProviderAsset(provider, providerAssetId, options = {}) {
|
|
362
|
+
return this.findCurrentUsagePage((query) => query.where("u.provider", "=", provider).where("u.provider_asset_id", "=", providerAssetId), options);
|
|
363
|
+
}
|
|
364
|
+
async deleteSource(sourceKey) {
|
|
365
|
+
return this.deleteSources([sourceKey]);
|
|
366
|
+
}
|
|
367
|
+
async deleteSourceIfCurrent(sourceKey, expectedCurrentGeneration) {
|
|
368
|
+
let deleted = false;
|
|
369
|
+
await withTransaction(this.db, async (trx) => {
|
|
370
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
371
|
+
const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where("current_generation", "=", expectedCurrentGeneration).executeTakeFirst();
|
|
372
|
+
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
373
|
+
if (!deleted) return;
|
|
374
|
+
await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedCurrentGeneration);
|
|
375
|
+
});
|
|
376
|
+
return {
|
|
377
|
+
deleted,
|
|
378
|
+
source: await this.findSource(sourceKey)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
async deleteSourceIfMatching(sourceKey, expectedSource) {
|
|
382
|
+
let deleted = false;
|
|
383
|
+
await withTransaction(this.db, async (trx) => {
|
|
384
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
385
|
+
const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(expectedSource.collectionId, expectedSource.collectionSlug)).executeTakeFirst();
|
|
386
|
+
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
387
|
+
if (!deleted) return;
|
|
388
|
+
await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedSource.currentGeneration);
|
|
389
|
+
});
|
|
390
|
+
return {
|
|
391
|
+
deleted,
|
|
392
|
+
source: await this.findSource(sourceKey)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
async deleteSourceIfMatchingContentAbsent(sourceKey, expectedSource, collectionSlug, contentId) {
|
|
396
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
397
|
+
const tableName = `ec_${collectionSlug}`;
|
|
398
|
+
let deleted = false;
|
|
399
|
+
await withTransaction(this.db, async (trx) => {
|
|
400
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
401
|
+
const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "=", sourceKey).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(expectedSource.collectionId, expectedSource.collectionSlug)).where(sql`NOT EXISTS (SELECT 1 FROM ${sql.ref(tableName)} WHERE id = ${contentId})`).executeTakeFirst();
|
|
402
|
+
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
403
|
+
if (!deleted) return;
|
|
404
|
+
await this.deleteSourceGenerationOccurrences(trx, sourceKey, expectedSource.currentGeneration);
|
|
405
|
+
});
|
|
406
|
+
const contentPresent = deleted ? false : await this.contentRowExists(tableName, contentId);
|
|
407
|
+
return {
|
|
408
|
+
deleted,
|
|
409
|
+
contentPresent,
|
|
410
|
+
source: deleted || contentPresent ? null : await this.findSource(sourceKey)
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
async deleteSources(sourceKeys) {
|
|
414
|
+
return this.deleteSourceKeys(sourceKeys);
|
|
415
|
+
}
|
|
416
|
+
async deleteContentSources(collectionSlug, contentId) {
|
|
417
|
+
const sourceKeys = (await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_type", "=", "content").where("collection_slug", "=", collectionSlug).where("content_id", "=", contentId).execute()).map((row) => row.source_key);
|
|
418
|
+
return this.deleteSourceKeys(sourceKeys);
|
|
419
|
+
}
|
|
420
|
+
async deleteCollectionSources(collectionSlug) {
|
|
421
|
+
let deleted = 0;
|
|
422
|
+
while (true) {
|
|
423
|
+
const sourceRows = await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_type", "=", "content").where("collection_slug", "=", collectionSlug).orderBy("source_key", "asc").limit(SQL_BATCH_SIZE).execute();
|
|
424
|
+
if (sourceRows.length === 0) break;
|
|
425
|
+
deleted += await this.deleteSourceKeys(sourceRows.map((row) => row.source_key));
|
|
426
|
+
}
|
|
427
|
+
return deleted;
|
|
428
|
+
}
|
|
429
|
+
async findCollectionContentSources(collectionSlug, collectionId) {
|
|
430
|
+
let query = this.db.selectFrom("_emdash_media_usage_sources").selectAll().where("source_type", "=", "content").where("collection_slug", "=", collectionSlug).orderBy("source_key", "asc");
|
|
431
|
+
if (collectionId !== void 0) query = query.where("collection_id", "=", collectionId);
|
|
432
|
+
return (await query.execute()).map((row) => rowToSource(row));
|
|
433
|
+
}
|
|
434
|
+
async claimMediaUsageCleanup(input) {
|
|
435
|
+
const leaseDurationSeconds = cleanupDurationSeconds(input.leaseDurationSeconds);
|
|
436
|
+
const nextEligibleDelaySeconds = cleanupDurationSeconds(input.nextEligibleDelaySeconds);
|
|
437
|
+
const sweepSafetyWindowSeconds = cleanupDurationSeconds(input.sweepSafetyWindowSeconds);
|
|
438
|
+
const claimedAt = this.cleanupTimestampOffset(0);
|
|
439
|
+
const leaseExpiresAt = this.cleanupTimestampOffset(leaseDurationSeconds);
|
|
440
|
+
const nextEligibleAt = this.cleanupTimestampOffset(nextEligibleDelaySeconds);
|
|
441
|
+
const sweepBeforeAt = this.cleanupTimestampOffset(-sweepSafetyWindowSeconds);
|
|
442
|
+
const row = await this.db.updateTable("_emdash_media_usage_cleanup").set({
|
|
443
|
+
lease_token: input.leaseToken,
|
|
444
|
+
lease_expires_at: leaseExpiresAt,
|
|
445
|
+
next_eligible_at: nextEligibleAt,
|
|
446
|
+
last_started_at: claimedAt,
|
|
447
|
+
updated_at: claimedAt,
|
|
448
|
+
scan_before_at: sql`CASE
|
|
449
|
+
WHEN scan_before_at IS NULL THEN ${sweepBeforeAt}
|
|
450
|
+
ELSE scan_before_at
|
|
451
|
+
END`
|
|
452
|
+
}).where("task_key", "=", "projection_gc").where(this.cleanupTimestampIsDue("next_eligible_at")).where((eb) => eb.or([eb("lease_token", "is", null), this.cleanupTimestampIsDue("lease_expires_at")])).returning([
|
|
453
|
+
"cursor_created_at",
|
|
454
|
+
"cursor_id",
|
|
455
|
+
"last_started_at",
|
|
456
|
+
"scan_before_at",
|
|
457
|
+
"consecutive_failures"
|
|
458
|
+
]).executeTakeFirst();
|
|
459
|
+
if (!row) return null;
|
|
460
|
+
if (!row.last_started_at || !row.scan_before_at) throw new Error("Media usage cleanup claim did not persist its database timestamps");
|
|
461
|
+
return {
|
|
462
|
+
leaseToken: input.leaseToken,
|
|
463
|
+
cursor: row.cursor_created_at && row.cursor_id ? {
|
|
464
|
+
createdAt: row.cursor_created_at,
|
|
465
|
+
id: row.cursor_id
|
|
466
|
+
} : null,
|
|
467
|
+
claimedAt: row.last_started_at,
|
|
468
|
+
scanBeforeAt: row.scan_before_at,
|
|
469
|
+
consecutiveFailures: row.consecutive_failures
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
async findMediaUsageCleanupCandidates(input) {
|
|
473
|
+
let query = this.db.selectFrom("_emdash_media_usage as u").leftJoin("_emdash_media_usage_sources as s", "s.source_key", "u.source_key").leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select([
|
|
474
|
+
"u.id as id",
|
|
475
|
+
"u.source_key as source_key",
|
|
476
|
+
"u.generation as generation",
|
|
477
|
+
"u.created_at as created_at",
|
|
478
|
+
"s.current_generation as current_generation",
|
|
479
|
+
"s.indexed_at as indexed_at",
|
|
480
|
+
"writer.expires_at as write_lease_expires_at"
|
|
481
|
+
]).where("u.created_at", "<", input.cutoff).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(Math.max(0, Math.floor(input.limit)));
|
|
482
|
+
if (input.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(input.cleanupLease));
|
|
483
|
+
if (input.cursor) query = query.where((eb) => eb.or([eb("u.created_at", ">", input.cursor.createdAt), eb.and([eb("u.created_at", "=", input.cursor.createdAt), eb("u.id", ">", input.cursor.id)])]));
|
|
484
|
+
return (await query.execute()).map((row) => ({
|
|
485
|
+
id: row.id,
|
|
486
|
+
sourceKey: row.source_key,
|
|
487
|
+
generation: row.generation,
|
|
488
|
+
createdAt: row.created_at,
|
|
489
|
+
currentGeneration: row.current_generation,
|
|
490
|
+
indexedAt: row.indexed_at,
|
|
491
|
+
writeLeaseExpiresAt: row.write_lease_expires_at
|
|
492
|
+
}));
|
|
493
|
+
}
|
|
494
|
+
async completeMediaUsageCleanup(input) {
|
|
495
|
+
const updates = {
|
|
496
|
+
lease_token: null,
|
|
497
|
+
lease_expires_at: null,
|
|
498
|
+
cursor_created_at: input.sweepComplete ? null : input.nextCursor?.createdAt ?? null,
|
|
499
|
+
cursor_id: input.sweepComplete ? null : input.nextCursor?.id ?? null,
|
|
500
|
+
...input.sweepComplete ? { scan_before_at: null } : {},
|
|
501
|
+
consecutive_failures: 0,
|
|
502
|
+
last_completed_at: this.cleanupTimestampOffset(0),
|
|
503
|
+
last_candidate_count: input.candidateCount,
|
|
504
|
+
last_deleted_orphans: input.deletedOrphans,
|
|
505
|
+
last_deleted_stale: input.deletedStale,
|
|
506
|
+
last_deleted_abandoned: input.deletedAbandoned,
|
|
507
|
+
last_deleted_write_leases: input.deletedWriteLeases,
|
|
508
|
+
last_backlog_lower_bound: input.backlogLowerBound,
|
|
509
|
+
last_scan_has_more: input.scanHasMore ? 1 : 0,
|
|
510
|
+
last_duration_ms: input.durationMs,
|
|
511
|
+
last_error_code: null,
|
|
512
|
+
updated_at: this.cleanupTimestampOffset(0)
|
|
513
|
+
};
|
|
514
|
+
const result = await this.db.updateTable("_emdash_media_usage_cleanup").set(updates).where("task_key", "=", "projection_gc").where("lease_token", "=", input.leaseToken).where(this.cleanupLeaseExpiryIsInFuture("_emdash_media_usage_cleanup.lease_expires_at")).executeTakeFirst();
|
|
515
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
516
|
+
}
|
|
517
|
+
async failMediaUsageCleanup(input) {
|
|
518
|
+
const retryDelaySeconds = cleanupDurationSeconds(input.retryDelaySeconds);
|
|
519
|
+
const result = await this.db.updateTable("_emdash_media_usage_cleanup").set({
|
|
520
|
+
lease_token: null,
|
|
521
|
+
lease_expires_at: null,
|
|
522
|
+
next_eligible_at: this.cleanupTimestampOffset(retryDelaySeconds),
|
|
523
|
+
consecutive_failures: input.consecutiveFailures,
|
|
524
|
+
last_completed_at: this.cleanupTimestampOffset(0),
|
|
525
|
+
last_duration_ms: input.durationMs,
|
|
526
|
+
last_error_code: input.errorCode,
|
|
527
|
+
updated_at: this.cleanupTimestampOffset(0)
|
|
528
|
+
}).where("task_key", "=", "projection_gc").where("lease_token", "=", input.leaseToken).where(this.cleanupLeaseExpiryIsInFuture("_emdash_media_usage_cleanup.lease_expires_at")).executeTakeFirst();
|
|
529
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
530
|
+
}
|
|
531
|
+
async deleteOrphanOccurrencesOlderThan(cutoff, limit, options = {}) {
|
|
532
|
+
const batchLimit = Math.floor(limit);
|
|
533
|
+
if (batchLimit <= 0) return 0;
|
|
534
|
+
if (options.candidateIds) return this.deleteOrphanCandidateIds(options.candidateIds.slice(0, batchLimit), cutoff, options.cleanupLease, options.canIssueStatement);
|
|
535
|
+
if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
|
|
536
|
+
let query = this.db.selectFrom("_emdash_media_usage as u").leftJoin("_emdash_media_usage_sources as s", (join) => join.onRef("s.source_key", "=", "u.source_key")).leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select("u.id").where("s.source_key", "is", null).where("u.created_at", "<", cutoff).where(this.noActiveGenerationWriteExpression("u")).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(batchLimit);
|
|
537
|
+
if (options.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
|
|
538
|
+
const rows = await query.execute();
|
|
539
|
+
return this.deleteOrphanCandidateIds(rows.map((row) => row.id), cutoff, options.cleanupLease, options.canIssueStatement);
|
|
540
|
+
}
|
|
541
|
+
async deleteStaleGenerationsOlderThan(cutoff, limit, options = {}) {
|
|
542
|
+
const batchLimit = Math.floor(limit);
|
|
543
|
+
if (batchLimit <= 0) return 0;
|
|
544
|
+
if (options.candidateIds) return this.deleteStaleCandidateIds(options.candidateIds.slice(0, batchLimit), cutoff, options.cleanupLease, options.canIssueStatement);
|
|
545
|
+
if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
|
|
546
|
+
let query = this.db.selectFrom("_emdash_media_usage as u").innerJoin("_emdash_media_usage_sources as s", (join) => join.onRef("s.source_key", "=", "u.source_key")).leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select("u.id").where("u.created_at", "<", cutoff).whereRef("u.generation", "!=", "s.current_generation").whereRef("u.created_at", "<", "s.indexed_at").where(this.noActiveGenerationWriteExpression("u")).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(batchLimit);
|
|
547
|
+
if (options.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
|
|
548
|
+
const rows = await query.execute();
|
|
549
|
+
return this.deleteStaleCandidateIds(rows.map((row) => row.id), cutoff, options.cleanupLease, options.canIssueStatement);
|
|
550
|
+
}
|
|
551
|
+
async deleteAbandonedGenerationsOlderThan(cutoff, limit, options = {}) {
|
|
552
|
+
const batchLimit = Math.floor(limit);
|
|
553
|
+
if (batchLimit <= 0) return 0;
|
|
554
|
+
if (options.candidateIds) return this.deleteAbandonedCandidateIds(options.candidateIds.slice(0, batchLimit), cutoff, options.cleanupLease, options.canIssueStatement);
|
|
555
|
+
if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
|
|
556
|
+
let query = this.db.selectFrom("_emdash_media_usage as u").innerJoin("_emdash_media_usage_sources as s", (join) => join.onRef("s.source_key", "=", "u.source_key")).leftJoin("_emdash_media_usage_generation_writes as writer", (join) => join.onRef("writer.source_key", "=", "u.source_key").onRef("writer.generation", "=", "u.generation")).select("u.id").where("u.created_at", "<", cutoff).whereRef("u.generation", "!=", "s.current_generation").whereRef("u.created_at", ">=", "s.indexed_at").where(this.noActiveGenerationWriteExpression("u")).orderBy("u.created_at", "asc").orderBy("u.id", "asc").limit(batchLimit);
|
|
557
|
+
if (options.cleanupLease) query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
|
|
558
|
+
const rows = await query.execute();
|
|
559
|
+
return this.deleteAbandonedCandidateIds(rows.map((row) => row.id), cutoff, options.cleanupLease, options.canIssueStatement);
|
|
560
|
+
}
|
|
561
|
+
async deleteExpiredGenerationWriteLeases(limit, cleanupLease, canIssueStatement) {
|
|
562
|
+
const batchLimit = Math.floor(limit);
|
|
563
|
+
if (batchLimit <= 0 || !canIssueCleanupStatement(canIssueStatement)) return 0;
|
|
564
|
+
let query = this.db.selectFrom("_emdash_media_usage_generation_writes").select("lease_token").where(this.generationWriteLeaseHasExpired("expires_at")).orderBy("expires_at", "asc").orderBy("lease_token", "asc").limit(batchLimit);
|
|
565
|
+
if (cleanupLease) query = query.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
566
|
+
const rows = await query.execute();
|
|
567
|
+
if (rows.length === 0 || !canIssueCleanupStatement(canIssueStatement)) return 0;
|
|
568
|
+
let deleteQuery = this.db.deleteFrom("_emdash_media_usage_generation_writes").where("lease_token", "in", rows.map((row) => row.lease_token)).where(this.generationWriteLeaseHasExpired("expires_at"));
|
|
569
|
+
if (cleanupLease) deleteQuery = deleteQuery.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
570
|
+
const result = await deleteQuery.executeTakeFirst();
|
|
571
|
+
return Number(result.numDeletedRows ?? 0);
|
|
572
|
+
}
|
|
573
|
+
async upsertIndexStatus(input) {
|
|
574
|
+
const now = input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
575
|
+
const row = {
|
|
576
|
+
adapter_id: input.adapterId,
|
|
577
|
+
scope_type: input.scopeType,
|
|
578
|
+
scope_key: input.scopeKey,
|
|
579
|
+
status: input.status,
|
|
580
|
+
schema_version: input.schemaVersion ?? 1,
|
|
581
|
+
started_at: input.startedAt ?? null,
|
|
582
|
+
completed_at: input.completedAt ?? null,
|
|
583
|
+
cursor: input.cursor ?? null,
|
|
584
|
+
indexed_source_count: input.indexedSourceCount ?? 0,
|
|
585
|
+
failed_source_count: input.failedSourceCount ?? 0,
|
|
586
|
+
last_error_code: input.lastErrorCode ?? null,
|
|
587
|
+
updated_at: now
|
|
588
|
+
};
|
|
589
|
+
await this.db.insertInto("_emdash_media_usage_index_status").values(row).onConflict((oc) => oc.columns([
|
|
590
|
+
"adapter_id",
|
|
591
|
+
"scope_type",
|
|
592
|
+
"scope_key"
|
|
593
|
+
]).doUpdateSet({
|
|
594
|
+
status: row.status,
|
|
595
|
+
schema_version: row.schema_version,
|
|
596
|
+
started_at: row.started_at,
|
|
597
|
+
completed_at: row.completed_at,
|
|
598
|
+
cursor: row.cursor,
|
|
599
|
+
indexed_source_count: row.indexed_source_count,
|
|
600
|
+
failed_source_count: row.failed_source_count,
|
|
601
|
+
last_error_code: row.last_error_code,
|
|
602
|
+
updated_at: row.updated_at
|
|
603
|
+
})).execute();
|
|
604
|
+
const status = await this.findIndexStatus(input);
|
|
605
|
+
if (!status) throw new Error(`Media usage index status ${input.adapterId}:${input.scopeType}:${input.scopeKey} was not persisted`);
|
|
606
|
+
return status;
|
|
607
|
+
}
|
|
608
|
+
async invalidateIndexStatusForSchemaChange(collectionSlug) {
|
|
609
|
+
const result = await this.db.updateTable("_emdash_media_usage_index_status as status").set({
|
|
610
|
+
change_epoch: sql`change_epoch + 1`,
|
|
611
|
+
status: "stale",
|
|
612
|
+
completed_at: null,
|
|
613
|
+
cursor: null,
|
|
614
|
+
last_error_code: "CONTENT_USAGE_STALE",
|
|
615
|
+
reconciliation_required: 1,
|
|
616
|
+
updated_at: this.sortableUtcTimestamp()
|
|
617
|
+
}).where("status.adapter_id", "=", "content-media").where("status.scope_type", "=", "collection").where("status.scope_key", "=", collectionSlug).where("status.capture_state", "=", "active").where((eb) => eb.exists(eb.selectFrom("_emdash_collections as collection").select("collection.id").whereRef("collection.id", "=", "status.collection_id").whereRef("collection.slug", "=", "status.scope_key"))).where(sql`EXISTS (
|
|
618
|
+
SELECT 1
|
|
619
|
+
FROM _emdash_media_usage_activation AS activation
|
|
620
|
+
WHERE activation.task_key = 'incremental_capture'
|
|
621
|
+
AND activation.state = 'active'
|
|
622
|
+
)`).executeTakeFirst();
|
|
623
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
624
|
+
}
|
|
625
|
+
async beginIndexStatusRepair(input) {
|
|
626
|
+
return this.upsertIndexStatus({
|
|
627
|
+
adapterId: input.adapterId,
|
|
628
|
+
scopeType: input.scopeType,
|
|
629
|
+
scopeKey: input.scopeKey,
|
|
630
|
+
status: "running",
|
|
631
|
+
schemaVersion: input.schemaVersion,
|
|
632
|
+
startedAt: input.startedAt,
|
|
633
|
+
completedAt: null,
|
|
634
|
+
cursor: input.runToken,
|
|
635
|
+
indexedSourceCount: 0,
|
|
636
|
+
failedSourceCount: 0,
|
|
637
|
+
lastErrorCode: null,
|
|
638
|
+
updatedAt: input.updatedAt
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
async finalizeIndexStatusRepairIfRunning(input) {
|
|
642
|
+
const updates = {
|
|
643
|
+
status: input.status,
|
|
644
|
+
completed_at: input.completedAt,
|
|
645
|
+
cursor: null,
|
|
646
|
+
indexed_source_count: input.indexedSourceCount ?? 0,
|
|
647
|
+
failed_source_count: input.failedSourceCount ?? 0,
|
|
648
|
+
last_error_code: input.lastErrorCode ?? null,
|
|
649
|
+
updated_at: input.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
650
|
+
};
|
|
651
|
+
if (input.schemaVersion !== void 0) updates.schema_version = input.schemaVersion;
|
|
652
|
+
const result = await this.db.updateTable("_emdash_media_usage_index_status").set(updates).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("status", "=", "running").where("cursor", "=", input.runToken).executeTakeFirst();
|
|
653
|
+
return {
|
|
654
|
+
finalized: Number(result.numUpdatedRows ?? 0) > 0,
|
|
655
|
+
status: await this.findIndexStatus(input)
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
async beginIndexStatusRepairAtCurrentEpoch(input) {
|
|
659
|
+
const now = this.sortableUtcTimestamp();
|
|
660
|
+
const row = await this.db.updateTable("_emdash_media_usage_index_status").set({
|
|
661
|
+
status: "running",
|
|
662
|
+
schema_version: input.schemaVersion,
|
|
663
|
+
started_at: now,
|
|
664
|
+
completed_at: null,
|
|
665
|
+
cursor: input.runToken,
|
|
666
|
+
indexed_source_count: 0,
|
|
667
|
+
failed_source_count: 0,
|
|
668
|
+
last_error_code: null,
|
|
669
|
+
reconciliation_required: 1,
|
|
670
|
+
updated_at: now
|
|
671
|
+
}).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("collection_id", "=", input.collectionId).where("capture_state", "=", "active").where(sql`EXISTS (
|
|
672
|
+
SELECT 1
|
|
673
|
+
FROM _emdash_collections AS collection
|
|
674
|
+
WHERE collection.id = ${input.collectionId}
|
|
675
|
+
AND collection.slug = ${input.scopeKey}
|
|
676
|
+
)`).where(sql`EXISTS (
|
|
677
|
+
SELECT 1
|
|
678
|
+
FROM _emdash_media_usage_activation AS activation
|
|
679
|
+
WHERE activation.task_key = 'incremental_capture'
|
|
680
|
+
AND activation.state = 'active'
|
|
681
|
+
)`).returning(["change_epoch", "started_at"]).executeTakeFirst();
|
|
682
|
+
if (!row?.started_at) return null;
|
|
683
|
+
return {
|
|
684
|
+
changeEpoch: row.change_epoch,
|
|
685
|
+
startedAt: row.started_at
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
async finalizeIndexStatusRepairAtEpoch(input) {
|
|
689
|
+
const now = this.sortableUtcTimestamp();
|
|
690
|
+
const updates = {
|
|
691
|
+
status: input.status,
|
|
692
|
+
schema_version: input.schemaVersion,
|
|
693
|
+
completed_at: now,
|
|
694
|
+
cursor: null,
|
|
695
|
+
indexed_source_count: input.indexedSourceCount,
|
|
696
|
+
failed_source_count: input.failedSourceCount,
|
|
697
|
+
last_error_code: input.lastErrorCode,
|
|
698
|
+
reconciliation_required: input.status === "complete" ? 0 : 1,
|
|
699
|
+
updated_at: now
|
|
700
|
+
};
|
|
701
|
+
let query = this.db.updateTable("_emdash_media_usage_index_status").set(updates).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("collection_id", "=", input.collectionId).where("status", "=", "running").where("cursor", "=", input.runToken).where("change_epoch", "=", input.startingEpoch).where(sql`EXISTS (
|
|
702
|
+
SELECT 1
|
|
703
|
+
FROM _emdash_collections AS collection
|
|
704
|
+
WHERE collection.id = ${input.collectionId}
|
|
705
|
+
AND collection.slug = ${input.scopeKey}
|
|
706
|
+
)`);
|
|
707
|
+
if (input.status === "complete") query = query.where(sql`NOT EXISTS (
|
|
708
|
+
SELECT 1
|
|
709
|
+
FROM _emdash_media_usage_work AS work
|
|
710
|
+
WHERE work.collection_id = ${input.collectionId}
|
|
711
|
+
)`);
|
|
712
|
+
const result = await query.executeTakeFirst();
|
|
713
|
+
const finalized = Number(result.numUpdatedRows ?? 0) > 0;
|
|
714
|
+
if (!finalized) await this.db.updateTable("_emdash_media_usage_index_status").set({
|
|
715
|
+
status: "stale",
|
|
716
|
+
completed_at: null,
|
|
717
|
+
cursor: null,
|
|
718
|
+
last_error_code: "CONTENT_USAGE_REPAIR_CONFLICT",
|
|
719
|
+
reconciliation_required: 1,
|
|
720
|
+
updated_at: this.sortableUtcTimestamp()
|
|
721
|
+
}).where("adapter_id", "=", input.adapterId).where("scope_type", "=", input.scopeType).where("scope_key", "=", input.scopeKey).where("collection_id", "=", input.collectionId).where("status", "=", "running").where("cursor", "=", input.runToken).execute();
|
|
722
|
+
return {
|
|
723
|
+
finalized,
|
|
724
|
+
status: await this.findIndexStatusForCollection(input, input.collectionId)
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
async recordIncrementalSuccess(input) {
|
|
728
|
+
const observed = await this.db.selectFrom("_emdash_media_usage_index_status").select("change_epoch").where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", input.collectionSlug).where("collection_id", "=", input.collectionId).where("capture_state", "=", "active").where(sql`EXISTS (
|
|
729
|
+
SELECT 1
|
|
730
|
+
FROM _emdash_collections AS collection
|
|
731
|
+
WHERE collection.id = ${input.collectionId}
|
|
732
|
+
AND collection.slug = ${input.collectionSlug}
|
|
733
|
+
)`).executeTakeFirst();
|
|
734
|
+
if (!observed) return false;
|
|
735
|
+
const canComplete = sql`(
|
|
736
|
+
reconciliation_required = 0
|
|
737
|
+
AND status IN ('complete', 'stale', 'partial')
|
|
738
|
+
AND NOT EXISTS (
|
|
739
|
+
SELECT 1
|
|
740
|
+
FROM _emdash_media_usage_work AS work
|
|
741
|
+
WHERE work.collection_id = ${input.collectionId}
|
|
742
|
+
)
|
|
743
|
+
)`;
|
|
744
|
+
const now = this.sortableUtcTimestamp();
|
|
745
|
+
const result = await this.db.updateTable("_emdash_media_usage_index_status").set({
|
|
746
|
+
status: sql`CASE WHEN ${canComplete} THEN 'complete' ELSE status END`,
|
|
747
|
+
completed_at: sql`CASE WHEN ${canComplete} THEN ${now} ELSE completed_at END`,
|
|
748
|
+
last_error_code: sql`CASE WHEN ${canComplete} THEN NULL ELSE last_error_code END`,
|
|
749
|
+
last_incremental_success_at: now,
|
|
750
|
+
updated_at: now
|
|
751
|
+
}).where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", input.collectionSlug).where("collection_id", "=", input.collectionId).where("change_epoch", "=", observed.change_epoch).where("capture_state", "=", "active").where(sql`EXISTS (
|
|
752
|
+
SELECT 1
|
|
753
|
+
FROM _emdash_collections AS collection
|
|
754
|
+
WHERE collection.id = ${input.collectionId}
|
|
755
|
+
AND collection.slug = ${input.collectionSlug}
|
|
756
|
+
)`).executeTakeFirst();
|
|
757
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
758
|
+
}
|
|
759
|
+
async recordIncrementalFailure(input) {
|
|
760
|
+
const now = this.sortableUtcTimestamp();
|
|
761
|
+
const automaticRunOwnsCoverage = sql`EXISTS (
|
|
762
|
+
SELECT 1
|
|
763
|
+
FROM _emdash_media_usage_reconciliations AS reconciliation
|
|
764
|
+
WHERE reconciliation.collection_id = ${input.collectionId}
|
|
765
|
+
AND reconciliation.run_token = cursor
|
|
766
|
+
)`;
|
|
767
|
+
const result = await this.db.updateTable("_emdash_media_usage_index_status").set({
|
|
768
|
+
status: sql`CASE
|
|
769
|
+
WHEN ${automaticRunOwnsCoverage} THEN status
|
|
770
|
+
WHEN reconciliation_required = 0 THEN 'partial'
|
|
771
|
+
WHEN status = 'running' THEN 'stale'
|
|
772
|
+
ELSE status
|
|
773
|
+
END`,
|
|
774
|
+
completed_at: sql`CASE
|
|
775
|
+
WHEN ${automaticRunOwnsCoverage} THEN completed_at
|
|
776
|
+
WHEN reconciliation_required = 0 OR status = 'running' THEN NULL
|
|
777
|
+
ELSE completed_at
|
|
778
|
+
END`,
|
|
779
|
+
cursor: sql`CASE
|
|
780
|
+
WHEN ${automaticRunOwnsCoverage} THEN cursor
|
|
781
|
+
WHEN status = 'running' THEN NULL
|
|
782
|
+
ELSE cursor
|
|
783
|
+
END`,
|
|
784
|
+
last_error_code: input.errorCode,
|
|
785
|
+
updated_at: now
|
|
786
|
+
}).where("adapter_id", "=", "content-media").where("scope_type", "=", "collection").where("scope_key", "=", input.collectionSlug).where("collection_id", "=", input.collectionId).where(sql`EXISTS (
|
|
787
|
+
SELECT 1
|
|
788
|
+
FROM _emdash_media_usage_work AS work
|
|
789
|
+
WHERE work.collection_id = ${input.collectionId}
|
|
790
|
+
AND work.content_id = ${input.contentId}
|
|
791
|
+
AND work.work_version = ${input.workVersion}
|
|
792
|
+
AND work.state = 'failed'
|
|
793
|
+
AND work.last_error_code = ${input.errorCode}
|
|
794
|
+
)`).where(sql`EXISTS (
|
|
795
|
+
SELECT 1
|
|
796
|
+
FROM _emdash_collections AS collection
|
|
797
|
+
WHERE collection.id = ${input.collectionId}
|
|
798
|
+
AND collection.slug = ${input.collectionSlug}
|
|
799
|
+
)`).executeTakeFirst();
|
|
800
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
801
|
+
}
|
|
802
|
+
async findIndexStatus(identity) {
|
|
803
|
+
const row = await this.db.selectFrom("_emdash_media_usage_index_status").selectAll().where("adapter_id", "=", identity.adapterId).where("scope_type", "=", identity.scopeType).where("scope_key", "=", identity.scopeKey).executeTakeFirst();
|
|
804
|
+
return row ? rowToIndexStatus(row) : null;
|
|
805
|
+
}
|
|
806
|
+
async findIndexStatusForCollection(identity, collectionId) {
|
|
807
|
+
const row = await this.db.selectFrom("_emdash_media_usage_index_status").selectAll().where("adapter_id", "=", identity.adapterId).where("scope_type", "=", identity.scopeType).where("scope_key", "=", identity.scopeKey).where("collection_id", "=", collectionId).executeTakeFirst();
|
|
808
|
+
return row ? rowToIndexStatus(row) : null;
|
|
809
|
+
}
|
|
810
|
+
async deleteIndexStatus(identity, collectionId) {
|
|
811
|
+
let query = this.db.deleteFrom("_emdash_media_usage_index_status").where("adapter_id", "=", identity.adapterId).where("scope_type", "=", identity.scopeType).where("scope_key", "=", identity.scopeKey);
|
|
812
|
+
if (collectionId !== void 0) query = query.where("collection_id", "=", collectionId);
|
|
813
|
+
const result = await query.executeTakeFirst();
|
|
814
|
+
return Number(result.numDeletedRows ?? 0);
|
|
815
|
+
}
|
|
816
|
+
sortableUtcTimestamp() {
|
|
817
|
+
return isPostgres(this.db) ? sql`to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')` : sql`strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
818
|
+
}
|
|
819
|
+
async findCurrentUsagePage(applyFilter, options) {
|
|
820
|
+
const limit = Math.min(Math.max(1, options.limit ?? 50), 100);
|
|
821
|
+
let query = applyFilter(this.currentUsageBaseQuery()).orderBy("u.id", "asc").limit(limit + 1);
|
|
822
|
+
if (options.cursor) {
|
|
823
|
+
const { id } = decodeCursor(options.cursor);
|
|
824
|
+
query = query.where("u.id", ">", id);
|
|
825
|
+
}
|
|
826
|
+
const rows = await query.execute();
|
|
827
|
+
const items = rows.slice(0, limit).map(rowToUsageRecord);
|
|
828
|
+
const result = { items };
|
|
829
|
+
if (rows.length > limit && items.length > 0) {
|
|
830
|
+
const last = items.at(-1);
|
|
831
|
+
result.nextCursor = encodeCursor(last.occurrence.id, last.occurrence.id);
|
|
832
|
+
}
|
|
833
|
+
return result;
|
|
834
|
+
}
|
|
835
|
+
currentUsageBaseQuery() {
|
|
836
|
+
return this.db.selectFrom("_emdash_media_usage_sources as s").innerJoin("_emdash_media_usage as u", (join) => join.onRef("u.source_key", "=", "s.source_key").onRef("u.generation", "=", "s.current_generation")).select(currentUsageSelect);
|
|
837
|
+
}
|
|
838
|
+
currentContentMediaUsageBaseQuery() {
|
|
839
|
+
return this.db.selectFrom("_emdash_media_usage as u").crossJoin("_emdash_media_usage_sources as s").innerJoin("_emdash_collections as collection", "collection.slug", "s.collection_slug").whereRef("s.source_key", "=", "u.source_key").whereRef("s.current_generation", "=", "u.generation").where("s.source_type", "=", "content").where("s.collection_slug", "is not", null).where("s.content_id", "is not", null).where("s.source_variant", "in", ["columns", "draft_overlay"]).where(contentSourceMatchesActiveCollection("s", "collection.id")).where(CONTENT_SOURCE_ELIGIBILITY);
|
|
840
|
+
}
|
|
841
|
+
async deleteOrphanCandidateIds(ids, cutoff, cleanupLease, canIssueStatement) {
|
|
842
|
+
let deleted = 0;
|
|
843
|
+
for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
|
|
844
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
845
|
+
if (cleanupLease) {
|
|
846
|
+
await this.markOrphanCandidatesForCleanup(idBatch, cutoff, cleanupLease);
|
|
847
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
848
|
+
}
|
|
849
|
+
let query = this.db.deleteFrom("_emdash_media_usage").where("id", "in", idBatch).where("created_at", "<", cutoff).where(sql`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources source WHERE source.source_key = _emdash_media_usage.source_key)`).where(this.noActiveGenerationWriteExpression());
|
|
850
|
+
if (cleanupLease) query = query.where("cleanup_lease_token", "=", cleanupLease.leaseToken).where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
851
|
+
const result = await query.executeTakeFirst();
|
|
852
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
853
|
+
}
|
|
854
|
+
return deleted;
|
|
855
|
+
}
|
|
856
|
+
async deleteStaleCandidateIds(ids, cutoff, cleanupLease, canIssueStatement) {
|
|
857
|
+
let deleted = 0;
|
|
858
|
+
for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
|
|
859
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
860
|
+
if (cleanupLease) {
|
|
861
|
+
await this.markStaleCandidatesForCleanup(idBatch, cutoff, cleanupLease);
|
|
862
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
863
|
+
}
|
|
864
|
+
let query = this.db.deleteFrom("_emdash_media_usage").where("id", "in", idBatch).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", "<", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression());
|
|
865
|
+
if (cleanupLease) query = query.where("cleanup_lease_token", "=", cleanupLease.leaseToken).where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
866
|
+
const result = await query.executeTakeFirst();
|
|
867
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
868
|
+
}
|
|
869
|
+
return deleted;
|
|
870
|
+
}
|
|
871
|
+
async deleteAbandonedCandidateIds(ids, cutoff, cleanupLease, canIssueStatement) {
|
|
872
|
+
let deleted = 0;
|
|
873
|
+
for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
|
|
874
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
875
|
+
if (cleanupLease) {
|
|
876
|
+
await this.markAbandonedCandidatesForCleanup(idBatch, cutoff, cleanupLease);
|
|
877
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
878
|
+
}
|
|
879
|
+
let query = this.db.deleteFrom("_emdash_media_usage").where("id", "in", idBatch).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", ">=", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression());
|
|
880
|
+
if (cleanupLease) query = query.where("cleanup_lease_token", "=", cleanupLease.leaseToken).where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
881
|
+
const result = await query.executeTakeFirst();
|
|
882
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
883
|
+
}
|
|
884
|
+
return deleted;
|
|
885
|
+
}
|
|
886
|
+
async markOrphanCandidatesForCleanup(ids, cutoff, cleanupLease) {
|
|
887
|
+
await this.db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: cleanupLease.leaseToken }).where("id", "in", ids).where("created_at", "<", cutoff).where(sql`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources source WHERE source.source_key = _emdash_media_usage.source_key)`).where(this.noActiveGenerationWriteExpression()).where(this.activeCleanupLeaseExpression(cleanupLease)).execute();
|
|
888
|
+
}
|
|
889
|
+
async markStaleCandidatesForCleanup(ids, cutoff, cleanupLease) {
|
|
890
|
+
await this.db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: cleanupLease.leaseToken }).where("id", "in", ids).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", "<", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression()).where(this.activeCleanupLeaseExpression(cleanupLease)).execute();
|
|
891
|
+
}
|
|
892
|
+
async markAbandonedCandidatesForCleanup(ids, cutoff, cleanupLease) {
|
|
893
|
+
await this.db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: cleanupLease.leaseToken }).where("id", "in", ids).where("created_at", "<", cutoff).where((eb) => eb.exists(eb.selectFrom("_emdash_media_usage_sources as source").select("source.source_key").whereRef("source.source_key", "=", "_emdash_media_usage.source_key").whereRef("source.current_generation", "!=", "_emdash_media_usage.generation").whereRef("_emdash_media_usage.created_at", ">=", "source.indexed_at"))).where(this.noActiveGenerationWriteExpression()).where(this.activeCleanupLeaseExpression(cleanupLease)).execute();
|
|
894
|
+
}
|
|
895
|
+
noActiveGenerationWriteExpression(usageTable = "_emdash_media_usage") {
|
|
896
|
+
return sql`NOT EXISTS (
|
|
897
|
+
SELECT 1
|
|
898
|
+
FROM _emdash_media_usage_generation_writes AS writer
|
|
899
|
+
WHERE writer.source_key = ${sql.ref(`${usageTable}.source_key`)}
|
|
900
|
+
AND writer.generation = ${sql.ref(`${usageTable}.generation`)}
|
|
901
|
+
AND ${this.generationWriteLeaseExpiryIsInFuture("writer.expires_at")}
|
|
902
|
+
)`;
|
|
903
|
+
}
|
|
904
|
+
activeCleanupLeaseExpression(cleanupLease) {
|
|
905
|
+
const rowLock = isPostgres(this.db) ? sql` FOR UPDATE` : sql``;
|
|
906
|
+
return sql`EXISTS (
|
|
907
|
+
SELECT 1
|
|
908
|
+
FROM _emdash_media_usage_cleanup AS cleanup
|
|
909
|
+
WHERE cleanup.task_key = 'projection_gc'
|
|
910
|
+
AND cleanup.lease_token = ${cleanupLease.leaseToken}
|
|
911
|
+
AND ${this.cleanupLeaseExpiryIsInFuture("cleanup.lease_expires_at")}
|
|
912
|
+
${rowLock}
|
|
913
|
+
)`;
|
|
914
|
+
}
|
|
915
|
+
cleanupLeaseExpiryIsInFuture(column) {
|
|
916
|
+
const leaseExpiresAt = sql.ref(column);
|
|
917
|
+
return isPostgres(this.db) ? sql`${leaseExpiresAt}::timestamptz > clock_timestamp()` : sql`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
918
|
+
}
|
|
919
|
+
cleanupTimestampIsDue(column) {
|
|
920
|
+
const timestamp = sql.ref(column);
|
|
921
|
+
return isPostgres(this.db) ? sql`${timestamp}::timestamptz <= clock_timestamp()` : sql`${timestamp} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
922
|
+
}
|
|
923
|
+
cleanupTimestampOffset(offsetSeconds) {
|
|
924
|
+
if (isPostgres(this.db)) return sql`to_char(
|
|
925
|
+
(clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
|
|
926
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
927
|
+
)`;
|
|
928
|
+
return sql`strftime(
|
|
929
|
+
'%Y-%m-%dT%H:%M:%fZ',
|
|
930
|
+
'now',
|
|
931
|
+
${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
|
|
932
|
+
)`;
|
|
933
|
+
}
|
|
934
|
+
generationWriteLeaseHasExpired(column) {
|
|
935
|
+
const leaseExpiresAt = sql.ref(column);
|
|
936
|
+
return isPostgres(this.db) ? sql`${leaseExpiresAt}::timestamptz <= clock_timestamp()` : sql`${leaseExpiresAt} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
937
|
+
}
|
|
938
|
+
async deleteSourceKeys(sourceKeys) {
|
|
939
|
+
const uniqueSourceKeys = [...new Set(sourceKeys)];
|
|
940
|
+
if (uniqueSourceKeys.length === 0) return 0;
|
|
941
|
+
return withTransaction(this.db, async (trx) => {
|
|
942
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
943
|
+
let deleted = 0;
|
|
944
|
+
for (const sourceKeyBatch of chunks(uniqueSourceKeys, SQL_BATCH_SIZE)) {
|
|
945
|
+
const result = await trx.deleteFrom("_emdash_media_usage_sources").where("source_key", "in", sourceKeyBatch).executeTakeFirst();
|
|
946
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
947
|
+
await trx.updateTable("_emdash_media_usage").set({ cleanup_lease_token: null }).where("source_key", "in", sourceKeyBatch).execute();
|
|
948
|
+
await trx.deleteFrom("_emdash_media_usage").where("source_key", "in", sourceKeyBatch).execute();
|
|
949
|
+
}
|
|
950
|
+
return deleted;
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
async deleteSourceGenerationOccurrences(db, sourceKey, generation) {
|
|
954
|
+
await db.updateTable("_emdash_media_usage").set({ cleanup_lease_token: null }).where("source_key", "=", sourceKey).where("generation", "=", generation).execute();
|
|
955
|
+
await db.deleteFrom("_emdash_media_usage").where("source_key", "=", sourceKey).where("generation", "=", generation).execute();
|
|
956
|
+
}
|
|
957
|
+
async lockCleanupBeforeSourceDelete(db) {
|
|
958
|
+
if (!isPostgres(this.db)) return;
|
|
959
|
+
await sql`
|
|
960
|
+
SELECT 1
|
|
961
|
+
FROM _emdash_media_usage_cleanup
|
|
962
|
+
WHERE task_key = 'projection_gc'
|
|
963
|
+
FOR SHARE
|
|
964
|
+
`.execute(db);
|
|
965
|
+
}
|
|
966
|
+
async insertOccurrences(db, sourceKey, generation, occurrences, now) {
|
|
967
|
+
if (occurrences.length === 0) return;
|
|
968
|
+
const rows = occurrences.map((occurrence) => ({
|
|
969
|
+
id: ulid(),
|
|
970
|
+
source_key: sourceKey,
|
|
971
|
+
generation,
|
|
972
|
+
field_slug: occurrence.fieldSlug,
|
|
973
|
+
field_path: occurrence.fieldPath,
|
|
974
|
+
occurrence_index: occurrence.occurrenceIndex ?? 0,
|
|
975
|
+
reference_type: occurrence.referenceType,
|
|
976
|
+
media_id: occurrence.mediaId,
|
|
977
|
+
provider: occurrence.provider,
|
|
978
|
+
provider_asset_id: occurrence.providerAssetId,
|
|
979
|
+
media_kind: occurrence.mediaKind ?? null,
|
|
980
|
+
mime_type: occurrence.mimeType ?? null,
|
|
981
|
+
created_at: now
|
|
982
|
+
}));
|
|
983
|
+
for (const rowBatch of chunks(rows, OCCURRENCE_INSERT_BATCH_SIZE)) await db.insertInto("_emdash_media_usage").values(rowBatch).execute();
|
|
984
|
+
}
|
|
985
|
+
async lockCanonicalSourceCollection(db, source) {
|
|
986
|
+
if (source.collectionId === void 0 || source.collectionId === null) return true;
|
|
987
|
+
if (!source.collectionSlug) return false;
|
|
988
|
+
if (!isPostgres(this.db)) return true;
|
|
989
|
+
return await db.selectFrom("_emdash_collections").select("id").where("id", "=", source.collectionId).where("slug", "=", source.collectionSlug).forKeyShare().executeTakeFirst() !== void 0;
|
|
990
|
+
}
|
|
991
|
+
async upsertSource(db, source, generation, now, leaseToken) {
|
|
992
|
+
const row = this.buildSourceRow(source, generation, now);
|
|
993
|
+
return this.persistSourceIfWriteLease(db, row, leaseToken, sql`
|
|
994
|
+
ON CONFLICT (source_key) DO UPDATE SET
|
|
995
|
+
source_type = excluded.source_type,
|
|
996
|
+
collection_id = excluded.collection_id,
|
|
997
|
+
collection_slug = excluded.collection_slug,
|
|
998
|
+
content_id = excluded.content_id,
|
|
999
|
+
source_variant = excluded.source_variant,
|
|
1000
|
+
locale = excluded.locale,
|
|
1001
|
+
translation_group = excluded.translation_group,
|
|
1002
|
+
content_slug = excluded.content_slug,
|
|
1003
|
+
content_title = excluded.content_title,
|
|
1004
|
+
content_status = excluded.content_status,
|
|
1005
|
+
content_scheduled_at = excluded.content_scheduled_at,
|
|
1006
|
+
content_deleted_at = excluded.content_deleted_at,
|
|
1007
|
+
revision_id = excluded.revision_id,
|
|
1008
|
+
current_generation = excluded.current_generation,
|
|
1009
|
+
schema_version = excluded.schema_version,
|
|
1010
|
+
source_updated_at = excluded.source_updated_at,
|
|
1011
|
+
source_version = excluded.source_version,
|
|
1012
|
+
source_fingerprint = excluded.source_fingerprint,
|
|
1013
|
+
identity_version = excluded.identity_version,
|
|
1014
|
+
source_completeness = excluded.source_completeness,
|
|
1015
|
+
last_attempted_at = excluded.last_attempted_at,
|
|
1016
|
+
last_error_code = excluded.last_error_code,
|
|
1017
|
+
indexed_at = excluded.indexed_at,
|
|
1018
|
+
updated_at = excluded.updated_at
|
|
1019
|
+
`);
|
|
1020
|
+
}
|
|
1021
|
+
async insertSourceIfAbsent(db, row, leaseToken) {
|
|
1022
|
+
return this.persistSourceIfWriteLease(db, row, leaseToken, sql`ON CONFLICT (source_key) DO NOTHING`);
|
|
1023
|
+
}
|
|
1024
|
+
async persistSourceIfWriteLease(db, row, leaseToken, conflict) {
|
|
1025
|
+
const result = await sql`
|
|
1026
|
+
INSERT INTO _emdash_media_usage_sources (
|
|
1027
|
+
source_key,
|
|
1028
|
+
source_type,
|
|
1029
|
+
collection_id,
|
|
1030
|
+
collection_slug,
|
|
1031
|
+
content_id,
|
|
1032
|
+
source_variant,
|
|
1033
|
+
locale,
|
|
1034
|
+
translation_group,
|
|
1035
|
+
content_slug,
|
|
1036
|
+
content_title,
|
|
1037
|
+
content_status,
|
|
1038
|
+
content_scheduled_at,
|
|
1039
|
+
content_deleted_at,
|
|
1040
|
+
revision_id,
|
|
1041
|
+
current_generation,
|
|
1042
|
+
schema_version,
|
|
1043
|
+
source_updated_at,
|
|
1044
|
+
source_version,
|
|
1045
|
+
source_fingerprint,
|
|
1046
|
+
identity_version,
|
|
1047
|
+
source_completeness,
|
|
1048
|
+
last_attempted_at,
|
|
1049
|
+
last_error_code,
|
|
1050
|
+
indexed_at,
|
|
1051
|
+
updated_at
|
|
1052
|
+
)
|
|
1053
|
+
SELECT
|
|
1054
|
+
${row.source_key},
|
|
1055
|
+
${row.source_type},
|
|
1056
|
+
${row.collection_id},
|
|
1057
|
+
${row.collection_slug},
|
|
1058
|
+
${row.content_id},
|
|
1059
|
+
${row.source_variant},
|
|
1060
|
+
${row.locale},
|
|
1061
|
+
${row.translation_group},
|
|
1062
|
+
${row.content_slug},
|
|
1063
|
+
${row.content_title},
|
|
1064
|
+
${row.content_status},
|
|
1065
|
+
${row.content_scheduled_at},
|
|
1066
|
+
${row.content_deleted_at},
|
|
1067
|
+
${row.revision_id},
|
|
1068
|
+
${row.current_generation},
|
|
1069
|
+
${row.schema_version},
|
|
1070
|
+
${row.source_updated_at},
|
|
1071
|
+
${row.source_version},
|
|
1072
|
+
${row.source_fingerprint},
|
|
1073
|
+
${row.identity_version},
|
|
1074
|
+
${row.source_completeness},
|
|
1075
|
+
${row.last_attempted_at},
|
|
1076
|
+
${row.last_error_code},
|
|
1077
|
+
${row.indexed_at},
|
|
1078
|
+
${row.updated_at}
|
|
1079
|
+
WHERE EXISTS (
|
|
1080
|
+
SELECT 1
|
|
1081
|
+
FROM _emdash_media_usage_generation_writes
|
|
1082
|
+
WHERE source_key = ${row.source_key}
|
|
1083
|
+
AND generation = ${row.current_generation}
|
|
1084
|
+
AND lease_token = ${leaseToken}
|
|
1085
|
+
AND ${this.generationWriteLeaseExpiryIsInFuture("expires_at")}
|
|
1086
|
+
)
|
|
1087
|
+
AND ${this.currentCollectionExists(row.collection_id, row.collection_slug)}
|
|
1088
|
+
AND ${this.currentCanonicalContentExists(row)}
|
|
1089
|
+
${conflict}
|
|
1090
|
+
`.execute(db);
|
|
1091
|
+
return Number(result.numAffectedRows ?? 0) > 0;
|
|
1092
|
+
}
|
|
1093
|
+
generationWriteLeaseExpression(row, leaseToken) {
|
|
1094
|
+
return (eb) => eb.exists(eb.selectFrom("_emdash_media_usage_generation_writes").select("source_key").where("source_key", "=", row.source_key).where("generation", "=", row.current_generation).where("lease_token", "=", leaseToken).where(this.generationWriteLeaseExpiryIsInFuture("_emdash_media_usage_generation_writes.expires_at")));
|
|
1095
|
+
}
|
|
1096
|
+
generationWriteLeaseExpiryIsInFuture(column) {
|
|
1097
|
+
const leaseExpiresAt = sql.ref(column);
|
|
1098
|
+
return isPostgres(this.db) ? sql`${leaseExpiresAt}::timestamptz > clock_timestamp()` : sql`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
1099
|
+
}
|
|
1100
|
+
async withGenerationWriteLease(source, generation, write) {
|
|
1101
|
+
const leaseToken = ulid();
|
|
1102
|
+
const owner = (await sql`
|
|
1103
|
+
INSERT INTO _emdash_media_usage_generation_writes (
|
|
1104
|
+
source_key, generation, lease_token, expires_at, created_at
|
|
1105
|
+
)
|
|
1106
|
+
SELECT
|
|
1107
|
+
${source.sourceKey},
|
|
1108
|
+
${generation},
|
|
1109
|
+
${leaseToken},
|
|
1110
|
+
${this.generationWriteLeaseTimestampOffset(MEDIA_USAGE_GENERATION_WRITE_LEASE_MS / 1e3)},
|
|
1111
|
+
${this.generationWriteLeaseTimestampOffset(0)}
|
|
1112
|
+
WHERE ${this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null)}
|
|
1113
|
+
RETURNING created_at
|
|
1114
|
+
`.execute(this.db)).rows[0];
|
|
1115
|
+
if (!owner) return false;
|
|
1116
|
+
try {
|
|
1117
|
+
await write(leaseToken, owner.created_at);
|
|
1118
|
+
return true;
|
|
1119
|
+
} finally {
|
|
1120
|
+
try {
|
|
1121
|
+
await this.db.deleteFrom("_emdash_media_usage_generation_writes").where("source_key", "=", source.sourceKey).where("generation", "=", generation).where("lease_token", "=", leaseToken).execute();
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
console.error("[media-usage] Failed to release generation write lease:", error);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
generationWriteLeaseTimestampOffset(offsetSeconds) {
|
|
1128
|
+
if (isPostgres(this.db)) return sql`to_char(
|
|
1129
|
+
(clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
|
|
1130
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
1131
|
+
)`;
|
|
1132
|
+
return sql`strftime(
|
|
1133
|
+
'%Y-%m-%dT%H:%M:%fZ',
|
|
1134
|
+
'now',
|
|
1135
|
+
${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
|
|
1136
|
+
)`;
|
|
1137
|
+
}
|
|
1138
|
+
async updateSourceIfGeneration(db, row, expectedCurrentGeneration, leaseToken) {
|
|
1139
|
+
const result = await db.updateTable("_emdash_media_usage_sources").set(this.sourceUpdateSet(row)).where("source_key", "=", row.source_key).where("current_generation", "=", expectedCurrentGeneration).where(this.generationWriteLeaseExpression(row, leaseToken)).where(this.currentCollectionExists(row.collection_id, row.collection_slug)).where(this.currentCanonicalContentExists(row)).executeTakeFirst();
|
|
1140
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1141
|
+
}
|
|
1142
|
+
async updateSourceIfMatching(db, row, expectedSource, leaseToken) {
|
|
1143
|
+
const result = await db.updateTable("_emdash_media_usage_sources").set(this.sourceUpdateSet(row)).where("source_key", "=", row.source_key).where(this.sourceMatchExpression(expectedSource)).where(this.generationWriteLeaseExpression(row, leaseToken)).where(this.currentCollectionExists(row.collection_id, row.collection_slug)).where(this.currentCanonicalContentExists(row)).executeTakeFirst();
|
|
1144
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1145
|
+
}
|
|
1146
|
+
async updateAttemptedSourceIfMatching(db, source, row, expectedSource) {
|
|
1147
|
+
const result = await db.updateTable("_emdash_media_usage_sources").set(this.attemptedSourceUpdateSet(source, row)).where("source_key", "=", row.source_key).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(row.collection_id, row.collection_slug)).where(this.currentCanonicalContentExists(row)).executeTakeFirst();
|
|
1148
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1149
|
+
}
|
|
1150
|
+
sourceMatchExpression(expectedSource) {
|
|
1151
|
+
return (eb) => eb.and([
|
|
1152
|
+
eb("current_generation", "=", expectedSource.currentGeneration),
|
|
1153
|
+
eb("source_completeness", "=", expectedSource.sourceCompleteness),
|
|
1154
|
+
this.nullableStringExpression(eb, "collection_id", expectedSource.collectionId),
|
|
1155
|
+
this.nullableStringExpression(eb, "updated_at", expectedSource.updatedAt),
|
|
1156
|
+
this.nullableStringExpression(eb, "source_fingerprint", expectedSource.sourceFingerprint),
|
|
1157
|
+
this.nullableStringExpression(eb, "source_updated_at", expectedSource.sourceUpdatedAt),
|
|
1158
|
+
this.nullableNumberExpression(eb, "source_version", expectedSource.sourceVersion),
|
|
1159
|
+
this.nullableNumberExpression(eb, "identity_version", expectedSource.identityVersion),
|
|
1160
|
+
this.nullableStringExpression(eb, "revision_id", expectedSource.revisionId),
|
|
1161
|
+
this.nullableStringExpression(eb, "last_attempted_at", expectedSource.lastAttemptedAt),
|
|
1162
|
+
this.nullableStringExpression(eb, "last_error_code", expectedSource.lastErrorCode)
|
|
1163
|
+
]);
|
|
1164
|
+
}
|
|
1165
|
+
async projectionMatchesCurrentGeneration(source, expectedCurrentGeneration) {
|
|
1166
|
+
const fingerprint = source.sourceFingerprint;
|
|
1167
|
+
if (!isMediaUsageProjectionFingerprint(fingerprint)) return false;
|
|
1168
|
+
return await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_key", "=", source.sourceKey).where("current_generation", "=", expectedCurrentGeneration).where("source_fingerprint", "=", fingerprint).where("source_completeness", "=", source.sourceCompleteness ?? "complete").where("last_error_code", "is", null).where(this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null)).executeTakeFirst() !== void 0;
|
|
1169
|
+
}
|
|
1170
|
+
async projectionMatchesExpectedSource(source, expectedSource) {
|
|
1171
|
+
const fingerprint = source.sourceFingerprint;
|
|
1172
|
+
if (!isMediaUsageProjectionFingerprint(fingerprint) || expectedSource.sourceFingerprint !== fingerprint || expectedSource.sourceCompleteness !== (source.sourceCompleteness ?? "complete") || expectedSource.lastErrorCode !== null) return false;
|
|
1173
|
+
return await this.db.selectFrom("_emdash_media_usage_sources").select("source_key").where("source_key", "=", source.sourceKey).where(this.sourceMatchExpression(expectedSource)).where(this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null)).executeTakeFirst() !== void 0;
|
|
1174
|
+
}
|
|
1175
|
+
nullableStringExpression(eb, column, value) {
|
|
1176
|
+
return value === null ? eb(column, "is", null) : eb(column, "=", value);
|
|
1177
|
+
}
|
|
1178
|
+
currentCollectionExists(collectionId, collectionSlug) {
|
|
1179
|
+
if (collectionId === null) return sql`1 = 1`;
|
|
1180
|
+
return sql`EXISTS (
|
|
1181
|
+
SELECT 1
|
|
1182
|
+
FROM _emdash_collections
|
|
1183
|
+
WHERE id = ${collectionId}
|
|
1184
|
+
AND slug = ${collectionSlug}
|
|
1185
|
+
)`;
|
|
1186
|
+
}
|
|
1187
|
+
currentCanonicalContentExists(row) {
|
|
1188
|
+
if (row.collection_id === null || row.identity_version !== 1 || row.source_type !== "content") return sql`1 = 1`;
|
|
1189
|
+
if (!row.collection_slug || !row.content_id || row.source_version === null || row.source_updated_at === null) return sql`1 = 0`;
|
|
1190
|
+
validateIdentifier(row.collection_slug, "collection slug");
|
|
1191
|
+
const tableName = `ec_${row.collection_slug}`;
|
|
1192
|
+
validateIdentifier(tableName, "content table");
|
|
1193
|
+
const revisionColumn = row.source_variant === "columns" ? "live_revision_id" : row.source_variant === "draft_overlay" ? "draft_revision_id" : null;
|
|
1194
|
+
if (!revisionColumn) return sql`1 = 0`;
|
|
1195
|
+
const revision = sql.ref(`content.${revisionColumn}`);
|
|
1196
|
+
const revisionMatches = row.revision_id === null ? sql`${revision} IS NULL` : sql`${revision} = ${row.revision_id}`;
|
|
1197
|
+
return sql`EXISTS (
|
|
1198
|
+
SELECT 1
|
|
1199
|
+
FROM ${sql.ref(tableName)} AS content
|
|
1200
|
+
WHERE content.id = ${row.content_id}
|
|
1201
|
+
AND content.version = ${row.source_version}
|
|
1202
|
+
AND content.updated_at = ${row.source_updated_at}
|
|
1203
|
+
AND ${revisionMatches}
|
|
1204
|
+
)`;
|
|
1205
|
+
}
|
|
1206
|
+
nullableNumberExpression(eb, column, value) {
|
|
1207
|
+
return value === null ? eb(column, "is", null) : eb(column, "=", value);
|
|
1208
|
+
}
|
|
1209
|
+
async contentRowExists(tableName, contentId) {
|
|
1210
|
+
return (await sql`
|
|
1211
|
+
SELECT id
|
|
1212
|
+
FROM ${sql.ref(tableName)}
|
|
1213
|
+
WHERE id = ${contentId}
|
|
1214
|
+
LIMIT 1
|
|
1215
|
+
`.execute(this.db)).rows.length > 0;
|
|
1216
|
+
}
|
|
1217
|
+
buildSourceRow(source, generation, now) {
|
|
1218
|
+
return {
|
|
1219
|
+
source_key: source.sourceKey,
|
|
1220
|
+
source_type: source.sourceType,
|
|
1221
|
+
collection_id: source.collectionId ?? null,
|
|
1222
|
+
collection_slug: source.collectionSlug ?? null,
|
|
1223
|
+
content_id: source.contentId ?? null,
|
|
1224
|
+
source_variant: source.sourceVariant,
|
|
1225
|
+
locale: source.locale ?? null,
|
|
1226
|
+
translation_group: source.translationGroup ?? null,
|
|
1227
|
+
content_slug: source.contentSlug ?? null,
|
|
1228
|
+
content_title: source.contentTitle ?? null,
|
|
1229
|
+
content_status: source.contentStatus ?? null,
|
|
1230
|
+
content_scheduled_at: source.contentScheduledAt ?? null,
|
|
1231
|
+
content_deleted_at: source.contentDeletedAt ?? null,
|
|
1232
|
+
revision_id: source.revisionId ?? null,
|
|
1233
|
+
current_generation: generation,
|
|
1234
|
+
schema_version: source.schemaVersion ?? 1,
|
|
1235
|
+
source_updated_at: source.sourceUpdatedAt ?? null,
|
|
1236
|
+
source_version: source.sourceVersion ?? null,
|
|
1237
|
+
source_fingerprint: source.sourceFingerprint ?? null,
|
|
1238
|
+
identity_version: source.identityVersion ?? null,
|
|
1239
|
+
source_completeness: source.sourceCompleteness ?? "complete",
|
|
1240
|
+
last_attempted_at: source.lastAttemptedAt ?? now,
|
|
1241
|
+
last_error_code: null,
|
|
1242
|
+
indexed_at: now,
|
|
1243
|
+
updated_at: now
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
buildAttemptedSourceRow(source, generation, now) {
|
|
1247
|
+
return {
|
|
1248
|
+
source_key: source.sourceKey,
|
|
1249
|
+
source_type: source.sourceType,
|
|
1250
|
+
collection_id: source.collectionId ?? null,
|
|
1251
|
+
collection_slug: source.collectionSlug ?? null,
|
|
1252
|
+
content_id: source.contentId ?? null,
|
|
1253
|
+
source_variant: source.sourceVariant,
|
|
1254
|
+
locale: source.locale ?? null,
|
|
1255
|
+
translation_group: source.translationGroup ?? null,
|
|
1256
|
+
content_slug: source.contentSlug ?? null,
|
|
1257
|
+
content_title: source.contentTitle ?? null,
|
|
1258
|
+
content_status: source.contentStatus ?? null,
|
|
1259
|
+
content_scheduled_at: source.contentScheduledAt ?? null,
|
|
1260
|
+
content_deleted_at: source.contentDeletedAt ?? null,
|
|
1261
|
+
revision_id: source.revisionId ?? null,
|
|
1262
|
+
current_generation: generation,
|
|
1263
|
+
schema_version: source.schemaVersion ?? 1,
|
|
1264
|
+
source_updated_at: source.sourceUpdatedAt ?? null,
|
|
1265
|
+
source_version: source.sourceVersion ?? null,
|
|
1266
|
+
source_fingerprint: source.sourceFingerprint ?? null,
|
|
1267
|
+
identity_version: source.identityVersion ?? null,
|
|
1268
|
+
source_completeness: source.sourceCompleteness ?? (source.lastErrorCode ? "failed" : "unknown"),
|
|
1269
|
+
last_attempted_at: source.lastAttemptedAt ?? now,
|
|
1270
|
+
last_error_code: source.lastErrorCode ?? null,
|
|
1271
|
+
indexed_at: now,
|
|
1272
|
+
updated_at: now
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
attemptedSourceUpdateSet(source, row) {
|
|
1276
|
+
const updates = {
|
|
1277
|
+
source_type: row.source_type,
|
|
1278
|
+
source_variant: row.source_variant,
|
|
1279
|
+
source_completeness: row.source_completeness,
|
|
1280
|
+
last_attempted_at: row.last_attempted_at,
|
|
1281
|
+
last_error_code: row.last_error_code,
|
|
1282
|
+
updated_at: row.updated_at
|
|
1283
|
+
};
|
|
1284
|
+
if (source.collectionSlug !== void 0) updates.collection_slug = row.collection_slug;
|
|
1285
|
+
if (source.collectionId !== void 0) updates.collection_id = row.collection_id;
|
|
1286
|
+
if (source.contentId !== void 0) updates.content_id = row.content_id;
|
|
1287
|
+
if (source.locale !== void 0) updates.locale = row.locale;
|
|
1288
|
+
if (source.translationGroup !== void 0) updates.translation_group = row.translation_group;
|
|
1289
|
+
if (source.contentSlug !== void 0) updates.content_slug = row.content_slug;
|
|
1290
|
+
if (source.contentTitle !== void 0) updates.content_title = row.content_title;
|
|
1291
|
+
if (source.contentStatus !== void 0) updates.content_status = row.content_status;
|
|
1292
|
+
if (source.contentScheduledAt !== void 0) updates.content_scheduled_at = row.content_scheduled_at;
|
|
1293
|
+
if (source.contentDeletedAt !== void 0) updates.content_deleted_at = row.content_deleted_at;
|
|
1294
|
+
if (source.revisionId !== void 0) updates.revision_id = row.revision_id;
|
|
1295
|
+
if (source.schemaVersion !== void 0) updates.schema_version = row.schema_version;
|
|
1296
|
+
if (source.sourceUpdatedAt !== void 0) updates.source_updated_at = row.source_updated_at;
|
|
1297
|
+
if (source.sourceVersion !== void 0) updates.source_version = row.source_version;
|
|
1298
|
+
if (source.sourceFingerprint !== void 0) updates.source_fingerprint = row.source_fingerprint;
|
|
1299
|
+
if (source.identityVersion !== void 0) updates.identity_version = row.identity_version;
|
|
1300
|
+
return updates;
|
|
1301
|
+
}
|
|
1302
|
+
sourceUpdateSet(row) {
|
|
1303
|
+
return {
|
|
1304
|
+
source_type: row.source_type,
|
|
1305
|
+
collection_id: row.collection_id,
|
|
1306
|
+
collection_slug: row.collection_slug,
|
|
1307
|
+
content_id: row.content_id,
|
|
1308
|
+
source_variant: row.source_variant,
|
|
1309
|
+
locale: row.locale,
|
|
1310
|
+
translation_group: row.translation_group,
|
|
1311
|
+
content_slug: row.content_slug,
|
|
1312
|
+
content_title: row.content_title,
|
|
1313
|
+
content_status: row.content_status,
|
|
1314
|
+
content_scheduled_at: row.content_scheduled_at,
|
|
1315
|
+
content_deleted_at: row.content_deleted_at,
|
|
1316
|
+
revision_id: row.revision_id,
|
|
1317
|
+
current_generation: row.current_generation,
|
|
1318
|
+
schema_version: row.schema_version,
|
|
1319
|
+
source_updated_at: row.source_updated_at,
|
|
1320
|
+
source_version: row.source_version,
|
|
1321
|
+
source_fingerprint: row.source_fingerprint,
|
|
1322
|
+
identity_version: row.identity_version,
|
|
1323
|
+
source_completeness: row.source_completeness,
|
|
1324
|
+
last_attempted_at: row.last_attempted_at,
|
|
1325
|
+
last_error_code: row.last_error_code,
|
|
1326
|
+
indexed_at: row.indexed_at,
|
|
1327
|
+
updated_at: row.updated_at
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
const currentUsageSelect = [
|
|
1332
|
+
"s.source_key as source_key",
|
|
1333
|
+
"s.source_type as source_type",
|
|
1334
|
+
"s.collection_id as collection_id",
|
|
1335
|
+
"s.collection_slug as collection_slug",
|
|
1336
|
+
"s.content_id as content_id",
|
|
1337
|
+
"s.source_variant as source_variant",
|
|
1338
|
+
"s.locale as locale",
|
|
1339
|
+
"s.translation_group as translation_group",
|
|
1340
|
+
"s.content_slug as content_slug",
|
|
1341
|
+
"s.content_title as content_title",
|
|
1342
|
+
"s.content_status as content_status",
|
|
1343
|
+
"s.content_scheduled_at as content_scheduled_at",
|
|
1344
|
+
"s.content_deleted_at as content_deleted_at",
|
|
1345
|
+
"s.revision_id as revision_id",
|
|
1346
|
+
"s.current_generation as current_generation",
|
|
1347
|
+
"s.schema_version as schema_version",
|
|
1348
|
+
"s.source_updated_at as source_updated_at",
|
|
1349
|
+
"s.source_version as source_version",
|
|
1350
|
+
"s.source_fingerprint as source_fingerprint",
|
|
1351
|
+
"s.identity_version as identity_version",
|
|
1352
|
+
"s.source_completeness as source_completeness",
|
|
1353
|
+
"s.last_attempted_at as last_attempted_at",
|
|
1354
|
+
"s.last_error_code as last_error_code",
|
|
1355
|
+
"s.indexed_at as indexed_at",
|
|
1356
|
+
"s.created_at as source_created_at",
|
|
1357
|
+
"s.updated_at as source_row_updated_at",
|
|
1358
|
+
"u.id as occurrence_id",
|
|
1359
|
+
"u.generation as generation",
|
|
1360
|
+
"u.field_slug as field_slug",
|
|
1361
|
+
"u.field_path as field_path",
|
|
1362
|
+
"u.occurrence_index as occurrence_index",
|
|
1363
|
+
"u.reference_type as reference_type",
|
|
1364
|
+
"u.media_id as media_id",
|
|
1365
|
+
"u.provider as provider",
|
|
1366
|
+
"u.provider_asset_id as provider_asset_id",
|
|
1367
|
+
"u.media_kind as media_kind",
|
|
1368
|
+
"u.mime_type as mime_type",
|
|
1369
|
+
"u.created_at as occurrence_created_at"
|
|
1370
|
+
];
|
|
1371
|
+
function groupUsageRows(rows) {
|
|
1372
|
+
const groups = [];
|
|
1373
|
+
for (const row of rows) {
|
|
1374
|
+
if (row.collection_slug === null || row.content_id === null) continue;
|
|
1375
|
+
const record = rowToUsageRecord(row);
|
|
1376
|
+
let group = groups.at(-1);
|
|
1377
|
+
if (!group || group.collectionSlug !== row.collection_slug || group.contentId !== row.content_id) {
|
|
1378
|
+
group = {
|
|
1379
|
+
collectionSlug: row.collection_slug,
|
|
1380
|
+
contentId: row.content_id,
|
|
1381
|
+
contentDeletedAt: row.entry_deleted_at,
|
|
1382
|
+
sources: []
|
|
1383
|
+
};
|
|
1384
|
+
groups.push(group);
|
|
1385
|
+
}
|
|
1386
|
+
let source = group.sources.at(-1);
|
|
1387
|
+
if (!source || source.source.sourceKey !== record.source.sourceKey) {
|
|
1388
|
+
source = {
|
|
1389
|
+
source: record.source,
|
|
1390
|
+
occurrences: []
|
|
1391
|
+
};
|
|
1392
|
+
group.sources.push(source);
|
|
1393
|
+
}
|
|
1394
|
+
source.occurrences.push(record.occurrence);
|
|
1395
|
+
}
|
|
1396
|
+
return groups;
|
|
1397
|
+
}
|
|
1398
|
+
function rowToSource(row) {
|
|
1399
|
+
return {
|
|
1400
|
+
sourceKey: row.source_key,
|
|
1401
|
+
sourceType: row.source_type,
|
|
1402
|
+
collectionId: row.collection_id,
|
|
1403
|
+
collectionSlug: row.collection_slug,
|
|
1404
|
+
contentId: row.content_id,
|
|
1405
|
+
sourceVariant: row.source_variant,
|
|
1406
|
+
locale: row.locale,
|
|
1407
|
+
translationGroup: row.translation_group,
|
|
1408
|
+
contentSlug: row.content_slug,
|
|
1409
|
+
contentTitle: row.content_title,
|
|
1410
|
+
contentStatus: row.content_status,
|
|
1411
|
+
contentScheduledAt: row.content_scheduled_at,
|
|
1412
|
+
contentDeletedAt: row.content_deleted_at,
|
|
1413
|
+
revisionId: row.revision_id,
|
|
1414
|
+
currentGeneration: row.current_generation,
|
|
1415
|
+
schemaVersion: Number(row.schema_version),
|
|
1416
|
+
sourceUpdatedAt: row.source_updated_at,
|
|
1417
|
+
sourceVersion: row.source_version === null ? null : Number(row.source_version),
|
|
1418
|
+
sourceFingerprint: row.source_fingerprint,
|
|
1419
|
+
identityVersion: row.identity_version === null ? null : Number(row.identity_version),
|
|
1420
|
+
sourceCompleteness: row.source_completeness,
|
|
1421
|
+
lastAttemptedAt: row.last_attempted_at,
|
|
1422
|
+
lastErrorCode: row.last_error_code,
|
|
1423
|
+
indexedAt: row.indexed_at,
|
|
1424
|
+
createdAt: row.created_at,
|
|
1425
|
+
updatedAt: row.updated_at
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
function rowToOccurrence(row) {
|
|
1429
|
+
return {
|
|
1430
|
+
id: row.id,
|
|
1431
|
+
sourceKey: row.source_key,
|
|
1432
|
+
generation: row.generation,
|
|
1433
|
+
fieldSlug: row.field_slug,
|
|
1434
|
+
fieldPath: row.field_path,
|
|
1435
|
+
occurrenceIndex: Number(row.occurrence_index),
|
|
1436
|
+
referenceType: row.reference_type,
|
|
1437
|
+
mediaId: row.media_id,
|
|
1438
|
+
provider: row.provider,
|
|
1439
|
+
providerAssetId: row.provider_asset_id,
|
|
1440
|
+
mediaKind: row.media_kind,
|
|
1441
|
+
mimeType: row.mime_type,
|
|
1442
|
+
createdAt: row.created_at
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
function rowToUsageRecord(row) {
|
|
1446
|
+
return {
|
|
1447
|
+
source: rowToSource({
|
|
1448
|
+
source_key: row.source_key,
|
|
1449
|
+
source_type: row.source_type,
|
|
1450
|
+
collection_id: row.collection_id,
|
|
1451
|
+
collection_slug: row.collection_slug,
|
|
1452
|
+
content_id: row.content_id,
|
|
1453
|
+
source_variant: row.source_variant,
|
|
1454
|
+
locale: row.locale,
|
|
1455
|
+
translation_group: row.translation_group,
|
|
1456
|
+
content_slug: row.content_slug,
|
|
1457
|
+
content_title: row.content_title,
|
|
1458
|
+
content_status: row.content_status,
|
|
1459
|
+
content_scheduled_at: row.content_scheduled_at,
|
|
1460
|
+
content_deleted_at: row.content_deleted_at,
|
|
1461
|
+
revision_id: row.revision_id,
|
|
1462
|
+
current_generation: row.current_generation,
|
|
1463
|
+
schema_version: row.schema_version,
|
|
1464
|
+
source_updated_at: row.source_updated_at,
|
|
1465
|
+
source_version: row.source_version,
|
|
1466
|
+
source_fingerprint: row.source_fingerprint,
|
|
1467
|
+
identity_version: row.identity_version,
|
|
1468
|
+
source_completeness: row.source_completeness,
|
|
1469
|
+
last_attempted_at: row.last_attempted_at,
|
|
1470
|
+
last_error_code: row.last_error_code,
|
|
1471
|
+
indexed_at: row.indexed_at,
|
|
1472
|
+
created_at: row.source_created_at,
|
|
1473
|
+
updated_at: row.source_row_updated_at
|
|
1474
|
+
}),
|
|
1475
|
+
occurrence: rowToOccurrence({
|
|
1476
|
+
id: row.occurrence_id,
|
|
1477
|
+
source_key: row.source_key,
|
|
1478
|
+
generation: row.generation,
|
|
1479
|
+
field_slug: row.field_slug,
|
|
1480
|
+
field_path: row.field_path,
|
|
1481
|
+
occurrence_index: row.occurrence_index,
|
|
1482
|
+
reference_type: row.reference_type,
|
|
1483
|
+
media_id: row.media_id,
|
|
1484
|
+
provider: row.provider,
|
|
1485
|
+
provider_asset_id: row.provider_asset_id,
|
|
1486
|
+
media_kind: row.media_kind,
|
|
1487
|
+
mime_type: row.mime_type,
|
|
1488
|
+
created_at: row.occurrence_created_at,
|
|
1489
|
+
cleanup_lease_token: null
|
|
1490
|
+
})
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
function rowToIndexStatus(row) {
|
|
1494
|
+
return {
|
|
1495
|
+
adapterId: row.adapter_id,
|
|
1496
|
+
scopeType: row.scope_type,
|
|
1497
|
+
scopeKey: row.scope_key,
|
|
1498
|
+
status: row.status,
|
|
1499
|
+
schemaVersion: Number(row.schema_version),
|
|
1500
|
+
startedAt: row.started_at,
|
|
1501
|
+
completedAt: row.completed_at,
|
|
1502
|
+
cursor: row.cursor,
|
|
1503
|
+
indexedSourceCount: Number(row.indexed_source_count),
|
|
1504
|
+
failedSourceCount: Number(row.failed_source_count),
|
|
1505
|
+
lastErrorCode: row.last_error_code,
|
|
1506
|
+
updatedAt: row.updated_at
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
//#endregion
|
|
1511
|
+
//#region src/media/usage/types.ts
|
|
1512
|
+
const CONTENT_SOURCE_SCHEMA_VERSION = 1;
|
|
1513
|
+
|
|
1514
|
+
//#endregion
|
|
1515
|
+
//#region src/media/usage/content-fields.ts
|
|
1516
|
+
async function buildContentMediaUsageFieldFingerprint(discovery) {
|
|
1517
|
+
return (await buildCanonicalSha256Fingerprint("media-usage-fields:v1:sha256:", {
|
|
1518
|
+
fingerprintVersion: 1,
|
|
1519
|
+
contentSourceSchemaVersion: CONTENT_SOURCE_SCHEMA_VERSION,
|
|
1520
|
+
extractionFields: discovery.extractionFields.map((field) => ({
|
|
1521
|
+
slug: field.slug,
|
|
1522
|
+
type: field.type,
|
|
1523
|
+
...field.type === "repeater" ? { subFields: (field.validation?.subFields ?? []).map((subField) => ({
|
|
1524
|
+
slug: subField.slug,
|
|
1525
|
+
type: subField.type
|
|
1526
|
+
})).toSorted(compareFieldIdentity) } : {}
|
|
1527
|
+
})).toSorted(compareFieldIdentity),
|
|
1528
|
+
displayFieldSlugs: discovery.displayFieldSlugs.toSorted(compareStrings)
|
|
1529
|
+
})).fingerprint;
|
|
1530
|
+
}
|
|
1531
|
+
var MediaUsageFieldDiscoveryError = class extends Error {
|
|
1532
|
+
constructor(message, code) {
|
|
1533
|
+
super(message);
|
|
1534
|
+
this.code = code;
|
|
1535
|
+
this.name = "MediaUsageFieldDiscoveryError";
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
const DISPLAY_FIELD_SLUGS = ["title", "name"];
|
|
1539
|
+
const SUPPORTED_TOP_LEVEL_TYPES = [
|
|
1540
|
+
"file",
|
|
1541
|
+
"image",
|
|
1542
|
+
"portableText"
|
|
1543
|
+
];
|
|
1544
|
+
async function loadContentMediaUsageFields(db, collectionSlug, collectionId) {
|
|
1545
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
1546
|
+
let query = db.selectFrom("_emdash_fields").innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id").select([
|
|
1547
|
+
"_emdash_fields.slug",
|
|
1548
|
+
"_emdash_fields.type",
|
|
1549
|
+
"_emdash_fields.validation"
|
|
1550
|
+
]).where("_emdash_collections.slug", "=", collectionSlug);
|
|
1551
|
+
if (collectionId !== void 0) query = query.where("_emdash_collections.id", "=", collectionId);
|
|
1552
|
+
const rows = await query.execute();
|
|
1553
|
+
const extractionFields = [];
|
|
1554
|
+
const rowBySlug = /* @__PURE__ */ new Map();
|
|
1555
|
+
for (const row of rows) {
|
|
1556
|
+
rowBySlug.set(row.slug, row);
|
|
1557
|
+
if (isSupportedTopLevelType(row.type)) {
|
|
1558
|
+
validateIdentifier(row.slug, "media usage field slug");
|
|
1559
|
+
extractionFields.push({
|
|
1560
|
+
slug: row.slug,
|
|
1561
|
+
type: row.type
|
|
1562
|
+
});
|
|
1563
|
+
continue;
|
|
1564
|
+
}
|
|
1565
|
+
if (row.type === "repeater") {
|
|
1566
|
+
validateIdentifier(row.slug, "media usage field slug");
|
|
1567
|
+
const subFields = normalizeRepeaterImageSubFields(row.validation);
|
|
1568
|
+
if (subFields.length > 0) extractionFields.push({
|
|
1569
|
+
slug: row.slug,
|
|
1570
|
+
type: "repeater",
|
|
1571
|
+
validation: { subFields }
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
extractionFields.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
1576
|
+
return {
|
|
1577
|
+
extractionFields,
|
|
1578
|
+
displayFieldSlugs: DISPLAY_FIELD_SLUGS.filter((slug) => {
|
|
1579
|
+
if (!rowBySlug.has(slug)) return false;
|
|
1580
|
+
validateIdentifier(slug, "media usage display field slug");
|
|
1581
|
+
return true;
|
|
1582
|
+
})
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
function normalizeRepeaterImageSubFields(rawValidation) {
|
|
1586
|
+
const validation = parseValidation(rawValidation);
|
|
1587
|
+
if (!isRecord$2(validation) || !Array.isArray(validation.subFields)) return [];
|
|
1588
|
+
const subFields = [];
|
|
1589
|
+
for (const subField of validation.subFields) {
|
|
1590
|
+
if (!isRecord$2(subField) || subField.type !== "image") continue;
|
|
1591
|
+
if (typeof subField.slug !== "string") continue;
|
|
1592
|
+
validateIdentifier(subField.slug, "media usage repeater sub-field slug");
|
|
1593
|
+
subFields.push({
|
|
1594
|
+
slug: subField.slug,
|
|
1595
|
+
type: "image"
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
return subFields.toSorted((a, b) => a.slug.localeCompare(b.slug));
|
|
1599
|
+
}
|
|
1600
|
+
function parseValidation(rawValidation) {
|
|
1601
|
+
if (!rawValidation) return null;
|
|
1602
|
+
try {
|
|
1603
|
+
return JSON.parse(rawValidation);
|
|
1604
|
+
} catch {
|
|
1605
|
+
throw new MediaUsageFieldDiscoveryError("Repeater field validation must be valid JSON before media usage can be discovered", "INVALID_REPEATER_VALIDATION");
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
function isSupportedTopLevelType(value) {
|
|
1609
|
+
return SUPPORTED_TOP_LEVEL_TYPES.includes(value);
|
|
1610
|
+
}
|
|
1611
|
+
function isRecord$2(value) {
|
|
1612
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1613
|
+
}
|
|
1614
|
+
function compareFieldIdentity(a, b) {
|
|
1615
|
+
return compareStrings(a.slug, b.slug);
|
|
1616
|
+
}
|
|
1617
|
+
function compareStrings(a, b) {
|
|
1618
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/media/usage/extractor.ts
|
|
1623
|
+
const URL_LIKE_RE = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i;
|
|
1624
|
+
function extractMediaUsageOccurrences({ fields, data }) {
|
|
1625
|
+
const occurrences = [];
|
|
1626
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1627
|
+
for (const field of fields) {
|
|
1628
|
+
const value = data[field.slug];
|
|
1629
|
+
if (field.type === "image") {
|
|
1630
|
+
addOccurrence(occurrences, seen, {
|
|
1631
|
+
fieldSlug: field.slug,
|
|
1632
|
+
fieldPath: field.slug,
|
|
1633
|
+
referenceType: "image_field",
|
|
1634
|
+
value,
|
|
1635
|
+
fallbackKind: "image"
|
|
1636
|
+
});
|
|
1637
|
+
continue;
|
|
1638
|
+
}
|
|
1639
|
+
if (field.type === "file") {
|
|
1640
|
+
addOccurrence(occurrences, seen, {
|
|
1641
|
+
fieldSlug: field.slug,
|
|
1642
|
+
fieldPath: field.slug,
|
|
1643
|
+
referenceType: "file_field",
|
|
1644
|
+
value,
|
|
1645
|
+
fallbackKind: null
|
|
1646
|
+
});
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (field.type === "repeater") {
|
|
1650
|
+
extractRepeaterOccurrences(occurrences, seen, field.slug, value, field.validation?.subFields);
|
|
1651
|
+
continue;
|
|
1652
|
+
}
|
|
1653
|
+
if (field.type === "portableText") extractPortableTextOccurrences(occurrences, seen, field.slug, value);
|
|
1654
|
+
}
|
|
1655
|
+
return occurrences;
|
|
1656
|
+
}
|
|
1657
|
+
function extractRepeaterOccurrences(occurrences, seen, fieldSlug, value, subFields) {
|
|
1658
|
+
if (!Array.isArray(value) || !Array.isArray(subFields)) return;
|
|
1659
|
+
for (const [itemIndex, item] of value.entries()) {
|
|
1660
|
+
if (!isRecord$1(item)) continue;
|
|
1661
|
+
for (const subField of subFields) {
|
|
1662
|
+
if (subField.type !== "image") continue;
|
|
1663
|
+
addOccurrence(occurrences, seen, {
|
|
1664
|
+
fieldSlug,
|
|
1665
|
+
fieldPath: `${fieldSlug}[${itemIndex}].${subField.slug}`,
|
|
1666
|
+
referenceType: "image_field",
|
|
1667
|
+
value: item[subField.slug],
|
|
1668
|
+
fallbackKind: "image"
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function extractPortableTextOccurrences(occurrences, seen, fieldSlug, value) {
|
|
1674
|
+
if (!Array.isArray(value)) return;
|
|
1675
|
+
for (const [blockIndex, block] of value.entries()) {
|
|
1676
|
+
if (!isRecord$1(block) || block._type !== "image" || !isRecord$1(block.asset)) continue;
|
|
1677
|
+
const provider = normalizeProvider(block.asset.provider);
|
|
1678
|
+
const ref = readPortableTextAssetRef(block.asset, provider);
|
|
1679
|
+
if (!ref) continue;
|
|
1680
|
+
addRefOccurrence(occurrences, seen, {
|
|
1681
|
+
fieldSlug,
|
|
1682
|
+
fieldPath: `${fieldSlug}[${blockIndex}].asset.${ref.key}`,
|
|
1683
|
+
referenceType: "portable_text_image",
|
|
1684
|
+
ref: buildMediaRef({
|
|
1685
|
+
id: ref.id,
|
|
1686
|
+
provider,
|
|
1687
|
+
mimeType: normalizeMimeValue(block.asset.mimeType),
|
|
1688
|
+
fallbackKind: "image"
|
|
1689
|
+
})
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
function addOccurrence(occurrences, seen, input) {
|
|
1694
|
+
const ref = readMediaRef(input.value, input.fallbackKind);
|
|
1695
|
+
if (!ref) return;
|
|
1696
|
+
addRefOccurrence(occurrences, seen, {
|
|
1697
|
+
fieldSlug: input.fieldSlug,
|
|
1698
|
+
fieldPath: input.fieldPath,
|
|
1699
|
+
referenceType: input.referenceType,
|
|
1700
|
+
ref
|
|
1701
|
+
});
|
|
1702
|
+
}
|
|
1703
|
+
function addRefOccurrence(occurrences, seen, input) {
|
|
1704
|
+
if (!input.ref) return;
|
|
1705
|
+
const occurrence = {
|
|
1706
|
+
fieldSlug: input.fieldSlug,
|
|
1707
|
+
fieldPath: input.fieldPath,
|
|
1708
|
+
occurrenceIndex: 0,
|
|
1709
|
+
referenceType: input.referenceType,
|
|
1710
|
+
mediaId: input.ref.mediaId,
|
|
1711
|
+
provider: input.ref.provider,
|
|
1712
|
+
providerAssetId: input.ref.providerAssetId,
|
|
1713
|
+
mediaKind: input.ref.mediaKind,
|
|
1714
|
+
mimeType: input.ref.mimeType
|
|
1715
|
+
};
|
|
1716
|
+
const key = [
|
|
1717
|
+
occurrence.fieldSlug,
|
|
1718
|
+
occurrence.fieldPath,
|
|
1719
|
+
occurrence.occurrenceIndex,
|
|
1720
|
+
occurrence.referenceType,
|
|
1721
|
+
occurrence.provider,
|
|
1722
|
+
occurrence.providerAssetId,
|
|
1723
|
+
occurrence.mediaId ?? ""
|
|
1724
|
+
].join("\0");
|
|
1725
|
+
if (seen.has(key)) return;
|
|
1726
|
+
seen.add(key);
|
|
1727
|
+
occurrences.push(occurrence);
|
|
1728
|
+
}
|
|
1729
|
+
function readMediaRef(value, fallbackKind) {
|
|
1730
|
+
if (typeof value === "string") {
|
|
1731
|
+
const id = normalizeLocalMediaId(value);
|
|
1732
|
+
return id ? buildMediaRef({
|
|
1733
|
+
id,
|
|
1734
|
+
provider: "local",
|
|
1735
|
+
mimeType: null,
|
|
1736
|
+
fallbackKind
|
|
1737
|
+
}) : null;
|
|
1738
|
+
}
|
|
1739
|
+
if (!isRecord$1(value)) return null;
|
|
1740
|
+
const provider = normalizeProvider(value.provider);
|
|
1741
|
+
const id = provider === "local" ? normalizeLocalMediaId(value.id) : normalizeStableId(value.id);
|
|
1742
|
+
if (!id) return null;
|
|
1743
|
+
return buildMediaRef({
|
|
1744
|
+
id,
|
|
1745
|
+
provider,
|
|
1746
|
+
mimeType: normalizeMimeValue(value.mimeType),
|
|
1747
|
+
fallbackKind
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
function buildMediaRef(input) {
|
|
1751
|
+
const provider = normalizeProvider(input.provider);
|
|
1752
|
+
if (provider === "external") return null;
|
|
1753
|
+
return {
|
|
1754
|
+
mediaId: provider === "local" ? input.id : null,
|
|
1755
|
+
provider,
|
|
1756
|
+
providerAssetId: input.id,
|
|
1757
|
+
mediaKind: mediaKindFromMime(input.mimeType) ?? input.fallbackKind,
|
|
1758
|
+
mimeType: input.mimeType
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
function readPortableTextAssetRef(asset, provider) {
|
|
1762
|
+
const normalizeId = provider === "local" ? normalizeLocalMediaId : normalizeStableId;
|
|
1763
|
+
const ref = normalizeId(asset._ref);
|
|
1764
|
+
if (ref) return {
|
|
1765
|
+
key: "_ref",
|
|
1766
|
+
id: ref
|
|
1767
|
+
};
|
|
1768
|
+
const id = normalizeId(asset.id);
|
|
1769
|
+
if (id) return {
|
|
1770
|
+
key: "id",
|
|
1771
|
+
id
|
|
1772
|
+
};
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
function normalizeProvider(value) {
|
|
1776
|
+
return readString$1(value)?.trim() || "local";
|
|
1777
|
+
}
|
|
1778
|
+
function normalizeLocalMediaId(value) {
|
|
1779
|
+
const id = normalizeStableId(value);
|
|
1780
|
+
if (!id) return null;
|
|
1781
|
+
return id.includes("/") ? null : id;
|
|
1782
|
+
}
|
|
1783
|
+
function normalizeStableId(value) {
|
|
1784
|
+
if (typeof value !== "string") return null;
|
|
1785
|
+
const trimmed = value.trim();
|
|
1786
|
+
if (!trimmed) return null;
|
|
1787
|
+
if (URL_LIKE_RE.test(trimmed)) return null;
|
|
1788
|
+
if (trimmed.startsWith(INTERNAL_MEDIA_PREFIX)) return null;
|
|
1789
|
+
return trimmed;
|
|
1790
|
+
}
|
|
1791
|
+
function normalizeMimeValue(value) {
|
|
1792
|
+
if (typeof value !== "string") return null;
|
|
1793
|
+
const normalized = normalizeMime(value);
|
|
1794
|
+
return normalized.includes("/") ? normalized : null;
|
|
1795
|
+
}
|
|
1796
|
+
function mediaKindFromMime(mimeType) {
|
|
1797
|
+
if (!mimeType) return null;
|
|
1798
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
1799
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
1800
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
1801
|
+
if (mimeType.startsWith("font/") || mimeType.startsWith("application/font-")) return "font";
|
|
1802
|
+
if (mimeType.startsWith("text/")) return "text";
|
|
1803
|
+
if (isDocumentMime(mimeType)) return "document";
|
|
1804
|
+
if (isArchiveMime(mimeType)) return "archive";
|
|
1805
|
+
return "other";
|
|
1806
|
+
}
|
|
1807
|
+
function isDocumentMime(mimeType) {
|
|
1808
|
+
return mimeType === "application/pdf" || mimeType === "application/msword" || mimeType === "application/rtf" || mimeType === "application/vnd.ms-excel" || mimeType === "application/vnd.ms-powerpoint" || mimeType.startsWith("application/vnd.openxmlformats-officedocument.");
|
|
1809
|
+
}
|
|
1810
|
+
function isArchiveMime(mimeType) {
|
|
1811
|
+
return mimeType === "application/zip" || mimeType === "application/gzip" || mimeType === "application/x-tar" || mimeType === "application/x-7z-compressed" || mimeType === "application/x-rar-compressed" || mimeType === "application/vnd.rar";
|
|
1812
|
+
}
|
|
1813
|
+
function readString$1(value) {
|
|
1814
|
+
return typeof value === "string" ? value : null;
|
|
1815
|
+
}
|
|
1816
|
+
function isRecord$1(value) {
|
|
1817
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
//#endregion
|
|
1821
|
+
//#region src/media/usage/source-key.ts
|
|
1822
|
+
const MEDIA_USAGE_CONTENT_SOURCE_VARIANTS = ["columns", "draft_overlay"];
|
|
1823
|
+
function buildContentMediaUsageSourceKey(input) {
|
|
1824
|
+
if (input.collectionId) return `content:${input.collectionId}:${input.contentId}:${input.sourceVariant}`;
|
|
1825
|
+
return `content:${input.collectionSlug}:${input.contentId}:${input.sourceVariant}`;
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
//#endregion
|
|
1829
|
+
//#region src/media/usage/content-snapshots.ts
|
|
1830
|
+
const CONTENT_COLLECTION_ID_RESULT = "__emdash_media_usage_collection_id";
|
|
1831
|
+
const CONTENT_SYSTEM_COLUMNS = [
|
|
1832
|
+
"id",
|
|
1833
|
+
"slug",
|
|
1834
|
+
"status",
|
|
1835
|
+
"created_at",
|
|
1836
|
+
"updated_at",
|
|
1837
|
+
"published_at",
|
|
1838
|
+
"scheduled_at",
|
|
1839
|
+
"deleted_at",
|
|
1840
|
+
"version",
|
|
1841
|
+
"live_revision_id",
|
|
1842
|
+
"draft_revision_id",
|
|
1843
|
+
"locale",
|
|
1844
|
+
"translation_group"
|
|
1845
|
+
];
|
|
1846
|
+
async function loadContentMediaUsageSnapshots(db, collectionSlug, contentId, fieldDiscovery, options = {}) {
|
|
1847
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
1848
|
+
if (options.identityVersion !== void 0 && !options.collectionId) throw new Error("Canonical media usage snapshots require a collection identity");
|
|
1849
|
+
const discovery = fieldDiscovery ?? await loadContentMediaUsageFields(db, collectionSlug);
|
|
1850
|
+
const row = await loadContentRow(db, collectionSlug, contentId, [...discovery.extractionFields.map((field) => field.slug), ...discovery.displayFieldSlugs], options.collectionId);
|
|
1851
|
+
if (!row) return {
|
|
1852
|
+
success: false,
|
|
1853
|
+
error: "CONTENT_NOT_FOUND"
|
|
1854
|
+
};
|
|
1855
|
+
const collectionId = readString(row[CONTENT_COLLECTION_ID_RESULT]);
|
|
1856
|
+
if (!collectionId) throw new Error("Media usage snapshot query did not return a collection identity");
|
|
1857
|
+
const columnsData = projectData(row, discovery.extractionFields.map((field) => field.slug));
|
|
1858
|
+
const displayData = projectRawData(row, discovery.displayFieldSlugs);
|
|
1859
|
+
const occurrences = extractMediaUsageOccurrences({
|
|
1860
|
+
fields: discovery.extractionFields,
|
|
1861
|
+
data: columnsData
|
|
1862
|
+
});
|
|
1863
|
+
const columnsRevisionId = readNullableString(row.live_revision_id);
|
|
1864
|
+
const columnsSource = buildContentSource({
|
|
1865
|
+
collectionId: options.collectionId,
|
|
1866
|
+
collectionSlug,
|
|
1867
|
+
identityVersion: options.identityVersion,
|
|
1868
|
+
row,
|
|
1869
|
+
displayData,
|
|
1870
|
+
sourceVariant: "columns",
|
|
1871
|
+
revisionId: columnsRevisionId
|
|
1872
|
+
});
|
|
1873
|
+
const columnsProjection = await buildMediaUsageProjectionFingerprint({
|
|
1874
|
+
collectionId,
|
|
1875
|
+
source: columnsSource,
|
|
1876
|
+
occurrences,
|
|
1877
|
+
extractionFields: discovery.extractionFields
|
|
1878
|
+
});
|
|
1879
|
+
columnsSource.sourceFingerprint = columnsProjection.fingerprint;
|
|
1880
|
+
const snapshots = [{
|
|
1881
|
+
source: columnsSource,
|
|
1882
|
+
occurrences,
|
|
1883
|
+
fields: discovery.extractionFields,
|
|
1884
|
+
projectionByteLength: columnsProjection.byteLength
|
|
1885
|
+
}];
|
|
1886
|
+
const draftRevisionId = readNullableString(row.draft_revision_id);
|
|
1887
|
+
if (draftRevisionId) {
|
|
1888
|
+
const attemptedDraftSource = buildContentSource({
|
|
1889
|
+
collectionId: options.collectionId,
|
|
1890
|
+
collectionSlug,
|
|
1891
|
+
identityVersion: options.identityVersion,
|
|
1892
|
+
row,
|
|
1893
|
+
displayData,
|
|
1894
|
+
sourceVariant: "draft_overlay",
|
|
1895
|
+
revisionId: draftRevisionId
|
|
1896
|
+
});
|
|
1897
|
+
const revisionResult = await loadRevisionRow(db, draftRevisionId);
|
|
1898
|
+
if (!revisionResult) return {
|
|
1899
|
+
success: false,
|
|
1900
|
+
error: "DRAFT_REVISION_NOT_FOUND",
|
|
1901
|
+
source: attemptedDraftSource,
|
|
1902
|
+
snapshots
|
|
1903
|
+
};
|
|
1904
|
+
if (!revisionResult.success) return {
|
|
1905
|
+
success: false,
|
|
1906
|
+
error: "DRAFT_REVISION_INVALID",
|
|
1907
|
+
source: attemptedDraftSource,
|
|
1908
|
+
snapshots
|
|
1909
|
+
};
|
|
1910
|
+
const revision = revisionResult.revision;
|
|
1911
|
+
if (revision.collection !== collectionSlug || revision.entryId !== row.id) return {
|
|
1912
|
+
success: false,
|
|
1913
|
+
error: "DRAFT_REVISION_MISMATCH",
|
|
1914
|
+
source: attemptedDraftSource,
|
|
1915
|
+
snapshots
|
|
1916
|
+
};
|
|
1917
|
+
const revisionData = stripRevisionMetadata(revision.data);
|
|
1918
|
+
const draftOverlayData = {
|
|
1919
|
+
...columnsData,
|
|
1920
|
+
...revisionData
|
|
1921
|
+
};
|
|
1922
|
+
const draftDisplayData = {
|
|
1923
|
+
...displayData,
|
|
1924
|
+
...projectPresentData(revisionData, discovery.displayFieldSlugs)
|
|
1925
|
+
};
|
|
1926
|
+
const draftContentSlug = readNullableString(revision.data._slug) ?? readNullableString(row.slug);
|
|
1927
|
+
const draftOccurrences = extractMediaUsageOccurrences({
|
|
1928
|
+
fields: discovery.extractionFields,
|
|
1929
|
+
data: draftOverlayData
|
|
1930
|
+
});
|
|
1931
|
+
const draftSource = buildContentSource({
|
|
1932
|
+
collectionId: options.collectionId,
|
|
1933
|
+
collectionSlug,
|
|
1934
|
+
identityVersion: options.identityVersion,
|
|
1935
|
+
row,
|
|
1936
|
+
displayData: draftDisplayData,
|
|
1937
|
+
sourceVariant: "draft_overlay",
|
|
1938
|
+
revisionId: draftRevisionId,
|
|
1939
|
+
contentSlug: draftContentSlug
|
|
1940
|
+
});
|
|
1941
|
+
const draftProjection = await buildMediaUsageProjectionFingerprint({
|
|
1942
|
+
collectionId,
|
|
1943
|
+
source: draftSource,
|
|
1944
|
+
occurrences: draftOccurrences,
|
|
1945
|
+
extractionFields: discovery.extractionFields
|
|
1946
|
+
});
|
|
1947
|
+
draftSource.sourceFingerprint = draftProjection.fingerprint;
|
|
1948
|
+
snapshots.push({
|
|
1949
|
+
source: draftSource,
|
|
1950
|
+
occurrences: draftOccurrences,
|
|
1951
|
+
fields: discovery.extractionFields,
|
|
1952
|
+
projectionByteLength: draftProjection.byteLength
|
|
1953
|
+
});
|
|
1954
|
+
}
|
|
1955
|
+
return {
|
|
1956
|
+
success: true,
|
|
1957
|
+
snapshots
|
|
1958
|
+
};
|
|
1959
|
+
}
|
|
1960
|
+
async function loadContentRow(db, collectionSlug, contentId, fieldSlugs, expectedCollectionId) {
|
|
1961
|
+
const tableName = getContentTableName(collectionSlug);
|
|
1962
|
+
const columnRefs = uniqueColumns([...CONTENT_SYSTEM_COLUMNS, ...fieldSlugs]).map((column) => sql.ref(`content.${column}`));
|
|
1963
|
+
return (await sql`
|
|
1964
|
+
SELECT
|
|
1965
|
+
${sql.join(columnRefs, sql`, `)},
|
|
1966
|
+
collection.id AS __emdash_media_usage_collection_id
|
|
1967
|
+
FROM ${sql.ref(tableName)} AS content
|
|
1968
|
+
INNER JOIN _emdash_collections AS collection
|
|
1969
|
+
ON collection.slug = ${collectionSlug}
|
|
1970
|
+
${expectedCollectionId ? sql`AND collection.id = ${expectedCollectionId}` : sql``}
|
|
1971
|
+
WHERE content.id = ${contentId}
|
|
1972
|
+
LIMIT 1
|
|
1973
|
+
`.execute(db)).rows[0] ?? null;
|
|
1974
|
+
}
|
|
1975
|
+
async function loadRevisionRow(db, revisionId) {
|
|
1976
|
+
const row = await db.selectFrom("revisions").select([
|
|
1977
|
+
"id",
|
|
1978
|
+
"collection",
|
|
1979
|
+
"entry_id",
|
|
1980
|
+
"data"
|
|
1981
|
+
]).where("id", "=", revisionId).executeTakeFirst();
|
|
1982
|
+
if (!row) return null;
|
|
1983
|
+
const data = parseRevisionData(row.data);
|
|
1984
|
+
if (!data) return { success: false };
|
|
1985
|
+
return {
|
|
1986
|
+
success: true,
|
|
1987
|
+
revision: {
|
|
1988
|
+
id: row.id,
|
|
1989
|
+
collection: row.collection,
|
|
1990
|
+
entryId: row.entry_id,
|
|
1991
|
+
data
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function buildContentSource(input) {
|
|
1996
|
+
const { collectionId, collectionSlug, identityVersion, row, displayData, sourceVariant, revisionId } = input;
|
|
1997
|
+
const contentId = readString(row.id) ?? "";
|
|
1998
|
+
const contentSlug = input.contentSlug ?? readNullableString(row.slug);
|
|
1999
|
+
return {
|
|
2000
|
+
sourceKey: buildContentMediaUsageSourceKey({
|
|
2001
|
+
collectionId,
|
|
2002
|
+
collectionSlug,
|
|
2003
|
+
contentId,
|
|
2004
|
+
sourceVariant
|
|
2005
|
+
}),
|
|
2006
|
+
sourceType: "content",
|
|
2007
|
+
collectionId,
|
|
2008
|
+
collectionSlug,
|
|
2009
|
+
contentId,
|
|
2010
|
+
sourceVariant,
|
|
2011
|
+
locale: readNullableString(row.locale),
|
|
2012
|
+
translationGroup: readNullableString(row.translation_group),
|
|
2013
|
+
contentSlug,
|
|
2014
|
+
contentTitle: deriveContentTitle(displayData, contentSlug, contentId),
|
|
2015
|
+
contentStatus: readNullableString(row.status),
|
|
2016
|
+
contentScheduledAt: readNullableString(row.scheduled_at),
|
|
2017
|
+
contentDeletedAt: readNullableString(row.deleted_at),
|
|
2018
|
+
revisionId,
|
|
2019
|
+
schemaVersion: CONTENT_SOURCE_SCHEMA_VERSION,
|
|
2020
|
+
sourceUpdatedAt: readNullableString(row.updated_at),
|
|
2021
|
+
sourceVersion: readNumber(row.version),
|
|
2022
|
+
identityVersion
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
function projectData(row, fieldSlugs) {
|
|
2026
|
+
const data = {};
|
|
2027
|
+
for (const fieldSlug of fieldSlugs) data[fieldSlug] = deserializeValue(row[fieldSlug] ?? null);
|
|
2028
|
+
return data;
|
|
2029
|
+
}
|
|
2030
|
+
function projectRawData(row, fieldSlugs) {
|
|
2031
|
+
const data = {};
|
|
2032
|
+
for (const fieldSlug of fieldSlugs) data[fieldSlug] = row[fieldSlug] ?? null;
|
|
2033
|
+
return data;
|
|
2034
|
+
}
|
|
2035
|
+
function projectPresentData(row, fieldSlugs) {
|
|
2036
|
+
const data = {};
|
|
2037
|
+
for (const fieldSlug of fieldSlugs) if (Object.hasOwn(row, fieldSlug)) data[fieldSlug] = row[fieldSlug];
|
|
2038
|
+
return data;
|
|
2039
|
+
}
|
|
2040
|
+
function uniqueColumns(columns) {
|
|
2041
|
+
const unique = [...new Set(columns)];
|
|
2042
|
+
for (const column of unique) validateIdentifier(column, "content media usage column");
|
|
2043
|
+
return unique;
|
|
2044
|
+
}
|
|
2045
|
+
function getContentTableName(collectionSlug) {
|
|
2046
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2047
|
+
return `ec_${collectionSlug}`;
|
|
2048
|
+
}
|
|
2049
|
+
function deserializeValue(value) {
|
|
2050
|
+
if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) try {
|
|
2051
|
+
return JSON.parse(value);
|
|
2052
|
+
} catch {
|
|
2053
|
+
return value;
|
|
2054
|
+
}
|
|
2055
|
+
return value;
|
|
2056
|
+
}
|
|
2057
|
+
function parseRevisionData(value) {
|
|
2058
|
+
if (typeof value === "string") try {
|
|
2059
|
+
const parsed = JSON.parse(value);
|
|
2060
|
+
return isRecord(parsed) ? parsed : null;
|
|
2061
|
+
} catch {
|
|
2062
|
+
return null;
|
|
2063
|
+
}
|
|
2064
|
+
return isRecord(value) ? value : null;
|
|
2065
|
+
}
|
|
2066
|
+
function stripRevisionMetadata(data) {
|
|
2067
|
+
const stripped = {};
|
|
2068
|
+
for (const [key, value] of Object.entries(data)) if (!key.startsWith("_")) stripped[key] = value;
|
|
2069
|
+
return stripped;
|
|
2070
|
+
}
|
|
2071
|
+
function deriveContentTitle(displayData, contentSlug, contentId) {
|
|
2072
|
+
for (const fieldSlug of ["title", "name"]) {
|
|
2073
|
+
const value = displayData[fieldSlug];
|
|
2074
|
+
if (typeof value === "string" && value.trim()) return value;
|
|
2075
|
+
}
|
|
2076
|
+
return contentSlug ?? contentId;
|
|
2077
|
+
}
|
|
2078
|
+
function readString(value) {
|
|
2079
|
+
return typeof value === "string" ? value : null;
|
|
2080
|
+
}
|
|
2081
|
+
function readNullableString(value) {
|
|
2082
|
+
return value === null || value === void 0 ? null : readString(value);
|
|
2083
|
+
}
|
|
2084
|
+
function readNumber(value) {
|
|
2085
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
2086
|
+
if (typeof value === "bigint") return Number(value);
|
|
2087
|
+
if (typeof value === "string" && value) {
|
|
2088
|
+
const parsed = Number(value);
|
|
2089
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
2090
|
+
}
|
|
2091
|
+
return null;
|
|
2092
|
+
}
|
|
2093
|
+
function isRecord(value) {
|
|
2094
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
//#endregion
|
|
2098
|
+
//#region src/media/usage/content-refresh.ts
|
|
2099
|
+
const CONTENT_MEDIA_USAGE_ADAPTER_ID = "content-media";
|
|
2100
|
+
const CONTENT_MEDIA_USAGE_COLLECTION_SCOPE = "collection";
|
|
2101
|
+
const CONTENT_USAGE_LOCKS_KEY = Symbol.for("emdash.mediaUsage.contentLocks");
|
|
2102
|
+
const CONTENT_USAGE_COLLECTION_LOCKS_KEY = Symbol.for("emdash.mediaUsage.collectionLocks");
|
|
2103
|
+
const CONTENT_USAGE_REFRESH_MAX_ATTEMPTS = 2;
|
|
2104
|
+
const MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS = Object.freeze({
|
|
2105
|
+
maxOccurrenceMutationUnitsPerClaim: 12,
|
|
2106
|
+
maxProjectionMutationBytesPerClaim: 512 * 1024
|
|
2107
|
+
});
|
|
2108
|
+
const ZERO_RESULT = {
|
|
2109
|
+
success: true,
|
|
2110
|
+
refreshedSourceCount: 0,
|
|
2111
|
+
deletedSourceCount: 0,
|
|
2112
|
+
failedSourceCount: 0
|
|
2113
|
+
};
|
|
2114
|
+
function createContentMediaUsageAdmissionBudget() {
|
|
2115
|
+
return {
|
|
2116
|
+
remainingOccurrenceMutationUnits: MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxOccurrenceMutationUnitsPerClaim,
|
|
2117
|
+
remainingProjectionMutationBytes: MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxProjectionMutationBytesPerClaim,
|
|
2118
|
+
hasReservedMutation: false
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
async function planContentMediaUsageProjectionAdmission(repo, snapshots, observedSources, canonicalSourceKeys, budget) {
|
|
2122
|
+
const snapshotSourceKeys = new Set(snapshots.map((snapshot) => snapshot.source.sourceKey));
|
|
2123
|
+
const absentSources = canonicalSourceKeys.filter((sourceKey) => !snapshotSourceKeys.has(sourceKey)).map((sourceKey) => observedSources.get(sourceKey)).filter((source) => source !== void 0);
|
|
2124
|
+
let deletionOccurrenceUnits = 0;
|
|
2125
|
+
let deletionBytes = 0;
|
|
2126
|
+
for (const source of absentSources) {
|
|
2127
|
+
const measurement = await repo.measureSourceGenerationDeletion(source.sourceKey, source.currentGeneration, MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxOccurrenceMutationUnitsPerClaim);
|
|
2128
|
+
if (measurement.exceedsOccurrenceLimit) return budget.hasReservedMutation ? { outcome: "claim_budget_deferred" } : { outcome: "intrinsic_resource_limit" };
|
|
2129
|
+
deletionOccurrenceUnits += measurement.occurrenceCount;
|
|
2130
|
+
deletionBytes += storedMediaUsageSourceByteLength(source) + measurement.occurrenceBytes * 2;
|
|
2131
|
+
}
|
|
2132
|
+
const noOpSourceKeys = /* @__PURE__ */ new Set();
|
|
2133
|
+
let cost = projectionAdmissionCost(snapshots, noOpSourceKeys, deletionOccurrenceUnits, deletionBytes);
|
|
2134
|
+
if (exceedsProjectionAdmissionLimits(cost)) {
|
|
2135
|
+
for (const snapshot of snapshots) {
|
|
2136
|
+
const expectedSource = observedSources.get(snapshot.source.sourceKey);
|
|
2137
|
+
if (expectedSource && await repo.projectionMatchesExpectedSource(snapshot.source, expectedSource)) noOpSourceKeys.add(snapshot.source.sourceKey);
|
|
2138
|
+
}
|
|
2139
|
+
cost = projectionAdmissionCost(snapshots, noOpSourceKeys, deletionOccurrenceUnits, deletionBytes);
|
|
2140
|
+
}
|
|
2141
|
+
if (exceedsProjectionAdmissionLimits(cost)) return budget.hasReservedMutation ? { outcome: "claim_budget_deferred" } : { outcome: "intrinsic_resource_limit" };
|
|
2142
|
+
if (cost.occurrenceMutationUnits > budget.remainingOccurrenceMutationUnits || cost.projectionMutationBytes > budget.remainingProjectionMutationBytes) return { outcome: "claim_budget_deferred" };
|
|
2143
|
+
budget.remainingOccurrenceMutationUnits -= cost.occurrenceMutationUnits;
|
|
2144
|
+
budget.remainingProjectionMutationBytes -= cost.projectionMutationBytes;
|
|
2145
|
+
if (cost.occurrenceMutationUnits > 0 || cost.projectionMutationBytes > 0) budget.hasReservedMutation = true;
|
|
2146
|
+
return {
|
|
2147
|
+
outcome: "admitted",
|
|
2148
|
+
noOpSourceKeys,
|
|
2149
|
+
absentSources,
|
|
2150
|
+
...cost
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
function projectionAdmissionCost(snapshots, noOpSourceKeys, deletionOccurrenceUnits, deletionBytes) {
|
|
2154
|
+
return snapshots.reduce((cost, snapshot) => {
|
|
2155
|
+
if (noOpSourceKeys.has(snapshot.source.sourceKey)) return cost;
|
|
2156
|
+
cost.occurrenceMutationUnits += snapshot.occurrences.length;
|
|
2157
|
+
cost.projectionMutationBytes += snapshot.projectionByteLength;
|
|
2158
|
+
return cost;
|
|
2159
|
+
}, {
|
|
2160
|
+
occurrenceMutationUnits: deletionOccurrenceUnits,
|
|
2161
|
+
projectionMutationBytes: deletionBytes
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
function exceedsProjectionAdmissionLimits(cost) {
|
|
2165
|
+
return cost.occurrenceMutationUnits > MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxOccurrenceMutationUnitsPerClaim || cost.projectionMutationBytes > MEDIA_USAGE_PROJECTION_ADMISSION_LIMITS.maxProjectionMutationBytesPerClaim;
|
|
2166
|
+
}
|
|
2167
|
+
function storedMediaUsageSourceByteLength(source) {
|
|
2168
|
+
return new TextEncoder().encode(JSON.stringify(source)).byteLength;
|
|
2169
|
+
}
|
|
2170
|
+
async function refreshContentMediaUsage(db, collectionSlug, contentId) {
|
|
2171
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2172
|
+
return withContentUsageCollectionLock(collectionSlug, () => withContentUsageLock(collectionSlug, contentId, () => refreshContentMediaUsageUnlocked(db, collectionSlug, contentId, {})));
|
|
2173
|
+
}
|
|
2174
|
+
async function refreshContentMediaUsageForWork(db, collectionId, collectionSlug, contentId) {
|
|
2175
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2176
|
+
if (!collectionId) throw new Error("Durable media usage work requires a collection identity");
|
|
2177
|
+
return withContentUsageCollectionLock(collectionSlug, () => withContentUsageLock(collectionSlug, contentId, () => refreshContentMediaUsageUnlocked(db, collectionSlug, contentId, {
|
|
2178
|
+
collectionId,
|
|
2179
|
+
durableWork: true
|
|
2180
|
+
})));
|
|
2181
|
+
}
|
|
2182
|
+
async function refreshContentMediaUsageUnlocked(db, collectionSlug, contentId, options) {
|
|
2183
|
+
try {
|
|
2184
|
+
let conflictResult = null;
|
|
2185
|
+
if (options.durableWork) options.admissionBudget = createContentMediaUsageAdmissionBudget();
|
|
2186
|
+
for (let attempt = 0; attempt < CONTENT_USAGE_REFRESH_MAX_ATTEMPTS; attempt++) {
|
|
2187
|
+
const result = await refreshContentMediaUsageAttempt(db, collectionSlug, contentId, options);
|
|
2188
|
+
if (result.errorCode !== "CONTENT_USAGE_GENERATION_CONFLICT") return result;
|
|
2189
|
+
conflictResult = result;
|
|
2190
|
+
if (options.admissionBudget?.hasReservedMutation) break;
|
|
2191
|
+
}
|
|
2192
|
+
if (options.durableWork) return generationConflictResult({
|
|
2193
|
+
refreshedSourceCount: conflictResult?.refreshedSourceCount ?? 0,
|
|
2194
|
+
deletedSourceCount: conflictResult?.deletedSourceCount ?? 0
|
|
2195
|
+
});
|
|
2196
|
+
return markGenerationConflict(db, collectionSlug, {
|
|
2197
|
+
refreshedSourceCount: conflictResult?.refreshedSourceCount ?? 0,
|
|
2198
|
+
deletedSourceCount: conflictResult?.deletedSourceCount ?? 0
|
|
2199
|
+
});
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
console.error(`[media-usage] Failed to refresh ${collectionSlug}/${contentId}:`, error);
|
|
2202
|
+
if (!options.durableWork) await markContentMediaUsageCollectionStaleSafely(db, collectionSlug, "CONTENT_USAGE_REFRESH_ERROR");
|
|
2203
|
+
return {
|
|
2204
|
+
success: false,
|
|
2205
|
+
refreshedSourceCount: 0,
|
|
2206
|
+
deletedSourceCount: 0,
|
|
2207
|
+
failedSourceCount: 0,
|
|
2208
|
+
errorCode: "CONTENT_USAGE_REFRESH_ERROR"
|
|
2209
|
+
};
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
async function refreshContentMediaUsageAttempt(db, collectionSlug, contentId, options) {
|
|
2213
|
+
const repo = new MediaUsageRepository(db);
|
|
2214
|
+
const canonicalSourceKeys = contentSourceKeys(collectionSlug, contentId, options.collectionId);
|
|
2215
|
+
const observedSources = await repo.findSources(canonicalSourceKeys);
|
|
2216
|
+
const snapshotsResult = await loadContentMediaUsageSnapshots(db, collectionSlug, contentId, void 0, options.collectionId ? {
|
|
2217
|
+
collectionId: options.collectionId,
|
|
2218
|
+
identityVersion: 1
|
|
2219
|
+
} : void 0);
|
|
2220
|
+
if (!snapshotsResult.success) {
|
|
2221
|
+
if (snapshotsResult.error === "CONTENT_NOT_FOUND" && options.collectionId) {
|
|
2222
|
+
if (!await contentCollectionExists(db, collectionSlug, options.collectionId)) return generationConflictResult({
|
|
2223
|
+
refreshedSourceCount: 0,
|
|
2224
|
+
deletedSourceCount: 0
|
|
2225
|
+
});
|
|
2226
|
+
if (!options.admissionBudget) throw new Error("Durable media usage work requires an admission budget");
|
|
2227
|
+
const admission = await planContentMediaUsageProjectionAdmission(repo, [], observedSources, canonicalSourceKeys, options.admissionBudget);
|
|
2228
|
+
if (admission.outcome !== "admitted") return admissionFailureResult(admission.outcome);
|
|
2229
|
+
return deleteCanonicalContentSourcesIfAbsent(repo, admission.absentSources, collectionSlug, contentId);
|
|
2230
|
+
}
|
|
2231
|
+
if (snapshotsResult.error === "CONTENT_NOT_FOUND" && !await contentCollectionExists(db, collectionSlug)) {
|
|
2232
|
+
const deletedSourceCount = await repo.deleteContentSources(collectionSlug, contentId);
|
|
2233
|
+
return {
|
|
2234
|
+
...ZERO_RESULT,
|
|
2235
|
+
deletedSourceCount
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
return options.durableWork ? snapshotFailureResult(snapshotsResult) : markSnapshotFailure(db, collectionSlug, snapshotsResult);
|
|
2239
|
+
}
|
|
2240
|
+
if (!await contentCollectionExists(db, collectionSlug, options.collectionId)) {
|
|
2241
|
+
if (options.collectionId) return generationConflictResult({
|
|
2242
|
+
refreshedSourceCount: 0,
|
|
2243
|
+
deletedSourceCount: 0
|
|
2244
|
+
});
|
|
2245
|
+
const deletedSourceCount = await repo.deleteContentSources(collectionSlug, contentId);
|
|
2246
|
+
return {
|
|
2247
|
+
...ZERO_RESULT,
|
|
2248
|
+
deletedSourceCount
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
const admission = options.admissionBudget ? await planContentMediaUsageProjectionAdmission(repo, snapshotsResult.snapshots, observedSources, canonicalSourceKeys, options.admissionBudget) : null;
|
|
2252
|
+
if (admission && admission.outcome !== "admitted") return admissionFailureResult(admission.outcome);
|
|
2253
|
+
let refreshedSourceCount = 0;
|
|
2254
|
+
for (const snapshot of snapshotsResult.snapshots) {
|
|
2255
|
+
if (admission?.outcome === "admitted" && admission.noOpSourceKeys.has(snapshot.source.sourceKey)) {
|
|
2256
|
+
refreshedSourceCount++;
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
2259
|
+
const result = await repo.replaceSourceIfMatching(snapshot.source, snapshot.occurrences, observedSources.get(snapshot.source.sourceKey) ?? null);
|
|
2260
|
+
if (result.unchanged) {
|
|
2261
|
+
refreshedSourceCount++;
|
|
2262
|
+
continue;
|
|
2263
|
+
}
|
|
2264
|
+
if (!result.replaced) return generationConflictResult({
|
|
2265
|
+
refreshedSourceCount,
|
|
2266
|
+
deletedSourceCount: 0
|
|
2267
|
+
});
|
|
2268
|
+
refreshedSourceCount++;
|
|
2269
|
+
}
|
|
2270
|
+
if (!await contentCollectionExists(db, collectionSlug, options.collectionId)) {
|
|
2271
|
+
if (options.collectionId) return generationConflictResult({
|
|
2272
|
+
refreshedSourceCount,
|
|
2273
|
+
deletedSourceCount: 0
|
|
2274
|
+
});
|
|
2275
|
+
const deletedSourceCount = await repo.deleteContentSources(collectionSlug, contentId);
|
|
2276
|
+
return {
|
|
2277
|
+
...ZERO_RESULT,
|
|
2278
|
+
deletedSourceCount
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
const expectedSourceKeys = new Set(snapshotsResult.snapshots.map((snapshot) => snapshot.source.sourceKey));
|
|
2282
|
+
const absentSources = admission?.outcome === "admitted" ? admission.absentSources : canonicalSourceKeys.filter((sourceKey) => !expectedSourceKeys.has(sourceKey)).map((sourceKey) => observedSources.get(sourceKey)).filter((source) => source !== void 0);
|
|
2283
|
+
let deletedSourceCount = 0;
|
|
2284
|
+
for (const expectedSource of absentSources) {
|
|
2285
|
+
const result = await repo.deleteSourceIfMatching(expectedSource.sourceKey, expectedSource);
|
|
2286
|
+
if (result.deleted) {
|
|
2287
|
+
deletedSourceCount++;
|
|
2288
|
+
continue;
|
|
2289
|
+
}
|
|
2290
|
+
if (result.source) return generationConflictResult({
|
|
2291
|
+
refreshedSourceCount,
|
|
2292
|
+
deletedSourceCount
|
|
2293
|
+
});
|
|
2294
|
+
}
|
|
2295
|
+
return {
|
|
2296
|
+
success: true,
|
|
2297
|
+
refreshedSourceCount,
|
|
2298
|
+
deletedSourceCount,
|
|
2299
|
+
failedSourceCount: 0
|
|
2300
|
+
};
|
|
2301
|
+
}
|
|
2302
|
+
function contentSourceKeys(collectionSlug, contentId, collectionId) {
|
|
2303
|
+
return MEDIA_USAGE_CONTENT_SOURCE_VARIANTS.map((sourceVariant) => buildContentMediaUsageSourceKey({
|
|
2304
|
+
collectionId,
|
|
2305
|
+
collectionSlug,
|
|
2306
|
+
contentId,
|
|
2307
|
+
sourceVariant
|
|
2308
|
+
}));
|
|
2309
|
+
}
|
|
2310
|
+
function admissionFailureResult(outcome) {
|
|
2311
|
+
return {
|
|
2312
|
+
...generationConflictResult({
|
|
2313
|
+
refreshedSourceCount: 0,
|
|
2314
|
+
deletedSourceCount: 0
|
|
2315
|
+
}),
|
|
2316
|
+
errorCode: outcome === "intrinsic_resource_limit" ? "CONTENT_USAGE_RESOURCE_LIMIT" : "CONTENT_USAGE_GENERATION_CONFLICT"
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
async function markGenerationConflict(db, collectionSlug, counts) {
|
|
2320
|
+
await markContentMediaUsageCollectionStaleSafely(db, collectionSlug, "CONTENT_USAGE_GENERATION_CONFLICT");
|
|
2321
|
+
return {
|
|
2322
|
+
success: false,
|
|
2323
|
+
refreshedSourceCount: counts.refreshedSourceCount,
|
|
2324
|
+
deletedSourceCount: counts.deletedSourceCount,
|
|
2325
|
+
failedSourceCount: 0,
|
|
2326
|
+
errorCode: "CONTENT_USAGE_GENERATION_CONFLICT"
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
function generationConflictResult(counts) {
|
|
2330
|
+
return {
|
|
2331
|
+
success: false,
|
|
2332
|
+
refreshedSourceCount: counts.refreshedSourceCount,
|
|
2333
|
+
deletedSourceCount: counts.deletedSourceCount,
|
|
2334
|
+
failedSourceCount: 0,
|
|
2335
|
+
errorCode: "CONTENT_USAGE_GENERATION_CONFLICT"
|
|
2336
|
+
};
|
|
2337
|
+
}
|
|
2338
|
+
async function contentCollectionExists(db, collectionSlug, collectionId) {
|
|
2339
|
+
let query = db.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug);
|
|
2340
|
+
if (collectionId) query = query.where("id", "=", collectionId);
|
|
2341
|
+
return await query.executeTakeFirst() !== void 0;
|
|
2342
|
+
}
|
|
2343
|
+
async function deleteContentMediaUsage(db, collectionSlug, contentId) {
|
|
2344
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2345
|
+
return withContentUsageCollectionLock(collectionSlug, () => withContentUsageLock(collectionSlug, contentId, () => deleteContentMediaUsageUnlocked(db, collectionSlug, contentId)));
|
|
2346
|
+
}
|
|
2347
|
+
async function deleteContentMediaUsageUnlocked(db, collectionSlug, contentId) {
|
|
2348
|
+
try {
|
|
2349
|
+
const deletedSourceCount = await new MediaUsageRepository(db).deleteContentSources(collectionSlug, contentId);
|
|
2350
|
+
return {
|
|
2351
|
+
...ZERO_RESULT,
|
|
2352
|
+
deletedSourceCount
|
|
2353
|
+
};
|
|
2354
|
+
} catch (error) {
|
|
2355
|
+
console.error(`[media-usage] Failed to delete usage for ${collectionSlug}/${contentId}:`, error);
|
|
2356
|
+
await markContentMediaUsageCollectionStaleSafely(db, collectionSlug, "CONTENT_USAGE_DELETE_ERROR");
|
|
2357
|
+
return {
|
|
2358
|
+
success: false,
|
|
2359
|
+
refreshedSourceCount: 0,
|
|
2360
|
+
deletedSourceCount: 0,
|
|
2361
|
+
failedSourceCount: 0,
|
|
2362
|
+
errorCode: "CONTENT_USAGE_DELETE_ERROR"
|
|
2363
|
+
};
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
async function deleteContentMediaUsageCollection(db, collectionSlug) {
|
|
2367
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2368
|
+
return withContentUsageCollectionLock(collectionSlug, () => deleteContentMediaUsageCollectionUnlocked(db, collectionSlug));
|
|
2369
|
+
}
|
|
2370
|
+
async function deleteContentMediaUsageCollectionUnlocked(db, collectionSlug) {
|
|
2371
|
+
try {
|
|
2372
|
+
const repo = new MediaUsageRepository(db);
|
|
2373
|
+
const deletedSourceCount = await repo.deleteCollectionSources(collectionSlug);
|
|
2374
|
+
await repo.deleteIndexStatus({
|
|
2375
|
+
adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
|
|
2376
|
+
scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
|
|
2377
|
+
scopeKey: collectionSlug
|
|
2378
|
+
});
|
|
2379
|
+
return {
|
|
2380
|
+
...ZERO_RESULT,
|
|
2381
|
+
deletedSourceCount
|
|
2382
|
+
};
|
|
2383
|
+
} catch (error) {
|
|
2384
|
+
console.error(`[media-usage] Failed to delete usage for collection ${collectionSlug}:`, error);
|
|
2385
|
+
try {
|
|
2386
|
+
await new MediaUsageRepository(db).deleteIndexStatus({
|
|
2387
|
+
adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
|
|
2388
|
+
scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
|
|
2389
|
+
scopeKey: collectionSlug
|
|
2390
|
+
});
|
|
2391
|
+
} catch (statusError) {
|
|
2392
|
+
console.error(`[media-usage] Failed to clear usage status for deleted collection ${collectionSlug}:`, statusError);
|
|
2393
|
+
}
|
|
2394
|
+
return {
|
|
2395
|
+
success: false,
|
|
2396
|
+
refreshedSourceCount: 0,
|
|
2397
|
+
deletedSourceCount: 0,
|
|
2398
|
+
failedSourceCount: 0,
|
|
2399
|
+
errorCode: "CONTENT_USAGE_DELETE_ERROR"
|
|
2400
|
+
};
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
async function refreshContentMediaUsageAfterWrite(db, collectionSlug, contentId) {
|
|
2404
|
+
const result = await refreshContentMediaUsage(db, collectionSlug, contentId);
|
|
2405
|
+
if (!result.success) console.error(`[media-usage] Usage refresh for ${collectionSlug}/${contentId} finished with ${result.errorCode}`);
|
|
2406
|
+
}
|
|
2407
|
+
async function markContentMediaUsageCollectionStale(db, collectionSlug, lastErrorCode) {
|
|
2408
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2409
|
+
const repo = new MediaUsageRepository(db);
|
|
2410
|
+
const identity = {
|
|
2411
|
+
adapterId: CONTENT_MEDIA_USAGE_ADAPTER_ID,
|
|
2412
|
+
scopeType: CONTENT_MEDIA_USAGE_COLLECTION_SCOPE,
|
|
2413
|
+
scopeKey: collectionSlug
|
|
2414
|
+
};
|
|
2415
|
+
const existing = await repo.findIndexStatus(identity);
|
|
2416
|
+
await repo.upsertIndexStatus({
|
|
2417
|
+
...identity,
|
|
2418
|
+
status: "stale",
|
|
2419
|
+
schemaVersion: existing?.schemaVersion ?? CONTENT_SOURCE_SCHEMA_VERSION,
|
|
2420
|
+
startedAt: existing?.startedAt ?? null,
|
|
2421
|
+
completedAt: existing?.completedAt ?? null,
|
|
2422
|
+
cursor: existing?.cursor ?? null,
|
|
2423
|
+
indexedSourceCount: existing?.indexedSourceCount ?? 0,
|
|
2424
|
+
failedSourceCount: existing?.failedSourceCount ?? 0,
|
|
2425
|
+
lastErrorCode
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
async function invalidateContentMediaUsageSchemaChange(db, collectionSlug) {
|
|
2429
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2430
|
+
if (!await tableExists(db, "_emdash_media_usage_activation")) return false;
|
|
2431
|
+
if ((await db.selectFrom("_emdash_media_usage_activation").select("state").where("task_key", "=", "incremental_capture").executeTakeFirst())?.state !== "active") return false;
|
|
2432
|
+
if (!await new MediaUsageRepository(db).invalidateIndexStatusForSchemaChange(collectionSlug)) throw new Error(`Cannot invalidate media usage coverage for collection ${collectionSlug}`);
|
|
2433
|
+
return true;
|
|
2434
|
+
}
|
|
2435
|
+
async function findNonTranslatableSiblingContentIds(db, collectionSlug, updatedContentId, translationGroup, updatedData) {
|
|
2436
|
+
if (!isI18nEnabled() || !updatedData || !translationGroup) return [];
|
|
2437
|
+
validateIdentifier(collectionSlug, "collection slug");
|
|
2438
|
+
const collection = await db.selectFrom("_emdash_collections").select("id").where("slug", "=", collectionSlug).executeTakeFirst();
|
|
2439
|
+
if (!collection) return [];
|
|
2440
|
+
const touchedNonTranslatableSlugs = (await db.selectFrom("_emdash_fields").select("slug").where("collection_id", "=", collection.id).where("translatable", "=", 0).execute()).filter((field) => field.slug in updatedData).map((field) => field.slug);
|
|
2441
|
+
if (touchedNonTranslatableSlugs.length === 0) return [];
|
|
2442
|
+
const usageFields = await loadContentMediaUsageFields(db, collectionSlug);
|
|
2443
|
+
const usageRelevantSlugs = new Set([...usageFields.extractionFields.map((field) => field.slug), ...usageFields.displayFieldSlugs]);
|
|
2444
|
+
if (!touchedNonTranslatableSlugs.some((slug) => usageRelevantSlugs.has(slug))) return [];
|
|
2445
|
+
const tableName = `ec_${collectionSlug}`;
|
|
2446
|
+
return (await sql`
|
|
2447
|
+
SELECT id
|
|
2448
|
+
FROM ${sql.ref(tableName)}
|
|
2449
|
+
WHERE translation_group = ${translationGroup}
|
|
2450
|
+
AND id != ${updatedContentId}
|
|
2451
|
+
ORDER BY id ASC
|
|
2452
|
+
`.execute(db)).rows.map((row) => row.id);
|
|
2453
|
+
}
|
|
2454
|
+
async function markSnapshotFailure(db, collectionSlug, result) {
|
|
2455
|
+
const repo = new MediaUsageRepository(db);
|
|
2456
|
+
if (result.source) await repo.markSourceAttempted({
|
|
2457
|
+
...result.source,
|
|
2458
|
+
sourceCompleteness: "failed",
|
|
2459
|
+
lastErrorCode: result.error
|
|
2460
|
+
});
|
|
2461
|
+
await markContentMediaUsageCollectionStale(db, collectionSlug, result.error);
|
|
2462
|
+
return {
|
|
2463
|
+
success: false,
|
|
2464
|
+
refreshedSourceCount: 0,
|
|
2465
|
+
deletedSourceCount: 0,
|
|
2466
|
+
failedSourceCount: result.source ? 1 : 0,
|
|
2467
|
+
errorCode: result.error
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
function snapshotFailureResult(result) {
|
|
2471
|
+
return {
|
|
2472
|
+
success: false,
|
|
2473
|
+
refreshedSourceCount: 0,
|
|
2474
|
+
deletedSourceCount: 0,
|
|
2475
|
+
failedSourceCount: result.source ? 1 : 0,
|
|
2476
|
+
errorCode: result.error
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
async function deleteCanonicalContentSourcesIfAbsent(repo, observedSources, collectionSlug, contentId) {
|
|
2480
|
+
let deletedSourceCount = 0;
|
|
2481
|
+
for (const source of observedSources) {
|
|
2482
|
+
const result = await repo.deleteSourceIfMatchingContentAbsent(source.sourceKey, source, collectionSlug, contentId);
|
|
2483
|
+
if (result.deleted) {
|
|
2484
|
+
deletedSourceCount++;
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
if (result.contentPresent || result.source) return generationConflictResult({
|
|
2488
|
+
refreshedSourceCount: 0,
|
|
2489
|
+
deletedSourceCount
|
|
2490
|
+
});
|
|
2491
|
+
}
|
|
2492
|
+
return {
|
|
2493
|
+
...ZERO_RESULT,
|
|
2494
|
+
deletedSourceCount
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
async function markContentMediaUsageCollectionStaleSafely(db, collectionSlug, lastErrorCode) {
|
|
2498
|
+
try {
|
|
2499
|
+
await markContentMediaUsageCollectionStale(db, collectionSlug, lastErrorCode);
|
|
2500
|
+
return true;
|
|
2501
|
+
} catch (error) {
|
|
2502
|
+
console.error(`[media-usage] Failed to mark ${collectionSlug} stale:`, error);
|
|
2503
|
+
return false;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
async function withContentUsageLock(collectionSlug, contentId, fn) {
|
|
2507
|
+
const locks = getContentUsageLocks();
|
|
2508
|
+
const lockKey = `${collectionSlug}\0${contentId}`;
|
|
2509
|
+
const previous = locks.get(lockKey) ?? Promise.resolve();
|
|
2510
|
+
let releaseCurrent;
|
|
2511
|
+
const current = new Promise((resolve) => {
|
|
2512
|
+
releaseCurrent = resolve;
|
|
2513
|
+
});
|
|
2514
|
+
const next = previous.catch(() => {}).then(() => current);
|
|
2515
|
+
locks.set(lockKey, next);
|
|
2516
|
+
try {
|
|
2517
|
+
await previous.catch(() => {});
|
|
2518
|
+
return await fn();
|
|
2519
|
+
} finally {
|
|
2520
|
+
releaseCurrent();
|
|
2521
|
+
if (locks.get(lockKey) === next) locks.delete(lockKey);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
async function withContentUsageCollectionLock(collectionSlug, fn) {
|
|
2525
|
+
const locks = getContentUsageCollectionLocks();
|
|
2526
|
+
const previous = locks.get(collectionSlug) ?? Promise.resolve();
|
|
2527
|
+
let releaseCurrent;
|
|
2528
|
+
const current = new Promise((resolve) => {
|
|
2529
|
+
releaseCurrent = resolve;
|
|
2530
|
+
});
|
|
2531
|
+
const next = previous.catch(() => {}).then(() => current);
|
|
2532
|
+
locks.set(collectionSlug, next);
|
|
2533
|
+
try {
|
|
2534
|
+
await previous.catch(() => {});
|
|
2535
|
+
return await fn();
|
|
2536
|
+
} finally {
|
|
2537
|
+
releaseCurrent();
|
|
2538
|
+
if (locks.get(collectionSlug) === next) locks.delete(collectionSlug);
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
function getContentUsageLocks() {
|
|
2542
|
+
const global = globalThis;
|
|
2543
|
+
const existing = global[CONTENT_USAGE_LOCKS_KEY];
|
|
2544
|
+
if (existing instanceof Map) return existing;
|
|
2545
|
+
const locks = /* @__PURE__ */ new Map();
|
|
2546
|
+
global[CONTENT_USAGE_LOCKS_KEY] = locks;
|
|
2547
|
+
return locks;
|
|
2548
|
+
}
|
|
2549
|
+
function getContentUsageCollectionLocks() {
|
|
2550
|
+
const global = globalThis;
|
|
2551
|
+
const existing = global[CONTENT_USAGE_COLLECTION_LOCKS_KEY];
|
|
2552
|
+
if (existing instanceof Map) return existing;
|
|
2553
|
+
const locks = /* @__PURE__ */ new Map();
|
|
2554
|
+
global[CONTENT_USAGE_COLLECTION_LOCKS_KEY] = locks;
|
|
2555
|
+
return locks;
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
//#endregion
|
|
2559
|
+
export { loadContentMediaUsageFields as _, findNonTranslatableSiblingContentIds as a, markContentMediaUsageCollectionStaleSafely as c, withContentUsageCollectionLock as d, loadContentMediaUsageSnapshots as f, buildContentMediaUsageFieldFingerprint as g, MediaUsageFieldDiscoveryError as h, deleteContentMediaUsageCollection as i, refreshContentMediaUsageAfterWrite as l, buildContentMediaUsageSourceKey as m, CONTENT_MEDIA_USAGE_COLLECTION_SCOPE as n, invalidateContentMediaUsageSchemaChange as o, MEDIA_USAGE_CONTENT_SOURCE_VARIANTS as p, deleteContentMediaUsage as r, markContentMediaUsageCollectionStale as s, CONTENT_MEDIA_USAGE_ADAPTER_ID as t, refreshContentMediaUsageForWork as u, CONTENT_SOURCE_SCHEMA_VERSION as v, MediaUsageRepository as y };
|
|
2560
|
+
//# sourceMappingURL=content-refresh-DNERP4-f.mjs.map
|