emdash 0.9.0 → 0.11.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-Ded_1vng.mjs} +167 -254
- package/dist/apply-Ded_1vng.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.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +94 -43
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +12 -11
- 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-gFn1r0vA.mjs} +4 -4
- package/dist/{byline-BSaNL1w7.mjs.map → byline-gFn1r0vA.mjs.map} +1 -1
- package/dist/{bylines-CvJ3PYz2.mjs → bylines-DTFI8nDM.mjs} +5 -5
- package/dist/{bylines-CvJ3PYz2.mjs.map → bylines-DTFI8nDM.mjs.map} +1 -1
- package/dist/{cache-C6N_hhN7.mjs → cache-BAJbeoZ8.mjs} +3 -3
- package/dist/{cache-C6N_hhN7.mjs.map → cache-BAJbeoZ8.mjs.map} +1 -1
- package/dist/{chunks-NBQVDOci.mjs → chunks-BK1oZS-l.mjs} +2 -2
- package/dist/{chunks-NBQVDOci.mjs.map → chunks-BK1oZS-l.mjs.map} +1 -1
- package/dist/cli/index.mjs +342 -95
- 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-CERxPUN0.mjs} +14 -3
- package/dist/content-CERxPUN0.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +6 -4
- package/dist/database/instrumentation.d.mts.map +1 -1
- package/dist/database/instrumentation.mjs +19 -7
- package/dist/database/instrumentation.mjs.map +1 -1
- 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-Cg-rC4Gj.d.mts} +110 -87
- package/dist/index-Cg-rC4Gj.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +29 -28
- package/dist/{load-DDqMMvZL.mjs → load-DR1VwFXR.mjs} +2 -2
- package/dist/{load-DDqMMvZL.mjs.map → load-DR1VwFXR.mjs.map} +1 -1
- package/dist/{loader-CKLbBnhK.mjs → loader-ou_PXAjg.mjs} +31 -6
- package/dist/loader-ou_PXAjg.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-1fFhub9c.mjs} +22 -10
- package/dist/media-1fFhub9c.mjs.map +1 -0
- 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-8c_meo_K.mjs} +13 -13
- package/dist/{query-Cg9ZKRQ0.mjs.map → query-8c_meo_K.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-Do34mz_P.mjs} +7 -6
- package/dist/registry-Do34mz_P.mjs.map +1 -0
- package/dist/{request-cache-B-bmkipQ.mjs → request-cache-D4I69LeL.mjs} +6 -2
- package/dist/request-cache-D4I69LeL.mjs.map +1 -0
- package/dist/request-context.d.mts +27 -1
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs +16 -3
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-C7ADox5q.mjs → runner-DIcU2UCC.mjs} +465 -148
- package/dist/runner-DIcU2UCC.mjs.map +1 -0
- package/dist/{runner-Bnoj7vjK.d.mts → runner-Iu3IZSDM.d.mts} +2 -2
- package/dist/{runner-Bnoj7vjK.d.mts.map → runner-Iu3IZSDM.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +3 -3
- package/dist/{search-dOGEccMa.mjs → search-DuWhx4NG.mjs} +322 -108
- package/dist/search-DuWhx4NG.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-Bw76xAxo.mjs +407 -0
- package/dist/taxonomies-Bw76xAxo.mjs.map +1 -0
- package/dist/taxonomy-D6NvlKo8.mjs +218 -0
- package/dist/taxonomy-D6NvlKo8.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-CIOg5AR8.mjs → types-56BKbld_.mjs} +1 -1
- package/dist/types-56BKbld_.mjs.map +1 -0
- 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-CrtWgIvl.d.mts → types-BQx6ZXpR.d.mts} +10 -1
- package/dist/types-BQx6ZXpR.d.mts.map +1 -0
- 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-DiI8NOG_.mjs +16 -0
- package/dist/types-DiI8NOG_.mjs.map +1 -0
- package/dist/{types-BuBIptGk.d.mts → types-IN5z_S3P.d.mts} +158 -92
- package/dist/types-IN5z_S3P.d.mts.map +1 -0
- package/dist/{types-BSyXeCFW.d.mts → types-IZSZfEwv.d.mts} +4 -3
- package/dist/types-IZSZfEwv.d.mts.map +1 -0
- 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-BfQh_C_y.d.mts → validate-CO3JjFV5.d.mts} +22 -5
- package/dist/validate-CO3JjFV5.d.mts.map +1 -0
- package/dist/{validate-Baqf0slj.mjs → validate-UK4Ja1uo.mjs} +14 -10
- package/dist/validate-UK4Ja1uo.mjs.map +1 -0
- package/dist/{validation-BfEI7tNe.mjs → validation-Vc5DQkJa.mjs} +5 -5
- package/dist/{validation-BfEI7tNe.mjs.map → validation-Vc5DQkJa.mjs.map} +1 -1
- package/dist/version-Bg31I_Ff.mjs +7 -0
- package/dist/{version-DoxrVdYf.mjs.map → version-Bg31I_Ff.mjs.map} +1 -1
- package/dist/{zod-generator-CC0xNe_K.mjs → zod-generator-CHnJUP2l.mjs} +8 -3
- package/dist/zod-generator-CHnJUP2l.mjs.map +1 -0
- package/package.json +9 -8
- package/src/api/errors.ts +5 -0
- package/src/api/handlers/content.ts +20 -0
- package/src/api/handlers/dashboard.ts +29 -36
- package/src/api/handlers/media-allowlist.ts +40 -0
- package/src/api/handlers/media.ts +1 -1
- package/src/api/handlers/menus.ts +400 -89
- package/src/api/handlers/taxonomies.ts +273 -97
- package/src/api/handlers/validate-media-fields.ts +125 -0
- package/src/api/schemas/common.ts +7 -0
- package/src/api/schemas/media.ts +23 -3
- package/src/api/schemas/menus.ts +23 -0
- package/src/api/schemas/schema.ts +11 -2
- package/src/api/schemas/taxonomies.ts +39 -0
- package/src/astro/integration/routes.ts +10 -0
- package/src/astro/middleware.ts +46 -11
- 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/media/upload-url.ts +10 -4
- package/src/astro/routes/api/media.ts +12 -4
- 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/astro/types.ts +5 -1
- package/src/auth/rate-limit.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +81 -6
- package/src/cli/commands/bundle.ts +18 -15
- package/src/cli/commands/export-seed.ts +139 -24
- package/src/cli/commands/plugin-init.ts +216 -90
- package/src/database/instrumentation.ts +22 -8
- package/src/database/migrations/016_api_tokens.ts +18 -3
- package/src/database/migrations/036_i18n_menus_and_taxonomies.ts +477 -0
- package/src/database/migrations/037_credential_algorithm.ts +18 -0
- package/src/database/migrations/runner.ts +4 -0
- package/src/database/repositories/content.ts +11 -0
- package/src/database/repositories/media.ts +40 -10
- package/src/database/repositories/taxonomy.ts +193 -89
- package/src/database/types.ts +12 -3
- package/src/emdash-runtime.ts +16 -3
- package/src/fields/file.ts +7 -6
- package/src/fields/image.ts +12 -11
- package/src/fields/types.ts +3 -0
- package/src/i18n/resolve.ts +37 -0
- package/src/index.ts +1 -1
- package/src/loader.ts +49 -2
- package/src/mcp/server.ts +114 -26
- package/src/media/mime.ts +75 -0
- package/src/menus/index.ts +143 -124
- package/src/menus/types.ts +15 -1
- package/src/plugins/types.ts +81 -191
- package/src/request-cache.ts +6 -2
- package/src/request-context.ts +42 -2
- package/src/schema/registry.ts +5 -5
- package/src/schema/types.ts +3 -2
- package/src/schema/zod-generator.ts +12 -2
- package/src/seed/apply.ts +157 -54
- package/src/seed/types.ts +18 -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/media-BW32b4gi.mjs.map +0 -1
- package/dist/registry-Dw70ChxB.mjs.map +0 -1
- package/dist/request-cache-B-bmkipQ.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-4fVtCIm0.mjs +0 -68
- package/dist/types-4fVtCIm0.mjs.map +0 -1
- package/dist/types-BSyXeCFW.d.mts.map +0 -1
- package/dist/types-BuBIptGk.d.mts.map +0 -1
- package/dist/types-CIOg5AR8.mjs.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 {
|
|
@@ -72,7 +76,8 @@ export interface UserTable {
|
|
|
72
76
|
export interface CredentialTable {
|
|
73
77
|
id: string; // Base64url credential ID
|
|
74
78
|
user_id: string;
|
|
75
|
-
public_key: Uint8Array; //
|
|
79
|
+
public_key: Uint8Array; // SEC1 or PKIX encoded public key
|
|
80
|
+
algorithm: number;
|
|
76
81
|
counter: number;
|
|
77
82
|
device_type: string; // 'singleDevice' | 'multiDevice'
|
|
78
83
|
backed_up: number; // 0 or 1
|
|
@@ -292,6 +297,8 @@ export interface MenuTable {
|
|
|
292
297
|
label: string;
|
|
293
298
|
created_at: Generated<string>;
|
|
294
299
|
updated_at: Generated<string>;
|
|
300
|
+
locale: Generated<string>;
|
|
301
|
+
translation_group: string | null;
|
|
295
302
|
}
|
|
296
303
|
|
|
297
304
|
export interface MenuItemTable {
|
|
@@ -301,13 +308,15 @@ export interface MenuItemTable {
|
|
|
301
308
|
sort_order: number;
|
|
302
309
|
type: string;
|
|
303
310
|
reference_collection: string | null;
|
|
304
|
-
reference_id: string | null;
|
|
311
|
+
reference_id: string | null; // stores translation_group of referenced content/term
|
|
305
312
|
custom_url: string | null;
|
|
306
313
|
label: string;
|
|
307
314
|
title_attr: string | null;
|
|
308
315
|
target: string | null;
|
|
309
316
|
css_classes: string | null;
|
|
310
317
|
created_at: Generated<string>;
|
|
318
|
+
locale: Generated<string>;
|
|
319
|
+
translation_group: string | null;
|
|
311
320
|
}
|
|
312
321
|
|
|
313
322
|
// Widget Areas
|
package/src/emdash-runtime.ts
CHANGED
|
@@ -1287,6 +1287,8 @@ export class EmDashRuntime {
|
|
|
1287
1287
|
// or arbitrary `Record<string, unknown>` for plugin field widgets that
|
|
1288
1288
|
// need per-field config (e.g. a checkbox grid receiving its column defs).
|
|
1289
1289
|
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
|
|
1290
|
+
id?: string;
|
|
1291
|
+
validation?: Record<string, unknown>;
|
|
1290
1292
|
}
|
|
1291
1293
|
> = {};
|
|
1292
1294
|
|
|
@@ -1296,6 +1298,9 @@ export class EmDashRuntime {
|
|
|
1296
1298
|
label: field.label,
|
|
1297
1299
|
required: field.required,
|
|
1298
1300
|
};
|
|
1301
|
+
// Always include the field's database ID so the admin can forward it
|
|
1302
|
+
// to upload/media-list API calls for MIME allowlist widening.
|
|
1303
|
+
entry.id = field.id;
|
|
1299
1304
|
if (field.widget) entry.widget = field.widget;
|
|
1300
1305
|
// Plugin field widgets read their per-field config from `field.options`,
|
|
1301
1306
|
// which the seed schema types as `Record<string, unknown>`. Pass it
|
|
@@ -1312,8 +1317,12 @@ export class EmDashRuntime {
|
|
|
1312
1317
|
}));
|
|
1313
1318
|
}
|
|
1314
1319
|
// Include full validation for repeater fields (subFields, minItems, maxItems)
|
|
1315
|
-
|
|
1316
|
-
|
|
1320
|
+
// and for file/image fields (allowedMimeTypes).
|
|
1321
|
+
if (
|
|
1322
|
+
(field.type === "repeater" || field.type === "file" || field.type === "image") &&
|
|
1323
|
+
field.validation
|
|
1324
|
+
) {
|
|
1325
|
+
entry.validation = { ...field.validation };
|
|
1317
1326
|
}
|
|
1318
1327
|
fields[field.slug] = entry;
|
|
1319
1328
|
}
|
|
@@ -1980,7 +1989,11 @@ export class EmDashRuntime {
|
|
|
1980
1989
|
// Media Handlers
|
|
1981
1990
|
// =========================================================================
|
|
1982
1991
|
|
|
1983
|
-
async handleMediaList(params: {
|
|
1992
|
+
async handleMediaList(params: {
|
|
1993
|
+
cursor?: string;
|
|
1994
|
+
limit?: number;
|
|
1995
|
+
mimeType?: string | readonly string[];
|
|
1996
|
+
}) {
|
|
1984
1997
|
return handleMediaList(this.db, params);
|
|
1985
1998
|
}
|
|
1986
1999
|
|
package/src/fields/file.ts
CHANGED
|
@@ -5,13 +5,10 @@ import type { FieldDefinition, FieldUIHints, FileValue } from "./types.js";
|
|
|
5
5
|
export interface FileOptions {
|
|
6
6
|
required?: boolean;
|
|
7
7
|
maxSize?: number; // In bytes
|
|
8
|
-
allowedTypes?: string[]; // MIME types
|
|
8
|
+
allowedTypes?: string[]; // MIME types — exact (image/png) or prefix (image/)
|
|
9
9
|
helpText?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
13
|
-
* File field - file upload
|
|
14
|
-
*/
|
|
15
12
|
export function file(options: FileOptions = {}): FieldDefinition<FileValue> {
|
|
16
13
|
const fileObjSchema = z.object({
|
|
17
14
|
id: z.string(),
|
|
@@ -21,21 +18,25 @@ export function file(options: FileOptions = {}): FieldDefinition<FileValue> {
|
|
|
21
18
|
size: z.number(),
|
|
22
19
|
});
|
|
23
20
|
|
|
24
|
-
// Optional vs required
|
|
25
21
|
const schema: z.ZodTypeAny = options.required ? fileObjSchema : fileObjSchema.optional();
|
|
26
22
|
|
|
27
23
|
const ui: FieldUIHints = {
|
|
28
24
|
widget: "file",
|
|
29
25
|
helpText: options.helpText,
|
|
30
26
|
maxSize: options.maxSize,
|
|
31
|
-
allowedTypes: options.allowedTypes,
|
|
32
27
|
};
|
|
33
28
|
|
|
29
|
+
const validation =
|
|
30
|
+
options.allowedTypes && options.allowedTypes.length > 0
|
|
31
|
+
? { allowedMimeTypes: [...options.allowedTypes] }
|
|
32
|
+
: undefined;
|
|
33
|
+
|
|
34
34
|
return {
|
|
35
35
|
type: "file",
|
|
36
36
|
columnType: "TEXT",
|
|
37
37
|
schema,
|
|
38
38
|
options,
|
|
39
39
|
ui,
|
|
40
|
+
validation,
|
|
40
41
|
};
|
|
41
42
|
}
|
package/src/fields/image.ts
CHANGED
|
@@ -2,9 +2,6 @@ import { z } from "astro/zod";
|
|
|
2
2
|
|
|
3
3
|
import type { FieldDefinition, ImageValue } from "./types.js";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Image field schema
|
|
7
|
-
*/
|
|
8
5
|
const imageSchema = z.object({
|
|
9
6
|
id: z.string(),
|
|
10
7
|
src: z.string(),
|
|
@@ -13,22 +10,26 @@ const imageSchema = z.object({
|
|
|
13
10
|
height: z.number().optional(),
|
|
14
11
|
});
|
|
15
12
|
|
|
16
|
-
|
|
17
|
-
* Image field
|
|
18
|
-
* References media items from the media library
|
|
19
|
-
*/
|
|
20
|
-
export function image(options?: {
|
|
13
|
+
export interface ImageOptions {
|
|
21
14
|
required?: boolean;
|
|
22
15
|
maxSize?: number; // in bytes
|
|
23
|
-
allowedTypes?: string[]; // MIME types
|
|
24
|
-
}
|
|
16
|
+
allowedTypes?: string[]; // MIME types — exact or prefix
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function image(options: ImageOptions = {}): FieldDefinition<ImageValue | undefined> {
|
|
20
|
+
const validation =
|
|
21
|
+
options.allowedTypes && options.allowedTypes.length > 0
|
|
22
|
+
? { allowedMimeTypes: [...options.allowedTypes] }
|
|
23
|
+
: undefined;
|
|
24
|
+
|
|
25
25
|
return {
|
|
26
26
|
type: "image",
|
|
27
27
|
columnType: "TEXT",
|
|
28
|
-
schema: options
|
|
28
|
+
schema: options.required === false ? imageSchema.optional() : imageSchema,
|
|
29
29
|
options,
|
|
30
30
|
ui: {
|
|
31
31
|
widget: "image",
|
|
32
32
|
},
|
|
33
|
+
validation,
|
|
33
34
|
};
|
|
34
35
|
}
|