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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { Kysely } from "kysely";
|
|
2
|
+
|
|
3
|
+
import type { Database } from "../../database/types.js";
|
|
4
|
+
import { matchesMimeAllowlist, parseAllowedMimeTypes } from "../../media/mime.js";
|
|
5
|
+
import { requestCached } from "../../request-cache.js";
|
|
6
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
7
|
+
import type { ApiResult } from "../types.js";
|
|
8
|
+
|
|
9
|
+
interface FieldRow {
|
|
10
|
+
slug: string;
|
|
11
|
+
type: string;
|
|
12
|
+
allowedMimeTypes: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MediaRefValue {
|
|
16
|
+
id?: unknown;
|
|
17
|
+
provider?: unknown;
|
|
18
|
+
mimeType?: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function asMediaRef(value: unknown): MediaRefValue | null {
|
|
22
|
+
if (value === null || value === undefined) return null;
|
|
23
|
+
if (typeof value !== "object" || Array.isArray(value)) return null;
|
|
24
|
+
return value as MediaRefValue;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fail(message: string): ApiResult<never> {
|
|
28
|
+
return { success: false, error: { code: "INVALID_MIME_FOR_FIELD", message } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function loadMediaFieldsForCollection(
|
|
32
|
+
db: Kysely<Database>,
|
|
33
|
+
collectionSlug: string,
|
|
34
|
+
): Promise<FieldRow[]> {
|
|
35
|
+
const rows = await db
|
|
36
|
+
.selectFrom("_emdash_fields")
|
|
37
|
+
.innerJoin("_emdash_collections", "_emdash_collections.id", "_emdash_fields.collection_id")
|
|
38
|
+
.select(["_emdash_fields.slug", "_emdash_fields.type", "_emdash_fields.validation"])
|
|
39
|
+
.where("_emdash_collections.slug", "=", collectionSlug)
|
|
40
|
+
.where("_emdash_fields.type", "in", ["file", "image"])
|
|
41
|
+
.execute();
|
|
42
|
+
|
|
43
|
+
const out: FieldRow[] = [];
|
|
44
|
+
for (const row of rows) {
|
|
45
|
+
const list = parseAllowedMimeTypes(row.validation);
|
|
46
|
+
if (!list) continue;
|
|
47
|
+
out.push({ slug: row.slug, type: row.type, allowedMimeTypes: list });
|
|
48
|
+
}
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function validateMediaFields(
|
|
53
|
+
db: Kysely<Database>,
|
|
54
|
+
collectionSlug: string,
|
|
55
|
+
data: Record<string, unknown>,
|
|
56
|
+
): Promise<ApiResult<true>> {
|
|
57
|
+
// Cache is keyed on slug only. If a handler creates/modifies a field and
|
|
58
|
+
// then writes content in the same request (e.g. bulk import), the cached
|
|
59
|
+
// list will be stale for that request. This is an edge case in normal use.
|
|
60
|
+
const fields = await requestCached(`mediaFields:${collectionSlug}`, () =>
|
|
61
|
+
loadMediaFieldsForCollection(db, collectionSlug),
|
|
62
|
+
);
|
|
63
|
+
if (fields.length === 0) return { success: true, data: true };
|
|
64
|
+
|
|
65
|
+
// Collect local media ids that need a MIME lookup
|
|
66
|
+
const localIds = new Set<string>();
|
|
67
|
+
for (const field of fields) {
|
|
68
|
+
const ref = asMediaRef(data[field.slug]);
|
|
69
|
+
if (!ref) continue;
|
|
70
|
+
const provider = typeof ref.provider === "string" ? ref.provider : "local";
|
|
71
|
+
if (provider === "local" && typeof ref.id === "string") {
|
|
72
|
+
localIds.add(ref.id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Batch-load local media MIMEs
|
|
77
|
+
const idList = [...localIds];
|
|
78
|
+
const mimeById = new Map<string, string>();
|
|
79
|
+
if (idList.length > 0) {
|
|
80
|
+
for (const batch of chunks(idList, SQL_BATCH_SIZE)) {
|
|
81
|
+
const rows = await db
|
|
82
|
+
.selectFrom("media")
|
|
83
|
+
.select(["id", "mime_type"])
|
|
84
|
+
.where("id", "in", batch)
|
|
85
|
+
.execute();
|
|
86
|
+
for (const r of rows) mimeById.set(r.id, r.mime_type);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const field of fields) {
|
|
91
|
+
const value = data[field.slug];
|
|
92
|
+
if (value === null || value === undefined) continue;
|
|
93
|
+
const ref = asMediaRef(value);
|
|
94
|
+
if (!ref) continue;
|
|
95
|
+
|
|
96
|
+
const provider = typeof ref.provider === "string" ? ref.provider : "local";
|
|
97
|
+
|
|
98
|
+
// External providers carry mimeType in the ref; trust it as-is.
|
|
99
|
+
// Local media: look up the stored mimeType by id.
|
|
100
|
+
let mime: string | undefined;
|
|
101
|
+
if (provider === "local") {
|
|
102
|
+
if (typeof ref.id !== "string") {
|
|
103
|
+
return fail(`Field '${field.slug}' references media with an invalid id`);
|
|
104
|
+
}
|
|
105
|
+
mime = mimeById.get(ref.id);
|
|
106
|
+
if (!mime) {
|
|
107
|
+
return fail(`Field '${field.slug}' references media with unknown MIME type`);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
if (typeof ref.mimeType !== "string") {
|
|
111
|
+
return fail(`Field '${field.slug}' requires a mimeType declaration for non-local media`);
|
|
112
|
+
}
|
|
113
|
+
// TODO: long-term, consider a server-side HEAD probe or provider-vouched
|
|
114
|
+
// MIMEs for non-local refs; for now the constraint is only as strong as
|
|
115
|
+
// the client that constructed the ref.
|
|
116
|
+
mime = ref.mimeType;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!matchesMimeAllowlist(mime, field.allowedMimeTypes)) {
|
|
120
|
+
return fail(`Field '${field.slug}' does not accept ${mime}`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return { success: true, data: true };
|
|
125
|
+
}
|
|
@@ -59,6 +59,13 @@ export const localeCode = z
|
|
|
59
59
|
.regex(/^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i, "Invalid locale code")
|
|
60
60
|
.transform((v) => v.toLowerCase());
|
|
61
61
|
|
|
62
|
+
/** Shared `?locale=xx` query shape for endpoints that filter by locale. */
|
|
63
|
+
export const localeFilterQuery = z
|
|
64
|
+
.object({
|
|
65
|
+
locale: z.string().min(1).optional(),
|
|
66
|
+
})
|
|
67
|
+
.meta({ id: "LocaleFilterQuery" });
|
|
68
|
+
|
|
62
69
|
// ---------------------------------------------------------------------------
|
|
63
70
|
// OpenAPI: Shared response schemas
|
|
64
71
|
// ---------------------------------------------------------------------------
|
package/src/api/schemas/media.ts
CHANGED
|
@@ -6,9 +6,21 @@ import { cursorPaginationQuery } from "./common.js";
|
|
|
6
6
|
// Media: Input schemas
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Accepts a comma-separated string (from URL query params) or an array of
|
|
11
|
+
* strings (from JSON body or programmatic use) and normalises to string[].
|
|
12
|
+
*/
|
|
13
|
+
const mimeTypeFilter = z
|
|
14
|
+
.union([z.string(), z.array(z.string())])
|
|
15
|
+
.transform((v) => {
|
|
16
|
+
const arr = Array.isArray(v) ? v : v.split(",");
|
|
17
|
+
return arr.map((s) => s.trim()).filter((s) => s.length > 0);
|
|
18
|
+
})
|
|
19
|
+
.optional();
|
|
20
|
+
|
|
9
21
|
export const mediaListQuery = cursorPaginationQuery
|
|
10
22
|
.extend({
|
|
11
|
-
mimeType:
|
|
23
|
+
mimeType: mimeTypeFilter,
|
|
12
24
|
})
|
|
13
25
|
.meta({ id: "MediaListQuery" });
|
|
14
26
|
|
|
@@ -30,6 +42,10 @@ export function formatFileSize(bytes: number): string {
|
|
|
30
42
|
return `${Math.floor(bytes / 1024 / 1024)}MB`;
|
|
31
43
|
}
|
|
32
44
|
|
|
45
|
+
// Matches a full MIME type (type/subtype) with an optional semicolon-delimited
|
|
46
|
+
// parameter section. Forbids CR/LF to prevent header injection.
|
|
47
|
+
const CONTENT_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_+\-.]*\/[a-z0-9!#$&^_+\-.]+(\s*;[^\r\n]*)?$/i;
|
|
48
|
+
|
|
33
49
|
export function mediaUploadUrlBody(maxSize: number) {
|
|
34
50
|
if (!Number.isFinite(maxSize) || maxSize <= 0) {
|
|
35
51
|
throw new Error(`EmDash: maxUploadSize must be a positive finite number, got ${maxSize}`);
|
|
@@ -37,13 +53,17 @@ export function mediaUploadUrlBody(maxSize: number) {
|
|
|
37
53
|
return z
|
|
38
54
|
.object({
|
|
39
55
|
filename: z.string().min(1, "filename is required"),
|
|
40
|
-
contentType: z
|
|
56
|
+
contentType: z
|
|
57
|
+
.string()
|
|
58
|
+
.min(1, "contentType is required")
|
|
59
|
+
.regex(CONTENT_TYPE_RE, "Invalid content type"),
|
|
41
60
|
size: z
|
|
42
61
|
.number()
|
|
43
62
|
.int()
|
|
44
63
|
.positive()
|
|
45
64
|
.max(maxSize, `File size must not exceed ${formatFileSize(maxSize)}`),
|
|
46
65
|
contentHash: z.string().optional(),
|
|
66
|
+
fieldId: z.string().optional(),
|
|
47
67
|
})
|
|
48
68
|
.meta({ id: "MediaUploadUrlBody" });
|
|
49
69
|
}
|
|
@@ -59,7 +79,7 @@ export const mediaConfirmBody = z
|
|
|
59
79
|
export const mediaProviderListQuery = cursorPaginationQuery
|
|
60
80
|
.extend({
|
|
61
81
|
query: z.string().optional(),
|
|
62
|
-
mimeType:
|
|
82
|
+
mimeType: mimeTypeFilter,
|
|
63
83
|
})
|
|
64
84
|
.meta({ id: "MediaProviderListQuery" });
|
|
65
85
|
|
package/src/api/schemas/menus.ts
CHANGED
|
@@ -20,6 +20,10 @@ export const createMenuBody = z
|
|
|
20
20
|
.object({
|
|
21
21
|
name: z.string().min(1),
|
|
22
22
|
label: z.string().min(1),
|
|
23
|
+
locale: z.string().min(1).optional(),
|
|
24
|
+
/** When set, clones the items from the source menu. The new menu joins
|
|
25
|
+
* the source's translation_group. */
|
|
26
|
+
translationOf: z.string().min(1).optional(),
|
|
23
27
|
})
|
|
24
28
|
.meta({ id: "CreateMenuBody" });
|
|
25
29
|
|
|
@@ -87,6 +91,8 @@ export const menuSchema = z
|
|
|
87
91
|
label: z.string(),
|
|
88
92
|
created_at: z.string(),
|
|
89
93
|
updated_at: z.string(),
|
|
94
|
+
locale: z.string(),
|
|
95
|
+
translation_group: z.string().nullable(),
|
|
90
96
|
})
|
|
91
97
|
.meta({ id: "Menu" });
|
|
92
98
|
|
|
@@ -105,9 +111,26 @@ export const menuItemSchema = z
|
|
|
105
111
|
target: z.string().nullable(),
|
|
106
112
|
css_classes: z.string().nullable(),
|
|
107
113
|
created_at: z.string(),
|
|
114
|
+
locale: z.string(),
|
|
115
|
+
translation_group: z.string().nullable(),
|
|
108
116
|
})
|
|
109
117
|
.meta({ id: "MenuItem" });
|
|
110
118
|
|
|
119
|
+
export const menuTranslationsSchema = z
|
|
120
|
+
.object({
|
|
121
|
+
translationGroup: z.string().nullable(),
|
|
122
|
+
translations: z.array(
|
|
123
|
+
z.object({
|
|
124
|
+
id: z.string(),
|
|
125
|
+
name: z.string(),
|
|
126
|
+
label: z.string(),
|
|
127
|
+
locale: z.string(),
|
|
128
|
+
updatedAt: z.string(),
|
|
129
|
+
}),
|
|
130
|
+
),
|
|
131
|
+
})
|
|
132
|
+
.meta({ id: "MenuTranslations" });
|
|
133
|
+
|
|
111
134
|
export const menuListItemSchema = menuSchema
|
|
112
135
|
.extend({
|
|
113
136
|
itemCount: z.number().int(),
|
|
@@ -49,6 +49,15 @@ const fieldValidation = z
|
|
|
49
49
|
subFields: z.array(repeaterSubFieldSchema).min(1).optional(),
|
|
50
50
|
minItems: z.number().int().min(0).optional(),
|
|
51
51
|
maxItems: z.number().int().min(1).optional(),
|
|
52
|
+
allowedMimeTypes: z
|
|
53
|
+
.array(
|
|
54
|
+
z
|
|
55
|
+
.string()
|
|
56
|
+
.regex(/^[a-z0-9][a-z0-9!#$&^_+\-.]*\/[a-z0-9!#$&^_+\-.]*$/i, "Invalid MIME type"),
|
|
57
|
+
)
|
|
58
|
+
.min(1, "allowedMimeTypes must not be empty — omit the field to allow all types")
|
|
59
|
+
.max(64, "allowedMimeTypes may contain at most 64 entries")
|
|
60
|
+
.optional(),
|
|
52
61
|
})
|
|
53
62
|
.optional();
|
|
54
63
|
|
|
@@ -92,7 +101,7 @@ export const createFieldBody = z
|
|
|
92
101
|
required: z.boolean().optional(),
|
|
93
102
|
unique: z.boolean().optional(),
|
|
94
103
|
defaultValue: z.unknown().optional(),
|
|
95
|
-
validation: fieldValidation,
|
|
104
|
+
validation: fieldValidation.nullable(),
|
|
96
105
|
widget: z.string().optional(),
|
|
97
106
|
options: fieldWidgetOptions,
|
|
98
107
|
sortOrder: z.number().int().min(0).optional(),
|
|
@@ -107,7 +116,7 @@ export const updateFieldBody = z
|
|
|
107
116
|
required: z.boolean().optional(),
|
|
108
117
|
unique: z.boolean().optional(),
|
|
109
118
|
defaultValue: z.unknown().optional(),
|
|
110
|
-
validation: fieldValidation,
|
|
119
|
+
validation: fieldValidation.nullable(),
|
|
111
120
|
widget: z.string().optional(),
|
|
112
121
|
options: fieldWidgetOptions,
|
|
113
122
|
sortOrder: z.number().int().min(0).optional(),
|
|
@@ -15,6 +15,7 @@ export const createTaxonomyDefBody = z
|
|
|
15
15
|
.max(63)
|
|
16
16
|
.regex(/^[a-z][a-z0-9_]*$/, "Name must be lowercase alphanumeric with underscores"),
|
|
17
17
|
label: z.string().min(1).max(200),
|
|
18
|
+
labelSingular: z.string().min(1).max(200).optional(),
|
|
18
19
|
hierarchical: z.boolean().optional().default(false),
|
|
19
20
|
collections: z
|
|
20
21
|
.array(
|
|
@@ -23,6 +24,8 @@ export const createTaxonomyDefBody = z
|
|
|
23
24
|
.max(100)
|
|
24
25
|
.optional()
|
|
25
26
|
.default([]),
|
|
27
|
+
locale: z.string().min(1).optional(),
|
|
28
|
+
translationOf: z.string().min(1).optional(),
|
|
26
29
|
})
|
|
27
30
|
.meta({ id: "CreateTaxonomyDefBody" });
|
|
28
31
|
|
|
@@ -36,6 +39,8 @@ export const createTermBody = z
|
|
|
36
39
|
label: z.string().min(1),
|
|
37
40
|
parentId: z.string().nullish(),
|
|
38
41
|
description: z.string().optional(),
|
|
42
|
+
locale: z.string().min(1).optional(),
|
|
43
|
+
translationOf: z.string().min(1).optional(),
|
|
39
44
|
})
|
|
40
45
|
.meta({ id: "CreateTermBody" });
|
|
41
46
|
|
|
@@ -60,9 +65,25 @@ export const taxonomyDefSchema = z
|
|
|
60
65
|
labelSingular: z.string().optional(),
|
|
61
66
|
hierarchical: z.boolean(),
|
|
62
67
|
collections: z.array(z.string()),
|
|
68
|
+
locale: z.string(),
|
|
69
|
+
translationGroup: z.string().nullable(),
|
|
63
70
|
})
|
|
64
71
|
.meta({ id: "TaxonomyDef" });
|
|
65
72
|
|
|
73
|
+
export const taxonomyDefTranslationsSchema = z
|
|
74
|
+
.object({
|
|
75
|
+
translationGroup: z.string().nullable(),
|
|
76
|
+
translations: z.array(
|
|
77
|
+
z.object({
|
|
78
|
+
id: z.string(),
|
|
79
|
+
name: z.string(),
|
|
80
|
+
label: z.string(),
|
|
81
|
+
locale: z.string(),
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
})
|
|
85
|
+
.meta({ id: "TaxonomyDefTranslations" });
|
|
86
|
+
|
|
66
87
|
export const taxonomyListResponseSchema = z
|
|
67
88
|
.object({ taxonomies: z.array(taxonomyDefSchema) })
|
|
68
89
|
.meta({ id: "TaxonomyListResponse" });
|
|
@@ -75,9 +96,25 @@ export const termSchema = z
|
|
|
75
96
|
label: z.string(),
|
|
76
97
|
parentId: z.string().nullable(),
|
|
77
98
|
description: z.string().optional(),
|
|
99
|
+
locale: z.string(),
|
|
100
|
+
translationGroup: z.string().nullable(),
|
|
78
101
|
})
|
|
79
102
|
.meta({ id: "Term" });
|
|
80
103
|
|
|
104
|
+
export const termTranslationsSchema = z
|
|
105
|
+
.object({
|
|
106
|
+
translationGroup: z.string().nullable(),
|
|
107
|
+
translations: z.array(
|
|
108
|
+
z.object({
|
|
109
|
+
id: z.string(),
|
|
110
|
+
slug: z.string(),
|
|
111
|
+
label: z.string(),
|
|
112
|
+
locale: z.string(),
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
})
|
|
116
|
+
.meta({ id: "TermTranslations" });
|
|
117
|
+
|
|
81
118
|
export const termWithCountSchema: z.ZodType = z
|
|
82
119
|
.object({
|
|
83
120
|
id: z.string(),
|
|
@@ -88,6 +125,8 @@ export const termWithCountSchema: z.ZodType = z
|
|
|
88
125
|
description: z.string().optional(),
|
|
89
126
|
count: z.number().int(),
|
|
90
127
|
children: z.array(z.lazy(() => termWithCountSchema)),
|
|
128
|
+
locale: z.string(),
|
|
129
|
+
translationGroup: z.string().nullable(),
|
|
91
130
|
})
|
|
92
131
|
.meta({ id: "TermWithCount" });
|
|
93
132
|
|
|
@@ -313,6 +313,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
313
313
|
entrypoint: resolveRoute("api/taxonomies/[name]/terms/[slug].ts"),
|
|
314
314
|
});
|
|
315
315
|
|
|
316
|
+
injectRoute({
|
|
317
|
+
pattern: "/_emdash/api/taxonomies/[name]/terms/[slug]/translations",
|
|
318
|
+
entrypoint: resolveRoute("api/taxonomies/[name]/terms/[slug]/translations.ts"),
|
|
319
|
+
});
|
|
320
|
+
|
|
316
321
|
injectRoute({
|
|
317
322
|
pattern: "/_emdash/api/content/[collection]/[id]/terms/[taxonomy]",
|
|
318
323
|
entrypoint: resolveRoute("api/content/[collection]/[id]/terms/[taxonomy].ts"),
|
|
@@ -555,6 +560,11 @@ export function injectCoreRoutes(injectRoute: InjectRoute): void {
|
|
|
555
560
|
entrypoint: resolveRoute("api/menus/[name]/reorder.ts"),
|
|
556
561
|
});
|
|
557
562
|
|
|
563
|
+
injectRoute({
|
|
564
|
+
pattern: "/_emdash/api/menus/[name]/translations",
|
|
565
|
+
entrypoint: resolveRoute("api/menus/[name]/translations.ts"),
|
|
566
|
+
});
|
|
567
|
+
|
|
558
568
|
// Widget area routes
|
|
559
569
|
injectRoute({
|
|
560
570
|
pattern: "/_emdash/api/widget-areas",
|
package/src/astro/middleware.ts
CHANGED
|
@@ -47,7 +47,12 @@ import { createPublicMediaUrlResolver } from "../media/url.js";
|
|
|
47
47
|
import type { SandboxRunner } from "../plugins/sandbox/types.js";
|
|
48
48
|
import type { ResolvedPlugin } from "../plugins/types.js";
|
|
49
49
|
import { invalidateUrlPatternCache } from "../query.js";
|
|
50
|
-
import {
|
|
50
|
+
import {
|
|
51
|
+
createRequestMetrics,
|
|
52
|
+
getRequestContext,
|
|
53
|
+
type RequestMetrics,
|
|
54
|
+
runWithContext,
|
|
55
|
+
} from "../request-context.js";
|
|
51
56
|
import type { EmDashConfig } from "./integration/runtime.js";
|
|
52
57
|
import type { EmDashHandlers } from "./types.js";
|
|
53
58
|
|
|
@@ -209,6 +214,33 @@ function finalizeResponse(
|
|
|
209
214
|
return res;
|
|
210
215
|
}
|
|
211
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Append always-on counters (db.*, cache.*) to the Server-Timing list.
|
|
219
|
+
*
|
|
220
|
+
* dur values for `count`, `hit`, `miss` are integer counts — Server-Timing
|
|
221
|
+
* spec only models milliseconds, but browsers show whatever number is given,
|
|
222
|
+
* which is the convention most projects use for non-time samples.
|
|
223
|
+
*/
|
|
224
|
+
function pushMetricsTimings(
|
|
225
|
+
timings: Array<{ name: string; dur: number; desc?: string }>,
|
|
226
|
+
metrics: RequestMetrics,
|
|
227
|
+
): void {
|
|
228
|
+
if (metrics.dbCount > 0) {
|
|
229
|
+
timings.push({ name: "db.total", dur: metrics.dbTotalMs, desc: "DB total" });
|
|
230
|
+
timings.push({ name: "db.count", dur: metrics.dbCount, desc: "Query count" });
|
|
231
|
+
if (metrics.dbFirstOffset !== null) {
|
|
232
|
+
timings.push({ name: "db.first", dur: metrics.dbFirstOffset, desc: "First query at" });
|
|
233
|
+
}
|
|
234
|
+
if (metrics.dbLastOffset !== null) {
|
|
235
|
+
timings.push({ name: "db.last", dur: metrics.dbLastOffset, desc: "Last query at" });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (metrics.cacheHits + metrics.cacheMisses > 0) {
|
|
239
|
+
timings.push({ name: "cache.hit", dur: metrics.cacheHits, desc: "Cache hits" });
|
|
240
|
+
timings.push({ name: "cache.miss", dur: metrics.cacheMisses, desc: "Cache misses" });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
212
244
|
/** Public routes that require the runtime (sitemap, robots.txt, etc.) */
|
|
213
245
|
const PUBLIC_RUNTIME_ROUTES = new Set(["/sitemap.xml", "/robots.txt"]);
|
|
214
246
|
const SITEMAP_COLLECTION_RE = /^\/sitemap-[a-z][a-z0-9_]*\.xml$/;
|
|
@@ -252,6 +284,8 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
252
284
|
? createRecorder(url.pathname, request.method, request.headers.get("x-perf-phase") ?? "default")
|
|
253
285
|
: undefined;
|
|
254
286
|
|
|
287
|
+
const metrics = createRequestMetrics(performance.now());
|
|
288
|
+
|
|
255
289
|
const run = async (): Promise<Response> => {
|
|
256
290
|
// Process /_emdash routes and public routes with an active session
|
|
257
291
|
// (logged-in editors need the runtime for toolbar/visual editing on public pages)
|
|
@@ -355,13 +389,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
355
389
|
const response = await next();
|
|
356
390
|
timings.push({ name: "render", dur: performance.now() - t0, desc: "Page render" });
|
|
357
391
|
timings.push({ name: "mw", dur: performance.now() - mwStart, desc: "Total middleware" });
|
|
392
|
+
pushMetricsTimings(timings, metrics);
|
|
358
393
|
return finalizeResponse(response, timings);
|
|
359
394
|
};
|
|
360
395
|
if (anonScoped) {
|
|
361
396
|
const parent = getRequestContext();
|
|
362
397
|
const ctx = parent
|
|
363
398
|
? { ...parent, db: anonScoped.db }
|
|
364
|
-
: { editMode: false, db: anonScoped.db };
|
|
399
|
+
: { editMode: false, db: anonScoped.db, metrics };
|
|
365
400
|
return runWithContext(ctx, async () => {
|
|
366
401
|
const response = await runAnon();
|
|
367
402
|
anonScoped.commit();
|
|
@@ -516,12 +551,15 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
516
551
|
const response = await next();
|
|
517
552
|
timings.push({ name: "render", dur: performance.now() - t0, desc: "Page render" });
|
|
518
553
|
timings.push({ name: "mw", dur: performance.now() - mwStart, desc: "Total middleware" });
|
|
554
|
+
pushMetricsTimings(timings, metrics);
|
|
519
555
|
return finalizeResponse(response, timings);
|
|
520
556
|
};
|
|
521
557
|
|
|
522
558
|
if (scoped) {
|
|
523
559
|
const parent = getRequestContext();
|
|
524
|
-
const ctx = parent
|
|
560
|
+
const ctx = parent
|
|
561
|
+
? { ...parent, db: scoped.db }
|
|
562
|
+
: { editMode: false, db: scoped.db, metrics };
|
|
525
563
|
return runWithContext(ctx, async () => {
|
|
526
564
|
const response = await renderAndFinalize();
|
|
527
565
|
scoped.commit();
|
|
@@ -542,20 +580,17 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
542
580
|
const parent = getRequestContext();
|
|
543
581
|
const ctx = parent
|
|
544
582
|
? { ...parent, editMode, db: playgroundDb, dbIsIsolated: true }
|
|
545
|
-
: { editMode, db: playgroundDb, dbIsIsolated: true };
|
|
583
|
+
: { editMode, db: playgroundDb, dbIsIsolated: true, metrics };
|
|
546
584
|
return runWithContext(ctx, doInit);
|
|
547
585
|
}
|
|
548
586
|
return doInit();
|
|
549
587
|
};
|
|
550
588
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
flushRecorder(queryRecorder);
|
|
556
|
-
}
|
|
589
|
+
try {
|
|
590
|
+
return await runWithContext({ editMode: false, queryRecorder, metrics }, run);
|
|
591
|
+
} finally {
|
|
592
|
+
if (queryRecorder) flushRecorder(queryRecorder);
|
|
557
593
|
}
|
|
558
|
-
return run();
|
|
559
594
|
});
|
|
560
595
|
|
|
561
596
|
export default onRequest;
|
|
@@ -16,7 +16,7 @@ export const DELETE: APIRoute = async ({ params, locals, cache }) => {
|
|
|
16
16
|
const collection = params.collection!;
|
|
17
17
|
const id = params.id!;
|
|
18
18
|
|
|
19
|
-
const denied = requirePerm(user, "
|
|
19
|
+
const denied = requirePerm(user, "content:delete_permanent");
|
|
20
20
|
if (denied) return denied;
|
|
21
21
|
|
|
22
22
|
if (!emdash?.handleContentPermanentDelete) {
|