emdash 0.9.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-DoNJiveC.d.mts → adapters-BktHA7EO.d.mts} +1 -1
- package/dist/{adapters-DoNJiveC.d.mts.map → adapters-BktHA7EO.d.mts.map} +1 -1
- package/dist/{apply-BzltprvY.mjs → apply-UsrFuO7l.mjs} +156 -254
- package/dist/apply-UsrFuO7l.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +10 -2
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +5 -5
- package/dist/astro/middleware/redirect.mjs +5 -5
- package/dist/astro/middleware/request-context.mjs +4 -4
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.mjs +35 -34
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{base64-BRICGH2l.mjs → base64-MBPo9ozB.mjs} +1 -1
- package/dist/{base64-BRICGH2l.mjs.map → base64-MBPo9ozB.mjs.map} +1 -1
- package/dist/{byline-BSaNL1w7.mjs → byline-C3vnhIpU.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-C3vnhIpU.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-esI7ioa9.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-esI7ioa9.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-fTzxgMFJ.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-fTzxgMFJ.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-Da2-b-oA.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-Da2-b-oA.mjs.map} +1 -1
- package/dist/cli/index.mjs +251 -79
- 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-BI0V3ICQ.mjs → config-CVssduLe.mjs} +1 -1
- package/dist/{config-BI0V3ICQ.mjs.map → config-CVssduLe.mjs.map} +1 -1
- package/dist/{content-8lOYF0pr.mjs → content-C7G4QXkK.mjs} +14 -3
- package/dist/content-C7G4QXkK.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +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-WRezodiz.mjs → db-errors-B7P2pSCn.mjs} +1 -1
- package/dist/{db-errors-WRezodiz.mjs.map → db-errors-B7P2pSCn.mjs.map} +1 -1
- package/dist/{default-D8ksjWhO.mjs → default-pHuz9WF6.mjs} +1 -1
- package/dist/{default-D8ksjWhO.mjs.map → default-pHuz9WF6.mjs.map} +1 -1
- package/dist/{error-D_-tqP-I.mjs → error-DqnRMM5z.mjs} +1 -1
- package/dist/{error-D_-tqP-I.mjs.map → error-DqnRMM5z.mjs.map} +1 -1
- package/dist/{index-BFRaVcD6.d.mts → index-DjPMOfO0.d.mts} +82 -67
- package/dist/index-DjPMOfO0.d.mts.map +1 -0
- package/dist/index.d.mts +10 -10
- package/dist/index.mjs +28 -27
- package/dist/{load-DDqMMvZL.mjs → load-sXRuM7Us.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-sXRuM7Us.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-Bx2_9-5e.mjs} +31 -6
- package/dist/loader-Bx2_9-5e.mjs.map +1 -0
- package/dist/{manifest-schema-DqWNC3lM.mjs → manifest-schema-CXAbd1vH.mjs} +1 -1
- package/dist/{manifest-schema-DqWNC3lM.mjs.map → manifest-schema-CXAbd1vH.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/media/local-runtime.mjs +3 -3
- package/dist/{media-BW32b4gi.mjs → media-D8FbNsl0.mjs} +2 -2
- package/dist/{media-BW32b4gi.mjs.map → media-D8FbNsl0.mjs.map} +1 -1
- package/dist/{mode-ier8jbBk.mjs → mode-YhqNVef_.mjs} +1 -1
- package/dist/{mode-ier8jbBk.mjs.map → mode-YhqNVef_.mjs.map} +1 -1
- package/dist/{options-BVp3UsTS.mjs → options-nPxWnrya.mjs} +1 -1
- package/dist/{options-BVp3UsTS.mjs.map → options-nPxWnrya.mjs.map} +1 -1
- 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-BE4o_2dc.d.mts → placeholder-CDPtkelt.d.mts} +1 -1
- package/dist/{placeholder-BE4o_2dc.d.mts.map → placeholder-CDPtkelt.d.mts.map} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs → placeholder-Ci0RLeCk.mjs} +1 -1
- package/dist/{placeholder-CIJejMlK.mjs.map → placeholder-Ci0RLeCk.mjs.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +2 -2
- package/dist/{public-url-DByxYjUw.mjs → public-url-B1AxbbbQ.mjs} +1 -1
- package/dist/{public-url-DByxYjUw.mjs.map → public-url-B1AxbbbQ.mjs.map} +1 -1
- package/dist/{query-Cg9ZKRQ0.mjs → query-Bo-msrmu.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-Bo-msrmu.mjs.map} +1 -1
- package/dist/{redirect-BhUBKRc1.mjs → redirect-C5H7VGIX.mjs} +3 -3
- package/dist/{redirect-BhUBKRc1.mjs.map → redirect-C5H7VGIX.mjs.map} +1 -1
- package/dist/{registry-Dw70ChxB.mjs → registry-Beb7wxFc.mjs} +5 -5
- package/dist/{registry-Dw70ChxB.mjs.map → registry-Beb7wxFc.mjs.map} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-C-tIpYIw.mjs} +1 -1
- package/dist/{request-cache-B-bmkipQ.mjs.map → request-cache-C-tIpYIw.mjs.map} +1 -1
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Clwe4Mme.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Clwe4Mme.d.mts.map} +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DMnlIkh4.mjs} +433 -138
- package/dist/runner-DMnlIkh4.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DkN-BqsS.mjs} +164 -92
- package/dist/search-DkN-BqsS.mjs.map +1 -0
- package/dist/{secrets-CW3reAnU.mjs → secrets-CZ8rxLX3.mjs} +3 -3
- package/dist/{secrets-CW3reAnU.mjs.map → secrets-CZ8rxLX3.mjs.map} +1 -1
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +15 -14
- 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-D7zMmWi2.mjs → tokens-CyRDPVW2.mjs} +2 -2
- package/dist/{tokens-D7zMmWi2.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-DNEfeMaU.d.mts → transport-DX_5rpsq.d.mts} +1 -1
- package/dist/{transport-DNEfeMaU.d.mts.map → transport-DX_5rpsq.d.mts.map} +1 -1
- package/dist/{transport-BeMCmin1.mjs → transport-xpzIjCIB.mjs} +1 -1
- package/dist/{transport-BeMCmin1.mjs.map → transport-xpzIjCIB.mjs.map} +1 -1
- package/dist/{types-CRxNbK-Z.mjs → types-BIgulNsW.mjs} +2 -2
- package/dist/{types-CRxNbK-Z.mjs.map → types-BIgulNsW.mjs.map} +1 -1
- package/dist/{types-CJsYGpco.d.mts → types-B_CXXnzh.d.mts} +1 -1
- package/dist/{types-CJsYGpco.d.mts.map → types-B_CXXnzh.d.mts.map} +1 -1
- package/dist/{types-M78DQ1lx.d.mts → types-C-aFbqmA.d.mts} +1 -1
- package/dist/{types-M78DQ1lx.d.mts.map → types-C-aFbqmA.d.mts.map} +1 -1
- package/dist/{types-4fVtCIm0.mjs → types-CoO6mpV3.mjs} +1 -1
- package/dist/{types-4fVtCIm0.mjs.map → types-CoO6mpV3.mjs.map} +1 -1
- package/dist/{types-BuBIptGk.d.mts → types-D19uBYWn.d.mts} +149 -4
- package/dist/types-D19uBYWn.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-Dl1fgFjn.d.mts} +1 -1
- package/dist/{types-BSyXeCFW.d.mts.map → types-Dl1fgFjn.d.mts.map} +1 -1
- package/dist/{types-CrtWgIvl.d.mts → types-Dtx1mSMX.d.mts} +9 -1
- package/dist/types-Dtx1mSMX.d.mts.map +1 -0
- package/dist/{types-CIOg5AR8.mjs → types-Eg829jj9.mjs} +1 -1
- package/dist/{types-CIOg5AR8.mjs.map → types-Eg829jj9.mjs.map} +1 -1
- package/dist/{types-CDbKp7ND.mjs → types-K-EkEQCI.mjs} +1 -1
- package/dist/{types-CDbKp7ND.mjs.map → types-K-EkEQCI.mjs.map} +1 -1
- package/dist/{validate-Baqf0slj.mjs → validate-CBIbxM3L.mjs} +14 -10
- package/dist/validate-CBIbxM3L.mjs.map +1 -0
- package/dist/{validate-BfQh_C_y.d.mts → validate-DHGwADqO.d.mts} +18 -5
- package/dist/validate-DHGwADqO.d.mts.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-B1NYiEos.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-B1NYiEos.mjs.map} +1 -1
- package/dist/version-CMD42IRC.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-CMD42IRC.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-BNJDQBSZ.mjs} +8 -3
- package/dist/zod-generator-BNJDQBSZ.mjs.map +1 -0
- package/package.json +6 -6
- package/src/api/handlers/content.ts +11 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/menus.ts +256 -75
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/routes/api/content/[collection]/[id]/permanent.ts +1 -1
- 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/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/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 -6
- package/src/cli/commands/export-seed.ts +82 -21
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +10 -2
- package/src/i18n/resolve.ts +37 -0
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +77 -18
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +140 -54
- package/src/seed/types.ts +14 -1
- package/src/seed/validate.ts +27 -13
- package/src/taxonomies/index.ts +230 -213
- package/src/taxonomies/types.ts +10 -0
- package/dist/apply-BzltprvY.mjs.map +0 -1
- package/dist/content-8lOYF0pr.mjs.map +0 -1
- package/dist/index-BFRaVcD6.d.mts.map +0 -1
- package/dist/loader-CKLbBnhK.mjs.map +0 -1
- package/dist/runner-C7ADox5q.mjs.map +0 -1
- package/dist/search-dOGEccMa.mjs.map +0 -1
- package/dist/taxonomies-ZlRtD6AG.mjs +0 -315
- package/dist/taxonomies-ZlRtD6AG.mjs.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CrtWgIvl.d.mts.map +0 -1
- package/dist/validate-Baqf0slj.mjs.map +0 -1
- package/dist/validate-BfQh_C_y.d.mts.map +0 -1
- package/dist/version-DoxrVdYf.mjs +0 -7
- package/dist/zod-generator-CC0xNe_K.mjs.map +0 -1
package/src/taxonomies/index.ts
CHANGED
|
@@ -1,115 +1,134 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime API for taxonomies
|
|
2
|
+
* Runtime API for taxonomies.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* All helpers are locale-aware. When a locale is not passed explicitly we fall
|
|
5
|
+
* back to the request context or the configured `defaultLocale` (see
|
|
6
|
+
* `i18n/resolve.ts`).
|
|
7
|
+
*
|
|
8
|
+
* Because `content_taxonomies.taxonomy_id` stores the translation_group (not a
|
|
9
|
+
* specific term id), the joins here are `taxonomies.translation_group =
|
|
10
|
+
* content_taxonomies.taxonomy_id` + filter by `taxonomies.locale`, which picks
|
|
11
|
+
* the right per-locale term.
|
|
5
12
|
*/
|
|
6
13
|
|
|
14
|
+
import { resolveLocale, resolveLocaleChain } from "../i18n/resolve.js";
|
|
7
15
|
import { getDb } from "../loader.js";
|
|
8
16
|
import { peekRequestCache, requestCached, setRequestCacheEntry } from "../request-cache.js";
|
|
9
17
|
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
10
18
|
import { isMissingTableError } from "../utils/db-errors.js";
|
|
11
19
|
import type { TaxonomyDef, TaxonomyTerm, TaxonomyTermRow } from "./types.js";
|
|
12
20
|
|
|
21
|
+
export interface TaxonomyQueryOptions {
|
|
22
|
+
locale?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/**
|
|
14
26
|
* 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
27
|
*/
|
|
22
28
|
export function invalidateTermCache(): void {
|
|
23
29
|
// Intentionally empty.
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
27
|
-
* Get
|
|
33
|
+
* Get every taxonomy definition. Definitions are per-locale (one row per
|
|
34
|
+
* locale inside the same translation_group) — by default we resolve to the
|
|
35
|
+
* active locale.
|
|
28
36
|
*/
|
|
29
|
-
export async function getTaxonomyDefs(): Promise<TaxonomyDef[]> {
|
|
30
|
-
|
|
37
|
+
export async function getTaxonomyDefs(options: TaxonomyQueryOptions = {}): Promise<TaxonomyDef[]> {
|
|
38
|
+
const locale = resolveLocale(options.locale);
|
|
39
|
+
return requestCached(`taxonomy-defs:${locale ?? "*"}`, async () => {
|
|
31
40
|
const db = await getDb();
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return rows.map(
|
|
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
|
-
}));
|
|
41
|
+
let query = db.selectFrom("_emdash_taxonomy_defs").selectAll();
|
|
42
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
43
|
+
const rows = await query.execute();
|
|
44
|
+
return rows.map(rowToTaxonomyDef);
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
/**
|
|
47
|
-
* Get a single taxonomy definition by name
|
|
49
|
+
* Get a single taxonomy definition by name. Uses the fallback chain so even
|
|
50
|
+
* if there is no translation for the active locale we still return something.
|
|
48
51
|
*
|
|
49
52
|
* If `getTaxonomyDefs()` has already loaded the full list in this request
|
|
50
53
|
* (which happens during entry-term hydration on every page that renders a
|
|
51
|
-
* collection),
|
|
52
|
-
* second
|
|
54
|
+
* collection), search the matching def in memory rather than running a
|
|
55
|
+
* second query against `_emdash_taxonomy_defs`.
|
|
53
56
|
*/
|
|
54
|
-
export async function getTaxonomyDef(
|
|
55
|
-
|
|
57
|
+
export async function getTaxonomyDef(
|
|
58
|
+
name: string,
|
|
59
|
+
options: TaxonomyQueryOptions = {},
|
|
60
|
+
): Promise<TaxonomyDef | null> {
|
|
61
|
+
const chain = resolveLocaleChain(options.locale);
|
|
62
|
+
const peekKey = `taxonomy-defs:${resolveLocale(options.locale) ?? "*"}`;
|
|
63
|
+
const allDefs = peekRequestCache<TaxonomyDef[]>(peekKey);
|
|
56
64
|
if (allDefs) {
|
|
57
|
-
|
|
65
|
+
const defs = await allDefs;
|
|
66
|
+
if (chain.length === 0) return defs.find((d) => d.name === name) ?? null;
|
|
67
|
+
for (const locale of chain) {
|
|
68
|
+
const found = defs.find((d) => d.name === name && d.locale === locale);
|
|
69
|
+
if (found) return found;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
58
72
|
}
|
|
59
73
|
|
|
60
|
-
return requestCached(`taxonomy-def:${name}`, async () => {
|
|
74
|
+
return requestCached(`taxonomy-def:${name}:${chain.join(",")}`, async () => {
|
|
61
75
|
const db = await getDb();
|
|
62
76
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
if (chain.length === 0) {
|
|
78
|
+
const row = await db
|
|
79
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
80
|
+
.selectAll()
|
|
81
|
+
.where("name", "=", name)
|
|
82
|
+
.orderBy("locale", "asc")
|
|
83
|
+
.executeTakeFirst();
|
|
84
|
+
return row ? rowToTaxonomyDef(row) : null;
|
|
85
|
+
}
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
87
|
+
for (const locale of chain) {
|
|
88
|
+
const row = await db
|
|
89
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
90
|
+
.selectAll()
|
|
91
|
+
.where("name", "=", name)
|
|
92
|
+
.where("locale", "=", locale)
|
|
93
|
+
.executeTakeFirst();
|
|
94
|
+
if (row) return rowToTaxonomyDef(row);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
79
97
|
});
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
/**
|
|
83
|
-
*
|
|
101
|
+
* All terms of a taxonomy in a specific locale (flat for non-hierarchical,
|
|
102
|
+
* tree for hierarchical).
|
|
84
103
|
*/
|
|
85
|
-
export async function getTaxonomyTerms(
|
|
86
|
-
|
|
104
|
+
export async function getTaxonomyTerms(
|
|
105
|
+
taxonomyName: string,
|
|
106
|
+
options: TaxonomyQueryOptions = {},
|
|
107
|
+
): Promise<TaxonomyTerm[]> {
|
|
108
|
+
const locale = resolveLocale(options.locale);
|
|
109
|
+
return requestCached(`taxonomy-terms:${taxonomyName}:${locale ?? "*"}`, async () => {
|
|
87
110
|
const db = await getDb();
|
|
88
111
|
|
|
89
|
-
|
|
90
|
-
const def = await getTaxonomyDef(taxonomyName);
|
|
112
|
+
const def = await getTaxonomyDef(taxonomyName, options);
|
|
91
113
|
if (!def) return [];
|
|
92
114
|
|
|
93
|
-
|
|
94
|
-
const rows = await db
|
|
115
|
+
let termsQuery = db
|
|
95
116
|
.selectFrom("taxonomies")
|
|
96
117
|
.selectAll()
|
|
97
118
|
.where("name", "=", taxonomyName)
|
|
98
|
-
.orderBy("label", "asc")
|
|
99
|
-
|
|
119
|
+
.orderBy("label", "asc");
|
|
120
|
+
if (locale !== undefined) termsQuery = termsQuery.where("locale", "=", locale);
|
|
121
|
+
const rows = await termsQuery.execute();
|
|
100
122
|
|
|
101
|
-
//
|
|
123
|
+
// Counts are keyed by translation_group (what the pivot stores).
|
|
102
124
|
const countsResult = await db
|
|
103
125
|
.selectFrom("content_taxonomies")
|
|
104
126
|
.select(["taxonomy_id"])
|
|
105
127
|
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
106
128
|
.groupBy("taxonomy_id")
|
|
107
129
|
.execute();
|
|
108
|
-
|
|
109
130
|
const counts = new Map<string, number>();
|
|
110
|
-
for (const row of countsResult)
|
|
111
|
-
counts.set(row.taxonomy_id, row.count);
|
|
112
|
-
}
|
|
131
|
+
for (const row of countsResult) counts.set(row.taxonomy_id, row.count);
|
|
113
132
|
|
|
114
133
|
const flatTerms: TaxonomyTermRow[] = rows.map((row) => ({
|
|
115
134
|
id: row.id,
|
|
@@ -118,12 +137,11 @@ export async function getTaxonomyTerms(taxonomyName: string): Promise<TaxonomyTe
|
|
|
118
137
|
label: row.label,
|
|
119
138
|
parent_id: row.parent_id,
|
|
120
139
|
data: row.data,
|
|
140
|
+
locale: row.locale,
|
|
141
|
+
translation_group: row.translation_group,
|
|
121
142
|
}));
|
|
122
143
|
|
|
123
|
-
|
|
124
|
-
if (def.hierarchical) {
|
|
125
|
-
return buildTree(flatTerms, counts);
|
|
126
|
-
}
|
|
144
|
+
if (def.hierarchical) return buildTree(flatTerms, counts);
|
|
127
145
|
|
|
128
146
|
return flatTerms.map((term) => ({
|
|
129
147
|
id: term.id,
|
|
@@ -131,50 +149,71 @@ export async function getTaxonomyTerms(taxonomyName: string): Promise<TaxonomyTe
|
|
|
131
149
|
slug: term.slug,
|
|
132
150
|
label: term.label,
|
|
133
151
|
children: [],
|
|
134
|
-
count: counts.get(term.id) ?? 0,
|
|
152
|
+
count: counts.get(term.translation_group ?? term.id) ?? 0,
|
|
153
|
+
locale: term.locale,
|
|
154
|
+
translationGroup: term.translation_group,
|
|
135
155
|
}));
|
|
136
156
|
});
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
/**
|
|
140
|
-
* Get a single term by taxonomy
|
|
160
|
+
* Get a single term by (taxonomy, slug). Honours the fallback chain — if the
|
|
161
|
+
* slug exists in a fallback locale, we return that row (useful for deep-linking
|
|
162
|
+
* to a term page when the translation is missing).
|
|
141
163
|
*/
|
|
142
|
-
export async function getTerm(
|
|
164
|
+
export async function getTerm(
|
|
165
|
+
taxonomyName: string,
|
|
166
|
+
slug: string,
|
|
167
|
+
options: TaxonomyQueryOptions = {},
|
|
168
|
+
): Promise<TaxonomyTerm | null> {
|
|
143
169
|
const db = await getDb();
|
|
170
|
+
const chain = resolveLocaleChain(options.locale);
|
|
144
171
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
172
|
+
let row: Awaited<ReturnType<ReturnType<typeof selectTerm>["executeTakeFirst"]>>;
|
|
173
|
+
const selectTerm = () =>
|
|
174
|
+
db
|
|
175
|
+
.selectFrom("taxonomies")
|
|
176
|
+
.selectAll()
|
|
177
|
+
.where("name", "=", taxonomyName)
|
|
178
|
+
.where("slug", "=", slug);
|
|
179
|
+
|
|
180
|
+
if (chain.length === 0) {
|
|
181
|
+
row = await selectTerm().orderBy("locale", "asc").executeTakeFirst();
|
|
182
|
+
} else {
|
|
183
|
+
row = undefined;
|
|
184
|
+
for (const locale of chain) {
|
|
185
|
+
row = await selectTerm().where("locale", "=", locale).executeTakeFirst();
|
|
186
|
+
if (row) break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
151
189
|
|
|
152
190
|
if (!row) return null;
|
|
153
191
|
|
|
154
|
-
// Get entry count
|
|
155
192
|
const countResult = await db
|
|
156
193
|
.selectFrom("content_taxonomies")
|
|
157
194
|
.select((eb) => eb.fn.count<number>("entry_id").as("count"))
|
|
158
|
-
.where("taxonomy_id", "=", row.id)
|
|
195
|
+
.where("taxonomy_id", "=", row.translation_group ?? row.id)
|
|
159
196
|
.executeTakeFirst();
|
|
160
|
-
|
|
161
197
|
const count = countResult?.count ?? 0;
|
|
162
198
|
|
|
163
|
-
|
|
164
|
-
const childRows = await db
|
|
199
|
+
let childrenQuery = db
|
|
165
200
|
.selectFrom("taxonomies")
|
|
166
201
|
.selectAll()
|
|
167
202
|
.where("parent_id", "=", row.id)
|
|
168
|
-
.orderBy("label", "asc")
|
|
169
|
-
|
|
203
|
+
.orderBy("label", "asc");
|
|
204
|
+
const termLocale = row.locale;
|
|
205
|
+
if (termLocale) childrenQuery = childrenQuery.where("locale", "=", termLocale);
|
|
206
|
+
const childRows = await childrenQuery.execute();
|
|
170
207
|
|
|
171
|
-
const children = childRows.map((child) => ({
|
|
208
|
+
const children = childRows.map<TaxonomyTerm>((child) => ({
|
|
172
209
|
id: child.id,
|
|
173
210
|
name: child.name,
|
|
174
211
|
slug: child.slug,
|
|
175
212
|
label: child.label,
|
|
176
213
|
parentId: child.parent_id ?? undefined,
|
|
177
214
|
children: [],
|
|
215
|
+
locale: child.locale,
|
|
216
|
+
translationGroup: child.translation_group,
|
|
178
217
|
}));
|
|
179
218
|
|
|
180
219
|
return {
|
|
@@ -186,89 +225,75 @@ export async function getTerm(taxonomyName: string, slug: string): Promise<Taxon
|
|
|
186
225
|
description: row.data ? JSON.parse(row.data).description : undefined,
|
|
187
226
|
children,
|
|
188
227
|
count,
|
|
228
|
+
locale: row.locale,
|
|
229
|
+
translationGroup: row.translation_group,
|
|
189
230
|
};
|
|
190
231
|
}
|
|
191
232
|
|
|
192
233
|
/**
|
|
193
|
-
*
|
|
234
|
+
* Terms assigned to a content entry, resolved into the active locale. Terms
|
|
235
|
+
* whose translation_group lacks a row in the requested locale are omitted.
|
|
194
236
|
*/
|
|
195
237
|
export function getEntryTerms(
|
|
196
238
|
collection: string,
|
|
197
239
|
entryId: string,
|
|
198
240
|
taxonomyName?: string,
|
|
241
|
+
options: TaxonomyQueryOptions = {},
|
|
199
242
|
): Promise<TaxonomyTerm[]> {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
.innerJoin("taxonomies", "taxonomies.id", "content_taxonomies.taxonomy_id")
|
|
206
|
-
.selectAll("taxonomies")
|
|
207
|
-
.where("content_taxonomies.collection", "=", collection)
|
|
208
|
-
.where("content_taxonomies.entry_id", "=", entryId);
|
|
243
|
+
const locale = resolveLocale(options.locale);
|
|
244
|
+
return requestCached(
|
|
245
|
+
`terms:${collection}:${entryId}:${taxonomyName ?? "*"}:${locale ?? "*"}`,
|
|
246
|
+
async () => {
|
|
247
|
+
const db = await getDb();
|
|
209
248
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
let query = db
|
|
250
|
+
.selectFrom("content_taxonomies")
|
|
251
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
252
|
+
.selectAll("taxonomies")
|
|
253
|
+
.where("content_taxonomies.collection", "=", collection)
|
|
254
|
+
.where("content_taxonomies.entry_id", "=", entryId);
|
|
213
255
|
|
|
214
|
-
|
|
256
|
+
if (taxonomyName) query = query.where("taxonomies.name", "=", taxonomyName);
|
|
257
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
215
258
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
259
|
+
const rows = await query.execute();
|
|
260
|
+
return rows.map<TaxonomyTerm>((row) => ({
|
|
261
|
+
id: row.id,
|
|
262
|
+
name: row.name,
|
|
263
|
+
slug: row.slug,
|
|
264
|
+
label: row.label,
|
|
265
|
+
parentId: row.parent_id ?? undefined,
|
|
266
|
+
children: [],
|
|
267
|
+
locale: row.locale,
|
|
268
|
+
translationGroup: row.translation_group,
|
|
269
|
+
}));
|
|
270
|
+
},
|
|
271
|
+
);
|
|
225
272
|
}
|
|
226
273
|
|
|
227
274
|
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* This is more efficient than calling getEntryTerms for each entry
|
|
231
|
-
* when you need terms for a list of entries.
|
|
232
|
-
*
|
|
233
|
-
* @param collection - The collection type (e.g., "posts")
|
|
234
|
-
* @param entryIds - Array of entry IDs
|
|
235
|
-
* @param taxonomyName - The taxonomy name (e.g., "categories")
|
|
236
|
-
* @returns Map from entry ID to array of terms
|
|
275
|
+
* Terms for multiple entries of one taxonomy, single query.
|
|
237
276
|
*/
|
|
238
277
|
export async function getTermsForEntries(
|
|
239
278
|
collection: string,
|
|
240
279
|
entryIds: string[],
|
|
241
280
|
taxonomyName: string,
|
|
281
|
+
options: TaxonomyQueryOptions = {},
|
|
242
282
|
): Promise<Map<string, TaxonomyTerm[]>> {
|
|
243
283
|
const result = new Map<string, TaxonomyTerm[]>();
|
|
244
|
-
|
|
245
|
-
// Initialize all entry IDs with empty arrays so callers can always
|
|
246
|
-
// expect the key to be present.
|
|
247
284
|
const uniqueIds = [...new Set(entryIds)];
|
|
248
|
-
for (const id of uniqueIds)
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (uniqueIds.length === 0) {
|
|
253
|
-
return result;
|
|
254
|
-
}
|
|
285
|
+
for (const id of uniqueIds) result.set(id, []);
|
|
286
|
+
if (uniqueIds.length === 0) return result;
|
|
255
287
|
|
|
256
288
|
const db = await getDb();
|
|
289
|
+
const locale = resolveLocale(options.locale);
|
|
257
290
|
|
|
258
|
-
// Chunk the IN clause so we stay below D1's ~100 bound-parameter limit
|
|
259
|
-
// (and equivalent limits on other dialects). Matches getContentBylinesMany.
|
|
260
|
-
//
|
|
261
|
-
// Sites with no term assignments get back empty rows for one query —
|
|
262
|
-
// the previous "has any term assignments" probe spent a round-trip on
|
|
263
|
-
// every request to save that single query on empty sites, which is
|
|
264
|
-
// backwards. Pre-migration databases (content_taxonomies missing) fall
|
|
265
|
-
// through to the `isMissingTableError` catch and return empties.
|
|
266
291
|
for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
|
|
267
292
|
let rows;
|
|
268
293
|
try {
|
|
269
|
-
|
|
294
|
+
let query = db
|
|
270
295
|
.selectFrom("content_taxonomies")
|
|
271
|
-
.innerJoin("taxonomies", "taxonomies.
|
|
296
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
272
297
|
.select([
|
|
273
298
|
"content_taxonomies.entry_id",
|
|
274
299
|
"taxonomies.id",
|
|
@@ -276,18 +301,20 @@ export async function getTermsForEntries(
|
|
|
276
301
|
"taxonomies.slug",
|
|
277
302
|
"taxonomies.label",
|
|
278
303
|
"taxonomies.parent_id",
|
|
304
|
+
"taxonomies.locale",
|
|
305
|
+
"taxonomies.translation_group",
|
|
279
306
|
])
|
|
280
307
|
.where("content_taxonomies.collection", "=", collection)
|
|
281
308
|
.where("content_taxonomies.entry_id", "in", chunk)
|
|
282
|
-
.where("taxonomies.name", "=", taxonomyName)
|
|
283
|
-
|
|
309
|
+
.where("taxonomies.name", "=", taxonomyName);
|
|
310
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
311
|
+
rows = await query.execute();
|
|
284
312
|
} catch (error) {
|
|
285
313
|
if (isMissingTableError(error)) return result;
|
|
286
314
|
throw error;
|
|
287
315
|
}
|
|
288
316
|
|
|
289
317
|
for (const row of rows) {
|
|
290
|
-
const entryId = row.entry_id;
|
|
291
318
|
const term: TaxonomyTerm = {
|
|
292
319
|
id: row.id,
|
|
293
320
|
name: row.name,
|
|
@@ -295,12 +322,11 @@ export async function getTermsForEntries(
|
|
|
295
322
|
label: row.label,
|
|
296
323
|
parentId: row.parent_id ?? undefined,
|
|
297
324
|
children: [],
|
|
325
|
+
locale: row.locale,
|
|
326
|
+
translationGroup: row.translation_group,
|
|
298
327
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (terms) {
|
|
302
|
-
terms.push(term);
|
|
303
|
-
}
|
|
328
|
+
const terms = result.get(row.entry_id);
|
|
329
|
+
if (terms) terms.push(term);
|
|
304
330
|
}
|
|
305
331
|
}
|
|
306
332
|
|
|
@@ -308,57 +334,29 @@ export async function getTermsForEntries(
|
|
|
308
334
|
}
|
|
309
335
|
|
|
310
336
|
/**
|
|
311
|
-
* Batch-fetch terms for multiple entries across ALL taxonomies in
|
|
312
|
-
*
|
|
313
|
-
* Returns a Map keyed by entry ID, where each value is a Record keyed by
|
|
314
|
-
* taxonomy name with the matching terms as an array. Used by
|
|
315
|
-
* getEmDashCollection to eagerly hydrate `entry.data.terms` and avoid
|
|
316
|
-
* the N+1 pattern that callers hit when they loop and call getEntryTerms.
|
|
317
|
-
*
|
|
318
|
-
* Pre-migration databases (content_taxonomies missing) return an empty
|
|
319
|
-
* Map — the join falls through to the `isMissingTableError` branch.
|
|
337
|
+
* Batch-fetch terms for multiple entries across ALL taxonomies in one query.
|
|
338
|
+
* Primes the request-cache for subsequent per-entry calls to `getEntryTerms`.
|
|
320
339
|
*/
|
|
321
340
|
export async function getAllTermsForEntries(
|
|
322
341
|
collection: string,
|
|
323
342
|
entryIds: string[],
|
|
343
|
+
options: TaxonomyQueryOptions = {},
|
|
324
344
|
): Promise<Map<string, Record<string, TaxonomyTerm[]>>> {
|
|
325
345
|
const result = new Map<string, Record<string, TaxonomyTerm[]>>();
|
|
326
|
-
|
|
327
|
-
// Initialize unique entry IDs with empty objects so callers can always
|
|
328
|
-
// expect the key to be present. Deduping also reduces wasted bound
|
|
329
|
-
// parameters when a caller accidentally passes duplicates.
|
|
330
346
|
const uniqueIds = [...new Set(entryIds)];
|
|
331
|
-
for (const id of uniqueIds) {
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (uniqueIds.length === 0) {
|
|
336
|
-
return result;
|
|
337
|
-
}
|
|
347
|
+
for (const id of uniqueIds) result.set(id, {});
|
|
348
|
+
if (uniqueIds.length === 0) return result;
|
|
338
349
|
|
|
339
350
|
const db = await getDb();
|
|
351
|
+
const locale = resolveLocale(options.locale);
|
|
352
|
+
const applicableTaxonomyNames = await getCollectionTaxonomyNames(collection, { locale });
|
|
340
353
|
|
|
341
|
-
// Look up which taxonomies apply to this collection. Used below to
|
|
342
|
-
// seed empty arrays for taxonomies the entry has no terms in — so
|
|
343
|
-
// callers (including the pre-populated getEntryTerms cache) get a
|
|
344
|
-
// deterministic `[]` back rather than a cache miss that triggers a DB
|
|
345
|
-
// round-trip just to confirm "no terms".
|
|
346
|
-
const applicableTaxonomyNames = await getCollectionTaxonomyNames(collection);
|
|
347
|
-
|
|
348
|
-
// Chunk the IN clause to stay below D1's ~100 bound-parameter limit
|
|
349
|
-
// (and equivalent limits on other dialects). Matches getContentBylinesMany.
|
|
350
|
-
//
|
|
351
|
-
// Previously we did a separate "has any assignments" probe to skip the
|
|
352
|
-
// join on empty sites. That traded one query per request for a query
|
|
353
|
-
// saved only on empty sites — backwards. Now the join runs directly
|
|
354
|
-
// (returning zero rows cheaply) and pre-migration databases are caught
|
|
355
|
-
// by the `isMissingTableError` branch below.
|
|
356
354
|
for (const chunk of chunks(uniqueIds, SQL_BATCH_SIZE)) {
|
|
357
355
|
let rows;
|
|
358
356
|
try {
|
|
359
|
-
|
|
357
|
+
let query = db
|
|
360
358
|
.selectFrom("content_taxonomies")
|
|
361
|
-
.innerJoin("taxonomies", "taxonomies.
|
|
359
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
362
360
|
.select([
|
|
363
361
|
"content_taxonomies.entry_id",
|
|
364
362
|
"taxonomies.id",
|
|
@@ -366,15 +364,18 @@ export async function getAllTermsForEntries(
|
|
|
366
364
|
"taxonomies.slug",
|
|
367
365
|
"taxonomies.label",
|
|
368
366
|
"taxonomies.parent_id",
|
|
367
|
+
"taxonomies.locale",
|
|
368
|
+
"taxonomies.translation_group",
|
|
369
369
|
])
|
|
370
370
|
.where("content_taxonomies.collection", "=", collection)
|
|
371
371
|
.where("content_taxonomies.entry_id", "in", chunk)
|
|
372
|
-
.orderBy("taxonomies.label", "asc")
|
|
373
|
-
|
|
372
|
+
.orderBy("taxonomies.label", "asc");
|
|
373
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
374
|
+
rows = await query.execute();
|
|
374
375
|
} catch (error) {
|
|
375
376
|
if (isMissingTableError(error)) {
|
|
376
377
|
for (const id of uniqueIds) {
|
|
377
|
-
primeEntryTermsCache(collection, id, {}, applicableTaxonomyNames);
|
|
378
|
+
primeEntryTermsCache(collection, id, {}, applicableTaxonomyNames, locale);
|
|
378
379
|
}
|
|
379
380
|
return result;
|
|
380
381
|
}
|
|
@@ -382,7 +383,6 @@ export async function getAllTermsForEntries(
|
|
|
382
383
|
}
|
|
383
384
|
|
|
384
385
|
for (const row of rows) {
|
|
385
|
-
const entryId = row.entry_id;
|
|
386
386
|
const term: TaxonomyTerm = {
|
|
387
387
|
id: row.id,
|
|
388
388
|
name: row.name,
|
|
@@ -390,25 +390,19 @@ export async function getAllTermsForEntries(
|
|
|
390
390
|
label: row.label,
|
|
391
391
|
parentId: row.parent_id ?? undefined,
|
|
392
392
|
children: [],
|
|
393
|
+
locale: row.locale,
|
|
394
|
+
translationGroup: row.translation_group,
|
|
393
395
|
};
|
|
394
|
-
|
|
395
|
-
const byTaxonomy = result.get(entryId);
|
|
396
|
+
const byTaxonomy = result.get(row.entry_id);
|
|
396
397
|
if (!byTaxonomy) continue;
|
|
397
398
|
const existing = byTaxonomy[row.name];
|
|
398
|
-
if (existing)
|
|
399
|
-
|
|
400
|
-
} else {
|
|
401
|
-
byTaxonomy[row.name] = [term];
|
|
402
|
-
}
|
|
399
|
+
if (existing) existing.push(term);
|
|
400
|
+
else byTaxonomy[row.name] = [term];
|
|
403
401
|
}
|
|
404
402
|
}
|
|
405
403
|
|
|
406
|
-
// Prime the request-scoped cache so legacy callers of getEntryTerms
|
|
407
|
-
// (which still work per-entry) hit the in-memory cache instead of
|
|
408
|
-
// re-querying. This is what gives us the N+1 win in existing templates
|
|
409
|
-
// without requiring them to be rewritten.
|
|
410
404
|
for (const [entryId, byTaxonomy] of result) {
|
|
411
|
-
primeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames);
|
|
405
|
+
primeEntryTermsCache(collection, entryId, byTaxonomy, applicableTaxonomyNames, locale);
|
|
412
406
|
}
|
|
413
407
|
|
|
414
408
|
return result;
|
|
@@ -420,9 +414,12 @@ export async function getAllTermsForEntries(
|
|
|
420
414
|
*
|
|
421
415
|
* Returns an empty list when taxonomies haven't been defined yet.
|
|
422
416
|
*/
|
|
423
|
-
async function getCollectionTaxonomyNames(
|
|
417
|
+
async function getCollectionTaxonomyNames(
|
|
418
|
+
collection: string,
|
|
419
|
+
options: TaxonomyQueryOptions,
|
|
420
|
+
): Promise<string[]> {
|
|
424
421
|
try {
|
|
425
|
-
const defs = await getTaxonomyDefs();
|
|
422
|
+
const defs = await getTaxonomyDefs(options);
|
|
426
423
|
return defs.filter((d) => d.collections.includes(collection)).map((d) => d.name);
|
|
427
424
|
} catch (error) {
|
|
428
425
|
if (isMissingTableError(error)) return [];
|
|
@@ -447,44 +444,64 @@ function primeEntryTermsCache(
|
|
|
447
444
|
entryId: string,
|
|
448
445
|
byTaxonomy: Record<string, TaxonomyTerm[]>,
|
|
449
446
|
applicableTaxonomyNames: string[],
|
|
447
|
+
locale: string | undefined,
|
|
450
448
|
): void {
|
|
451
|
-
|
|
452
|
-
// getEntryTerms(collection, id, "tag") doesn't miss the cache when an
|
|
453
|
-
// entry has no tags.
|
|
449
|
+
const localeKey = locale ?? "*";
|
|
454
450
|
for (const name of applicableTaxonomyNames) {
|
|
455
|
-
setRequestCacheEntry(
|
|
451
|
+
setRequestCacheEntry(
|
|
452
|
+
`terms:${collection}:${entryId}:${name}:${localeKey}`,
|
|
453
|
+
byTaxonomy[name] ?? [],
|
|
454
|
+
);
|
|
456
455
|
}
|
|
457
|
-
// Also seed individual names that show up in data but aren't listed
|
|
458
|
-
// as applicable (e.g. taxonomy reassigned to a different collection
|
|
459
|
-
// since the terms were written).
|
|
460
456
|
for (const [name, terms] of Object.entries(byTaxonomy)) {
|
|
461
|
-
setRequestCacheEntry(`terms:${collection}:${entryId}:${name}`, terms);
|
|
457
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:${name}:${localeKey}`, terms);
|
|
462
458
|
}
|
|
463
|
-
// Flattened `*` view — all terms across all taxonomies in one array.
|
|
464
459
|
const allTerms = Object.values(byTaxonomy).flat();
|
|
465
|
-
setRequestCacheEntry(`terms:${collection}:${entryId}
|
|
460
|
+
setRequestCacheEntry(`terms:${collection}:${entryId}:*:${localeKey}`, allTerms);
|
|
466
461
|
}
|
|
467
462
|
|
|
468
463
|
/**
|
|
469
|
-
* Get entries by term (
|
|
464
|
+
* Get entries by term. Both the lookup (term slug in the active locale) and
|
|
465
|
+
* the content query respect the active locale.
|
|
470
466
|
*/
|
|
471
467
|
export async function getEntriesByTerm(
|
|
472
468
|
collection: string,
|
|
473
469
|
taxonomyName: string,
|
|
474
470
|
termSlug: string,
|
|
471
|
+
options: TaxonomyQueryOptions = {},
|
|
475
472
|
): Promise<Array<{ id: string; data: Record<string, unknown> }>> {
|
|
476
473
|
const { getEmDashCollection } = await import("../query.js");
|
|
477
474
|
|
|
478
|
-
|
|
479
|
-
// a generic options object with `where` for filtering by taxonomy
|
|
480
|
-
const options: Record<string, unknown> = {
|
|
475
|
+
const queryOptions: Record<string, unknown> = {
|
|
481
476
|
where: { [taxonomyName]: termSlug },
|
|
482
477
|
};
|
|
483
|
-
|
|
484
|
-
|
|
478
|
+
if (options.locale !== undefined) queryOptions.locale = options.locale;
|
|
479
|
+
const { entries } = await getEmDashCollection(collection, queryOptions);
|
|
485
480
|
return entries;
|
|
486
481
|
}
|
|
487
482
|
|
|
483
|
+
function rowToTaxonomyDef(row: {
|
|
484
|
+
id: string;
|
|
485
|
+
name: string;
|
|
486
|
+
label: string;
|
|
487
|
+
label_singular: string | null;
|
|
488
|
+
hierarchical: number;
|
|
489
|
+
collections: string | null;
|
|
490
|
+
locale: string;
|
|
491
|
+
translation_group: string | null;
|
|
492
|
+
}): TaxonomyDef {
|
|
493
|
+
return {
|
|
494
|
+
id: row.id,
|
|
495
|
+
name: row.name,
|
|
496
|
+
label: row.label,
|
|
497
|
+
labelSingular: row.label_singular ?? undefined,
|
|
498
|
+
hierarchical: row.hierarchical === 1,
|
|
499
|
+
collections: row.collections ? JSON.parse(row.collections) : [],
|
|
500
|
+
locale: row.locale,
|
|
501
|
+
translationGroup: row.translation_group,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
488
505
|
/**
|
|
489
506
|
* Build tree structure from flat terms
|
|
490
507
|
*/
|
|
@@ -492,7 +509,6 @@ function buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): T
|
|
|
492
509
|
const map = new Map<string, TaxonomyTerm>();
|
|
493
510
|
const roots: TaxonomyTerm[] = [];
|
|
494
511
|
|
|
495
|
-
// First pass: create nodes
|
|
496
512
|
for (const term of flatTerms) {
|
|
497
513
|
map.set(term.id, {
|
|
498
514
|
id: term.id,
|
|
@@ -502,11 +518,12 @@ function buildTree(flatTerms: TaxonomyTermRow[], counts: Map<string, number>): T
|
|
|
502
518
|
parentId: term.parent_id ?? undefined,
|
|
503
519
|
description: term.data ? JSON.parse(term.data).description : undefined,
|
|
504
520
|
children: [],
|
|
505
|
-
count: counts.get(term.id) ?? 0,
|
|
521
|
+
count: counts.get(term.translation_group ?? term.id) ?? 0,
|
|
522
|
+
locale: term.locale,
|
|
523
|
+
translationGroup: term.translation_group,
|
|
506
524
|
});
|
|
507
525
|
}
|
|
508
526
|
|
|
509
|
-
// Second pass: build tree
|
|
510
527
|
for (const term of map.values()) {
|
|
511
528
|
if (term.parentId && map.has(term.parentId)) {
|
|
512
529
|
map.get(term.parentId)!.children.push(term);
|