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
package/src/fields/types.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { z } from "astro/zod";
|
|
2
2
|
|
|
3
|
+
import type { FieldValidation } from "../schema/types.js";
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* SQLite column types that map from field types
|
|
5
7
|
*/
|
|
@@ -19,6 +21,7 @@ export interface FieldDefinition<_T = unknown> {
|
|
|
19
21
|
schema: z.ZodTypeAny;
|
|
20
22
|
options?: unknown;
|
|
21
23
|
ui?: FieldUIHints;
|
|
24
|
+
validation?: FieldValidation;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
27
|
/**
|
|
@@ -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/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ export type {
|
|
|
27
27
|
export type { MediaItem, CreateMediaInput } from "./database/repositories/media.js";
|
|
28
28
|
|
|
29
29
|
// Fields
|
|
30
|
-
export { portableText, image, reference } from "./fields/index.js";
|
|
30
|
+
export { portableText, image, file, reference } from "./fields/index.js";
|
|
31
31
|
export { normalizeMediaValue } from "./media/normalize.js";
|
|
32
32
|
export { generatePlaceholder } from "./media/placeholder.js";
|
|
33
33
|
export type { PlaceholderData } from "./media/placeholder.js";
|
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
|
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -1667,16 +1667,19 @@ export function createMcpServer(): McpServer {
|
|
|
1667
1667
|
description:
|
|
1668
1668
|
"List all taxonomy definitions (e.g. categories, tags). Taxonomies are " +
|
|
1669
1669
|
"classification systems applied to content. Each has a name, label, and " +
|
|
1670
|
-
"can be hierarchical (categories) or flat (tags)."
|
|
1671
|
-
|
|
1670
|
+
"can be hierarchical (categories) or flat (tags). Optionally filter by " +
|
|
1671
|
+
"locale.",
|
|
1672
|
+
inputSchema: z.object({
|
|
1673
|
+
locale: z.string().optional().describe("Filter by locale (omit for all)"),
|
|
1674
|
+
}),
|
|
1672
1675
|
annotations: { readOnlyHint: true },
|
|
1673
1676
|
},
|
|
1674
|
-
async (
|
|
1677
|
+
async (args, extra) => {
|
|
1675
1678
|
requireScope(extra, "content:read");
|
|
1676
1679
|
const ec = getEmDash(extra);
|
|
1677
1680
|
try {
|
|
1678
1681
|
const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
|
|
1679
|
-
return unwrap(await handleTaxonomyList(ec.db));
|
|
1682
|
+
return unwrap(await handleTaxonomyList(ec.db, { locale: args.locale }));
|
|
1680
1683
|
} catch (error) {
|
|
1681
1684
|
return respondHandlerError(error, "TAXONOMY_LIST_ERROR");
|
|
1682
1685
|
}
|
|
@@ -1695,6 +1698,7 @@ export function createMcpServer(): McpServer {
|
|
|
1695
1698
|
taxonomy: z.string().describe("Taxonomy name (e.g. 'categories', 'tags')"),
|
|
1696
1699
|
limit: z.number().int().min(1).max(100).optional().describe("Max items (default 50)"),
|
|
1697
1700
|
cursor: z.string().min(1).max(2048).optional().describe("Pagination cursor"),
|
|
1701
|
+
locale: z.string().optional().describe("Filter by locale (omit for all)"),
|
|
1698
1702
|
}),
|
|
1699
1703
|
annotations: { readOnlyHint: true },
|
|
1700
1704
|
},
|
|
@@ -1702,9 +1706,8 @@ export function createMcpServer(): McpServer {
|
|
|
1702
1706
|
requireScope(extra, "content:read");
|
|
1703
1707
|
const ec = getEmDash(extra);
|
|
1704
1708
|
try {
|
|
1705
|
-
// Verify taxonomy exists via handler layer
|
|
1706
1709
|
const { handleTaxonomyList } = await import("../api/handlers/taxonomies.js");
|
|
1707
|
-
const listResult = await handleTaxonomyList(ec.db);
|
|
1710
|
+
const listResult = await handleTaxonomyList(ec.db, { locale: args.locale });
|
|
1708
1711
|
if (!listResult.success) return unwrap(listResult);
|
|
1709
1712
|
|
|
1710
1713
|
const taxonomies = (listResult.data as { taxonomies: Array<{ name: string; id?: string }> })
|
|
@@ -1712,13 +1715,12 @@ export function createMcpServer(): McpServer {
|
|
|
1712
1715
|
const taxonomy = taxonomies.find((t: { name: string }) => t.name === args.taxonomy);
|
|
1713
1716
|
if (!taxonomy) return respondError("NOT_FOUND", `Taxonomy '${args.taxonomy}' not found`);
|
|
1714
1717
|
|
|
1715
|
-
// Paginated term query via repository (avoids N+1 of handleTermList)
|
|
1716
1718
|
const { TaxonomyRepository } = await import("../database/repositories/taxonomy.js");
|
|
1717
1719
|
const { decodeCursor, encodeCursor, InvalidCursorError } =
|
|
1718
1720
|
await import("../database/repositories/types.js");
|
|
1719
1721
|
const repo = new TaxonomyRepository(ec.db);
|
|
1720
1722
|
const limit = Math.min(args.limit ?? 50, 100);
|
|
1721
|
-
const terms = await repo.findByName(args.taxonomy);
|
|
1723
|
+
const terms = await repo.findByName(args.taxonomy, { locale: args.locale });
|
|
1722
1724
|
|
|
1723
1725
|
// Manual keyset pagination over the sorted-by-label results.
|
|
1724
1726
|
// Using a base64-encoded `(label, id)` cursor matches the
|
|
@@ -1760,6 +1762,8 @@ export function createMcpServer(): McpServer {
|
|
|
1760
1762
|
label: t.label,
|
|
1761
1763
|
parentId: t.parentId,
|
|
1762
1764
|
description: typeof t.data?.description === "string" ? t.data.description : undefined,
|
|
1765
|
+
locale: t.locale,
|
|
1766
|
+
translationGroup: t.translationGroup,
|
|
1763
1767
|
})),
|
|
1764
1768
|
nextCursor,
|
|
1765
1769
|
});
|
|
@@ -1785,6 +1789,11 @@ export function createMcpServer(): McpServer {
|
|
|
1785
1789
|
label: z.string().describe("Display name"),
|
|
1786
1790
|
parentId: z.string().optional().describe("Parent term ID for hierarchical taxonomies"),
|
|
1787
1791
|
description: z.string().optional().describe("Description of the term"),
|
|
1792
|
+
locale: z.string().optional().describe("Locale for the new term (e.g. 'es')"),
|
|
1793
|
+
translationOf: z
|
|
1794
|
+
.string()
|
|
1795
|
+
.optional()
|
|
1796
|
+
.describe("Term id to join as a translation (same translation_group)"),
|
|
1788
1797
|
}),
|
|
1789
1798
|
},
|
|
1790
1799
|
async (args, extra) => {
|
|
@@ -1799,6 +1808,8 @@ export function createMcpServer(): McpServer {
|
|
|
1799
1808
|
label: args.label,
|
|
1800
1809
|
parentId: args.parentId,
|
|
1801
1810
|
description: args.description,
|
|
1811
|
+
locale: args.locale,
|
|
1812
|
+
translationOf: args.translationOf,
|
|
1802
1813
|
}),
|
|
1803
1814
|
);
|
|
1804
1815
|
} catch (error) {
|
|
@@ -1875,6 +1886,29 @@ export function createMcpServer(): McpServer {
|
|
|
1875
1886
|
},
|
|
1876
1887
|
);
|
|
1877
1888
|
|
|
1889
|
+
server.registerTool(
|
|
1890
|
+
"taxonomy_term_translations",
|
|
1891
|
+
{
|
|
1892
|
+
title: "List Term Translations",
|
|
1893
|
+
description:
|
|
1894
|
+
"Return every locale variant of a taxonomy term, identified via its shared translation_group.",
|
|
1895
|
+
inputSchema: z.object({
|
|
1896
|
+
id: z.string().describe("Term id (or translation_group)"),
|
|
1897
|
+
}),
|
|
1898
|
+
annotations: { readOnlyHint: true },
|
|
1899
|
+
},
|
|
1900
|
+
async (args, extra) => {
|
|
1901
|
+
requireScope(extra, "content:read");
|
|
1902
|
+
const ec = getEmDash(extra);
|
|
1903
|
+
try {
|
|
1904
|
+
const { handleTermTranslations } = await import("../api/handlers/taxonomies.js");
|
|
1905
|
+
return unwrap(await handleTermTranslations(ec.db, args.id));
|
|
1906
|
+
} catch (error) {
|
|
1907
|
+
return respondHandlerError(error, "TERM_TRANSLATIONS_ERROR");
|
|
1908
|
+
}
|
|
1909
|
+
},
|
|
1910
|
+
);
|
|
1911
|
+
|
|
1878
1912
|
// =====================================================================
|
|
1879
1913
|
// Menu tools
|
|
1880
1914
|
// =====================================================================
|
|
@@ -1884,18 +1918,20 @@ export function createMcpServer(): McpServer {
|
|
|
1884
1918
|
{
|
|
1885
1919
|
title: "List Menus",
|
|
1886
1920
|
description:
|
|
1887
|
-
"List
|
|
1888
|
-
"
|
|
1889
|
-
"
|
|
1890
|
-
inputSchema: z.object({
|
|
1921
|
+
"List navigation menus. Menus are per-locale: filter by `locale` to " +
|
|
1922
|
+
"get just one locale's worth, or omit to list every row (one per " +
|
|
1923
|
+
"locale per menu name).",
|
|
1924
|
+
inputSchema: z.object({
|
|
1925
|
+
locale: z.string().optional().describe("Filter by locale (omit for all)"),
|
|
1926
|
+
}),
|
|
1891
1927
|
annotations: { readOnlyHint: true },
|
|
1892
1928
|
},
|
|
1893
|
-
async (
|
|
1929
|
+
async (args, extra) => {
|
|
1894
1930
|
requireScope(extra, "content:read");
|
|
1895
1931
|
const ec = getEmDash(extra);
|
|
1896
1932
|
try {
|
|
1897
1933
|
const { handleMenuList } = await import("../api/handlers/menus.js");
|
|
1898
|
-
return unwrap(await handleMenuList(ec.db));
|
|
1934
|
+
return unwrap(await handleMenuList(ec.db, { locale: args.locale }));
|
|
1899
1935
|
} catch (error) {
|
|
1900
1936
|
return respondHandlerError(error, "MENU_LIST_ERROR");
|
|
1901
1937
|
}
|
|
@@ -1907,11 +1943,11 @@ export function createMcpServer(): McpServer {
|
|
|
1907
1943
|
{
|
|
1908
1944
|
title: "Get Menu with Items",
|
|
1909
1945
|
description:
|
|
1910
|
-
"Get a menu by name including
|
|
1911
|
-
"
|
|
1912
|
-
"for nesting.",
|
|
1946
|
+
"Get a menu by name, including its items. When multiple locales exist, " +
|
|
1947
|
+
"pass `locale` to pick the right one.",
|
|
1913
1948
|
inputSchema: z.object({
|
|
1914
1949
|
name: z.string().describe("Menu name (e.g. 'main', 'footer')"),
|
|
1950
|
+
locale: z.string().optional().describe("Locale to resolve the menu for"),
|
|
1915
1951
|
}),
|
|
1916
1952
|
annotations: { readOnlyHint: true },
|
|
1917
1953
|
},
|
|
@@ -1920,13 +1956,36 @@ export function createMcpServer(): McpServer {
|
|
|
1920
1956
|
const ec = getEmDash(extra);
|
|
1921
1957
|
try {
|
|
1922
1958
|
const { handleMenuGet } = await import("../api/handlers/menus.js");
|
|
1923
|
-
return unwrap(await handleMenuGet(ec.db, args.name));
|
|
1959
|
+
return unwrap(await handleMenuGet(ec.db, args.name, { locale: args.locale }));
|
|
1924
1960
|
} catch (error) {
|
|
1925
1961
|
return respondHandlerError(error, "MENU_GET_ERROR");
|
|
1926
1962
|
}
|
|
1927
1963
|
},
|
|
1928
1964
|
);
|
|
1929
1965
|
|
|
1966
|
+
server.registerTool(
|
|
1967
|
+
"menu_translations",
|
|
1968
|
+
{
|
|
1969
|
+
title: "List Menu Translations",
|
|
1970
|
+
description:
|
|
1971
|
+
"Return every locale variant of a menu, identified via the shared translation_group.",
|
|
1972
|
+
inputSchema: z.object({
|
|
1973
|
+
id: z.string().describe("Menu id (or translation_group)"),
|
|
1974
|
+
}),
|
|
1975
|
+
annotations: { readOnlyHint: true },
|
|
1976
|
+
},
|
|
1977
|
+
async (args, extra) => {
|
|
1978
|
+
requireScope(extra, "content:read");
|
|
1979
|
+
const ec = getEmDash(extra);
|
|
1980
|
+
try {
|
|
1981
|
+
const { handleMenuTranslations } = await import("../api/handlers/menus.js");
|
|
1982
|
+
return unwrap(await handleMenuTranslations(ec.db, args.id));
|
|
1983
|
+
} catch (error) {
|
|
1984
|
+
return respondHandlerError(error, "MENU_TRANSLATIONS_ERROR");
|
|
1985
|
+
}
|
|
1986
|
+
},
|
|
1987
|
+
);
|
|
1988
|
+
|
|
1930
1989
|
server.registerTool(
|
|
1931
1990
|
"menu_create",
|
|
1932
1991
|
{
|
|
@@ -1934,13 +1993,23 @@ export function createMcpServer(): McpServer {
|
|
|
1934
1993
|
description:
|
|
1935
1994
|
"Create a new navigation menu. The `name` is the stable identifier used " +
|
|
1936
1995
|
"by site templates (e.g. 'main', 'footer'); `label` is the human-readable " +
|
|
1937
|
-
"name shown in the admin.
|
|
1996
|
+
"name shown in the admin. Menus are per-locale, so pass `locale` when " +
|
|
1997
|
+
"the same menu name exists in multiple translations. Add items afterwards " +
|
|
1998
|
+
"with menu_set_items. If `translationOf` is set, `locale` must also be set.",
|
|
1999
|
+
// `locale`-when-`translationOf` is enforced inside handleMenuCreate
|
|
2000
|
+
// so REST/SDK callers get the same guard. The description above
|
|
2001
|
+
// documents the rule; the handler returns VALIDATION_ERROR.
|
|
1938
2002
|
inputSchema: z.object({
|
|
1939
2003
|
name: z
|
|
1940
2004
|
.string()
|
|
1941
2005
|
.regex(COLLECTION_SLUG_PATTERN)
|
|
1942
2006
|
.describe("Stable identifier (lowercase letters, numbers, underscores)"),
|
|
1943
2007
|
label: z.string().describe("Display name for the admin"),
|
|
2008
|
+
locale: z.string().optional().describe("Locale for this menu (e.g. 'fr-fr')"),
|
|
2009
|
+
translationOf: z
|
|
2010
|
+
.string()
|
|
2011
|
+
.optional()
|
|
2012
|
+
.describe("Existing menu id to create this locale variant from"),
|
|
1944
2013
|
}),
|
|
1945
2014
|
},
|
|
1946
2015
|
async (args, extra) => {
|
|
@@ -1949,7 +2018,14 @@ export function createMcpServer(): McpServer {
|
|
|
1949
2018
|
const ec = getEmDash(extra);
|
|
1950
2019
|
try {
|
|
1951
2020
|
const { handleMenuCreate } = await import("../api/handlers/menus.js");
|
|
1952
|
-
return unwrap(
|
|
2021
|
+
return unwrap(
|
|
2022
|
+
await handleMenuCreate(ec.db, {
|
|
2023
|
+
name: args.name,
|
|
2024
|
+
label: args.label,
|
|
2025
|
+
locale: args.locale,
|
|
2026
|
+
translationOf: args.translationOf,
|
|
2027
|
+
}),
|
|
2028
|
+
);
|
|
1953
2029
|
} catch (error) {
|
|
1954
2030
|
return respondHandlerError(error, "MENU_CREATE_ERROR");
|
|
1955
2031
|
}
|
|
@@ -1960,10 +2036,13 @@ export function createMcpServer(): McpServer {
|
|
|
1960
2036
|
"menu_update",
|
|
1961
2037
|
{
|
|
1962
2038
|
title: "Update Menu",
|
|
1963
|
-
description:
|
|
2039
|
+
description:
|
|
2040
|
+
"Update a menu's label. The `name` (stable identifier) cannot be changed. " +
|
|
2041
|
+
"On multi-locale installs, pass `locale` so the correct translation is updated.",
|
|
1964
2042
|
inputSchema: z.object({
|
|
1965
2043
|
name: z.string().describe("Menu name to update"),
|
|
1966
2044
|
label: z.string().describe("New display label"),
|
|
2045
|
+
locale: z.string().optional().describe("Locale of the menu to update"),
|
|
1967
2046
|
}),
|
|
1968
2047
|
},
|
|
1969
2048
|
async (args, extra) => {
|
|
@@ -1972,7 +2051,9 @@ export function createMcpServer(): McpServer {
|
|
|
1972
2051
|
const ec = getEmDash(extra);
|
|
1973
2052
|
try {
|
|
1974
2053
|
const { handleMenuUpdate } = await import("../api/handlers/menus.js");
|
|
1975
|
-
return unwrap(
|
|
2054
|
+
return unwrap(
|
|
2055
|
+
await handleMenuUpdate(ec.db, args.name, { label: args.label, locale: args.locale }),
|
|
2056
|
+
);
|
|
1976
2057
|
} catch (error) {
|
|
1977
2058
|
return respondHandlerError(error, "MENU_UPDATE_ERROR");
|
|
1978
2059
|
}
|
|
@@ -1983,9 +2064,12 @@ export function createMcpServer(): McpServer {
|
|
|
1983
2064
|
"menu_delete",
|
|
1984
2065
|
{
|
|
1985
2066
|
title: "Delete Menu",
|
|
1986
|
-
description:
|
|
2067
|
+
description:
|
|
2068
|
+
"Delete a menu. Items are also removed. Cannot be undone. On multi-locale " +
|
|
2069
|
+
"installs, pass `locale` so only the intended translation is removed.",
|
|
1987
2070
|
inputSchema: z.object({
|
|
1988
2071
|
name: z.string().describe("Menu name to delete"),
|
|
2072
|
+
locale: z.string().optional().describe("Locale of the menu to delete"),
|
|
1989
2073
|
}),
|
|
1990
2074
|
annotations: { destructiveHint: true },
|
|
1991
2075
|
},
|
|
@@ -1995,7 +2079,7 @@ export function createMcpServer(): McpServer {
|
|
|
1995
2079
|
const ec = getEmDash(extra);
|
|
1996
2080
|
try {
|
|
1997
2081
|
const { handleMenuDelete } = await import("../api/handlers/menus.js");
|
|
1998
|
-
return unwrap(await handleMenuDelete(ec.db, args.name));
|
|
2082
|
+
return unwrap(await handleMenuDelete(ec.db, args.name, { locale: args.locale }));
|
|
1999
2083
|
} catch (error) {
|
|
2000
2084
|
return respondHandlerError(error, "MENU_DELETE_ERROR");
|
|
2001
2085
|
}
|
|
@@ -2010,9 +2094,11 @@ export function createMcpServer(): McpServer {
|
|
|
2010
2094
|
"Replace the entire item list of a menu in one call. This is atomic: the " +
|
|
2011
2095
|
"existing items are deleted and the new list is inserted in the order " +
|
|
2012
2096
|
"provided. Use this rather than per-item add/remove tools so the resulting " +
|
|
2013
|
-
"order and parent links are unambiguous."
|
|
2097
|
+
"order and parent links are unambiguous. On multi-locale installs, pass " +
|
|
2098
|
+
"`locale` so only the intended translation is rewritten.",
|
|
2014
2099
|
inputSchema: z.object({
|
|
2015
2100
|
name: z.string().describe("Menu name to update"),
|
|
2101
|
+
locale: z.string().optional().describe("Locale of the menu to rewrite"),
|
|
2016
2102
|
items: z
|
|
2017
2103
|
.array(
|
|
2018
2104
|
z.object({
|
|
@@ -2056,7 +2142,9 @@ export function createMcpServer(): McpServer {
|
|
|
2056
2142
|
const ec = getEmDash(extra);
|
|
2057
2143
|
try {
|
|
2058
2144
|
const { handleMenuSetItems } = await import("../api/handlers/menus.js");
|
|
2059
|
-
return unwrap(
|
|
2145
|
+
return unwrap(
|
|
2146
|
+
await handleMenuSetItems(ec.db, args.name, args.items, { locale: args.locale }),
|
|
2147
|
+
);
|
|
2060
2148
|
} catch (error) {
|
|
2061
2149
|
return respondHandlerError(error, "MENU_SET_ITEMS_ERROR");
|
|
2062
2150
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function normalizeMime(mime: string): string {
|
|
2
|
+
return mime.split(";")[0]!.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function matchesMimeAllowlist(mime: string, allowList: readonly string[]): boolean {
|
|
6
|
+
const normalized = normalizeMime(mime);
|
|
7
|
+
for (const entry of allowList) {
|
|
8
|
+
if (!entry || !entry.includes("/")) continue;
|
|
9
|
+
const normalizedEntry = normalizeMime(entry);
|
|
10
|
+
if (normalizedEntry.endsWith("/")) {
|
|
11
|
+
if (normalized.startsWith(normalizedEntry)) return true;
|
|
12
|
+
} else if (normalized === normalizedEntry) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const EXTENSION_TO_MIME: Readonly<Record<string, string>> = {
|
|
20
|
+
".pdf": "application/pdf",
|
|
21
|
+
".png": "image/png",
|
|
22
|
+
".jpg": "image/jpeg",
|
|
23
|
+
".jpeg": "image/jpeg",
|
|
24
|
+
".gif": "image/gif",
|
|
25
|
+
".webp": "image/webp",
|
|
26
|
+
".svg": "image/svg+xml",
|
|
27
|
+
".mp3": "audio/mpeg",
|
|
28
|
+
".wav": "audio/wav",
|
|
29
|
+
".mp4": "video/mp4",
|
|
30
|
+
".webm": "video/webm",
|
|
31
|
+
".zip": "application/zip",
|
|
32
|
+
".tar": "application/x-tar",
|
|
33
|
+
".gz": "application/gzip",
|
|
34
|
+
".csv": "text/csv",
|
|
35
|
+
".doc": "application/msword",
|
|
36
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
37
|
+
".xls": "application/vnd.ms-excel",
|
|
38
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
39
|
+
".txt": "text/plain",
|
|
40
|
+
".rtf": "application/rtf",
|
|
41
|
+
".vtt": "text/vtt",
|
|
42
|
+
".srt": "application/x-subrip",
|
|
43
|
+
".woff": "font/woff",
|
|
44
|
+
".woff2": "font/woff2",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const VALID_MIME_RE = /^[a-z0-9][a-z0-9!#$&^_+\-.]*\/[a-z0-9!#$&^_+\-.]*$/i;
|
|
48
|
+
|
|
49
|
+
export function expandExtensionShorthand(entry: string): string | null {
|
|
50
|
+
const trimmed = entry.trim();
|
|
51
|
+
if (!trimmed) return null;
|
|
52
|
+
if (trimmed.includes("/")) return VALID_MIME_RE.test(trimmed) ? trimmed : null;
|
|
53
|
+
if (trimmed.startsWith(".")) {
|
|
54
|
+
return EXTENSION_TO_MIME[trimmed.toLowerCase()] ?? null;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Extract the `allowedMimeTypes` list from a `_emdash_fields.validation` row
|
|
61
|
+
* (raw JSON string). Returns null when the value is missing, malformed, or the
|
|
62
|
+
* list is empty — callers treat that as "no field-specific constraint".
|
|
63
|
+
*/
|
|
64
|
+
export function parseAllowedMimeTypes(rawValidation: string | null | undefined): string[] | null {
|
|
65
|
+
if (!rawValidation) return null;
|
|
66
|
+
try {
|
|
67
|
+
const parsed: unknown = JSON.parse(rawValidation);
|
|
68
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
69
|
+
const list = (parsed as { allowedMimeTypes?: unknown }).allowedMimeTypes;
|
|
70
|
+
if (!Array.isArray(list) || list.length === 0) return null;
|
|
71
|
+
return list.filter((entry): entry is string => typeof entry === "string");
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|