emdash 0.5.0 → 0.7.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-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-5uslYdUu.mjs} +197 -25
- package/dist/apply-5uslYdUu.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 +203 -33
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +30 -4
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +11 -4
- 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 +467 -186
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +17 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +12 -11
- 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 +1 -1
- package/dist/{config-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/{content-BsBoyj8G.mjs → content-D7J5y73J.mjs} +27 -1
- package/dist/{content-BsBoyj8G.mjs.map → content-D7J5y73J.mjs.map} +1 -1
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- 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-D0UT85nC.mjs +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CCWzlriB.d.mts → index-De6_Xv3v.d.mts} +209 -19
- package/dist/index-De6_Xv3v.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.mjs.map} +1 -1
- 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-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-B6Vu0d2i.mjs → query-g4Ug-9j9.mjs} +79 -12
- package/dist/query-g4Ug-9j9.mjs.map +1 -0
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-CN0Rt9Ob.mjs} +66 -10
- package/dist/redirect-CN0Rt9Ob.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-BR2xKwhn.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-BR2xKwhn.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-Cn1SYvYF.mjs → search-B0effn3j.mjs} +210 -226
- package/dist/search-B0effn3j.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -9
- 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-K2z0Uhnj.mjs +308 -0
- package/dist/taxonomies-K2z0Uhnj.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-C2v0c34j.d.mts} +10 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-C2v0c34j.d.mts.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-DeG21anB.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-C3ronwXb.d.mts → types-DgrIP0tF.d.mts} +102 -4
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/{validate-Db1yNL3i.d.mts → validate-kM8Pjuf7.d.mts} +5 -52
- package/dist/validate-kM8Pjuf7.d.mts.map +1 -0
- package/dist/version-BnTKdfam.mjs +7 -0
- package/dist/{version-CMMjTuqu.mjs.map → version-BnTKdfam.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/content.ts +2 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +178 -0
- package/src/astro/integration/index.ts +44 -0
- package/src/astro/integration/routes.ts +6 -0
- package/src/astro/integration/runtime.ts +117 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +33 -1
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +21 -10
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +19 -1
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +5 -4
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +32 -8
- package/src/astro/routes/api/setup/index.ts +5 -2
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +1 -1
- package/src/astro/types.ts +9 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +39 -0
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/redirect.ts +111 -8
- package/src/database/types.ts +9 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +312 -92
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/mcp/server.ts +76 -3
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +41 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CCWzlriB.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-Cn1SYvYF.mjs.map +0 -1
- package/dist/types-C3ronwXb.d.mts.map +0 -1
- package/dist/types-DeG21anB.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-Db1yNL3i.d.mts.map +0 -1
- package/dist/version-CMMjTuqu.mjs +0 -7
package/src/search/query.ts
CHANGED
|
@@ -275,9 +275,10 @@ export async function getSuggestions(
|
|
|
275
275
|
const ftsTable = ftsManager.getFtsTableName(collection);
|
|
276
276
|
const contentTable = ftsManager.getContentTableName(collection);
|
|
277
277
|
|
|
278
|
-
// Use prefix search for autocomplete
|
|
279
|
-
|
|
280
|
-
|
|
278
|
+
// Use prefix search for autocomplete. `escapeQuery` already appends `*`
|
|
279
|
+
// to each term for prefix matching, so we must not append another one.
|
|
280
|
+
const prefixQuery = escapeQuery(query);
|
|
281
|
+
if (!prefixQuery) {
|
|
281
282
|
continue;
|
|
282
283
|
}
|
|
283
284
|
|
package/src/seed/apply.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { BylineRepository } from "../database/repositories/byline.js";
|
|
|
14
14
|
import { ContentRepository } from "../database/repositories/content.js";
|
|
15
15
|
import { MediaRepository } from "../database/repositories/media.js";
|
|
16
16
|
import { RedirectRepository } from "../database/repositories/redirect.js";
|
|
17
|
+
import { RevisionRepository } from "../database/repositories/revision.js";
|
|
17
18
|
import { TaxonomyRepository } from "../database/repositories/taxonomy.js";
|
|
18
19
|
import { withTransaction } from "../database/transaction.js";
|
|
19
20
|
import type { Database } from "../database/types.js";
|
|
@@ -371,6 +372,7 @@ export async function applySeed(
|
|
|
371
372
|
await withTransaction(db, async (trx) => {
|
|
372
373
|
const trxContentRepo = new ContentRepository(trx);
|
|
373
374
|
const trxBylineRepo = new BylineRepository(trx);
|
|
375
|
+
const trxRevisionRepo = new RevisionRepository(trx);
|
|
374
376
|
|
|
375
377
|
await trxContentRepo.update(collectionSlug, existing.id, {
|
|
376
378
|
status,
|
|
@@ -386,6 +388,23 @@ export async function applySeed(
|
|
|
386
388
|
true,
|
|
387
389
|
);
|
|
388
390
|
await applyContentTaxonomies(trx, collectionSlug, existing.id, entry, true);
|
|
391
|
+
|
|
392
|
+
// Seed is declarative — when status is "published", promote to a live
|
|
393
|
+
// revision so the admin UI shows "Unpublish" instead of "Save & Publish"
|
|
394
|
+
// and `live_revision_id` is populated for downstream queries.
|
|
395
|
+
//
|
|
396
|
+
// Create a fresh revision from the updated data and stage it as the
|
|
397
|
+
// draft so `publish()` picks it up instead of re-syncing stale data
|
|
398
|
+
// from an existing live revision.
|
|
399
|
+
if (status === "published") {
|
|
400
|
+
const draft = await trxRevisionRepo.create({
|
|
401
|
+
collection: collectionSlug,
|
|
402
|
+
entryId: existing.id,
|
|
403
|
+
data: resolvedData,
|
|
404
|
+
});
|
|
405
|
+
await trxContentRepo.setDraftRevision(collectionSlug, existing.id, draft.id);
|
|
406
|
+
await trxContentRepo.publish(collectionSlug, existing.id);
|
|
407
|
+
}
|
|
389
408
|
});
|
|
390
409
|
|
|
391
410
|
seedIdMap.set(entry.id, existing.id);
|
|
@@ -434,6 +453,13 @@ export async function applySeed(
|
|
|
434
453
|
await applyContentBylines(trxBylineRepo, collectionSlug, item.id, entry, seedBylineIdMap);
|
|
435
454
|
await applyContentTaxonomies(trx, collectionSlug, item.id, entry, false);
|
|
436
455
|
|
|
456
|
+
// Seed is declarative — when status is "published", promote to a live
|
|
457
|
+
// revision so the admin UI shows "Unpublish" instead of "Save & Publish"
|
|
458
|
+
// and `live_revision_id` is populated for downstream queries.
|
|
459
|
+
if (status === "published") {
|
|
460
|
+
await trxContentRepo.publish(collectionSlug, item.id);
|
|
461
|
+
}
|
|
462
|
+
|
|
437
463
|
return item;
|
|
438
464
|
});
|
|
439
465
|
|
|
@@ -792,7 +818,15 @@ async function applyContentTaxonomies(
|
|
|
792
818
|
.execute();
|
|
793
819
|
}
|
|
794
820
|
|
|
795
|
-
if (!entry.taxonomies)
|
|
821
|
+
if (!entry.taxonomies) {
|
|
822
|
+
// In update mode we may have just deleted rows above; invalidate so
|
|
823
|
+
// hydration doesn't serve stale "has terms" cached value.
|
|
824
|
+
if (isUpdate) {
|
|
825
|
+
const { invalidateTermCache } = await import("../taxonomies/index.js");
|
|
826
|
+
invalidateTermCache();
|
|
827
|
+
}
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
796
830
|
|
|
797
831
|
for (const [taxonomyName, termSlugs] of Object.entries(entry.taxonomies)) {
|
|
798
832
|
const termRepo = new TaxonomyRepository(db);
|
|
@@ -804,6 +838,12 @@ async function applyContentTaxonomies(
|
|
|
804
838
|
}
|
|
805
839
|
}
|
|
806
840
|
}
|
|
841
|
+
|
|
842
|
+
// Seed writes directly to content_taxonomies. Clear the cache so
|
|
843
|
+
// the worker lifetime cached "has any term assignments" probe
|
|
844
|
+
// re-runs on the next read.
|
|
845
|
+
const { invalidateTermCache } = await import("../taxonomies/index.js");
|
|
846
|
+
invalidateTermCache();
|
|
807
847
|
}
|
|
808
848
|
|
|
809
849
|
/**
|
package/src/settings/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { MediaRepository } from "../database/repositories/media.js";
|
|
|
11
11
|
import { OptionsRepository } from "../database/repositories/options.js";
|
|
12
12
|
import type { Database } from "../database/types.js";
|
|
13
13
|
import { getDb } from "../loader.js";
|
|
14
|
+
import { peekRequestCache, requestCached } from "../request-cache.js";
|
|
14
15
|
import type { Storage } from "../storage/types.js";
|
|
15
16
|
import type { SiteSettings, SiteSettingKey, MediaReference } from "./types.js";
|
|
16
17
|
|
|
@@ -75,8 +76,24 @@ async function resolveMediaReference(
|
|
|
75
76
|
export async function getSiteSetting<K extends SiteSettingKey>(
|
|
76
77
|
key: K,
|
|
77
78
|
): Promise<SiteSettings[K] | undefined> {
|
|
78
|
-
|
|
79
|
-
|
|
79
|
+
// If `getSiteSettings()` has already been called in this request,
|
|
80
|
+
// read from that (request-cached) batch rather than firing a second
|
|
81
|
+
// options-table query. Common layout: a Base template pulls the
|
|
82
|
+
// whole settings object up-front, then `EmDashHead` or a plugin
|
|
83
|
+
// asks for one key — no reason the singular call should round-trip
|
|
84
|
+
// again.
|
|
85
|
+
const primed = peekRequestCache<Partial<SiteSettings>>("siteSettings");
|
|
86
|
+
if (primed) {
|
|
87
|
+
const settings = await primed;
|
|
88
|
+
return settings[key];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Otherwise cache per-key. Templates that pull several settings
|
|
92
|
+
// independently still share the in-flight query for each one.
|
|
93
|
+
return requestCached(`siteSetting:${key}`, async () => {
|
|
94
|
+
const db = await getDb();
|
|
95
|
+
return getSiteSettingWithDb(key, db);
|
|
96
|
+
});
|
|
80
97
|
}
|
|
81
98
|
|
|
82
99
|
/**
|
|
@@ -124,9 +141,11 @@ export async function getSiteSettingWithDb<K extends SiteSettingKey>(
|
|
|
124
141
|
* console.log(settings.logo?.url); // "/_emdash/api/media/file/abc123"
|
|
125
142
|
* ```
|
|
126
143
|
*/
|
|
127
|
-
export
|
|
128
|
-
|
|
129
|
-
|
|
144
|
+
export function getSiteSettings(): Promise<Partial<SiteSettings>> {
|
|
145
|
+
return requestCached("siteSettings", async () => {
|
|
146
|
+
const db = await getDb();
|
|
147
|
+
return getSiteSettingsWithDb(db);
|
|
148
|
+
});
|
|
130
149
|
}
|
|
131
150
|
|
|
132
151
|
/**
|
package/src/taxonomies/index.ts
CHANGED
|
@@ -5,103 +5,125 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { getDb } from "../loader.js";
|
|
8
|
+
import { requestCached, setRequestCacheEntry } from "../request-cache.js";
|
|
9
|
+
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
10
|
+
import { isMissingTableError } from "../utils/db-errors.js";
|
|
8
11
|
import type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from "./types.js";
|
|
9
12
|
|
|
13
|
+
/**
|
|
14
|
+
* No-op — kept for API compatibility.
|
|
15
|
+
*
|
|
16
|
+
* Used to invalidate a worker-lifetime "has any term assignments?" probe.
|
|
17
|
+
* That probe added a query on every cold isolate to save one query on
|
|
18
|
+
* sites with zero term assignments (i.e. the wrong tradeoff), so we
|
|
19
|
+
* dropped it. The batch term join below returns an empty map for empty
|
|
20
|
+
* sites at the same cost as the probe, without the pre-check.
|
|
21
|
+
*/
|
|
22
|
+
export function invalidateTermCache(): void {
|
|
23
|
+
// Intentionally empty.
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
/**
|
|
11
27
|
* Get all taxonomy definitions
|
|
12
28
|
*/
|
|
13
29
|
export async function getTaxonomyDefs(): Promise<TaxonomyDef[]> {
|
|
14
|
-
|
|
30
|
+
return requestCached("taxonomy-defs:all", async () => {
|
|
31
|
+
const db = await getDb();
|
|
15
32
|
|
|
16
|
-
|
|
33
|
+
const rows = await db.selectFrom("_emdash_taxonomy_defs").selectAll().execute();
|
|
17
34
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
return rows.map((row) => ({
|
|
36
|
+
id: row.id,
|
|
37
|
+
name: row.name,
|
|
38
|
+
label: row.label,
|
|
39
|
+
labelSingular: row.label_singular ?? undefined,
|
|
40
|
+
hierarchical: row.hierarchical === 1,
|
|
41
|
+
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
42
|
+
}));
|
|
43
|
+
});
|
|
26
44
|
}
|
|
27
45
|
|
|
28
46
|
/**
|
|
29
47
|
* Get a single taxonomy definition by name
|
|
30
48
|
*/
|
|
31
49
|
export async function getTaxonomyDef(name: string): Promise<TaxonomyDef | null> {
|
|
32
|
-
|
|
50
|
+
return requestCached(`taxonomy-def:${name}`, async () => {
|
|
51
|
+
const db = await getDb();
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
53
|
+
const row = await db
|
|
54
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
55
|
+
.selectAll()
|
|
56
|
+
.where("name", "=", name)
|
|
57
|
+
.executeTakeFirst();
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
if (!row) return null;
|
|
41
60
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
61
|
+
return {
|
|
62
|
+
id: row.id,
|
|
63
|
+
name: row.name,
|
|
64
|
+
label: row.label,
|
|
65
|
+
labelSingular: row.label_singular ?? undefined,
|
|
66
|
+
hierarchical: row.hierarchical === 1,
|
|
67
|
+
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
68
|
+
};
|
|
69
|
+
});
|
|
50
70
|
}
|
|
51
71
|
|
|
52
72
|
/**
|
|
53
73
|
* Get all terms for a taxonomy (as tree for hierarchical, flat for tags)
|
|
54
74
|
*/
|
|
55
75
|
export async function getTaxonomyTerms(taxonomyName: string): Promise<TaxonomyTerm[]> {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
return requestCached(`taxonomy-terms:${taxonomyName}`, async () => {
|
|
77
|
+
const db = await getDb();
|
|
78
|
+
|
|
79
|
+
// Get taxonomy definition to check if hierarchical
|
|
80
|
+
const def = await getTaxonomyDef(taxonomyName);
|
|
81
|
+
if (!def) return [];
|
|
82
|
+
|
|
83
|
+
// Get all terms for this taxonomy
|
|
84
|
+
const rows = await db
|
|
85
|
+
.selectFrom("taxonomies")
|
|
86
|
+
.selectAll()
|
|
87
|
+
.where("name", "=", taxonomyName)
|
|
88
|
+
.orderBy("label", "asc")
|
|
89
|
+
.execute();
|
|
90
|
+
|
|
91
|
+
// Count entries for each term
|
|
92
|
+
const countsResult = await db
|
|
93
|
+
.selectFrom("content_taxonomies")
|
|
94
|
+
.select(["taxonomy_id"])
|
|
95
|
+
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
96
|
+
.groupBy("taxonomy_id")
|
|
97
|
+
.execute();
|
|
98
|
+
|
|
99
|
+
const counts = new Map<string, number>();
|
|
100
|
+
for (const row of countsResult) {
|
|
101
|
+
counts.set(row.taxonomy_id, row.count);
|
|
102
|
+
}
|
|
82
103
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
104
|
+
const flatTerms: TaxonomyTermRow[] = rows.map((row) => ({
|
|
105
|
+
id: row.id,
|
|
106
|
+
name: row.name,
|
|
107
|
+
slug: row.slug,
|
|
108
|
+
label: row.label,
|
|
109
|
+
parent_id: row.parent_id,
|
|
110
|
+
data: row.data,
|
|
111
|
+
}));
|
|
91
112
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
// If hierarchical, build tree. Otherwise return flat
|
|
114
|
+
if (def.hierarchical) {
|
|
115
|
+
return buildTree(flatTerms, counts);
|
|
116
|
+
}
|
|
96
117
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
return flatTerms.map((term) => ({
|
|
119
|
+
id: term.id,
|
|
120
|
+
name: term.name,
|
|
121
|
+
slug: term.slug,
|
|
122
|
+
label: term.label,
|
|
123
|
+
children: [],
|
|
124
|
+
count: counts.get(term.id) ?? 0,
|
|
125
|
+
}));
|
|
126
|
+
});
|
|
105
127
|
}
|
|
106
128
|
|
|
107
129
|
/**
|
|
@@ -160,34 +182,36 @@ export async function getTerm(taxonomyName: string, slug: string): Promise<Taxon
|
|
|
160
182
|
/**
|
|
161
183
|
* Get terms assigned to an entry
|
|
162
184
|
*/
|
|
163
|
-
export
|
|
185
|
+
export function getEntryTerms(
|
|
164
186
|
collection: string,
|
|
165
187
|
entryId: string,
|
|
166
188
|
taxonomyName?: string,
|
|
167
189
|
): Promise<TaxonomyTerm[]> {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
190
|
+
return requestCached(`terms:${collection}:${entryId}:${taxonomyName ?? "*"}`, async () => {
|
|
191
|
+
const db = await getDb();
|
|
192
|
+
|
|
193
|
+
let query = db
|
|
194
|
+
.selectFrom("content_taxonomies")
|
|
195
|
+
.innerJoin("taxonomies", "taxonomies.id", "content_taxonomies.taxonomy_id")
|
|
196
|
+
.selectAll("taxonomies")
|
|
197
|
+
.where("content_taxonomies.collection", "=", collection)
|
|
198
|
+
.where("content_taxonomies.entry_id", "=", entryId);
|
|
199
|
+
|
|
200
|
+
if (taxonomyName) {
|
|
201
|
+
query = query.where("taxonomies.name", "=", taxonomyName);
|
|
202
|
+
}
|
|
180
203
|
|
|
181
|
-
|
|
204
|
+
const rows = await query.execute();
|
|
182
205
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
206
|
+
return rows.map((row) => ({
|
|
207
|
+
id: row.id,
|
|
208
|
+
name: row.name,
|
|
209
|
+
slug: row.slug,
|
|
210
|
+
label: row.label,
|
|
211
|
+
parentId: row.parent_id ?? undefined,
|
|
212
|
+
children: [],
|
|
213
|
+
}));
|
|
214
|
+
});
|
|
191
215
|
}
|
|
192
216
|
|
|
193
217
|
/**
|
|
@@ -208,53 +232,229 @@ export async function getTermsForEntries(
|
|
|
208
232
|
): Promise<Map<string, TaxonomyTerm[]>> {
|
|
209
233
|
const result = new Map<string, TaxonomyTerm[]>();
|
|
210
234
|
|
|
211
|
-
// Initialize all entry IDs with empty arrays
|
|
212
|
-
|
|
235
|
+
// Initialize all entry IDs with empty arrays so callers can always
|
|
236
|
+
// expect the key to be present.
|
|
237
|
+
const uniqueIds = [...new Set(entryIds)];
|
|
238
|
+
for (const id of uniqueIds) {
|
|
213
239
|
result.set(id, []);
|
|
214
240
|
}
|
|
215
241
|
|
|
216
|
-
if (
|
|
242
|
+
if (uniqueIds.length === 0) {
|
|
217
243
|
return result;
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
const db = await getDb();
|
|
221
247
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
248
|
+
// Chunk the IN clause so we stay below D1's ~100 bound-parameter limit
|
|
249
|
+
// (and equivalent limits on other dialects). Matches getContentBylinesMany.
|
|
250
|
+
//
|
|
251
|
+
// Sites with no term assignments get back empty rows for one query —
|
|
252
|
+
// the previous "has any term assignments" probe spent a round-trip on
|
|
253
|
+
// every request to save that single query on empty sites, which is
|
|
254
|
+
// backwards. Pre-migration databases (content_taxonomies missing) fall
|
|
255
|
+
// through to the `isMissingTableError` catch and return empties.
|
|
256
|
+
for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
|
|
257
|
+
let rows;
|
|
258
|
+
try {
|
|
259
|
+
rows = await db
|
|
260
|
+
.selectFrom("content_taxonomies")
|
|
261
|
+
.innerJoin("taxonomies", "taxonomies.id", "content_taxonomies.taxonomy_id")
|
|
262
|
+
.select([
|
|
263
|
+
"content_taxonomies.entry_id",
|
|
264
|
+
"taxonomies.id",
|
|
265
|
+
"taxonomies.name",
|
|
266
|
+
"taxonomies.slug",
|
|
267
|
+
"taxonomies.label",
|
|
268
|
+
"taxonomies.parent_id",
|
|
269
|
+
])
|
|
270
|
+
.where("content_taxonomies.collection", "=", collection)
|
|
271
|
+
.where("content_taxonomies.entry_id", "in", chunk)
|
|
272
|
+
.where("taxonomies.name", "=", taxonomyName)
|
|
273
|
+
.execute();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
if (isMissingTableError(error)) return result;
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
237
278
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
279
|
+
for (const row of rows) {
|
|
280
|
+
const entryId = row.entry_id;
|
|
281
|
+
const term: TaxonomyTerm = {
|
|
282
|
+
id: row.id,
|
|
283
|
+
name: row.name,
|
|
284
|
+
slug: row.slug,
|
|
285
|
+
label: row.label,
|
|
286
|
+
parentId: row.parent_id ?? undefined,
|
|
287
|
+
children: [],
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const terms = result.get(entryId);
|
|
291
|
+
if (terms) {
|
|
292
|
+
terms.push(term);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return result;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Batch-fetch terms for multiple entries across ALL taxonomies in a single query.
|
|
302
|
+
*
|
|
303
|
+
* Returns a Map keyed by entry ID, where each value is a Record keyed by
|
|
304
|
+
* taxonomy name with the matching terms as an array. Used by
|
|
305
|
+
* getEmDashCollection to eagerly hydrate `entry.data.terms` and avoid
|
|
306
|
+
* the N+1 pattern that callers hit when they loop and call getEntryTerms.
|
|
307
|
+
*
|
|
308
|
+
* Pre-migration databases (content_taxonomies missing) return an empty
|
|
309
|
+
* Map — the join falls through to the `isMissingTableError` branch.
|
|
310
|
+
*/
|
|
311
|
+
export async function getAllTermsForEntries(
|
|
312
|
+
collection: string,
|
|
313
|
+
entryIds: string[],
|
|
314
|
+
): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {
|
|
315
|
+
const result = new Map<string, Record<string, TaxonomyTerm[]>>();
|
|
316
|
+
|
|
317
|
+
// Initialize unique entry IDs with empty objects so callers can always
|
|
318
|
+
// expect the key to be present. Deduping also reduces wasted bound
|
|
319
|
+
// parameters when a caller accidentally passes duplicates.
|
|
320
|
+
const uniqueIds = [...new Set(entryIds)];
|
|
321
|
+
for (const id of uniqueIds) {
|
|
322
|
+
result.set(id, {});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (uniqueIds.length === 0) {
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const db = await getDb();
|
|
248
330
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
331
|
+
// Look up which taxonomies apply to this collection. Used below to
|
|
332
|
+
// seed empty arrays for taxonomies the entry has no terms in — so
|
|
333
|
+
// callers (including the pre-populated getEntryTerms cache) get a
|
|
334
|
+
// deterministic `[]` back rather than a cache miss that triggers a DB
|
|
335
|
+
// round-trip just to confirm "no terms".
|
|
336
|
+
const applicableTaxonomyNames = await getCollectionTaxonomyNames(collection);
|
|
337
|
+
|
|
338
|
+
// Chunk the IN clause to stay below D1's ~100 bound-parameter limit
|
|
339
|
+
// (and equivalent limits on other dialects). Matches getContentBylinesMany.
|
|
340
|
+
//
|
|
341
|
+
// Previously we did a separate "has any assignments" probe to skip the
|
|
342
|
+
// join on empty sites. That traded one query per request for a query
|
|
343
|
+
// saved only on empty sites — backwards. Now the join runs directly
|
|
344
|
+
// (returning zero rows cheaply) and pre-migration databases are caught
|
|
345
|
+
// by the `isMissingTableError` branch below.
|
|
346
|
+
for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
|
|
347
|
+
let rows;
|
|
348
|
+
try {
|
|
349
|
+
rows = await db
|
|
350
|
+
.selectFrom("content_taxonomies")
|
|
351
|
+
.innerJoin("taxonomies", "taxonomies.id", "content_taxonomies.taxonomy_id")
|
|
352
|
+
.select([
|
|
353
|
+
"content_taxonomies.entry_id",
|
|
354
|
+
"taxonomies.id",
|
|
355
|
+
"taxonomies.name",
|
|
356
|
+
"taxonomies.slug",
|
|
357
|
+
"taxonomies.label",
|
|
358
|
+
"taxonomies.parent_id",
|
|
359
|
+
])
|
|
360
|
+
.where("content_taxonomies.collection", "=", collection)
|
|
361
|
+
.where("content_taxonomies.entry_id", "in", chunk)
|
|
362
|
+
.orderBy("taxonomies.label", "asc")
|
|
363
|
+
.execute();
|
|
364
|
+
} catch (error) {
|
|
365
|
+
if (isMissingTableError(error)) {
|
|
366
|
+
for (const id of uniqueIds) {
|
|
367
|
+
primeEntryTermsCache(collection, id, {}, applicableTaxonomyNames);
|
|
368
|
+
}
|
|
369
|
+
return result;
|
|
370
|
+
}
|
|
371
|
+
throw error;
|
|
252
372
|
}
|
|
373
|
+
|
|
374
|
+
for (const row of rows) {
|
|
375
|
+
const entryId = row.entry_id;
|
|
376
|
+
const term: TaxonomyTerm = {
|
|
377
|
+
id: row.id,
|
|
378
|
+
name: row.name,
|
|
379
|
+
slug: row.slug,
|
|
380
|
+
label: row.label,
|
|
381
|
+
parentId: row.parent_id ?? undefined,
|
|
382
|
+
children: [],
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
const byTaxonomy = result.get(entryId);
|
|
386
|
+
if (!byTaxonomy) continue;
|
|
387
|
+
const existing = byTaxonomy[row.name];
|
|
388
|
+
if (existing) {
|
|
389
|
+
existing.push(term);
|
|
390
|
+
} else {
|
|
391
|
+
byTaxonomy[row.name] = [term];
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Prime the request-scoped cache so legacy callers of getEntryTerms
|
|
397
|
+
// (which still work per-entry) hit the in-memory cache instead of
|
|
398
|
+
// re-querying. This is what gives us the N+1 win in existing templates
|
|
399
|
+
// without requiring them to be rewritten.
|
|
400
|
+
for (const [entryId, byTaxonomy] of result) {
|
|
401
|
+
primeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames);
|
|
253
402
|
}
|
|
254
403
|
|
|
255
404
|
return result;
|
|
256
405
|
}
|
|
257
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Return the list of taxonomy names applicable to a collection, request-
|
|
409
|
+
* cached so a page render only pays for it once.
|
|
410
|
+
*
|
|
411
|
+
* Returns an empty list when taxonomies haven't been defined yet.
|
|
412
|
+
*/
|
|
413
|
+
async function getCollectionTaxonomyNames(collection: string): Promise<string[]> {
|
|
414
|
+
try {
|
|
415
|
+
const defs = await getTaxonomyDefs();
|
|
416
|
+
return defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);
|
|
417
|
+
} catch (error) {
|
|
418
|
+
if (isMissingTableError(error)) return [];
|
|
419
|
+
throw error;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Pre-populate the request-cache for every getEntryTerms call-shape that
|
|
425
|
+
* could hit this entry:
|
|
426
|
+
*
|
|
427
|
+
* getEntryTerms(collection, entryId) -> key `terms:C:E:*`
|
|
428
|
+
* getEntryTerms(collection, entryId, "tag") -> key `terms:C:E:tag`
|
|
429
|
+
* getEntryTerms(collection, entryId, "category") -> key `terms:C:E:category`
|
|
430
|
+
* ...one per taxonomy that applies to this collection
|
|
431
|
+
*
|
|
432
|
+
* Taxonomies with no rows on this entry are seeded with `[]` so legacy
|
|
433
|
+
* callers short-circuit to the cached empty array instead of re-querying.
|
|
434
|
+
*/
|
|
435
|
+
function primeEntryTermsCache(
|
|
436
|
+
collection: string,
|
|
437
|
+
entryId: string,
|
|
438
|
+
byTaxonomy: Record<string, TaxonomyTerm[]>,
|
|
439
|
+
applicableTaxonomyNames: string[],
|
|
440
|
+
): void {
|
|
441
|
+
// Seed every applicable taxonomy with at least [] so
|
|
442
|
+
// getEntryTerms(collection, id, "tag") doesn't miss the cache when an
|
|
443
|
+
// entry has no tags.
|
|
444
|
+
for (const name of applicableTaxonomyNames) {
|
|
445
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:${name}`, byTaxonomy[name] ?? []);
|
|
446
|
+
}
|
|
447
|
+
// Also seed individual names that show up in data but aren't listed
|
|
448
|
+
// as applicable (e.g. taxonomy reassigned to a different collection
|
|
449
|
+
// since the terms were written).
|
|
450
|
+
for (const [name, terms] of Object.entries(byTaxonomy)) {
|
|
451
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:${name}`, terms);
|
|
452
|
+
}
|
|
453
|
+
// Flattened `*` view — all terms across all taxonomies in one array.
|
|
454
|
+
const allTerms = Object.values(byTaxonomy).flat();
|
|
455
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:*`, allTerms);
|
|
456
|
+
}
|
|
457
|
+
|
|
258
458
|
/**
|
|
259
459
|
* Get entries by term (wraps getEmDashCollection)
|
|
260
460
|
*/
|