emdash 0.6.0 → 0.8.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-Di31kZ28.d.mts → adapters-BKSf3T9R.d.mts} +1 -1
- package/dist/{adapters-Di31kZ28.d.mts.map → adapters-BKSf3T9R.d.mts.map} +1 -1
- package/dist/{apply-B4MsLM-w.mjs → apply-x0eMK1lX.mjs} +186 -28
- package/dist/apply-x0eMK1lX.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +92 -17
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +22 -2
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +7 -2
- package/dist/astro/middleware/request-context.mjs.map +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +263 -74
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +25 -8
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-C4OVd8b3.mjs → byline-Chbr2GoP.mjs} +3 -3
- package/dist/byline-Chbr2GoP.mjs.map +1 -0
- package/dist/{bylines-hPTW79hw.mjs → bylines-CRNsVG88.mjs} +4 -4
- package/dist/{bylines-hPTW79hw.mjs.map → bylines-CRNsVG88.mjs.map} +1 -1
- package/dist/cli/index.mjs +17 -13
- 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/{content-BsBoyj8G.mjs → content-BcQPYxdV.mjs} +39 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{db-errors-D0UT85nC.mjs → db-errors-l1Qh2RPR.mjs} +1 -1
- package/dist/{db-errors-D0UT85nC.mjs.map → db-errors-l1Qh2RPR.mjs.map} +1 -1
- package/dist/{default-CME5YdZ3.mjs → default-DCVqE5ib.mjs} +1 -1
- package/dist/{default-CME5YdZ3.mjs.map → default-DCVqE5ib.mjs.map} +1 -1
- package/dist/{error-CiYn9yDu.mjs → error-zG5T1UGA.mjs} +1 -1
- package/dist/error-zG5T1UGA.mjs.map +1 -0
- package/dist/{index-BYv0mB9g.d.mts → index-DIb-CzNx.d.mts} +232 -15
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +23 -21
- package/dist/{load-CBcmDIot.mjs → load-CyEoextb.mjs} +1 -1
- package/dist/{load-CBcmDIot.mjs.map → load-CyEoextb.mjs.map} +1 -1
- package/dist/{loader-DeiBJEMe.mjs → loader-CndGj8kM.mjs} +8 -6
- package/dist/loader-CndGj8kM.mjs.map +1 -0
- package/dist/{manifest-schema-V30qsMft.mjs → manifest-schema-DH9xhc6t.mjs} +13 -1
- package/dist/manifest-schema-DH9xhc6t.mjs.map +1 -0
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/media/local-runtime.mjs +2 -2
- package/dist/{media-DqHVh136.mjs → media-D8FbNsl0.mjs} +4 -7
- package/dist/media-D8FbNsl0.mjs.map +1 -0
- package/dist/{mode-CpNnGkPz.mjs → mode-BnAOqItE.mjs} +1 -1
- package/dist/mode-BnAOqItE.mjs.map +1 -0
- package/dist/page/index.d.mts +2 -2
- package/dist/placeholder-C-fk5hYI.mjs.map +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts → placeholder-D29tWZ7o.d.mts} +1 -1
- package/dist/{placeholder-tzpqGWII.d.mts.map → placeholder-D29tWZ7o.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-Bk_3vKvU.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-Bk_3vKvU.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-7lGhLBNZ.mjs → redirect-D_pshWdf.mjs} +69 -13
- package/dist/redirect-D_pshWdf.mjs.map +1 -0
- package/dist/{registry-Ci3WxVAr.mjs → registry-C3Mr0ODu.mjs} +33 -9
- package/dist/registry-C3Mr0ODu.mjs.map +1 -0
- package/dist/{request-cache-DiR961CV.mjs → request-cache-Ci7f5pBb.mjs} +1 -1
- package/dist/request-cache-Ci7f5pBb.mjs.map +1 -0
- package/dist/{runner-Fl2NcUUz.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-Fl2NcUUz.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/{runner-Cd-_WyDo.mjs → runner-tQ7BJ4T7.mjs} +211 -134
- package/dist/runner-tQ7BJ4T7.mjs.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-DI4bM2w9.mjs → search-BoZYFuUk.mjs} +339 -102
- package/dist/search-BoZYFuUk.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +12 -12
- 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.d.mts.map +1 -1
- package/dist/storage/s3.mjs +4 -4
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{taxonomies-DbrKzDju.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-DbrKzDju.mjs.map → taxonomies-B4IAshV8.mjs.map} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs → tokens-D9vnZqYS.mjs} +1 -1
- package/dist/{tokens-BFPFx3CA.mjs.map → tokens-D9vnZqYS.mjs.map} +1 -1
- package/dist/{transport-BykRfpyy.mjs → transport-C9ugt2Nr.mjs} +1 -1
- package/dist/{transport-BykRfpyy.mjs.map → transport-C9ugt2Nr.mjs.map} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts → transport-CUnEL3Vs.d.mts} +1 -1
- package/dist/{transport-H4Iwx7tC.d.mts.map → transport-CUnEL3Vs.d.mts.map} +1 -1
- package/dist/types-BIgulNsW.mjs +68 -0
- package/dist/types-BIgulNsW.mjs.map +1 -0
- package/dist/{types-DDS4MxsT.mjs → types-Bm1dn-q3.mjs} +1 -1
- package/dist/{types-DDS4MxsT.mjs.map → types-Bm1dn-q3.mjs.map} +1 -1
- package/dist/{types-CnZYHyLW.d.mts → types-BmPPSUEx.d.mts} +1 -1
- package/dist/{types-CnZYHyLW.d.mts.map → types-BmPPSUEx.d.mts.map} +1 -1
- package/dist/{types-6CUZRrZP.d.mts → types-BrA0xf5I.d.mts} +24 -2
- package/dist/{types-6CUZRrZP.d.mts.map → types-BrA0xf5I.d.mts.map} +1 -1
- package/dist/{types-8xrvl_68.d.mts → types-CS8FIX7L.d.mts} +10 -1
- package/dist/{types-8xrvl_68.d.mts.map → types-CS8FIX7L.d.mts.map} +1 -1
- package/dist/{types-BH2L167P.mjs → types-CgqmmMJB.mjs} +1 -1
- package/dist/{types-BH2L167P.mjs.map → types-CgqmmMJB.mjs.map} +1 -1
- package/dist/{types-CFWjXmus.d.mts → types-DIMwPFub.d.mts} +1 -1
- package/dist/{types-CFWjXmus.d.mts.map → types-DIMwPFub.d.mts.map} +1 -1
- package/dist/{types-DgrIP0tF.d.mts → types-i36XcA_X.d.mts} +49 -6
- package/dist/types-i36XcA_X.d.mts.map +1 -0
- package/dist/{validate-CqsNItbt.mjs → validate-CxVsLehf.mjs} +2 -2
- package/dist/{validate-CqsNItbt.mjs.map → validate-CxVsLehf.mjs.map} +1 -1
- package/dist/{validate-CaLH1Ia2.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-CaLH1Ia2.d.mts.map → validate-DHxmpFJt.d.mts.map} +1 -1
- package/dist/validation-C-ZpN2GI.mjs +144 -0
- package/dist/validation-C-ZpN2GI.mjs.map +1 -0
- package/dist/version-Bbq8TCrz.mjs +7 -0
- package/dist/{version-Uaf2ynPX.mjs.map → version-Bbq8TCrz.mjs.map} +1 -1
- package/dist/zod-generator-CpwccCIv.mjs +132 -0
- package/dist/zod-generator-CpwccCIv.mjs.map +1 -0
- package/package.json +18 -5
- package/src/api/auth-storage.ts +37 -0
- package/src/api/error.ts +6 -0
- package/src/api/errors.ts +8 -0
- package/src/api/handlers/comments.ts +13 -0
- package/src/api/handlers/content.ts +124 -3
- package/src/api/handlers/index.ts +2 -0
- package/src/api/handlers/media.ts +8 -1
- package/src/api/handlers/menus.ts +160 -21
- package/src/api/handlers/redirects.ts +16 -3
- package/src/api/handlers/sections.ts +8 -1
- package/src/api/handlers/taxonomies.ts +128 -16
- package/src/api/handlers/validation.ts +212 -0
- package/src/api/openapi/document.ts +4 -1
- package/src/api/public-url.ts +6 -3
- package/src/api/route-utils.ts +14 -0
- package/src/api/schemas/common.ts +1 -1
- package/src/api/schemas/content.ts +8 -0
- package/src/api/schemas/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/font-provider.ts +3 -1
- package/src/astro/integration/index.ts +15 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +74 -2
- package/src/astro/integration/virtual-modules.ts +41 -0
- package/src/astro/integration/vite-config.ts +43 -12
- package/src/astro/middleware/auth.ts +21 -0
- package/src/astro/middleware.ts +18 -1
- package/src/astro/routes/PluginRegistry.tsx +10 -1
- package/src/astro/routes/admin.astro +14 -7
- package/src/astro/routes/api/auth/magic-link/send.ts +2 -1
- package/src/astro/routes/api/auth/mode.ts +57 -0
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +23 -3
- package/src/astro/routes/api/auth/oauth/[provider].ts +10 -4
- package/src/astro/routes/api/auth/passkey/options.ts +2 -1
- package/src/astro/routes/api/auth/signup/request.ts +26 -8
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +10 -6
- package/src/astro/routes/api/content/[collection]/[id]/compare.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/preview-url.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/revisions.ts +1 -1
- package/src/astro/routes/api/content/[collection]/[id]/translations.ts +26 -0
- package/src/astro/routes/api/content/[collection]/[id].ts +30 -2
- package/src/astro/routes/api/content/[collection]/index.ts +20 -10
- package/src/astro/routes/api/content/[collection]/trash.ts +1 -1
- package/src/astro/routes/api/import/wordpress/media.ts +2 -7
- package/src/astro/routes/api/import/wordpress/prepare.ts +10 -0
- package/src/astro/routes/api/import/wordpress-plugin/analyze.ts +4 -3
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +4 -3
- package/src/astro/routes/api/manifest.ts +7 -0
- package/src/astro/routes/api/oauth/device/code.ts +2 -1
- package/src/astro/routes/api/oauth/device/token.ts +2 -1
- package/src/astro/routes/api/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin-verify.ts +30 -5
- package/src/astro/routes/api/setup/admin.ts +38 -8
- package/src/astro/routes/api/setup/index.ts +7 -4
- package/src/astro/routes/api/setup/status.ts +3 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets/[id].ts +4 -1
- package/src/astro/routes/api/widget-areas/[name]/widgets.ts +4 -1
- package/src/astro/routes/api/widget-areas/[name].ts +4 -1
- package/src/astro/routes/api/widget-areas/index.ts +4 -1
- package/src/astro/types.ts +18 -0
- package/src/auth/mode.ts +15 -3
- package/src/auth/providers/github-admin.tsx +29 -0
- package/src/auth/providers/github.ts +31 -0
- package/src/auth/providers/google-admin.tsx +44 -0
- package/src/auth/providers/google.ts +31 -0
- package/src/auth/rate-limit.ts +50 -22
- package/src/auth/setup-nonce.ts +22 -0
- package/src/auth/trusted-proxy.ts +92 -0
- package/src/auth/types.ts +114 -4
- package/src/cli/commands/bundle.ts +3 -1
- package/src/components/EmDashImage.astro +7 -6
- package/src/components/Gallery.astro +5 -3
- package/src/components/Image.astro +8 -3
- package/src/components/InlinePortableTextEditor.tsx +2 -1
- package/src/components/LiveSearch.astro +5 -14
- package/src/database/migrations/035_bounded_404_log.ts +112 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/audit.ts +6 -8
- package/src/database/repositories/byline.ts +6 -8
- package/src/database/repositories/comment.ts +12 -16
- package/src/database/repositories/content.ts +79 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/options.ts +25 -0
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +123 -24
- package/src/database/repositories/taxonomy.ts +14 -3
- package/src/database/repositories/types.ts +57 -8
- package/src/database/repositories/user.ts +6 -8
- package/src/database/types.ts +9 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/import/registry.ts +4 -3
- package/src/import/ssrf.ts +253 -12
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +753 -107
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/context.ts +15 -3
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manager.ts +6 -0
- package/src/plugins/manifest-schema.ts +12 -0
- package/src/plugins/request-meta.ts +66 -15
- package/src/plugins/routes.ts +3 -1
- package/src/plugins/types.ts +23 -2
- package/src/query.ts +1 -1
- package/src/request-cache.ts +3 -0
- package/src/schema/registry.ts +41 -5
- package/src/search/fts-manager.ts +0 -2
- package/src/search/query.ts +111 -26
- package/src/search/types.ts +8 -1
- package/src/sections/index.ts +7 -9
- package/src/seed/apply.ts +26 -0
- package/src/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/visual-editing/toolbar.ts +6 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-B4MsLM-w.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-BsBoyj8G.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-BYv0mB9g.d.mts.map +0 -1
- package/dist/loader-DeiBJEMe.mjs.map +0 -1
- package/dist/manifest-schema-V30qsMft.mjs.map +0 -1
- package/dist/media-DqHVh136.mjs.map +0 -1
- package/dist/mode-CpNnGkPz.mjs.map +0 -1
- package/dist/redirect-7lGhLBNZ.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/runner-Cd-_WyDo.mjs.map +0 -1
- package/dist/search-DI4bM2w9.mjs.map +0 -1
- package/dist/types-CMMN0pNg.mjs +0 -31
- package/dist/types-CMMN0pNg.mjs.map +0 -1
- package/dist/types-DgrIP0tF.d.mts.map +0 -1
- package/dist/version-Uaf2ynPX.mjs +0 -7
|
@@ -14,6 +14,7 @@ import { RevisionRepository } from "../../database/repositories/revision.js";
|
|
|
14
14
|
import { SeoRepository } from "../../database/repositories/seo.js";
|
|
15
15
|
import {
|
|
16
16
|
EmDashValidationError,
|
|
17
|
+
InvalidCursorError,
|
|
17
18
|
type ContentItem,
|
|
18
19
|
type ContentSeo,
|
|
19
20
|
type ContentSeoInput,
|
|
@@ -23,9 +24,26 @@ import type { Database } from "../../database/types.js";
|
|
|
23
24
|
import { validateIdentifier } from "../../database/validate.js";
|
|
24
25
|
import { isI18nEnabled } from "../../i18n/config.js";
|
|
25
26
|
import { invalidateRedirectCache } from "../../redirects/cache.js";
|
|
27
|
+
import { isMissingTableError } from "../../utils/db-errors.js";
|
|
26
28
|
import { encodeRev, validateRev } from "../rev.js";
|
|
27
29
|
import type { ApiResult, ContentListResponse, ContentResponse } from "../types.js";
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Narrow a caught error to one carrying a structured `apiError` discriminant.
|
|
33
|
+
* Used by transaction callbacks that want to surface a specific error code
|
|
34
|
+
* through the standard Error throwing path.
|
|
35
|
+
*/
|
|
36
|
+
function hasApiError(error: unknown): error is Error & { apiError: { code: string } } {
|
|
37
|
+
if (!(error instanceof Error) || !("apiError" in error)) return false;
|
|
38
|
+
const { apiError } = error;
|
|
39
|
+
return (
|
|
40
|
+
typeof apiError === "object" &&
|
|
41
|
+
apiError !== null &&
|
|
42
|
+
"code" in apiError &&
|
|
43
|
+
typeof apiError.code === "string"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
29
47
|
/**
|
|
30
48
|
* Extract a slug source (title or name) from content data.
|
|
31
49
|
* Returns null if no suitable string field is found.
|
|
@@ -267,6 +285,28 @@ export async function handleContentList(
|
|
|
267
285
|
},
|
|
268
286
|
};
|
|
269
287
|
} catch (error) {
|
|
288
|
+
if (error instanceof InvalidCursorError) {
|
|
289
|
+
return {
|
|
290
|
+
success: false,
|
|
291
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (isMissingTableError(error)) {
|
|
295
|
+
return {
|
|
296
|
+
success: false,
|
|
297
|
+
error: {
|
|
298
|
+
code: "COLLECTION_NOT_FOUND",
|
|
299
|
+
message: `Collection '${collection}' not found`,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
if (error instanceof EmDashValidationError) {
|
|
304
|
+
// e.g. invalid orderBy field
|
|
305
|
+
return {
|
|
306
|
+
success: false,
|
|
307
|
+
error: { code: "VALIDATION_ERROR", message: error.message },
|
|
308
|
+
};
|
|
309
|
+
}
|
|
270
310
|
console.error("Content list error:", error);
|
|
271
311
|
return {
|
|
272
312
|
success: false,
|
|
@@ -453,6 +493,46 @@ export async function handleContentCreate(
|
|
|
453
493
|
data: { item, _rev: encodeRev(item) },
|
|
454
494
|
};
|
|
455
495
|
} catch (error) {
|
|
496
|
+
if (isMissingTableError(error)) {
|
|
497
|
+
return {
|
|
498
|
+
success: false,
|
|
499
|
+
error: {
|
|
500
|
+
code: "COLLECTION_NOT_FOUND",
|
|
501
|
+
message: `Collection '${collection}' not found`,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
if (error instanceof EmDashValidationError) {
|
|
506
|
+
return {
|
|
507
|
+
success: false,
|
|
508
|
+
error: { code: "VALIDATION_ERROR", message: error.message },
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
// SQLite UNIQUE constraint OR Postgres unique_violation — slug
|
|
512
|
+
// collisions and any other unique violations land here. Match
|
|
513
|
+
// specifically on "unique constraint failed" / "duplicate key" so we
|
|
514
|
+
// don't false-positive on NOT NULL or CHECK violations whose
|
|
515
|
+
// messages also contain "constraint failed".
|
|
516
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
517
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
518
|
+
// Detect slug-specific collisions by message fingerprint
|
|
519
|
+
if (message.includes("slug")) {
|
|
520
|
+
return {
|
|
521
|
+
success: false,
|
|
522
|
+
error: {
|
|
523
|
+
code: "SLUG_CONFLICT",
|
|
524
|
+
message: `Slug '${body.slug ?? "(auto-generated)"}' already exists in collection '${collection}'`,
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
success: false,
|
|
530
|
+
error: {
|
|
531
|
+
code: "CONFLICT",
|
|
532
|
+
message: "Unique constraint violation",
|
|
533
|
+
},
|
|
534
|
+
};
|
|
535
|
+
}
|
|
456
536
|
console.error("Content create error:", error);
|
|
457
537
|
return {
|
|
458
538
|
success: false,
|
|
@@ -483,6 +563,7 @@ export async function handleContentUpdate(
|
|
|
483
563
|
bylines?: ContentBylineInput[];
|
|
484
564
|
_rev?: string;
|
|
485
565
|
seo?: ContentSeoInput;
|
|
566
|
+
publishedAt?: string | null;
|
|
486
567
|
},
|
|
487
568
|
): Promise<ApiResult<ContentResponse>> {
|
|
488
569
|
try {
|
|
@@ -542,6 +623,7 @@ export async function handleContentUpdate(
|
|
|
542
623
|
slug: body.slug,
|
|
543
624
|
status: body.status,
|
|
544
625
|
authorId: body.authorId,
|
|
626
|
+
publishedAt: body.publishedAt,
|
|
545
627
|
});
|
|
546
628
|
|
|
547
629
|
if (body.bylines !== undefined) {
|
|
@@ -602,11 +684,44 @@ export async function handleContentUpdate(
|
|
|
602
684
|
} catch (error) {
|
|
603
685
|
// Handle structured errors thrown from inside the transaction
|
|
604
686
|
// (rev check failures, not-found)
|
|
605
|
-
if (error
|
|
606
|
-
|
|
687
|
+
if (hasApiError(error)) {
|
|
688
|
+
return {
|
|
689
|
+
success: false,
|
|
690
|
+
error: { code: error.apiError.code, message: error.message },
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
if (isMissingTableError(error)) {
|
|
607
694
|
return {
|
|
608
695
|
success: false,
|
|
609
|
-
error: {
|
|
696
|
+
error: {
|
|
697
|
+
code: "COLLECTION_NOT_FOUND",
|
|
698
|
+
message: `Collection '${collection}' not found`,
|
|
699
|
+
},
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
if (error instanceof EmDashValidationError) {
|
|
703
|
+
return {
|
|
704
|
+
success: false,
|
|
705
|
+
error: { code: "VALIDATION_ERROR", message: error.message },
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
const message = error instanceof Error ? error.message.toLowerCase() : "";
|
|
709
|
+
if (message.includes("unique constraint failed") || message.includes("duplicate key")) {
|
|
710
|
+
if (message.includes("slug")) {
|
|
711
|
+
return {
|
|
712
|
+
success: false,
|
|
713
|
+
error: {
|
|
714
|
+
code: "SLUG_CONFLICT",
|
|
715
|
+
message: `Slug '${body.slug ?? id}' already exists in collection '${collection}'`,
|
|
716
|
+
},
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
return {
|
|
720
|
+
success: false,
|
|
721
|
+
error: {
|
|
722
|
+
code: "CONFLICT",
|
|
723
|
+
message: "Unique constraint violation",
|
|
724
|
+
},
|
|
610
725
|
};
|
|
611
726
|
}
|
|
612
727
|
console.error("Content update error:", error);
|
|
@@ -867,6 +982,12 @@ export async function handleContentListTrashed(
|
|
|
867
982
|
},
|
|
868
983
|
};
|
|
869
984
|
} catch (error) {
|
|
985
|
+
if (error instanceof InvalidCursorError) {
|
|
986
|
+
return {
|
|
987
|
+
success: false,
|
|
988
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
989
|
+
};
|
|
990
|
+
}
|
|
870
991
|
console.error("Content list trashed error:", error);
|
|
871
992
|
return {
|
|
872
993
|
success: false,
|
|
@@ -113,11 +113,13 @@ export {
|
|
|
113
113
|
handleMenuItemUpdate,
|
|
114
114
|
handleMenuItemDelete,
|
|
115
115
|
handleMenuItemReorder,
|
|
116
|
+
handleMenuSetItems,
|
|
116
117
|
type MenuListItem,
|
|
117
118
|
type MenuWithItems,
|
|
118
119
|
type CreateMenuItemInput,
|
|
119
120
|
type UpdateMenuItemInput,
|
|
120
121
|
type ReorderItem,
|
|
122
|
+
type MenuSetItemsInput,
|
|
121
123
|
} from "./menus.js";
|
|
122
124
|
|
|
123
125
|
// Section handlers
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Kysely } from "kysely";
|
|
6
6
|
|
|
7
7
|
import { MediaRepository, type MediaItem } from "../../database/repositories/media.js";
|
|
8
|
+
import { InvalidCursorError } from "../../database/repositories/types.js";
|
|
8
9
|
import type { Database } from "../../database/types.js";
|
|
9
10
|
import type { ApiResult } from "../types.js";
|
|
10
11
|
|
|
@@ -43,7 +44,13 @@ export async function handleMediaList(
|
|
|
43
44
|
nextCursor: result.nextCursor,
|
|
44
45
|
},
|
|
45
46
|
};
|
|
46
|
-
} catch {
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof InvalidCursorError) {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
47
54
|
return {
|
|
48
55
|
success: false,
|
|
49
56
|
error: {
|
|
@@ -42,26 +42,33 @@ export interface MenuWithItems extends MenuRow {
|
|
|
42
42
|
*/
|
|
43
43
|
export async function handleMenuList(db: Kysely<Database>): Promise<ApiResult<MenuListItem[]>> {
|
|
44
44
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
45
|
+
// Single query: LEFT JOIN + GROUP BY for the per-menu item count.
|
|
46
|
+
// Avoids the N+1 of one count query per menu.
|
|
47
|
+
const rows = await db
|
|
48
|
+
.selectFrom("_emdash_menus as m")
|
|
49
|
+
.leftJoin("_emdash_menu_items as i", "i.menu_id", "m.id")
|
|
50
|
+
.select(({ fn }) => [
|
|
51
|
+
"m.id",
|
|
52
|
+
"m.name",
|
|
53
|
+
"m.label",
|
|
54
|
+
"m.created_at",
|
|
55
|
+
"m.updated_at",
|
|
56
|
+
fn.count<number>("i.id").as("itemCount"),
|
|
57
|
+
])
|
|
58
|
+
.groupBy(["m.id", "m.name", "m.label", "m.created_at", "m.updated_at"])
|
|
59
|
+
.orderBy("m.name", "asc")
|
|
49
60
|
.execute();
|
|
50
61
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
itemCount: count,
|
|
62
|
-
};
|
|
63
|
-
}),
|
|
64
|
-
);
|
|
62
|
+
// SQLite returns count as `number`, but some dialects (Postgres)
|
|
63
|
+
// return `string` from a count() aggregate. Normalize to number.
|
|
64
|
+
const menusWithCounts: MenuListItem[] = rows.map((row) => ({
|
|
65
|
+
id: row.id,
|
|
66
|
+
name: row.name,
|
|
67
|
+
label: row.label,
|
|
68
|
+
created_at: row.created_at,
|
|
69
|
+
updated_at: row.updated_at,
|
|
70
|
+
itemCount: typeof row.itemCount === "string" ? Number(row.itemCount) : row.itemCount,
|
|
71
|
+
}));
|
|
65
72
|
|
|
66
73
|
return { success: true, data: menusWithCounts };
|
|
67
74
|
} catch {
|
|
@@ -135,7 +142,7 @@ export async function handleMenuGet(
|
|
|
135
142
|
if (!menu) {
|
|
136
143
|
return {
|
|
137
144
|
success: false,
|
|
138
|
-
error: { code: "NOT_FOUND", message:
|
|
145
|
+
error: { code: "NOT_FOUND", message: `Menu '${name}' not found` },
|
|
139
146
|
};
|
|
140
147
|
}
|
|
141
148
|
|
|
@@ -173,7 +180,7 @@ export async function handleMenuUpdate(
|
|
|
173
180
|
if (!menu) {
|
|
174
181
|
return {
|
|
175
182
|
success: false,
|
|
176
|
-
error: { code: "NOT_FOUND", message:
|
|
183
|
+
error: { code: "NOT_FOUND", message: `Menu '${name}' not found` },
|
|
177
184
|
};
|
|
178
185
|
}
|
|
179
186
|
|
|
@@ -217,10 +224,14 @@ export async function handleMenuDelete(
|
|
|
217
224
|
if (!menu) {
|
|
218
225
|
return {
|
|
219
226
|
success: false,
|
|
220
|
-
error: { code: "NOT_FOUND", message:
|
|
227
|
+
error: { code: "NOT_FOUND", message: `Menu '${name}' not found` },
|
|
221
228
|
};
|
|
222
229
|
}
|
|
223
230
|
|
|
231
|
+
// D1 has FOREIGN KEYS off by default, so the migration's `ON DELETE
|
|
232
|
+
// CASCADE` won't fire there. Delete items explicitly first — this is
|
|
233
|
+
// idempotent on SQLite/Postgres where the cascade also fires.
|
|
234
|
+
await db.deleteFrom("_emdash_menu_items").where("menu_id", "=", menu.id).execute();
|
|
224
235
|
await db.deleteFrom("_emdash_menus").where("id", "=", menu.id).execute();
|
|
225
236
|
|
|
226
237
|
return { success: true, data: { deleted: true } };
|
|
@@ -443,6 +454,134 @@ export interface ReorderItem {
|
|
|
443
454
|
sortOrder: number;
|
|
444
455
|
}
|
|
445
456
|
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
// Atomic-replace menu items (used by the MCP `menu_set_items` tool)
|
|
459
|
+
// ---------------------------------------------------------------------------
|
|
460
|
+
|
|
461
|
+
export interface MenuSetItemsInput {
|
|
462
|
+
label: string;
|
|
463
|
+
type: "custom" | "page" | "post" | "taxonomy" | "collection";
|
|
464
|
+
customUrl?: string;
|
|
465
|
+
referenceCollection?: string;
|
|
466
|
+
referenceId?: string;
|
|
467
|
+
titleAttr?: string;
|
|
468
|
+
target?: string;
|
|
469
|
+
cssClasses?: string;
|
|
470
|
+
/**
|
|
471
|
+
* Index of the parent item in this same array. Must be strictly less
|
|
472
|
+
* than the current item's index so the insert order resolves parents
|
|
473
|
+
* before children. `undefined` makes the item top-level.
|
|
474
|
+
*/
|
|
475
|
+
parentIndex?: number;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Replace the entire set of items for a menu in one atomic transaction.
|
|
480
|
+
*
|
|
481
|
+
* Existing items are deleted and the new list is inserted in the order
|
|
482
|
+
* provided. `parentIndex` references resolve to actual parent IDs as the
|
|
483
|
+
* insert proceeds.
|
|
484
|
+
*/
|
|
485
|
+
export async function handleMenuSetItems(
|
|
486
|
+
db: Kysely<Database>,
|
|
487
|
+
menuName: string,
|
|
488
|
+
items: MenuSetItemsInput[],
|
|
489
|
+
): Promise<ApiResult<{ name: string; itemCount: number }>> {
|
|
490
|
+
// Validate parentIndex references — must be strictly earlier so
|
|
491
|
+
// the array can be inserted in order with parents resolved first.
|
|
492
|
+
// Negative indices are out of range; only Zod's `.nonnegative()` at
|
|
493
|
+
// the MCP boundary catches them today, so guard explicitly here for
|
|
494
|
+
// any caller that bypasses Zod (REST routes, direct handler use).
|
|
495
|
+
for (let i = 0; i < items.length; i++) {
|
|
496
|
+
const item = items[i];
|
|
497
|
+
if (item?.parentIndex !== undefined) {
|
|
498
|
+
if (item.parentIndex < 0 || item.parentIndex >= i) {
|
|
499
|
+
return {
|
|
500
|
+
success: false,
|
|
501
|
+
error: {
|
|
502
|
+
code: "VALIDATION_ERROR",
|
|
503
|
+
message: `item[${i}].parentIndex (${item.parentIndex}) must reference an earlier item`,
|
|
504
|
+
},
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
// Sentinel for "menu not found" thrown from inside the transaction
|
|
512
|
+
// so the rollback fires before we return the structured error.
|
|
513
|
+
const notFoundSentinel = Symbol("menu-not-found");
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
await withTransaction(db, async (trx) => {
|
|
517
|
+
// Existence check INSIDE the transaction so a concurrent
|
|
518
|
+
// menu_delete between lookup and write can't leave orphan
|
|
519
|
+
// items on D1 (FKs disabled by default).
|
|
520
|
+
const menu = await trx
|
|
521
|
+
.selectFrom("_emdash_menus")
|
|
522
|
+
.select("id")
|
|
523
|
+
.where("name", "=", menuName)
|
|
524
|
+
.executeTakeFirst();
|
|
525
|
+
|
|
526
|
+
if (!menu) {
|
|
527
|
+
throw notFoundSentinel;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
await trx.deleteFrom("_emdash_menu_items").where("menu_id", "=", menu.id).execute();
|
|
531
|
+
|
|
532
|
+
const insertedIds: string[] = [];
|
|
533
|
+
for (let i = 0; i < items.length; i++) {
|
|
534
|
+
const item = items[i];
|
|
535
|
+
if (!item) continue;
|
|
536
|
+
const id = ulid();
|
|
537
|
+
const parentId =
|
|
538
|
+
item.parentIndex !== undefined ? (insertedIds[item.parentIndex] ?? null) : null;
|
|
539
|
+
await trx
|
|
540
|
+
.insertInto("_emdash_menu_items")
|
|
541
|
+
.values({
|
|
542
|
+
id,
|
|
543
|
+
menu_id: menu.id,
|
|
544
|
+
parent_id: parentId,
|
|
545
|
+
sort_order: i,
|
|
546
|
+
type: item.type,
|
|
547
|
+
reference_collection: item.referenceCollection ?? null,
|
|
548
|
+
reference_id: item.referenceId ?? null,
|
|
549
|
+
custom_url: item.customUrl ?? null,
|
|
550
|
+
label: item.label,
|
|
551
|
+
title_attr: item.titleAttr ?? null,
|
|
552
|
+
target: item.target ?? null,
|
|
553
|
+
css_classes: item.cssClasses ?? null,
|
|
554
|
+
})
|
|
555
|
+
.execute();
|
|
556
|
+
insertedIds.push(id);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
await trx
|
|
560
|
+
.updateTable("_emdash_menus")
|
|
561
|
+
.set({ updated_at: new Date().toISOString() })
|
|
562
|
+
.where("id", "=", menu.id)
|
|
563
|
+
.execute();
|
|
564
|
+
});
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (error === notFoundSentinel) {
|
|
567
|
+
return {
|
|
568
|
+
success: false,
|
|
569
|
+
error: { code: "NOT_FOUND", message: `Menu '${menuName}' not found` },
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
throw error;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return { success: true, data: { name: menuName, itemCount: items.length } };
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error("[emdash] handleMenuSetItems failed:", error);
|
|
578
|
+
return {
|
|
579
|
+
success: false,
|
|
580
|
+
error: { code: "MENU_SET_ITEMS_ERROR", message: "Failed to set menu items" },
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
446
585
|
/**
|
|
447
586
|
* Batch reorder menu items.
|
|
448
587
|
*/
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type NotFoundEntry,
|
|
12
12
|
type NotFoundSummary,
|
|
13
13
|
} from "../../database/repositories/redirect.js";
|
|
14
|
+
import { InvalidCursorError } from "../../database/repositories/types.js";
|
|
14
15
|
import type { FindManyResult } from "../../database/repositories/types.js";
|
|
15
16
|
import type { Database } from "../../database/types.js";
|
|
16
17
|
import { wouldCreateLoop, detectLoops, type RedirectEdge } from "../../redirects/loops.js";
|
|
@@ -48,7 +49,13 @@ export async function handleRedirectList(
|
|
|
48
49
|
...(loopRedirectIds.length > 0 ? { loopRedirectIds } : {}),
|
|
49
50
|
},
|
|
50
51
|
};
|
|
51
|
-
} catch {
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof InvalidCursorError) {
|
|
54
|
+
return {
|
|
55
|
+
success: false,
|
|
56
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
52
59
|
return {
|
|
53
60
|
success: false,
|
|
54
61
|
error: { code: "REDIRECT_LIST_ERROR", message: "Failed to fetch redirects" },
|
|
@@ -318,7 +325,7 @@ export async function handleRedirectDelete(
|
|
|
318
325
|
function loopError(loopPath: string[]): ApiResult<never> {
|
|
319
326
|
const hops = loopPath
|
|
320
327
|
.slice(0, -1)
|
|
321
|
-
.map((p, i) => `${p} \u2192 ${loopPath[i + 1]
|
|
328
|
+
.map((p, i) => `${p} \u2192 ${loopPath[i + 1]}`)
|
|
322
329
|
.join("\n");
|
|
323
330
|
return {
|
|
324
331
|
success: false,
|
|
@@ -387,7 +394,13 @@ export async function handleNotFoundList(
|
|
|
387
394
|
const repo = new RedirectRepository(db);
|
|
388
395
|
const result = await repo.find404s(params);
|
|
389
396
|
return { success: true, data: result };
|
|
390
|
-
} catch {
|
|
397
|
+
} catch (error) {
|
|
398
|
+
if (error instanceof InvalidCursorError) {
|
|
399
|
+
return {
|
|
400
|
+
success: false,
|
|
401
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
402
|
+
};
|
|
403
|
+
}
|
|
391
404
|
return {
|
|
392
405
|
success: false,
|
|
393
406
|
error: { code: "NOT_FOUND_LIST_ERROR", message: "Failed to fetch 404 log" },
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import type { Kysely } from "kysely";
|
|
6
6
|
import { ulid } from "ulidx";
|
|
7
7
|
|
|
8
|
+
import { InvalidCursorError } from "../../database/repositories/types.js";
|
|
8
9
|
import type { FindManyResult } from "../../database/repositories/types.js";
|
|
9
10
|
import type { Database } from "../../database/types.js";
|
|
10
11
|
import {
|
|
@@ -36,7 +37,13 @@ export async function handleSectionList(
|
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
return { success: true, data: result };
|
|
39
|
-
} catch {
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error instanceof InvalidCursorError) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: { code: "INVALID_CURSOR", message: error.message },
|
|
45
|
+
};
|
|
46
|
+
}
|
|
40
47
|
return {
|
|
41
48
|
success: false,
|
|
42
49
|
error: { code: "SECTION_LIST_ERROR", message: "Failed to fetch sections" },
|