emdash 0.32.0 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapters-C0ONzkUP.d.mts → adapters-34EN2eXO.d.mts} +34 -2
- package/dist/adapters-34EN2eXO.d.mts.map +1 -0
- package/dist/after-CBU2vkoO.mjs +96 -0
- package/dist/after-CBU2vkoO.mjs.map +1 -0
- package/dist/{allowed-origins-CCEi9bPI.mjs → allowed-origins-DW7sYyqg.mjs} +2 -2
- package/dist/{allowed-origins-CCEi9bPI.mjs.map → allowed-origins-DW7sYyqg.mjs.map} +1 -1
- package/dist/api/route-utils.d.mts +3 -4
- package/dist/api/route-utils.d.mts.map +1 -1
- package/dist/api/route-utils.mjs +26 -25
- package/dist/api/route-utils.mjs.map +1 -1
- package/dist/api/schemas/index.d.mts +2 -2
- package/dist/api/schemas/index.mjs +6 -5
- package/dist/{api-DgPbMlng.mjs → api-DdAsirBe.mjs} +321 -428
- package/dist/api-DdAsirBe.mjs.map +1 -0
- package/dist/{api-tokens-p-iMzvXR.mjs → api-tokens-CFXu8F_2.mjs} +2 -2
- package/dist/{api-tokens-p-iMzvXR.mjs.map → api-tokens-CFXu8F_2.mjs.map} +1 -1
- package/dist/{apply-1_6ra7NP.mjs → apply-D5zfeD09.mjs} +139 -102
- package/dist/apply-D5zfeD09.mjs.map +1 -0
- package/dist/astro/index.d.mts +12 -12
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +103 -35
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +10 -10
- package/dist/astro/middleware/auth.mjs +9 -9
- package/dist/astro/middleware/media-usage-write-fence.d.mts +7 -0
- package/dist/astro/middleware/media-usage-write-fence.d.mts.map +1 -0
- package/dist/astro/middleware/media-usage-write-fence.mjs +35 -0
- package/dist/astro/middleware/media-usage-write-fence.mjs.map +1 -0
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +13 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -5
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts +12 -11
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +1915 -223
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs +9 -8
- package/dist/astro/routes/api/admin/allowed-domains/_domain_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs +9 -8
- package/dist/astro/routes/api/admin/allowed-domains/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/api-tokens/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/api-tokens/index.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_/usage.mjs +6 -6
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/index.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs +12 -11
- package/dist/astro/routes/api/admin/byline-fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/admin/bylines/_id_/index.mjs +20 -20
- package/dist/astro/routes/api/admin/bylines/_id_/translations.mjs +20 -20
- package/dist/astro/routes/api/admin/bylines/index.mjs +20 -20
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs +18 -17
- package/dist/astro/routes/api/admin/comments/_id_/status.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/_id_.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/bulk.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/bulk.mjs.map +1 -1
- package/dist/astro/routes/api/admin/comments/counts.mjs +8 -8
- package/dist/astro/routes/api/admin/comments/index.mjs +14 -13
- package/dist/astro/routes/api/admin/comments/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/hooks/exclusive/_hookName_.mjs +6 -6
- package/dist/astro/routes/api/admin/hooks/exclusive/index.mjs +5 -5
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs +36 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs +36 -0
- package/dist/astro/routes/api/admin/media-usage/collection-deletions/retry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +15 -14
- package/dist/astro/routes/api/admin/media-usage/repair.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/work/index.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs +37 -0
- package/dist/astro/routes/api/admin/media-usage/work/index.mjs.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts +8 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.d.mts.map +1 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs +37 -0
- package/dist/astro/routes/api/admin/media-usage/work/retry.mjs.map +1 -0
- package/dist/astro/routes/api/admin/oauth-clients/_id_.mjs +5 -5
- package/dist/astro/routes/api/admin/oauth-clients/index.mjs +5 -5
- package/dist/astro/routes/api/admin/plugins/_id_/disable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/_id_/disable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/_id_/enable.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/mcp.mjs +6 -6
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/icon.mjs +4 -4
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +55 -46
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/registry/artifact.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.d.mts.map +1 -1
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +56 -47
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs.map +1 -1
- package/dist/astro/routes/api/admin/plugins/updates.mjs +52 -46
- package/dist/astro/routes/api/admin/plugins/updates.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/thumbnail.mjs +4 -4
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +52 -46
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/disable.mjs +4 -4
- package/dist/astro/routes/api/admin/users/_id_/enable.mjs +3 -3
- package/dist/astro/routes/api/admin/users/_id_/index.mjs +10 -9
- package/dist/astro/routes/api/admin/users/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs +6 -6
- package/dist/astro/routes/api/admin/users/_id_/send-recovery.mjs.map +1 -1
- package/dist/astro/routes/api/admin/users/index.mjs +9 -8
- package/dist/astro/routes/api/admin/users/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/dev-bypass.mjs +8 -8
- package/dist/astro/routes/api/auth/invite/accept.mjs +3 -3
- package/dist/astro/routes/api/auth/invite/complete.mjs +14 -13
- package/dist/astro/routes/api/auth/invite/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/index.mjs +12 -11
- package/dist/astro/routes/api/auth/invite/index.mjs.map +1 -1
- package/dist/astro/routes/api/auth/invite/register-options.mjs +13 -12
- package/dist/astro/routes/api/auth/invite/register-options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/logout.mjs +4 -4
- package/dist/astro/routes/api/auth/magic-link/send.mjs +14 -13
- package/dist/astro/routes/api/auth/magic-link/send.mjs.map +1 -1
- package/dist/astro/routes/api/auth/magic-link/verify.mjs +4 -4
- package/dist/astro/routes/api/auth/me.mjs +10 -9
- package/dist/astro/routes/api/auth/me.mjs.map +1 -1
- package/dist/astro/routes/api/auth/mode.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_/callback.mjs +4 -4
- package/dist/astro/routes/api/auth/oauth/_provider_.mjs +2 -2
- package/dist/astro/routes/api/auth/passkey/_id_.mjs +9 -8
- package/dist/astro/routes/api/auth/passkey/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/index.mjs +3 -3
- package/dist/astro/routes/api/auth/passkey/options.mjs +15 -14
- package/dist/astro/routes/api/auth/passkey/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/options.mjs +13 -12
- package/dist/astro/routes/api/auth/passkey/register/options.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/register/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/passkey/verify.mjs +14 -13
- package/dist/astro/routes/api/auth/passkey/verify.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/complete.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/complete.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/request.mjs +14 -13
- package/dist/astro/routes/api/auth/signup/request.mjs.map +1 -1
- package/dist/astro/routes/api/auth/signup/verify.mjs +3 -3
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs +20 -19
- package/dist/astro/routes/api/comments/_collection_/_contentId_/index.mjs.map +1 -1
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs +17 -16
- package/dist/astro/routes/api/comments/_collection_/_contentId_/reactions.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/compare.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/discard-draft.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/duplicate.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/permanent.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/preview-url.mjs +15 -15
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_/publish.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs +21 -20
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/children.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs +21 -20
- package/dist/astro/routes/api/content/_collection_/_id_/references/_relation_/parents.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/restore.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_/revisions.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_/schedule.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.d.mts.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs +47 -34
- package/dist/astro/routes/api/content/_collection_/_id_/terms/_taxonomy_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/_id_/translations.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/_id_/unpublish.mjs +5 -5
- package/dist/astro/routes/api/content/_collection_/_id_.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/authors.mjs +4 -4
- package/dist/astro/routes/api/content/_collection_/index.mjs +11 -10
- package/dist/astro/routes/api/content/_collection_/index.mjs.map +1 -1
- package/dist/astro/routes/api/content/_collection_/trash.mjs +10 -9
- package/dist/astro/routes/api/content/_collection_/trash.mjs.map +1 -1
- package/dist/astro/routes/api/dashboard.mjs +12 -10
- package/dist/astro/routes/api/dashboard.mjs.map +1 -1
- package/dist/astro/routes/api/dev/emails.mjs +4 -4
- package/dist/astro/routes/api/import/probe.d.mts +3 -3
- package/dist/astro/routes/api/import/probe.mjs +22 -20
- package/dist/astro/routes/api/import/probe.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/analyze.mjs +5 -5
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/execute.mjs +18 -18
- package/dist/astro/routes/api/import/wordpress/execute.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/media.d.mts +10 -10
- package/dist/astro/routes/api/import/wordpress/media.mjs +14 -12
- package/dist/astro/routes/api/import/wordpress/media.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/prepare.mjs +13 -12
- package/dist/astro/routes/api/import/wordpress/prepare.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.mjs +15 -15
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.d.mts +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +22 -20
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs.map +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/callback.mjs +1 -1
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +11 -11
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +32 -31
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs.map +1 -1
- package/dist/astro/routes/api/manifest.mjs +6 -6
- package/dist/astro/routes/api/mcp.d.mts.map +1 -1
- package/dist/astro/routes/api/mcp.mjs +174 -72
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +14 -12
- package/dist/astro/routes/api/media/_id_/confirm.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +7 -7
- package/dist/astro/routes/api/media/_id_/usage.mjs +17 -16
- package/dist/astro/routes/api/media/_id_/usage.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_.mjs +16 -15
- package/dist/astro/routes/api/media/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/media/file/_...key_.mjs +3 -3
- package/dist/astro/routes/api/media/providers/_providerId_/_itemId_.mjs +4 -4
- package/dist/astro/routes/api/media/providers/_providerId_/index.mjs +4 -4
- package/dist/astro/routes/api/media/providers/index.mjs +4 -4
- package/dist/astro/routes/api/media/upload-url.mjs +11 -10
- package/dist/astro/routes/api/media/upload-url.mjs.map +1 -1
- package/dist/astro/routes/api/media.mjs +19 -16
- package/dist/astro/routes/api/media.mjs.map +1 -1
- package/dist/astro/routes/api/menus/_name_/items/_id_.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/items.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/reorder.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_/translations.mjs +13 -13
- package/dist/astro/routes/api/menus/_name_.mjs +13 -13
- package/dist/astro/routes/api/menus/index.mjs +13 -13
- package/dist/astro/routes/api/oauth/authorize.mjs +6 -6
- package/dist/astro/routes/api/oauth/device/authorize.mjs +7 -7
- package/dist/astro/routes/api/oauth/device/code.mjs +10 -10
- package/dist/astro/routes/api/oauth/device/token.mjs +9 -9
- package/dist/astro/routes/api/oauth/register.mjs +4 -4
- package/dist/astro/routes/api/oauth/token/refresh.mjs +7 -7
- package/dist/astro/routes/api/oauth/token/revoke.mjs +7 -7
- package/dist/astro/routes/api/oauth/token.mjs +7 -7
- package/dist/astro/routes/api/openapi.json.mjs +183 -6
- package/dist/astro/routes/api/openapi.json.mjs.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.d.mts.map +1 -1
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs +8 -7
- package/dist/astro/routes/api/plugins/_pluginId_/_...path_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/index.mjs +14 -13
- package/dist/astro/routes/api/redirects/404s/index.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/404s/summary.mjs +14 -13
- package/dist/astro/routes/api/redirects/404s/summary.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/_id_.mjs +16 -14
- package/dist/astro/routes/api/redirects/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/redirects/index.mjs +16 -14
- package/dist/astro/routes/api/redirects/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/index.mjs +21 -20
- package/dist/astro/routes/api/relations/_id_/index.mjs.map +1 -1
- package/dist/astro/routes/api/relations/_id_/translations.mjs +16 -15
- package/dist/astro/routes/api/relations/_id_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/relations/index.mjs +21 -20
- package/dist/astro/routes/api/relations/index.mjs.map +1 -1
- package/dist/astro/routes/api/revisions/_revisionId_/index.mjs +4 -4
- package/dist/astro/routes/api/revisions/_revisionId_/restore.mjs +5 -5
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.d.mts.map +1 -1
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/index.mjs +52 -46
- package/dist/astro/routes/api/schema/collections/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/collections/reorder.d.mts +8 -0
- package/dist/astro/routes/api/schema/collections/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/schema/collections/reorder.mjs +71 -0
- package/dist/astro/routes/api/schema/collections/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/schema/index.mjs +14 -11
- package/dist/astro/routes/api/schema/index.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/schema/orphans/index.mjs +52 -46
- package/dist/astro/routes/api/schema/orphans/index.mjs.map +1 -1
- package/dist/astro/routes/api/search/enable.d.mts +1 -0
- package/dist/astro/routes/api/search/enable.d.mts.map +1 -1
- package/dist/astro/routes/api/search/enable.mjs +19 -15
- package/dist/astro/routes/api/search/enable.mjs.map +1 -1
- package/dist/astro/routes/api/search/index.mjs +13 -13
- package/dist/astro/routes/api/search/rebuild.mjs +15 -15
- package/dist/astro/routes/api/search/rebuild.mjs.map +1 -1
- package/dist/astro/routes/api/search/stats.mjs +9 -9
- package/dist/astro/routes/api/search/suggest.mjs +13 -13
- package/dist/astro/routes/api/sections/_slug_.mjs +13 -12
- package/dist/astro/routes/api/sections/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/sections/index.mjs +13 -12
- package/dist/astro/routes/api/sections/index.mjs.map +1 -1
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +7 -7
- package/dist/astro/routes/api/settings/backups/export.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/index.mjs +9 -9
- package/dist/astro/routes/api/settings/email.mjs +6 -6
- package/dist/astro/routes/api/settings.mjs +18 -17
- package/dist/astro/routes/api/settings.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin-verify.mjs +15 -14
- package/dist/astro/routes/api/setup/admin-verify.mjs.map +1 -1
- package/dist/astro/routes/api/setup/admin.mjs +14 -13
- package/dist/astro/routes/api/setup/admin.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-bypass.mjs +34 -32
- package/dist/astro/routes/api/setup/dev-bypass.mjs.map +1 -1
- package/dist/astro/routes/api/setup/dev-reset.mjs +4 -4
- package/dist/astro/routes/api/setup/index.mjs +36 -34
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/setup/status.mjs +5 -5
- package/dist/astro/routes/api/snapshot.mjs +10 -10
- package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs +44 -0
- package/dist/astro/routes/api/taxonomies/_name_/reorder.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_/translations.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/_name_/terms/_slug_.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.d.mts.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs +21 -19
- package/dist/astro/routes/api/taxonomies/_name_/terms/index.mjs.map +1 -1
- package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts +8 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs +46 -0
- package/dist/astro/routes/api/taxonomies/_name_/translations.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_.d.mts +19 -0
- package/dist/astro/routes/api/taxonomies/_name_.d.mts.map +1 -0
- package/dist/astro/routes/api/taxonomies/_name_.mjs +88 -0
- package/dist/astro/routes/api/taxonomies/_name_.mjs.map +1 -0
- package/dist/astro/routes/api/taxonomies/index.mjs +19 -18
- package/dist/astro/routes/api/taxonomies/index.mjs.map +1 -1
- package/dist/astro/routes/api/themes/preview.mjs +8 -8
- package/dist/astro/routes/api/typegen.mjs +6 -6
- package/dist/astro/routes/api/well-known/auth.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-authorization-server.mjs +2 -2
- package/dist/astro/routes/api/well-known/oauth-protected-resource.mjs +2 -2
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs +10 -9
- package/dist/astro/routes/api/widget-areas/_name_/reorder.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets/_id_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/_name_/widgets.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/_name_.mjs +8 -7
- package/dist/astro/routes/api/widget-areas/_name_.mjs.map +1 -1
- package/dist/astro/routes/api/widget-areas/index.mjs +13 -12
- package/dist/astro/routes/api/widget-areas/index.mjs.map +1 -1
- package/dist/astro/routes/api/widget-components.mjs +4 -4
- package/dist/astro/routes/robots.txt.mjs +11 -10
- package/dist/astro/routes/robots.txt.mjs.map +1 -1
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +14 -14
- package/dist/astro/routes/sitemap.xml.mjs +12 -11
- package/dist/astro/routes/sitemap.xml.mjs.map +1 -1
- package/dist/astro/types.d.mts +41 -17
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/auth/providers/github.d.mts +1 -1
- package/dist/auth/providers/google.d.mts +1 -1
- package/dist/{authorize-R5iYYrpa.mjs → authorize-1iFJzPaZ.mjs} +2 -2
- package/dist/{authorize-R5iYYrpa.mjs.map → authorize-1iFJzPaZ.mjs.map} +1 -1
- package/dist/{backup-LpQ1V_sR.mjs → backup-LHjIAVNP.mjs} +5 -5
- package/dist/{backup-LpQ1V_sR.mjs.map → backup-LHjIAVNP.mjs.map} +1 -1
- package/dist/{base64-B-PsqheR.mjs → base64-CvLhUKQv.mjs} +1 -1
- package/dist/{base64-B-PsqheR.mjs.map → base64-CvLhUKQv.mjs.map} +1 -1
- package/dist/bundle-CBEXrBnb.mjs +663 -0
- package/dist/bundle-CBEXrBnb.mjs.map +1 -0
- package/dist/{byline-fields-T9ZLZMxJ.mjs → byline-fields-C5S8wygO.mjs} +2 -2
- package/dist/{byline-fields-T9ZLZMxJ.mjs.map → byline-fields-C5S8wygO.mjs.map} +1 -1
- package/dist/{byline-C5TAqs8N.mjs → byline-pEcoDps8.mjs} +9 -9
- package/dist/{byline-C5TAqs8N.mjs.map → byline-pEcoDps8.mjs.map} +1 -1
- package/dist/{byline-registry-BCuOp4UF.mjs → byline-registry-DDun8sxh.mjs} +4 -4
- package/dist/{byline-registry-BCuOp4UF.mjs.map → byline-registry-DDun8sxh.mjs.map} +1 -1
- package/dist/{bylines-B8-WGdja.mjs → bylines-BWemdX2k.mjs} +8 -8
- package/dist/{bylines-B8-WGdja.mjs.map → bylines-BWemdX2k.mjs.map} +1 -1
- package/dist/{bylines-DGuqBLjV.mjs → bylines-DJfSfXCL.mjs} +5 -5
- package/dist/{bylines-DGuqBLjV.mjs.map → bylines-DJfSfXCL.mjs.map} +1 -1
- package/dist/cache-CBJaDy_E.mjs +108 -0
- package/dist/cache-CBJaDy_E.mjs.map +1 -0
- package/dist/{challenge-store-BFzgFRog.mjs → challenge-store-CZyFVaad.mjs} +1 -1
- package/dist/{challenge-store-BFzgFRog.mjs.map → challenge-store-CZyFVaad.mjs.map} +1 -1
- package/dist/{chunks-D5dlPeRb.mjs → chunks-DfPax1I0.mjs} +2 -2
- package/dist/{chunks-D5dlPeRb.mjs.map → chunks-DfPax1I0.mjs.map} +1 -1
- package/dist/cli/index.mjs +193 -707
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +74 -4
- package/dist/client/index.d.mts.map +1 -1
- package/dist/client/index.mjs +25 -2
- package/dist/client/index.mjs.map +1 -1
- package/dist/collection-deletion-C8BugSGz.mjs +779 -0
- package/dist/collection-deletion-C8BugSGz.mjs.map +1 -0
- package/dist/{comment-DnTxxVHv.mjs → comment-Dxlep6cE.mjs} +3 -3
- package/dist/{comment-DnTxxVHv.mjs.map → comment-Dxlep6cE.mjs.map} +1 -1
- package/dist/{comment-reaction-BUlUz2bh.mjs → comment-reaction-D6qfHKBc.mjs} +2 -2
- package/dist/{comment-reaction-BUlUz2bh.mjs.map → comment-reaction-D6qfHKBc.mjs.map} +1 -1
- package/dist/{comments-QPKgK3nk.mjs → comments-CTOuhTSX.mjs} +3 -3
- package/dist/{comments-QPKgK3nk.mjs.map → comments-CTOuhTSX.mjs.map} +1 -1
- package/dist/{components-DZkL4Fx0.mjs → components-CQy3lh9x.mjs} +1 -1
- package/dist/{components-DZkL4Fx0.mjs.map → components-CQy3lh9x.mjs.map} +1 -1
- package/dist/{config-DyF3OKg_.mjs → config-C3EBstHi.mjs} +18 -2
- package/dist/config-C3EBstHi.mjs.map +1 -0
- package/dist/{content-CpfKV9QE.mjs → content-BNfwh7xX.mjs} +667 -183
- package/dist/content-BNfwh7xX.mjs.map +1 -0
- package/dist/content-list-query-DhQ84eVV.d.mts +26 -0
- package/dist/content-list-query-DhQ84eVV.d.mts.map +1 -0
- package/dist/content-refresh-DNERP4-f.mjs +2560 -0
- package/dist/content-refresh-DNERP4-f.mjs.map +1 -0
- package/dist/{context-kO4YZB03.mjs → context-DRgOvSnW.mjs} +28 -20
- package/dist/context-DRgOvSnW.mjs.map +1 -0
- package/dist/{cron-BlKIMD_e.mjs → cron-Y9aLtgrA.mjs} +1 -1
- package/dist/{cron-BlKIMD_e.mjs.map → cron-Y9aLtgrA.mjs.map} +1 -1
- package/dist/{dashboard-DoBwPnON.mjs → dashboard-CB8J89rJ.mjs} +45 -11
- package/dist/dashboard-CB8J89rJ.mjs.map +1 -0
- package/dist/db/index.d.mts +4 -4
- package/dist/db/index.mjs +3 -3
- package/dist/db/index.mjs.map +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-CcWLaRiR.mjs → db-errors-Cl-sKYTm.mjs} +1 -1
- package/dist/{db-errors-CcWLaRiR.mjs.map → db-errors-Cl-sKYTm.mjs.map} +1 -1
- package/dist/{default-C4v9KP56.mjs → default-DdDl3fxv.mjs} +1 -1
- package/dist/{default-C4v9KP56.mjs.map → default-DdDl3fxv.mjs.map} +1 -1
- package/dist/deferred-tasks-BMsa9F64.d.mts +9 -0
- package/dist/deferred-tasks-BMsa9F64.d.mts.map +1 -0
- package/dist/{device-flow-DhUsyJU6.mjs → device-flow-CaioYGK3.mjs} +5 -5
- package/dist/{device-flow-DhUsyJU6.mjs.map → device-flow-CaioYGK3.mjs.map} +1 -1
- package/dist/{dialect-helpers-DFwyWjTM.mjs → dialect-helpers-BSbfaO3F.mjs} +24 -16
- package/dist/dialect-helpers-BSbfaO3F.mjs.map +1 -0
- package/dist/dist-D_6CKR8z.mjs +810 -0
- package/dist/dist-D_6CKR8z.mjs.map +1 -0
- package/dist/{email-console-C-9Ng8DM.mjs → email-console-BHNjIwiB.mjs} +1 -1
- package/dist/{email-console-C-9Ng8DM.mjs.map → email-console-BHNjIwiB.mjs.map} +1 -1
- package/dist/{enrich-CFJJgxs_.mjs → enrich-_ySMEvEE.mjs} +2 -2
- package/dist/{enrich-CFJJgxs_.mjs.map → enrich-_ySMEvEE.mjs.map} +1 -1
- package/dist/{error-DmmN74gW.mjs → error-DupyJZE2.mjs} +3 -3
- package/dist/{error-DmmN74gW.mjs.map → error-DupyJZE2.mjs.map} +1 -1
- package/dist/{errors-DtEXIQQV.mjs → errors-CLUPm7aD.mjs} +15 -2
- package/dist/errors-CLUPm7aD.mjs.map +1 -0
- package/dist/{escape-CdW31WsG.mjs → escape-C_aLKP3H.mjs} +1 -1
- package/dist/{escape-CdW31WsG.mjs.map → escape-C_aLKP3H.mjs.map} +1 -1
- package/dist/{field-defs-cache-QMVnzTH6.mjs → field-defs-cache-hg6QkjV9.mjs} +5 -5
- package/dist/{field-defs-cache-QMVnzTH6.mjs.map → field-defs-cache-hg6QkjV9.mjs.map} +1 -1
- package/dist/{fts-manager-vncsZIJm.mjs → fts-manager-CZ-5dVwJ.mjs} +111 -61
- package/dist/fts-manager-CZ-5dVwJ.mjs.map +1 -0
- package/dist/{hash-Cb7U1b5M.mjs → hash-Cg0TXJPV.mjs} +1 -1
- package/dist/{hash-Cb7U1b5M.mjs.map → hash-Cg0TXJPV.mjs.map} +1 -1
- package/dist/{hreflang-CZbSx6Bg.d.mts → hreflang-D3Uehfv2.d.mts} +2 -2
- package/dist/{hreflang-CZbSx6Bg.d.mts.map → hreflang-D3Uehfv2.d.mts.map} +1 -1
- package/dist/{hreflang-Bg-zMpkr.mjs → hreflang-DNswoYsi.mjs} +7 -7
- package/dist/{hreflang-Bg-zMpkr.mjs.map → hreflang-DNswoYsi.mjs.map} +1 -1
- package/dist/{import-PKhLeXvn.mjs → import-B41u8Rot.mjs} +7 -7
- package/dist/{import-PKhLeXvn.mjs.map → import-B41u8Rot.mjs.map} +1 -1
- package/dist/{index-j9rTh3cp.d.mts → index-C3Y-rsyN.d.mts} +286 -93
- package/dist/index-C3Y-rsyN.d.mts.map +1 -0
- package/dist/{index-BeNto_Vs.d.mts → index-Cof4-Fab.d.mts} +5 -10
- package/dist/index-Cof4-Fab.d.mts.map +1 -0
- package/dist/index.d.mts +20 -19
- package/dist/index.mjs +79 -71
- package/dist/{init-lock-DJkX6Hto.mjs → init-lock-6b309ZrF.mjs} +1 -1
- package/dist/{init-lock-DJkX6Hto.mjs.map → init-lock-6b309ZrF.mjs.map} +1 -1
- package/dist/{load-BwTdWE8B.mjs → load-DmvJrtxS.mjs} +2 -2
- package/dist/{load-BwTdWE8B.mjs.map → load-DmvJrtxS.mjs.map} +1 -1
- package/dist/{loader-C1XOLV5b.mjs → loader-DLBqRiGP.mjs} +120 -64
- package/dist/loader-DLBqRiGP.mjs.map +1 -0
- package/dist/{manifest-schema-bCq54i7F.mjs → manifest-schema-BXbUAHod.mjs} +1 -1
- package/dist/{manifest-schema-bCq54i7F.mjs.map → manifest-schema-BXbUAHod.mjs.map} +1 -1
- package/dist/media/image-endpoint.mjs +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +3 -2
- package/dist/media/index.mjs.map +1 -1
- package/dist/media/local-runtime.d.mts +12 -12
- package/dist/media/local-runtime.mjs +10 -9
- package/dist/media/local-runtime.mjs.map +1 -1
- package/dist/{media-DWIzoVFL.mjs → media-BWWWb8kg.mjs} +92 -3
- package/dist/media-BWWWb8kg.mjs.map +1 -0
- package/dist/{media-tQDZEdu7.mjs → media-CzhAn2--.mjs} +2 -2
- package/dist/{media-tQDZEdu7.mjs.map → media-CzhAn2--.mjs.map} +1 -1
- package/dist/{media-allowlist-B7TPD6aq.mjs → media-allowlist-BD6HhgXA.mjs} +14 -3
- package/dist/media-allowlist-BD6HhgXA.mjs.map +1 -0
- package/dist/{media-upload-Bc9Nc5Zf.mjs → media-upload-9yas9zGH.mjs} +11 -9
- package/dist/{media-upload-Bc9Nc5Zf.mjs.map → media-upload-9yas9zGH.mjs.map} +1 -1
- package/dist/{media-url-BCm5vBn6.mjs → media-url-BvoX88FA.mjs} +1 -1
- package/dist/{media-url-BCm5vBn6.mjs.map → media-url-BvoX88FA.mjs.map} +1 -1
- package/dist/{media-usage-DPJcNaAq.mjs → media-usage-De3qvwjc.mjs} +138 -61
- package/dist/media-usage-De3qvwjc.mjs.map +1 -0
- package/dist/media-usage-work-B9Bksh_b.mjs +152 -0
- package/dist/media-usage-work-B9Bksh_b.mjs.map +1 -0
- package/dist/media-usage-work-DrCbqBcl.mjs +424 -0
- package/dist/media-usage-work-DrCbqBcl.mjs.map +1 -0
- package/dist/media-usage-write-fence-aHEgN1zB.mjs +42 -0
- package/dist/media-usage-write-fence-aHEgN1zB.mjs.map +1 -0
- package/dist/{menus-C6qhqP4X.mjs → menus-CC-yn1F9.mjs} +554 -121
- package/dist/menus-CC-yn1F9.mjs.map +1 -0
- package/dist/{menus-BxPLFfIc.mjs → menus-fxvEDYE6.mjs} +5 -5
- package/dist/{menus-BxPLFfIc.mjs.map → menus-fxvEDYE6.mjs.map} +1 -1
- package/dist/{mode-fiXRMfeA.mjs → mode-Bpe-tNNy.mjs} +1 -1
- package/dist/{mode-fiXRMfeA.mjs.map → mode-Bpe-tNNy.mjs.map} +1 -1
- package/dist/{normalize-C-SHXmra.mjs → normalize-Bc-s_r-a.mjs} +1 -1
- package/dist/{normalize-C-SHXmra.mjs.map → normalize-Bc-s_r-a.mjs.map} +1 -1
- package/dist/{oauth-authorization-C9tFinly.mjs → oauth-authorization-BXO1MKej.mjs} +5 -5
- package/dist/{oauth-authorization-C9tFinly.mjs.map → oauth-authorization-BXO1MKej.mjs.map} +1 -1
- package/dist/{oauth-clients-BwTxE1aE.mjs → oauth-clients-C_DNqtRu.mjs} +1 -1
- package/dist/{oauth-clients-BwTxE1aE.mjs.map → oauth-clients-C_DNqtRu.mjs.map} +1 -1
- package/dist/{oauth-state-store-DaRUwPWO.mjs → oauth-state-store-DwAxMtk4.mjs} +1 -1
- package/dist/{oauth-state-store-DaRUwPWO.mjs.map → oauth-state-store-DwAxMtk4.mjs.map} +1 -1
- package/dist/{oauth-user-lookup-CzuzOSlF.mjs → oauth-user-lookup-BxfZizdk.mjs} +1 -1
- package/dist/{oauth-user-lookup-CzuzOSlF.mjs.map → oauth-user-lookup-BxfZizdk.mjs.map} +1 -1
- package/dist/object-cache/memory.d.mts +1 -1
- package/dist/{object-cache-Bok5j2ae.mjs → object-cache-71tPfV4A.mjs} +106 -5
- package/dist/object-cache-71tPfV4A.mjs.map +1 -0
- package/dist/{options-BlmBHTvX.mjs → options-CBXIHUoT.mjs} +1 -1
- package/dist/{options-BlmBHTvX.mjs.map → options-CBXIHUoT.mjs.map} +1 -1
- package/dist/{options-aeIs8WFu.d.mts → options-DRe1pIBL.d.mts} +4 -3
- package/dist/options-DRe1pIBL.d.mts.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{parse-BL49yb9D.mjs → parse-DFbuc-Fh.mjs} +2 -2
- package/dist/{parse-BL49yb9D.mjs.map → parse-DFbuc-Fh.mjs.map} +1 -1
- package/dist/{passkey-config-KMhgFjdQ.mjs → passkey-config-_2GOwwK8.mjs} +1 -1
- package/dist/{passkey-config-KMhgFjdQ.mjs.map → passkey-config-_2GOwwK8.mjs.map} +1 -1
- package/dist/{placeholder-Dxo5Zt58.d.mts → placeholder-CzW7Lshs.d.mts} +1 -1
- package/dist/{placeholder-Dxo5Zt58.d.mts.map → placeholder-CzW7Lshs.d.mts.map} +1 -1
- package/dist/plugin-types.d.mts +8 -1
- package/dist/plugin-types.d.mts.map +1 -1
- package/dist/plugin-utils.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.d.mts +10 -10
- package/dist/plugins/adapt-sandbox-entry.mjs +7 -4
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/{portable-text-CNahFfj6.mjs → portable-text-DhKbDAGr.mjs} +1 -1
- package/dist/{portable-text-CNahFfj6.mjs.map → portable-text-DhKbDAGr.mjs.map} +1 -1
- package/dist/{preview-D4Jnbfx7.mjs → preview-PW-nOnq4.mjs} +2 -2
- package/dist/{preview-D4Jnbfx7.mjs.map → preview-PW-nOnq4.mjs.map} +1 -1
- package/dist/{public-url-DSGTnJFw.mjs → public-url-CWoFhQT8.mjs} +14 -3
- package/dist/public-url-CWoFhQT8.mjs.map +1 -0
- package/dist/{query-DCiXI7OZ.mjs → query-BgjfiLUQ.mjs} +38 -77
- package/dist/query-BgjfiLUQ.mjs.map +1 -0
- package/dist/{query-1xOcEy18.mjs → query-vgZNwIhh.mjs} +4 -4
- package/dist/{query-1xOcEy18.mjs.map → query-vgZNwIhh.mjs.map} +1 -1
- package/dist/{rate-limit-Vkp9gQ1Y.mjs → rate-limit-L5anykgo.mjs} +2 -2
- package/dist/{rate-limit-Vkp9gQ1Y.mjs.map → rate-limit-L5anykgo.mjs.map} +1 -1
- package/dist/{redirect-BJ6d1yHh.mjs → redirect-COsS6SWJ.mjs} +3 -3
- package/dist/{redirect-BJ6d1yHh.mjs.map → redirect-COsS6SWJ.mjs.map} +1 -1
- package/dist/{redirect-CDgfv9vh.mjs → redirect-KBNE3UEs.mjs} +1 -1
- package/dist/{redirect-CDgfv9vh.mjs.map → redirect-KBNE3UEs.mjs.map} +1 -1
- package/dist/{redirects-Cwkq2H-G.mjs → redirects-CufRQt0Q.mjs} +5 -5
- package/dist/{redirects-Cwkq2H-G.mjs.map → redirects-CufRQt0Q.mjs.map} +1 -1
- package/dist/{redirects-BOn2DIUs.mjs → redirects-cV9g7V-0.mjs} +149 -16
- package/dist/redirects-cV9g7V-0.mjs.map +1 -0
- package/dist/registry-C581V4TP.mjs +1275 -0
- package/dist/registry-C581V4TP.mjs.map +1 -0
- package/dist/{relation-DJ5DZh7B.mjs → relation-DtmAQH7s.mjs} +3 -3
- package/dist/{relation-DJ5DZh7B.mjs.map → relation-DtmAQH7s.mjs.map} +1 -1
- package/dist/{relations-C4duJnwI.mjs → relations-BBApI4e2.mjs} +3 -3
- package/dist/{relations-C4duJnwI.mjs.map → relations-BBApI4e2.mjs.map} +1 -1
- package/dist/{relations-DpTjftCl.mjs → relations-DICSyfsh.mjs} +6 -6
- package/dist/{relations-DpTjftCl.mjs.map → relations-DICSyfsh.mjs.map} +1 -1
- package/dist/{relations-DHFQGX0h.d.mts → relations-DItI3MN9.d.mts} +401 -40
- package/dist/relations-DItI3MN9.d.mts.map +1 -0
- package/dist/request-context.d.mts +3 -0
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{request-meta-DzXYYI-n.mjs → request-meta-DgZJh3zG.mjs} +2 -2
- package/dist/{request-meta-DzXYYI-n.mjs.map → request-meta-DgZJh3zG.mjs.map} +1 -1
- package/dist/{resolve-BUvFE0Lr.mjs → resolve-DOhVEvty.mjs} +2 -2
- package/dist/{resolve-BUvFE0Lr.mjs.map → resolve-DOhVEvty.mjs.map} +1 -1
- package/dist/{runner-BsI18UgP.mjs → runner-BiDvq5dv.mjs} +1404 -235
- package/dist/runner-BiDvq5dv.mjs.map +1 -0
- package/dist/{runner-CHdkERbt.d.mts → runner-CgQbELYC.d.mts} +2 -2
- package/dist/{runner-CHdkERbt.d.mts.map → runner-CgQbELYC.d.mts.map} +1 -1
- package/dist/runtime.d.mts +11 -11
- package/dist/runtime.mjs +5 -4
- package/dist/runtime.mjs.map +1 -1
- package/dist/{schema-eqZhkR0S.mjs → schema-BFyTg38T.mjs} +3 -3
- package/dist/{schema-eqZhkR0S.mjs.map → schema-BFyTg38T.mjs.map} +1 -1
- package/dist/schema-CeQSNwB3.mjs +390 -0
- package/dist/schema-CeQSNwB3.mjs.map +1 -0
- package/dist/{scopes-Be2d8bSb.mjs → scopes-BVeuhGBE.mjs} +3 -3
- package/dist/{scopes-Be2d8bSb.mjs.map → scopes-BVeuhGBE.mjs.map} +1 -1
- package/dist/{search-CeO678cp.mjs → search-BpuPKZb0.mjs} +26 -16
- package/dist/search-BpuPKZb0.mjs.map +1 -0
- package/dist/{secrets-870d-7yA.mjs → secrets-dtYs2hDo.mjs} +5 -5
- package/dist/{secrets-870d-7yA.mjs.map → secrets-dtYs2hDo.mjs.map} +1 -1
- package/dist/{sections-DSuEfirn.mjs → sections-COctk6hf.mjs} +3 -3
- package/dist/{sections-DSuEfirn.mjs.map → sections-COctk6hf.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +28 -26
- package/dist/seo/index.d.mts +3 -3
- package/dist/seo/index.mjs +20 -19
- package/dist/seo/index.mjs.map +1 -1
- package/dist/{seo-C3GDfT0V.mjs → seo-AFpVvTqr.mjs} +6 -4
- package/dist/seo-AFpVvTqr.mjs.map +1 -0
- package/dist/{seo-BAPKiED8.mjs → seo-CvDxfvFb.mjs} +3 -3
- package/dist/{seo-BAPKiED8.mjs.map → seo-CvDxfvFb.mjs.map} +1 -1
- package/dist/{service-DyFT-k-R.mjs → service-xMiMSTsR.mjs} +4 -4
- package/dist/{service-DyFT-k-R.mjs.map → service-xMiMSTsR.mjs.map} +1 -1
- package/dist/{session-user-BrK2zz7S.mjs → session-user-BzVemLkw.mjs} +2 -2
- package/dist/{session-user-BrK2zz7S.mjs.map → session-user-BzVemLkw.mjs.map} +1 -1
- package/dist/{settings-DgKouY2S.mjs → settings-B372MV0p.mjs} +7 -7
- package/dist/{settings-DgKouY2S.mjs.map → settings-B372MV0p.mjs.map} +1 -1
- package/dist/{settings-CREKyj8I.mjs → settings-BE9aqn_9.mjs} +3 -3
- package/dist/{settings-CREKyj8I.mjs.map → settings-BE9aqn_9.mjs.map} +1 -1
- package/dist/{setup-complete-L7bMK2VI.mjs → setup-complete-DF7TAfD5.mjs} +2 -2
- package/dist/{setup-complete-L7bMK2VI.mjs.map → setup-complete-DF7TAfD5.mjs.map} +1 -1
- package/dist/{setup-nonce-Ct5tjWcq.mjs → setup-nonce-Do-U_cg9.mjs} +1 -1
- package/dist/{setup-nonce-Ct5tjWcq.mjs.map → setup-nonce-Do-U_cg9.mjs.map} +1 -1
- package/dist/{single-flight-cache-C2exrGAi.mjs → single-flight-cache-B0eLKS6K.mjs} +2 -2
- package/dist/{single-flight-cache-C2exrGAi.mjs.map → single-flight-cache-B0eLKS6K.mjs.map} +1 -1
- package/dist/site-url-Bowsi9T-.mjs +16 -0
- package/dist/site-url-Bowsi9T-.mjs.map +1 -0
- package/dist/slugify-B994zkXC.mjs +17 -0
- package/dist/slugify-B994zkXC.mjs.map +1 -0
- package/dist/{snapshot-BdpUJKD-.mjs → snapshot-B5h6aPMh.mjs} +2 -2
- package/dist/{snapshot-BdpUJKD-.mjs.map → snapshot-B5h6aPMh.mjs.map} +1 -1
- package/dist/{ssrf-CviKqWmq.mjs → ssrf-Cpvg_n3A.mjs} +1 -1
- package/dist/{ssrf-CviKqWmq.mjs.map → ssrf-Cpvg_n3A.mjs.map} +1 -1
- package/dist/{state-xxv6ZTMv.mjs → state-DYwPue40.mjs} +1 -1
- package/dist/{state-xxv6ZTMv.mjs.map → state-DYwPue40.mjs.map} +1 -1
- package/dist/{status-COfycGIU.mjs → status-CWVYpbD8.mjs} +1 -1
- package/dist/{status-COfycGIU.mjs.map → status-CWVYpbD8.mjs.map} +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{taxonomies-B61CRSha.mjs → taxonomies-B0o9ExNV.mjs} +85 -75
- package/dist/taxonomies-B0o9ExNV.mjs.map +1 -0
- package/dist/{taxonomies-Dbdki3Un.mjs → taxonomies-Dk3MAklL.mjs} +360 -56
- package/dist/taxonomies-Dk3MAklL.mjs.map +1 -0
- package/dist/taxonomy-CPeM8d2o.mjs +484 -0
- package/dist/taxonomy-CPeM8d2o.mjs.map +1 -0
- package/dist/{tokens-DVltpO2D.mjs → tokens-C39klX8R.mjs} +2 -2
- package/dist/{tokens-DVltpO2D.mjs.map → tokens-C39klX8R.mjs.map} +1 -1
- package/dist/{transaction-D0FOsb3X.mjs → transaction-NQj4VJ7Z.mjs} +1 -1
- package/dist/{transaction-D0FOsb3X.mjs.map → transaction-NQj4VJ7Z.mjs.map} +1 -1
- package/dist/{transport-BWQo0Foy.mjs → transport--CfOflrp.mjs} +1 -1
- package/dist/{transport-BWQo0Foy.mjs.map → transport--CfOflrp.mjs.map} +1 -1
- package/dist/{transport-DokJOScS.d.mts → transport-B_rfeU-a.d.mts} +1 -1
- package/dist/{transport-DokJOScS.d.mts.map → transport-B_rfeU-a.d.mts.map} +1 -1
- package/dist/{trusted-proxy-CwjQj0YG.mjs → trusted-proxy-CEVekpby.mjs} +1 -1
- package/dist/{trusted-proxy-CwjQj0YG.mjs.map → trusted-proxy-CEVekpby.mjs.map} +1 -1
- package/dist/{types-o7xo7VgH.mjs → types-75KIqzL9.mjs} +20 -3
- package/dist/types-75KIqzL9.mjs.map +1 -0
- package/dist/{types-C9P3ysxq.d.mts → types-BPuloofb.d.mts} +1 -1
- package/dist/{types-C9P3ysxq.d.mts.map → types-BPuloofb.d.mts.map} +1 -1
- package/dist/{types-BD40g7zz.mjs → types-BW1syrlw.mjs} +1 -1
- package/dist/{types-BD40g7zz.mjs.map → types-BW1syrlw.mjs.map} +1 -1
- package/dist/{types-jISEalTX.d.mts → types-C3_AbkDP.d.mts} +53 -10
- package/dist/types-C3_AbkDP.d.mts.map +1 -0
- package/dist/{types-84Bque3-.d.mts → types-CAGdePf3.d.mts} +133 -1
- package/dist/types-CAGdePf3.d.mts.map +1 -0
- package/dist/{types-CcSZCcF1.d.mts → types-CFUfyt1s.d.mts} +1 -1
- package/dist/{types-CcSZCcF1.d.mts.map → types-CFUfyt1s.d.mts.map} +1 -1
- package/dist/{types-DQLZ58GV.d.mts → types-CPx2lIGo.d.mts} +1 -1
- package/dist/{types-DQLZ58GV.d.mts.map → types-CPx2lIGo.d.mts.map} +1 -1
- package/dist/{types-XrQQ-Aex.mjs → types-CQPWhPxE.mjs} +15 -11
- package/dist/types-CQPWhPxE.mjs.map +1 -0
- package/dist/{types-BQY7VYw6.d.mts → types-Cs-TFLNf.d.mts} +28 -8
- package/dist/types-Cs-TFLNf.d.mts.map +1 -0
- package/dist/types-CtzytVXn.mjs +18 -0
- package/dist/types-CtzytVXn.mjs.map +1 -0
- package/dist/{types-DqZVPILm.d.mts → types-D4_1Ga4L.d.mts} +44 -5
- package/dist/types-D4_1Ga4L.d.mts.map +1 -0
- package/dist/{types-Dr2GLlwu.d.mts → types-DefV3OiO.d.mts} +1 -1
- package/dist/{types-Dr2GLlwu.d.mts.map → types-DefV3OiO.d.mts.map} +1 -1
- package/dist/{types-DsAMyD_o.d.mts → types-X0MThr7x.d.mts} +1 -1
- package/dist/{types-DsAMyD_o.d.mts.map → types-X0MThr7x.d.mts.map} +1 -1
- package/dist/{upload-attempts-C5yd6Gae.mjs → upload-attempts-hlne3iA1.mjs} +1 -1
- package/dist/{upload-attempts-C5yd6Gae.mjs.map → upload-attempts-hlne3iA1.mjs.map} +1 -1
- package/dist/url-pattern-BHS0V5Ng.mjs +33 -0
- package/dist/url-pattern-BHS0V5Ng.mjs.map +1 -0
- package/dist/{user-BAumEmpA.mjs → user-sHgJ62SP.mjs} +3 -3
- package/dist/{user-BAumEmpA.mjs.map → user-sHgJ62SP.mjs.map} +1 -1
- package/dist/{utils-BbUgmeZG.mjs → utils-D4vD_Y98.mjs} +3 -3
- package/dist/{utils-BbUgmeZG.mjs.map → utils-D4vD_Y98.mjs.map} +1 -1
- package/dist/{validate-Bs_wT2ul.mjs → validate-BkqIG_Uj.mjs} +24 -5
- package/dist/validate-BkqIG_Uj.mjs.map +1 -0
- package/dist/{validate-VPnKoIzW.mjs → validate-BpwoDOgt.mjs} +25 -2
- package/dist/validate-BpwoDOgt.mjs.map +1 -0
- package/dist/{validate-CdYcY-Ln.d.mts → validate-DeGtBN84.d.mts} +25 -7
- package/dist/validate-DeGtBN84.d.mts.map +1 -0
- package/dist/{validation-BngR0nit.mjs → validation-CiJb_X5o.mjs} +14 -13
- package/dist/{validation-BngR0nit.mjs.map → validation-CiJb_X5o.mjs.map} +1 -1
- package/dist/version-x0NjTScG.mjs +7 -0
- package/dist/{version-Bdfb34kk.mjs.map → version-x0NjTScG.mjs.map} +1 -1
- package/dist/{widgets-L-VndTKD.mjs → widgets-Cwzvww0W.mjs} +3 -3
- package/dist/{widgets-L-VndTKD.mjs.map → widgets-Cwzvww0W.mjs.map} +1 -1
- package/dist/{wxr-taxonomies-DueaLFd9.d.mts → wxr-taxonomies-CmC0ZsDr.d.mts} +1 -1
- package/dist/{wxr-taxonomies-DueaLFd9.d.mts.map → wxr-taxonomies-CmC0ZsDr.d.mts.map} +1 -1
- package/dist/{wxr-taxonomies-BexjtNid.mjs → wxr-taxonomies-QZjWxjyk.mjs} +13 -32
- package/dist/{wxr-taxonomies-BexjtNid.mjs.map → wxr-taxonomies-QZjWxjyk.mjs.map} +1 -1
- package/dist/{zod-generator-DMzfga3g.mjs → zod-generator-D11MhxG4.mjs} +53 -6
- package/dist/zod-generator-D11MhxG4.mjs.map +1 -0
- package/package.json +18 -7
- package/src/after.ts +6 -1
- package/src/api/errors.ts +13 -0
- package/src/api/handlers/content.ts +172 -33
- package/src/api/handlers/dashboard.ts +12 -2
- package/src/api/handlers/index.ts +16 -0
- package/src/api/handlers/manifest.ts +2 -0
- package/src/api/handlers/marketplace.ts +14 -0
- package/src/api/handlers/media-allowlist.ts +13 -2
- package/src/api/handlers/media-usage-work.ts +188 -0
- package/src/api/handlers/media-usage.ts +5 -3
- package/src/api/handlers/registry.ts +14 -0
- package/src/api/handlers/revision.ts +19 -5
- package/src/api/handlers/schema.ts +46 -3
- package/src/api/handlers/seo.ts +5 -2
- package/src/api/handlers/snapshot.ts +1 -1
- package/src/api/handlers/taxonomies.ts +446 -83
- package/src/api/media-usage-write-fence.ts +64 -0
- package/src/api/openapi/document.ts +247 -0
- package/src/api/public-url.ts +14 -2
- package/src/api/schemas/common.ts +3 -1
- package/src/api/schemas/content.ts +122 -1
- package/src/api/schemas/media-usage.ts +133 -0
- package/src/api/schemas/schema.ts +91 -3
- package/src/api/schemas/search.ts +2 -0
- package/src/api/schemas/taxonomies.ts +58 -8
- package/src/api/site-url.ts +16 -6
- package/src/api/types.ts +1 -0
- package/src/astro/integration/index.ts +55 -42
- package/src/astro/integration/routes.ts +48 -4
- package/src/astro/integration/runtime.ts +28 -0
- package/src/astro/integration/virtual-modules.ts +27 -1
- package/src/astro/integration/vite-config.ts +16 -1
- package/src/astro/middleware/media-usage-write-fence.ts +30 -0
- package/src/astro/middleware/redirect.ts +14 -12
- package/src/astro/middleware/scoped-db.ts +35 -4
- package/src/astro/middleware.ts +131 -30
- package/src/astro/routes/api/admin/comments/[id]/status.ts +1 -1
- package/src/astro/routes/api/admin/media-usage/collection-deletions/index.ts +22 -0
- package/src/astro/routes/api/admin/media-usage/collection-deletions/retry.ts +22 -0
- package/src/astro/routes/api/admin/media-usage/work/index.ts +26 -0
- package/src/astro/routes/api/admin/media-usage/work/retry.ts +26 -0
- package/src/astro/routes/api/admin/plugins/[id]/disable.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/enable.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/uninstall.ts +4 -0
- package/src/astro/routes/api/admin/plugins/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/marketplace/[id]/install.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/uninstall.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/[id]/update.ts +4 -0
- package/src/astro/routes/api/admin/plugins/registry/install.ts +4 -0
- package/src/astro/routes/api/admin/users/[id]/send-recovery.ts +2 -2
- package/src/astro/routes/api/auth/invite/index.ts +2 -2
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -2
- package/src/astro/routes/api/auth/signup/request.ts +2 -2
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +67 -31
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -10
- package/src/astro/routes/api/mcp.ts +1 -0
- package/src/astro/routes/api/plugins/[pluginId]/[...path].ts +7 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +1 -7
- package/src/astro/routes/api/schema/collections/reorder.ts +31 -0
- package/src/astro/routes/api/schema/index.ts +2 -0
- package/src/astro/routes/api/search/enable.ts +5 -1
- package/src/astro/routes/api/search/rebuild.ts +6 -1
- package/src/astro/routes/api/taxonomies/[name]/reorder.ts +41 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +1 -0
- package/src/astro/routes/api/taxonomies/[name]/translations.ts +48 -0
- package/src/astro/routes/api/taxonomies/[name].ts +102 -0
- package/src/astro/types.ts +31 -4
- package/src/cleanup.ts +25 -26
- package/src/cli/commands/dev.ts +13 -3
- package/src/cli/commands/doctor.ts +193 -6
- package/src/cli/commands/export-seed.ts +6 -1
- package/src/cli/index.ts +1 -1
- package/src/client/index.ts +119 -0
- package/src/components/Block.astro +12 -0
- package/src/components/CommentForm.astro +1 -1
- package/src/components/Comments.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +260 -32
- package/src/components/OrderedList.astro +14 -0
- package/src/components/PortableText.astro +26 -4
- package/src/components/index.ts +12 -1
- package/src/components/ordered-list.ts +549 -0
- package/src/content/converters/mark-safety.ts +120 -0
- package/src/content/converters/numbered-list.ts +148 -0
- package/src/content/converters/portable-text-to-prosemirror.ts +84 -25
- package/src/content/converters/prosemirror-to-portable-text.ts +35 -14
- package/src/content/converters/types.ts +2 -0
- package/src/content/portable-text-lists.ts +205 -0
- package/src/content-list-query.ts +29 -0
- package/src/database/dialect-helpers.ts +3 -22
- package/src/database/migrations/055_content_translation_group_locale_index.ts +67 -0
- package/src/database/migrations/056_taxonomy_term_sort_order.ts +189 -0
- package/src/database/migrations/057_collection_hidden.ts +24 -0
- package/src/database/migrations/058_collection_sort_order.ts +20 -0
- package/src/database/migrations/059_revision_prune_queue.ts +36 -0
- package/src/database/migrations/060_collection_admin_config.ts +16 -0
- package/src/database/migrations/061_media_usage_cleanup.ts +77 -0
- package/src/database/migrations/062_media_usage_cleanup_fence.ts +295 -0
- package/src/database/migrations/063_media_usage_incremental_work.ts +397 -0
- package/src/database/migrations/064_fts_plain_text.ts +253 -0
- package/src/database/migrations/065_media_usage_collection_deletion.ts +85 -0
- package/src/database/migrations/066_media_usage_reconciliation.ts +115 -0
- package/src/database/migrations/067_indexed_content_fields.ts +39 -0
- package/src/database/migrations/068_content_taxonomy_entry_groups.ts +61 -0
- package/src/database/migrations/069_collection_title_date_fields.ts +45 -0
- package/src/database/migrations/070_collection_routable.ts +18 -0
- package/src/database/migrations/runner.ts +32 -0
- package/src/database/repositories/content.ts +1051 -273
- package/src/database/repositories/media-usage-work.ts +848 -0
- package/src/database/repositories/media-usage.ts +1502 -138
- package/src/database/repositories/revision.ts +98 -22
- package/src/database/repositories/taxonomy.ts +429 -103
- package/src/database/repositories/types.ts +58 -2
- package/src/database/types.ts +147 -17
- package/src/database/validate.ts +39 -0
- package/src/db/adapters.ts +36 -0
- package/src/db/index.ts +3 -0
- package/src/deferred-tasks.ts +71 -0
- package/src/emdash-runtime.ts +493 -110
- package/src/fields/file.ts +7 -4
- package/src/fields/image.ts +8 -1
- package/src/fields/types.ts +18 -5
- package/src/i18n/config.ts +32 -0
- package/src/i18n/repair-locale-casing.ts +0 -14
- package/src/i18n/taxonomy-locale-diagnostic.ts +47 -0
- package/src/import/wxr-taxonomies.ts +7 -47
- package/src/index.ts +27 -2
- package/src/loader.ts +165 -89
- package/src/mcp/server.ts +242 -34
- package/src/media/usage/activation.ts +626 -0
- package/src/media/usage/capture-triggers.ts +543 -0
- package/src/media/usage/cleanup.ts +305 -0
- package/src/media/usage/collection-deletion-processor.ts +356 -0
- package/src/media/usage/collection-deletion.ts +836 -0
- package/src/media/usage/content-fields.ts +40 -3
- package/src/media/usage/content-refresh.ts +356 -47
- package/src/media/usage/content-repair.ts +180 -61
- package/src/media/usage/content-snapshots.ts +85 -105
- package/src/media/usage/gc.ts +50 -0
- package/src/media/usage/projection-fingerprint.ts +129 -0
- package/src/media/usage/reconciliation-processor.ts +372 -0
- package/src/media/usage/reconciliation.ts +1117 -0
- package/src/media/usage/source-key.ts +4 -0
- package/src/media/usage/types.ts +2 -0
- package/src/media/usage/work-processor.ts +248 -0
- package/src/menus/index.ts +246 -141
- package/src/object-cache/index.ts +160 -3
- package/src/plugin-types.ts +8 -0
- package/src/plugins/adapt-sandbox-entry.ts +24 -5
- package/src/plugins/context.ts +24 -7
- package/src/plugins/index.ts +9 -0
- package/src/plugins/routes.ts +77 -1
- package/src/plugins/sandbox/index.ts +10 -1
- package/src/plugins/sandbox/types.ts +78 -1
- package/src/plugins/scheduler/node.ts +13 -1
- package/src/plugins/scheduler/types.ts +2 -0
- package/src/plugins/storage-indexes.ts +50 -11
- package/src/plugins/types.ts +39 -2
- package/src/query.ts +50 -79
- package/src/redirects/cache.ts +79 -19
- package/src/request-context.ts +3 -0
- package/src/scheduled-publish.ts +6 -1
- package/src/scheduler-health.ts +46 -0
- package/src/schema/registry.ts +863 -249
- package/src/schema/types.ts +74 -8
- package/src/schema/url-pattern.ts +33 -0
- package/src/schema/zod-generator.ts +77 -5
- package/src/search/fts-manager.ts +167 -86
- package/src/search/index.ts +2 -0
- package/src/search/query.ts +32 -11
- package/src/search/types.ts +8 -0
- package/src/seed/apply.ts +89 -16
- package/src/seed/types.ts +21 -3
- package/src/seed/validate.ts +52 -2
- package/src/taxonomies/index.ts +88 -82
- package/src/taxonomies/term-counts.ts +23 -12
- package/src/ui-comments.ts +19 -0
- package/src/ui.ts +11 -3
- package/src/utils/slugify.ts +1 -29
- package/src/virtual-modules.d.ts +24 -0
- package/dist/adapters-C0ONzkUP.d.mts.map +0 -1
- package/dist/after-B1IIdH3Y.mjs +0 -29
- package/dist/after-B1IIdH3Y.mjs.map +0 -1
- package/dist/api-DgPbMlng.mjs.map +0 -1
- package/dist/apply-1_6ra7NP.mjs.map +0 -1
- package/dist/cache-B26cufFd.mjs +0 -65
- package/dist/cache-B26cufFd.mjs.map +0 -1
- package/dist/config-DyF3OKg_.mjs.map +0 -1
- package/dist/content-CpfKV9QE.mjs.map +0 -1
- package/dist/content-refresh-DgqUBeTv.mjs +0 -1629
- package/dist/content-refresh-DgqUBeTv.mjs.map +0 -1
- package/dist/context-kO4YZB03.mjs.map +0 -1
- package/dist/dashboard-DoBwPnON.mjs.map +0 -1
- package/dist/dialect-helpers-DFwyWjTM.mjs.map +0 -1
- package/dist/errors-DtEXIQQV.mjs.map +0 -1
- package/dist/fts-manager-vncsZIJm.mjs.map +0 -1
- package/dist/index-BeNto_Vs.d.mts.map +0 -1
- package/dist/index-j9rTh3cp.d.mts.map +0 -1
- package/dist/loader-C1XOLV5b.mjs.map +0 -1
- package/dist/media-DWIzoVFL.mjs.map +0 -1
- package/dist/media-allowlist-B7TPD6aq.mjs.map +0 -1
- package/dist/media-usage-DPJcNaAq.mjs.map +0 -1
- package/dist/menus-C6qhqP4X.mjs.map +0 -1
- package/dist/object-cache-Bok5j2ae.mjs.map +0 -1
- package/dist/options-aeIs8WFu.d.mts.map +0 -1
- package/dist/public-url-DSGTnJFw.mjs.map +0 -1
- package/dist/query-DCiXI7OZ.mjs.map +0 -1
- package/dist/redirects-BOn2DIUs.mjs.map +0 -1
- package/dist/registry-BP1JK2xh.mjs +0 -795
- package/dist/registry-BP1JK2xh.mjs.map +0 -1
- package/dist/relations-DHFQGX0h.d.mts.map +0 -1
- package/dist/runner-BsI18UgP.mjs.map +0 -1
- package/dist/search-CeO678cp.mjs.map +0 -1
- package/dist/seo-C3GDfT0V.mjs.map +0 -1
- package/dist/site-url-8N3kSyVZ.mjs +0 -13
- package/dist/site-url-8N3kSyVZ.mjs.map +0 -1
- package/dist/slugify-C_tqlU4G.mjs +0 -30
- package/dist/slugify-C_tqlU4G.mjs.map +0 -1
- package/dist/taxonomies-B61CRSha.mjs.map +0 -1
- package/dist/taxonomies-Dbdki3Un.mjs.map +0 -1
- package/dist/taxonomy-DvwWAPvA.mjs +0 -304
- package/dist/taxonomy-DvwWAPvA.mjs.map +0 -1
- package/dist/types-84Bque3-.d.mts.map +0 -1
- package/dist/types-BQY7VYw6.d.mts.map +0 -1
- package/dist/types-DqZVPILm.d.mts.map +0 -1
- package/dist/types-XrQQ-Aex.mjs.map +0 -1
- package/dist/types-jISEalTX.d.mts.map +0 -1
- package/dist/types-o7xo7VgH.mjs.map +0 -1
- package/dist/validate-Bs_wT2ul.mjs.map +0 -1
- package/dist/validate-CdYcY-Ln.d.mts.map +0 -1
- package/dist/validate-VPnKoIzW.mjs.map +0 -1
- package/dist/version-Bdfb34kk.mjs +0 -7
- package/dist/zod-generator-DMzfga3g.mjs.map +0 -1
- /package/dist/{api-tokens-CEsW_jCg.mjs → api-tokens-CyTsLszZ.mjs} +0 -0
- /package/dist/{ssrf-BB__xfNM.mjs → ssrf-DpXgKsna.mjs} +0 -0
- /package/dist/{types-CK0LxPUQ.mjs → types-CP1CflWf.mjs} +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { sql, type Kysely } from "kysely";
|
|
2
2
|
import { ulid } from "ulidx";
|
|
3
3
|
|
|
4
|
+
import type { ContentFieldFilterValue, ContentFieldFilters } from "../../content-list-query.js";
|
|
4
5
|
import { invalidateCollectionCache } from "../../object-cache/index.js";
|
|
6
|
+
import { isIndexableFieldType, type FieldType } from "../../schema/types.js";
|
|
5
7
|
import { buildFtsPrefixMatch, buildSlugGlobPrefix } from "../../search/match.js";
|
|
6
8
|
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
7
9
|
import { isMissingTableError } from "../../utils/db-errors.js";
|
|
@@ -16,9 +18,13 @@ import type {
|
|
|
16
18
|
FindManyResult,
|
|
17
19
|
ContentItem,
|
|
18
20
|
ContentDateField,
|
|
21
|
+
ContentBylineFilter,
|
|
19
22
|
} from "./types.js";
|
|
20
23
|
import {
|
|
24
|
+
ContentCollectionNotFoundError,
|
|
25
|
+
ContentMutationConflictError,
|
|
21
26
|
EmDashValidationError,
|
|
27
|
+
InvalidCursorError,
|
|
22
28
|
ScheduledNotDueError,
|
|
23
29
|
encodeCursor,
|
|
24
30
|
decodeCursor,
|
|
@@ -26,9 +32,149 @@ import {
|
|
|
26
32
|
|
|
27
33
|
// Regex pattern for ULID validation
|
|
28
34
|
const ULID_PATTERN = /^[0-9A-Z]{26}$/;
|
|
35
|
+
const SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
|
|
36
|
+
const MAX_DRAFT_STAGE_ATTEMPTS = 32;
|
|
29
37
|
|
|
30
38
|
// LIKE wildcards that must be escaped so user search input is matched literally.
|
|
31
39
|
const LIKE_WILDCARD_RE = /[\\%_]/g;
|
|
40
|
+
const MAX_INDEXED_FIELD_FILTERS = 20;
|
|
41
|
+
const MAX_IN_FILTER_VALUES = SQL_BATCH_SIZE;
|
|
42
|
+
const MAX_FILTER_STRING_LENGTH = 2048;
|
|
43
|
+
|
|
44
|
+
type NormalizedFilterScalar = string | number;
|
|
45
|
+
|
|
46
|
+
type ResolvedFieldFilter =
|
|
47
|
+
| { column: string; kind: "null" }
|
|
48
|
+
| { column: string; kind: "exact"; value: NormalizedFilterScalar }
|
|
49
|
+
| { column: string; kind: "in"; values: NormalizedFilterScalar[] }
|
|
50
|
+
| {
|
|
51
|
+
column: string;
|
|
52
|
+
kind: "range";
|
|
53
|
+
bounds: Partial<Record<"gt" | "gte" | "lt" | "lte", NormalizedFilterScalar>>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function nullableColumnMatch(column: string, value: string | null): ReturnType<typeof sql> {
|
|
57
|
+
validateIdentifier(column, "content column");
|
|
58
|
+
return value === null ? sql`${sql.ref(column)} IS NULL` : sql`${sql.ref(column)} = ${value}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sameStoredValue(left: unknown, right: unknown): boolean {
|
|
62
|
+
return Object.is(serializeValue(left), serializeValue(right));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function matchesPublication(
|
|
66
|
+
observed: ContentItem,
|
|
67
|
+
existing: ContentItem,
|
|
68
|
+
revision: { data: Record<string, unknown> },
|
|
69
|
+
revisionId: string,
|
|
70
|
+
slug: string | null,
|
|
71
|
+
publishedAt: string,
|
|
72
|
+
updatedAt: string,
|
|
73
|
+
): boolean {
|
|
74
|
+
if (
|
|
75
|
+
observed.version !== existing.version + 1 ||
|
|
76
|
+
observed.status !== "published" ||
|
|
77
|
+
observed.slug !== slug ||
|
|
78
|
+
observed.liveRevisionId !== revisionId ||
|
|
79
|
+
observed.draftRevisionId !== null ||
|
|
80
|
+
observed.scheduledAt !== null ||
|
|
81
|
+
observed.publishedAt !== publishedAt ||
|
|
82
|
+
observed.updatedAt !== updatedAt
|
|
83
|
+
) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return Object.entries(revision.data).every(
|
|
88
|
+
([key, value]) =>
|
|
89
|
+
SYSTEM_COLUMNS.has(key) || key.startsWith("_") || sameStoredValue(observed.data[key], value),
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function matchesLifecyclePublication(
|
|
94
|
+
observed: ContentItem,
|
|
95
|
+
existing: ContentItem,
|
|
96
|
+
liveRevisionId: string,
|
|
97
|
+
publishedAt: string,
|
|
98
|
+
updatedAt: string,
|
|
99
|
+
): boolean {
|
|
100
|
+
if (
|
|
101
|
+
observed.version !== existing.version + 1 ||
|
|
102
|
+
observed.status !== "published" ||
|
|
103
|
+
observed.slug !== existing.slug ||
|
|
104
|
+
observed.liveRevisionId !== liveRevisionId ||
|
|
105
|
+
observed.draftRevisionId !== null ||
|
|
106
|
+
observed.scheduledAt !== null ||
|
|
107
|
+
observed.publishedAt !== publishedAt ||
|
|
108
|
+
observed.updatedAt !== updatedAt
|
|
109
|
+
) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return Object.entries(existing.data).every(([key, value]) =>
|
|
114
|
+
sameStoredValue(observed.data[key], value),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function matchesPublicationFence(observed: ContentItem, existing: ContentItem): boolean {
|
|
119
|
+
return (
|
|
120
|
+
observed.version === existing.version &&
|
|
121
|
+
observed.status === existing.status &&
|
|
122
|
+
observed.liveRevisionId === existing.liveRevisionId &&
|
|
123
|
+
observed.draftRevisionId === existing.draftRevisionId &&
|
|
124
|
+
observed.scheduledAt === existing.scheduledAt
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isConfirmedStatementFailure(error: unknown): boolean {
|
|
129
|
+
if (typeof error !== "object" || error === null || !("code" in error)) return false;
|
|
130
|
+
const code = (error as { code?: unknown }).code;
|
|
131
|
+
return typeof code === "string" && (code.startsWith("SQLITE_") || SQLSTATE_PATTERN.test(code));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
interface ResolvedOrderField {
|
|
135
|
+
column: string;
|
|
136
|
+
indexedCustomField: boolean;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
type IndexedOrderValue = string | number | null;
|
|
140
|
+
|
|
141
|
+
interface IndexedFieldCursorPayload {
|
|
142
|
+
version: 1;
|
|
143
|
+
field: string;
|
|
144
|
+
value: IndexedOrderValue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function encodeIndexedFieldCursor(field: string, value: IndexedOrderValue, id: string): string {
|
|
148
|
+
const payload: IndexedFieldCursorPayload = { version: 1, field, value };
|
|
149
|
+
return encodeCursor(JSON.stringify(payload), id);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function decodeIndexedFieldCursor(
|
|
153
|
+
cursor: string,
|
|
154
|
+
field: string,
|
|
155
|
+
): { value: IndexedOrderValue; id: string } {
|
|
156
|
+
const { orderValue, id } = decodeCursor(cursor);
|
|
157
|
+
let payload: unknown;
|
|
158
|
+
try {
|
|
159
|
+
payload = JSON.parse(orderValue);
|
|
160
|
+
} catch {
|
|
161
|
+
throw new InvalidCursorError(cursor);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (payload === null || typeof payload !== "object") {
|
|
165
|
+
throw new InvalidCursorError(cursor);
|
|
166
|
+
}
|
|
167
|
+
const candidate = payload as Partial<IndexedFieldCursorPayload>;
|
|
168
|
+
const validValue =
|
|
169
|
+
candidate.value === null ||
|
|
170
|
+
typeof candidate.value === "string" ||
|
|
171
|
+
typeof candidate.value === "number";
|
|
172
|
+
if (candidate.version !== 1 || candidate.field !== field || !validValue) {
|
|
173
|
+
throw new InvalidCursorError(cursor);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { value: candidate.value as IndexedOrderValue, id };
|
|
177
|
+
}
|
|
32
178
|
|
|
33
179
|
/**
|
|
34
180
|
* Whitelist mapping a public date-filter field to its physical column. Keeping
|
|
@@ -42,6 +188,29 @@ const DATE_FILTER_COLUMNS: Record<ContentDateField, "created_at" | "updated_at"
|
|
|
42
188
|
publishedAt: "published_at",
|
|
43
189
|
};
|
|
44
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Built-in sort fields → their physical columns. A closed set that blocks
|
|
193
|
+
* sorting by arbitrary columns; per-collection fields are allowed
|
|
194
|
+
* separately via `mapOrderField`'s `sortableExtras`.
|
|
195
|
+
*/
|
|
196
|
+
const ORDER_FIELD_COLUMNS: Record<string, string> = {
|
|
197
|
+
createdAt: "created_at",
|
|
198
|
+
updatedAt: "updated_at",
|
|
199
|
+
publishedAt: "published_at",
|
|
200
|
+
scheduledAt: "scheduled_at",
|
|
201
|
+
deletedAt: "deleted_at",
|
|
202
|
+
title: "title",
|
|
203
|
+
name: "name",
|
|
204
|
+
slug: "slug",
|
|
205
|
+
status: "status",
|
|
206
|
+
locale: "locale",
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
/** True when `field` maps to a system column and needs no per-collection resolution. */
|
|
210
|
+
export function isSystemOrderField(field: string): boolean {
|
|
211
|
+
return field in ORDER_FIELD_COLUMNS;
|
|
212
|
+
}
|
|
213
|
+
|
|
45
214
|
/**
|
|
46
215
|
* System columns that exist in every ec_* table
|
|
47
216
|
*/
|
|
@@ -89,6 +258,16 @@ function serializeValue(value: unknown): unknown {
|
|
|
89
258
|
return value;
|
|
90
259
|
}
|
|
91
260
|
|
|
261
|
+
function writableContentData(data: Record<string, unknown>): Record<string, unknown> {
|
|
262
|
+
const writable: Record<string, unknown> = {};
|
|
263
|
+
for (const [key, value] of Object.entries(data)) {
|
|
264
|
+
if (SYSTEM_COLUMNS.has(key)) continue;
|
|
265
|
+
validateIdentifier(key, "content field name");
|
|
266
|
+
writable[key] = value;
|
|
267
|
+
}
|
|
268
|
+
return writable;
|
|
269
|
+
}
|
|
270
|
+
|
|
92
271
|
/**
|
|
93
272
|
* Deserialize a value from database storage
|
|
94
273
|
* Attempts to parse JSON strings that look like objects/arrays
|
|
@@ -130,7 +309,7 @@ export class ContentRepository {
|
|
|
130
309
|
* Create a new content item
|
|
131
310
|
*/
|
|
132
311
|
async create(input: CreateContentInput): Promise<ContentItem> {
|
|
133
|
-
const id = ulid();
|
|
312
|
+
const id = input.id ?? ulid();
|
|
134
313
|
const now = new Date().toISOString();
|
|
135
314
|
|
|
136
315
|
const {
|
|
@@ -228,7 +407,7 @@ export class ContentRepository {
|
|
|
228
407
|
* (optionally scoped to a locale) and appends a numeric suffix (`-1`,
|
|
229
408
|
* `-2`, etc.) on collision to guarantee uniqueness.
|
|
230
409
|
*
|
|
231
|
-
* Returns
|
|
410
|
+
* Returns null when slug normalization cannot produce a value.
|
|
232
411
|
*/
|
|
233
412
|
async generateUniqueSlug(type: string, text: string, locale?: string): Promise<string | null> {
|
|
234
413
|
const baseSlug = slugify(text);
|
|
@@ -502,7 +681,13 @@ export class ContentRepository {
|
|
|
502
681
|
// Determine ordering
|
|
503
682
|
const orderField = options.orderBy?.field || "createdAt";
|
|
504
683
|
const orderDirection = options.orderBy?.direction || "desc";
|
|
505
|
-
const
|
|
684
|
+
const resolvedOrderField = await this.resolveOrderField(
|
|
685
|
+
type,
|
|
686
|
+
orderField,
|
|
687
|
+
options.sortableExtras,
|
|
688
|
+
);
|
|
689
|
+
const dbField = resolvedOrderField.column;
|
|
690
|
+
const resolvedFieldFilters = await this.resolveFieldFilters(type, options.where?.fieldFilters);
|
|
506
691
|
|
|
507
692
|
// Validate order direction to prevent injection
|
|
508
693
|
const safeOrderDirection = orderDirection.toLowerCase() === "asc" ? "ASC" : "DESC";
|
|
@@ -529,40 +714,90 @@ export class ContentRepository {
|
|
|
529
714
|
|
|
530
715
|
query = this.applySearchFilter(query, options.where, type);
|
|
531
716
|
query = this.applyDateFilter(query, options.where);
|
|
717
|
+
query = this.applyBylineFilter(query, options.where, type);
|
|
718
|
+
query = this.applyFieldFilters(query, resolvedFieldFilters);
|
|
532
719
|
|
|
533
720
|
// Handle cursor pagination — decodeCursor throws InvalidCursorError
|
|
534
721
|
// on malformed input; let it propagate so handlers surface a
|
|
535
722
|
// structured INVALID_CURSOR rather than silently returning page 1.
|
|
536
723
|
if (options.cursor) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
724
|
+
if (resolvedOrderField.indexedCustomField) {
|
|
725
|
+
const { value, id: cursorId } = decodeIndexedFieldCursor(options.cursor, orderField);
|
|
726
|
+
const isPresent = sql<boolean>`${sql.ref(dbField)} IS NOT NULL`;
|
|
727
|
+
const falseLiteral = sql<boolean>`FALSE`;
|
|
728
|
+
const trueLiteral = sql<boolean>`TRUE`;
|
|
729
|
+
if (safeOrderDirection === "ASC" && value === null) {
|
|
730
|
+
query = query.where(sql<boolean>`
|
|
731
|
+
(${isPresent}) > ${falseLiteral}
|
|
732
|
+
OR ((${isPresent}) = ${falseLiteral} AND ${sql.ref("id")} > ${cursorId})
|
|
733
|
+
`);
|
|
734
|
+
} else if (safeOrderDirection === "DESC" && value === null) {
|
|
735
|
+
query = query.where(sql<boolean>`
|
|
736
|
+
(${isPresent}) = ${falseLiteral} AND ${sql.ref("id")} < ${cursorId}
|
|
737
|
+
`);
|
|
738
|
+
} else if (safeOrderDirection === "ASC") {
|
|
739
|
+
query = query.where(sql<boolean>`
|
|
740
|
+
(${isPresent}, ${sql.ref(dbField)}, ${sql.ref("id")})
|
|
741
|
+
> (${trueLiteral}, ${value}, ${cursorId})
|
|
742
|
+
`);
|
|
743
|
+
} else {
|
|
744
|
+
query = query.where(sql<boolean>`
|
|
745
|
+
(${isPresent}, ${sql.ref(dbField)}, ${sql.ref("id")})
|
|
746
|
+
< (${trueLiteral}, ${value}, ${cursorId})
|
|
747
|
+
`);
|
|
748
|
+
}
|
|
546
749
|
} else {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
750
|
+
const { orderValue, id: cursorId } = decodeCursor(options.cursor);
|
|
751
|
+
|
|
752
|
+
if (safeOrderDirection === "DESC") {
|
|
753
|
+
query = query.where((eb) =>
|
|
754
|
+
eb.or([
|
|
755
|
+
eb(dbField as any, "<", orderValue),
|
|
756
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", "<", cursorId)]),
|
|
757
|
+
]),
|
|
758
|
+
);
|
|
759
|
+
} else {
|
|
760
|
+
query = query.where((eb) =>
|
|
761
|
+
eb.or([
|
|
762
|
+
eb(dbField as any, ">", orderValue),
|
|
763
|
+
eb.and([eb(dbField as any, "=", orderValue), eb("id", ">", cursorId)]),
|
|
764
|
+
]),
|
|
765
|
+
);
|
|
766
|
+
}
|
|
553
767
|
}
|
|
554
768
|
}
|
|
555
769
|
|
|
556
770
|
// Apply ordering and limit
|
|
557
|
-
|
|
558
|
-
.
|
|
559
|
-
|
|
560
|
-
|
|
771
|
+
const indexedOrderFilter = resolvedOrderField.indexedCustomField
|
|
772
|
+
? resolvedFieldFilters.find((filter) => filter.column === dbField)
|
|
773
|
+
: undefined;
|
|
774
|
+
if (resolvedOrderField.indexedCustomField && !indexedOrderFilter) {
|
|
775
|
+
query = query.orderBy(
|
|
776
|
+
sql<boolean>`${sql.ref(dbField)} IS NOT NULL`,
|
|
777
|
+
safeOrderDirection === "ASC" ? "asc" : "desc",
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
if (indexedOrderFilter?.kind !== "null") {
|
|
781
|
+
query = query.orderBy(dbField as any, safeOrderDirection === "ASC" ? "asc" : "desc");
|
|
782
|
+
}
|
|
783
|
+
query = query.orderBy("id", safeOrderDirection === "ASC" ? "asc" : "desc").limit(limit + 1);
|
|
561
784
|
|
|
562
785
|
// Run the page fetch and the unbounded count together — the UI needs
|
|
563
786
|
// both to render a stable denominator (kept on every page intentionally),
|
|
564
787
|
// and issuing them in parallel on SQLite is essentially free.
|
|
565
|
-
|
|
788
|
+
//
|
|
789
|
+
// Settled rather than raced: a collection whose table is missing rejects
|
|
790
|
+
// both, and `Promise.all` returns on the first. The loser stays in flight
|
|
791
|
+
// holding a pooled connection, so a Postgres pool destroyed in that window
|
|
792
|
+
// never finishes closing.
|
|
793
|
+
const [rowsResult, countResult] = await Promise.allSettled([
|
|
794
|
+
query.execute(),
|
|
795
|
+
this.countWithResolvedFilters(type, options.where, resolvedFieldFilters),
|
|
796
|
+
]);
|
|
797
|
+
if (rowsResult.status === "rejected") throw rowsResult.reason;
|
|
798
|
+
if (countResult.status === "rejected") throw countResult.reason;
|
|
799
|
+
const rows = rowsResult.value;
|
|
800
|
+
const total = countResult.value;
|
|
566
801
|
const hasMore = rows.length > limit;
|
|
567
802
|
const items = rows.slice(0, limit);
|
|
568
803
|
|
|
@@ -574,11 +809,26 @@ export class ContentRepository {
|
|
|
574
809
|
if (hasMore && items.length > 0) {
|
|
575
810
|
const lastRow = items.at(-1) as Record<string, unknown>;
|
|
576
811
|
const lastOrderValue = lastRow[dbField];
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
812
|
+
if (resolvedOrderField.indexedCustomField) {
|
|
813
|
+
if (
|
|
814
|
+
lastOrderValue !== null &&
|
|
815
|
+
typeof lastOrderValue !== "string" &&
|
|
816
|
+
typeof lastOrderValue !== "number"
|
|
817
|
+
) {
|
|
818
|
+
throw new EmDashValidationError(`Invalid indexed value for order field: ${orderField}`);
|
|
819
|
+
}
|
|
820
|
+
mappedResult.nextCursor = encodeIndexedFieldCursor(
|
|
821
|
+
orderField,
|
|
822
|
+
lastOrderValue,
|
|
823
|
+
String(lastRow.id),
|
|
824
|
+
);
|
|
825
|
+
} else {
|
|
826
|
+
const orderStr =
|
|
827
|
+
typeof lastOrderValue === "string" || typeof lastOrderValue === "number"
|
|
828
|
+
? String(lastOrderValue)
|
|
829
|
+
: "";
|
|
830
|
+
mappedResult.nextCursor = encodeCursor(orderStr, String(lastRow.id));
|
|
831
|
+
}
|
|
582
832
|
}
|
|
583
833
|
|
|
584
834
|
return mappedResult;
|
|
@@ -591,15 +841,8 @@ export class ContentRepository {
|
|
|
591
841
|
const tableName = getTableName(type);
|
|
592
842
|
const now = new Date().toISOString();
|
|
593
843
|
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
// (version:updated_at) always moves — concurrent editors still get a
|
|
597
|
-
// 409 on stale saves. `updated_at` is stamped only when the request
|
|
598
|
-
// actually carries column writes: a draft-only save on a
|
|
599
|
-
// revision-supporting collection resolves to a column no-op here (all
|
|
600
|
-
// data went into draft storage), and stamping it anyway would register
|
|
601
|
-
// a phantom modification for public "last modified" consumers like
|
|
602
|
-
// sitemap <lastmod> and JSON-LD dateModified (#2143).
|
|
844
|
+
// Every update advances the optimistic-concurrency version. updated_at
|
|
845
|
+
// advances only when a content-row column actually changes.
|
|
603
846
|
const updates: Record<string, unknown> = {};
|
|
604
847
|
|
|
605
848
|
if (input.status !== undefined) {
|
|
@@ -628,11 +871,8 @@ export class ContentRepository {
|
|
|
628
871
|
|
|
629
872
|
// Update data fields (skip system columns to prevent injection via data)
|
|
630
873
|
if (input.data !== undefined && typeof input.data === "object") {
|
|
631
|
-
for (const [key, value] of Object.entries(input.data)) {
|
|
632
|
-
|
|
633
|
-
validateIdentifier(key, "content field name");
|
|
634
|
-
updates[key] = serializeValue(value);
|
|
635
|
-
}
|
|
874
|
+
for (const [key, value] of Object.entries(writableContentData(input.data))) {
|
|
875
|
+
updates[key] = serializeValue(value);
|
|
636
876
|
}
|
|
637
877
|
}
|
|
638
878
|
|
|
@@ -649,18 +889,6 @@ export class ContentRepository {
|
|
|
649
889
|
.where("deleted_at" as never, "is", null)
|
|
650
890
|
.execute();
|
|
651
891
|
|
|
652
|
-
// Re-stamp the taxonomy pivot only when a denormalized column actually
|
|
653
|
-
// moved (status/publishedAt/scheduledAt). A plain content edit bumps
|
|
654
|
-
// `updated_at` — which is not denormalized — so it needs no pivot write,
|
|
655
|
-
// keeping the common edit path free of taxonomy write amplification.
|
|
656
|
-
if (
|
|
657
|
-
input.status !== undefined ||
|
|
658
|
-
input.publishedAt !== undefined ||
|
|
659
|
-
input.scheduledAt !== undefined
|
|
660
|
-
) {
|
|
661
|
-
await this.restampEntryPivot(type, id);
|
|
662
|
-
}
|
|
663
|
-
|
|
664
892
|
if (hasColumnWrites) invalidateCollectionCache(type);
|
|
665
893
|
|
|
666
894
|
const updated = await this.findById(type, id);
|
|
@@ -671,6 +899,159 @@ export class ContentRepository {
|
|
|
671
899
|
return updated;
|
|
672
900
|
}
|
|
673
901
|
|
|
902
|
+
/**
|
|
903
|
+
* Update plugin-authored fields without letting content columns diverge
|
|
904
|
+
* from the revision pointers that publication promotes.
|
|
905
|
+
*/
|
|
906
|
+
async updateDraftAware(
|
|
907
|
+
type: string,
|
|
908
|
+
id: string,
|
|
909
|
+
input: UpdateContentInput,
|
|
910
|
+
): Promise<ContentItem> {
|
|
911
|
+
const data = input.data ? writableContentData(input.data) : {};
|
|
912
|
+
const stagedSlug = typeof input.slug === "string" ? input.slug : undefined;
|
|
913
|
+
const hasDraftUpdate = Object.keys(data).length > 0 || stagedSlug !== undefined;
|
|
914
|
+
|
|
915
|
+
if (!hasDraftUpdate) {
|
|
916
|
+
return this.update(type, id, { ...input, data });
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const collectionRows = await this.db
|
|
920
|
+
.selectFrom("_emdash_collections as collection")
|
|
921
|
+
.leftJoin("_emdash_fields as field", "field.collection_id", "collection.id")
|
|
922
|
+
.select(["collection.supports", "field.slug as fieldSlug"])
|
|
923
|
+
.where("collection.slug", "=", type)
|
|
924
|
+
.execute();
|
|
925
|
+
const supportsRaw = collectionRows[0]?.supports;
|
|
926
|
+
const supports: unknown = supportsRaw ? JSON.parse(supportsRaw) : [];
|
|
927
|
+
if (!Array.isArray(supports) || !supports.includes("revisions")) {
|
|
928
|
+
return this.update(type, id, { ...input, data });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const fieldSlugs = new Set(collectionRows.map((row) => row.fieldSlug).filter(Boolean));
|
|
932
|
+
for (const field of Object.keys(data)) {
|
|
933
|
+
if (!fieldSlugs.has(field)) {
|
|
934
|
+
throw new EmDashValidationError(`Unknown field '${field}' in collection '${type}'`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
939
|
+
let existing = await this.findById(type, id);
|
|
940
|
+
|
|
941
|
+
for (let attempt = 0; existing && attempt < MAX_DRAFT_STAGE_ATTEMPTS; attempt++) {
|
|
942
|
+
let baseData = existing.data;
|
|
943
|
+
if (existing.draftRevisionId) {
|
|
944
|
+
const draft = await revisionRepo.findById(existing.draftRevisionId);
|
|
945
|
+
if (draft) baseData = draft.data;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const mergedData = { ...baseData, ...data };
|
|
949
|
+
if (stagedSlug !== undefined) mergedData._slug = stagedSlug;
|
|
950
|
+
const revision = await revisionRepo.create({
|
|
951
|
+
collection: type,
|
|
952
|
+
entryId: id,
|
|
953
|
+
data: mergedData,
|
|
954
|
+
...(input.authorId ? { authorId: input.authorId } : {}),
|
|
955
|
+
});
|
|
956
|
+
|
|
957
|
+
let staged: boolean;
|
|
958
|
+
try {
|
|
959
|
+
staged = await this.replaceDraftRevisionForUpdate(type, id, revision.id, existing, input);
|
|
960
|
+
} catch (error) {
|
|
961
|
+
await this.deleteUnstagedRevision(revisionRepo, type, id, revision.id);
|
|
962
|
+
throw error;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
if (staged) {
|
|
966
|
+
const updated = await this.findById(type, id);
|
|
967
|
+
if (!updated) throw new Error("Content not found");
|
|
968
|
+
const draftData: Record<string, unknown> = {};
|
|
969
|
+
for (const [key, value] of Object.entries(mergedData)) {
|
|
970
|
+
if (!key.startsWith("_")) draftData[key] = value;
|
|
971
|
+
}
|
|
972
|
+
return { ...updated, data: { ...updated.data, ...draftData } };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
await this.deleteUnstagedRevision(revisionRepo, type, id, revision.id);
|
|
976
|
+
existing = await this.findById(type, id);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
if (!existing) throw new Error("Content not found");
|
|
980
|
+
throw new ContentMutationConflictError();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
private async deleteUnstagedRevision(
|
|
984
|
+
revisionRepo: RevisionRepository,
|
|
985
|
+
type: string,
|
|
986
|
+
id: string,
|
|
987
|
+
revisionId: string,
|
|
988
|
+
): Promise<void> {
|
|
989
|
+
try {
|
|
990
|
+
await revisionRepo.deleteIfUnreferenced(type, id, revisionId);
|
|
991
|
+
} catch (error) {
|
|
992
|
+
console.error(`[content] Failed to clean up unstaged revision ${revisionId}:`, error);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
private async replaceDraftRevisionForUpdate(
|
|
997
|
+
type: string,
|
|
998
|
+
id: string,
|
|
999
|
+
revisionId: string,
|
|
1000
|
+
expected: ContentItem,
|
|
1001
|
+
input: UpdateContentInput,
|
|
1002
|
+
): Promise<boolean> {
|
|
1003
|
+
const tableName = getTableName(type);
|
|
1004
|
+
const assignments = [sql`draft_revision_id = ${revisionId}`];
|
|
1005
|
+
let liveMetadataChanged = false;
|
|
1006
|
+
|
|
1007
|
+
if (input.status !== undefined) {
|
|
1008
|
+
assignments.push(sql`status = ${input.status}`);
|
|
1009
|
+
liveMetadataChanged = true;
|
|
1010
|
+
}
|
|
1011
|
+
if (input.slug === null) {
|
|
1012
|
+
assignments.push(sql`slug = NULL`);
|
|
1013
|
+
liveMetadataChanged = true;
|
|
1014
|
+
}
|
|
1015
|
+
if (input.publishedAt !== undefined) {
|
|
1016
|
+
assignments.push(sql`published_at = ${input.publishedAt}`);
|
|
1017
|
+
liveMetadataChanged = true;
|
|
1018
|
+
}
|
|
1019
|
+
if (input.scheduledAt !== undefined) {
|
|
1020
|
+
assignments.push(sql`scheduled_at = ${input.scheduledAt}`);
|
|
1021
|
+
liveMetadataChanged = true;
|
|
1022
|
+
}
|
|
1023
|
+
if (input.authorId !== undefined) {
|
|
1024
|
+
assignments.push(sql`author_id = ${input.authorId}`);
|
|
1025
|
+
liveMetadataChanged = true;
|
|
1026
|
+
}
|
|
1027
|
+
if (input.primaryBylineId !== undefined) {
|
|
1028
|
+
assignments.push(sql`primary_byline_id = ${input.primaryBylineId}`);
|
|
1029
|
+
liveMetadataChanged = true;
|
|
1030
|
+
}
|
|
1031
|
+
if (liveMetadataChanged) assignments.push(sql`updated_at = ${new Date().toISOString()}`);
|
|
1032
|
+
assignments.push(sql`version = version + 1`);
|
|
1033
|
+
|
|
1034
|
+
const result = await sql`
|
|
1035
|
+
UPDATE ${sql.ref(tableName)}
|
|
1036
|
+
SET ${sql.join(assignments, sql`, `)}
|
|
1037
|
+
WHERE id = ${id}
|
|
1038
|
+
AND deleted_at IS NULL
|
|
1039
|
+
AND version = ${expected.version}
|
|
1040
|
+
AND ${nullableColumnMatch("live_revision_id", expected.liveRevisionId)}
|
|
1041
|
+
AND ${nullableColumnMatch("draft_revision_id", expected.draftRevisionId)}
|
|
1042
|
+
AND EXISTS (
|
|
1043
|
+
SELECT 1 FROM revisions
|
|
1044
|
+
WHERE revisions.id = ${revisionId}
|
|
1045
|
+
AND revisions.collection = ${type}
|
|
1046
|
+
AND revisions.entry_id = ${id}
|
|
1047
|
+
)
|
|
1048
|
+
`.execute(this.db);
|
|
1049
|
+
|
|
1050
|
+
const changed = (result.numAffectedRows ?? 0n) > 0n;
|
|
1051
|
+
if (changed && liveMetadataChanged) invalidateCollectionCache(type);
|
|
1052
|
+
return changed;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
674
1055
|
/**
|
|
675
1056
|
* Delete content (soft delete - moves to trash)
|
|
676
1057
|
*/
|
|
@@ -687,7 +1068,6 @@ export class ContentRepository {
|
|
|
687
1068
|
|
|
688
1069
|
const changed = (result.numAffectedRows ?? 0n) > 0n;
|
|
689
1070
|
if (changed) {
|
|
690
|
-
await this.restampEntryPivot(type, id);
|
|
691
1071
|
invalidateCollectionCache(type);
|
|
692
1072
|
}
|
|
693
1073
|
return changed;
|
|
@@ -710,36 +1090,10 @@ export class ContentRepository {
|
|
|
710
1090
|
const restored = result.rows[0];
|
|
711
1091
|
if (!restored) return null;
|
|
712
1092
|
|
|
713
|
-
await this.restampEntryPivot(type, id);
|
|
714
1093
|
invalidateCollectionCache(type);
|
|
715
1094
|
return this.mapRow(type, restored);
|
|
716
1095
|
}
|
|
717
1096
|
|
|
718
|
-
/**
|
|
719
|
-
* Re-stamp the denormalized filter + sort columns on every
|
|
720
|
-
* `content_taxonomies` pivot row for an entry from its authoritative `ec_*`
|
|
721
|
-
* row (migration 051). Called after any mutation that moves one of those
|
|
722
|
-
* columns so a taxonomy-filtered listing can seek the entry directly.
|
|
723
|
-
*
|
|
724
|
-
* A single correlated `UPDATE` reads the post-mutation values from `ec_*`, so
|
|
725
|
-
* the pivot converges to the authoritative row. This is NOT atomic with the
|
|
726
|
-
* `ec_*` mutation on D1 (no transactions), which is why the read path
|
|
727
|
-
* re-checks the real predicates on the joined `ec_*` row. Untagged entries
|
|
728
|
-
* have no pivot rows, so the statement is a cheap no-op for them.
|
|
729
|
-
*/
|
|
730
|
-
private async restampEntryPivot(type: string, id: string): Promise<void> {
|
|
731
|
-
const tableName = getTableName(type);
|
|
732
|
-
await sql`
|
|
733
|
-
UPDATE content_taxonomies
|
|
734
|
-
SET (status, scheduled_at, deleted_at, locale, published_at, created_at) = (
|
|
735
|
-
SELECT status, scheduled_at, deleted_at, locale, published_at, created_at
|
|
736
|
-
FROM ${sql.ref(tableName)}
|
|
737
|
-
WHERE ${sql.ref(tableName)}.id = ${id}
|
|
738
|
-
)
|
|
739
|
-
WHERE collection = ${type} AND entry_id = ${id}
|
|
740
|
-
`.execute(this.db);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
1097
|
/**
|
|
744
1098
|
* Permanently delete content (cannot be undone)
|
|
745
1099
|
*/
|
|
@@ -905,7 +1259,7 @@ export class ContentRepository {
|
|
|
905
1259
|
eb.or(
|
|
906
1260
|
columns.map((col) => {
|
|
907
1261
|
validateIdentifier(col, "search column");
|
|
908
|
-
return
|
|
1262
|
+
return sql<boolean>`lower(CAST(${sql.ref(col)} AS TEXT)) LIKE lower(${pattern}) ESCAPE '\\'`;
|
|
909
1263
|
}),
|
|
910
1264
|
),
|
|
911
1265
|
);
|
|
@@ -936,10 +1290,128 @@ export class ContentRepository {
|
|
|
936
1290
|
return next;
|
|
937
1291
|
}
|
|
938
1292
|
|
|
1293
|
+
/**
|
|
1294
|
+
* Apply the optional byline filter as a correlated (NOT) EXISTS against
|
|
1295
|
+
* `_emdash_content_bylines`.
|
|
1296
|
+
*
|
|
1297
|
+
* Correlating from the content table preserves the outer sort index so
|
|
1298
|
+
* `LIMIT` can short-circuit. `mode: "none"` tests the junction rather than
|
|
1299
|
+
* `primary_byline_id` because the two are written in the same call but
|
|
1300
|
+
* are not atomically consistent, so the junction is authoritative.
|
|
1301
|
+
*
|
|
1302
|
+
* Whether a credit *renders* is locale-scoped; whether one *exists* is
|
|
1303
|
+
* not. Both are needed: the first decides what the filter matches, the
|
|
1304
|
+
* second decides whether the author fallback applies at all.
|
|
1305
|
+
*/
|
|
1306
|
+
private applyBylineFilter<QB extends { where: (cb: (eb: any) => unknown) => QB }>(
|
|
1307
|
+
query: QB,
|
|
1308
|
+
where: { bylineFilter?: ContentBylineFilter } | undefined,
|
|
1309
|
+
type: string,
|
|
1310
|
+
): QB {
|
|
1311
|
+
const filter = where?.bylineFilter;
|
|
1312
|
+
if (!filter) return query;
|
|
1313
|
+
const tableName = getTableName(type);
|
|
1314
|
+
const idColumn = `${tableName}.id`;
|
|
1315
|
+
const authorColumn = `${tableName}.author_id`;
|
|
1316
|
+
const localeColumn = `${tableName}.locale`;
|
|
1317
|
+
|
|
1318
|
+
// An explicit credit that actually renders — optionally within a given
|
|
1319
|
+
// set of translation groups. The junction stores a group, but a credit
|
|
1320
|
+
// resolves only where that group has a byline row at the locale the
|
|
1321
|
+
// list is scoped to, so this repeats the join `getContentBylinesMany`
|
|
1322
|
+
// makes. `locale` falls back to each entry's own when the list spans
|
|
1323
|
+
// locales.
|
|
1324
|
+
const creditRenders = (eb: any, bylineIds?: string[]) => {
|
|
1325
|
+
let sub = eb
|
|
1326
|
+
.selectFrom("_emdash_content_bylines as cb")
|
|
1327
|
+
.innerJoin("_emdash_bylines as b", "b.translation_group", "cb.byline_id")
|
|
1328
|
+
.select("cb.id")
|
|
1329
|
+
.where("cb.collection_slug", "=", type)
|
|
1330
|
+
.whereRef("cb.content_id", "=", idColumn);
|
|
1331
|
+
sub = filter.locale
|
|
1332
|
+
? sub.where("b.locale", "=", filter.locale)
|
|
1333
|
+
: sub.whereRef("b.locale", "=", localeColumn);
|
|
1334
|
+
if (bylineIds) sub = sub.where("cb.byline_id", "in", bylineIds);
|
|
1335
|
+
return eb.exists(sub);
|
|
1336
|
+
};
|
|
1337
|
+
|
|
1338
|
+
// Whether the entry carries an explicit credit at all, at any locale.
|
|
1339
|
+
// Deliberately not locale-scoped: the author fallback is suppressed by
|
|
1340
|
+
// the presence of a junction row, even one that renders nothing here
|
|
1341
|
+
// (`hydrateBylinesMany` gates inference on `primaryBylineId`).
|
|
1342
|
+
const hasExplicitCredit = (eb: any) =>
|
|
1343
|
+
eb.exists(
|
|
1344
|
+
eb
|
|
1345
|
+
.selectFrom("_emdash_content_bylines as cb")
|
|
1346
|
+
.select("cb.id")
|
|
1347
|
+
.where("cb.collection_slug", "=", type)
|
|
1348
|
+
.whereRef("cb.content_id", "=", idColumn),
|
|
1349
|
+
);
|
|
1350
|
+
|
|
1351
|
+
// The entry's author owns a byline row — optionally within a given set
|
|
1352
|
+
// of translation groups — at the locale the list is scoped to. Matching
|
|
1353
|
+
// the locale is what keeps the filter agreeing with the list: an
|
|
1354
|
+
// inferred credit renders only when the author's byline has a row at
|
|
1355
|
+
// that locale (`hydrateBylinesMany` -> `findByUserIds`), and byline
|
|
1356
|
+
// translations start life with a null `user_id`, so a group translated
|
|
1357
|
+
// into the locale but not re-linked resolves to no credit. `locale`
|
|
1358
|
+
// falls back to each entry's own when the list spans locales.
|
|
1359
|
+
const authorHasByline = (eb: any, bylineIds?: string[]) => {
|
|
1360
|
+
let sub = eb
|
|
1361
|
+
.selectFrom("_emdash_bylines as b")
|
|
1362
|
+
.select("b.id")
|
|
1363
|
+
.whereRef("b.user_id", "=", authorColumn);
|
|
1364
|
+
sub = filter.locale
|
|
1365
|
+
? sub.where("b.locale", "=", filter.locale)
|
|
1366
|
+
: sub.whereRef("b.locale", "=", localeColumn);
|
|
1367
|
+
if (bylineIds) sub = sub.where("b.translation_group", "in", bylineIds);
|
|
1368
|
+
return eb.exists(sub);
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
if (filter.mode === "none") {
|
|
1372
|
+
return query.where((eb: any) => {
|
|
1373
|
+
const uncredited = eb.not(creditRenders(eb));
|
|
1374
|
+
// With inference on, "no byline" means none is rendered, so an
|
|
1375
|
+
// entry that falls through to an author byline is excluded too.
|
|
1376
|
+
return filter.includeInferred
|
|
1377
|
+
? eb.and([uncredited, eb.or([hasExplicitCredit(eb), eb.not(authorHasByline(eb))])])
|
|
1378
|
+
: uncredited;
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const bylineIds = filter.bylineIds ?? [];
|
|
1383
|
+
if (bylineIds.length === 0) {
|
|
1384
|
+
// A filter that resolved to no ids must match nothing rather than
|
|
1385
|
+
// silently degrade to "no filter" and return the whole collection.
|
|
1386
|
+
// `1 = 0` rather than a bound `false`: better-sqlite3 refuses to
|
|
1387
|
+
// bind JS booleans.
|
|
1388
|
+
return query.where(() => sql<boolean>`1 = 0`);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
return query.where((eb: any) => {
|
|
1392
|
+
if (!filter.includeInferred) return creditRenders(eb, bylineIds);
|
|
1393
|
+
// Inference applies only where no explicit credit exists, so an
|
|
1394
|
+
// entry credited to someone else never matches on its author.
|
|
1395
|
+
return eb.or([
|
|
1396
|
+
creditRenders(eb, bylineIds),
|
|
1397
|
+
eb.and([eb.not(hasExplicitCredit(eb)), authorHasByline(eb, bylineIds)]),
|
|
1398
|
+
]);
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
939
1402
|
/**
|
|
940
1403
|
* Count content items
|
|
941
1404
|
*/
|
|
942
1405
|
async count(type: string, where?: FindManyOptions["where"]): Promise<number> {
|
|
1406
|
+
const resolvedFieldFilters = await this.resolveFieldFilters(type, where?.fieldFilters);
|
|
1407
|
+
return this.countWithResolvedFilters(type, where, resolvedFieldFilters);
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
private async countWithResolvedFilters(
|
|
1411
|
+
type: string,
|
|
1412
|
+
where: FindManyOptions["where"] | undefined,
|
|
1413
|
+
resolvedFieldFilters: ResolvedFieldFilter[],
|
|
1414
|
+
): Promise<number> {
|
|
943
1415
|
const tableName = getTableName(type);
|
|
944
1416
|
|
|
945
1417
|
let query = this.db
|
|
@@ -961,6 +1433,8 @@ export class ContentRepository {
|
|
|
961
1433
|
|
|
962
1434
|
query = this.applySearchFilter(query, where, type);
|
|
963
1435
|
query = this.applyDateFilter(query, where);
|
|
1436
|
+
query = this.applyBylineFilter(query, where, type);
|
|
1437
|
+
query = this.applyFieldFilters(query, resolvedFieldFilters);
|
|
964
1438
|
|
|
965
1439
|
const result = await query.executeTakeFirst();
|
|
966
1440
|
return Number(result?.count || 0);
|
|
@@ -989,8 +1463,16 @@ export class ContentRepository {
|
|
|
989
1463
|
// get overall statistics for a content type in a single query
|
|
990
1464
|
async getStats(
|
|
991
1465
|
type: string,
|
|
992
|
-
|
|
1466
|
+
now = new Date(),
|
|
1467
|
+
): Promise<{
|
|
1468
|
+
total: number;
|
|
1469
|
+
published: number;
|
|
1470
|
+
draft: number;
|
|
1471
|
+
scheduled: number;
|
|
1472
|
+
overdueScheduled: number;
|
|
1473
|
+
}> {
|
|
993
1474
|
const tableName = getTableName(type);
|
|
1475
|
+
const nowIso = now.toISOString();
|
|
994
1476
|
|
|
995
1477
|
const result = await this.db
|
|
996
1478
|
.selectFrom(tableName as keyof Database)
|
|
@@ -999,6 +1481,9 @@ export class ContentRepository {
|
|
|
999
1481
|
eb.fn.sum(eb.case().when("status", "=", "published").then(1).else(0).end()).as("published"),
|
|
1000
1482
|
eb.fn.sum(eb.case().when("status", "=", "draft").then(1).else(0).end()).as("draft"),
|
|
1001
1483
|
sql<number>`SUM(CASE WHEN scheduled_at IS NOT NULL THEN 1 ELSE 0 END)`.as("scheduled"),
|
|
1484
|
+
sql<number>`SUM(CASE WHEN scheduled_at IS NOT NULL AND scheduled_at <= ${nowIso} THEN 1 ELSE 0 END)`.as(
|
|
1485
|
+
"overdue_scheduled",
|
|
1486
|
+
),
|
|
1002
1487
|
])
|
|
1003
1488
|
.where("deleted_at" as never, "is", null)
|
|
1004
1489
|
.executeTakeFirst();
|
|
@@ -1008,6 +1493,7 @@ export class ContentRepository {
|
|
|
1008
1493
|
published: Number(result?.published || 0),
|
|
1009
1494
|
draft: Number(result?.draft || 0),
|
|
1010
1495
|
scheduled: Number(result?.scheduled || 0),
|
|
1496
|
+
overdueScheduled: Number(result?.overdue_scheduled || 0),
|
|
1011
1497
|
};
|
|
1012
1498
|
}
|
|
1013
1499
|
|
|
@@ -1049,7 +1535,6 @@ export class ContentRepository {
|
|
|
1049
1535
|
AND deleted_at IS NULL
|
|
1050
1536
|
`.execute(this.db);
|
|
1051
1537
|
|
|
1052
|
-
await this.restampEntryPivot(type, id);
|
|
1053
1538
|
invalidateCollectionCache(type);
|
|
1054
1539
|
|
|
1055
1540
|
const updated = await this.findById(type, id);
|
|
@@ -1089,7 +1574,6 @@ export class ContentRepository {
|
|
|
1089
1574
|
AND deleted_at IS NULL
|
|
1090
1575
|
`.execute(this.db);
|
|
1091
1576
|
|
|
1092
|
-
await this.restampEntryPivot(type, id);
|
|
1093
1577
|
invalidateCollectionCache(type);
|
|
1094
1578
|
|
|
1095
1579
|
const updated = await this.findById(type, id);
|
|
@@ -1158,6 +1642,10 @@ export class ContentRepository {
|
|
|
1158
1642
|
* Lets callers resolve many edge groups without an N+1 per group. The caller
|
|
1159
1643
|
* groups the flat result by `translationGroup` itself.
|
|
1160
1644
|
*
|
|
1645
|
+
* `translation_group` leads the sort so the ordering follows
|
|
1646
|
+
* `idx_{table}_del_tg_locale` past its `deleted_at` equality; callers group by
|
|
1647
|
+
* `translationGroup`, so the per-group locale order they rely on is preserved.
|
|
1648
|
+
*
|
|
1161
1649
|
* `publishedOnly` restricts the result to `status = 'published'` — reference
|
|
1162
1650
|
* reads pass this for callers without `content:read_drafts` so draft/scheduled
|
|
1163
1651
|
* entries never leak through an edge traversal.
|
|
@@ -1184,7 +1672,7 @@ export class ContentRepository {
|
|
|
1184
1672
|
WHERE translation_group IN (${sql.join(chunk)})
|
|
1185
1673
|
AND deleted_at IS NULL
|
|
1186
1674
|
${publishedFilter}
|
|
1187
|
-
ORDER BY locale ASC
|
|
1675
|
+
ORDER BY translation_group ASC, locale ASC
|
|
1188
1676
|
`.execute(this.db);
|
|
1189
1677
|
for (const row of result.rows) items.push(this.mapRow(type, row));
|
|
1190
1678
|
}
|
|
@@ -1267,6 +1755,8 @@ export class ContentRepository {
|
|
|
1267
1755
|
* Syncs the draft revision's data into the content table columns so the
|
|
1268
1756
|
* content table always reflects the published version.
|
|
1269
1757
|
* If no draft revision exists, creates one from current data and publishes it.
|
|
1758
|
+
* When `promoteRevision` is false, publishes the current content-table data
|
|
1759
|
+
* by changing lifecycle metadata only.
|
|
1270
1760
|
*
|
|
1271
1761
|
* `publishedAt` (optional) overrides the publication timestamp. If omitted,
|
|
1272
1762
|
* the existing `published_at` is preserved (idempotent re-publish keeps the
|
|
@@ -1274,19 +1764,18 @@ export class ContentRepository {
|
|
|
1274
1764
|
* an explicit value to backdate a publish (e.g. when migrating content from
|
|
1275
1765
|
* another CMS).
|
|
1276
1766
|
*
|
|
1277
|
-
* `requireDue`
|
|
1278
|
-
* `
|
|
1279
|
-
*
|
|
1280
|
-
* between selection and publish. It claims the row with a single conditional
|
|
1281
|
-
* UPDATE (clearing `scheduled_at`) before any other write, so it is atomic
|
|
1282
|
-
* even on D1 (no multi-statement transactions) and serialises against
|
|
1283
|
-
* `unschedule()` and concurrent sweeps — no TOCTOU and no double publish.
|
|
1767
|
+
* `requireDue` gates the final update on the row still being due.
|
|
1768
|
+
* `expectedScheduledAt` additionally fences changes made after a sweep
|
|
1769
|
+
* selected the row but before publication preparation began.
|
|
1284
1770
|
*/
|
|
1285
1771
|
async publish(
|
|
1286
1772
|
type: string,
|
|
1287
1773
|
id: string,
|
|
1288
1774
|
publishedAt?: string,
|
|
1289
1775
|
requireDue = false,
|
|
1776
|
+
expectedScheduledAt?: string,
|
|
1777
|
+
promoteRevision = true,
|
|
1778
|
+
requireSlug = true,
|
|
1290
1779
|
): Promise<ContentItem> {
|
|
1291
1780
|
const tableName = getTableName(type);
|
|
1292
1781
|
const now = new Date().toISOString();
|
|
@@ -1295,167 +1784,239 @@ export class ContentRepository {
|
|
|
1295
1784
|
if (!existing) {
|
|
1296
1785
|
throw new EmDashValidationError("Content item not found");
|
|
1297
1786
|
}
|
|
1787
|
+
if (
|
|
1788
|
+
requireDue &&
|
|
1789
|
+
expectedScheduledAt !== undefined &&
|
|
1790
|
+
existing.scheduledAt !== expectedScheduledAt
|
|
1791
|
+
) {
|
|
1792
|
+
throw new ScheduledNotDueError();
|
|
1793
|
+
}
|
|
1794
|
+
if (!promoteRevision && requireSlug && !existing.slug?.trim()) {
|
|
1795
|
+
throw new EmDashValidationError("Cannot publish routable content without a slug");
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
if (!promoteRevision) {
|
|
1799
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
1800
|
+
let provisionalRevisionId: string | null = null;
|
|
1801
|
+
try {
|
|
1802
|
+
let liveRevisionId = existing.liveRevisionId;
|
|
1803
|
+
if (!liveRevisionId) {
|
|
1804
|
+
const revision = await revisionRepo.create({
|
|
1805
|
+
collection: type,
|
|
1806
|
+
entryId: id,
|
|
1807
|
+
data: existing.data,
|
|
1808
|
+
});
|
|
1809
|
+
liveRevisionId = revision.id;
|
|
1810
|
+
provisionalRevisionId = revision.id;
|
|
1811
|
+
}
|
|
1298
1812
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1813
|
+
const intendedPublishedAt = publishedAt ?? existing.publishedAt ?? now;
|
|
1814
|
+
const duePredicate = requireDue
|
|
1815
|
+
? sql`AND scheduled_at IS NOT NULL AND scheduled_at <= ${now}`
|
|
1816
|
+
: sql``;
|
|
1817
|
+
let published = false;
|
|
1818
|
+
try {
|
|
1819
|
+
const result = await sql`
|
|
1820
|
+
UPDATE ${sql.ref(tableName)}
|
|
1821
|
+
SET live_revision_id = ${liveRevisionId},
|
|
1822
|
+
draft_revision_id = NULL,
|
|
1823
|
+
status = 'published',
|
|
1824
|
+
scheduled_at = NULL,
|
|
1825
|
+
published_at = ${intendedPublishedAt},
|
|
1826
|
+
updated_at = ${now},
|
|
1827
|
+
version = version + 1
|
|
1828
|
+
WHERE id = ${id}
|
|
1829
|
+
AND deleted_at IS NULL
|
|
1830
|
+
AND version = ${existing.version}
|
|
1831
|
+
AND status = ${existing.status}
|
|
1832
|
+
AND ${nullableColumnMatch("live_revision_id", existing.liveRevisionId)}
|
|
1833
|
+
AND ${nullableColumnMatch("draft_revision_id", existing.draftRevisionId)}
|
|
1834
|
+
AND ${nullableColumnMatch("scheduled_at", existing.scheduledAt)}
|
|
1835
|
+
${duePredicate}
|
|
1836
|
+
`.execute(this.db);
|
|
1837
|
+
published = (result.numAffectedRows ?? 0n) > 0n;
|
|
1838
|
+
} catch (error) {
|
|
1839
|
+
if (isConfirmedStatementFailure(error)) throw error;
|
|
1840
|
+
let observed: ContentItem | null;
|
|
1841
|
+
try {
|
|
1842
|
+
observed = await this.findById(type, id);
|
|
1843
|
+
} catch (reconciliationError) {
|
|
1844
|
+
throw new Error("Unable to confirm whether content publication completed", {
|
|
1845
|
+
cause: reconciliationError,
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
if (!observed) {
|
|
1849
|
+
throw new Error("Unable to confirm whether content publication completed", {
|
|
1850
|
+
cause: error,
|
|
1851
|
+
});
|
|
1852
|
+
}
|
|
1853
|
+
published = matchesLifecyclePublication(
|
|
1854
|
+
observed,
|
|
1855
|
+
existing,
|
|
1856
|
+
liveRevisionId,
|
|
1857
|
+
intendedPublishedAt,
|
|
1858
|
+
now,
|
|
1859
|
+
);
|
|
1860
|
+
if (!published && matchesPublicationFence(observed, existing)) throw error;
|
|
1861
|
+
if (!published) {
|
|
1862
|
+
throw new Error("Unable to confirm whether content publication completed", {
|
|
1863
|
+
cause: error,
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (!published) {
|
|
1869
|
+
throw requireDue ? new ScheduledNotDueError() : new ContentMutationConflictError();
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
invalidateCollectionCache(type);
|
|
1873
|
+
const updated = await this.findById(type, id);
|
|
1874
|
+
if (!updated) throw new Error("Content not found");
|
|
1875
|
+
return updated;
|
|
1876
|
+
} catch (error) {
|
|
1877
|
+
if (provisionalRevisionId) {
|
|
1878
|
+
try {
|
|
1879
|
+
await revisionRepo.deleteIfUnreferenced(type, id, provisionalRevisionId);
|
|
1880
|
+
} catch (cleanupError) {
|
|
1881
|
+
console.error(
|
|
1882
|
+
`[content] Failed to clean up provisional revision ${provisionalRevisionId}:`,
|
|
1883
|
+
cleanupError,
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
throw error;
|
|
1319
1888
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
// claim, the restore writes the snapshot value rather than the one the
|
|
1325
|
-
// claim actually cleared. That window is tiny and the restore is
|
|
1326
|
-
// best-effort retry bookkeeping, so the imprecision is acceptable.
|
|
1327
|
-
claimedScheduledAt = existing.scheduledAt;
|
|
1328
|
-
claimedUpdatedAt = existing.updatedAt;
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// Track whether the final publish write committed. On D1 the claim above
|
|
1332
|
-
// is already durable (withTransaction is a no-op there), so if a later
|
|
1333
|
-
// step throws we must restore the schedule — otherwise the row is left
|
|
1334
|
-
// `scheduled` with `scheduled_at = NULL` and no sweep ever retries it.
|
|
1335
|
-
let publishCommitted = false;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
1892
|
+
let provisionalRevisionId: string | null = null;
|
|
1336
1893
|
try {
|
|
1337
|
-
const revisionRepo = new RevisionRepository(this.db);
|
|
1338
1894
|
let revisionToPublish = existing.draftRevisionId || existing.liveRevisionId;
|
|
1339
1895
|
|
|
1340
1896
|
if (!revisionToPublish) {
|
|
1341
|
-
// No revision exists - create one from current data
|
|
1342
1897
|
const revision = await revisionRepo.create({
|
|
1343
1898
|
collection: type,
|
|
1344
1899
|
entryId: id,
|
|
1345
1900
|
data: existing.data,
|
|
1346
1901
|
});
|
|
1347
1902
|
revisionToPublish = revision.id;
|
|
1903
|
+
provisionalRevisionId = revision.id;
|
|
1348
1904
|
}
|
|
1349
1905
|
|
|
1350
|
-
// Sync the revision's data into the content table columns
|
|
1351
|
-
// so the content table always holds the published version
|
|
1352
1906
|
const revision = await revisionRepo.findById(revisionToPublish);
|
|
1353
|
-
if (revision) {
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
// Validate before any write: the `(slug, locale)` unique index
|
|
1357
|
-
// covers every row (drafts and trashed entries included), and
|
|
1358
|
-
// letting the constraint fire mid-publish would surface as an
|
|
1359
|
-
// opaque 500 after draft data already hit the live columns.
|
|
1360
|
-
// A NULL locale never collides (unique indexes treat NULLs as
|
|
1361
|
-
// distinct), so the check only applies to localized rows.
|
|
1362
|
-
const stagedSlug = typeof revision.data._slug === "string" ? revision.data._slug : null;
|
|
1363
|
-
if (stagedSlug !== null && stagedSlug !== existing.slug && existing.locale !== null) {
|
|
1364
|
-
const conflict = await this.findBySlugIncludingTrashed(type, stagedSlug, existing.locale);
|
|
1365
|
-
if (conflict && conflict.id !== id) {
|
|
1366
|
-
throw new EmDashValidationError(
|
|
1367
|
-
`Cannot publish: slug '${stagedSlug}' is already used by another entry` +
|
|
1368
|
-
` in this collection (id: ${conflict.id}). Choose a different slug.`,
|
|
1369
|
-
{ code: "SLUG_CONFLICT" },
|
|
1370
|
-
);
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1907
|
+
if (!revision || revision.collection !== type || revision.entryId !== id) {
|
|
1908
|
+
throw new EmDashValidationError("Revision does not belong to the specified content item");
|
|
1909
|
+
}
|
|
1373
1910
|
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1911
|
+
const stagedSlug = typeof revision.data._slug === "string" ? revision.data._slug : null;
|
|
1912
|
+
const intendedSlug = stagedSlug ?? existing.slug;
|
|
1913
|
+
if (requireSlug && !intendedSlug?.trim()) {
|
|
1914
|
+
throw new EmDashValidationError("Cannot publish routable content without a slug");
|
|
1915
|
+
}
|
|
1916
|
+
const intendedPublishedAt = publishedAt ?? existing.publishedAt ?? now;
|
|
1917
|
+
if (stagedSlug !== null && stagedSlug !== existing.slug && existing.locale !== null) {
|
|
1918
|
+
const conflict = await this.findBySlugIncludingTrashed(type, stagedSlug, existing.locale);
|
|
1919
|
+
if (conflict && conflict.id !== id) {
|
|
1920
|
+
throw new EmDashValidationError(
|
|
1921
|
+
`Cannot publish: slug '${stagedSlug}' is already used by another entry` +
|
|
1922
|
+
` in this collection (id: ${conflict.id}). Choose a different slug.`,
|
|
1923
|
+
{ code: "SLUG_CONFLICT" },
|
|
1924
|
+
);
|
|
1384
1925
|
}
|
|
1385
|
-
|
|
1386
|
-
await this.syncDataColumns(type, id, revision.data);
|
|
1387
1926
|
}
|
|
1388
1927
|
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1928
|
+
const assignments: ReturnType<typeof sql>[] = [];
|
|
1929
|
+
if (stagedSlug !== null) assignments.push(sql`slug = ${stagedSlug}`);
|
|
1930
|
+
for (const [key, value] of Object.entries(revision.data)) {
|
|
1931
|
+
if (SYSTEM_COLUMNS.has(key) || key.startsWith("_")) continue;
|
|
1932
|
+
validateIdentifier(key, "content field name");
|
|
1933
|
+
assignments.push(sql`${sql.ref(key)} = ${serializeValue(value)}`);
|
|
1934
|
+
}
|
|
1935
|
+
assignments.push(
|
|
1936
|
+
sql`live_revision_id = ${revisionToPublish}`,
|
|
1937
|
+
sql`draft_revision_id = NULL`,
|
|
1938
|
+
sql`status = 'published'`,
|
|
1939
|
+
sql`scheduled_at = NULL`,
|
|
1940
|
+
sql`published_at = ${intendedPublishedAt}`,
|
|
1941
|
+
sql`updated_at = ${now}`,
|
|
1942
|
+
sql`version = version + 1`,
|
|
1943
|
+
);
|
|
1944
|
+
|
|
1945
|
+
const duePredicate = requireDue
|
|
1946
|
+
? sql`AND scheduled_at IS NOT NULL AND scheduled_at <= ${now}`
|
|
1947
|
+
: sql``;
|
|
1948
|
+
let promoted = false;
|
|
1949
|
+
try {
|
|
1950
|
+
const result = await sql`
|
|
1407
1951
|
UPDATE ${sql.ref(tableName)}
|
|
1408
|
-
SET
|
|
1409
|
-
draft_revision_id = NULL,
|
|
1410
|
-
status = 'published',
|
|
1411
|
-
scheduled_at = NULL,
|
|
1412
|
-
published_at = COALESCE(published_at, ${now}),
|
|
1413
|
-
updated_at = ${now}
|
|
1952
|
+
SET ${sql.join(assignments, sql`, `)}
|
|
1414
1953
|
WHERE id = ${id}
|
|
1415
1954
|
AND deleted_at IS NULL
|
|
1955
|
+
AND version = ${existing.version}
|
|
1956
|
+
AND status = ${existing.status}
|
|
1957
|
+
AND ${nullableColumnMatch("live_revision_id", existing.liveRevisionId)}
|
|
1958
|
+
AND ${nullableColumnMatch("draft_revision_id", existing.draftRevisionId)}
|
|
1959
|
+
AND ${nullableColumnMatch("scheduled_at", existing.scheduledAt)}
|
|
1960
|
+
${duePredicate}
|
|
1961
|
+
AND EXISTS (
|
|
1962
|
+
SELECT 1 FROM revisions
|
|
1963
|
+
WHERE revisions.id = ${revisionToPublish}
|
|
1964
|
+
AND revisions.collection = ${type}
|
|
1965
|
+
AND revisions.entry_id = ${id}
|
|
1966
|
+
)
|
|
1416
1967
|
`.execute(this.db);
|
|
1968
|
+
promoted = (result.numAffectedRows ?? 0n) > 0n;
|
|
1969
|
+
} catch (error) {
|
|
1970
|
+
if (isConfirmedStatementFailure(error)) throw error;
|
|
1971
|
+
let observed: ContentItem | null;
|
|
1972
|
+
try {
|
|
1973
|
+
observed = await this.findById(type, id);
|
|
1974
|
+
} catch (reconciliationError) {
|
|
1975
|
+
throw new Error("Unable to confirm whether content publication completed", {
|
|
1976
|
+
cause: reconciliationError,
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
if (!observed) {
|
|
1980
|
+
throw new Error("Unable to confirm whether content publication completed", {
|
|
1981
|
+
cause: error,
|
|
1982
|
+
});
|
|
1983
|
+
}
|
|
1984
|
+
promoted = matchesPublication(
|
|
1985
|
+
observed,
|
|
1986
|
+
existing,
|
|
1987
|
+
revision,
|
|
1988
|
+
revisionToPublish,
|
|
1989
|
+
intendedSlug,
|
|
1990
|
+
intendedPublishedAt,
|
|
1991
|
+
now,
|
|
1992
|
+
);
|
|
1993
|
+
if (!promoted && matchesPublicationFence(observed, existing)) throw error;
|
|
1994
|
+
if (!promoted) {
|
|
1995
|
+
throw new Error("Unable to confirm whether content publication completed", {
|
|
1996
|
+
cause: error,
|
|
1997
|
+
});
|
|
1998
|
+
}
|
|
1417
1999
|
}
|
|
1418
|
-
publishCommitted = true;
|
|
1419
2000
|
|
|
1420
|
-
|
|
2001
|
+
if (!promoted) {
|
|
2002
|
+
throw requireDue ? new ScheduledNotDueError() : new ContentMutationConflictError();
|
|
2003
|
+
}
|
|
1421
2004
|
|
|
2005
|
+
invalidateCollectionCache(type);
|
|
1422
2006
|
const updated = await this.findById(type, id);
|
|
1423
2007
|
if (!updated) {
|
|
1424
2008
|
throw new Error("Content not found");
|
|
1425
2009
|
}
|
|
1426
2010
|
|
|
1427
|
-
invalidateCollectionCache(type);
|
|
1428
2011
|
return updated;
|
|
1429
2012
|
} catch (error) {
|
|
1430
|
-
|
|
1431
|
-
// failed publish stays retryable. Skipped when the publish actually
|
|
1432
|
-
// committed (the failure was afterwards). On SQLite/Postgres the
|
|
1433
|
-
// enclosing transaction rolls the claim back, so this restore also
|
|
1434
|
-
// rolls back — a harmless no-op. Never mask the original error.
|
|
1435
|
-
if (requireDue && claimedScheduledAt && !publishCommitted) {
|
|
2013
|
+
if (provisionalRevisionId) {
|
|
1436
2014
|
try {
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
// queued. This avoids re-adding a stale schedule (and triggering a
|
|
1440
|
-
// redundant republish) when another actor fully published the row
|
|
1441
|
-
// in the failure window — that publish clears draft_revision_id.
|
|
1442
|
-
// Restore updated_at to its pre-claim value too — the claim bumped
|
|
1443
|
-
// it to `now`, and a failed publish made no real change, so leaving
|
|
1444
|
-
// it advanced would be a phantom modification for "changed since"
|
|
1445
|
-
// consumers (sync, ETags, incremental indexers).
|
|
1446
|
-
await sql`
|
|
1447
|
-
UPDATE ${sql.ref(tableName)}
|
|
1448
|
-
SET scheduled_at = ${claimedScheduledAt},
|
|
1449
|
-
updated_at = ${claimedUpdatedAt ?? now}
|
|
1450
|
-
WHERE id = ${id}
|
|
1451
|
-
AND scheduled_at IS NULL
|
|
1452
|
-
AND deleted_at IS NULL
|
|
1453
|
-
AND (status != 'published' OR draft_revision_id IS NOT NULL)
|
|
1454
|
-
`.execute(this.db);
|
|
1455
|
-
} catch (restoreError) {
|
|
2015
|
+
await revisionRepo.deleteIfUnreferenced(type, id, provisionalRevisionId);
|
|
2016
|
+
} catch (cleanupError) {
|
|
1456
2017
|
console.error(
|
|
1457
|
-
`[content] Failed to
|
|
1458
|
-
|
|
2018
|
+
`[content] Failed to clean up provisional revision ${provisionalRevisionId}:`,
|
|
2019
|
+
cleanupError,
|
|
1459
2020
|
);
|
|
1460
2021
|
}
|
|
1461
2022
|
}
|
|
@@ -1507,7 +2068,6 @@ export class ContentRepository {
|
|
|
1507
2068
|
AND deleted_at IS NULL
|
|
1508
2069
|
`.execute(this.db);
|
|
1509
2070
|
|
|
1510
|
-
await this.restampEntryPivot(type, id);
|
|
1511
2071
|
invalidateCollectionCache(type);
|
|
1512
2072
|
|
|
1513
2073
|
const updated = await this.findById(type, id);
|
|
@@ -1530,8 +2090,6 @@ export class ContentRepository {
|
|
|
1530
2090
|
* content row pointing at a missing or unrelated revision.
|
|
1531
2091
|
*/
|
|
1532
2092
|
async setDraftRevision(type: string, id: string, revisionId: string): Promise<void> {
|
|
1533
|
-
const tableName = getTableName(type);
|
|
1534
|
-
|
|
1535
2093
|
const existing = await this.findById(type, id);
|
|
1536
2094
|
if (!existing) {
|
|
1537
2095
|
throw new EmDashValidationError("Content item not found");
|
|
@@ -1547,15 +2105,35 @@ export class ContentRepository {
|
|
|
1547
2105
|
throw new EmDashValidationError("Revision does not belong to the specified content item");
|
|
1548
2106
|
}
|
|
1549
2107
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
2108
|
+
if (!(await this.replaceDraftRevision(type, id, revisionId, existing))) {
|
|
2109
|
+
throw new ContentMutationConflictError();
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
async replaceDraftRevision(
|
|
2114
|
+
type: string,
|
|
2115
|
+
id: string,
|
|
2116
|
+
revisionId: string,
|
|
2117
|
+
expected: Pick<ContentItem, "version" | "liveRevisionId" | "draftRevisionId">,
|
|
2118
|
+
): Promise<boolean> {
|
|
2119
|
+
const tableName = getTableName(type);
|
|
2120
|
+
const result = await sql`
|
|
1554
2121
|
UPDATE ${sql.ref(tableName)}
|
|
1555
|
-
SET draft_revision_id = ${revisionId}
|
|
2122
|
+
SET draft_revision_id = ${revisionId},
|
|
2123
|
+
version = version + 1
|
|
1556
2124
|
WHERE id = ${id}
|
|
1557
2125
|
AND deleted_at IS NULL
|
|
2126
|
+
AND version = ${expected.version}
|
|
2127
|
+
AND ${nullableColumnMatch("live_revision_id", expected.liveRevisionId)}
|
|
2128
|
+
AND ${nullableColumnMatch("draft_revision_id", expected.draftRevisionId)}
|
|
2129
|
+
AND EXISTS (
|
|
2130
|
+
SELECT 1 FROM revisions
|
|
2131
|
+
WHERE revisions.id = ${revisionId}
|
|
2132
|
+
AND revisions.collection = ${type}
|
|
2133
|
+
AND revisions.entry_id = ${id}
|
|
2134
|
+
)
|
|
1558
2135
|
`.execute(this.db);
|
|
2136
|
+
return (result.numAffectedRows ?? 0n) > 0n;
|
|
1559
2137
|
}
|
|
1560
2138
|
|
|
1561
2139
|
/**
|
|
@@ -1595,35 +2173,6 @@ export class ContentRepository {
|
|
|
1595
2173
|
return updated;
|
|
1596
2174
|
}
|
|
1597
2175
|
|
|
1598
|
-
/**
|
|
1599
|
-
* Sync data columns in the content table from a data object.
|
|
1600
|
-
* Used to promote revision data into the content table on publish.
|
|
1601
|
-
* Keys starting with _ are revision metadata (e.g. _slug) and are skipped.
|
|
1602
|
-
*/
|
|
1603
|
-
private async syncDataColumns(
|
|
1604
|
-
type: string,
|
|
1605
|
-
id: string,
|
|
1606
|
-
data: Record<string, unknown>,
|
|
1607
|
-
): Promise<void> {
|
|
1608
|
-
const tableName = getTableName(type);
|
|
1609
|
-
const updates: Record<string, unknown> = {};
|
|
1610
|
-
|
|
1611
|
-
for (const [key, value] of Object.entries(data)) {
|
|
1612
|
-
if (SYSTEM_COLUMNS.has(key)) continue;
|
|
1613
|
-
if (key.startsWith("_")) continue; // revision metadata
|
|
1614
|
-
validateIdentifier(key, "content field name");
|
|
1615
|
-
updates[key] = serializeValue(value);
|
|
1616
|
-
}
|
|
1617
|
-
|
|
1618
|
-
if (Object.keys(updates).length === 0) return;
|
|
1619
|
-
|
|
1620
|
-
await this.db
|
|
1621
|
-
.updateTable(tableName as keyof Database)
|
|
1622
|
-
.set(updates)
|
|
1623
|
-
.where("id", "=", id)
|
|
1624
|
-
.execute();
|
|
1625
|
-
}
|
|
1626
|
-
|
|
1627
2176
|
/**
|
|
1628
2177
|
* Count content items with a pending schedule.
|
|
1629
2178
|
* Includes both draft-scheduled (status='scheduled') and published
|
|
@@ -1675,28 +2224,257 @@ export class ContentRepository {
|
|
|
1675
2224
|
};
|
|
1676
2225
|
}
|
|
1677
2226
|
|
|
2227
|
+
private normalizeFilterScalar(
|
|
2228
|
+
field: string,
|
|
2229
|
+
type: FieldType,
|
|
2230
|
+
value: unknown,
|
|
2231
|
+
): NormalizedFilterScalar {
|
|
2232
|
+
if (type === "number" || type === "integer") {
|
|
2233
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
2234
|
+
throw new EmDashValidationError(`Filter for field "${field}" must use a finite number`);
|
|
2235
|
+
}
|
|
2236
|
+
if (type === "integer" && !Number.isInteger(value)) {
|
|
2237
|
+
throw new EmDashValidationError(`Filter for field "${field}" must use an integer`);
|
|
2238
|
+
}
|
|
2239
|
+
return value;
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
if (type === "boolean") {
|
|
2243
|
+
if (typeof value !== "boolean") {
|
|
2244
|
+
throw new EmDashValidationError(`Filter for field "${field}" must use a boolean`);
|
|
2245
|
+
}
|
|
2246
|
+
return value ? 1 : 0;
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
if (typeof value !== "string") {
|
|
2250
|
+
throw new EmDashValidationError(`Filter for field "${field}" must use a string`);
|
|
2251
|
+
}
|
|
2252
|
+
if (value.length > MAX_FILTER_STRING_LENGTH) {
|
|
2253
|
+
throw new EmDashValidationError(
|
|
2254
|
+
`Filter value for field "${field}" exceeds ${MAX_FILTER_STRING_LENGTH} characters`,
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
return value;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
private normalizeFieldFilter(
|
|
2261
|
+
field: string,
|
|
2262
|
+
type: FieldType,
|
|
2263
|
+
value: ContentFieldFilterValue,
|
|
2264
|
+
): ResolvedFieldFilter {
|
|
2265
|
+
if (value === null) return { column: field, kind: "null" };
|
|
2266
|
+
if (typeof value !== "object") {
|
|
2267
|
+
return {
|
|
2268
|
+
column: field,
|
|
2269
|
+
kind: "exact",
|
|
2270
|
+
value: this.normalizeFilterScalar(field, type, value),
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
if (Array.isArray(value)) {
|
|
2274
|
+
throw new EmDashValidationError(`Invalid filter for field "${field}"`);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2277
|
+
const record = value as Record<string, unknown>;
|
|
2278
|
+
const keys = Object.keys(record);
|
|
2279
|
+
if (keys.length === 1 && keys[0] === "in") {
|
|
2280
|
+
if (!Array.isArray(record.in) || record.in.length === 0) {
|
|
2281
|
+
throw new EmDashValidationError(`IN filter for field "${field}" must not be empty`);
|
|
2282
|
+
}
|
|
2283
|
+
if (record.in.length > MAX_IN_FILTER_VALUES) {
|
|
2284
|
+
throw new EmDashValidationError(
|
|
2285
|
+
`IN filter for field "${field}" exceeds ${MAX_IN_FILTER_VALUES} values`,
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
return {
|
|
2289
|
+
column: field,
|
|
2290
|
+
kind: "in",
|
|
2291
|
+
values: record.in.map((entry) => this.normalizeFilterScalar(field, type, entry)),
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
const rangeKeys = new Set(["gt", "gte", "lt", "lte"]);
|
|
2296
|
+
if (keys.length === 0 || keys.some((key) => !rangeKeys.has(key))) {
|
|
2297
|
+
throw new EmDashValidationError(`Invalid filter operator for field "${field}"`);
|
|
2298
|
+
}
|
|
2299
|
+
if (type === "boolean") {
|
|
2300
|
+
throw new EmDashValidationError(`Boolean field "${field}" does not support range filters`);
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
const bounds: Partial<Record<"gt" | "gte" | "lt" | "lte", NormalizedFilterScalar>> = {};
|
|
2304
|
+
for (const key of keys as Array<"gt" | "gte" | "lt" | "lte">) {
|
|
2305
|
+
if (record[key] === undefined) continue;
|
|
2306
|
+
bounds[key] = this.normalizeFilterScalar(field, type, record[key]);
|
|
2307
|
+
}
|
|
2308
|
+
if (Object.keys(bounds).length === 0) {
|
|
2309
|
+
throw new EmDashValidationError(`Range filter for field "${field}" has no bounds`);
|
|
2310
|
+
}
|
|
2311
|
+
return { column: field, kind: "range", bounds };
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
private async collectionExists(type: string): Promise<boolean> {
|
|
2315
|
+
const collection = await this.db
|
|
2316
|
+
.selectFrom("_emdash_collections")
|
|
2317
|
+
.where("slug", "=", type)
|
|
2318
|
+
.select("id")
|
|
2319
|
+
.executeTakeFirst();
|
|
2320
|
+
return collection !== undefined;
|
|
2321
|
+
}
|
|
2322
|
+
|
|
2323
|
+
private async resolveFieldFilters(
|
|
2324
|
+
type: string,
|
|
2325
|
+
filters: ContentFieldFilters | undefined,
|
|
2326
|
+
): Promise<ResolvedFieldFilter[]> {
|
|
2327
|
+
const resolvedFilters = filters ?? {};
|
|
2328
|
+
const fields = Object.keys(resolvedFilters);
|
|
2329
|
+
if (fields.length === 0) return [];
|
|
2330
|
+
if (fields.length > MAX_INDEXED_FIELD_FILTERS) {
|
|
2331
|
+
if (!(await this.collectionExists(type))) return [];
|
|
2332
|
+
throw new EmDashValidationError(
|
|
2333
|
+
`Content list queries support at most ${MAX_INDEXED_FIELD_FILTERS} indexed field filters`,
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
const rows = await this.db
|
|
2337
|
+
.selectFrom("_emdash_fields as field")
|
|
2338
|
+
.innerJoin("_emdash_collections as collection", "collection.id", "field.collection_id")
|
|
2339
|
+
.where("collection.slug", "=", type)
|
|
2340
|
+
.where("field.slug", "in", fields)
|
|
2341
|
+
.where("field.indexed", "=", 1)
|
|
2342
|
+
.select(["field.slug", "field.type"])
|
|
2343
|
+
.execute();
|
|
2344
|
+
const metadata = new Map(rows.map((row) => [row.slug, row.type as FieldType]));
|
|
2345
|
+
|
|
2346
|
+
if (metadata.size === 0 && !(await this.collectionExists(type))) return [];
|
|
2347
|
+
|
|
2348
|
+
for (const field of fields) {
|
|
2349
|
+
try {
|
|
2350
|
+
validateIdentifier(field, "content filter field");
|
|
2351
|
+
} catch {
|
|
2352
|
+
throw new EmDashValidationError(`Invalid content filter field: ${field}`);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
const normalized = fields.map((field) => {
|
|
2357
|
+
const fieldType = metadata.get(field);
|
|
2358
|
+
if (!fieldType || !isIndexableFieldType(fieldType)) {
|
|
2359
|
+
throw new EmDashValidationError(
|
|
2360
|
+
`Cannot filter by field "${field}". Custom fields must be indexed before filtering.`,
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
return this.normalizeFieldFilter(field, fieldType, resolvedFilters[field]);
|
|
2364
|
+
});
|
|
2365
|
+
const operandCount = normalized.reduce((total, filter) => {
|
|
2366
|
+
if (filter.kind === "null") return total;
|
|
2367
|
+
if (filter.kind === "exact") return total + 1;
|
|
2368
|
+
if (filter.kind === "in") return total + filter.values.length;
|
|
2369
|
+
return total + Object.keys(filter.bounds).length;
|
|
2370
|
+
}, 0);
|
|
2371
|
+
if (operandCount > SQL_BATCH_SIZE) {
|
|
2372
|
+
throw new EmDashValidationError(
|
|
2373
|
+
`Indexed field filters have a total operand budget of ${SQL_BATCH_SIZE}`,
|
|
2374
|
+
);
|
|
2375
|
+
}
|
|
2376
|
+
return normalized;
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
private applyFieldFilters<QB extends { where: (cb: (eb: any) => unknown) => QB }>(
|
|
2380
|
+
query: QB,
|
|
2381
|
+
filters: ResolvedFieldFilter[],
|
|
2382
|
+
): QB {
|
|
2383
|
+
let next = query;
|
|
2384
|
+
for (const filter of filters) {
|
|
2385
|
+
const column = sql.ref(filter.column);
|
|
2386
|
+
const isPresent = sql<boolean>`${column} IS NOT NULL`;
|
|
2387
|
+
if (filter.kind === "null") {
|
|
2388
|
+
next = next.where(() => sql<boolean>`(${isPresent}) = FALSE AND ${column} IS NULL`);
|
|
2389
|
+
continue;
|
|
2390
|
+
}
|
|
2391
|
+
if (filter.kind === "exact") {
|
|
2392
|
+
next = next.where(
|
|
2393
|
+
() => sql<boolean>`(${isPresent}) = TRUE AND ${column} = ${filter.value}`,
|
|
2394
|
+
);
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
if (filter.kind === "in") {
|
|
2398
|
+
const values = sql.join(
|
|
2399
|
+
filter.values.map((value) => sql`${value}`),
|
|
2400
|
+
sql`, `,
|
|
2401
|
+
);
|
|
2402
|
+
next = next.where(() => sql<boolean>`(${isPresent}) = TRUE AND ${column} IN (${values})`);
|
|
2403
|
+
continue;
|
|
2404
|
+
}
|
|
2405
|
+
|
|
2406
|
+
next = next.where(() => sql<boolean>`(${isPresent}) = TRUE`);
|
|
2407
|
+
if (filter.bounds.gt !== undefined) {
|
|
2408
|
+
next = next.where(() => sql<boolean>`${column} > ${filter.bounds.gt}`);
|
|
2409
|
+
}
|
|
2410
|
+
if (filter.bounds.gte !== undefined) {
|
|
2411
|
+
next = next.where(() => sql<boolean>`${column} >= ${filter.bounds.gte}`);
|
|
2412
|
+
}
|
|
2413
|
+
if (filter.bounds.lt !== undefined) {
|
|
2414
|
+
next = next.where(() => sql<boolean>`${column} < ${filter.bounds.lt}`);
|
|
2415
|
+
}
|
|
2416
|
+
if (filter.bounds.lte !== undefined) {
|
|
2417
|
+
next = next.where(() => sql<boolean>`${column} <= ${filter.bounds.lte}`);
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
return next;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
1678
2423
|
/**
|
|
1679
2424
|
* Map order field names to database columns.
|
|
1680
2425
|
* Only allows known fields to prevent column enumeration via crafted orderBy values.
|
|
1681
2426
|
*/
|
|
1682
|
-
private mapOrderField(field: string): string {
|
|
1683
|
-
const
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
}
|
|
2427
|
+
private mapOrderField(field: string, sortableExtras: readonly string[] = []): string {
|
|
2428
|
+
const mapped = ORDER_FIELD_COLUMNS[field];
|
|
2429
|
+
if (mapped) return mapped;
|
|
2430
|
+
|
|
2431
|
+
// A collection's configured titleField/dateField are allowed as
|
|
2432
|
+
// sort columns. The caller passes the collection's *actual* values (resolved
|
|
2433
|
+
// server-side, never client-supplied), so this stays a closed set per
|
|
2434
|
+
// request and doesn't reopen the column-enumeration hole. The slug is a
|
|
2435
|
+
// validated identifier that maps directly to the column.
|
|
2436
|
+
if (sortableExtras.includes(field)) {
|
|
2437
|
+
validateIdentifier(field, "order field");
|
|
2438
|
+
return field;
|
|
2439
|
+
}
|
|
1695
2440
|
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
2441
|
+
throw new EmDashValidationError(`Invalid order field: ${field}`);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
private async resolveOrderField(
|
|
2445
|
+
type: string,
|
|
2446
|
+
field: string,
|
|
2447
|
+
sortableExtras: readonly string[] = [],
|
|
2448
|
+
): Promise<ResolvedOrderField> {
|
|
2449
|
+
try {
|
|
2450
|
+
return { column: this.mapOrderField(field, sortableExtras), indexedCustomField: false };
|
|
2451
|
+
} catch (error) {
|
|
2452
|
+
if (!(error instanceof EmDashValidationError)) throw error;
|
|
2453
|
+
}
|
|
2454
|
+
|
|
2455
|
+
const customField = await this.db
|
|
2456
|
+
.selectFrom("_emdash_collections as collection")
|
|
2457
|
+
.leftJoin("_emdash_fields as field", (join) =>
|
|
2458
|
+
join
|
|
2459
|
+
.onRef("field.collection_id", "=", "collection.id")
|
|
2460
|
+
.on("field.slug", "=", field)
|
|
2461
|
+
.on("field.indexed", "=", 1),
|
|
2462
|
+
)
|
|
2463
|
+
.where("collection.slug", "=", type)
|
|
2464
|
+
.select(["collection.id as collectionId", "field.slug as fieldSlug"])
|
|
2465
|
+
.executeTakeFirst();
|
|
2466
|
+
|
|
2467
|
+
if (!customField) {
|
|
2468
|
+
throw new ContentCollectionNotFoundError(type);
|
|
1699
2469
|
}
|
|
1700
|
-
|
|
2470
|
+
|
|
2471
|
+
if (!customField.fieldSlug) {
|
|
2472
|
+
throw new EmDashValidationError(
|
|
2473
|
+
`Invalid order field: ${field}. Custom fields must be indexed before sorting.`,
|
|
2474
|
+
);
|
|
2475
|
+
}
|
|
2476
|
+
|
|
2477
|
+
validateIdentifier(customField.fieldSlug, "content order field");
|
|
2478
|
+
return { column: customField.fieldSlug, indexedCustomField: true };
|
|
1701
2479
|
}
|
|
1702
2480
|
}
|