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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Kysely } from "kysely";
|
|
1
|
+
import type { Kysely, Selectable } from "kysely";
|
|
2
2
|
import { ulid } from "ulidx";
|
|
3
3
|
|
|
4
4
|
import type { Database, TaxonomyTable, ContentTaxonomyTable } from "../types.js";
|
|
@@ -10,6 +10,8 @@ export interface Taxonomy {
|
|
|
10
10
|
label: string;
|
|
11
11
|
parentId: string | null;
|
|
12
12
|
data: Record<string, unknown> | null;
|
|
13
|
+
locale: string;
|
|
14
|
+
translationGroup: string | null;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface CreateTaxonomyInput {
|
|
@@ -18,6 +20,11 @@ export interface CreateTaxonomyInput {
|
|
|
18
20
|
label: string;
|
|
19
21
|
parentId?: string;
|
|
20
22
|
data?: Record<string, unknown>;
|
|
23
|
+
/** Omit to let the DB default (current value: 'en') apply. Higher layers
|
|
24
|
+
* resolve the locale from the request context / i18n config. */
|
|
25
|
+
locale?: string;
|
|
26
|
+
/** When set, links the new term into the source term's translation_group. */
|
|
27
|
+
translationOf?: string;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
export interface UpdateTaxonomyInput {
|
|
@@ -27,16 +34,29 @@ export interface UpdateTaxonomyInput {
|
|
|
27
34
|
data?: Record<string, unknown>;
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
export interface FindOptions {
|
|
38
|
+
parentId?: string | null;
|
|
39
|
+
locale?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
/**
|
|
31
|
-
* Taxonomy repository for categories, tags, and other classification
|
|
43
|
+
* Taxonomy repository for categories, tags, and other classification.
|
|
32
44
|
*
|
|
33
|
-
*
|
|
45
|
+
* Terms are per-locale. Translations of the same term share a `translation_group`
|
|
46
|
+
* ULID. `content_taxonomies.taxonomy_id` stores the translation_group so a single
|
|
47
|
+
* association spans every locale of a post.
|
|
48
|
+
*
|
|
49
|
+
* The repository does not resolve locale fallbacks on its own — callers supply
|
|
50
|
+
* the locale they want. Runtime helpers and handlers use `getFallbackChain()`
|
|
51
|
+
* from `i18n/config` when they need fallback behaviour.
|
|
34
52
|
*/
|
|
35
53
|
export class TaxonomyRepository {
|
|
36
54
|
constructor(private db: Kysely<Database>) {}
|
|
37
55
|
|
|
38
56
|
/**
|
|
39
|
-
* Create a new taxonomy term
|
|
57
|
+
* Create a new taxonomy term. When `translationOf` is set the new row joins
|
|
58
|
+
* the source term's translation_group; otherwise a fresh group is minted
|
|
59
|
+
* (matching the migration backfill pattern `translation_group = id`).
|
|
40
60
|
*/
|
|
41
61
|
async create(input: CreateTaxonomyInput): Promise<Taxonomy> {
|
|
42
62
|
const id = ulid();
|
|
@@ -44,58 +64,68 @@ export class TaxonomyRepository {
|
|
|
44
64
|
// Empty-string parentId is coerced to null defensively. Higher layers
|
|
45
65
|
// also normalize this — see handleTermCreate / handleTermUpdate.
|
|
46
66
|
const parentId = input.parentId === undefined || input.parentId === "" ? null : input.parentId;
|
|
47
|
-
const row: TaxonomyTable = {
|
|
48
|
-
id,
|
|
49
|
-
name: input.name,
|
|
50
|
-
slug: input.slug,
|
|
51
|
-
label: input.label,
|
|
52
|
-
parent_id: parentId,
|
|
53
|
-
data: input.data ? JSON.stringify(input.data) : null,
|
|
54
|
-
};
|
|
55
67
|
|
|
56
|
-
|
|
68
|
+
let translationGroup = id;
|
|
69
|
+
if (input.translationOf) {
|
|
70
|
+
const source = await this.findById(input.translationOf);
|
|
71
|
+
if (source?.translationGroup) translationGroup = source.translationGroup;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await this.db
|
|
75
|
+
.insertInto("taxonomies")
|
|
76
|
+
.values({
|
|
77
|
+
id,
|
|
78
|
+
name: input.name,
|
|
79
|
+
slug: input.slug,
|
|
80
|
+
label: input.label,
|
|
81
|
+
parent_id: parentId,
|
|
82
|
+
data: input.data ? JSON.stringify(input.data) : null,
|
|
83
|
+
// When omitted, the DB DEFAULT 'en' is used — keeps behaviour
|
|
84
|
+
// consistent with ContentRepository and lets higher layers
|
|
85
|
+
// supply an explicit locale from request context.
|
|
86
|
+
...(input.locale !== undefined ? { locale: input.locale } : {}),
|
|
87
|
+
translation_group: translationGroup,
|
|
88
|
+
})
|
|
89
|
+
.execute();
|
|
57
90
|
|
|
58
91
|
const taxonomy = await this.findById(id);
|
|
59
|
-
if (!taxonomy)
|
|
60
|
-
throw new Error("Failed to create taxonomy");
|
|
61
|
-
}
|
|
92
|
+
if (!taxonomy) throw new Error("Failed to create taxonomy");
|
|
62
93
|
return taxonomy;
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
/**
|
|
66
|
-
* Find taxonomy by ID
|
|
67
|
-
*/
|
|
68
96
|
async findById(id: string): Promise<Taxonomy | null> {
|
|
69
97
|
const row = await this.db
|
|
70
98
|
.selectFrom("taxonomies")
|
|
71
99
|
.selectAll()
|
|
72
100
|
.where("id", "=", id)
|
|
73
101
|
.executeTakeFirst();
|
|
74
|
-
|
|
75
102
|
return row ? this.rowToTaxonomy(row) : null;
|
|
76
103
|
}
|
|
77
104
|
|
|
78
105
|
/**
|
|
79
|
-
* Find
|
|
106
|
+
* Find a term by (name, slug). When `locale` is provided, filter by it.
|
|
107
|
+
* When omitted, returns the lowest-locale-code match (deterministic across
|
|
108
|
+
* calls). Mirrors `ContentRepository.findBySlug`.
|
|
80
109
|
*/
|
|
81
|
-
async findBySlug(name: string, slug: string): Promise<Taxonomy | null> {
|
|
82
|
-
|
|
110
|
+
async findBySlug(name: string, slug: string, locale?: string): Promise<Taxonomy | null> {
|
|
111
|
+
let query = this.db
|
|
83
112
|
.selectFrom("taxonomies")
|
|
84
113
|
.selectAll()
|
|
85
114
|
.where("name", "=", name)
|
|
86
|
-
.where("slug", "=", slug)
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
.where("slug", "=", slug);
|
|
116
|
+
if (locale !== undefined) query = query.where("locale", "=", locale);
|
|
117
|
+
const row = await query.orderBy("locale", "asc").executeTakeFirst();
|
|
89
118
|
return row ? this.rowToTaxonomy(row) : null;
|
|
90
119
|
}
|
|
91
120
|
|
|
92
121
|
/**
|
|
93
|
-
* Get all terms for a taxonomy (e.g., all categories)
|
|
122
|
+
* Get all terms for a taxonomy (e.g., all categories).
|
|
123
|
+
*
|
|
124
|
+
* `id asc` is a stable tiebreaker for terms that share a label. Without it
|
|
125
|
+
* the SQL ordering is implementation-defined when labels match, which
|
|
126
|
+
* breaks keyset pagination over `(label, id)`.
|
|
94
127
|
*/
|
|
95
|
-
async findByName(name: string, options:
|
|
96
|
-
// `id asc` is a stable tiebreaker for terms that share a label.
|
|
97
|
-
// Without it the SQL ordering is implementation-defined when labels
|
|
98
|
-
// match, which breaks keyset pagination over `(label, id)`.
|
|
128
|
+
async findByName(name: string, options: FindOptions = {}): Promise<Taxonomy[]> {
|
|
99
129
|
let query = this.db
|
|
100
130
|
.selectFrom("taxonomies")
|
|
101
131
|
.selectAll()
|
|
@@ -103,6 +133,8 @@ export class TaxonomyRepository {
|
|
|
103
133
|
.orderBy("label", "asc")
|
|
104
134
|
.orderBy("id", "asc");
|
|
105
135
|
|
|
136
|
+
if (options.locale !== undefined) query = query.where("locale", "=", options.locale);
|
|
137
|
+
|
|
106
138
|
if (options.parentId !== undefined) {
|
|
107
139
|
if (options.parentId === null) {
|
|
108
140
|
query = query.where("parent_id", "is", null);
|
|
@@ -115,9 +147,6 @@ export class TaxonomyRepository {
|
|
|
115
147
|
return rows.map((row) => this.rowToTaxonomy(row));
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
/**
|
|
119
|
-
* Get children of a taxonomy term
|
|
120
|
-
*/
|
|
121
150
|
async findChildren(parentId: string): Promise<Taxonomy[]> {
|
|
122
151
|
const rows = await this.db
|
|
123
152
|
.selectFrom("taxonomies")
|
|
@@ -126,18 +155,28 @@ export class TaxonomyRepository {
|
|
|
126
155
|
.orderBy("label", "asc")
|
|
127
156
|
.orderBy("id", "asc")
|
|
128
157
|
.execute();
|
|
129
|
-
|
|
130
158
|
return rows.map((row) => this.rowToTaxonomy(row));
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
/**
|
|
134
|
-
*
|
|
162
|
+
* Every translation sibling of a term (including itself), identified by
|
|
163
|
+
* their shared `translation_group`.
|
|
135
164
|
*/
|
|
165
|
+
async findTranslations(translationGroup: string): Promise<Taxonomy[]> {
|
|
166
|
+
const rows = await this.db
|
|
167
|
+
.selectFrom("taxonomies")
|
|
168
|
+
.selectAll()
|
|
169
|
+
.where("translation_group", "=", translationGroup)
|
|
170
|
+
.orderBy("locale", "asc")
|
|
171
|
+
.execute();
|
|
172
|
+
return rows.map((row) => this.rowToTaxonomy(row));
|
|
173
|
+
}
|
|
174
|
+
|
|
136
175
|
async update(id: string, input: UpdateTaxonomyInput): Promise<Taxonomy | null> {
|
|
137
176
|
const existing = await this.findById(id);
|
|
138
177
|
if (!existing) return null;
|
|
139
178
|
|
|
140
|
-
const updates:
|
|
179
|
+
const updates: Record<string, unknown> = {};
|
|
141
180
|
if (input.slug !== undefined) updates.slug = input.slug;
|
|
142
181
|
if (input.label !== undefined) updates.label = input.label;
|
|
143
182
|
if (input.parentId !== undefined) {
|
|
@@ -153,31 +192,42 @@ export class TaxonomyRepository {
|
|
|
153
192
|
return this.findById(id);
|
|
154
193
|
}
|
|
155
194
|
|
|
156
|
-
/**
|
|
157
|
-
* Delete a taxonomy term
|
|
158
|
-
*/
|
|
159
195
|
async delete(id: string): Promise<boolean> {
|
|
160
|
-
|
|
161
|
-
|
|
196
|
+
const term = await this.findById(id);
|
|
197
|
+
if (!term) return false;
|
|
198
|
+
|
|
199
|
+
// When deleting the last translation of a group the pivot rows that
|
|
200
|
+
// reference that translation_group become orphaned — purge them.
|
|
201
|
+
if (term.translationGroup) {
|
|
202
|
+
const siblings = await this.db
|
|
203
|
+
.selectFrom("taxonomies")
|
|
204
|
+
.select("id")
|
|
205
|
+
.where("translation_group", "=", term.translationGroup)
|
|
206
|
+
.where("id", "!=", id)
|
|
207
|
+
.execute();
|
|
208
|
+
if (siblings.length === 0) {
|
|
209
|
+
await this.db
|
|
210
|
+
.deleteFrom("content_taxonomies")
|
|
211
|
+
.where("taxonomy_id", "=", term.translationGroup)
|
|
212
|
+
.execute();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
162
215
|
|
|
163
216
|
const result = await this.db.deleteFrom("taxonomies").where("id", "=", id).executeTakeFirst();
|
|
164
|
-
|
|
165
|
-
return (result.numDeletedRows ?? 0) > 0;
|
|
217
|
+
return (result.numDeletedRows ?? 0n) > 0n;
|
|
166
218
|
}
|
|
167
219
|
|
|
168
|
-
// --- Content-Taxonomy Junction ---
|
|
220
|
+
// --- Content-Taxonomy Junction (taxonomy_id stores the translation_group) ---
|
|
169
221
|
|
|
170
|
-
/**
|
|
171
|
-
* Attach a taxonomy term to a content entry
|
|
172
|
-
*/
|
|
173
222
|
async attachToEntry(collection: string, entryId: string, taxonomyId: string): Promise<void> {
|
|
223
|
+
const group = await this.resolveTranslationGroup(taxonomyId);
|
|
224
|
+
if (!group) return;
|
|
225
|
+
|
|
174
226
|
const row: ContentTaxonomyTable = {
|
|
175
227
|
collection,
|
|
176
228
|
entry_id: entryId,
|
|
177
|
-
taxonomy_id:
|
|
229
|
+
taxonomy_id: group,
|
|
178
230
|
};
|
|
179
|
-
|
|
180
|
-
// Use INSERT OR IGNORE pattern for idempotency
|
|
181
231
|
await this.db
|
|
182
232
|
.insertInto("content_taxonomies")
|
|
183
233
|
.values(row)
|
|
@@ -185,58 +235,72 @@ export class TaxonomyRepository {
|
|
|
185
235
|
.execute();
|
|
186
236
|
}
|
|
187
237
|
|
|
188
|
-
/**
|
|
189
|
-
* Detach a taxonomy term from a content entry
|
|
190
|
-
*/
|
|
191
238
|
async detachFromEntry(collection: string, entryId: string, taxonomyId: string): Promise<void> {
|
|
239
|
+
const group = await this.resolveTranslationGroup(taxonomyId);
|
|
240
|
+
if (!group) return;
|
|
241
|
+
|
|
192
242
|
await this.db
|
|
193
243
|
.deleteFrom("content_taxonomies")
|
|
194
244
|
.where("collection", "=", collection)
|
|
195
245
|
.where("entry_id", "=", entryId)
|
|
196
|
-
.where("taxonomy_id", "=",
|
|
246
|
+
.where("taxonomy_id", "=", group)
|
|
197
247
|
.execute();
|
|
198
248
|
}
|
|
199
249
|
|
|
200
250
|
/**
|
|
201
|
-
*
|
|
251
|
+
* Taxonomy terms assigned to a content entry, resolved into a specific locale.
|
|
252
|
+
* Terms whose translation_group lacks a row in the requested locale are
|
|
253
|
+
* omitted — callers wanting fallback behaviour apply it themselves.
|
|
202
254
|
*/
|
|
203
255
|
async getTermsForEntry(
|
|
204
256
|
collection: string,
|
|
205
257
|
entryId: string,
|
|
206
258
|
taxonomyName?: string,
|
|
259
|
+
locale?: string,
|
|
207
260
|
): Promise<Taxonomy[]> {
|
|
208
261
|
let query = this.db
|
|
209
262
|
.selectFrom("content_taxonomies")
|
|
210
|
-
.innerJoin("taxonomies", "taxonomies.
|
|
263
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
211
264
|
.selectAll("taxonomies")
|
|
212
265
|
.where("content_taxonomies.collection", "=", collection)
|
|
213
266
|
.where("content_taxonomies.entry_id", "=", entryId);
|
|
214
267
|
|
|
215
|
-
if (taxonomyName)
|
|
216
|
-
|
|
217
|
-
}
|
|
268
|
+
if (taxonomyName) query = query.where("taxonomies.name", "=", taxonomyName);
|
|
269
|
+
if (locale !== undefined) query = query.where("taxonomies.locale", "=", locale);
|
|
218
270
|
|
|
219
|
-
const rows = await query.execute();
|
|
271
|
+
const rows = await query.orderBy("taxonomies.locale", "asc").execute();
|
|
220
272
|
return rows.map((row) => this.rowToTaxonomy(row));
|
|
221
273
|
}
|
|
222
274
|
|
|
223
275
|
/**
|
|
224
|
-
*
|
|
225
|
-
*
|
|
276
|
+
* Replace all assignments of a given taxonomy for one content entry.
|
|
277
|
+
* Term ids OR translation_groups are accepted and normalised to groups.
|
|
226
278
|
*/
|
|
227
279
|
async setTermsForEntry(
|
|
228
280
|
collection: string,
|
|
229
281
|
entryId: string,
|
|
230
282
|
taxonomyName: string,
|
|
231
|
-
|
|
283
|
+
termIds: string[],
|
|
232
284
|
): Promise<void> {
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
285
|
+
const groups: string[] = [];
|
|
286
|
+
for (const id of termIds) {
|
|
287
|
+
const group = await this.resolveTranslationGroup(id);
|
|
288
|
+
if (group) groups.push(group);
|
|
289
|
+
}
|
|
290
|
+
const newGroups = new Set(groups);
|
|
291
|
+
|
|
292
|
+
const current = await this.db
|
|
293
|
+
.selectFrom("content_taxonomies")
|
|
294
|
+
.innerJoin("taxonomies", "taxonomies.translation_group", "content_taxonomies.taxonomy_id")
|
|
295
|
+
.select(["content_taxonomies.taxonomy_id as group"])
|
|
296
|
+
.distinct()
|
|
297
|
+
.where("content_taxonomies.collection", "=", collection)
|
|
298
|
+
.where("content_taxonomies.entry_id", "=", entryId)
|
|
299
|
+
.where("taxonomies.name", "=", taxonomyName)
|
|
300
|
+
.execute();
|
|
301
|
+
const currentGroups = new Set(current.map((r) => r.group));
|
|
237
302
|
|
|
238
|
-
|
|
239
|
-
const toRemove = current.filter((t) => !newIds.has(t.id)).map((t) => t.id);
|
|
303
|
+
const toRemove = [...currentGroups].filter((g) => !newGroups.has(g));
|
|
240
304
|
if (toRemove.length > 0) {
|
|
241
305
|
await this.db
|
|
242
306
|
.deleteFrom("content_taxonomies")
|
|
@@ -246,8 +310,7 @@ export class TaxonomyRepository {
|
|
|
246
310
|
.execute();
|
|
247
311
|
}
|
|
248
312
|
|
|
249
|
-
|
|
250
|
-
const toAdd = taxonomyIds.filter((id) => !currentIds.has(id));
|
|
313
|
+
const toAdd = [...newGroups].filter((g) => !currentGroups.has(g));
|
|
251
314
|
if (toAdd.length > 0) {
|
|
252
315
|
await this.db
|
|
253
316
|
.insertInto("content_taxonomies")
|
|
@@ -263,44 +326,86 @@ export class TaxonomyRepository {
|
|
|
263
326
|
}
|
|
264
327
|
}
|
|
265
328
|
|
|
266
|
-
/**
|
|
267
|
-
* Remove all taxonomy associations for an entry (use when entry is deleted)
|
|
268
|
-
*/
|
|
269
329
|
async clearEntryTerms(collection: string, entryId: string): Promise<number> {
|
|
270
330
|
const result = await this.db
|
|
271
331
|
.deleteFrom("content_taxonomies")
|
|
272
332
|
.where("collection", "=", collection)
|
|
273
333
|
.where("entry_id", "=", entryId)
|
|
274
334
|
.executeTakeFirst();
|
|
275
|
-
|
|
276
335
|
return Number(result.numDeletedRows ?? 0);
|
|
277
336
|
}
|
|
278
337
|
|
|
279
338
|
/**
|
|
280
|
-
*
|
|
339
|
+
* Copy every term assignment from one content entry to another. Used when
|
|
340
|
+
* creating a translation of a post so the new translation inherits the
|
|
341
|
+
* source's term assignments. Safe to call when the source has no terms.
|
|
342
|
+
*/
|
|
343
|
+
async copyEntryTerms(
|
|
344
|
+
collection: string,
|
|
345
|
+
sourceEntryId: string,
|
|
346
|
+
targetEntryId: string,
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
const rows = await this.db
|
|
349
|
+
.selectFrom("content_taxonomies")
|
|
350
|
+
.select(["taxonomy_id"])
|
|
351
|
+
.where("collection", "=", collection)
|
|
352
|
+
.where("entry_id", "=", sourceEntryId)
|
|
353
|
+
.execute();
|
|
354
|
+
if (rows.length === 0) return;
|
|
355
|
+
|
|
356
|
+
await this.db
|
|
357
|
+
.insertInto("content_taxonomies")
|
|
358
|
+
.values(
|
|
359
|
+
rows.map((r) => ({
|
|
360
|
+
collection,
|
|
361
|
+
entry_id: targetEntryId,
|
|
362
|
+
taxonomy_id: r.taxonomy_id,
|
|
363
|
+
})),
|
|
364
|
+
)
|
|
365
|
+
.onConflict((oc) => oc.doNothing())
|
|
366
|
+
.execute();
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Count content entries that use any translation of this term. Accepts
|
|
371
|
+
* either a term id or a translation_group — we normalise to the group.
|
|
281
372
|
*/
|
|
282
|
-
async countEntriesWithTerm(
|
|
373
|
+
async countEntriesWithTerm(termIdOrGroup: string): Promise<number> {
|
|
374
|
+
const group = await this.resolveTranslationGroup(termIdOrGroup);
|
|
375
|
+
if (!group) return 0;
|
|
376
|
+
|
|
283
377
|
const result = await this.db
|
|
284
378
|
.selectFrom("content_taxonomies")
|
|
285
379
|
.select((eb) => eb.fn.count("entry_id").as("count"))
|
|
286
|
-
.where("taxonomy_id", "=",
|
|
380
|
+
.where("taxonomy_id", "=", group)
|
|
287
381
|
.executeTakeFirst();
|
|
382
|
+
return Number(result?.count ?? 0);
|
|
383
|
+
}
|
|
288
384
|
|
|
289
|
-
|
|
385
|
+
private async resolveTranslationGroup(idOrGroup: string): Promise<string | null> {
|
|
386
|
+
const row = await this.db
|
|
387
|
+
.selectFrom("taxonomies")
|
|
388
|
+
.select(["translation_group"])
|
|
389
|
+
.where((eb) => eb.or([eb("id", "=", idOrGroup), eb("translation_group", "=", idOrGroup)]))
|
|
390
|
+
.executeTakeFirst();
|
|
391
|
+
return row?.translation_group ?? null;
|
|
290
392
|
}
|
|
291
393
|
|
|
292
394
|
/**
|
|
293
|
-
* Batch count entries for multiple taxonomy
|
|
395
|
+
* Batch count entries for multiple taxonomy translation_groups.
|
|
294
396
|
* Chunks the query at SQL_BATCH_SIZE to stay below D1's bind-parameter limit.
|
|
295
|
-
* Returns a Map from
|
|
397
|
+
* Returns a Map from translation_group to count.
|
|
398
|
+
*
|
|
399
|
+
* Pass translation_groups (not term ids) — `content_taxonomies.taxonomy_id`
|
|
400
|
+
* stores the translation_group so a single assignment spans every locale.
|
|
296
401
|
*/
|
|
297
|
-
async countEntriesForTerms(
|
|
298
|
-
if (
|
|
402
|
+
async countEntriesForTerms(translationGroups: string[]): Promise<Map<string, number>> {
|
|
403
|
+
if (translationGroups.length === 0) return new Map();
|
|
299
404
|
|
|
300
405
|
const { chunks, SQL_BATCH_SIZE } = await import("../../utils/chunks.js");
|
|
301
406
|
|
|
302
407
|
const counts = new Map<string, number>();
|
|
303
|
-
for (const chunk of chunks(
|
|
408
|
+
for (const chunk of chunks(translationGroups, SQL_BATCH_SIZE)) {
|
|
304
409
|
const rows = await this.db
|
|
305
410
|
.selectFrom("content_taxonomies")
|
|
306
411
|
.select(["taxonomy_id", (eb) => eb.fn.count("entry_id").as("count")])
|
|
@@ -315,10 +420,7 @@ export class TaxonomyRepository {
|
|
|
315
420
|
return counts;
|
|
316
421
|
}
|
|
317
422
|
|
|
318
|
-
|
|
319
|
-
* Convert database row to Taxonomy object
|
|
320
|
-
*/
|
|
321
|
-
private rowToTaxonomy(row: TaxonomyTable): Taxonomy {
|
|
423
|
+
private rowToTaxonomy(row: Selectable<TaxonomyTable>): Taxonomy {
|
|
322
424
|
return {
|
|
323
425
|
id: row.id,
|
|
324
426
|
name: row.name,
|
|
@@ -326,6 +428,8 @@ export class TaxonomyRepository {
|
|
|
326
428
|
label: row.label,
|
|
327
429
|
parentId: row.parent_id,
|
|
328
430
|
data: row.data ? JSON.parse(row.data) : null,
|
|
431
|
+
locale: row.locale,
|
|
432
|
+
translationGroup: row.translation_group,
|
|
329
433
|
};
|
|
330
434
|
}
|
|
331
435
|
}
|
package/src/database/types.ts
CHANGED
|
@@ -20,12 +20,14 @@ export interface TaxonomyTable {
|
|
|
20
20
|
label: string;
|
|
21
21
|
parent_id: string | null;
|
|
22
22
|
data: string | null; // JSON
|
|
23
|
+
locale: Generated<string>; // e.g. 'en', 'es', 'fr'
|
|
24
|
+
translation_group: string | null; // shared across translations of the same term
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export interface ContentTaxonomyTable {
|
|
26
28
|
collection: string; // e.g., 'posts'
|
|
27
29
|
entry_id: string; // ID in the ec_* table
|
|
28
|
-
taxonomy_id: string;
|
|
30
|
+
taxonomy_id: string; // stores taxonomies.translation_group (locale-agnostic)
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
export interface TaxonomyDefTable {
|
|
@@ -36,6 +38,8 @@ export interface TaxonomyDefTable {
|
|
|
36
38
|
hierarchical: number; // 0 or 1 (SQLite boolean)
|
|
37
39
|
collections: string | null; // JSON array
|
|
38
40
|
created_at: Generated<string>;
|
|
41
|
+
locale: Generated<string>;
|
|
42
|
+
translation_group: string | null;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface MediaTable {
|
|
@@ -292,6 +296,8 @@ export interface MenuTable {
|
|
|
292
296
|
label: string;
|
|
293
297
|
created_at: Generated<string>;
|
|
294
298
|
updated_at: Generated<string>;
|
|
299
|
+
locale: Generated<string>;
|
|
300
|
+
translation_group: string | null;
|
|
295
301
|
}
|
|
296
302
|
|
|
297
303
|
export interface MenuItemTable {
|
|
@@ -301,13 +307,15 @@ export interface MenuItemTable {
|
|
|
301
307
|
sort_order: number;
|
|
302
308
|
type: string;
|
|
303
309
|
reference_collection: string | null;
|
|
304
|
-
reference_id: string | null;
|
|
310
|
+
reference_id: string | null; // stores translation_group of referenced content/term
|
|
305
311
|
custom_url: string | null;
|
|
306
312
|
label: string;
|
|
307
313
|
title_attr: string | null;
|
|
308
314
|
target: string | null;
|
|
309
315
|
css_classes: string | null;
|
|
310
316
|
created_at: Generated<string>;
|
|
317
|
+
locale: Generated<string>;
|
|
318
|
+
translation_group: string | null;
|
|
311
319
|
}
|
|
312
320
|
|
|
313
321
|
// Widget Areas
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared locale-resolution helpers.
|
|
3
|
+
*
|
|
4
|
+
* Matches the pattern used by `query.ts` for content: an explicit locale wins,
|
|
5
|
+
* otherwise we fall back to the request-context locale, otherwise to
|
|
6
|
+
* `defaultLocale` when i18n is enabled, otherwise to `undefined` (meaning "do
|
|
7
|
+
* not filter by locale" — legacy single-locale behaviour).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { getRequestContext } from "../request-context.js";
|
|
11
|
+
import { getFallbackChain, getI18nConfig, isI18nEnabled } from "./config.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the locale to use for a query given an optional explicit value.
|
|
15
|
+
* Returns `undefined` when no locale information is available; callers should
|
|
16
|
+
* treat that as "do not filter by locale".
|
|
17
|
+
*/
|
|
18
|
+
export function resolveLocale(explicit?: string): string | undefined {
|
|
19
|
+
if (explicit !== undefined) return explicit;
|
|
20
|
+
const ctxLocale = getRequestContext()?.locale;
|
|
21
|
+
if (ctxLocale !== undefined) return ctxLocale;
|
|
22
|
+
const cfg = getI18nConfig();
|
|
23
|
+
if (cfg && isI18nEnabled()) return cfg.defaultLocale;
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Fallback chain to try when looking up a single item. When i18n is disabled
|
|
29
|
+
* or the locale is unspecified, returns a single-element array (or empty when
|
|
30
|
+
* no locale resolves) so callers can iterate uniformly.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveLocaleChain(explicit?: string): string[] {
|
|
33
|
+
const locale = resolveLocale(explicit);
|
|
34
|
+
if (locale === undefined) return [];
|
|
35
|
+
if (!isI18nEnabled()) return [locale];
|
|
36
|
+
return getFallbackChain(locale);
|
|
37
|
+
}
|
package/src/loader.ts
CHANGED
|
@@ -125,12 +125,59 @@ const DATE_COLUMNS = new Set(["created_at", "updated_at", "published_at", "sched
|
|
|
125
125
|
*/
|
|
126
126
|
export const CURSOR_RAW_VALUES: unique symbol = Symbol("emdash:cursorRawValues");
|
|
127
127
|
|
|
128
|
+
const LOCAL_MEDIA_FILE_PREFIX = "/_emdash/api/media/file/";
|
|
129
|
+
const URL_SCHEME_PATTERN = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
|
|
130
|
+
|
|
128
131
|
/** Safely extract a string value from a record, returning fallback if not a string */
|
|
129
132
|
function rowStr(row: Record<string, unknown>, key: string, fallback = ""): string {
|
|
130
133
|
const val = row[key];
|
|
131
134
|
return typeof val === "string" ? val : fallback;
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
138
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isBareMediaKey(src: string): boolean {
|
|
142
|
+
return !src.startsWith("/") && !URL_SCHEME_PATTERN.test(src);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeLocalMediaValue(value: unknown): unknown {
|
|
146
|
+
if (Array.isArray(value)) {
|
|
147
|
+
return value.map(normalizeLocalMediaValue);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (!isRecord(value)) {
|
|
151
|
+
return value;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const normalized: Record<string, unknown> = {};
|
|
155
|
+
for (const [key, child] of Object.entries(value)) {
|
|
156
|
+
normalized[key] = normalizeLocalMediaValue(child);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (
|
|
160
|
+
normalized.provider === "local" &&
|
|
161
|
+
typeof normalized.src === "string" &&
|
|
162
|
+
normalized.src.length > 0
|
|
163
|
+
) {
|
|
164
|
+
const src = normalized.src;
|
|
165
|
+
if (src.startsWith(LOCAL_MEDIA_FILE_PREFIX)) {
|
|
166
|
+
const id = src.slice(LOCAL_MEDIA_FILE_PREFIX.length);
|
|
167
|
+
if (!normalized.id && id) {
|
|
168
|
+
normalized.id = id;
|
|
169
|
+
}
|
|
170
|
+
} else if (isBareMediaKey(src)) {
|
|
171
|
+
if (!normalized.id) {
|
|
172
|
+
normalized.id = src;
|
|
173
|
+
}
|
|
174
|
+
normalized.src = `${LOCAL_MEDIA_FILE_PREFIX}${src}`;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return normalized;
|
|
179
|
+
}
|
|
180
|
+
|
|
134
181
|
/**
|
|
135
182
|
* Map a database row to entry data
|
|
136
183
|
* Extracts content fields (non-system columns) and parses JSON where needed.
|
|
@@ -164,7 +211,7 @@ function mapRowToData(row: Record<string, unknown>): Record<string, unknown> {
|
|
|
164
211
|
try {
|
|
165
212
|
// Only parse if it looks like JSON (starts with { or [)
|
|
166
213
|
if (value.startsWith("{") || value.startsWith("[")) {
|
|
167
|
-
data[key] = JSON.parse(value);
|
|
214
|
+
data[key] = normalizeLocalMediaValue(JSON.parse(value));
|
|
168
215
|
} else {
|
|
169
216
|
data[key] = value;
|
|
170
217
|
}
|
|
@@ -194,7 +241,7 @@ function mapRevisionData(data: Record<string, unknown>): Record<string, unknown>
|
|
|
194
241
|
const result: Record<string, unknown> = {};
|
|
195
242
|
for (const [key, value] of Object.entries(data)) {
|
|
196
243
|
if (key.startsWith("_")) continue; // revision metadata
|
|
197
|
-
result[key] = value;
|
|
244
|
+
result[key] = normalizeLocalMediaValue(value);
|
|
198
245
|
}
|
|
199
246
|
return result;
|
|
200
247
|
}
|