emdash 0.8.0 → 0.10.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-BKSf3T9R.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-BKSf3T9R.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-x0eMK1lX.mjs → apply-UsrFuO7l.mjs} +207 -355
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +118 -4
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +6 -7
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +14 -57
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +15 -10
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +8 -5
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +70 -121
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -10
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-Chbr2GoP.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-Chbr2GoP.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/bylines-esI7ioa9.mjs +113 -0
- package/dist/bylines-esI7ioa9.mjs.map +1 -0
- package/dist/cache-fTzxgMFJ.mjs +65 -0
- package/dist/cache-fTzxgMFJ.mjs.map +1 -0
- package/dist/{chunks-HGz06Soa.mjs → chunks-Da2-b-oA.mjs} +8 -2
- package/dist/{chunks-HGz06Soa.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +456 -90
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +3 -3
- package/dist/client/index.mjs.map +1 -1
- package/dist/{config-BXwuX8Bx.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BXwuX8Bx.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-BcQPYxdV.mjs → content-C7G4QXkK.mjs} +42 -14
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/libsql.d.mts.map +1 -1
- package/dist/db/libsql.mjs +7 -2
- package/dist/db/libsql.mjs.map +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/db/sqlite.d.mts.map +1 -1
- package/dist/db/sqlite.mjs +8 -3
- package/dist/db/sqlite.mjs.map +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-l1Qh2RPR.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-DCVqE5ib.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-DCVqE5ib.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{dialect-helpers-DhTzaUxP.mjs → dialect-helpers-BKCvISIQ.mjs} +19 -2
- package/dist/dialect-helpers-BKCvISIQ.mjs.map +1 -0
- package/dist/{error-zG5T1UGA.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-zG5T1UGA.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-DIb-CzNx.d.mts → index-DjPMOfO0.d.mts} +162 -87
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +27 -24
- package/dist/{load-CyEoextb.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-CyEoextb.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CndGj8kM.mjs → loader-Bx2_9-5e.mjs} +53 -8
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DH9xhc6t.mjs → manifest-schema-CXAbd1vH.mjs} +33 -3
- package/dist/manifest-schema-CXAbd1vH.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/index.mjs +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-BnAOqItE.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-BnAOqItE.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/options-nPxWnrya.mjs +117 -0
- package/dist/options-nPxWnrya.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/{patterns-CrCYkMBb.mjs → patterns-DsUZ4uxI.mjs} +1 -1
- package/dist/{patterns-CrCYkMBb.mjs.map → patterns-DsUZ4uxI.mjs.map} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-D29tWZ7o.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-C-fk5hYI.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.d.mts.map +1 -1
- package/dist/plugins/adapt-sandbox-entry.mjs +6 -5
- package/dist/plugins/adapt-sandbox-entry.mjs.map +1 -1
- package/dist/public-url-B1AxbbbQ.mjs +51 -0
- package/dist/public-url-B1AxbbbQ.mjs.map +1 -0
- package/dist/{query-fqEdLFms.mjs → query-Bo-msrmu.mjs} +114 -16
- package/dist/query-Bo-msrmu.mjs.map +1 -0
- package/dist/{redirect-D_pshWdf.mjs → redirect-C5H7VGIX.mjs} +11 -6
- package/dist/redirect-C5H7VGIX.mjs.map +1 -0
- package/dist/{registry-C3Mr0ODu.mjs → registry-Beb7wxFc.mjs} +39 -5
- package/dist/registry-Beb7wxFc.mjs.map +1 -0
- package/dist/{request-cache-Ci7f5pBb.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-Ci7f5pBb.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/runner-Clwe4Mme.d.mts +44 -0
- package/dist/runner-Clwe4Mme.d.mts.map +1 -0
- package/dist/{runner-tQ7BJ4T7.mjs → runner-DMnlIkh4.mjs} +616 -191
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BoZYFuUk.mjs → search-DkN-BqsS.mjs} +270 -152
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/secrets-CZ8rxLX3.mjs +314 -0
- package/dist/secrets-CZ8rxLX3.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +13 -11
- package/dist/seo/index.d.mts +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-CTtewrSQ.mjs +407 -0
- package/dist/taxonomies-CTtewrSQ.mjs.map +1 -0
- package/dist/taxonomy-DSxx2K2L.mjs +218 -0
- package/dist/taxonomy-DSxx2K2L.mjs.map +1 -0
- package/dist/{tokens-D9vnZqYS.mjs → tokens-CyRDPVW2.mjs} +1 -1
- package/dist/{tokens-D9vnZqYS.mjs.map → tokens-CyRDPVW2.mjs.map} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs → transaction-D44LBXvU.mjs} +1 -1
- package/dist/{transaction-Cn2rjY78.mjs.map → transaction-D44LBXvU.mjs.map} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-CUnEL3Vs.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-C9ugt2Nr.mjs → transport-xpzIjCIB.mjs} +6 -5
- package/dist/{transport-C9ugt2Nr.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-BrA0xf5I.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-BrA0xf5I.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-DIMwPFub.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-DIMwPFub.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/types-CoO6mpV3.mjs +68 -0
- package/dist/types-CoO6mpV3.mjs.map +1 -0
- package/dist/{types-i36XcA_X.d.mts → types-D19uBYWn.d.mts} +83 -7
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BmPPSUEx.d.mts → types-Dl1fgFjn.d.mts} +24 -2
- package/dist/{types-BmPPSUEx.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CS8FIX7L.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-Bm1dn-q3.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-Bm1dn-q3.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CgqmmMJB.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CgqmmMJB.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-CxVsLehf.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-DHxmpFJt.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-C-ZpN2GI.mjs → validation-B1NYiEos.mjs} +6 -6
- package/dist/{validation-C-ZpN2GI.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-Bbq8TCrz.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CpwccCIv.mjs → zod-generator-BNJDQBSZ.mjs} +11 -6
- package/dist/{zod-generator-CpwccCIv.mjs.map → zod-generator-BNJDQBSZ.mjs.map} +1 -1
- package/locals.d.ts +1 -6
- package/package.json +9 -8
- package/src/api/handlers/comments.ts +6 -4
- package/src/api/handlers/content.ts +40 -1
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/device-flow.ts +5 -0
- package/src/api/handlers/marketplace.ts +11 -4
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/oauth-authorization.ts +72 -33
- package/src/api/handlers/revision.ts +23 -14
- package/src/api/handlers/taxonomies.ts +273 -100
- package/src/api/public-url.ts +48 -2
- package/src/api/schemas/comments.ts +2 -2
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/content.ts +17 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/sections.ts +3 -3
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/api/schemas/users.ts +1 -1
- package/src/api/types.ts +5 -1
- package/src/astro/integration/index.ts +17 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/integration/runtime.ts +30 -0
- package/src/astro/integration/virtual-modules.ts +32 -2
- package/src/astro/integration/vite-config.ts +6 -1
- package/src/astro/middleware/auth.ts +13 -6
- package/src/astro/middleware/redirect.ts +29 -16
- package/src/astro/middleware/request-context.ts +15 -5
- package/src/astro/middleware.ts +23 -9
- package/src/astro/routes/api/auth/invite/complete.ts +6 -1
- package/src/astro/routes/api/auth/passkey/register/verify.ts +6 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +6 -1
- package/src/astro/routes/api/auth/signup/complete.ts +6 -1
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -2
- package/src/astro/routes/api/content/[collection]/[id]/discard-draft.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +34 -12
- package/src/astro/routes/api/content/[collection]/[id]/publish.ts +32 -2
- package/src/astro/routes/api/content/[collection]/[id]/restore.ts +4 -2
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +3 -2
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +8 -4
- package/src/astro/routes/api/content/[collection]/[id].ts +12 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +3 -1
- package/src/astro/routes/api/import/wordpress/prepare.ts +7 -8
- package/src/astro/routes/api/import/wordpress/rewrite-url-helpers.ts +196 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +9 -177
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +3 -1
- package/src/astro/routes/api/manifest.ts +62 -45
- package/src/astro/routes/api/media/[id]/confirm.ts +10 -1
- package/src/astro/routes/api/media/providers/[providerId]/index.ts +12 -3
- package/src/astro/routes/api/menus/[name]/items.ts +16 -6
- package/src/astro/routes/api/menus/[name]/reorder.ts +8 -3
- package/src/astro/routes/api/menus/[name]/translations.ts +82 -0
- package/src/astro/routes/api/menus/[name].ts +19 -10
- package/src/astro/routes/api/menus/index.ts +9 -6
- package/src/astro/routes/api/openapi.json.ts +27 -10
- package/src/astro/routes/api/redirects/404s/index.ts +10 -4
- package/src/astro/routes/api/redirects/404s/summary.ts +4 -2
- package/src/astro/routes/api/redirects/[id].ts +10 -4
- package/src/astro/routes/api/redirects/index.ts +7 -3
- package/src/astro/routes/api/revisions/[revisionId]/index.ts +1 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +0 -2
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +0 -1
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -2
- package/src/astro/routes/api/schema/collections/index.ts +1 -1
- package/src/astro/routes/api/search/index.ts +10 -2
- package/src/astro/routes/api/sections/[slug].ts +10 -4
- package/src/astro/routes/api/sections/index.ts +7 -3
- package/src/astro/routes/api/setup/admin-verify.ts +6 -1
- package/src/astro/routes/api/snapshot.ts +44 -18
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug]/translations.ts +89 -0
- package/src/astro/routes/api/taxonomies/[name]/terms/[slug].ts +22 -22
- package/src/astro/routes/api/taxonomies/[name]/terms/index.ts +11 -14
- package/src/astro/routes/api/taxonomies/index.ts +9 -7
- package/src/astro/routes/api/themes/preview.ts +11 -5
- package/src/astro/types.ts +23 -3
- package/src/auth/allowed-origins.ts +168 -0
- package/src/auth/passkey-config.ts +35 -13
- package/src/bylines/index.ts +37 -88
- package/src/cli/commands/auth.ts +28 -6
- package/src/cli/commands/bundle-utils.ts +11 -2
- package/src/cli/commands/bundle.ts +28 -8
- package/src/cli/commands/content.ts +13 -0
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/login.ts +8 -1
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/cli/commands/publish.ts +24 -0
- package/src/cli/commands/secrets.ts +183 -0
- package/src/cli/credentials.ts +1 -1
- package/src/cli/index.ts +5 -1
- package/src/client/index.ts +4 -4
- package/src/client/transport.ts +17 -7
- package/src/components/Break.astro +2 -2
- package/src/components/EmDashHead.astro +18 -13
- package/src/components/Embed.astro +1 -1
- package/src/components/Gallery.astro +1 -1
- package/src/components/Image.astro +1 -1
- package/src/components/InlinePortableTextEditor.tsx +104 -18
- package/src/config/secrets.ts +528 -0
- package/src/database/dialect-helpers.ts +50 -0
- package/src/database/migrations/034_published_at_index.ts +1 -1
- package/src/database/migrations/035_bounded_404_log.ts +56 -39
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +158 -23
- package/src/database/repositories/content.ts +47 -12
- package/src/database/repositories/redirect.ts +14 -3
- package/src/database/repositories/taxonomy.ts +212 -82
- package/src/database/types.ts +10 -2
- package/src/db/libsql.ts +1 -3
- package/src/db/sqlite.ts +2 -5
- package/src/emdash-runtime.ts +84 -159
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +9 -0
- package/src/loader.ts +73 -3
- package/src/mcp/server.ts +180 -54
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/page/site-identity.ts +58 -0
- package/src/plugins/adapt-sandbox-entry.ts +22 -10
- package/src/plugins/context.ts +13 -10
- package/src/plugins/define-plugin.ts +40 -12
- package/src/plugins/hooks.ts +23 -19
- package/src/plugins/index.ts +9 -0
- package/src/plugins/manifest-schema.ts +37 -2
- package/src/plugins/types.ts +151 -11
- package/src/preview/urls.ts +23 -3
- package/src/query.ts +148 -5
- package/src/redirects/cache.ts +38 -18
- package/src/schema/registry.ts +56 -0
- package/src/schema/zod-generator.ts +39 -7
- package/src/seed/apply.ts +142 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/settings/index.ts +80 -6
- package/src/settings/types.ts +23 -1
- package/src/taxonomies/index.ts +237 -210
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-x0eMK1lX.mjs.map +0 -1
- package/dist/bylines-CRNsVG88.mjs +0 -157
- package/dist/bylines-CRNsVG88.mjs.map +0 -1
- package/dist/cache-BkKBuIvS.mjs +0 -56
- package/dist/cache-BkKBuIvS.mjs.map +0 -1
- package/dist/chunk-ClPoSABd.mjs +0 -21
- package/dist/content-BcQPYxdV.mjs.map +0 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +0 -1
- package/dist/index-DIb-CzNx.d.mts.map +0 -1
- package/dist/loader-CndGj8kM.mjs.map +0 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +0 -1
- package/dist/query-fqEdLFms.mjs.map +0 -1
- package/dist/redirect-D_pshWdf.mjs.map +0 -1
- package/dist/registry-C3Mr0ODu.mjs.map +0 -1
- package/dist/runner-OURCaApa.d.mts +0 -34
- package/dist/runner-OURCaApa.d.mts.map +0 -1
- package/dist/runner-tQ7BJ4T7.mjs.map +0 -1
- package/dist/search-BoZYFuUk.mjs.map +0 -1
- package/dist/taxonomies-B4IAshV8.mjs +0 -308
- package/dist/taxonomies-B4IAshV8.mjs.map +0 -1
- package/dist/types-CS8FIX7L.d.mts.map +0 -1
- package/dist/types-i36XcA_X.d.mts.map +0 -1
- package/dist/validate-CxVsLehf.mjs.map +0 -1
- package/dist/validate-DHxmpFJt.d.mts.map +0 -1
- package/dist/version-Bbq8TCrz.mjs +0 -7
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
+
import { normalizeCapabilities } from "./types.js";
|
|
16
17
|
import type {
|
|
17
18
|
PluginDefinition,
|
|
18
19
|
ResolvedPlugin,
|
|
@@ -20,6 +21,7 @@ import type {
|
|
|
20
21
|
ResolvedPluginHooks,
|
|
21
22
|
ResolvedHook,
|
|
22
23
|
HookConfig,
|
|
24
|
+
PluginCapability,
|
|
23
25
|
PluginStorageConfig,
|
|
24
26
|
StandardPluginDefinition,
|
|
25
27
|
} from "./types.js";
|
|
@@ -65,7 +67,7 @@ const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
|
|
|
65
67
|
* export default definePlugin({
|
|
66
68
|
* id: "my-plugin",
|
|
67
69
|
* version: "1.0.0",
|
|
68
|
-
* capabilities: ["read
|
|
70
|
+
* capabilities: ["content:read"],
|
|
69
71
|
* hooks: {
|
|
70
72
|
* "content:beforeSave": async (event, ctx) => {
|
|
71
73
|
* ctx.log.info("Saving content", { collection: event.collection });
|
|
@@ -143,8 +145,24 @@ function defineNativePlugin<TStorage extends PluginStorageConfig>(
|
|
|
143
145
|
throw new Error(`Invalid plugin version "${version}". Must be semver format (e.g., "1.0.0").`);
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
// Validate capabilities
|
|
147
|
-
|
|
148
|
+
// Validate capabilities. Both current names and deprecated aliases are
|
|
149
|
+
// accepted; aliases are silently rewritten to current names below so the
|
|
150
|
+
// runtime only ever sees the canonical form. Authors are warned at
|
|
151
|
+
// bundle/validate and hard-failed at publish.
|
|
152
|
+
const validCapabilities = new Set<string>([
|
|
153
|
+
// Current names
|
|
154
|
+
"network:request",
|
|
155
|
+
"network:request:unrestricted",
|
|
156
|
+
"content:read",
|
|
157
|
+
"content:write",
|
|
158
|
+
"media:read",
|
|
159
|
+
"media:write",
|
|
160
|
+
"users:read",
|
|
161
|
+
"email:send",
|
|
162
|
+
"hooks.email-transport:register",
|
|
163
|
+
"hooks.email-events:register",
|
|
164
|
+
"hooks.page-fragments:register",
|
|
165
|
+
// Deprecated aliases
|
|
148
166
|
"network:fetch",
|
|
149
167
|
"network:fetch:any",
|
|
150
168
|
"read:content",
|
|
@@ -152,7 +170,6 @@ function defineNativePlugin<TStorage extends PluginStorageConfig>(
|
|
|
152
170
|
"read:media",
|
|
153
171
|
"write:media",
|
|
154
172
|
"read:users",
|
|
155
|
-
"email:send",
|
|
156
173
|
"email:provide",
|
|
157
174
|
"email:intercept",
|
|
158
175
|
"page:inject",
|
|
@@ -163,16 +180,27 @@ function defineNativePlugin<TStorage extends PluginStorageConfig>(
|
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
182
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
183
|
+
// Silent normalization: rewrite deprecated names to current names. Done
|
|
184
|
+
// before the implication pass so implications work on canonical names.
|
|
185
|
+
// `as PluginCapability[]` is safe because `normalizeCapabilities` only
|
|
186
|
+
// returns strings from the validated input plus current names from the
|
|
187
|
+
// rename map, all of which are in the union.
|
|
188
|
+
const canonical = normalizeCapabilities(capabilities) as PluginCapability[];
|
|
189
|
+
|
|
190
|
+
// Capability implications: broader capabilities imply narrower ones.
|
|
191
|
+
// Operates on canonical names only.
|
|
192
|
+
const normalizedCapabilities: PluginCapability[] = [...canonical];
|
|
193
|
+
if (canonical.includes("content:write") && !canonical.includes("content:read")) {
|
|
194
|
+
normalizedCapabilities.push("content:read");
|
|
170
195
|
}
|
|
171
|
-
if (
|
|
172
|
-
normalizedCapabilities.push("read
|
|
196
|
+
if (canonical.includes("media:write") && !canonical.includes("media:read")) {
|
|
197
|
+
normalizedCapabilities.push("media:read");
|
|
173
198
|
}
|
|
174
|
-
if (
|
|
175
|
-
|
|
199
|
+
if (
|
|
200
|
+
canonical.includes("network:request:unrestricted") &&
|
|
201
|
+
!canonical.includes("network:request")
|
|
202
|
+
) {
|
|
203
|
+
normalizedCapabilities.push("network:request");
|
|
176
204
|
}
|
|
177
205
|
|
|
178
206
|
// Normalize hooks
|
package/src/plugins/hooks.ts
CHANGED
|
@@ -248,28 +248,32 @@ export class HookPipeline {
|
|
|
248
248
|
* capability will have that hook silently skipped at registration time.
|
|
249
249
|
*/
|
|
250
250
|
private static readonly HOOK_REQUIRED_CAPABILITY: ReadonlyMap<string, string> = new Map([
|
|
251
|
-
// Email
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
//
|
|
256
|
-
|
|
257
|
-
["
|
|
258
|
-
["
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
["content:
|
|
262
|
-
["content:
|
|
251
|
+
// Email — registering email:beforeSend/afterSend/deliver requires the
|
|
252
|
+
// matching `hooks.email-*:register` capability. These are distinct
|
|
253
|
+
// from `email:send` (which gates ctx.email) so that "this plugin
|
|
254
|
+
// reads/writes email events" is visible separately from "this
|
|
255
|
+
// plugin can send email".
|
|
256
|
+
["email:beforeSend", "hooks.email-events:register"],
|
|
257
|
+
["email:afterSend", "hooks.email-events:register"],
|
|
258
|
+
["email:deliver", "hooks.email-transport:register"],
|
|
259
|
+
// Content — beforeSave can mutate content, so requires content:write.
|
|
260
|
+
// afterSave is read-only notification, so content:read suffices.
|
|
261
|
+
["content:beforeSave", "content:write"],
|
|
262
|
+
["content:afterSave", "content:read"],
|
|
263
|
+
["content:beforeDelete", "content:read"],
|
|
264
|
+
["content:afterDelete", "content:read"],
|
|
265
|
+
["content:afterPublish", "content:read"],
|
|
266
|
+
["content:afterUnpublish", "content:read"],
|
|
263
267
|
// Media
|
|
264
|
-
["media:beforeUpload", "write
|
|
265
|
-
["media:afterUpload", "read
|
|
268
|
+
["media:beforeUpload", "media:write"],
|
|
269
|
+
["media:afterUpload", "media:read"],
|
|
266
270
|
// Comments — hooks expose author email, IP hash, user agent
|
|
267
|
-
["comment:beforeCreate", "read
|
|
268
|
-
["comment:moderate", "read
|
|
269
|
-
["comment:afterCreate", "read
|
|
270
|
-
["comment:afterModerate", "read
|
|
271
|
+
["comment:beforeCreate", "users:read"],
|
|
272
|
+
["comment:moderate", "users:read"],
|
|
273
|
+
["comment:afterCreate", "users:read"],
|
|
274
|
+
["comment:afterModerate", "users:read"],
|
|
271
275
|
// Page fragments — can inject arbitrary scripts into every public page
|
|
272
|
-
["page:fragments", "page:
|
|
276
|
+
["page:fragments", "hooks.page-fragments:register"],
|
|
273
277
|
]);
|
|
274
278
|
|
|
275
279
|
/**
|
package/src/plugins/index.ts
CHANGED
|
@@ -192,3 +192,12 @@ export type {
|
|
|
192
192
|
StandardRouteEntry,
|
|
193
193
|
} from "./types.js";
|
|
194
194
|
export { isStandardPluginDefinition } from "./types.js";
|
|
195
|
+
|
|
196
|
+
// Capability normalization (legacy → canonical alias layer)
|
|
197
|
+
export {
|
|
198
|
+
CAPABILITY_RENAMES,
|
|
199
|
+
isDeprecatedCapability,
|
|
200
|
+
normalizeCapability,
|
|
201
|
+
normalizeCapabilities,
|
|
202
|
+
} from "./types.js";
|
|
203
|
+
export type { CurrentPluginCapability, DeprecatedPluginCapability } from "./types.js";
|
|
@@ -12,7 +12,31 @@ import { z } from "zod";
|
|
|
12
12
|
|
|
13
13
|
// ── Enum values (must stay in sync with types.ts) ───────────────
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Current capability names — the ones authors should use going forward.
|
|
17
|
+
* See `PluginCapability` in `types.ts` for documentation of each.
|
|
18
|
+
*/
|
|
19
|
+
export const CURRENT_PLUGIN_CAPABILITIES = [
|
|
20
|
+
"network:request",
|
|
21
|
+
"network:request:unrestricted",
|
|
22
|
+
"content:read",
|
|
23
|
+
"content:write",
|
|
24
|
+
"media:read",
|
|
25
|
+
"media:write",
|
|
26
|
+
"users:read",
|
|
27
|
+
"email:send",
|
|
28
|
+
"hooks.email-transport:register",
|
|
29
|
+
"hooks.email-events:register",
|
|
30
|
+
"hooks.page-fragments:register",
|
|
31
|
+
] as const;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Legacy capability names accepted during the deprecation window.
|
|
35
|
+
* Normalized to current names via `normalizeCapability()` in types.ts
|
|
36
|
+
* before reaching the runtime. Plugin authors are warned at bundle/validate
|
|
37
|
+
* and hard-failed at publish.
|
|
38
|
+
*/
|
|
39
|
+
export const DEPRECATED_PLUGIN_CAPABILITIES = [
|
|
16
40
|
"network:fetch",
|
|
17
41
|
"network:fetch:any",
|
|
18
42
|
"read:content",
|
|
@@ -20,12 +44,23 @@ export const PLUGIN_CAPABILITIES = [
|
|
|
20
44
|
"read:media",
|
|
21
45
|
"write:media",
|
|
22
46
|
"read:users",
|
|
23
|
-
"email:send",
|
|
24
47
|
"email:provide",
|
|
25
48
|
"email:intercept",
|
|
26
49
|
"page:inject",
|
|
27
50
|
] as const;
|
|
28
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Full set of accepted capability strings — current + deprecated.
|
|
54
|
+
*
|
|
55
|
+
* The manifest schema accepts both during the transition. The runtime only
|
|
56
|
+
* ever sees current names because `normalizeCapability()` rewrites legacy
|
|
57
|
+
* names at every external boundary (definePlugin, adaptSandboxEntry).
|
|
58
|
+
*/
|
|
59
|
+
export const PLUGIN_CAPABILITIES = [
|
|
60
|
+
...CURRENT_PLUGIN_CAPABILITIES,
|
|
61
|
+
...DEPRECATED_PLUGIN_CAPABILITIES,
|
|
62
|
+
] as const;
|
|
63
|
+
|
|
29
64
|
/** Must stay in sync with FieldType in schema/types.ts */
|
|
30
65
|
const FIELD_TYPES = [
|
|
31
66
|
"string",
|
package/src/plugins/types.ts
CHANGED
|
@@ -20,20 +20,151 @@ import type { FieldType } from "../schema/types.js";
|
|
|
20
20
|
// =============================================================================
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Plugin capabilities determine what APIs are available in context
|
|
23
|
+
* Plugin capabilities determine what APIs are available in context.
|
|
24
|
+
*
|
|
25
|
+
* Capabilities follow the formula `<resource>[.<sub-resource>]:<verb>[:<qualifier>]`
|
|
26
|
+
* — resource first, verb second, matching RBAC. The `unrestricted` qualifier
|
|
27
|
+
* (used by `network:request:unrestricted`) is intentionally verbose so that
|
|
28
|
+
* granting it stands out in manifest review.
|
|
29
|
+
*
|
|
30
|
+
* Hook-registration capabilities (`hooks.<family>:register`) are a distinct
|
|
31
|
+
* audit category from data-access capabilities — they gate which hooks a
|
|
32
|
+
* plugin is allowed to register, not which context APIs it gets.
|
|
33
|
+
*
|
|
34
|
+
* @see CAPABILITY_RENAMES for the legacy → current mapping, and
|
|
35
|
+
* `normalizeCapability()` for the runtime alias layer.
|
|
24
36
|
*/
|
|
25
37
|
export type PluginCapability =
|
|
26
|
-
|
|
27
|
-
| "network:
|
|
28
|
-
| "
|
|
29
|
-
|
|
30
|
-
| "read
|
|
31
|
-
| "write
|
|
32
|
-
|
|
38
|
+
// ── Network ─────────────────────────────────────────────────
|
|
39
|
+
| "network:request" // ctx.http is available (host-restricted via allowedHosts)
|
|
40
|
+
| "network:request:unrestricted" // ctx.http is available (unrestricted outbound — use for user-configured URLs)
|
|
41
|
+
// ── Content ─────────────────────────────────────────────────
|
|
42
|
+
| "content:read" // ctx.content.get/list available
|
|
43
|
+
| "content:write" // ctx.content.create/update/delete available
|
|
44
|
+
// ── Media ───────────────────────────────────────────────────
|
|
45
|
+
| "media:read" // ctx.media.get/list available
|
|
46
|
+
| "media:write" // ctx.media.getUploadUrl/delete available
|
|
47
|
+
// ── Users ───────────────────────────────────────────────────
|
|
48
|
+
| "users:read" // ctx.users is available
|
|
49
|
+
// ── Email ───────────────────────────────────────────────────
|
|
33
50
|
| "email:send" // ctx.email is available (when a provider is configured)
|
|
34
|
-
|
|
35
|
-
| "email:
|
|
36
|
-
| "
|
|
51
|
+
// ── Hook registration ───────────────────────────────────────
|
|
52
|
+
| "hooks.email-transport:register" // can register email:deliver exclusive hook (transport provider)
|
|
53
|
+
| "hooks.email-events:register" // can register email:beforeSend / email:afterSend hooks
|
|
54
|
+
| "hooks.page-fragments:register" // can register page:fragments hook (inject scripts/styles into pages)
|
|
55
|
+
// ── Deprecated (legacy aliases) ─────────────────────────────
|
|
56
|
+
// Kept in the union for one minor with @deprecated tags so existing
|
|
57
|
+
// plugins typecheck during migration. Normalized to current names at
|
|
58
|
+
// definition time via normalizeCapability(). Will be removed in the
|
|
59
|
+
// following minor.
|
|
60
|
+
/** @deprecated Use `network:request` instead. */
|
|
61
|
+
| "network:fetch"
|
|
62
|
+
/** @deprecated Use `network:request:unrestricted` instead. */
|
|
63
|
+
| "network:fetch:any"
|
|
64
|
+
/** @deprecated Use `content:read` instead. */
|
|
65
|
+
| "read:content"
|
|
66
|
+
/** @deprecated Use `content:write` instead. */
|
|
67
|
+
| "write:content"
|
|
68
|
+
/** @deprecated Use `media:read` instead. */
|
|
69
|
+
| "read:media"
|
|
70
|
+
/** @deprecated Use `media:write` instead. */
|
|
71
|
+
| "write:media"
|
|
72
|
+
/** @deprecated Use `users:read` instead. */
|
|
73
|
+
| "read:users"
|
|
74
|
+
/** @deprecated Use `hooks.email-transport:register` instead. */
|
|
75
|
+
| "email:provide"
|
|
76
|
+
/** @deprecated Use `hooks.email-events:register` instead. */
|
|
77
|
+
| "email:intercept"
|
|
78
|
+
/** @deprecated Use `hooks.page-fragments:register` instead. */
|
|
79
|
+
| "page:inject";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Deprecated capability names that map to current names.
|
|
83
|
+
*
|
|
84
|
+
* These are accepted at every external boundary (manifest parse, definePlugin,
|
|
85
|
+
* adaptSandboxEntry) and silently normalized to the new names before reaching
|
|
86
|
+
* the runtime. The runtime never sees deprecated names.
|
|
87
|
+
*
|
|
88
|
+
* Authors are warned at `bundle` / `validate`, and hard-failed at `publish`.
|
|
89
|
+
*/
|
|
90
|
+
export type DeprecatedPluginCapability =
|
|
91
|
+
| "network:fetch"
|
|
92
|
+
| "network:fetch:any"
|
|
93
|
+
| "read:content"
|
|
94
|
+
| "write:content"
|
|
95
|
+
| "read:media"
|
|
96
|
+
| "write:media"
|
|
97
|
+
| "read:users"
|
|
98
|
+
| "email:provide"
|
|
99
|
+
| "email:intercept"
|
|
100
|
+
| "page:inject";
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Current (non-deprecated) capability names.
|
|
104
|
+
*/
|
|
105
|
+
export type CurrentPluginCapability = Exclude<PluginCapability, DeprecatedPluginCapability>;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Mapping from deprecated capability names to their current replacements.
|
|
109
|
+
*
|
|
110
|
+
* Used by `normalizeCapability()` and the marketplace `diffCapabilities`
|
|
111
|
+
* helper to compare manifests across the rename without flagging spurious
|
|
112
|
+
* "capability changed" prompts on upgrade.
|
|
113
|
+
*/
|
|
114
|
+
export const CAPABILITY_RENAMES: Readonly<
|
|
115
|
+
Record<DeprecatedPluginCapability, CurrentPluginCapability>
|
|
116
|
+
> = Object.freeze({
|
|
117
|
+
"network:fetch": "network:request",
|
|
118
|
+
"network:fetch:any": "network:request:unrestricted",
|
|
119
|
+
"read:content": "content:read",
|
|
120
|
+
"write:content": "content:write",
|
|
121
|
+
"read:media": "media:read",
|
|
122
|
+
"write:media": "media:write",
|
|
123
|
+
"read:users": "users:read",
|
|
124
|
+
"email:provide": "hooks.email-transport:register",
|
|
125
|
+
"email:intercept": "hooks.email-events:register",
|
|
126
|
+
"page:inject": "hooks.page-fragments:register",
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Type guard: is this capability one of the deprecated legacy names?
|
|
131
|
+
*
|
|
132
|
+
* Uses an own-property check so that prototype keys like "toString" or
|
|
133
|
+
* "constructor" don't accidentally pass.
|
|
134
|
+
*/
|
|
135
|
+
export function isDeprecatedCapability(cap: string): cap is DeprecatedPluginCapability {
|
|
136
|
+
return Object.hasOwn(CAPABILITY_RENAMES, cap);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Normalize a capability string — deprecated names map to current names,
|
|
141
|
+
* current names pass through unchanged. Unknown strings are returned as-is
|
|
142
|
+
* so that downstream validators can produce a precise error.
|
|
143
|
+
*/
|
|
144
|
+
export function normalizeCapability(cap: string): string {
|
|
145
|
+
if (isDeprecatedCapability(cap)) {
|
|
146
|
+
return CAPABILITY_RENAMES[cap];
|
|
147
|
+
}
|
|
148
|
+
return cap;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Normalize an array of capabilities. Deduplicates by normalized name so
|
|
153
|
+
* that a plugin declaring both `read:content` and `content:read` ends up
|
|
154
|
+
* with a single `content:read` entry.
|
|
155
|
+
*/
|
|
156
|
+
export function normalizeCapabilities(caps: readonly string[]): string[] {
|
|
157
|
+
const seen = new Set<string>();
|
|
158
|
+
const out: string[] = [];
|
|
159
|
+
for (const cap of caps) {
|
|
160
|
+
const normalized = normalizeCapability(cap);
|
|
161
|
+
if (!seen.has(normalized)) {
|
|
162
|
+
seen.add(normalized);
|
|
163
|
+
out.push(normalized);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
|
37
168
|
|
|
38
169
|
// =============================================================================
|
|
39
170
|
// Storage Types
|
|
@@ -1201,6 +1332,15 @@ export interface PortableTextBlockConfig {
|
|
|
1201
1332
|
placeholder?: string;
|
|
1202
1333
|
/** Block Kit form fields for the editing UI. If declared, replaces the simple URL input. */
|
|
1203
1334
|
fields?: PortableTextBlockField[];
|
|
1335
|
+
/**
|
|
1336
|
+
* Optional. Display category in the slash menu. Defaults to "Embeds".
|
|
1337
|
+
*
|
|
1338
|
+
* Plugin authors should pick a meaningful category that reflects what the
|
|
1339
|
+
* block actually is — e.g. "Sections", "Marketing", "Media", "Embeds",
|
|
1340
|
+
* "Layout". Blocks with the same category are grouped together in the
|
|
1341
|
+
* editor's slash menu.
|
|
1342
|
+
*/
|
|
1343
|
+
category?: string;
|
|
1204
1344
|
}
|
|
1205
1345
|
|
|
1206
1346
|
/**
|
package/src/preview/urls.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { generatePreviewToken } from "./tokens.js";
|
|
8
8
|
|
|
9
|
+
const REPEATED_SLASHES = /\/{2,}/g;
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Options for generating a preview URL
|
|
11
13
|
*/
|
|
@@ -20,8 +22,18 @@ export interface GetPreviewUrlOptions {
|
|
|
20
22
|
expiresIn?: string | number;
|
|
21
23
|
/** Base URL of the site. If not provided, returns a relative URL. */
|
|
22
24
|
baseUrl?: string;
|
|
23
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Custom path pattern. Supports `{collection}`, `{id}` and `{locale}`
|
|
27
|
+
* placeholders. Default: `"/{collection}/{id}"`.
|
|
28
|
+
*/
|
|
24
29
|
pathPattern?: string;
|
|
30
|
+
/**
|
|
31
|
+
* Locale segment substituted for the `{locale}` placeholder in `pathPattern`.
|
|
32
|
+
* Pass an empty string to omit the locale prefix (e.g. for the default locale
|
|
33
|
+
* when `prefixDefaultLocale` is `false`); adjacent slashes left by an empty
|
|
34
|
+
* value are collapsed and any trailing slash is trimmed.
|
|
35
|
+
*/
|
|
36
|
+
locale?: string;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
/**
|
|
@@ -65,6 +77,7 @@ export async function getPreviewUrl(options: GetPreviewUrlOptions): Promise<stri
|
|
|
65
77
|
expiresIn = "1h",
|
|
66
78
|
baseUrl,
|
|
67
79
|
pathPattern = "/{collection}/{id}",
|
|
80
|
+
locale = "",
|
|
68
81
|
} = options;
|
|
69
82
|
|
|
70
83
|
// Generate the signed token
|
|
@@ -74,8 +87,15 @@ export async function getPreviewUrl(options: GetPreviewUrlOptions): Promise<stri
|
|
|
74
87
|
secret,
|
|
75
88
|
});
|
|
76
89
|
|
|
77
|
-
// Build the path
|
|
78
|
-
|
|
90
|
+
// Build the path. `{locale}` may resolve to an empty string (default locale
|
|
91
|
+
// without a prefix); collapse the resulting double slashes and trim a
|
|
92
|
+
// trailing slash so the URL stays clean.
|
|
93
|
+
let path = pathPattern
|
|
94
|
+
.replace("{collection}", collection)
|
|
95
|
+
.replace("{id}", id)
|
|
96
|
+
.replace("{locale}", locale);
|
|
97
|
+
path = path.replace(REPEATED_SLASHES, "/");
|
|
98
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
79
99
|
|
|
80
100
|
// Add token as query parameter
|
|
81
101
|
const url = new URL(path, baseUrl || "http://placeholder");
|
package/src/query.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* and sets the context; query functions read it automatically.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { encodeCursor } from "./database/repositories/types.js";
|
|
15
16
|
import { getFallbackChain, getI18nConfig, isI18nEnabled } from "./i18n/config.js";
|
|
17
|
+
import { CURSOR_RAW_VALUES } from "./loader.js";
|
|
16
18
|
import { requestCached } from "./request-cache.js";
|
|
17
19
|
import { getRequestContext } from "./request-context.js";
|
|
18
20
|
import { isMissingTableError } from "./utils/db-errors.js";
|
|
@@ -279,9 +281,144 @@ export async function getEmDashCollection<T extends string, D = InferCollectionD
|
|
|
279
281
|
// appears on the home page AND in the sidebar) — caching collapses
|
|
280
282
|
// those duplicate queries, along with the bylines and taxonomy-term
|
|
281
283
|
// hydration each call would otherwise re-do.
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
//
|
|
285
|
+
// Bucket small limits to a shared minimum so a page with several
|
|
286
|
+
// "recent N posts" widgets at slightly different limits (e.g. a
|
|
287
|
+
// post-detail page asking for 4 in the body and 5 in the sidebar)
|
|
288
|
+
// shares one fetch + hydration round-trip rather than running two.
|
|
289
|
+
// Cursor-paginated calls are exempt: their limit is part of the
|
|
290
|
+
// pagination contract.
|
|
291
|
+
const bucketed = bucketFilter(filter);
|
|
292
|
+
const cached = await requestCached(collectionCacheKey(type, bucketed.fetchFilter), () =>
|
|
293
|
+
getEmDashCollectionUncached<T, D>(type, bucketed.fetchFilter),
|
|
284
294
|
);
|
|
295
|
+
return bucketed.requestedLimit === undefined
|
|
296
|
+
? cached
|
|
297
|
+
: sliceCollectionResult(cached, bucketed.requestedLimit, filter?.orderBy);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Threshold for limit bucketing. Page templates routinely render small
|
|
302
|
+
* "recent posts" widgets at limits 3-8; rounding those up to a single
|
|
303
|
+
* shared bucket lets one fetch satisfy several widgets within a request.
|
|
304
|
+
* Above this, the requested limit is honoured exactly — bucketing limit:50
|
|
305
|
+
* to limit:64 would waste hydration work for callers fetching real pages.
|
|
306
|
+
*/
|
|
307
|
+
const BUCKET_LIMIT_THRESHOLD = 10;
|
|
308
|
+
|
|
309
|
+
interface BucketedFilter {
|
|
310
|
+
/** Filter to pass to the loader (with limit possibly raised). */
|
|
311
|
+
fetchFilter: CollectionFilter | undefined;
|
|
312
|
+
/** Original limit; defined only when bucketing was applied. */
|
|
313
|
+
requestedLimit: number | undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** @internal exported for unit tests; not part of the public API. */
|
|
317
|
+
export function bucketFilter(filter: CollectionFilter | undefined): BucketedFilter {
|
|
318
|
+
const limit = filter?.limit;
|
|
319
|
+
if (
|
|
320
|
+
limit === undefined ||
|
|
321
|
+
limit >= BUCKET_LIMIT_THRESHOLD ||
|
|
322
|
+
limit <= 0 ||
|
|
323
|
+
filter?.cursor !== undefined
|
|
324
|
+
) {
|
|
325
|
+
return { fetchFilter: filter, requestedLimit: undefined };
|
|
326
|
+
}
|
|
327
|
+
return {
|
|
328
|
+
fetchFilter: { ...filter, limit: BUCKET_LIMIT_THRESHOLD },
|
|
329
|
+
requestedLimit: limit,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Slice a cached bucketed result down to the originally-requested limit
|
|
335
|
+
* and recompute `nextCursor` from the row that would have been the
|
|
336
|
+
* over-fetch detector for that limit. When truncation is needed, returns
|
|
337
|
+
* a shallow-copied result with a new `entries` array; otherwise returns
|
|
338
|
+
* the cached result unchanged (including error results and results
|
|
339
|
+
* already within the requested limit).
|
|
340
|
+
*/
|
|
341
|
+
/** @internal exported for unit tests; not part of the public API. */
|
|
342
|
+
export function sliceCollectionResult<D>(
|
|
343
|
+
cached: CollectionResult<D>,
|
|
344
|
+
limit: number,
|
|
345
|
+
orderBy: OrderBySpec | undefined,
|
|
346
|
+
): CollectionResult<D> {
|
|
347
|
+
if (cached.error) return cached;
|
|
348
|
+
if (cached.entries.length <= limit) return cached;
|
|
349
|
+
const sliced = cached.entries.slice(0, limit);
|
|
350
|
+
// Mirror the loader's encoding: cursor points at the last returned row,
|
|
351
|
+
// so "next page" picks up at the row immediately after it. See
|
|
352
|
+
// buildCursorCondition in loader.ts — it filters strictly past this row.
|
|
353
|
+
const lastEntry = sliced.at(-1);
|
|
354
|
+
const nextCursor = lastEntry ? encodeEntryCursor(lastEntry, orderBy) : undefined;
|
|
355
|
+
return { ...cached, entries: sliced, nextCursor };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/** Map of database column names to camelCase keys present on entry.data. */
|
|
359
|
+
const ENTRY_DATA_KEY_MAP: Record<string, string> = {
|
|
360
|
+
created_at: "createdAt",
|
|
361
|
+
updated_at: "updatedAt",
|
|
362
|
+
published_at: "publishedAt",
|
|
363
|
+
scheduled_at: "scheduledAt",
|
|
364
|
+
author_id: "authorId",
|
|
365
|
+
primary_byline_id: "primaryBylineId",
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// Mirror loader.ts FIELD_NAME_PATTERN. Kept in sync intentionally — diverging
|
|
369
|
+
// would let the encoder accept a field name the loader's getPrimarySort then
|
|
370
|
+
// rejected, producing a cursor that paginates against a different column.
|
|
371
|
+
const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Encode a `nextCursor` from a content entry, mirroring the loader's
|
|
375
|
+
* encoding scheme: `(orderValue, id)` where `orderValue` is the primary
|
|
376
|
+
* sort field's stringified value. For date columns, reads the raw DB
|
|
377
|
+
* string the loader stashed via CURSOR_RAW_VALUES — round-tripping the
|
|
378
|
+
* parsed Date through `toISOString()` would lose precision for stored
|
|
379
|
+
* values that aren't already ISO-with-milliseconds.
|
|
380
|
+
*/
|
|
381
|
+
function encodeEntryCursor<D>(
|
|
382
|
+
entry: ContentEntry<D>,
|
|
383
|
+
orderBy: OrderBySpec | undefined,
|
|
384
|
+
): string | undefined {
|
|
385
|
+
const data = entryData(entry);
|
|
386
|
+
const id = dataStr(data, "id");
|
|
387
|
+
if (!id) return undefined;
|
|
388
|
+
|
|
389
|
+
// Match loader.ts getPrimarySort: take the first valid field, default to created_at.
|
|
390
|
+
let dbField = "created_at";
|
|
391
|
+
if (orderBy) {
|
|
392
|
+
for (const field of Object.keys(orderBy)) {
|
|
393
|
+
if (FIELD_NAME_PATTERN.test(field)) {
|
|
394
|
+
dbField = field;
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Date columns: prefer the raw stored string captured by the loader so
|
|
401
|
+
// the cursor matches what a direct loader fetch would emit, regardless
|
|
402
|
+
// of how the DB stored the timestamp.
|
|
403
|
+
const rawDateValuesRaw = Reflect.get(data, CURSOR_RAW_VALUES);
|
|
404
|
+
if (rawDateValuesRaw !== null && typeof rawDateValuesRaw === "object") {
|
|
405
|
+
const raw = Reflect.get(rawDateValuesRaw, dbField);
|
|
406
|
+
if (typeof raw === "string") return encodeCursor(raw, id);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const dataKey = ENTRY_DATA_KEY_MAP[dbField] ?? dbField;
|
|
410
|
+
const value = data[dataKey];
|
|
411
|
+
let orderValue: string;
|
|
412
|
+
if (value instanceof Date) {
|
|
413
|
+
orderValue = value.toISOString();
|
|
414
|
+
} else if (typeof value === "string" || typeof value === "number") {
|
|
415
|
+
orderValue = String(value);
|
|
416
|
+
} else {
|
|
417
|
+
// Match the loader's empty-string fallback for null/undefined order
|
|
418
|
+
// values so cursor decoding stays valid even at the boundary.
|
|
419
|
+
orderValue = "";
|
|
420
|
+
}
|
|
421
|
+
return encodeCursor(orderValue, id);
|
|
285
422
|
}
|
|
286
423
|
|
|
287
424
|
/**
|
|
@@ -562,10 +699,16 @@ async function hydrateEntryBylines<D>(type: string, entries: ContentEntry<D>[]):
|
|
|
562
699
|
try {
|
|
563
700
|
const { getBylinesForEntries } = await import("./bylines/index.js");
|
|
564
701
|
|
|
565
|
-
const
|
|
566
|
-
|
|
702
|
+
const refs = entries
|
|
703
|
+
.map((e) => {
|
|
704
|
+
const data = entryData(e);
|
|
705
|
+
const id = dataStr(data, "id");
|
|
706
|
+
return id ? { id, authorId: dataStr(data, "authorId") || null } : null;
|
|
707
|
+
})
|
|
708
|
+
.filter((r): r is { id: string; authorId: string | null } => r !== null);
|
|
709
|
+
if (refs.length === 0) return;
|
|
567
710
|
|
|
568
|
-
const bylinesMap = await getBylinesForEntries(type,
|
|
711
|
+
const bylinesMap = await getBylinesForEntries(type, refs);
|
|
569
712
|
|
|
570
713
|
for (const entry of entries) {
|
|
571
714
|
const data = entryData(entry);
|