emdash 0.33.0 → 0.35.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-BMyNSSZi.d.mts → adapters-Bb1vG41D.d.mts} +33 -2
- package/dist/adapters-Bb1vG41D.d.mts.map +1 -0
- package/dist/{allowed-origins-CG1pACtS.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
- package/dist/{allowed-origins-CG1pACtS.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -3
- package/dist/api/route-utils.mjs +22 -21
- 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-D2hunekQ.mjs → api-BeBglVxU.mjs} +98 -407
- package/dist/api-BeBglVxU.mjs.map +1 -0
- package/dist/{api-tokens-CSND0i1z.mjs → api-tokens-KO24G1Fd.mjs} +2 -2
- package/dist/{api-tokens-CSND0i1z.mjs.map → api-tokens-KO24G1Fd.mjs.map} +1 -1
- package/dist/{apply-kwNEJW2l.mjs → apply-kUDt80-G.mjs} +131 -107
- package/dist/apply-kUDt80-G.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 +163 -49
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.mjs +8 -8
- 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 +10 -7
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +3 -3
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +12 -11
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +1763 -173
- 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 +17 -17
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +17 -17
- package/dist/astro/routes/api/admin/bylines/index.mjs +17 -17
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/bulk.mjs +12 -11
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/counts.mjs +6 -6
- package/dist/astro/routes/api/admin/comments/index.mjs +12 -11
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +5 -5
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +4 -4
- 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 +51 -43
- 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 +51 -43
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +47 -42
- 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 +47 -42
- 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 +50 -42
- 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 +50 -42
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +47 -42
- 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 +47 -42
- 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 +50 -42
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +47 -42
- 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 +50 -42
- 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 +51 -43
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +47 -42
- 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 +51 -43
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +47 -42
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +47 -42
- 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 +47 -42
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +9 -8
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +4 -4
- 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.d.mts.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +5 -9
- package/dist/astro/routes/api/auth/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/complete.mjs +13 -12
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.mjs +10 -9
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/register-options.mjs +12 -11
- 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 +12 -11
- 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 +3 -3
- 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 +14 -13
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +12 -11
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +13 -12
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/verify.mjs +13 -12
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/complete.mjs +13 -12
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/request.mjs +12 -11
- 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 +15 -14
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +14 -13
- 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 +13 -13
- 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 +18 -17
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +18 -17
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +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 +44 -31
- 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 +10 -8
- 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 +18 -16
- 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 +15 -15
- 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 +14 -14
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +18 -16
- 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 +28 -27
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +5 -5
- 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 +13 -11
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +6 -6
- 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 +18 -15
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +11 -11
- package/dist/astro/routes/api/menus/_name_/items.mjs +11 -11
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +11 -11
- package/dist/astro/routes/api/menus/_name_/translations.mjs +11 -11
- package/dist/astro/routes/api/menus/_name_.mjs +11 -11
- package/dist/astro/routes/api/menus/index.mjs +11 -11
- 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 +152 -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 +12 -11
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +12 -11
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +13 -12
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +13 -12
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +18 -17
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +13 -12
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +18 -17
- 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 +47 -42
- 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 +47 -42
- 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 +47 -42
- 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 +47 -42
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +47 -42
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.mjs +47 -42
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/index.mjs +10 -9
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +47 -42
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +47 -42
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +13 -13
- package/dist/astro/routes/api/search/index.mjs +12 -12
- package/dist/astro/routes/api/search/rebuild.mjs +13 -13
- package/dist/astro/routes/api/search/stats.mjs +8 -8
- package/dist/astro/routes/api/search/suggest.mjs +12 -12
- package/dist/astro/routes/api/sections/_slug_.mjs +12 -11
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +12 -11
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +6 -6
- package/dist/astro/routes/api/settings/backups/export.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/index.mjs +8 -8
- package/dist/astro/routes/api/settings/email.mjs +5 -5
- package/dist/astro/routes/api/settings.mjs +14 -13
- package/dist/astro/routes/api/settings.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin-verify.mjs +14 -13
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin.mjs +13 -12
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +30 -31
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +3 -3
- package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +31 -35
- 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 +8 -8
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +16 -15
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +18 -16
- 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 +16 -15
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +6 -6
- package/dist/astro/routes/api/typegen.mjs +6 -6
- package/dist/astro/routes/api/well-known/auth.mjs +1 -1
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +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 +12 -11
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +12 -11
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +7 -6
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +12 -11
- 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 +7 -6
- package/dist/astro/routes/robots.txt.mjs.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +10 -10
- package/dist/astro/routes/sitemap.xml.mjs +8 -7
- package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
- package/dist/astro/types.d.mts +35 -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-qRhWQnel.mjs → authorize-DUElxgOQ.mjs} +2 -2
- package/dist/{authorize-qRhWQnel.mjs.map → authorize-DUElxgOQ.mjs.map} +1 -1
- package/dist/{backup-CtA6LDNw.mjs → backup-DWYpJlLj.mjs} +4 -4
- package/dist/{backup-CtA6LDNw.mjs.map → backup-DWYpJlLj.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-vn56sLos.mjs +663 -0
- package/dist/bundle-vn56sLos.mjs.map +1 -0
- package/dist/{byline-fields-CNc0Yq5i.mjs → byline-fields-txaLGoME.mjs} +2 -2
- package/dist/{byline-fields-CNc0Yq5i.mjs.map → byline-fields-txaLGoME.mjs.map} +1 -1
- package/dist/{byline-registry-ZkPKqEwC.mjs → byline-registry-DDun8sxh.mjs} +2 -2
- package/dist/{byline-registry-ZkPKqEwC.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
- package/dist/{byline-Cp6csL84.mjs → byline-yjvWq4ip.mjs} +5 -5
- package/dist/{byline-Cp6csL84.mjs.map → byline-yjvWq4ip.mjs.map} +1 -1
- package/dist/{bylines-cf-C9lmF.mjs → bylines-Bu7pchQ4.mjs} +5 -5
- package/dist/{bylines-cf-C9lmF.mjs.map → bylines-Bu7pchQ4.mjs.map} +1 -1
- package/dist/{bylines-CEj5CdT4.mjs → bylines-DjXS5p0P.mjs} +7 -7
- package/dist/{bylines-CEj5CdT4.mjs.map → bylines-DjXS5p0P.mjs.map} +1 -1
- package/dist/{cache-C9FptTFR.mjs → cache-C46-3Irl.mjs} +3 -3
- package/dist/{cache-C9FptTFR.mjs.map → cache-C46-3Irl.mjs.map} +1 -1
- package/dist/{challenge-store-D9VNKELo.mjs → challenge-store-SIL5DsF2.mjs} +1 -1
- package/dist/{challenge-store-D9VNKELo.mjs.map → challenge-store-SIL5DsF2.mjs.map} +1 -1
- package/dist/{chunks-DbYzib_k.mjs → chunks-fiZ8G9RJ.mjs} +2 -2
- package/dist/{chunks-DbYzib_k.mjs.map → chunks-fiZ8G9RJ.mjs.map} +1 -1
- package/dist/cli/index.mjs +655 -677
- 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-DQPvMD8R.mjs +779 -0
- package/dist/collection-deletion-DQPvMD8R.mjs.map +1 -0
- package/dist/{comment-BIkJ6Fil.mjs → comment-Dxlep6cE.mjs} +2 -2
- package/dist/{comment-BIkJ6Fil.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
- package/dist/{comment-reaction-D-laEceb.mjs → comment-reaction-D-2J84UE.mjs} +2 -2
- package/dist/{comment-reaction-D-laEceb.mjs.map → comment-reaction-D-2J84UE.mjs.map} +1 -1
- package/dist/{comments-0_w6KChA.mjs → comments-CTfZtLmM.mjs} +3 -3
- package/dist/{comments-0_w6KChA.mjs.map → comments-CTfZtLmM.mjs.map} +1 -1
- package/dist/{components-BPxrRjpE.mjs → components-CQy3lh9x.mjs} +1 -1
- package/dist/{components-BPxrRjpE.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/config-COQzTe5Q.d.mts +34 -0
- package/dist/config-COQzTe5Q.d.mts.map +1 -0
- package/dist/{content-CJ94NEx5.mjs → content-ZHCc1ngN.mjs} +509 -78
- package/dist/content-ZHCc1ngN.mjs.map +1 -0
- package/dist/content-list-query-wJ2WN8Vp.d.mts +26 -0
- package/dist/content-list-query-wJ2WN8Vp.d.mts.map +1 -0
- package/dist/content-refresh-B_8SJzuU.mjs +2560 -0
- package/dist/content-refresh-B_8SJzuU.mjs.map +1 -0
- package/dist/{context-C_1nrqnW.mjs → context-ZldbZcle.mjs} +25 -17
- package/dist/context-ZldbZcle.mjs.map +1 -0
- package/dist/{cron-DRV-UC4K.mjs → cron-Y9aLtgrA.mjs} +1 -1
- package/dist/{cron-DRV-UC4K.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
- package/dist/{dashboard-DINnNVVU.mjs → dashboard-DvHbk_cv.mjs} +44 -10
- package/dist/dashboard-DvHbk_cv.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -4
- package/dist/db/index.mjs +31 -8
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql-migrations.d.mts +11 -0
- package/dist/db/libsql-migrations.d.mts.map +1 -0
- package/dist/db/libsql-migrations.mjs +57 -0
- package/dist/db/libsql-migrations.mjs.map +1 -0
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres-migrations.d.mts +10 -0
- package/dist/db/postgres-migrations.d.mts.map +1 -0
- package/dist/db/postgres-migrations.mjs +47 -0
- package/dist/db/postgres-migrations.mjs.map +1 -0
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite-migrations.d.mts +10 -0
- package/dist/db/sqlite-migrations.d.mts.map +1 -0
- package/dist/db/sqlite-migrations.mjs +28 -0
- package/dist/db/sqlite-migrations.mjs.map +1 -0
- 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-BD2FG0xV.mjs → default-DdDl3fxv.mjs} +1 -1
- package/dist/{default-BD2FG0xV.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
- package/dist/{deferred-tasks-C-WRCcKo.d.mts → deferred-tasks-SDWxrgzJ.d.mts} +1 -1
- package/dist/{deferred-tasks-C-WRCcKo.d.mts.map → deferred-tasks-SDWxrgzJ.d.mts.map} +1 -1
- package/dist/{device-flow-CTqd8mud.mjs → device-flow-C0GdD_z8.mjs} +4 -4
- package/dist/{device-flow-CTqd8mud.mjs.map → device-flow-C0GdD_z8.mjs.map} +1 -1
- package/dist/direct-executor-CDXeJCSs.mjs +85 -0
- package/dist/direct-executor-CDXeJCSs.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-C6WdcWbL.mjs → email-console-BHNjIwiB.mjs} +1 -1
- package/dist/{email-console-C6WdcWbL.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-oh_9i6bR.mjs → error-DupyJZE2.mjs} +3 -3
- package/dist/{error-oh_9i6bR.mjs.map → error-DupyJZE2.mjs.map} +1 -1
- package/dist/{errors-BucQ8VYz.mjs → errors-CLUPm7aD.mjs} +14 -2
- package/dist/errors-CLUPm7aD.mjs.map +1 -0
- package/dist/{escape-CXps5F_v.mjs → escape-DUY2r2Zs.mjs} +1 -1
- package/dist/{escape-CXps5F_v.mjs.map → escape-DUY2r2Zs.mjs.map} +1 -1
- package/dist/{field-defs-cache-WD82dMQq.mjs → field-defs-cache-BzwBlJrR.mjs} +4 -4
- package/dist/{field-defs-cache-WD82dMQq.mjs.map → field-defs-cache-BzwBlJrR.mjs.map} +1 -1
- package/dist/{fts-manager-BITakW_R.mjs → fts-manager-DyZEohZC.mjs} +76 -50
- package/dist/fts-manager-DyZEohZC.mjs.map +1 -0
- package/dist/{hreflang-BdZuVBhL.mjs → hreflang-BBe83kKa.mjs} +7 -7
- package/dist/{hreflang-BdZuVBhL.mjs.map → hreflang-BBe83kKa.mjs.map} +1 -1
- package/dist/{hreflang-BaLCNcDx.d.mts → hreflang-DwT2E0Sa.d.mts} +2 -2
- package/dist/{hreflang-BaLCNcDx.d.mts.map → hreflang-DwT2E0Sa.d.mts.map} +1 -1
- package/dist/identity-Ys6Dlc9C.mjs +35 -0
- package/dist/identity-Ys6Dlc9C.mjs.map +1 -0
- package/dist/{import-C-9uqyT3.mjs → import-DPkLmeXO.mjs} +5 -5
- package/dist/{import-C-9uqyT3.mjs.map → import-DPkLmeXO.mjs.map} +1 -1
- package/dist/{index-M37iz29I.d.mts → index-BVBZj4-7.d.mts} +236 -104
- package/dist/index-BVBZj4-7.d.mts.map +1 -0
- package/dist/{index-MGw47uWq.d.mts → index-C9KuQyO-.d.mts} +5 -10
- package/dist/index-C9KuQyO-.d.mts.map +1 -0
- package/dist/index.d.mts +21 -19
- package/dist/index.mjs +73 -67
- 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-DV1DZuUJ.mjs → load-YJ2xl3ra.mjs} +2 -2
- package/dist/{load-DV1DZuUJ.mjs.map → load-YJ2xl3ra.mjs.map} +1 -1
- package/dist/{loader-CGugY7Zl.mjs → loader-CdG_Ci3H.mjs} +95 -53
- package/dist/loader-CdG_Ci3H.mjs.map +1 -0
- package/dist/manifest-builder-B252yQUf.mjs +189 -0
- package/dist/manifest-builder-B252yQUf.mjs.map +1 -0
- package/dist/{manifest-schema-Da7kj23y.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
- package/dist/{manifest-schema-Da7kj23y.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 +6 -5
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-OhYL-7Io.mjs → media-BWWWb8kg.mjs} +92 -3
- package/dist/media-BWWWb8kg.mjs.map +1 -0
- package/dist/{media-BP7S-nHT.mjs → media-CzhAn2--.mjs} +2 -2
- package/dist/{media-BP7S-nHT.mjs.map → media-CzhAn2--.mjs.map} +1 -1
- package/dist/{media-allowlist-D-peH1H5.mjs → media-allowlist-D6x-Us02.mjs} +14 -3
- package/dist/media-allowlist-D6x-Us02.mjs.map +1 -0
- package/dist/{media-upload-DtrwU2B0.mjs → media-upload-CHBVn6m-.mjs} +10 -8
- package/dist/{media-upload-DtrwU2B0.mjs.map → media-upload-CHBVn6m-.mjs.map} +1 -1
- package/dist/{media-url-DYs_YLOx.mjs → media-url-BvoX88FA.mjs} +1 -1
- package/dist/{media-url-DYs_YLOx.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
- package/dist/{media-usage-BlxfTkN0.mjs → media-usage-BZCydfvu.mjs} +137 -60
- package/dist/media-usage-BZCydfvu.mjs.map +1 -0
- package/dist/media-usage-work-CLo_6tct.mjs +152 -0
- package/dist/media-usage-work-CLo_6tct.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-Y54RDPIN.mjs → menus-DSo5QeW0.mjs} +351 -95
- package/dist/menus-DSo5QeW0.mjs.map +1 -0
- package/dist/{menus-BcQWxeXu.mjs → menus-TCNSd2vp.mjs} +3 -3
- package/dist/{menus-BcQWxeXu.mjs.map → menus-TCNSd2vp.mjs.map} +1 -1
- package/dist/migrations/index.d.mts +22 -0
- package/dist/migrations/index.d.mts.map +1 -0
- package/dist/migrations/index.mjs +7 -0
- package/dist/{mode-BWyTBe3I.mjs → mode-BDoH_WcF.mjs} +1 -1
- package/dist/{mode-BWyTBe3I.mjs.map → mode-BDoH_WcF.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-ClvktmrL.mjs → oauth-authorization-iOikmrJd.mjs} +4 -4
- package/dist/{oauth-authorization-ClvktmrL.mjs.map → oauth-authorization-iOikmrJd.mjs.map} +1 -1
- package/dist/{oauth-clients-8ao-U66l.mjs → oauth-clients-CvI0nHMJ.mjs} +1 -1
- package/dist/{oauth-clients-8ao-U66l.mjs.map → oauth-clients-CvI0nHMJ.mjs.map} +1 -1
- package/dist/{oauth-state-store-B1ofdTiQ.mjs → oauth-state-store-Ba8KZlwW.mjs} +1 -1
- package/dist/{oauth-state-store-B1ofdTiQ.mjs.map → oauth-state-store-Ba8KZlwW.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-CscEuIiR.mjs → oauth-user-lookup-C5SQwGYC.mjs} +1 -1
- package/dist/{oauth-user-lookup-CscEuIiR.mjs.map → oauth-user-lookup-C5SQwGYC.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{options-CA4eVX6P.d.mts → options--hhz-jZ1.d.mts} +4 -3
- package/dist/options--hhz-jZ1.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-CJKNuewM.mjs → parse-DFbuc-Fh.mjs} +2 -2
- package/dist/{parse-CJKNuewM.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
- package/dist/{passkey-config-D-zrzMJI.mjs → passkey-config-DFAmUyuE.mjs} +1 -1
- package/dist/{passkey-config-D-zrzMJI.mjs.map → passkey-config-DFAmUyuE.mjs.map} +1 -1
- package/dist/{placeholder-DaeNS-VG.d.mts → placeholder-Dp0K3RkD.d.mts} +1 -1
- package/dist/{placeholder-DaeNS-VG.d.mts.map → placeholder-Dp0K3RkD.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/policy-TluUIxuu.mjs +45 -0
- package/dist/policy-TluUIxuu.mjs.map +1 -0
- package/dist/{portable-text-B_kqBmXB.mjs → portable-text-imBW5cHK.mjs} +1 -1
- package/dist/{portable-text-B_kqBmXB.mjs.map → portable-text-imBW5cHK.mjs.map} +1 -1
- package/dist/{preview-BU6PaLpM.mjs → preview-PW-nOnq4.mjs} +2 -2
- package/dist/{preview-BU6PaLpM.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
- package/dist/protocol-DjsJHTlK.d.mts +52 -0
- package/dist/protocol-DjsJHTlK.d.mts.map +1 -0
- package/dist/{public-url-Ox9oOlgp.mjs → public-url-CWoFhQT8.mjs} +1 -1
- package/dist/{public-url-Ox9oOlgp.mjs.map → public-url-CWoFhQT8.mjs.map} +1 -1
- package/dist/{query-rC9HKv1N.mjs → query-BWs7O5Il.mjs} +3 -3
- package/dist/{query-rC9HKv1N.mjs.map → query-BWs7O5Il.mjs.map} +1 -1
- package/dist/{query-C5w7PsRQ.mjs → query-zTVAR1Sl.mjs} +31 -49
- package/dist/query-zTVAR1Sl.mjs.map +1 -0
- package/dist/{rate-limit-kug6q99V.mjs → rate-limit-Bx4g9m3p.mjs} +2 -2
- package/dist/{rate-limit-kug6q99V.mjs.map → rate-limit-Bx4g9m3p.mjs.map} +1 -1
- package/dist/{redirect-Egf2R9yP.mjs → redirect-BgAOGHDq.mjs} +1 -1
- package/dist/{redirect-Egf2R9yP.mjs.map → redirect-BgAOGHDq.mjs.map} +1 -1
- package/dist/{redirect-DIJxXKOG.mjs → redirect-COsS6SWJ.mjs} +2 -2
- package/dist/{redirect-DIJxXKOG.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
- package/dist/{redirects-G8wAhWAf.mjs → redirects-DTDHWH31.mjs} +133 -14
- package/dist/redirects-DTDHWH31.mjs.map +1 -0
- package/dist/{redirects-Bzm812bY.mjs → redirects-DyChwDkG.mjs} +4 -4
- package/dist/{redirects-Bzm812bY.mjs.map → redirects-DyChwDkG.mjs.map} +1 -1
- package/dist/registry-Cqa2I749.mjs +1275 -0
- package/dist/registry-Cqa2I749.mjs.map +1 -0
- package/dist/{relation-Cbe3Sd3m.mjs → relation-BnHB0T4J.mjs} +3 -3
- package/dist/{relation-Cbe3Sd3m.mjs.map → relation-BnHB0T4J.mjs.map} +1 -1
- package/dist/{relations-Bw_E1TXo.mjs → relations-BBApI4e2.mjs} +3 -3
- package/dist/{relations-Bw_E1TXo.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
- package/dist/{relations-G2ta7GIl.d.mts → relations-BHo5A8Zf.d.mts} +367 -33
- package/dist/relations-BHo5A8Zf.d.mts.map +1 -0
- package/dist/{relations-qHwpUe66.mjs → relations-D5sM5fW0.mjs} +6 -6
- package/dist/{relations-qHwpUe66.mjs.map → relations-D5sM5fW0.mjs.map} +1 -1
- package/dist/request-context.d.mts +1 -1
- package/dist/{request-meta-Df-jFbAd.mjs → request-meta-DgZJh3zG.mjs} +2 -2
- package/dist/{request-meta-Df-jFbAd.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
- package/dist/{resolve-DZzG4HqM.mjs → resolve-DOhVEvty.mjs} +2 -2
- package/dist/{resolve-DZzG4HqM.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
- package/dist/{runner-BaADli8l.d.mts → runner-BYHZn3Jy.d.mts} +16 -3
- package/dist/runner-BYHZn3Jy.d.mts.map +1 -0
- package/dist/{runner-BFTrkYhu.mjs → runner-C1T11CfC.mjs} +1190 -257
- package/dist/runner-C1T11CfC.mjs.map +1 -0
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +4 -3
- package/dist/runtime.mjs.map +1 -1
- package/dist/schema-2AOsSn2v.mjs +390 -0
- package/dist/schema-2AOsSn2v.mjs.map +1 -0
- package/dist/{schema-CiQXr6_F.mjs → schema-DZ5YVVZj.mjs} +3 -3
- package/dist/{schema-CiQXr6_F.mjs.map → schema-DZ5YVVZj.mjs.map} +1 -1
- package/dist/{scopes-CNJVT5i5.mjs → scopes-8ClPF2bC.mjs} +3 -3
- package/dist/{scopes-CNJVT5i5.mjs.map → scopes-8ClPF2bC.mjs.map} +1 -1
- package/dist/{search-FZhx5dDU.mjs → search-u1QYdOCG.mjs} +25 -15
- package/dist/search-u1QYdOCG.mjs.map +1 -0
- package/dist/{secrets-DLd8RXee.mjs → secrets-BEgsAQEU.mjs} +3 -3
- package/dist/{secrets-DLd8RXee.mjs.map → secrets-BEgsAQEU.mjs.map} +1 -1
- package/dist/{sections-BrIrMUCn.mjs → sections-DmHviGyM.mjs} +3 -3
- package/dist/{sections-BrIrMUCn.mjs.map → sections-DmHviGyM.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +24 -22
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +16 -15
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-CobNfOvb.mjs → seo-AFpVvTqr.mjs} +5 -3
- package/dist/seo-AFpVvTqr.mjs.map +1 -0
- package/dist/{seo-DTHRntNC.mjs → seo-Dq4G-RrO.mjs} +2 -2
- package/dist/{seo-DTHRntNC.mjs.map → seo-Dq4G-RrO.mjs.map} +1 -1
- package/dist/{service-B-pYGPgR.mjs → service-BpArayPr.mjs} +3 -3
- package/dist/{service-B-pYGPgR.mjs.map → service-BpArayPr.mjs.map} +1 -1
- package/dist/{session-user-BvM9-zNl.mjs → session-user-BEorKVT-.mjs} +1 -1
- package/dist/{session-user-BvM9-zNl.mjs.map → session-user-BEorKVT-.mjs.map} +1 -1
- package/dist/{settings-DPHau-WH.mjs → settings-DE9xtzqd.mjs} +3 -3
- package/dist/{settings-DPHau-WH.mjs.map → settings-DE9xtzqd.mjs.map} +1 -1
- package/dist/{settings-kJhGiQ4z.mjs → settings-xRdoUZr0.mjs} +4 -4
- package/dist/{settings-kJhGiQ4z.mjs.map → settings-xRdoUZr0.mjs.map} +1 -1
- package/dist/{setup-complete-u50Qm8rY.mjs → setup-complete-BnT8pq_C.mjs} +1 -1
- package/dist/{setup-complete-u50Qm8rY.mjs.map → setup-complete-BnT8pq_C.mjs.map} +1 -1
- package/dist/{setup-nonce-DCjiiOD2.mjs → setup-nonce-CBitf_oQ.mjs} +1 -1
- package/dist/{setup-nonce-DCjiiOD2.mjs.map → setup-nonce-CBitf_oQ.mjs.map} +1 -1
- package/dist/{single-flight-cache-DW3w11Pu.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
- package/dist/{single-flight-cache-DW3w11Pu.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
- package/dist/{site-url-I-lJ5kma.mjs → site-url-DjpIXMXJ.mjs} +2 -2
- package/dist/{site-url-I-lJ5kma.mjs.map → site-url-DjpIXMXJ.mjs.map} +1 -1
- package/dist/slugify-B994zkXC.mjs +17 -0
- package/dist/slugify-B994zkXC.mjs.map +1 -0
- package/dist/{snapshot-I_fjLDaD.mjs → snapshot-BBlGkb2q.mjs} +1 -1
- package/dist/{snapshot-I_fjLDaD.mjs.map → snapshot-BBlGkb2q.mjs.map} +1 -1
- package/dist/{ssrf-CTa6v21i.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
- package/dist/{ssrf-CTa6v21i.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
- package/dist/{state-CQvoefYP.mjs → state-DYwPue40.mjs} +1 -1
- package/dist/{state-CQvoefYP.mjs.map → state-DYwPue40.mjs.map} +1 -1
- package/dist/{status-CIsmlC1E.mjs → status-CWVYpbD8.mjs} +1 -1
- package/dist/{status-CIsmlC1E.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +3 -3
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/target-DixqqV5i.mjs +27 -0
- package/dist/target-DixqqV5i.mjs.map +1 -0
- package/dist/{taxonomies-CLN1N92E.mjs → taxonomies-BlrncHi9.mjs} +75 -64
- package/dist/taxonomies-BlrncHi9.mjs.map +1 -0
- package/dist/{taxonomies-CKuaZER5.mjs → taxonomies-OHUIiIpI.mjs} +280 -56
- package/dist/taxonomies-OHUIiIpI.mjs.map +1 -0
- package/dist/{taxonomy-pOrB2kZe.mjs → taxonomy-BqCZ1TdU.mjs} +131 -78
- package/dist/taxonomy-BqCZ1TdU.mjs.map +1 -0
- package/dist/{tokens-DagaJGK8.mjs → tokens-C39klX8R.mjs} +2 -2
- package/dist/{tokens-DagaJGK8.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
- package/dist/{transport-DVsa5YYo.mjs → transport-DBJwa-Si.mjs} +1 -1
- package/dist/{transport-DVsa5YYo.mjs.map → transport-DBJwa-Si.mjs.map} +1 -1
- package/dist/{transport-CAwTaTp_.d.mts → transport-RQE36APG.d.mts} +1 -1
- package/dist/{transport-CAwTaTp_.d.mts.map → transport-RQE36APG.d.mts.map} +1 -1
- package/dist/{trusted-proxy-D20_7fyX.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
- package/dist/{trusted-proxy-D20_7fyX.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
- package/dist/{types-NBI3rF-_.mjs → types-75KIqzL9.mjs} +18 -2
- package/dist/types-75KIqzL9.mjs.map +1 -0
- package/dist/{types-BjxTKxrZ.mjs → types-BW1syrlw.mjs} +1 -1
- package/dist/{types-BjxTKxrZ.mjs.map → types-BW1syrlw.mjs.map} +1 -1
- package/dist/{types-DLG4IFEQ.d.mts → types-BcqdcT-r.d.mts} +1 -1
- package/dist/{types-DLG4IFEQ.d.mts.map → types-BcqdcT-r.d.mts.map} +1 -1
- package/dist/{types-LoagAGzH.d.mts → types-BntycPTF.d.mts} +1 -1
- package/dist/{types-LoagAGzH.d.mts.map → types-BntycPTF.d.mts.map} +1 -1
- package/dist/{types-BnFP1M3J.d.mts → types-C2pMvO62.d.mts} +32 -10
- package/dist/types-C2pMvO62.d.mts.map +1 -0
- package/dist/{types-Cd8iuS1J.mjs → types-CQPWhPxE.mjs} +9 -12
- package/dist/types-CQPWhPxE.mjs.map +1 -0
- package/dist/{types-C8oz_ayJ.d.mts → types-CXnOzDe1.d.mts} +1 -1
- package/dist/{types-C8oz_ayJ.d.mts.map → types-CXnOzDe1.d.mts.map} +1 -1
- package/dist/types-CtzytVXn.mjs.map +1 -1
- package/dist/{types-BGWPMiBO.d.mts → types-DN64_zzZ.d.mts} +44 -5
- package/dist/types-DN64_zzZ.d.mts.map +1 -0
- package/dist/{types-Dc4ntQjQ.d.mts → types-DQIDRKF2.d.mts} +124 -1
- package/dist/types-DQIDRKF2.d.mts.map +1 -0
- package/dist/{types-CbUcuxxc.d.mts → types-DWRhk9xZ.d.mts} +1 -1
- package/dist/{types-CbUcuxxc.d.mts.map → types-DWRhk9xZ.d.mts.map} +1 -1
- package/dist/{types-BK6U01Ro.d.mts → types-Dfk8KI5R.d.mts} +1 -1
- package/dist/{types-BK6U01Ro.d.mts.map → types-Dfk8KI5R.d.mts.map} +1 -1
- package/dist/{types-Cz8hNch2.d.mts → types-DrJrVCT1.d.mts} +25 -5
- package/dist/types-DrJrVCT1.d.mts.map +1 -0
- package/dist/{upload-attempts-D8QAFLUJ.mjs → upload-attempts-D4FS8LT6.mjs} +1 -1
- package/dist/{upload-attempts-D8QAFLUJ.mjs.map → upload-attempts-D4FS8LT6.mjs.map} +1 -1
- package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
- package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
- package/dist/{user-DBrq4UVq.mjs → user-DJZfUJVE.mjs} +3 -3
- package/dist/{user-DBrq4UVq.mjs.map → user-DJZfUJVE.mjs.map} +1 -1
- package/dist/{utils-hLzi_2Fn.mjs → utils-D4vD_Y98.mjs} +3 -3
- package/dist/{utils-hLzi_2Fn.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
- package/dist/{validate-mKo3RxBM.mjs → validate-BMLBvxOC.mjs} +24 -5
- package/dist/validate-BMLBvxOC.mjs.map +1 -0
- package/dist/{validate-C3oSmA7F.d.mts → validate-CTqt5nuE.d.mts} +15 -7
- package/dist/validate-CTqt5nuE.d.mts.map +1 -0
- package/dist/{validation-BgpucsvF.mjs → validation-npvENUCB.mjs} +11 -10
- package/dist/{validation-BgpucsvF.mjs.map → validation-npvENUCB.mjs.map} +1 -1
- package/dist/version-DB_fqCSI.mjs +7 -0
- package/dist/{version-C4PnpmoV.mjs.map → version-DB_fqCSI.mjs.map} +1 -1
- package/dist/{widgets-CJJtkiKh.mjs → widgets-BD3ho7Rq.mjs} +3 -3
- package/dist/{widgets-CJJtkiKh.mjs.map → widgets-BD3ho7Rq.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-9PPSy8X6.mjs → wxr-taxonomies-B1R3vBoD.mjs} +13 -32
- package/dist/{wxr-taxonomies-9PPSy8X6.mjs.map → wxr-taxonomies-B1R3vBoD.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts → wxr-taxonomies-Ca68766D.d.mts} +1 -1
- package/dist/{wxr-taxonomies-Bkpp_2tK.d.mts.map → wxr-taxonomies-Ca68766D.d.mts.map} +1 -1
- package/dist/{zod-generator-CG64Hnbh.mjs → zod-generator-LC-on_Vb.mjs} +52 -5
- package/dist/zod-generator-LC-on_Vb.mjs.map +1 -0
- package/package.json +32 -8
- package/src/api/errors.ts +12 -0
- package/src/api/handlers/content.ts +127 -29
- 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/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/schema.ts +9 -3
- package/src/api/handlers/seo.ts +5 -2
- package/src/api/handlers/taxonomies.ts +345 -82
- package/src/api/media-usage-write-fence.ts +64 -0
- package/src/api/openapi/document.ts +199 -0
- 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 +78 -3
- package/src/api/schemas/taxonomies.ts +40 -8
- package/src/api/types.ts +1 -0
- package/src/astro/integration/index.ts +101 -56
- package/src/astro/integration/routes.ts +34 -4
- package/src/astro/integration/runtime.ts +18 -0
- package/src/astro/integration/virtual-modules.ts +25 -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 +12 -2
- package/src/astro/middleware/scoped-db.ts +21 -0
- package/src/astro/middleware.ts +140 -16
- 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/auth/dev-bypass.ts +0 -4
- 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/setup/dev-bypass.ts +2 -7
- package/src/astro/routes/api/setup/index.ts +5 -13
- 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 +25 -4
- package/src/cleanup.ts +6 -12
- package/src/cli/commands/doctor.ts +193 -6
- package/src/cli/commands/export-seed.ts +4 -1
- package/src/cli/commands/migrate.ts +593 -0
- package/src/cli/index.ts +2 -23
- package/src/client/index.ts +119 -0
- package/src/components/Block.astro +12 -0
- package/src/components/InlinePortableTextEditor.tsx +89 -1
- package/src/content/converters/mark-safety.ts +120 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +25 -16
- package/src/content/converters/prosemirror-to-portable-text.ts +12 -2
- package/src/content-list-query.ts +29 -0
- package/src/database/index.ts +8 -2
- 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/policy.ts +70 -0
- package/src/database/migrations/runner.ts +71 -3
- package/src/database/repositories/content.ts +861 -96
- 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 +17 -6
- package/src/database/repositories/taxonomy.ts +234 -103
- package/src/database/repositories/types.ts +51 -2
- package/src/database/types.ts +134 -16
- package/src/db/adapters.ts +78 -2
- package/src/db/index.ts +6 -1
- package/src/db/libsql-migrations.ts +72 -0
- package/src/db/postgres-migrations.ts +59 -0
- package/src/db/sqlite-migrations.ts +37 -0
- package/src/emdash-runtime.ts +271 -35
- 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/normalize.ts +31 -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 +32 -3
- package/src/loader.ts +119 -65
- 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/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/migrations/config-loader.ts +205 -0
- package/src/migrations/direct-executor.ts +141 -0
- package/src/migrations/identity.ts +46 -0
- package/src/migrations/index.ts +19 -0
- package/src/migrations/integration-metadata.ts +67 -0
- package/src/migrations/manifest-builder.ts +55 -0
- package/src/migrations/manifest-writer.ts +63 -0
- package/src/migrations/manifest.ts +197 -0
- package/src/migrations/protocol.ts +59 -0
- package/src/migrations/target.ts +35 -0
- 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 +43 -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/types.ts +39 -2
- package/src/query.ts +29 -37
- package/src/scheduler-health.ts +46 -0
- package/src/schema/registry.ts +777 -255
- package/src/schema/types.ts +50 -8
- package/src/schema/url-pattern.ts +33 -0
- package/src/schema/zod-generator.ts +77 -5
- package/src/search/fts-manager.ts +117 -76
- package/src/search/query.ts +32 -11
- package/src/search/types.ts +2 -0
- package/src/seed/apply.ts +68 -15
- package/src/seed/types.ts +11 -3
- package/src/seed/validate.ts +52 -2
- package/src/taxonomies/index.ts +77 -75
- package/src/taxonomies/term-counts.ts +21 -8
- package/src/utils/slugify.ts +1 -29
- package/src/virtual-modules.d.ts +24 -0
- package/dist/adapters-BMyNSSZi.d.mts.map +0 -1
- package/dist/api-D2hunekQ.mjs.map +0 -1
- package/dist/apply-kwNEJW2l.mjs.map +0 -1
- package/dist/config-DyF3OKg_.mjs.map +0 -1
- package/dist/content-CJ94NEx5.mjs.map +0 -1
- package/dist/content-refresh-Xw7aKmou.mjs +0 -1629
- package/dist/content-refresh-Xw7aKmou.mjs.map +0 -1
- package/dist/context-C_1nrqnW.mjs.map +0 -1
- package/dist/dashboard-DINnNVVU.mjs.map +0 -1
- package/dist/errors-BucQ8VYz.mjs.map +0 -1
- package/dist/fts-manager-BITakW_R.mjs.map +0 -1
- package/dist/index-M37iz29I.d.mts.map +0 -1
- package/dist/index-MGw47uWq.d.mts.map +0 -1
- package/dist/loader-CGugY7Zl.mjs.map +0 -1
- package/dist/media-OhYL-7Io.mjs.map +0 -1
- package/dist/media-allowlist-D-peH1H5.mjs.map +0 -1
- package/dist/media-usage-BlxfTkN0.mjs.map +0 -1
- package/dist/menus-Y54RDPIN.mjs.map +0 -1
- package/dist/options-CA4eVX6P.d.mts.map +0 -1
- package/dist/query-C5w7PsRQ.mjs.map +0 -1
- package/dist/redirects-G8wAhWAf.mjs.map +0 -1
- package/dist/registry-Dbodb4Uo.mjs +0 -840
- package/dist/registry-Dbodb4Uo.mjs.map +0 -1
- package/dist/relations-G2ta7GIl.d.mts.map +0 -1
- package/dist/runner-BFTrkYhu.mjs.map +0 -1
- package/dist/runner-BaADli8l.d.mts.map +0 -1
- package/dist/search-FZhx5dDU.mjs.map +0 -1
- package/dist/seo-CobNfOvb.mjs.map +0 -1
- package/dist/slugify-C_tqlU4G.mjs +0 -30
- package/dist/slugify-C_tqlU4G.mjs.map +0 -1
- package/dist/taxonomies-CKuaZER5.mjs.map +0 -1
- package/dist/taxonomies-CLN1N92E.mjs.map +0 -1
- package/dist/taxonomy-pOrB2kZe.mjs.map +0 -1
- package/dist/types-BGWPMiBO.d.mts.map +0 -1
- package/dist/types-BnFP1M3J.d.mts.map +0 -1
- package/dist/types-Cd8iuS1J.mjs.map +0 -1
- package/dist/types-Cz8hNch2.d.mts.map +0 -1
- package/dist/types-Dc4ntQjQ.d.mts.map +0 -1
- package/dist/types-NBI3rF-_.mjs.map +0 -1
- package/dist/validate-C3oSmA7F.d.mts.map +0 -1
- package/dist/validate-mKo3RxBM.mjs.map +0 -1
- package/dist/version-C4PnpmoV.mjs +0 -7
- package/dist/zod-generator-CG64Hnbh.mjs.map +0 -1
- /package/dist/{api-tokens-Dai9dND0.mjs → api-tokens-rIXC6LdE.mjs} +0 -0
- /package/dist/{ssrf-Cj568Dgs.mjs → ssrf-DpXgKsna.mjs} +0 -0
- /package/dist/{types-DKuWPYV5.mjs → types-CP1CflWf.mjs} +0 -0
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import {
|
|
2
2
|
sql,
|
|
3
3
|
type ExpressionBuilder,
|
|
4
|
-
type Insertable,
|
|
5
4
|
type Kysely,
|
|
5
|
+
type RawBuilder,
|
|
6
6
|
type Selectable,
|
|
7
7
|
type Transaction,
|
|
8
8
|
type Updateable,
|
|
9
9
|
} from "kysely";
|
|
10
10
|
import { ulid } from "ulidx";
|
|
11
11
|
|
|
12
|
+
import { isMediaUsageProjectionFingerprint } from "../../media/usage/projection-fingerprint.js";
|
|
12
13
|
import type { MediaUsageContentSourceVariant } from "../../media/usage/source-key.js";
|
|
13
14
|
import type { MediaKind, MediaUsageReferenceType } from "../../media/usage/types.js";
|
|
14
15
|
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
16
|
+
import { isPostgres } from "../dialect-helpers.js";
|
|
15
17
|
import { withTransaction } from "../transaction.js";
|
|
16
18
|
import type {
|
|
17
19
|
Database,
|
|
@@ -24,18 +26,35 @@ import { decodeCursor, encodeCursor, InvalidCursorError, type FindManyResult } f
|
|
|
24
26
|
|
|
25
27
|
type DatabaseExecutor = Kysely<Database> | Transaction<Database>;
|
|
26
28
|
type MediaUsageSourceNullableStringColumn =
|
|
29
|
+
| "collection_id"
|
|
27
30
|
| "source_fingerprint"
|
|
28
31
|
| "source_updated_at"
|
|
29
32
|
| "revision_id"
|
|
30
33
|
| "updated_at"
|
|
31
34
|
| "last_attempted_at"
|
|
32
35
|
| "last_error_code";
|
|
33
|
-
|
|
34
36
|
const OCCURRENCE_BIND_COLUMNS = 13;
|
|
37
|
+
export const MEDIA_USAGE_GENERATION_WRITE_LEASE_MS = 60 * 60 * 1000;
|
|
35
38
|
const OCCURRENCE_INSERT_BATCH_SIZE = Math.max(
|
|
36
39
|
1,
|
|
37
40
|
Math.floor(SQL_BATCH_SIZE / OCCURRENCE_BIND_COLUMNS),
|
|
38
41
|
);
|
|
42
|
+
|
|
43
|
+
function cleanupDeleteBatchSize(cleanupLease: MediaUsageCleanupLease | undefined): number {
|
|
44
|
+
return cleanupLease ? SQL_BATCH_SIZE - 3 : SQL_BATCH_SIZE;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function canIssueCleanupStatement(canIssueStatement: (() => boolean) | undefined): boolean {
|
|
48
|
+
return canIssueStatement?.() ?? true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function cleanupDurationSeconds(value: number): number {
|
|
52
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
53
|
+
throw new Error("Media usage cleanup duration must be a non-negative whole number of seconds");
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
39
58
|
const CONTENT_SOURCE_ELIGIBILITY = sql<boolean>`(
|
|
40
59
|
s.source_variant = 'draft_overlay'
|
|
41
60
|
OR (
|
|
@@ -49,14 +68,37 @@ const CONTENT_SOURCE_ELIGIBILITY = sql<boolean>`(
|
|
|
49
68
|
AND overlay.collection_slug = s.collection_slug
|
|
50
69
|
AND overlay.content_id = s.content_id
|
|
51
70
|
AND overlay.source_variant = 'draft_overlay'
|
|
71
|
+
AND ${contentSourceMatchesActiveCollection("overlay", "s.collection_id")}
|
|
52
72
|
)
|
|
53
73
|
)
|
|
54
74
|
)
|
|
55
75
|
)`;
|
|
56
76
|
|
|
77
|
+
type ContentSourceAlias = "deleted_source" | "overlay" | "s" | "state";
|
|
78
|
+
type CurrentCollectionIdReference = "collection.id" | "page.collection_id" | "s.collection_id";
|
|
79
|
+
|
|
80
|
+
function contentSourceMatchesActiveCollection(
|
|
81
|
+
source: ContentSourceAlias,
|
|
82
|
+
currentCollectionId: CurrentCollectionIdReference,
|
|
83
|
+
): RawBuilder<boolean> {
|
|
84
|
+
return sql<boolean>`(
|
|
85
|
+
NOT EXISTS (
|
|
86
|
+
SELECT 1
|
|
87
|
+
FROM _emdash_media_usage_activation AS activation
|
|
88
|
+
WHERE activation.task_key = 'incremental_capture'
|
|
89
|
+
AND activation.state = 'active'
|
|
90
|
+
)
|
|
91
|
+
OR (
|
|
92
|
+
${sql.ref(`${source}.collection_id`)} = ${sql.ref(currentCollectionId)}
|
|
93
|
+
AND ${sql.ref(`${source}.identity_version`)} = 1
|
|
94
|
+
)
|
|
95
|
+
)`;
|
|
96
|
+
}
|
|
97
|
+
|
|
57
98
|
export interface MediaUsageSourceInput {
|
|
58
99
|
sourceKey: string;
|
|
59
100
|
sourceType: string;
|
|
101
|
+
collectionId?: string | null;
|
|
60
102
|
collectionSlug?: string | null;
|
|
61
103
|
contentId?: string | null;
|
|
62
104
|
sourceVariant: MediaUsageContentSourceVariant;
|
|
@@ -72,6 +114,7 @@ export interface MediaUsageSourceInput {
|
|
|
72
114
|
sourceUpdatedAt?: string | null;
|
|
73
115
|
sourceVersion?: number | null;
|
|
74
116
|
sourceFingerprint?: string | null;
|
|
117
|
+
identityVersion?: number | null;
|
|
75
118
|
sourceCompleteness?: MediaUsageSourceCompleteness;
|
|
76
119
|
lastAttemptedAt?: string | null;
|
|
77
120
|
lastErrorCode?: string | null;
|
|
@@ -92,6 +135,7 @@ export interface MediaUsageOccurrenceInput {
|
|
|
92
135
|
export interface MediaUsageSource {
|
|
93
136
|
sourceKey: string;
|
|
94
137
|
sourceType: string;
|
|
138
|
+
collectionId: string | null;
|
|
95
139
|
collectionSlug: string | null;
|
|
96
140
|
contentId: string | null;
|
|
97
141
|
sourceVariant: string;
|
|
@@ -108,6 +152,7 @@ export interface MediaUsageSource {
|
|
|
108
152
|
sourceUpdatedAt: string | null;
|
|
109
153
|
sourceVersion: number | null;
|
|
110
154
|
sourceFingerprint: string | null;
|
|
155
|
+
identityVersion: number | null;
|
|
111
156
|
sourceCompleteness: string;
|
|
112
157
|
lastAttemptedAt: string | null;
|
|
113
158
|
lastErrorCode: string | null;
|
|
@@ -118,6 +163,7 @@ export interface MediaUsageSource {
|
|
|
118
163
|
|
|
119
164
|
export interface MediaUsageGuardedReplaceResult {
|
|
120
165
|
replaced: boolean;
|
|
166
|
+
unchanged: boolean;
|
|
121
167
|
/** Populated only when a guarded replacement did not win the current source row. */
|
|
122
168
|
source: MediaUsageSource | null;
|
|
123
169
|
}
|
|
@@ -137,6 +183,59 @@ export interface MediaUsageGuardedAttemptResult {
|
|
|
137
183
|
source: MediaUsageSource | null;
|
|
138
184
|
}
|
|
139
185
|
|
|
186
|
+
export interface MediaUsageSourceGenerationDeletionMeasurement {
|
|
187
|
+
occurrenceCount: number;
|
|
188
|
+
occurrenceBytes: number;
|
|
189
|
+
exceedsOccurrenceLimit: boolean;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface MediaUsageCleanupCursor {
|
|
193
|
+
createdAt: string;
|
|
194
|
+
id: string;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export interface MediaUsageCleanupClaim {
|
|
198
|
+
leaseToken: string;
|
|
199
|
+
cursor: MediaUsageCleanupCursor | null;
|
|
200
|
+
claimedAt: string;
|
|
201
|
+
scanBeforeAt: string;
|
|
202
|
+
consecutiveFailures: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface MediaUsageCleanupCandidate {
|
|
206
|
+
id: string;
|
|
207
|
+
sourceKey: string;
|
|
208
|
+
generation: string;
|
|
209
|
+
createdAt: string;
|
|
210
|
+
currentGeneration: string | null;
|
|
211
|
+
indexedAt: string | null;
|
|
212
|
+
writeLeaseExpiresAt: string | null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface MediaUsageCleanupLease {
|
|
216
|
+
leaseToken: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface MediaUsageCleanupDeleteOptions {
|
|
220
|
+
candidateIds?: readonly string[];
|
|
221
|
+
cleanupLease?: MediaUsageCleanupLease;
|
|
222
|
+
canIssueStatement?: () => boolean;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface MediaUsageCleanupCompletion {
|
|
226
|
+
leaseToken: string;
|
|
227
|
+
nextCursor: MediaUsageCleanupCursor | null;
|
|
228
|
+
sweepComplete: boolean;
|
|
229
|
+
candidateCount: number;
|
|
230
|
+
deletedOrphans: number;
|
|
231
|
+
deletedStale: number;
|
|
232
|
+
deletedAbandoned: number;
|
|
233
|
+
deletedWriteLeases: number;
|
|
234
|
+
backlogLowerBound: number;
|
|
235
|
+
scanHasMore: boolean;
|
|
236
|
+
durationMs: number;
|
|
237
|
+
}
|
|
238
|
+
|
|
140
239
|
export interface MediaUsageIndexStatusRepairInput extends MediaUsageIndexStatusIdentity {
|
|
141
240
|
runToken: string;
|
|
142
241
|
schemaVersion?: number;
|
|
@@ -155,6 +254,30 @@ export interface MediaUsageIndexStatusFinalizeInput extends MediaUsageIndexStatu
|
|
|
155
254
|
updatedAt?: string;
|
|
156
255
|
}
|
|
157
256
|
|
|
257
|
+
export interface MediaUsageIndexStatusEpochRepairInput extends MediaUsageIndexStatusIdentity {
|
|
258
|
+
collectionId: string;
|
|
259
|
+
runToken: string;
|
|
260
|
+
schemaVersion: number;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export interface MediaUsageIndexStatusEpochRepairRun {
|
|
264
|
+
changeEpoch: number | string;
|
|
265
|
+
startedAt: string;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export interface MediaUsageIndexStatusEpochFinalizeInput extends MediaUsageIndexStatusEpochRepairInput {
|
|
269
|
+
startingEpoch: number | string;
|
|
270
|
+
status: Exclude<MediaUsageIndexStatusValue, "never" | "running" | "stale">;
|
|
271
|
+
indexedSourceCount: number;
|
|
272
|
+
failedSourceCount: number;
|
|
273
|
+
lastErrorCode: string | null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface MediaUsageIncrementalStatusIdentity {
|
|
277
|
+
collectionId: string;
|
|
278
|
+
collectionSlug: string;
|
|
279
|
+
}
|
|
280
|
+
|
|
158
281
|
export interface MediaUsageGuardedIndexStatusResult {
|
|
159
282
|
finalized: boolean;
|
|
160
283
|
status: MediaUsageIndexStatus | null;
|
|
@@ -214,6 +337,7 @@ export interface MediaUsageCollectionIndexStatusScope {
|
|
|
214
337
|
collectionSlug: string;
|
|
215
338
|
status: string | null;
|
|
216
339
|
schemaVersion: number | null;
|
|
340
|
+
reconciliationRequired: boolean;
|
|
217
341
|
}
|
|
218
342
|
|
|
219
343
|
export interface MediaUsageEntrySource {
|
|
@@ -231,6 +355,7 @@ export interface MediaUsageEntryGroup {
|
|
|
231
355
|
interface MediaUsageSourceRow {
|
|
232
356
|
source_key: string;
|
|
233
357
|
source_type: string;
|
|
358
|
+
collection_id: string | null;
|
|
234
359
|
collection_slug: string | null;
|
|
235
360
|
content_id: string | null;
|
|
236
361
|
source_variant: string;
|
|
@@ -247,6 +372,7 @@ interface MediaUsageSourceRow {
|
|
|
247
372
|
source_updated_at: string | null;
|
|
248
373
|
source_version: number | null;
|
|
249
374
|
source_fingerprint: string | null;
|
|
375
|
+
identity_version: number | null;
|
|
250
376
|
source_completeness: string;
|
|
251
377
|
last_attempted_at: string | null;
|
|
252
378
|
last_error_code: string | null;
|
|
@@ -279,6 +405,7 @@ export interface MediaUsageRecord {
|
|
|
279
405
|
interface JoinedUsageRow {
|
|
280
406
|
source_key: string;
|
|
281
407
|
source_type: string;
|
|
408
|
+
collection_id: string | null;
|
|
282
409
|
collection_slug: string | null;
|
|
283
410
|
content_id: string | null;
|
|
284
411
|
source_variant: string;
|
|
@@ -295,6 +422,7 @@ interface JoinedUsageRow {
|
|
|
295
422
|
source_updated_at: string | null;
|
|
296
423
|
source_version: number | null;
|
|
297
424
|
source_fingerprint: string | null;
|
|
425
|
+
identity_version: number | null;
|
|
298
426
|
source_completeness: string;
|
|
299
427
|
last_attempted_at: string | null;
|
|
300
428
|
last_error_code: string | null;
|
|
@@ -329,12 +457,26 @@ export class MediaUsageRepository {
|
|
|
329
457
|
occurrences: readonly MediaUsageOccurrenceInput[],
|
|
330
458
|
): Promise<MediaUsageSource> {
|
|
331
459
|
const generation = ulid();
|
|
332
|
-
const now = new Date().toISOString();
|
|
333
460
|
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
461
|
+
const admitted = await this.withGenerationWriteLease(
|
|
462
|
+
source,
|
|
463
|
+
generation,
|
|
464
|
+
async (leaseToken, now) => {
|
|
465
|
+
await withTransaction(this.db, async (trx) => {
|
|
466
|
+
if (!(await this.lockCanonicalSourceCollection(trx, source))) {
|
|
467
|
+
throw new Error(`Media usage collection is no longer current for ${source.sourceKey}`);
|
|
468
|
+
}
|
|
469
|
+
await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
|
|
470
|
+
const promoted = await this.upsertSource(trx, source, generation, now, leaseToken);
|
|
471
|
+
if (!promoted) {
|
|
472
|
+
throw new Error(`Media usage generation lease expired for ${source.sourceKey}`);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
);
|
|
477
|
+
if (!admitted) {
|
|
478
|
+
throw new Error(`Media usage collection is no longer current for ${source.sourceKey}`);
|
|
479
|
+
}
|
|
338
480
|
|
|
339
481
|
const replaced = await this.findSource(source.sourceKey);
|
|
340
482
|
if (!replaced) {
|
|
@@ -347,22 +489,36 @@ export class MediaUsageRepository {
|
|
|
347
489
|
occurrences: readonly MediaUsageOccurrenceInput[],
|
|
348
490
|
expectedCurrentGeneration: string | null,
|
|
349
491
|
): Promise<MediaUsageGuardedReplaceResult> {
|
|
492
|
+
if (
|
|
493
|
+
expectedCurrentGeneration !== null &&
|
|
494
|
+
(await this.projectionMatchesCurrentGeneration(source, expectedCurrentGeneration))
|
|
495
|
+
) {
|
|
496
|
+
return { replaced: false, unchanged: true, source: null };
|
|
497
|
+
}
|
|
350
498
|
const generation = ulid();
|
|
351
|
-
const now = new Date().toISOString();
|
|
352
|
-
const row = this.buildSourceRow(source, generation, now);
|
|
353
499
|
let replaced = false;
|
|
354
500
|
|
|
355
|
-
await
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
501
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
502
|
+
const row = this.buildSourceRow(source, generation, now);
|
|
503
|
+
await withTransaction(this.db, async (trx) => {
|
|
504
|
+
if (!(await this.lockCanonicalSourceCollection(trx, source))) return;
|
|
505
|
+
await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
|
|
506
|
+
if (expectedCurrentGeneration === null) {
|
|
507
|
+
replaced = await this.insertSourceIfAbsent(trx, row, leaseToken);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
replaced = await this.updateSourceIfGeneration(
|
|
511
|
+
trx,
|
|
512
|
+
row,
|
|
513
|
+
expectedCurrentGeneration,
|
|
514
|
+
leaseToken,
|
|
515
|
+
);
|
|
516
|
+
});
|
|
362
517
|
});
|
|
363
518
|
|
|
364
519
|
return {
|
|
365
520
|
replaced,
|
|
521
|
+
unchanged: false,
|
|
366
522
|
source: replaced ? null : await this.findSource(source.sourceKey),
|
|
367
523
|
};
|
|
368
524
|
}
|
|
@@ -397,41 +553,99 @@ export class MediaUsageRepository {
|
|
|
397
553
|
return sources;
|
|
398
554
|
}
|
|
399
555
|
|
|
556
|
+
async measureSourceGenerationDeletion(
|
|
557
|
+
sourceKey: string,
|
|
558
|
+
generation: string,
|
|
559
|
+
maxOccurrences: number,
|
|
560
|
+
): Promise<MediaUsageSourceGenerationDeletionMeasurement> {
|
|
561
|
+
if (!Number.isSafeInteger(maxOccurrences) || maxOccurrences < 0) {
|
|
562
|
+
throw new Error("Media usage deletion measurement requires a non-negative row limit");
|
|
563
|
+
}
|
|
564
|
+
const payload = sql<string>`
|
|
565
|
+
COALESCE(field_slug, '') || COALESCE(field_path, '') ||
|
|
566
|
+
COALESCE(reference_type, '') || COALESCE(media_id, '') ||
|
|
567
|
+
COALESCE(provider, '') || COALESCE(provider_asset_id, '') ||
|
|
568
|
+
COALESCE(media_kind, '') || COALESCE(mime_type, '')
|
|
569
|
+
`;
|
|
570
|
+
const occurrenceBytes = isPostgres(this.db)
|
|
571
|
+
? sql<number>`octet_length(${payload})`
|
|
572
|
+
: sql<number>`length(CAST(${payload} AS BLOB))`;
|
|
573
|
+
const rows = await this.db
|
|
574
|
+
.selectFrom("_emdash_media_usage")
|
|
575
|
+
.select(occurrenceBytes.as("occurrence_bytes"))
|
|
576
|
+
.where("source_key", "=", sourceKey)
|
|
577
|
+
.where("generation", "=", generation)
|
|
578
|
+
.limit(maxOccurrences + 1)
|
|
579
|
+
.execute();
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
occurrenceCount: rows.length,
|
|
583
|
+
occurrenceBytes: rows.reduce((total, row) => total + Number(row.occurrence_bytes), 0),
|
|
584
|
+
exceedsOccurrenceLimit: rows.length > maxOccurrences,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
400
588
|
async replaceSourceIfMatching(
|
|
401
589
|
source: MediaUsageSourceInput,
|
|
402
590
|
occurrences: readonly MediaUsageOccurrenceInput[],
|
|
403
591
|
expectedSource: MediaUsageSource | null,
|
|
404
592
|
): Promise<MediaUsageGuardedReplaceResult> {
|
|
593
|
+
if (
|
|
594
|
+
expectedSource !== null &&
|
|
595
|
+
(await this.projectionMatchesExpectedSource(source, expectedSource))
|
|
596
|
+
) {
|
|
597
|
+
return { replaced: false, unchanged: true, source: null };
|
|
598
|
+
}
|
|
405
599
|
const generation = ulid();
|
|
406
|
-
const now = new Date().toISOString();
|
|
407
|
-
const row = this.buildSourceRow(source, generation, now);
|
|
408
600
|
let replaced = false;
|
|
409
601
|
|
|
410
|
-
await
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
602
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
603
|
+
const row = this.buildSourceRow(source, generation, now);
|
|
604
|
+
await withTransaction(this.db, async (trx) => {
|
|
605
|
+
if (!(await this.lockCanonicalSourceCollection(trx, source))) return;
|
|
606
|
+
await this.insertOccurrences(trx, source.sourceKey, generation, occurrences, now);
|
|
607
|
+
if (expectedSource === null) {
|
|
608
|
+
replaced = await this.insertSourceIfAbsent(trx, row, leaseToken);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
replaced = await this.updateSourceIfMatching(trx, row, expectedSource, leaseToken);
|
|
612
|
+
});
|
|
417
613
|
});
|
|
418
614
|
|
|
419
615
|
return {
|
|
420
616
|
replaced,
|
|
617
|
+
unchanged: false,
|
|
421
618
|
source: replaced ? null : await this.findSource(source.sourceKey),
|
|
422
619
|
};
|
|
423
620
|
}
|
|
424
621
|
|
|
425
622
|
async markSourceAttempted(source: MediaUsageSourceInput): Promise<MediaUsageSource> {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
623
|
+
if (source.collectionId !== undefined && source.collectionId !== null) {
|
|
624
|
+
const expectedSource = await this.findSource(source.sourceKey);
|
|
625
|
+
const result = await this.markSourceAttemptedIfMatching(source, expectedSource);
|
|
626
|
+
if (!result.attempted) {
|
|
627
|
+
throw new Error(`Canonical media usage source ${source.sourceKey} is no longer current`);
|
|
628
|
+
}
|
|
629
|
+
const attempted = await this.findSource(source.sourceKey);
|
|
630
|
+
if (!attempted) {
|
|
631
|
+
throw new Error(`Media usage source ${source.sourceKey} was not persisted`);
|
|
632
|
+
}
|
|
633
|
+
return attempted;
|
|
634
|
+
}
|
|
429
635
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
.
|
|
433
|
-
|
|
434
|
-
.
|
|
636
|
+
const generation = ulid();
|
|
637
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
638
|
+
const row = this.buildAttemptedSourceRow(source, generation, now);
|
|
639
|
+
const updates = this.attemptedSourceUpdateSet(source, row);
|
|
640
|
+
const result = await this.db
|
|
641
|
+
.insertInto("_emdash_media_usage_sources")
|
|
642
|
+
.values(row)
|
|
643
|
+
.onConflict((oc) => oc.column("source_key").doUpdateSet(updates))
|
|
644
|
+
.executeTakeFirst();
|
|
645
|
+
if ((result.numInsertedOrUpdatedRows ?? 0n) <= 0n) {
|
|
646
|
+
throw new Error(`Media usage generation lease expired for ${source.sourceKey}`);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
435
649
|
|
|
436
650
|
const attempted = await this.findSource(source.sourceKey);
|
|
437
651
|
if (!attempted) {
|
|
@@ -444,14 +658,28 @@ export class MediaUsageRepository {
|
|
|
444
658
|
source: MediaUsageSourceInput,
|
|
445
659
|
expectedSource: MediaUsageSource | null,
|
|
446
660
|
): Promise<MediaUsageGuardedAttemptResult> {
|
|
447
|
-
const
|
|
448
|
-
const row = this.buildAttemptedSourceRow(source, now);
|
|
661
|
+
const generation = ulid();
|
|
449
662
|
let attempted = false;
|
|
450
663
|
|
|
451
664
|
if (expectedSource === null) {
|
|
452
|
-
|
|
665
|
+
await this.withGenerationWriteLease(source, generation, async (leaseToken, now) => {
|
|
666
|
+
const row = this.buildAttemptedSourceRow(source, generation, now);
|
|
667
|
+
await withTransaction(this.db, async (trx) => {
|
|
668
|
+
if (!(await this.lockCanonicalSourceCollection(trx, source))) return;
|
|
669
|
+
attempted = await this.persistSourceIfWriteLease(
|
|
670
|
+
trx,
|
|
671
|
+
row,
|
|
672
|
+
leaseToken,
|
|
673
|
+
sql`ON CONFLICT (source_key) DO NOTHING`,
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
453
677
|
} else {
|
|
454
|
-
|
|
678
|
+
const row = this.buildAttemptedSourceRow(source, generation, new Date().toISOString());
|
|
679
|
+
await withTransaction(this.db, async (trx) => {
|
|
680
|
+
if (!(await this.lockCanonicalSourceCollection(trx, source))) return;
|
|
681
|
+
attempted = await this.updateAttemptedSourceIfMatching(trx, source, row, expectedSource);
|
|
682
|
+
});
|
|
455
683
|
}
|
|
456
684
|
|
|
457
685
|
return {
|
|
@@ -482,6 +710,7 @@ export class MediaUsageRepository {
|
|
|
482
710
|
.whereRef("deleted_source.collection_slug", "=", "s.collection_slug")
|
|
483
711
|
.whereRef("deleted_source.content_id", "=", "s.content_id")
|
|
484
712
|
.where("deleted_source.source_variant", "in", ["columns", "draft_overlay"])
|
|
713
|
+
.where(contentSourceMatchesActiveCollection("deleted_source", "collection.id"))
|
|
485
714
|
.where("deleted_source.content_deleted_at", "is not", null),
|
|
486
715
|
),
|
|
487
716
|
),
|
|
@@ -519,6 +748,7 @@ export class MediaUsageRepository {
|
|
|
519
748
|
"collection.slug as collection_slug",
|
|
520
749
|
"status.status as status",
|
|
521
750
|
"status.schema_version as schema_version",
|
|
751
|
+
"status.reconciliation_required as reconciliation_required",
|
|
522
752
|
])
|
|
523
753
|
.orderBy("collection.slug", "asc")
|
|
524
754
|
.execute();
|
|
@@ -527,6 +757,8 @@ export class MediaUsageRepository {
|
|
|
527
757
|
collectionSlug: row.collection_slug,
|
|
528
758
|
status: row.status,
|
|
529
759
|
schemaVersion: row.schema_version === null ? null : Number(row.schema_version),
|
|
760
|
+
reconciliationRequired:
|
|
761
|
+
row.reconciliation_required !== null && Number(row.reconciliation_required) !== 0,
|
|
530
762
|
}));
|
|
531
763
|
}
|
|
532
764
|
|
|
@@ -541,7 +773,11 @@ export class MediaUsageRepository {
|
|
|
541
773
|
throw new InvalidCursorError(options.cursor ?? "");
|
|
542
774
|
}
|
|
543
775
|
let matchedGroups = this.currentContentMediaUsageBaseQuery()
|
|
544
|
-
.select([
|
|
776
|
+
.select([
|
|
777
|
+
"collection.id as collection_id",
|
|
778
|
+
"s.collection_slug as collection_slug",
|
|
779
|
+
"s.content_id as content_id",
|
|
780
|
+
])
|
|
545
781
|
.where("u.media_id", "=", mediaId)
|
|
546
782
|
.distinct();
|
|
547
783
|
if (cursor) {
|
|
@@ -574,7 +810,7 @@ export class MediaUsageRepository {
|
|
|
574
810
|
db
|
|
575
811
|
.selectFrom("page_groups as page")
|
|
576
812
|
.crossJoin("_emdash_media_usage_sources as state")
|
|
577
|
-
.select(["page.collection_slug", "page.content_id"])
|
|
813
|
+
.select(["page.collection_id", "page.collection_slug", "page.content_id"])
|
|
578
814
|
.select((eb) =>
|
|
579
815
|
eb.fn.max<string | null>("state.content_deleted_at").as("entry_deleted_at"),
|
|
580
816
|
)
|
|
@@ -582,13 +818,15 @@ export class MediaUsageRepository {
|
|
|
582
818
|
.whereRef("page.content_id", "=", "state.content_id")
|
|
583
819
|
.where("state.source_type", "=", "content")
|
|
584
820
|
.where("state.source_variant", "in", ["columns", "draft_overlay"])
|
|
585
|
-
.
|
|
821
|
+
.where(contentSourceMatchesActiveCollection("state", "page.collection_id"))
|
|
822
|
+
.groupBy(["page.collection_id", "page.collection_slug", "page.content_id"]),
|
|
586
823
|
)
|
|
587
824
|
.selectFrom("entry_state as page")
|
|
588
825
|
.crossJoin("_emdash_media_usage_sources as s")
|
|
589
826
|
.crossJoin("_emdash_media_usage as u")
|
|
590
827
|
.whereRef("page.collection_slug", "=", "s.collection_slug")
|
|
591
828
|
.whereRef("page.content_id", "=", "s.content_id")
|
|
829
|
+
.where(contentSourceMatchesActiveCollection("s", "page.collection_id"))
|
|
592
830
|
.whereRef("s.source_key", "=", "u.source_key")
|
|
593
831
|
.whereRef("s.current_generation", "=", "u.generation")
|
|
594
832
|
.select(currentUsageSelect)
|
|
@@ -692,6 +930,7 @@ export class MediaUsageRepository {
|
|
|
692
930
|
): Promise<MediaUsageGuardedDeleteResult> {
|
|
693
931
|
let deleted = false;
|
|
694
932
|
await withTransaction(this.db, async (trx) => {
|
|
933
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
695
934
|
const result = await trx
|
|
696
935
|
.deleteFrom("_emdash_media_usage_sources")
|
|
697
936
|
.where("source_key", "=", sourceKey)
|
|
@@ -714,10 +953,14 @@ export class MediaUsageRepository {
|
|
|
714
953
|
): Promise<MediaUsageGuardedDeleteResult> {
|
|
715
954
|
let deleted = false;
|
|
716
955
|
await withTransaction(this.db, async (trx) => {
|
|
956
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
717
957
|
const result = await trx
|
|
718
958
|
.deleteFrom("_emdash_media_usage_sources")
|
|
719
959
|
.where("source_key", "=", sourceKey)
|
|
720
960
|
.where(this.sourceMatchExpression(expectedSource))
|
|
961
|
+
.where(
|
|
962
|
+
this.currentCollectionExists(expectedSource.collectionId, expectedSource.collectionSlug),
|
|
963
|
+
)
|
|
721
964
|
.executeTakeFirst();
|
|
722
965
|
deleted = Number(result.numDeletedRows ?? 0) > 0;
|
|
723
966
|
if (!deleted) return;
|
|
@@ -744,10 +987,14 @@ export class MediaUsageRepository {
|
|
|
744
987
|
const tableName = `ec_${collectionSlug}`;
|
|
745
988
|
let deleted = false;
|
|
746
989
|
await withTransaction(this.db, async (trx) => {
|
|
990
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
747
991
|
const result = await trx
|
|
748
992
|
.deleteFrom("_emdash_media_usage_sources")
|
|
749
993
|
.where("source_key", "=", sourceKey)
|
|
750
994
|
.where(this.sourceMatchExpression(expectedSource))
|
|
995
|
+
.where(
|
|
996
|
+
this.currentCollectionExists(expectedSource.collectionId, expectedSource.collectionSlug),
|
|
997
|
+
)
|
|
751
998
|
.where(
|
|
752
999
|
sql<boolean>`NOT EXISTS (SELECT 1 FROM ${sql.ref(tableName)} WHERE id = ${contentId})`,
|
|
753
1000
|
)
|
|
@@ -803,136 +1050,360 @@ export class MediaUsageRepository {
|
|
|
803
1050
|
return deleted;
|
|
804
1051
|
}
|
|
805
1052
|
|
|
806
|
-
async findCollectionContentSources(
|
|
807
|
-
|
|
1053
|
+
async findCollectionContentSources(
|
|
1054
|
+
collectionSlug: string,
|
|
1055
|
+
collectionId?: string,
|
|
1056
|
+
): Promise<MediaUsageSource[]> {
|
|
1057
|
+
let query = this.db
|
|
808
1058
|
.selectFrom("_emdash_media_usage_sources")
|
|
809
1059
|
.selectAll()
|
|
810
1060
|
.where("source_type", "=", "content")
|
|
811
1061
|
.where("collection_slug", "=", collectionSlug)
|
|
812
|
-
.orderBy("source_key", "asc")
|
|
813
|
-
|
|
1062
|
+
.orderBy("source_key", "asc");
|
|
1063
|
+
if (collectionId !== undefined) query = query.where("collection_id", "=", collectionId);
|
|
1064
|
+
const rows = await query.execute();
|
|
814
1065
|
return rows.map((row) => rowToSource(row));
|
|
815
1066
|
}
|
|
816
1067
|
|
|
817
|
-
async
|
|
1068
|
+
async claimMediaUsageCleanup(input: {
|
|
1069
|
+
leaseToken: string;
|
|
1070
|
+
leaseDurationSeconds: number;
|
|
1071
|
+
nextEligibleDelaySeconds: number;
|
|
1072
|
+
sweepSafetyWindowSeconds: number;
|
|
1073
|
+
}): Promise<MediaUsageCleanupClaim | null> {
|
|
1074
|
+
const leaseDurationSeconds = cleanupDurationSeconds(input.leaseDurationSeconds);
|
|
1075
|
+
const nextEligibleDelaySeconds = cleanupDurationSeconds(input.nextEligibleDelaySeconds);
|
|
1076
|
+
const sweepSafetyWindowSeconds = cleanupDurationSeconds(input.sweepSafetyWindowSeconds);
|
|
1077
|
+
const claimedAt = this.cleanupTimestampOffset(0);
|
|
1078
|
+
const leaseExpiresAt = this.cleanupTimestampOffset(leaseDurationSeconds);
|
|
1079
|
+
const nextEligibleAt = this.cleanupTimestampOffset(nextEligibleDelaySeconds);
|
|
1080
|
+
const sweepBeforeAt = this.cleanupTimestampOffset(-sweepSafetyWindowSeconds);
|
|
1081
|
+
const row = await this.db
|
|
1082
|
+
.updateTable("_emdash_media_usage_cleanup")
|
|
1083
|
+
.set({
|
|
1084
|
+
lease_token: input.leaseToken,
|
|
1085
|
+
lease_expires_at: leaseExpiresAt,
|
|
1086
|
+
next_eligible_at: nextEligibleAt,
|
|
1087
|
+
last_started_at: claimedAt,
|
|
1088
|
+
updated_at: claimedAt,
|
|
1089
|
+
scan_before_at: sql<string>`CASE
|
|
1090
|
+
WHEN scan_before_at IS NULL THEN ${sweepBeforeAt}
|
|
1091
|
+
ELSE scan_before_at
|
|
1092
|
+
END`,
|
|
1093
|
+
})
|
|
1094
|
+
.where("task_key", "=", "projection_gc")
|
|
1095
|
+
.where(this.cleanupTimestampIsDue("next_eligible_at"))
|
|
1096
|
+
.where((eb) =>
|
|
1097
|
+
eb.or([eb("lease_token", "is", null), this.cleanupTimestampIsDue("lease_expires_at")]),
|
|
1098
|
+
)
|
|
1099
|
+
.returning([
|
|
1100
|
+
"cursor_created_at",
|
|
1101
|
+
"cursor_id",
|
|
1102
|
+
"last_started_at",
|
|
1103
|
+
"scan_before_at",
|
|
1104
|
+
"consecutive_failures",
|
|
1105
|
+
])
|
|
1106
|
+
.executeTakeFirst();
|
|
1107
|
+
if (!row) return null;
|
|
1108
|
+
if (!row.last_started_at || !row.scan_before_at) {
|
|
1109
|
+
throw new Error("Media usage cleanup claim did not persist its database timestamps");
|
|
1110
|
+
}
|
|
1111
|
+
return {
|
|
1112
|
+
leaseToken: input.leaseToken,
|
|
1113
|
+
cursor:
|
|
1114
|
+
row.cursor_created_at && row.cursor_id
|
|
1115
|
+
? { createdAt: row.cursor_created_at, id: row.cursor_id }
|
|
1116
|
+
: null,
|
|
1117
|
+
claimedAt: row.last_started_at,
|
|
1118
|
+
scanBeforeAt: row.scan_before_at,
|
|
1119
|
+
consecutiveFailures: row.consecutive_failures,
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
async findMediaUsageCleanupCandidates(input: {
|
|
1124
|
+
cutoff: string;
|
|
1125
|
+
cursor: MediaUsageCleanupCursor | null;
|
|
1126
|
+
limit: number;
|
|
1127
|
+
cleanupLease?: MediaUsageCleanupLease;
|
|
1128
|
+
}): Promise<MediaUsageCleanupCandidate[]> {
|
|
1129
|
+
let query = this.db
|
|
1130
|
+
.selectFrom("_emdash_media_usage as u")
|
|
1131
|
+
.leftJoin("_emdash_media_usage_sources as s", "s.source_key", "u.source_key")
|
|
1132
|
+
.leftJoin("_emdash_media_usage_generation_writes as writer", (join) =>
|
|
1133
|
+
join
|
|
1134
|
+
.onRef("writer.source_key", "=", "u.source_key")
|
|
1135
|
+
.onRef("writer.generation", "=", "u.generation"),
|
|
1136
|
+
)
|
|
1137
|
+
.select([
|
|
1138
|
+
"u.id as id",
|
|
1139
|
+
"u.source_key as source_key",
|
|
1140
|
+
"u.generation as generation",
|
|
1141
|
+
"u.created_at as created_at",
|
|
1142
|
+
"s.current_generation as current_generation",
|
|
1143
|
+
"s.indexed_at as indexed_at",
|
|
1144
|
+
"writer.expires_at as write_lease_expires_at",
|
|
1145
|
+
])
|
|
1146
|
+
.where("u.created_at", "<", input.cutoff)
|
|
1147
|
+
.orderBy("u.created_at", "asc")
|
|
1148
|
+
.orderBy("u.id", "asc")
|
|
1149
|
+
.limit(Math.max(0, Math.floor(input.limit)));
|
|
1150
|
+
if (input.cleanupLease) {
|
|
1151
|
+
query = query.where(this.activeCleanupLeaseExpression(input.cleanupLease));
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
if (input.cursor) {
|
|
1155
|
+
query = query.where((eb) =>
|
|
1156
|
+
eb.or([
|
|
1157
|
+
eb("u.created_at", ">", input.cursor!.createdAt),
|
|
1158
|
+
eb.and([
|
|
1159
|
+
eb("u.created_at", "=", input.cursor!.createdAt),
|
|
1160
|
+
eb("u.id", ">", input.cursor!.id),
|
|
1161
|
+
]),
|
|
1162
|
+
]),
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const rows = await query.execute();
|
|
1167
|
+
return rows.map((row) => ({
|
|
1168
|
+
id: row.id,
|
|
1169
|
+
sourceKey: row.source_key,
|
|
1170
|
+
generation: row.generation,
|
|
1171
|
+
createdAt: row.created_at,
|
|
1172
|
+
currentGeneration: row.current_generation,
|
|
1173
|
+
indexedAt: row.indexed_at,
|
|
1174
|
+
writeLeaseExpiresAt: row.write_lease_expires_at,
|
|
1175
|
+
}));
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
async completeMediaUsageCleanup(input: MediaUsageCleanupCompletion): Promise<boolean> {
|
|
1179
|
+
const updates = {
|
|
1180
|
+
lease_token: null,
|
|
1181
|
+
lease_expires_at: null,
|
|
1182
|
+
cursor_created_at: input.sweepComplete ? null : (input.nextCursor?.createdAt ?? null),
|
|
1183
|
+
cursor_id: input.sweepComplete ? null : (input.nextCursor?.id ?? null),
|
|
1184
|
+
...(input.sweepComplete ? { scan_before_at: null } : {}),
|
|
1185
|
+
consecutive_failures: 0,
|
|
1186
|
+
last_completed_at: this.cleanupTimestampOffset(0),
|
|
1187
|
+
last_candidate_count: input.candidateCount,
|
|
1188
|
+
last_deleted_orphans: input.deletedOrphans,
|
|
1189
|
+
last_deleted_stale: input.deletedStale,
|
|
1190
|
+
last_deleted_abandoned: input.deletedAbandoned,
|
|
1191
|
+
last_deleted_write_leases: input.deletedWriteLeases,
|
|
1192
|
+
last_backlog_lower_bound: input.backlogLowerBound,
|
|
1193
|
+
last_scan_has_more: input.scanHasMore ? 1 : 0,
|
|
1194
|
+
last_duration_ms: input.durationMs,
|
|
1195
|
+
last_error_code: null,
|
|
1196
|
+
updated_at: this.cleanupTimestampOffset(0),
|
|
1197
|
+
};
|
|
1198
|
+
const result = await this.db
|
|
1199
|
+
.updateTable("_emdash_media_usage_cleanup")
|
|
1200
|
+
.set(updates)
|
|
1201
|
+
.where("task_key", "=", "projection_gc")
|
|
1202
|
+
.where("lease_token", "=", input.leaseToken)
|
|
1203
|
+
.where(this.cleanupLeaseExpiryIsInFuture("_emdash_media_usage_cleanup.lease_expires_at"))
|
|
1204
|
+
.executeTakeFirst();
|
|
1205
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
async failMediaUsageCleanup(input: {
|
|
1209
|
+
leaseToken: string;
|
|
1210
|
+
retryDelaySeconds: number;
|
|
1211
|
+
consecutiveFailures: number;
|
|
1212
|
+
durationMs: number;
|
|
1213
|
+
errorCode: string;
|
|
1214
|
+
}): Promise<boolean> {
|
|
1215
|
+
const retryDelaySeconds = cleanupDurationSeconds(input.retryDelaySeconds);
|
|
1216
|
+
const result = await this.db
|
|
1217
|
+
.updateTable("_emdash_media_usage_cleanup")
|
|
1218
|
+
.set({
|
|
1219
|
+
lease_token: null,
|
|
1220
|
+
lease_expires_at: null,
|
|
1221
|
+
next_eligible_at: this.cleanupTimestampOffset(retryDelaySeconds),
|
|
1222
|
+
consecutive_failures: input.consecutiveFailures,
|
|
1223
|
+
last_completed_at: this.cleanupTimestampOffset(0),
|
|
1224
|
+
last_duration_ms: input.durationMs,
|
|
1225
|
+
last_error_code: input.errorCode,
|
|
1226
|
+
updated_at: this.cleanupTimestampOffset(0),
|
|
1227
|
+
})
|
|
1228
|
+
.where("task_key", "=", "projection_gc")
|
|
1229
|
+
.where("lease_token", "=", input.leaseToken)
|
|
1230
|
+
.where(this.cleanupLeaseExpiryIsInFuture("_emdash_media_usage_cleanup.lease_expires_at"))
|
|
1231
|
+
.executeTakeFirst();
|
|
1232
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
async deleteOrphanOccurrencesOlderThan(
|
|
1236
|
+
cutoff: string,
|
|
1237
|
+
limit: number,
|
|
1238
|
+
options: MediaUsageCleanupDeleteOptions = {},
|
|
1239
|
+
): Promise<number> {
|
|
818
1240
|
const batchLimit = Math.floor(limit);
|
|
819
1241
|
if (batchLimit <= 0) return 0;
|
|
1242
|
+
if (options.candidateIds) {
|
|
1243
|
+
return this.deleteOrphanCandidateIds(
|
|
1244
|
+
options.candidateIds.slice(0, batchLimit),
|
|
1245
|
+
cutoff,
|
|
1246
|
+
options.cleanupLease,
|
|
1247
|
+
options.canIssueStatement,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
|
|
820
1251
|
|
|
821
|
-
|
|
1252
|
+
let query = this.db
|
|
822
1253
|
.selectFrom("_emdash_media_usage as u")
|
|
823
1254
|
.leftJoin("_emdash_media_usage_sources as s", (join) =>
|
|
824
1255
|
join.onRef("s.source_key", "=", "u.source_key"),
|
|
825
1256
|
)
|
|
1257
|
+
.leftJoin("_emdash_media_usage_generation_writes as writer", (join) =>
|
|
1258
|
+
join
|
|
1259
|
+
.onRef("writer.source_key", "=", "u.source_key")
|
|
1260
|
+
.onRef("writer.generation", "=", "u.generation"),
|
|
1261
|
+
)
|
|
826
1262
|
.select("u.id")
|
|
827
1263
|
.where("s.source_key", "is", null)
|
|
828
1264
|
.where("u.created_at", "<", cutoff)
|
|
1265
|
+
.where(this.noActiveGenerationWriteExpression("u"))
|
|
829
1266
|
.orderBy("u.created_at", "asc")
|
|
830
1267
|
.orderBy("u.id", "asc")
|
|
831
|
-
.limit(batchLimit)
|
|
832
|
-
|
|
1268
|
+
.limit(batchLimit);
|
|
1269
|
+
if (options.cleanupLease) {
|
|
1270
|
+
query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
|
|
1271
|
+
}
|
|
1272
|
+
const rows = await query.execute();
|
|
833
1273
|
|
|
834
|
-
|
|
835
|
-
for (const idBatch of chunks(
|
|
1274
|
+
return this.deleteOrphanCandidateIds(
|
|
836
1275
|
rows.map((row) => row.id),
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
.where("id", "in", idBatch)
|
|
842
|
-
.where("created_at", "<", cutoff)
|
|
843
|
-
.where(
|
|
844
|
-
sql<boolean>`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources s WHERE s.source_key = _emdash_media_usage.source_key)`,
|
|
845
|
-
)
|
|
846
|
-
.executeTakeFirst();
|
|
847
|
-
deleted += Number(result.numDeletedRows ?? 0);
|
|
848
|
-
}
|
|
849
|
-
return deleted;
|
|
1276
|
+
cutoff,
|
|
1277
|
+
options.cleanupLease,
|
|
1278
|
+
options.canIssueStatement,
|
|
1279
|
+
);
|
|
850
1280
|
}
|
|
851
1281
|
|
|
852
|
-
async deleteStaleGenerationsOlderThan(
|
|
1282
|
+
async deleteStaleGenerationsOlderThan(
|
|
1283
|
+
cutoff: string,
|
|
1284
|
+
limit: number,
|
|
1285
|
+
options: MediaUsageCleanupDeleteOptions = {},
|
|
1286
|
+
): Promise<number> {
|
|
853
1287
|
const batchLimit = Math.floor(limit);
|
|
854
1288
|
if (batchLimit <= 0) return 0;
|
|
1289
|
+
if (options.candidateIds) {
|
|
1290
|
+
return this.deleteStaleCandidateIds(
|
|
1291
|
+
options.candidateIds.slice(0, batchLimit),
|
|
1292
|
+
cutoff,
|
|
1293
|
+
options.cleanupLease,
|
|
1294
|
+
options.canIssueStatement,
|
|
1295
|
+
);
|
|
1296
|
+
}
|
|
1297
|
+
if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
|
|
855
1298
|
|
|
856
|
-
|
|
1299
|
+
let query = this.db
|
|
857
1300
|
.selectFrom("_emdash_media_usage as u")
|
|
858
1301
|
.innerJoin("_emdash_media_usage_sources as s", (join) =>
|
|
859
1302
|
join.onRef("s.source_key", "=", "u.source_key"),
|
|
860
1303
|
)
|
|
1304
|
+
.leftJoin("_emdash_media_usage_generation_writes as writer", (join) =>
|
|
1305
|
+
join
|
|
1306
|
+
.onRef("writer.source_key", "=", "u.source_key")
|
|
1307
|
+
.onRef("writer.generation", "=", "u.generation"),
|
|
1308
|
+
)
|
|
861
1309
|
.select("u.id")
|
|
862
1310
|
.where("u.created_at", "<", cutoff)
|
|
863
1311
|
.whereRef("u.generation", "!=", "s.current_generation")
|
|
864
1312
|
.whereRef("u.created_at", "<", "s.indexed_at")
|
|
1313
|
+
.where(this.noActiveGenerationWriteExpression("u"))
|
|
865
1314
|
.orderBy("u.created_at", "asc")
|
|
866
1315
|
.orderBy("u.id", "asc")
|
|
867
|
-
.limit(batchLimit)
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const ids = rows.map((row) => row.id);
|
|
871
|
-
if (ids.length === 0) return 0;
|
|
872
|
-
|
|
873
|
-
let deleted = 0;
|
|
874
|
-
for (const idBatch of chunks(ids, SQL_BATCH_SIZE)) {
|
|
875
|
-
const result = await this.db
|
|
876
|
-
.deleteFrom("_emdash_media_usage")
|
|
877
|
-
.where("id", "in", idBatch)
|
|
878
|
-
.where("created_at", "<", cutoff)
|
|
879
|
-
.where((eb) =>
|
|
880
|
-
eb.exists(
|
|
881
|
-
eb
|
|
882
|
-
.selectFrom("_emdash_media_usage_sources as s")
|
|
883
|
-
.select("s.source_key")
|
|
884
|
-
.whereRef("s.source_key", "=", "_emdash_media_usage.source_key")
|
|
885
|
-
.whereRef("s.current_generation", "!=", "_emdash_media_usage.generation")
|
|
886
|
-
.whereRef("_emdash_media_usage.created_at", "<", "s.indexed_at"),
|
|
887
|
-
),
|
|
888
|
-
)
|
|
889
|
-
.executeTakeFirst();
|
|
890
|
-
deleted += Number(result.numDeletedRows ?? 0);
|
|
1316
|
+
.limit(batchLimit);
|
|
1317
|
+
if (options.cleanupLease) {
|
|
1318
|
+
query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
|
|
891
1319
|
}
|
|
892
|
-
|
|
1320
|
+
const rows = await query.execute();
|
|
1321
|
+
|
|
1322
|
+
return this.deleteStaleCandidateIds(
|
|
1323
|
+
rows.map((row) => row.id),
|
|
1324
|
+
cutoff,
|
|
1325
|
+
options.cleanupLease,
|
|
1326
|
+
options.canIssueStatement,
|
|
1327
|
+
);
|
|
893
1328
|
}
|
|
894
1329
|
|
|
895
|
-
async deleteAbandonedGenerationsOlderThan(
|
|
1330
|
+
async deleteAbandonedGenerationsOlderThan(
|
|
1331
|
+
cutoff: string,
|
|
1332
|
+
limit: number,
|
|
1333
|
+
options: MediaUsageCleanupDeleteOptions = {},
|
|
1334
|
+
): Promise<number> {
|
|
896
1335
|
const batchLimit = Math.floor(limit);
|
|
897
1336
|
if (batchLimit <= 0) return 0;
|
|
1337
|
+
if (options.candidateIds) {
|
|
1338
|
+
return this.deleteAbandonedCandidateIds(
|
|
1339
|
+
options.candidateIds.slice(0, batchLimit),
|
|
1340
|
+
cutoff,
|
|
1341
|
+
options.cleanupLease,
|
|
1342
|
+
options.canIssueStatement,
|
|
1343
|
+
);
|
|
1344
|
+
}
|
|
1345
|
+
if (!canIssueCleanupStatement(options.canIssueStatement)) return 0;
|
|
898
1346
|
|
|
899
|
-
|
|
1347
|
+
let query = this.db
|
|
900
1348
|
.selectFrom("_emdash_media_usage as u")
|
|
901
1349
|
.innerJoin("_emdash_media_usage_sources as s", (join) =>
|
|
902
1350
|
join.onRef("s.source_key", "=", "u.source_key"),
|
|
903
1351
|
)
|
|
1352
|
+
.leftJoin("_emdash_media_usage_generation_writes as writer", (join) =>
|
|
1353
|
+
join
|
|
1354
|
+
.onRef("writer.source_key", "=", "u.source_key")
|
|
1355
|
+
.onRef("writer.generation", "=", "u.generation"),
|
|
1356
|
+
)
|
|
904
1357
|
.select("u.id")
|
|
905
1358
|
.where("u.created_at", "<", cutoff)
|
|
906
1359
|
.whereRef("u.generation", "!=", "s.current_generation")
|
|
907
1360
|
.whereRef("u.created_at", ">=", "s.indexed_at")
|
|
1361
|
+
.where(this.noActiveGenerationWriteExpression("u"))
|
|
908
1362
|
.orderBy("u.created_at", "asc")
|
|
909
1363
|
.orderBy("u.id", "asc")
|
|
910
|
-
.limit(batchLimit)
|
|
911
|
-
|
|
1364
|
+
.limit(batchLimit);
|
|
1365
|
+
if (options.cleanupLease) {
|
|
1366
|
+
query = query.where(this.activeCleanupLeaseExpression(options.cleanupLease));
|
|
1367
|
+
}
|
|
1368
|
+
const rows = await query.execute();
|
|
912
1369
|
|
|
913
|
-
|
|
914
|
-
for (const idBatch of chunks(
|
|
1370
|
+
return this.deleteAbandonedCandidateIds(
|
|
915
1371
|
rows.map((row) => row.id),
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1372
|
+
cutoff,
|
|
1373
|
+
options.cleanupLease,
|
|
1374
|
+
options.canIssueStatement,
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
async deleteExpiredGenerationWriteLeases(
|
|
1379
|
+
limit: number,
|
|
1380
|
+
cleanupLease?: MediaUsageCleanupLease,
|
|
1381
|
+
canIssueStatement?: () => boolean,
|
|
1382
|
+
): Promise<number> {
|
|
1383
|
+
const batchLimit = Math.floor(limit);
|
|
1384
|
+
if (batchLimit <= 0 || !canIssueCleanupStatement(canIssueStatement)) return 0;
|
|
1385
|
+
let query = this.db
|
|
1386
|
+
.selectFrom("_emdash_media_usage_generation_writes")
|
|
1387
|
+
.select("lease_token")
|
|
1388
|
+
.where(this.generationWriteLeaseHasExpired("expires_at"))
|
|
1389
|
+
.orderBy("expires_at", "asc")
|
|
1390
|
+
.orderBy("lease_token", "asc")
|
|
1391
|
+
.limit(batchLimit);
|
|
1392
|
+
if (cleanupLease) query = query.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
1393
|
+
const rows = await query.execute();
|
|
1394
|
+
if (rows.length === 0 || !canIssueCleanupStatement(canIssueStatement)) return 0;
|
|
1395
|
+
let deleteQuery = this.db
|
|
1396
|
+
.deleteFrom("_emdash_media_usage_generation_writes")
|
|
1397
|
+
.where(
|
|
1398
|
+
"lease_token",
|
|
1399
|
+
"in",
|
|
1400
|
+
rows.map((row) => row.lease_token),
|
|
1401
|
+
)
|
|
1402
|
+
.where(this.generationWriteLeaseHasExpired("expires_at"));
|
|
1403
|
+
if (cleanupLease)
|
|
1404
|
+
deleteQuery = deleteQuery.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
1405
|
+
const result = await deleteQuery.executeTakeFirst();
|
|
1406
|
+
return Number(result.numDeletedRows ?? 0);
|
|
936
1407
|
}
|
|
937
1408
|
|
|
938
1409
|
async upsertIndexStatus(input: MediaUsageIndexStatusInput): Promise<MediaUsageIndexStatus> {
|
|
@@ -979,6 +1450,43 @@ export class MediaUsageRepository {
|
|
|
979
1450
|
return status;
|
|
980
1451
|
}
|
|
981
1452
|
|
|
1453
|
+
async invalidateIndexStatusForSchemaChange(collectionSlug: string): Promise<boolean> {
|
|
1454
|
+
const result = await this.db
|
|
1455
|
+
.updateTable("_emdash_media_usage_index_status as status")
|
|
1456
|
+
.set({
|
|
1457
|
+
change_epoch: sql<number>`change_epoch + 1`,
|
|
1458
|
+
status: "stale",
|
|
1459
|
+
completed_at: null,
|
|
1460
|
+
cursor: null,
|
|
1461
|
+
last_error_code: "CONTENT_USAGE_STALE",
|
|
1462
|
+
reconciliation_required: 1,
|
|
1463
|
+
updated_at: this.sortableUtcTimestamp(),
|
|
1464
|
+
})
|
|
1465
|
+
.where("status.adapter_id", "=", "content-media")
|
|
1466
|
+
.where("status.scope_type", "=", "collection")
|
|
1467
|
+
.where("status.scope_key", "=", collectionSlug)
|
|
1468
|
+
.where("status.capture_state", "=", "active")
|
|
1469
|
+
.where((eb) =>
|
|
1470
|
+
eb.exists(
|
|
1471
|
+
eb
|
|
1472
|
+
.selectFrom("_emdash_collections as collection")
|
|
1473
|
+
.select("collection.id")
|
|
1474
|
+
.whereRef("collection.id", "=", "status.collection_id")
|
|
1475
|
+
.whereRef("collection.slug", "=", "status.scope_key"),
|
|
1476
|
+
),
|
|
1477
|
+
)
|
|
1478
|
+
.where(
|
|
1479
|
+
sql<boolean>`EXISTS (
|
|
1480
|
+
SELECT 1
|
|
1481
|
+
FROM _emdash_media_usage_activation AS activation
|
|
1482
|
+
WHERE activation.task_key = 'incremental_capture'
|
|
1483
|
+
AND activation.state = 'active'
|
|
1484
|
+
)`,
|
|
1485
|
+
)
|
|
1486
|
+
.executeTakeFirst();
|
|
1487
|
+
return Number(result.numUpdatedRows ?? 0) === 1;
|
|
1488
|
+
}
|
|
1489
|
+
|
|
982
1490
|
async beginIndexStatusRepair(
|
|
983
1491
|
input: MediaUsageIndexStatusRepairInput,
|
|
984
1492
|
): Promise<MediaUsageIndexStatus> {
|
|
@@ -1029,6 +1537,247 @@ export class MediaUsageRepository {
|
|
|
1029
1537
|
};
|
|
1030
1538
|
}
|
|
1031
1539
|
|
|
1540
|
+
async beginIndexStatusRepairAtCurrentEpoch(
|
|
1541
|
+
input: MediaUsageIndexStatusEpochRepairInput,
|
|
1542
|
+
): Promise<MediaUsageIndexStatusEpochRepairRun | null> {
|
|
1543
|
+
const now = this.sortableUtcTimestamp();
|
|
1544
|
+
const row = await this.db
|
|
1545
|
+
.updateTable("_emdash_media_usage_index_status")
|
|
1546
|
+
.set({
|
|
1547
|
+
status: "running",
|
|
1548
|
+
schema_version: input.schemaVersion,
|
|
1549
|
+
started_at: now,
|
|
1550
|
+
completed_at: null,
|
|
1551
|
+
cursor: input.runToken,
|
|
1552
|
+
indexed_source_count: 0,
|
|
1553
|
+
failed_source_count: 0,
|
|
1554
|
+
last_error_code: null,
|
|
1555
|
+
reconciliation_required: 1,
|
|
1556
|
+
updated_at: now,
|
|
1557
|
+
})
|
|
1558
|
+
.where("adapter_id", "=", input.adapterId)
|
|
1559
|
+
.where("scope_type", "=", input.scopeType)
|
|
1560
|
+
.where("scope_key", "=", input.scopeKey)
|
|
1561
|
+
.where("collection_id", "=", input.collectionId)
|
|
1562
|
+
.where("capture_state", "=", "active")
|
|
1563
|
+
.where(
|
|
1564
|
+
sql<boolean>`EXISTS (
|
|
1565
|
+
SELECT 1
|
|
1566
|
+
FROM _emdash_collections AS collection
|
|
1567
|
+
WHERE collection.id = ${input.collectionId}
|
|
1568
|
+
AND collection.slug = ${input.scopeKey}
|
|
1569
|
+
)`,
|
|
1570
|
+
)
|
|
1571
|
+
.where(
|
|
1572
|
+
sql<boolean>`EXISTS (
|
|
1573
|
+
SELECT 1
|
|
1574
|
+
FROM _emdash_media_usage_activation AS activation
|
|
1575
|
+
WHERE activation.task_key = 'incremental_capture'
|
|
1576
|
+
AND activation.state = 'active'
|
|
1577
|
+
)`,
|
|
1578
|
+
)
|
|
1579
|
+
.returning(["change_epoch", "started_at"])
|
|
1580
|
+
.executeTakeFirst();
|
|
1581
|
+
if (!row?.started_at) return null;
|
|
1582
|
+
return { changeEpoch: row.change_epoch, startedAt: row.started_at };
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
async finalizeIndexStatusRepairAtEpoch(
|
|
1586
|
+
input: MediaUsageIndexStatusEpochFinalizeInput,
|
|
1587
|
+
): Promise<MediaUsageGuardedIndexStatusResult> {
|
|
1588
|
+
const now = this.sortableUtcTimestamp();
|
|
1589
|
+
const updates = {
|
|
1590
|
+
status: input.status,
|
|
1591
|
+
schema_version: input.schemaVersion,
|
|
1592
|
+
completed_at: now,
|
|
1593
|
+
cursor: null,
|
|
1594
|
+
indexed_source_count: input.indexedSourceCount,
|
|
1595
|
+
failed_source_count: input.failedSourceCount,
|
|
1596
|
+
last_error_code: input.lastErrorCode,
|
|
1597
|
+
reconciliation_required: input.status === "complete" ? 0 : 1,
|
|
1598
|
+
updated_at: now,
|
|
1599
|
+
};
|
|
1600
|
+
|
|
1601
|
+
let query = this.db
|
|
1602
|
+
.updateTable("_emdash_media_usage_index_status")
|
|
1603
|
+
.set(updates)
|
|
1604
|
+
.where("adapter_id", "=", input.adapterId)
|
|
1605
|
+
.where("scope_type", "=", input.scopeType)
|
|
1606
|
+
.where("scope_key", "=", input.scopeKey)
|
|
1607
|
+
.where("collection_id", "=", input.collectionId)
|
|
1608
|
+
.where("status", "=", "running")
|
|
1609
|
+
.where("cursor", "=", input.runToken)
|
|
1610
|
+
.where("change_epoch", "=", input.startingEpoch)
|
|
1611
|
+
.where(
|
|
1612
|
+
sql<boolean>`EXISTS (
|
|
1613
|
+
SELECT 1
|
|
1614
|
+
FROM _emdash_collections AS collection
|
|
1615
|
+
WHERE collection.id = ${input.collectionId}
|
|
1616
|
+
AND collection.slug = ${input.scopeKey}
|
|
1617
|
+
)`,
|
|
1618
|
+
);
|
|
1619
|
+
if (input.status === "complete") {
|
|
1620
|
+
query = query.where(
|
|
1621
|
+
sql<boolean>`NOT EXISTS (
|
|
1622
|
+
SELECT 1
|
|
1623
|
+
FROM _emdash_media_usage_work AS work
|
|
1624
|
+
WHERE work.collection_id = ${input.collectionId}
|
|
1625
|
+
)`,
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
const result = await query.executeTakeFirst();
|
|
1629
|
+
const finalized = Number(result.numUpdatedRows ?? 0) > 0;
|
|
1630
|
+
|
|
1631
|
+
if (!finalized) {
|
|
1632
|
+
await this.db
|
|
1633
|
+
.updateTable("_emdash_media_usage_index_status")
|
|
1634
|
+
.set({
|
|
1635
|
+
status: "stale",
|
|
1636
|
+
completed_at: null,
|
|
1637
|
+
cursor: null,
|
|
1638
|
+
last_error_code: "CONTENT_USAGE_REPAIR_CONFLICT",
|
|
1639
|
+
reconciliation_required: 1,
|
|
1640
|
+
updated_at: this.sortableUtcTimestamp(),
|
|
1641
|
+
})
|
|
1642
|
+
.where("adapter_id", "=", input.adapterId)
|
|
1643
|
+
.where("scope_type", "=", input.scopeType)
|
|
1644
|
+
.where("scope_key", "=", input.scopeKey)
|
|
1645
|
+
.where("collection_id", "=", input.collectionId)
|
|
1646
|
+
.where("status", "=", "running")
|
|
1647
|
+
.where("cursor", "=", input.runToken)
|
|
1648
|
+
.execute();
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
return {
|
|
1652
|
+
finalized,
|
|
1653
|
+
status: await this.findIndexStatusForCollection(input, input.collectionId),
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
async recordIncrementalSuccess(input: MediaUsageIncrementalStatusIdentity): Promise<boolean> {
|
|
1658
|
+
const observed = await this.db
|
|
1659
|
+
.selectFrom("_emdash_media_usage_index_status")
|
|
1660
|
+
.select("change_epoch")
|
|
1661
|
+
.where("adapter_id", "=", "content-media")
|
|
1662
|
+
.where("scope_type", "=", "collection")
|
|
1663
|
+
.where("scope_key", "=", input.collectionSlug)
|
|
1664
|
+
.where("collection_id", "=", input.collectionId)
|
|
1665
|
+
.where("capture_state", "=", "active")
|
|
1666
|
+
.where(
|
|
1667
|
+
sql<boolean>`EXISTS (
|
|
1668
|
+
SELECT 1
|
|
1669
|
+
FROM _emdash_collections AS collection
|
|
1670
|
+
WHERE collection.id = ${input.collectionId}
|
|
1671
|
+
AND collection.slug = ${input.collectionSlug}
|
|
1672
|
+
)`,
|
|
1673
|
+
)
|
|
1674
|
+
.executeTakeFirst();
|
|
1675
|
+
if (!observed) return false;
|
|
1676
|
+
|
|
1677
|
+
const canComplete = sql<boolean>`(
|
|
1678
|
+
reconciliation_required = 0
|
|
1679
|
+
AND status IN ('complete', 'stale', 'partial')
|
|
1680
|
+
AND NOT EXISTS (
|
|
1681
|
+
SELECT 1
|
|
1682
|
+
FROM _emdash_media_usage_work AS work
|
|
1683
|
+
WHERE work.collection_id = ${input.collectionId}
|
|
1684
|
+
)
|
|
1685
|
+
)`;
|
|
1686
|
+
const now = this.sortableUtcTimestamp();
|
|
1687
|
+
const result = await this.db
|
|
1688
|
+
.updateTable("_emdash_media_usage_index_status")
|
|
1689
|
+
.set({
|
|
1690
|
+
status: sql<string>`CASE WHEN ${canComplete} THEN 'complete' ELSE status END`,
|
|
1691
|
+
completed_at: sql<
|
|
1692
|
+
string | null
|
|
1693
|
+
>`CASE WHEN ${canComplete} THEN ${now} ELSE completed_at END`,
|
|
1694
|
+
last_error_code: sql<
|
|
1695
|
+
string | null
|
|
1696
|
+
>`CASE WHEN ${canComplete} THEN NULL ELSE last_error_code END`,
|
|
1697
|
+
last_incremental_success_at: now,
|
|
1698
|
+
updated_at: now,
|
|
1699
|
+
})
|
|
1700
|
+
.where("adapter_id", "=", "content-media")
|
|
1701
|
+
.where("scope_type", "=", "collection")
|
|
1702
|
+
.where("scope_key", "=", input.collectionSlug)
|
|
1703
|
+
.where("collection_id", "=", input.collectionId)
|
|
1704
|
+
.where("change_epoch", "=", observed.change_epoch)
|
|
1705
|
+
.where("capture_state", "=", "active")
|
|
1706
|
+
.where(
|
|
1707
|
+
sql<boolean>`EXISTS (
|
|
1708
|
+
SELECT 1
|
|
1709
|
+
FROM _emdash_collections AS collection
|
|
1710
|
+
WHERE collection.id = ${input.collectionId}
|
|
1711
|
+
AND collection.slug = ${input.collectionSlug}
|
|
1712
|
+
)`,
|
|
1713
|
+
)
|
|
1714
|
+
.executeTakeFirst();
|
|
1715
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async recordIncrementalFailure(
|
|
1719
|
+
input: MediaUsageIncrementalStatusIdentity & {
|
|
1720
|
+
contentId: string;
|
|
1721
|
+
workVersion: number | string;
|
|
1722
|
+
errorCode: string;
|
|
1723
|
+
},
|
|
1724
|
+
): Promise<boolean> {
|
|
1725
|
+
const now = this.sortableUtcTimestamp();
|
|
1726
|
+
const automaticRunOwnsCoverage = sql<boolean>`EXISTS (
|
|
1727
|
+
SELECT 1
|
|
1728
|
+
FROM _emdash_media_usage_reconciliations AS reconciliation
|
|
1729
|
+
WHERE reconciliation.collection_id = ${input.collectionId}
|
|
1730
|
+
AND reconciliation.run_token = cursor
|
|
1731
|
+
)`;
|
|
1732
|
+
const result = await this.db
|
|
1733
|
+
.updateTable("_emdash_media_usage_index_status")
|
|
1734
|
+
.set({
|
|
1735
|
+
status: sql<string>`CASE
|
|
1736
|
+
WHEN ${automaticRunOwnsCoverage} THEN status
|
|
1737
|
+
WHEN reconciliation_required = 0 THEN 'partial'
|
|
1738
|
+
WHEN status = 'running' THEN 'stale'
|
|
1739
|
+
ELSE status
|
|
1740
|
+
END`,
|
|
1741
|
+
completed_at: sql<string | null>`CASE
|
|
1742
|
+
WHEN ${automaticRunOwnsCoverage} THEN completed_at
|
|
1743
|
+
WHEN reconciliation_required = 0 OR status = 'running' THEN NULL
|
|
1744
|
+
ELSE completed_at
|
|
1745
|
+
END`,
|
|
1746
|
+
cursor: sql<string | null>`CASE
|
|
1747
|
+
WHEN ${automaticRunOwnsCoverage} THEN cursor
|
|
1748
|
+
WHEN status = 'running' THEN NULL
|
|
1749
|
+
ELSE cursor
|
|
1750
|
+
END`,
|
|
1751
|
+
last_error_code: input.errorCode,
|
|
1752
|
+
updated_at: now,
|
|
1753
|
+
})
|
|
1754
|
+
.where("adapter_id", "=", "content-media")
|
|
1755
|
+
.where("scope_type", "=", "collection")
|
|
1756
|
+
.where("scope_key", "=", input.collectionSlug)
|
|
1757
|
+
.where("collection_id", "=", input.collectionId)
|
|
1758
|
+
.where(
|
|
1759
|
+
sql<boolean>`EXISTS (
|
|
1760
|
+
SELECT 1
|
|
1761
|
+
FROM _emdash_media_usage_work AS work
|
|
1762
|
+
WHERE work.collection_id = ${input.collectionId}
|
|
1763
|
+
AND work.content_id = ${input.contentId}
|
|
1764
|
+
AND work.work_version = ${input.workVersion}
|
|
1765
|
+
AND work.state = 'failed'
|
|
1766
|
+
AND work.last_error_code = ${input.errorCode}
|
|
1767
|
+
)`,
|
|
1768
|
+
)
|
|
1769
|
+
.where(
|
|
1770
|
+
sql<boolean>`EXISTS (
|
|
1771
|
+
SELECT 1
|
|
1772
|
+
FROM _emdash_collections AS collection
|
|
1773
|
+
WHERE collection.id = ${input.collectionId}
|
|
1774
|
+
AND collection.slug = ${input.collectionSlug}
|
|
1775
|
+
)`,
|
|
1776
|
+
)
|
|
1777
|
+
.executeTakeFirst();
|
|
1778
|
+
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1032
1781
|
async findIndexStatus(
|
|
1033
1782
|
identity: MediaUsageIndexStatusIdentity,
|
|
1034
1783
|
): Promise<MediaUsageIndexStatus | null> {
|
|
@@ -1043,16 +1792,41 @@ export class MediaUsageRepository {
|
|
|
1043
1792
|
return row ? rowToIndexStatus(row) : null;
|
|
1044
1793
|
}
|
|
1045
1794
|
|
|
1046
|
-
async
|
|
1047
|
-
|
|
1048
|
-
|
|
1795
|
+
private async findIndexStatusForCollection(
|
|
1796
|
+
identity: MediaUsageIndexStatusIdentity,
|
|
1797
|
+
collectionId: string,
|
|
1798
|
+
): Promise<MediaUsageIndexStatus | null> {
|
|
1799
|
+
const row = await this.db
|
|
1800
|
+
.selectFrom("_emdash_media_usage_index_status")
|
|
1801
|
+
.selectAll()
|
|
1049
1802
|
.where("adapter_id", "=", identity.adapterId)
|
|
1050
1803
|
.where("scope_type", "=", identity.scopeType)
|
|
1051
1804
|
.where("scope_key", "=", identity.scopeKey)
|
|
1805
|
+
.where("collection_id", "=", collectionId)
|
|
1052
1806
|
.executeTakeFirst();
|
|
1807
|
+
return row ? rowToIndexStatus(row) : null;
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
async deleteIndexStatus(
|
|
1811
|
+
identity: MediaUsageIndexStatusIdentity,
|
|
1812
|
+
collectionId?: string,
|
|
1813
|
+
): Promise<number> {
|
|
1814
|
+
let query = this.db
|
|
1815
|
+
.deleteFrom("_emdash_media_usage_index_status")
|
|
1816
|
+
.where("adapter_id", "=", identity.adapterId)
|
|
1817
|
+
.where("scope_type", "=", identity.scopeType)
|
|
1818
|
+
.where("scope_key", "=", identity.scopeKey);
|
|
1819
|
+
if (collectionId !== undefined) query = query.where("collection_id", "=", collectionId);
|
|
1820
|
+
const result = await query.executeTakeFirst();
|
|
1053
1821
|
return Number(result.numDeletedRows ?? 0);
|
|
1054
1822
|
}
|
|
1055
1823
|
|
|
1824
|
+
private sortableUtcTimestamp(): RawBuilder<string> {
|
|
1825
|
+
return isPostgres(this.db)
|
|
1826
|
+
? sql<string>`to_char(clock_timestamp() AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"')`
|
|
1827
|
+
: sql<string>`strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1056
1830
|
private async findCurrentUsagePage(
|
|
1057
1831
|
applyFilter: (
|
|
1058
1832
|
query: ReturnType<MediaUsageRepository["currentUsageBaseQuery"]>,
|
|
@@ -1103,14 +1877,253 @@ export class MediaUsageRepository {
|
|
|
1103
1877
|
.where("s.collection_slug", "is not", null)
|
|
1104
1878
|
.where("s.content_id", "is not", null)
|
|
1105
1879
|
.where("s.source_variant", "in", ["columns", "draft_overlay"])
|
|
1880
|
+
.where(contentSourceMatchesActiveCollection("s", "collection.id"))
|
|
1106
1881
|
.where(CONTENT_SOURCE_ELIGIBILITY);
|
|
1107
1882
|
}
|
|
1108
1883
|
|
|
1884
|
+
private async deleteOrphanCandidateIds(
|
|
1885
|
+
ids: readonly string[],
|
|
1886
|
+
cutoff: string,
|
|
1887
|
+
cleanupLease?: MediaUsageCleanupLease,
|
|
1888
|
+
canIssueStatement?: () => boolean,
|
|
1889
|
+
): Promise<number> {
|
|
1890
|
+
let deleted = 0;
|
|
1891
|
+
for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
|
|
1892
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
1893
|
+
if (cleanupLease) {
|
|
1894
|
+
await this.markOrphanCandidatesForCleanup(idBatch, cutoff, cleanupLease);
|
|
1895
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
1896
|
+
}
|
|
1897
|
+
let query = this.db
|
|
1898
|
+
.deleteFrom("_emdash_media_usage")
|
|
1899
|
+
.where("id", "in", idBatch)
|
|
1900
|
+
.where("created_at", "<", cutoff)
|
|
1901
|
+
.where(
|
|
1902
|
+
sql<boolean>`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources source WHERE source.source_key = _emdash_media_usage.source_key)`,
|
|
1903
|
+
)
|
|
1904
|
+
.where(this.noActiveGenerationWriteExpression());
|
|
1905
|
+
if (cleanupLease) {
|
|
1906
|
+
query = query
|
|
1907
|
+
.where("cleanup_lease_token", "=", cleanupLease.leaseToken)
|
|
1908
|
+
.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
1909
|
+
}
|
|
1910
|
+
const result = await query.executeTakeFirst();
|
|
1911
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
1912
|
+
}
|
|
1913
|
+
return deleted;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
private async deleteStaleCandidateIds(
|
|
1917
|
+
ids: readonly string[],
|
|
1918
|
+
cutoff: string,
|
|
1919
|
+
cleanupLease?: MediaUsageCleanupLease,
|
|
1920
|
+
canIssueStatement?: () => boolean,
|
|
1921
|
+
): Promise<number> {
|
|
1922
|
+
let deleted = 0;
|
|
1923
|
+
for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
|
|
1924
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
1925
|
+
if (cleanupLease) {
|
|
1926
|
+
await this.markStaleCandidatesForCleanup(idBatch, cutoff, cleanupLease);
|
|
1927
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
1928
|
+
}
|
|
1929
|
+
let query = this.db
|
|
1930
|
+
.deleteFrom("_emdash_media_usage")
|
|
1931
|
+
.where("id", "in", idBatch)
|
|
1932
|
+
.where("created_at", "<", cutoff)
|
|
1933
|
+
.where((eb) =>
|
|
1934
|
+
eb.exists(
|
|
1935
|
+
eb
|
|
1936
|
+
.selectFrom("_emdash_media_usage_sources as source")
|
|
1937
|
+
.select("source.source_key")
|
|
1938
|
+
.whereRef("source.source_key", "=", "_emdash_media_usage.source_key")
|
|
1939
|
+
.whereRef("source.current_generation", "!=", "_emdash_media_usage.generation")
|
|
1940
|
+
.whereRef("_emdash_media_usage.created_at", "<", "source.indexed_at"),
|
|
1941
|
+
),
|
|
1942
|
+
)
|
|
1943
|
+
.where(this.noActiveGenerationWriteExpression());
|
|
1944
|
+
if (cleanupLease) {
|
|
1945
|
+
query = query
|
|
1946
|
+
.where("cleanup_lease_token", "=", cleanupLease.leaseToken)
|
|
1947
|
+
.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
1948
|
+
}
|
|
1949
|
+
const result = await query.executeTakeFirst();
|
|
1950
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
1951
|
+
}
|
|
1952
|
+
return deleted;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private async deleteAbandonedCandidateIds(
|
|
1956
|
+
ids: readonly string[],
|
|
1957
|
+
cutoff: string,
|
|
1958
|
+
cleanupLease?: MediaUsageCleanupLease,
|
|
1959
|
+
canIssueStatement?: () => boolean,
|
|
1960
|
+
): Promise<number> {
|
|
1961
|
+
let deleted = 0;
|
|
1962
|
+
for (const idBatch of chunks([...ids], cleanupDeleteBatchSize(cleanupLease))) {
|
|
1963
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
1964
|
+
if (cleanupLease) {
|
|
1965
|
+
await this.markAbandonedCandidatesForCleanup(idBatch, cutoff, cleanupLease);
|
|
1966
|
+
if (!canIssueCleanupStatement(canIssueStatement)) break;
|
|
1967
|
+
}
|
|
1968
|
+
let query = this.db
|
|
1969
|
+
.deleteFrom("_emdash_media_usage")
|
|
1970
|
+
.where("id", "in", idBatch)
|
|
1971
|
+
.where("created_at", "<", cutoff)
|
|
1972
|
+
.where((eb) =>
|
|
1973
|
+
eb.exists(
|
|
1974
|
+
eb
|
|
1975
|
+
.selectFrom("_emdash_media_usage_sources as source")
|
|
1976
|
+
.select("source.source_key")
|
|
1977
|
+
.whereRef("source.source_key", "=", "_emdash_media_usage.source_key")
|
|
1978
|
+
.whereRef("source.current_generation", "!=", "_emdash_media_usage.generation")
|
|
1979
|
+
.whereRef("_emdash_media_usage.created_at", ">=", "source.indexed_at"),
|
|
1980
|
+
),
|
|
1981
|
+
)
|
|
1982
|
+
.where(this.noActiveGenerationWriteExpression());
|
|
1983
|
+
if (cleanupLease) {
|
|
1984
|
+
query = query
|
|
1985
|
+
.where("cleanup_lease_token", "=", cleanupLease.leaseToken)
|
|
1986
|
+
.where(this.activeCleanupLeaseExpression(cleanupLease));
|
|
1987
|
+
}
|
|
1988
|
+
const result = await query.executeTakeFirst();
|
|
1989
|
+
deleted += Number(result.numDeletedRows ?? 0);
|
|
1990
|
+
}
|
|
1991
|
+
return deleted;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
private async markOrphanCandidatesForCleanup(
|
|
1995
|
+
ids: readonly string[],
|
|
1996
|
+
cutoff: string,
|
|
1997
|
+
cleanupLease: MediaUsageCleanupLease,
|
|
1998
|
+
): Promise<void> {
|
|
1999
|
+
await this.db
|
|
2000
|
+
.updateTable("_emdash_media_usage")
|
|
2001
|
+
.set({ cleanup_lease_token: cleanupLease.leaseToken })
|
|
2002
|
+
.where("id", "in", ids)
|
|
2003
|
+
.where("created_at", "<", cutoff)
|
|
2004
|
+
.where(
|
|
2005
|
+
sql<boolean>`NOT EXISTS (SELECT 1 FROM _emdash_media_usage_sources source WHERE source.source_key = _emdash_media_usage.source_key)`,
|
|
2006
|
+
)
|
|
2007
|
+
.where(this.noActiveGenerationWriteExpression())
|
|
2008
|
+
.where(this.activeCleanupLeaseExpression(cleanupLease))
|
|
2009
|
+
.execute();
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
private async markStaleCandidatesForCleanup(
|
|
2013
|
+
ids: readonly string[],
|
|
2014
|
+
cutoff: string,
|
|
2015
|
+
cleanupLease: MediaUsageCleanupLease,
|
|
2016
|
+
): Promise<void> {
|
|
2017
|
+
await this.db
|
|
2018
|
+
.updateTable("_emdash_media_usage")
|
|
2019
|
+
.set({ cleanup_lease_token: cleanupLease.leaseToken })
|
|
2020
|
+
.where("id", "in", ids)
|
|
2021
|
+
.where("created_at", "<", cutoff)
|
|
2022
|
+
.where((eb) =>
|
|
2023
|
+
eb.exists(
|
|
2024
|
+
eb
|
|
2025
|
+
.selectFrom("_emdash_media_usage_sources as source")
|
|
2026
|
+
.select("source.source_key")
|
|
2027
|
+
.whereRef("source.source_key", "=", "_emdash_media_usage.source_key")
|
|
2028
|
+
.whereRef("source.current_generation", "!=", "_emdash_media_usage.generation")
|
|
2029
|
+
.whereRef("_emdash_media_usage.created_at", "<", "source.indexed_at"),
|
|
2030
|
+
),
|
|
2031
|
+
)
|
|
2032
|
+
.where(this.noActiveGenerationWriteExpression())
|
|
2033
|
+
.where(this.activeCleanupLeaseExpression(cleanupLease))
|
|
2034
|
+
.execute();
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
private async markAbandonedCandidatesForCleanup(
|
|
2038
|
+
ids: readonly string[],
|
|
2039
|
+
cutoff: string,
|
|
2040
|
+
cleanupLease: MediaUsageCleanupLease,
|
|
2041
|
+
): Promise<void> {
|
|
2042
|
+
await this.db
|
|
2043
|
+
.updateTable("_emdash_media_usage")
|
|
2044
|
+
.set({ cleanup_lease_token: cleanupLease.leaseToken })
|
|
2045
|
+
.where("id", "in", ids)
|
|
2046
|
+
.where("created_at", "<", cutoff)
|
|
2047
|
+
.where((eb) =>
|
|
2048
|
+
eb.exists(
|
|
2049
|
+
eb
|
|
2050
|
+
.selectFrom("_emdash_media_usage_sources as source")
|
|
2051
|
+
.select("source.source_key")
|
|
2052
|
+
.whereRef("source.source_key", "=", "_emdash_media_usage.source_key")
|
|
2053
|
+
.whereRef("source.current_generation", "!=", "_emdash_media_usage.generation")
|
|
2054
|
+
.whereRef("_emdash_media_usage.created_at", ">=", "source.indexed_at"),
|
|
2055
|
+
),
|
|
2056
|
+
)
|
|
2057
|
+
.where(this.noActiveGenerationWriteExpression())
|
|
2058
|
+
.where(this.activeCleanupLeaseExpression(cleanupLease))
|
|
2059
|
+
.execute();
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
private noActiveGenerationWriteExpression(usageTable = "_emdash_media_usage") {
|
|
2063
|
+
const sourceKey = sql.ref(`${usageTable}.source_key`);
|
|
2064
|
+
const generation = sql.ref(`${usageTable}.generation`);
|
|
2065
|
+
return sql<boolean>`NOT EXISTS (
|
|
2066
|
+
SELECT 1
|
|
2067
|
+
FROM _emdash_media_usage_generation_writes AS writer
|
|
2068
|
+
WHERE writer.source_key = ${sourceKey}
|
|
2069
|
+
AND writer.generation = ${generation}
|
|
2070
|
+
AND ${this.generationWriteLeaseExpiryIsInFuture("writer.expires_at")}
|
|
2071
|
+
)`;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
private activeCleanupLeaseExpression(cleanupLease: MediaUsageCleanupLease) {
|
|
2075
|
+
const rowLock = isPostgres(this.db) ? sql` FOR UPDATE` : sql``;
|
|
2076
|
+
return sql<boolean>`EXISTS (
|
|
2077
|
+
SELECT 1
|
|
2078
|
+
FROM _emdash_media_usage_cleanup AS cleanup
|
|
2079
|
+
WHERE cleanup.task_key = 'projection_gc'
|
|
2080
|
+
AND cleanup.lease_token = ${cleanupLease.leaseToken}
|
|
2081
|
+
AND ${this.cleanupLeaseExpiryIsInFuture("cleanup.lease_expires_at")}
|
|
2082
|
+
${rowLock}
|
|
2083
|
+
)`;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
private cleanupLeaseExpiryIsInFuture(column: string) {
|
|
2087
|
+
const leaseExpiresAt = sql.ref(column);
|
|
2088
|
+
return isPostgres(this.db)
|
|
2089
|
+
? sql<boolean>`${leaseExpiresAt}::timestamptz > clock_timestamp()`
|
|
2090
|
+
: sql<boolean>`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
private cleanupTimestampIsDue(column: string) {
|
|
2094
|
+
const timestamp = sql.ref(column);
|
|
2095
|
+
return isPostgres(this.db)
|
|
2096
|
+
? sql<boolean>`${timestamp}::timestamptz <= clock_timestamp()`
|
|
2097
|
+
: sql<boolean>`${timestamp} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
private cleanupTimestampOffset(offsetSeconds: number): RawBuilder<string> {
|
|
2101
|
+
if (isPostgres(this.db)) {
|
|
2102
|
+
return sql<string>`to_char(
|
|
2103
|
+
(clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
|
|
2104
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
2105
|
+
)`;
|
|
2106
|
+
}
|
|
2107
|
+
return sql<string>`strftime(
|
|
2108
|
+
'%Y-%m-%dT%H:%M:%fZ',
|
|
2109
|
+
'now',
|
|
2110
|
+
${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
|
|
2111
|
+
)`;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
private generationWriteLeaseHasExpired(column: string) {
|
|
2115
|
+
const leaseExpiresAt = sql.ref(column);
|
|
2116
|
+
return isPostgres(this.db)
|
|
2117
|
+
? sql<boolean>`${leaseExpiresAt}::timestamptz <= clock_timestamp()`
|
|
2118
|
+
: sql<boolean>`${leaseExpiresAt} <= strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
1109
2121
|
private async deleteSourceKeys(sourceKeys: readonly string[]): Promise<number> {
|
|
1110
2122
|
const uniqueSourceKeys = [...new Set(sourceKeys)];
|
|
1111
2123
|
if (uniqueSourceKeys.length === 0) return 0;
|
|
1112
2124
|
|
|
1113
2125
|
return withTransaction(this.db, async (trx) => {
|
|
2126
|
+
await this.lockCleanupBeforeSourceDelete(trx);
|
|
1114
2127
|
let deleted = 0;
|
|
1115
2128
|
for (const sourceKeyBatch of chunks(uniqueSourceKeys, SQL_BATCH_SIZE)) {
|
|
1116
2129
|
const result = await trx
|
|
@@ -1119,6 +2132,11 @@ export class MediaUsageRepository {
|
|
|
1119
2132
|
.executeTakeFirst();
|
|
1120
2133
|
deleted += Number(result.numDeletedRows ?? 0);
|
|
1121
2134
|
|
|
2135
|
+
await trx
|
|
2136
|
+
.updateTable("_emdash_media_usage")
|
|
2137
|
+
.set({ cleanup_lease_token: null })
|
|
2138
|
+
.where("source_key", "in", sourceKeyBatch)
|
|
2139
|
+
.execute();
|
|
1122
2140
|
await trx
|
|
1123
2141
|
.deleteFrom("_emdash_media_usage")
|
|
1124
2142
|
.where("source_key", "in", sourceKeyBatch)
|
|
@@ -1133,8 +2151,12 @@ export class MediaUsageRepository {
|
|
|
1133
2151
|
sourceKey: string,
|
|
1134
2152
|
generation: string,
|
|
1135
2153
|
): Promise<void> {
|
|
1136
|
-
|
|
1137
|
-
|
|
2154
|
+
await db
|
|
2155
|
+
.updateTable("_emdash_media_usage")
|
|
2156
|
+
.set({ cleanup_lease_token: null })
|
|
2157
|
+
.where("source_key", "=", sourceKey)
|
|
2158
|
+
.where("generation", "=", generation)
|
|
2159
|
+
.execute();
|
|
1138
2160
|
await db
|
|
1139
2161
|
.deleteFrom("_emdash_media_usage")
|
|
1140
2162
|
.where("source_key", "=", sourceKey)
|
|
@@ -1142,6 +2164,16 @@ export class MediaUsageRepository {
|
|
|
1142
2164
|
.execute();
|
|
1143
2165
|
}
|
|
1144
2166
|
|
|
2167
|
+
private async lockCleanupBeforeSourceDelete(db: DatabaseExecutor): Promise<void> {
|
|
2168
|
+
if (!isPostgres(this.db)) return;
|
|
2169
|
+
await sql`
|
|
2170
|
+
SELECT 1
|
|
2171
|
+
FROM _emdash_media_usage_cleanup
|
|
2172
|
+
WHERE task_key = 'projection_gc'
|
|
2173
|
+
FOR SHARE
|
|
2174
|
+
`.execute(db);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
1145
2177
|
private async insertOccurrences(
|
|
1146
2178
|
db: DatabaseExecutor,
|
|
1147
2179
|
sourceKey: string,
|
|
@@ -1172,43 +2204,252 @@ export class MediaUsageRepository {
|
|
|
1172
2204
|
}
|
|
1173
2205
|
}
|
|
1174
2206
|
|
|
2207
|
+
private async lockCanonicalSourceCollection(
|
|
2208
|
+
db: DatabaseExecutor,
|
|
2209
|
+
source: MediaUsageSourceInput,
|
|
2210
|
+
): Promise<boolean> {
|
|
2211
|
+
if (source.collectionId === undefined || source.collectionId === null) return true;
|
|
2212
|
+
if (!source.collectionSlug) return false;
|
|
2213
|
+
if (!isPostgres(this.db)) return true;
|
|
2214
|
+
const collection = await db
|
|
2215
|
+
.selectFrom("_emdash_collections")
|
|
2216
|
+
.select("id")
|
|
2217
|
+
.where("id", "=", source.collectionId)
|
|
2218
|
+
.where("slug", "=", source.collectionSlug)
|
|
2219
|
+
.forKeyShare()
|
|
2220
|
+
.executeTakeFirst();
|
|
2221
|
+
return collection !== undefined;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
1175
2224
|
private async upsertSource(
|
|
1176
2225
|
db: DatabaseExecutor,
|
|
1177
2226
|
source: MediaUsageSourceInput,
|
|
1178
2227
|
generation: string,
|
|
1179
2228
|
now: string,
|
|
1180
|
-
|
|
2229
|
+
leaseToken: string,
|
|
2230
|
+
): Promise<boolean> {
|
|
1181
2231
|
const row = this.buildSourceRow(source, generation, now);
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
2232
|
+
return this.persistSourceIfWriteLease(
|
|
2233
|
+
db,
|
|
2234
|
+
row,
|
|
2235
|
+
leaseToken,
|
|
2236
|
+
sql`
|
|
2237
|
+
ON CONFLICT (source_key) DO UPDATE SET
|
|
2238
|
+
source_type = excluded.source_type,
|
|
2239
|
+
collection_id = excluded.collection_id,
|
|
2240
|
+
collection_slug = excluded.collection_slug,
|
|
2241
|
+
content_id = excluded.content_id,
|
|
2242
|
+
source_variant = excluded.source_variant,
|
|
2243
|
+
locale = excluded.locale,
|
|
2244
|
+
translation_group = excluded.translation_group,
|
|
2245
|
+
content_slug = excluded.content_slug,
|
|
2246
|
+
content_title = excluded.content_title,
|
|
2247
|
+
content_status = excluded.content_status,
|
|
2248
|
+
content_scheduled_at = excluded.content_scheduled_at,
|
|
2249
|
+
content_deleted_at = excluded.content_deleted_at,
|
|
2250
|
+
revision_id = excluded.revision_id,
|
|
2251
|
+
current_generation = excluded.current_generation,
|
|
2252
|
+
schema_version = excluded.schema_version,
|
|
2253
|
+
source_updated_at = excluded.source_updated_at,
|
|
2254
|
+
source_version = excluded.source_version,
|
|
2255
|
+
source_fingerprint = excluded.source_fingerprint,
|
|
2256
|
+
identity_version = excluded.identity_version,
|
|
2257
|
+
source_completeness = excluded.source_completeness,
|
|
2258
|
+
last_attempted_at = excluded.last_attempted_at,
|
|
2259
|
+
last_error_code = excluded.last_error_code,
|
|
2260
|
+
indexed_at = excluded.indexed_at,
|
|
2261
|
+
updated_at = excluded.updated_at
|
|
2262
|
+
`,
|
|
2263
|
+
);
|
|
1188
2264
|
}
|
|
1189
2265
|
|
|
1190
2266
|
private async insertSourceIfAbsent(
|
|
1191
2267
|
db: DatabaseExecutor,
|
|
1192
|
-
row:
|
|
2268
|
+
row: ReturnType<MediaUsageRepository["buildSourceRow"]>,
|
|
2269
|
+
leaseToken: string,
|
|
1193
2270
|
): Promise<boolean> {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
2271
|
+
return this.persistSourceIfWriteLease(
|
|
2272
|
+
db,
|
|
2273
|
+
row,
|
|
2274
|
+
leaseToken,
|
|
2275
|
+
sql`ON CONFLICT (source_key) DO NOTHING`,
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
|
|
2279
|
+
private async persistSourceIfWriteLease(
|
|
2280
|
+
db: DatabaseExecutor,
|
|
2281
|
+
row:
|
|
2282
|
+
| ReturnType<MediaUsageRepository["buildSourceRow"]>
|
|
2283
|
+
| ReturnType<MediaUsageRepository["buildAttemptedSourceRow"]>,
|
|
2284
|
+
leaseToken: string,
|
|
2285
|
+
conflict: RawBuilder<unknown>,
|
|
2286
|
+
): Promise<boolean> {
|
|
2287
|
+
const result = await sql`
|
|
2288
|
+
INSERT INTO _emdash_media_usage_sources (
|
|
2289
|
+
source_key,
|
|
2290
|
+
source_type,
|
|
2291
|
+
collection_id,
|
|
2292
|
+
collection_slug,
|
|
2293
|
+
content_id,
|
|
2294
|
+
source_variant,
|
|
2295
|
+
locale,
|
|
2296
|
+
translation_group,
|
|
2297
|
+
content_slug,
|
|
2298
|
+
content_title,
|
|
2299
|
+
content_status,
|
|
2300
|
+
content_scheduled_at,
|
|
2301
|
+
content_deleted_at,
|
|
2302
|
+
revision_id,
|
|
2303
|
+
current_generation,
|
|
2304
|
+
schema_version,
|
|
2305
|
+
source_updated_at,
|
|
2306
|
+
source_version,
|
|
2307
|
+
source_fingerprint,
|
|
2308
|
+
identity_version,
|
|
2309
|
+
source_completeness,
|
|
2310
|
+
last_attempted_at,
|
|
2311
|
+
last_error_code,
|
|
2312
|
+
indexed_at,
|
|
2313
|
+
updated_at
|
|
2314
|
+
)
|
|
2315
|
+
SELECT
|
|
2316
|
+
${row.source_key},
|
|
2317
|
+
${row.source_type},
|
|
2318
|
+
${row.collection_id},
|
|
2319
|
+
${row.collection_slug},
|
|
2320
|
+
${row.content_id},
|
|
2321
|
+
${row.source_variant},
|
|
2322
|
+
${row.locale},
|
|
2323
|
+
${row.translation_group},
|
|
2324
|
+
${row.content_slug},
|
|
2325
|
+
${row.content_title},
|
|
2326
|
+
${row.content_status},
|
|
2327
|
+
${row.content_scheduled_at},
|
|
2328
|
+
${row.content_deleted_at},
|
|
2329
|
+
${row.revision_id},
|
|
2330
|
+
${row.current_generation},
|
|
2331
|
+
${row.schema_version},
|
|
2332
|
+
${row.source_updated_at},
|
|
2333
|
+
${row.source_version},
|
|
2334
|
+
${row.source_fingerprint},
|
|
2335
|
+
${row.identity_version},
|
|
2336
|
+
${row.source_completeness},
|
|
2337
|
+
${row.last_attempted_at},
|
|
2338
|
+
${row.last_error_code},
|
|
2339
|
+
${row.indexed_at},
|
|
2340
|
+
${row.updated_at}
|
|
2341
|
+
WHERE EXISTS (
|
|
2342
|
+
SELECT 1
|
|
2343
|
+
FROM _emdash_media_usage_generation_writes
|
|
2344
|
+
WHERE source_key = ${row.source_key}
|
|
2345
|
+
AND generation = ${row.current_generation}
|
|
2346
|
+
AND lease_token = ${leaseToken}
|
|
2347
|
+
AND ${this.generationWriteLeaseExpiryIsInFuture("expires_at")}
|
|
2348
|
+
)
|
|
2349
|
+
AND ${this.currentCollectionExists(row.collection_id, row.collection_slug)}
|
|
2350
|
+
AND ${this.currentCanonicalContentExists(row)}
|
|
2351
|
+
${conflict}
|
|
2352
|
+
`.execute(db);
|
|
2353
|
+
return Number(result.numAffectedRows ?? 0) > 0;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
private generationWriteLeaseExpression(
|
|
2357
|
+
row: ReturnType<MediaUsageRepository["buildSourceRow"]>,
|
|
2358
|
+
leaseToken: string,
|
|
2359
|
+
) {
|
|
2360
|
+
return (eb: ExpressionBuilder<Database, "_emdash_media_usage_sources">) =>
|
|
2361
|
+
eb.exists(
|
|
2362
|
+
eb
|
|
2363
|
+
.selectFrom("_emdash_media_usage_generation_writes")
|
|
2364
|
+
.select("source_key")
|
|
2365
|
+
.where("source_key", "=", row.source_key)
|
|
2366
|
+
.where("generation", "=", row.current_generation)
|
|
2367
|
+
.where("lease_token", "=", leaseToken)
|
|
2368
|
+
.where(
|
|
2369
|
+
this.generationWriteLeaseExpiryIsInFuture(
|
|
2370
|
+
"_emdash_media_usage_generation_writes.expires_at",
|
|
2371
|
+
),
|
|
2372
|
+
),
|
|
2373
|
+
);
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
private generationWriteLeaseExpiryIsInFuture(column: string) {
|
|
2377
|
+
const leaseExpiresAt = sql.ref(column);
|
|
2378
|
+
return isPostgres(this.db)
|
|
2379
|
+
? sql<boolean>`${leaseExpiresAt}::timestamptz > clock_timestamp()`
|
|
2380
|
+
: sql<boolean>`${leaseExpiresAt} > strftime('%Y-%m-%dT%H:%M:%fZ', 'now')`;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
private async withGenerationWriteLease(
|
|
2384
|
+
source: Pick<MediaUsageSourceInput, "sourceKey" | "collectionId" | "collectionSlug">,
|
|
2385
|
+
generation: string,
|
|
2386
|
+
write: (leaseToken: string, startedAt: string) => Promise<void>,
|
|
2387
|
+
): Promise<boolean> {
|
|
2388
|
+
const leaseToken = ulid();
|
|
2389
|
+
const lease = await sql<{ created_at: string }>`
|
|
2390
|
+
INSERT INTO _emdash_media_usage_generation_writes (
|
|
2391
|
+
source_key, generation, lease_token, expires_at, created_at
|
|
2392
|
+
)
|
|
2393
|
+
SELECT
|
|
2394
|
+
${source.sourceKey},
|
|
2395
|
+
${generation},
|
|
2396
|
+
${leaseToken},
|
|
2397
|
+
${this.generationWriteLeaseTimestampOffset(MEDIA_USAGE_GENERATION_WRITE_LEASE_MS / 1000)},
|
|
2398
|
+
${this.generationWriteLeaseTimestampOffset(0)}
|
|
2399
|
+
WHERE ${this.currentCollectionExists(
|
|
2400
|
+
source.collectionId ?? null,
|
|
2401
|
+
source.collectionSlug ?? null,
|
|
2402
|
+
)}
|
|
2403
|
+
RETURNING created_at
|
|
2404
|
+
`.execute(this.db);
|
|
2405
|
+
const owner = lease.rows[0];
|
|
2406
|
+
if (!owner) return false;
|
|
2407
|
+
|
|
2408
|
+
try {
|
|
2409
|
+
await write(leaseToken, owner.created_at);
|
|
2410
|
+
return true;
|
|
2411
|
+
} finally {
|
|
2412
|
+
try {
|
|
2413
|
+
await this.db
|
|
2414
|
+
.deleteFrom("_emdash_media_usage_generation_writes")
|
|
2415
|
+
.where("source_key", "=", source.sourceKey)
|
|
2416
|
+
.where("generation", "=", generation)
|
|
2417
|
+
.where("lease_token", "=", leaseToken)
|
|
2418
|
+
.execute();
|
|
2419
|
+
} catch (error) {
|
|
2420
|
+
console.error("[media-usage] Failed to release generation write lease:", error);
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
private generationWriteLeaseTimestampOffset(offsetSeconds: number): RawBuilder<string> {
|
|
2426
|
+
if (isPostgres(this.db)) {
|
|
2427
|
+
return sql<string>`to_char(
|
|
2428
|
+
(clock_timestamp() AT TIME ZONE 'UTC') + (${offsetSeconds} * INTERVAL '1 second'),
|
|
2429
|
+
'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
|
|
2430
|
+
)`;
|
|
2431
|
+
}
|
|
2432
|
+
return sql<string>`strftime(
|
|
2433
|
+
'%Y-%m-%dT%H:%M:%fZ',
|
|
2434
|
+
'now',
|
|
2435
|
+
${`${offsetSeconds >= 0 ? "+" : ""}${offsetSeconds} seconds`}
|
|
2436
|
+
)`;
|
|
1200
2437
|
}
|
|
1201
2438
|
|
|
1202
2439
|
private async updateSourceIfGeneration(
|
|
1203
2440
|
db: DatabaseExecutor,
|
|
1204
2441
|
row: ReturnType<MediaUsageRepository["buildSourceRow"]>,
|
|
1205
2442
|
expectedCurrentGeneration: string,
|
|
2443
|
+
leaseToken: string,
|
|
1206
2444
|
): Promise<boolean> {
|
|
1207
2445
|
const result = await db
|
|
1208
2446
|
.updateTable("_emdash_media_usage_sources")
|
|
1209
2447
|
.set(this.sourceUpdateSet(row))
|
|
1210
2448
|
.where("source_key", "=", row.source_key)
|
|
1211
2449
|
.where("current_generation", "=", expectedCurrentGeneration)
|
|
2450
|
+
.where(this.generationWriteLeaseExpression(row, leaseToken))
|
|
2451
|
+
.where(this.currentCollectionExists(row.collection_id, row.collection_slug))
|
|
2452
|
+
.where(this.currentCanonicalContentExists(row))
|
|
1212
2453
|
.executeTakeFirst();
|
|
1213
2454
|
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1214
2455
|
}
|
|
@@ -1217,12 +2458,16 @@ export class MediaUsageRepository {
|
|
|
1217
2458
|
db: DatabaseExecutor,
|
|
1218
2459
|
row: ReturnType<MediaUsageRepository["buildSourceRow"]>,
|
|
1219
2460
|
expectedSource: MediaUsageSource,
|
|
2461
|
+
leaseToken: string,
|
|
1220
2462
|
): Promise<boolean> {
|
|
1221
2463
|
const result = await db
|
|
1222
2464
|
.updateTable("_emdash_media_usage_sources")
|
|
1223
2465
|
.set(this.sourceUpdateSet(row))
|
|
1224
2466
|
.where("source_key", "=", row.source_key)
|
|
1225
2467
|
.where(this.sourceMatchExpression(expectedSource))
|
|
2468
|
+
.where(this.generationWriteLeaseExpression(row, leaseToken))
|
|
2469
|
+
.where(this.currentCollectionExists(row.collection_id, row.collection_slug))
|
|
2470
|
+
.where(this.currentCanonicalContentExists(row))
|
|
1226
2471
|
.executeTakeFirst();
|
|
1227
2472
|
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1228
2473
|
}
|
|
@@ -1238,6 +2483,8 @@ export class MediaUsageRepository {
|
|
|
1238
2483
|
.set(this.attemptedSourceUpdateSet(source, row))
|
|
1239
2484
|
.where("source_key", "=", row.source_key)
|
|
1240
2485
|
.where(this.sourceMatchExpression(expectedSource))
|
|
2486
|
+
.where(this.currentCollectionExists(row.collection_id, row.collection_slug))
|
|
2487
|
+
.where(this.currentCanonicalContentExists(row))
|
|
1241
2488
|
.executeTakeFirst();
|
|
1242
2489
|
return Number(result.numUpdatedRows ?? 0) > 0;
|
|
1243
2490
|
}
|
|
@@ -1247,16 +2494,64 @@ export class MediaUsageRepository {
|
|
|
1247
2494
|
eb.and([
|
|
1248
2495
|
eb("current_generation", "=", expectedSource.currentGeneration),
|
|
1249
2496
|
eb("source_completeness", "=", expectedSource.sourceCompleteness),
|
|
2497
|
+
this.nullableStringExpression(eb, "collection_id", expectedSource.collectionId),
|
|
1250
2498
|
this.nullableStringExpression(eb, "updated_at", expectedSource.updatedAt),
|
|
1251
2499
|
this.nullableStringExpression(eb, "source_fingerprint", expectedSource.sourceFingerprint),
|
|
1252
2500
|
this.nullableStringExpression(eb, "source_updated_at", expectedSource.sourceUpdatedAt),
|
|
1253
2501
|
this.nullableNumberExpression(eb, "source_version", expectedSource.sourceVersion),
|
|
2502
|
+
this.nullableNumberExpression(eb, "identity_version", expectedSource.identityVersion),
|
|
1254
2503
|
this.nullableStringExpression(eb, "revision_id", expectedSource.revisionId),
|
|
1255
2504
|
this.nullableStringExpression(eb, "last_attempted_at", expectedSource.lastAttemptedAt),
|
|
1256
2505
|
this.nullableStringExpression(eb, "last_error_code", expectedSource.lastErrorCode),
|
|
1257
2506
|
]);
|
|
1258
2507
|
}
|
|
1259
2508
|
|
|
2509
|
+
private async projectionMatchesCurrentGeneration(
|
|
2510
|
+
source: MediaUsageSourceInput,
|
|
2511
|
+
expectedCurrentGeneration: string,
|
|
2512
|
+
): Promise<boolean> {
|
|
2513
|
+
const fingerprint = source.sourceFingerprint;
|
|
2514
|
+
if (!isMediaUsageProjectionFingerprint(fingerprint)) return false;
|
|
2515
|
+
const row = await this.db
|
|
2516
|
+
.selectFrom("_emdash_media_usage_sources")
|
|
2517
|
+
.select("source_key")
|
|
2518
|
+
.where("source_key", "=", source.sourceKey)
|
|
2519
|
+
.where("current_generation", "=", expectedCurrentGeneration)
|
|
2520
|
+
.where("source_fingerprint", "=", fingerprint!)
|
|
2521
|
+
.where("source_completeness", "=", source.sourceCompleteness ?? "complete")
|
|
2522
|
+
.where("last_error_code", "is", null)
|
|
2523
|
+
.where(
|
|
2524
|
+
this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null),
|
|
2525
|
+
)
|
|
2526
|
+
.executeTakeFirst();
|
|
2527
|
+
return row !== undefined;
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
async projectionMatchesExpectedSource(
|
|
2531
|
+
source: MediaUsageSourceInput,
|
|
2532
|
+
expectedSource: MediaUsageSource,
|
|
2533
|
+
): Promise<boolean> {
|
|
2534
|
+
const fingerprint = source.sourceFingerprint;
|
|
2535
|
+
if (
|
|
2536
|
+
!isMediaUsageProjectionFingerprint(fingerprint) ||
|
|
2537
|
+
expectedSource.sourceFingerprint !== fingerprint ||
|
|
2538
|
+
expectedSource.sourceCompleteness !== (source.sourceCompleteness ?? "complete") ||
|
|
2539
|
+
expectedSource.lastErrorCode !== null
|
|
2540
|
+
) {
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
const row = await this.db
|
|
2544
|
+
.selectFrom("_emdash_media_usage_sources")
|
|
2545
|
+
.select("source_key")
|
|
2546
|
+
.where("source_key", "=", source.sourceKey)
|
|
2547
|
+
.where(this.sourceMatchExpression(expectedSource))
|
|
2548
|
+
.where(
|
|
2549
|
+
this.currentCollectionExists(source.collectionId ?? null, source.collectionSlug ?? null),
|
|
2550
|
+
)
|
|
2551
|
+
.executeTakeFirst();
|
|
2552
|
+
return row !== undefined;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
1260
2555
|
private nullableStringExpression(
|
|
1261
2556
|
eb: ExpressionBuilder<Database, "_emdash_media_usage_sources">,
|
|
1262
2557
|
column: MediaUsageSourceNullableStringColumn,
|
|
@@ -1265,9 +2560,63 @@ export class MediaUsageRepository {
|
|
|
1265
2560
|
return value === null ? eb(column, "is", null) : eb(column, "=", value);
|
|
1266
2561
|
}
|
|
1267
2562
|
|
|
2563
|
+
private currentCollectionExists(
|
|
2564
|
+
collectionId: string | null,
|
|
2565
|
+
collectionSlug: string | null,
|
|
2566
|
+
): RawBuilder<boolean> {
|
|
2567
|
+
if (collectionId === null) return sql<boolean>`1 = 1`;
|
|
2568
|
+
return sql<boolean>`EXISTS (
|
|
2569
|
+
SELECT 1
|
|
2570
|
+
FROM _emdash_collections
|
|
2571
|
+
WHERE id = ${collectionId}
|
|
2572
|
+
AND slug = ${collectionSlug}
|
|
2573
|
+
)`;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
private currentCanonicalContentExists(
|
|
2577
|
+
row:
|
|
2578
|
+
| ReturnType<MediaUsageRepository["buildSourceRow"]>
|
|
2579
|
+
| ReturnType<MediaUsageRepository["buildAttemptedSourceRow"]>,
|
|
2580
|
+
): RawBuilder<boolean> {
|
|
2581
|
+
if (row.collection_id === null || row.identity_version !== 1 || row.source_type !== "content") {
|
|
2582
|
+
return sql<boolean>`1 = 1`;
|
|
2583
|
+
}
|
|
2584
|
+
if (
|
|
2585
|
+
!row.collection_slug ||
|
|
2586
|
+
!row.content_id ||
|
|
2587
|
+
row.source_version === null ||
|
|
2588
|
+
row.source_updated_at === null
|
|
2589
|
+
) {
|
|
2590
|
+
return sql<boolean>`1 = 0`;
|
|
2591
|
+
}
|
|
2592
|
+
validateIdentifier(row.collection_slug, "collection slug");
|
|
2593
|
+
const tableName = `ec_${row.collection_slug}`;
|
|
2594
|
+
validateIdentifier(tableName, "content table");
|
|
2595
|
+
const revisionColumn =
|
|
2596
|
+
row.source_variant === "columns"
|
|
2597
|
+
? "live_revision_id"
|
|
2598
|
+
: row.source_variant === "draft_overlay"
|
|
2599
|
+
? "draft_revision_id"
|
|
2600
|
+
: null;
|
|
2601
|
+
if (!revisionColumn) return sql<boolean>`1 = 0`;
|
|
2602
|
+
const revision = sql.ref(`content.${revisionColumn}`);
|
|
2603
|
+
const revisionMatches =
|
|
2604
|
+
row.revision_id === null
|
|
2605
|
+
? sql<boolean>`${revision} IS NULL`
|
|
2606
|
+
: sql<boolean>`${revision} = ${row.revision_id}`;
|
|
2607
|
+
return sql<boolean>`EXISTS (
|
|
2608
|
+
SELECT 1
|
|
2609
|
+
FROM ${sql.ref(tableName)} AS content
|
|
2610
|
+
WHERE content.id = ${row.content_id}
|
|
2611
|
+
AND content.version = ${row.source_version}
|
|
2612
|
+
AND content.updated_at = ${row.source_updated_at}
|
|
2613
|
+
AND ${revisionMatches}
|
|
2614
|
+
)`;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
1268
2617
|
private nullableNumberExpression(
|
|
1269
2618
|
eb: ExpressionBuilder<Database, "_emdash_media_usage_sources">,
|
|
1270
|
-
column: "source_version",
|
|
2619
|
+
column: "source_version" | "identity_version",
|
|
1271
2620
|
value: number | null,
|
|
1272
2621
|
) {
|
|
1273
2622
|
return value === null ? eb(column, "is", null) : eb(column, "=", value);
|
|
@@ -1287,6 +2636,7 @@ export class MediaUsageRepository {
|
|
|
1287
2636
|
return {
|
|
1288
2637
|
source_key: source.sourceKey,
|
|
1289
2638
|
source_type: source.sourceType,
|
|
2639
|
+
collection_id: source.collectionId ?? null,
|
|
1290
2640
|
collection_slug: source.collectionSlug ?? null,
|
|
1291
2641
|
content_id: source.contentId ?? null,
|
|
1292
2642
|
source_variant: source.sourceVariant,
|
|
@@ -1303,6 +2653,7 @@ export class MediaUsageRepository {
|
|
|
1303
2653
|
source_updated_at: source.sourceUpdatedAt ?? null,
|
|
1304
2654
|
source_version: source.sourceVersion ?? null,
|
|
1305
2655
|
source_fingerprint: source.sourceFingerprint ?? null,
|
|
2656
|
+
identity_version: source.identityVersion ?? null,
|
|
1306
2657
|
// Complete means this source was fully refreshed for the extractor's current
|
|
1307
2658
|
// schema/version coverage, not that every possible reference shape is known.
|
|
1308
2659
|
source_completeness: source.sourceCompleteness ?? "complete",
|
|
@@ -1313,10 +2664,11 @@ export class MediaUsageRepository {
|
|
|
1313
2664
|
};
|
|
1314
2665
|
}
|
|
1315
2666
|
|
|
1316
|
-
private buildAttemptedSourceRow(source: MediaUsageSourceInput, now: string) {
|
|
2667
|
+
private buildAttemptedSourceRow(source: MediaUsageSourceInput, generation: string, now: string) {
|
|
1317
2668
|
return {
|
|
1318
2669
|
source_key: source.sourceKey,
|
|
1319
2670
|
source_type: source.sourceType,
|
|
2671
|
+
collection_id: source.collectionId ?? null,
|
|
1320
2672
|
collection_slug: source.collectionSlug ?? null,
|
|
1321
2673
|
content_id: source.contentId ?? null,
|
|
1322
2674
|
source_variant: source.sourceVariant,
|
|
@@ -1328,11 +2680,12 @@ export class MediaUsageRepository {
|
|
|
1328
2680
|
content_scheduled_at: source.contentScheduledAt ?? null,
|
|
1329
2681
|
content_deleted_at: source.contentDeletedAt ?? null,
|
|
1330
2682
|
revision_id: source.revisionId ?? null,
|
|
1331
|
-
current_generation:
|
|
2683
|
+
current_generation: generation,
|
|
1332
2684
|
schema_version: source.schemaVersion ?? 1,
|
|
1333
2685
|
source_updated_at: source.sourceUpdatedAt ?? null,
|
|
1334
2686
|
source_version: source.sourceVersion ?? null,
|
|
1335
2687
|
source_fingerprint: source.sourceFingerprint ?? null,
|
|
2688
|
+
identity_version: source.identityVersion ?? null,
|
|
1336
2689
|
source_completeness:
|
|
1337
2690
|
source.sourceCompleteness ?? (source.lastErrorCode ? "failed" : "unknown"),
|
|
1338
2691
|
last_attempted_at: source.lastAttemptedAt ?? now,
|
|
@@ -1356,6 +2709,7 @@ export class MediaUsageRepository {
|
|
|
1356
2709
|
};
|
|
1357
2710
|
|
|
1358
2711
|
if (source.collectionSlug !== undefined) updates.collection_slug = row.collection_slug;
|
|
2712
|
+
if (source.collectionId !== undefined) updates.collection_id = row.collection_id;
|
|
1359
2713
|
if (source.contentId !== undefined) updates.content_id = row.content_id;
|
|
1360
2714
|
if (source.locale !== undefined) updates.locale = row.locale;
|
|
1361
2715
|
if (source.translationGroup !== undefined) updates.translation_group = row.translation_group;
|
|
@@ -1373,6 +2727,7 @@ export class MediaUsageRepository {
|
|
|
1373
2727
|
if (source.sourceFingerprint !== undefined) {
|
|
1374
2728
|
updates.source_fingerprint = row.source_fingerprint;
|
|
1375
2729
|
}
|
|
2730
|
+
if (source.identityVersion !== undefined) updates.identity_version = row.identity_version;
|
|
1376
2731
|
|
|
1377
2732
|
return updates;
|
|
1378
2733
|
}
|
|
@@ -1382,6 +2737,7 @@ export class MediaUsageRepository {
|
|
|
1382
2737
|
): Updateable<MediaUsageSourceTable> {
|
|
1383
2738
|
return {
|
|
1384
2739
|
source_type: row.source_type,
|
|
2740
|
+
collection_id: row.collection_id,
|
|
1385
2741
|
collection_slug: row.collection_slug,
|
|
1386
2742
|
content_id: row.content_id,
|
|
1387
2743
|
source_variant: row.source_variant,
|
|
@@ -1398,6 +2754,7 @@ export class MediaUsageRepository {
|
|
|
1398
2754
|
source_updated_at: row.source_updated_at,
|
|
1399
2755
|
source_version: row.source_version,
|
|
1400
2756
|
source_fingerprint: row.source_fingerprint,
|
|
2757
|
+
identity_version: row.identity_version,
|
|
1401
2758
|
source_completeness: row.source_completeness,
|
|
1402
2759
|
last_attempted_at: row.last_attempted_at,
|
|
1403
2760
|
last_error_code: row.last_error_code,
|
|
@@ -1410,6 +2767,7 @@ export class MediaUsageRepository {
|
|
|
1410
2767
|
const currentUsageSelect = [
|
|
1411
2768
|
"s.source_key as source_key",
|
|
1412
2769
|
"s.source_type as source_type",
|
|
2770
|
+
"s.collection_id as collection_id",
|
|
1413
2771
|
"s.collection_slug as collection_slug",
|
|
1414
2772
|
"s.content_id as content_id",
|
|
1415
2773
|
"s.source_variant as source_variant",
|
|
@@ -1426,6 +2784,7 @@ const currentUsageSelect = [
|
|
|
1426
2784
|
"s.source_updated_at as source_updated_at",
|
|
1427
2785
|
"s.source_version as source_version",
|
|
1428
2786
|
"s.source_fingerprint as source_fingerprint",
|
|
2787
|
+
"s.identity_version as identity_version",
|
|
1429
2788
|
"s.source_completeness as source_completeness",
|
|
1430
2789
|
"s.last_attempted_at as last_attempted_at",
|
|
1431
2790
|
"s.last_error_code as last_error_code",
|
|
@@ -1482,6 +2841,7 @@ function rowToSource(row: MediaUsageSourceRow): MediaUsageSource {
|
|
|
1482
2841
|
return {
|
|
1483
2842
|
sourceKey: row.source_key,
|
|
1484
2843
|
sourceType: row.source_type,
|
|
2844
|
+
collectionId: row.collection_id,
|
|
1485
2845
|
collectionSlug: row.collection_slug,
|
|
1486
2846
|
contentId: row.content_id,
|
|
1487
2847
|
sourceVariant: row.source_variant,
|
|
@@ -1498,6 +2858,7 @@ function rowToSource(row: MediaUsageSourceRow): MediaUsageSource {
|
|
|
1498
2858
|
sourceUpdatedAt: row.source_updated_at,
|
|
1499
2859
|
sourceVersion: row.source_version === null ? null : Number(row.source_version),
|
|
1500
2860
|
sourceFingerprint: row.source_fingerprint,
|
|
2861
|
+
identityVersion: row.identity_version === null ? null : Number(row.identity_version),
|
|
1501
2862
|
sourceCompleteness: row.source_completeness,
|
|
1502
2863
|
lastAttemptedAt: row.last_attempted_at,
|
|
1503
2864
|
lastErrorCode: row.last_error_code,
|
|
@@ -1530,6 +2891,7 @@ function rowToUsageRecord(row: JoinedUsageRow): MediaUsageRecord {
|
|
|
1530
2891
|
source: rowToSource({
|
|
1531
2892
|
source_key: row.source_key,
|
|
1532
2893
|
source_type: row.source_type,
|
|
2894
|
+
collection_id: row.collection_id,
|
|
1533
2895
|
collection_slug: row.collection_slug,
|
|
1534
2896
|
content_id: row.content_id,
|
|
1535
2897
|
source_variant: row.source_variant,
|
|
@@ -1546,6 +2908,7 @@ function rowToUsageRecord(row: JoinedUsageRow): MediaUsageRecord {
|
|
|
1546
2908
|
source_updated_at: row.source_updated_at,
|
|
1547
2909
|
source_version: row.source_version,
|
|
1548
2910
|
source_fingerprint: row.source_fingerprint,
|
|
2911
|
+
identity_version: row.identity_version,
|
|
1549
2912
|
source_completeness: row.source_completeness,
|
|
1550
2913
|
last_attempted_at: row.last_attempted_at,
|
|
1551
2914
|
last_error_code: row.last_error_code,
|
|
@@ -1567,6 +2930,7 @@ function rowToUsageRecord(row: JoinedUsageRow): MediaUsageRecord {
|
|
|
1567
2930
|
media_kind: row.media_kind,
|
|
1568
2931
|
mime_type: row.mime_type,
|
|
1569
2932
|
created_at: row.occurrence_created_at,
|
|
2933
|
+
cleanup_lease_token: null,
|
|
1570
2934
|
}),
|
|
1571
2935
|
};
|
|
1572
2936
|
}
|