emdash 0.2.0 → 0.4.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-N6BF7RCD.d.mts → adapters-C2BzVy0p.d.mts} +1 -1
- package/dist/{adapters-N6BF7RCD.d.mts.map → adapters-C2BzVy0p.d.mts.map} +1 -1
- package/dist/{apply-wmVEOSbR.mjs → apply-Cma_PiF6.mjs} +38 -23
- package/dist/apply-Cma_PiF6.mjs.map +1 -0
- package/dist/astro/index.d.mts +25 -11
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +38 -25
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.mjs +2 -2
- package/dist/astro/middleware/redirect.d.mts.map +1 -1
- package/dist/astro/middleware/redirect.mjs +20 -8
- package/dist/astro/middleware/redirect.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +12 -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 +52 -45
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +9 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-1WQPlISL.mjs → byline-WuOq9MFJ.mjs} +5 -4
- package/dist/byline-WuOq9MFJ.mjs.map +1 -0
- package/dist/{bylines-BYdTYmia.mjs → bylines-C_Wsnz4L.mjs} +38 -6
- package/dist/bylines-C_Wsnz4L.mjs.map +1 -0
- package/dist/cache-E3Dts-yT.mjs +56 -0
- package/dist/cache-E3Dts-yT.mjs.map +1 -0
- package/dist/cli/index.mjs +13 -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/{config-Cq8H0SfX.mjs → config-DkxPrM9l.mjs} +1 -1
- package/dist/{config-Cq8H0SfX.mjs.map → config-DkxPrM9l.mjs.map} +1 -1
- package/dist/{content-BmXndhdi.mjs → content-BsBoyj8G.mjs} +20 -3
- package/dist/content-BsBoyj8G.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +2 -2
- 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/{default-WYlzADZL.mjs → default-PUx9RK6u.mjs} +1 -1
- package/dist/{default-WYlzADZL.mjs.map → default-PUx9RK6u.mjs.map} +1 -1
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +4 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +1 -0
- package/dist/{error-DrxtnGPg.mjs → error-HBeQbVhV.mjs} +1 -1
- package/dist/{error-DrxtnGPg.mjs.map → error-HBeQbVhV.mjs.map} +1 -1
- package/dist/{index-UHEVQMus.d.mts → index-CRg3PWfZ.d.mts} +59 -33
- package/dist/index-CRg3PWfZ.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -20
- package/dist/{load-Veizk2cT.mjs → load-BhSSm-TS.mjs} +1 -1
- package/dist/{load-Veizk2cT.mjs.map → load-BhSSm-TS.mjs.map} +1 -1
- package/dist/{loader-CHb2v0jm.mjs → loader-BYzwzORf.mjs} +4 -2
- package/dist/loader-BYzwzORf.mjs.map +1 -0
- package/dist/{manifest-schema-CuMio1A9.mjs → manifest-schema-BsXINkQD.mjs} +1 -1
- package/dist/{manifest-schema-CuMio1A9.mjs.map → manifest-schema-BsXINkQD.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/{mode-CYeM2rPt.mjs → mode-CyPLdO3C.mjs} +1 -1
- package/dist/{mode-CYeM2rPt.mjs.map → mode-CyPLdO3C.mjs.map} +1 -1
- package/dist/page/index.d.mts +1 -1
- package/dist/patterns-CrCYkMBb.mjs +93 -0
- package/dist/patterns-CrCYkMBb.mjs.map +1 -0
- package/dist/{placeholder-bOx1xCTY.d.mts → placeholder-BBCtpTES.d.mts} +1 -1
- package/dist/{placeholder-bOx1xCTY.d.mts.map → placeholder-BBCtpTES.d.mts.map} +1 -1
- package/dist/{placeholder-aiCD8aSZ.mjs → placeholder-DntBEQo7.mjs} +1 -1
- package/dist/{placeholder-aiCD8aSZ.mjs.map → placeholder-DntBEQo7.mjs.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-5Hcv_5ER.mjs → query-B6Vu0d2i.mjs} +35 -16
- package/dist/{query-5Hcv_5ER.mjs.map → query-B6Vu0d2i.mjs.map} +1 -1
- package/dist/{redirect-DIfIni3r.mjs → redirect-7lGhLBNZ.mjs} +10 -93
- package/dist/redirect-7lGhLBNZ.mjs.map +1 -0
- package/dist/{registry-1EvbAfsC.mjs → registry-BgnP3ysR.mjs} +27 -37
- package/dist/registry-BgnP3ysR.mjs.map +1 -0
- package/dist/{runner-BoN0-FPi.mjs → runner-Cd-_WyDo.mjs} +18 -6
- package/dist/runner-Cd-_WyDo.mjs.map +1 -0
- package/dist/{runner-DTqkzOzc.d.mts → runner-DYv3rX8P.d.mts} +10 -3
- package/dist/runner-DYv3rX8P.d.mts.map +1 -0
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-BsYMed12.mjs → search-B5p9D36n.mjs} +108 -57
- package/dist/search-B5p9D36n.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +10 -10
- 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 +11 -3
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +76 -15
- package/dist/storage/s3.mjs.map +1 -1
- package/dist/{tokens-DrB-W6Q-.mjs → tokens-DKHiCYCB.mjs} +1 -1
- package/dist/{tokens-DrB-W6Q-.mjs.map → tokens-DKHiCYCB.mjs.map} +1 -1
- package/dist/transaction-Cn2rjY78.mjs +28 -0
- package/dist/transaction-Cn2rjY78.mjs.map +1 -0
- package/dist/{transport-Bl8cTdYt.mjs → transport-BtcQ-Z7T.mjs} +1 -1
- package/dist/{transport-Bl8cTdYt.mjs.map → transport-BtcQ-Z7T.mjs.map} +1 -1
- package/dist/{transport-COOs9GSE.d.mts → transport-CKQA_G44.d.mts} +1 -1
- package/dist/{transport-COOs9GSE.d.mts.map → transport-CKQA_G44.d.mts.map} +1 -1
- package/dist/{types-7-UjSEyB.d.mts → types-B6BzlZxx.d.mts} +1 -1
- package/dist/{types-7-UjSEyB.d.mts.map → types-B6BzlZxx.d.mts.map} +1 -1
- package/dist/{types-6dqxBqsH.d.mts → types-BYWYxLcp.d.mts} +109 -5
- package/dist/types-BYWYxLcp.d.mts.map +1 -0
- package/dist/{types-CIsTnQvJ.d.mts → types-BmkQR1En.d.mts} +1 -1
- package/dist/{types-CIsTnQvJ.d.mts.map → types-BmkQR1En.d.mts.map} +1 -1
- package/dist/{types-BljtYPSd.d.mts → types-DNZpaCBk.d.mts} +14 -6
- package/dist/types-DNZpaCBk.d.mts.map +1 -0
- package/dist/{types-Bec-r_3_.mjs → types-Dz9_WMS6.mjs} +1 -1
- package/dist/types-Dz9_WMS6.mjs.map +1 -0
- package/dist/{types-CcreFIIH.d.mts → types-gLYVCXCQ.d.mts} +1 -1
- package/dist/{types-CcreFIIH.d.mts.map → types-gLYVCXCQ.d.mts.map} +1 -1
- package/dist/{types-DuNbGKjF.mjs → types-xxCWI3j0.mjs} +1 -1
- package/dist/{types-DuNbGKjF.mjs.map → types-xxCWI3j0.mjs.map} +1 -1
- package/dist/{validate-B7KP7VLM.d.mts → validate-CcNRWH6I.d.mts} +4 -4
- package/dist/{validate-B7KP7VLM.d.mts.map → validate-CcNRWH6I.d.mts.map} +1 -1
- package/dist/{validate-CXnRKfJK.mjs → validate-DuZDIxfy.mjs} +2 -2
- package/dist/{validate-CXnRKfJK.mjs.map → validate-DuZDIxfy.mjs.map} +1 -1
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +11 -11
- package/dist/{validate-CqRJb_xU.mjs.map → validate-VPnKoIzW.mjs.map} +1 -1
- package/dist/version-DlTDRdpv.mjs +7 -0
- package/dist/version-DlTDRdpv.mjs.map +1 -0
- package/package.json +7 -5
- package/src/api/handlers/content.ts +36 -25
- package/src/api/handlers/menus.ts +19 -16
- package/src/api/handlers/redirects.ts +95 -3
- package/src/api/schemas/redirects.ts +1 -0
- package/src/astro/integration/index.ts +2 -3
- package/src/astro/integration/runtime.ts +8 -14
- package/src/astro/integration/vite-config.ts +14 -4
- package/src/astro/middleware/redirect.ts +30 -15
- package/src/astro/middleware.ts +11 -19
- package/src/astro/routes/admin.astro +2 -2
- package/src/astro/routes/api/admin/bylines/[id]/index.ts +3 -0
- package/src/astro/routes/api/admin/bylines/index.ts +2 -0
- package/src/astro/routes/api/comments/[collection]/[contentId]/index.ts +2 -0
- package/src/astro/routes/api/import/wordpress/rewrite-urls.ts +2 -0
- package/src/astro/routes/api/manifest.ts +3 -1
- package/src/astro/routes/api/redirects/[id].ts +3 -0
- package/src/astro/routes/api/redirects/index.ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/index.ts +2 -0
- package/src/astro/routes/api/schema/collections/index.ts +1 -0
- package/src/astro/storage/adapters.ts +19 -5
- package/src/astro/storage/types.ts +12 -4
- package/src/astro/types.ts +1 -0
- package/src/bylines/index.ts +50 -2
- package/src/cleanup.ts +3 -3
- package/src/cli/commands/bundle-utils.ts +5 -5
- package/src/database/dialect-helpers.ts +3 -0
- package/src/database/migrations/011_sections.ts +2 -2
- package/src/database/migrations/runner.ts +23 -2
- package/src/database/repositories/byline.ts +2 -1
- package/src/database/repositories/content.ts +5 -0
- package/src/database/repositories/redirect.ts +13 -0
- package/src/database/validate.ts +10 -10
- package/src/emdash-runtime.ts +23 -9
- package/src/index.ts +3 -0
- package/src/loader.ts +2 -0
- package/src/mcp/server.ts +40 -67
- package/src/menus/index.ts +4 -0
- package/src/plugins/context.ts +28 -4
- package/src/plugins/cron.ts +29 -4
- package/src/plugins/hooks.ts +22 -10
- package/src/plugins/index.ts +1 -0
- package/src/plugins/manager.ts +6 -2
- package/src/plugins/marketplace.ts +33 -3
- package/src/plugins/routes.ts +3 -3
- package/src/plugins/types.ts +7 -0
- package/src/query.ts +37 -14
- package/src/redirects/cache.ts +68 -0
- package/src/redirects/loops.ts +318 -0
- package/src/schema/registry.ts +3 -0
- package/src/search/fts-manager.ts +24 -11
- package/src/search/query.ts +8 -9
- package/src/seed/apply.ts +49 -28
- package/src/storage/s3.ts +94 -25
- package/src/storage/types.ts +13 -5
- package/src/utils/slugify.ts +11 -0
- package/src/version.ts +12 -0
- package/src/visual-editing/toolbar.ts +11 -1
- package/dist/apply-wmVEOSbR.mjs.map +0 -1
- package/dist/byline-1WQPlISL.mjs.map +0 -1
- package/dist/bylines-BYdTYmia.mjs.map +0 -1
- package/dist/content-BmXndhdi.mjs.map +0 -1
- package/dist/dialect-helpers-B9uSp2GJ.mjs.map +0 -1
- package/dist/index-UHEVQMus.d.mts.map +0 -1
- package/dist/loader-CHb2v0jm.mjs.map +0 -1
- package/dist/redirect-DIfIni3r.mjs.map +0 -1
- package/dist/registry-1EvbAfsC.mjs.map +0 -1
- package/dist/runner-BoN0-FPi.mjs.map +0 -1
- package/dist/runner-DTqkzOzc.d.mts.map +0 -1
- package/dist/search-BsYMed12.mjs.map +0 -1
- package/dist/types-6dqxBqsH.d.mts.map +0 -1
- package/dist/types-Bec-r_3_.mjs.map +0 -1
- package/dist/types-BljtYPSd.d.mts.map +0 -1
|
@@ -15,6 +15,7 @@ import { getSiteBaseUrl } from "#api/site-url.js";
|
|
|
15
15
|
import { sendCommentNotification } from "#comments/notifications.js";
|
|
16
16
|
import { createComment, type CommentHookRunner } from "#comments/service.js";
|
|
17
17
|
import { CommentRepository } from "#db/repositories/comment.js";
|
|
18
|
+
import { validateIdentifier } from "#db/validate.js";
|
|
18
19
|
import { extractRequestMeta } from "#plugins/request-meta.js";
|
|
19
20
|
import type { CollectionCommentSettings, ModerationDecision } from "#plugins/types.js";
|
|
20
21
|
|
|
@@ -106,6 +107,7 @@ export const POST: APIRoute = async ({ params, request, locals }) => {
|
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// Verify the content item exists, is published, and not soft-deleted
|
|
110
|
+
validateIdentifier(collection, "collection");
|
|
109
111
|
const contentRow = await emdash.db
|
|
110
112
|
.selectFrom(`ec_${collection}` as never)
|
|
111
113
|
.select(["id" as never, "slug" as never, "author_id" as never, "published_at" as never])
|
|
@@ -17,6 +17,7 @@ import { requirePerm } from "#api/authorize.js";
|
|
|
17
17
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
18
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
19
|
import { wpRewriteUrlsBody } from "#api/schemas.js";
|
|
20
|
+
import { validateIdentifier } from "#db/validate.js";
|
|
20
21
|
import { normalizeMediaValue } from "#media/normalize.js";
|
|
21
22
|
import type { MediaProvider } from "#media/types.js";
|
|
22
23
|
import type { EmDashHandlers } from "#types";
|
|
@@ -280,6 +281,7 @@ async function rewriteUrls(
|
|
|
280
281
|
continue;
|
|
281
282
|
|
|
282
283
|
// Get table name
|
|
284
|
+
validateIdentifier(collection.slug, "collection slug");
|
|
283
285
|
const tableName = `ec_${collection.slug}`;
|
|
284
286
|
|
|
285
287
|
try {
|
|
@@ -11,6 +11,7 @@ import type { APIRoute } from "astro";
|
|
|
11
11
|
|
|
12
12
|
import { getAuthMode } from "#auth/mode.js";
|
|
13
13
|
|
|
14
|
+
import { COMMIT, VERSION } from "../../../version.js";
|
|
14
15
|
import type { EmDashManifest } from "../../types.js";
|
|
15
16
|
|
|
16
17
|
export const prerender = false;
|
|
@@ -43,7 +44,8 @@ export const GET: APIRoute = async ({ locals }) => {
|
|
|
43
44
|
signupEnabled,
|
|
44
45
|
}
|
|
45
46
|
: {
|
|
46
|
-
version:
|
|
47
|
+
version: VERSION,
|
|
48
|
+
commit: COMMIT,
|
|
47
49
|
hash: "default",
|
|
48
50
|
collections: {},
|
|
49
51
|
plugins: {},
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
} from "#api/handlers/redirects.js";
|
|
18
18
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
19
19
|
import { updateRedirectBody } from "#api/schemas.js";
|
|
20
|
+
import { invalidateRedirectCache } from "#redirects/cache.js";
|
|
20
21
|
|
|
21
22
|
export const prerender = false;
|
|
22
23
|
|
|
@@ -57,6 +58,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
57
58
|
if (isParseError(body)) return body;
|
|
58
59
|
|
|
59
60
|
const result = await handleRedirectUpdate(db, id, body);
|
|
61
|
+
invalidateRedirectCache();
|
|
60
62
|
return unwrapResult(result);
|
|
61
63
|
} catch (error) {
|
|
62
64
|
return handleError(error, "Failed to update redirect", "REDIRECT_UPDATE_ERROR");
|
|
@@ -77,6 +79,7 @@ export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
|
77
79
|
|
|
78
80
|
try {
|
|
79
81
|
const result = await handleRedirectDelete(db, id);
|
|
82
|
+
invalidateRedirectCache();
|
|
80
83
|
return unwrapResult(result);
|
|
81
84
|
} catch (error) {
|
|
82
85
|
return handleError(error, "Failed to delete redirect", "REDIRECT_DELETE_ERROR");
|
|
@@ -12,6 +12,7 @@ import { handleError, unwrapResult } from "#api/error.js";
|
|
|
12
12
|
import { handleRedirectCreate, handleRedirectList } from "#api/handlers/redirects.js";
|
|
13
13
|
import { isParseError, parseBody, parseQuery } from "#api/parse.js";
|
|
14
14
|
import { createRedirectBody, redirectsListQuery } from "#api/schemas.js";
|
|
15
|
+
import { invalidateRedirectCache } from "#redirects/cache.js";
|
|
15
16
|
|
|
16
17
|
export const prerender = false;
|
|
17
18
|
|
|
@@ -45,6 +46,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
45
46
|
if (isParseError(body)) return body;
|
|
46
47
|
|
|
47
48
|
const result = await handleRedirectCreate(db, body);
|
|
49
|
+
invalidateRedirectCache();
|
|
48
50
|
return unwrapResult(result, 201);
|
|
49
51
|
} catch (error) {
|
|
50
52
|
return handleError(error, "Failed to create redirect", "REDIRECT_CREATE_ERROR");
|
|
@@ -59,6 +59,7 @@ export const PUT: APIRoute = async ({ params, request, locals }) => {
|
|
|
59
59
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- parseBody validates via Zod
|
|
60
60
|
body as UpdateCollectionInput,
|
|
61
61
|
);
|
|
62
|
+
emdash!.invalidateManifest();
|
|
62
63
|
return unwrapResult(result);
|
|
63
64
|
};
|
|
64
65
|
|
|
@@ -76,5 +77,6 @@ export const DELETE: APIRoute = async ({ params, url, locals }) => {
|
|
|
76
77
|
const result = await handleSchemaCollectionDelete(emdash!.db, slug, {
|
|
77
78
|
force,
|
|
78
79
|
});
|
|
80
|
+
emdash!.invalidateManifest();
|
|
79
81
|
return unwrapResult(result);
|
|
80
82
|
};
|
|
@@ -43,5 +43,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
43
43
|
|
|
44
44
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- Zod schema output narrowed to CreateCollectionInput
|
|
45
45
|
const result = await handleSchemaCollectionCreate(emdash!.db, body as CreateCollectionInput);
|
|
46
|
+
emdash!.invalidateManifest();
|
|
46
47
|
return unwrapResult(result, 201);
|
|
47
48
|
};
|
|
@@ -32,20 +32,34 @@ import type { StorageDescriptor, S3StorageConfig, LocalStorageConfig } from "./t
|
|
|
32
32
|
/**
|
|
33
33
|
* S3-compatible storage adapter
|
|
34
34
|
*
|
|
35
|
-
* Works with AWS S3, Cloudflare R2 (via S3 API),
|
|
35
|
+
* Works with AWS S3, Cloudflare R2 (via S3 API), MinIO, etc.
|
|
36
|
+
*
|
|
37
|
+
* Any field omitted here is resolved from the matching `S3_*` environment
|
|
38
|
+
* variable when the container starts (`S3_ENDPOINT`, `S3_BUCKET`,
|
|
39
|
+
* `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `S3_REGION`, `S3_PUBLIC_URL`).
|
|
40
|
+
* Explicit values always take precedence over env vars.
|
|
41
|
+
*
|
|
42
|
+
* Note: env var resolution reads `process.env` on Node at runtime.
|
|
43
|
+
* Workers users should continue passing explicit values to `s3({...})`.
|
|
36
44
|
*
|
|
37
45
|
* @example
|
|
38
46
|
* ```ts
|
|
47
|
+
* // All fields from env (container deployments)
|
|
48
|
+
* storage: s3()
|
|
49
|
+
*
|
|
50
|
+
* // Mix: CDN from config, credentials from env
|
|
51
|
+
* storage: s3({ publicUrl: "https://cdn.example.com" })
|
|
52
|
+
*
|
|
53
|
+
* // All explicit (unchanged from before)
|
|
39
54
|
* storage: s3({
|
|
40
55
|
* endpoint: "https://xxx.r2.cloudflarestorage.com",
|
|
41
56
|
* bucket: "media",
|
|
42
|
-
* accessKeyId: process.env.R2_ACCESS_KEY_ID
|
|
43
|
-
* secretAccessKey: process.env.R2_SECRET_ACCESS_KEY
|
|
44
|
-
* publicUrl: "https://cdn.example.com", // optional CDN
|
|
57
|
+
* accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
|
58
|
+
* secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
|
|
45
59
|
* })
|
|
46
60
|
* ```
|
|
47
61
|
*/
|
|
48
|
-
export function s3(config: S3StorageConfig): StorageDescriptor {
|
|
62
|
+
export function s3(config: Partial<S3StorageConfig> = {}): StorageDescriptor {
|
|
49
63
|
return {
|
|
50
64
|
entrypoint: "emdash/storage/s3",
|
|
51
65
|
config,
|
|
@@ -39,10 +39,18 @@ export interface S3StorageConfig {
|
|
|
39
39
|
endpoint: string;
|
|
40
40
|
/** Bucket name */
|
|
41
41
|
bucket: string;
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Access key ID.
|
|
44
|
+
* May be resolved from the `S3_ACCESS_KEY_ID` env var at runtime on Node.
|
|
45
|
+
* Must be provided together with `secretAccessKey`, or both omitted.
|
|
46
|
+
*/
|
|
47
|
+
accessKeyId?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Secret access key.
|
|
50
|
+
* May be resolved from the `S3_SECRET_ACCESS_KEY` env var at runtime on Node.
|
|
51
|
+
* Must be provided together with `accessKeyId`, or both omitted.
|
|
52
|
+
*/
|
|
53
|
+
secretAccessKey?: string;
|
|
46
54
|
/** Optional region (defaults to "auto") */
|
|
47
55
|
region?: string;
|
|
48
56
|
/** Optional public URL prefix for CDN */
|
package/src/astro/types.ts
CHANGED
package/src/bylines/index.ts
CHANGED
|
@@ -14,6 +14,48 @@ import { validateIdentifier } from "../database/validate.js";
|
|
|
14
14
|
import { getDb } from "../loader.js";
|
|
15
15
|
import { chunks, SQL_BATCH_SIZE } from "../utils/chunks.js";
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Cached result of "does any byline exist in the database?"
|
|
19
|
+
* null = not yet checked, true/false = cached result.
|
|
20
|
+
* Invalidated when bylines are created or deleted.
|
|
21
|
+
*/
|
|
22
|
+
let hasBylines: boolean | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Invalidate the cached "has any bylines" check.
|
|
26
|
+
* Call this when bylines are created, updated, or deleted.
|
|
27
|
+
*/
|
|
28
|
+
export function invalidateBylineCache(): void {
|
|
29
|
+
hasBylines = null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if any bylines exist in the database. Result is cached
|
|
34
|
+
* for the lifetime of the worker/process and invalidated on writes.
|
|
35
|
+
*/
|
|
36
|
+
async function hasAnyBylines(): Promise<boolean> {
|
|
37
|
+
if (hasBylines !== null) return hasBylines;
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const db = await getDb();
|
|
41
|
+
const result = await sql<{ id: string }>`
|
|
42
|
+
SELECT id FROM _emdash_bylines LIMIT 1
|
|
43
|
+
`.execute(db);
|
|
44
|
+
hasBylines = result.rows.length > 0;
|
|
45
|
+
} catch (error: unknown) {
|
|
46
|
+
// Only treat "no such table" as a safe false -- anything else should
|
|
47
|
+
// not be cached so the next request retries.
|
|
48
|
+
const message = error instanceof Error ? error.message : "";
|
|
49
|
+
if (message.includes("no such table")) {
|
|
50
|
+
hasBylines = false;
|
|
51
|
+
} else {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return hasBylines;
|
|
57
|
+
}
|
|
58
|
+
|
|
17
59
|
/**
|
|
18
60
|
* Get a byline by ID.
|
|
19
61
|
*
|
|
@@ -134,6 +176,12 @@ export async function getBylinesForEntries(
|
|
|
134
176
|
return result;
|
|
135
177
|
}
|
|
136
178
|
|
|
179
|
+
// Skip DB queries entirely when no bylines have been created.
|
|
180
|
+
// The cache is invalidated when bylines are created/deleted.
|
|
181
|
+
if (!(await hasAnyBylines())) {
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
|
|
137
185
|
const db = await getDb();
|
|
138
186
|
const repo = new BylineRepository(db);
|
|
139
187
|
|
|
@@ -199,8 +247,8 @@ async function getAuthorId(
|
|
|
199
247
|
collection: string,
|
|
200
248
|
entryId: string,
|
|
201
249
|
): Promise<string | null> {
|
|
250
|
+
validateIdentifier(collection, "collection");
|
|
202
251
|
const tableName = `ec_${collection}`;
|
|
203
|
-
validateIdentifier(tableName, "content table");
|
|
204
252
|
|
|
205
253
|
const result = await sql<{ author_id: string | null }>`
|
|
206
254
|
SELECT author_id FROM ${sql.ref(tableName)}
|
|
@@ -220,8 +268,8 @@ async function getAuthorIds(
|
|
|
220
268
|
collection: string,
|
|
221
269
|
entryIds: string[],
|
|
222
270
|
): Promise<Map<string, string>> {
|
|
271
|
+
validateIdentifier(collection, "collection");
|
|
223
272
|
const tableName = `ec_${collection}`;
|
|
224
|
-
validateIdentifier(tableName, "content table");
|
|
225
273
|
|
|
226
274
|
const map = new Map<string, string>();
|
|
227
275
|
for (const chunk of chunks(entryIds, SQL_BATCH_SIZE)) {
|
package/src/cleanup.ts
CHANGED
|
@@ -121,11 +121,11 @@ export async function runSystemCleanup(
|
|
|
121
121
|
* them down to REVISION_KEEP_COUNT.
|
|
122
122
|
*/
|
|
123
123
|
async function pruneExcessiveRevisions(db: Kysely<Database>): Promise<number> {
|
|
124
|
-
const entries = await sql<{ collection: string; entry_id: string
|
|
125
|
-
SELECT collection, entry_id
|
|
124
|
+
const entries = await sql<{ collection: string; entry_id: string }>`
|
|
125
|
+
SELECT collection, entry_id
|
|
126
126
|
FROM revisions
|
|
127
127
|
GROUP BY collection, entry_id
|
|
128
|
-
HAVING
|
|
128
|
+
HAVING COUNT(*) > ${REVISION_PRUNE_THRESHOLD}
|
|
129
129
|
`.execute(db);
|
|
130
130
|
|
|
131
131
|
if (entries.rows.length === 0) return 0;
|
|
@@ -196,11 +196,7 @@ export async function resolveSourceEntry(
|
|
|
196
196
|
): Promise<string | undefined> {
|
|
197
197
|
const cleaned = distPath.replace(LEADING_DOT_SLASH_RE, "");
|
|
198
198
|
|
|
199
|
-
//
|
|
200
|
-
const direct = resolve(pluginDir, cleaned);
|
|
201
|
-
if (await fileExists(direct)) return direct;
|
|
202
|
-
|
|
203
|
-
// Convert dist path to src: dist/foo.mjs → src/foo.ts
|
|
199
|
+
// Prefer source over dist — dist/foo.mjs → src/foo.ts
|
|
204
200
|
const srcPath = cleaned.replace(DIST_PREFIX_RE, "src/").replace(MJS_EXT_RE, ".ts");
|
|
205
201
|
const srcFull = resolve(pluginDir, srcPath);
|
|
206
202
|
if (await fileExists(srcFull)) return srcFull;
|
|
@@ -210,6 +206,10 @@ export async function resolveSourceEntry(
|
|
|
210
206
|
const tsxFull = resolve(pluginDir, tsxPath);
|
|
211
207
|
if (await fileExists(tsxFull)) return tsxFull;
|
|
212
208
|
|
|
209
|
+
// Fall back to direct path (might be source already, or pre-compiled plugin)
|
|
210
|
+
const direct = resolve(pluginDir, cleaned);
|
|
211
|
+
if (await fileExists(direct)) return direct;
|
|
212
|
+
|
|
213
213
|
return undefined;
|
|
214
214
|
}
|
|
215
215
|
|
|
@@ -14,6 +14,7 @@ import type { ColumnDataType, Kysely, RawBuilder } from "kysely";
|
|
|
14
14
|
import { sql } from "kysely";
|
|
15
15
|
|
|
16
16
|
import type { DatabaseDialectType } from "../db/adapters.js";
|
|
17
|
+
import { validateIdentifier, validateJsonFieldName } from "./validate.js";
|
|
17
18
|
|
|
18
19
|
export type { DatabaseDialectType };
|
|
19
20
|
|
|
@@ -131,6 +132,8 @@ export function binaryType(db: Kysely<any>): ColumnDataType {
|
|
|
131
132
|
*/
|
|
132
133
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- accepts any Kysely instance
|
|
133
134
|
export function jsonExtractExpr(db: Kysely<any>, column: string, path: string): string {
|
|
135
|
+
validateIdentifier(column, "JSON column name");
|
|
136
|
+
validateJsonFieldName(path, "JSON path");
|
|
134
137
|
if (isPostgres(db)) {
|
|
135
138
|
return `${column}->>'${path}'`;
|
|
136
139
|
}
|
|
@@ -58,8 +58,8 @@ export async function up(db: Kysely<unknown>): Promise<void> {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
export async function down(db: Kysely<unknown>): Promise<void> {
|
|
61
|
-
await db.schema.dropIndex("
|
|
62
|
-
await db.schema.dropIndex("
|
|
61
|
+
await db.schema.dropIndex("idx_sections_source").execute();
|
|
62
|
+
await db.schema.dropIndex("idx_sections_category").execute();
|
|
63
63
|
await db.schema.dropTable("_emdash_sections").execute();
|
|
64
64
|
await db.schema.dropTable("_emdash_section_categories").execute();
|
|
65
65
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Kysely, type Migration, type MigrationProvider, Migrator } from "kysely";
|
|
1
|
+
import { type Kysely, type Migration, type MigrationProvider, Migrator, sql } from "kysely";
|
|
2
2
|
|
|
3
3
|
import type { Database } from "../types.js";
|
|
4
4
|
// Import migrations statically for bundling
|
|
@@ -122,9 +122,30 @@ export async function getMigrationStatus(db: Kysely<Database>): Promise<Migratio
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
/**
|
|
125
|
-
* Run all pending migrations
|
|
125
|
+
* Run all pending migrations.
|
|
126
|
+
*
|
|
127
|
+
* Includes a fast-path: if the migration table already exists and contains
|
|
128
|
+
* exactly MIGRATION_COUNT rows, all migrations have been applied and we can
|
|
129
|
+
* skip the Kysely Migrator entirely. This avoids the expensive
|
|
130
|
+
* `pragma_table_info` introspection that Kysely runs for every table in the
|
|
131
|
+
* database (twice!) just to check if the migration tables exist.
|
|
132
|
+
* On D1 with ~57 tables, that's ~116 queries saved per init.
|
|
126
133
|
*/
|
|
127
134
|
export async function runMigrations(db: Kysely<Database>): Promise<{ applied: string[] }> {
|
|
135
|
+
// Fast path: check if all migrations are already applied.
|
|
136
|
+
// A single cheap query vs the Migrator's full schema introspection.
|
|
137
|
+
try {
|
|
138
|
+
const result = await sql<{ count: number }>`
|
|
139
|
+
SELECT COUNT(*) as count FROM ${sql.ref(MIGRATION_TABLE)}
|
|
140
|
+
`.execute(db);
|
|
141
|
+
if (result.rows[0]?.count === MIGRATION_COUNT) {
|
|
142
|
+
return { applied: [] };
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
// Table doesn't exist yet (first run). Fall through to the Migrator
|
|
146
|
+
// which will create it.
|
|
147
|
+
}
|
|
148
|
+
|
|
128
149
|
const migrator = new Migrator({
|
|
129
150
|
db,
|
|
130
151
|
provider: new StaticMigrationProvider(),
|
|
@@ -3,6 +3,7 @@ import { ulid } from "ulidx";
|
|
|
3
3
|
|
|
4
4
|
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
5
5
|
import { listTablesLike } from "../dialect-helpers.js";
|
|
6
|
+
import { withTransaction } from "../transaction.js";
|
|
6
7
|
import type { BylineTable, Database } from "../types.js";
|
|
7
8
|
import { validateIdentifier } from "../validate.js";
|
|
8
9
|
import {
|
|
@@ -197,7 +198,7 @@ export class BylineRepository {
|
|
|
197
198
|
const existing = await this.findById(id);
|
|
198
199
|
if (!existing) return false;
|
|
199
200
|
|
|
200
|
-
await this.db
|
|
201
|
+
await withTransaction(this.db, async (trx) => {
|
|
201
202
|
await trx.deleteFrom("_emdash_content_bylines").where("byline_id", "=", id).execute();
|
|
202
203
|
|
|
203
204
|
await trx.deleteFrom("_emdash_bylines").where("id", "=", id).execute();
|
|
@@ -3,6 +3,7 @@ import { ulid } from "ulidx";
|
|
|
3
3
|
|
|
4
4
|
import { slugify } from "../../utils/slugify.js";
|
|
5
5
|
import type { Database } from "../types.js";
|
|
6
|
+
import { validateIdentifier } from "../validate.js";
|
|
6
7
|
import { RevisionRepository } from "./revision.js";
|
|
7
8
|
import type {
|
|
8
9
|
CreateContentInput,
|
|
@@ -41,6 +42,7 @@ const SYSTEM_COLUMNS = new Set([
|
|
|
41
42
|
* Get the table name for a collection type
|
|
42
43
|
*/
|
|
43
44
|
function getTableName(type: string): string {
|
|
45
|
+
validateIdentifier(type, "collection type");
|
|
44
46
|
return `ec_${type}`;
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -168,6 +170,7 @@ export class ContentRepository {
|
|
|
168
170
|
if (data && typeof data === "object") {
|
|
169
171
|
for (const [key, value] of Object.entries(data)) {
|
|
170
172
|
if (!SYSTEM_COLUMNS.has(key)) {
|
|
173
|
+
validateIdentifier(key, "content field name");
|
|
171
174
|
columns.push(key);
|
|
172
175
|
values.push(serializeValue(value));
|
|
173
176
|
}
|
|
@@ -578,6 +581,7 @@ export class ContentRepository {
|
|
|
578
581
|
if (input.data !== undefined && typeof input.data === "object") {
|
|
579
582
|
for (const [key, value] of Object.entries(input.data)) {
|
|
580
583
|
if (!SYSTEM_COLUMNS.has(key)) {
|
|
584
|
+
validateIdentifier(key, "content field name");
|
|
581
585
|
updates[key] = serializeValue(value);
|
|
582
586
|
}
|
|
583
587
|
}
|
|
@@ -1079,6 +1083,7 @@ export class ContentRepository {
|
|
|
1079
1083
|
for (const [key, value] of Object.entries(data)) {
|
|
1080
1084
|
if (SYSTEM_COLUMNS.has(key)) continue;
|
|
1081
1085
|
if (key.startsWith("_")) continue; // revision metadata
|
|
1086
|
+
validateIdentifier(key, "content field name");
|
|
1082
1087
|
updates[key] = serializeValue(value);
|
|
1083
1088
|
}
|
|
1084
1089
|
|
|
@@ -237,6 +237,19 @@ export class RedirectRepository {
|
|
|
237
237
|
return BigInt(result.numDeletedRows) > 0n;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Fetch all enabled redirects (for loop detection graph building).
|
|
242
|
+
* Not paginated — returns the full set.
|
|
243
|
+
*/
|
|
244
|
+
async findAllEnabled(): Promise<Redirect[]> {
|
|
245
|
+
const rows = await this.db
|
|
246
|
+
.selectFrom("_emdash_redirects")
|
|
247
|
+
.selectAll()
|
|
248
|
+
.where("enabled", "=", 1)
|
|
249
|
+
.execute();
|
|
250
|
+
return rows.map(rowToRedirect);
|
|
251
|
+
}
|
|
252
|
+
|
|
240
253
|
// --- Matching -----------------------------------------------------------
|
|
241
254
|
|
|
242
255
|
async findExactMatch(path: string): Promise<Redirect | null> {
|
package/src/database/validate.ts
CHANGED
|
@@ -79,16 +79,6 @@ export function validateIdentifier(value: string, label = "identifier"): void {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
* Validate that a string is a safe SQL identifier, allowing hyphens.
|
|
84
|
-
*
|
|
85
|
-
* Like `validateIdentifier` but also permits hyphens, which appear in
|
|
86
|
-
* plugin IDs (e.g., "my-plugin"). Matches `/^[a-z][a-z0-9_-]*$/`.
|
|
87
|
-
*
|
|
88
|
-
* @param value - The string to validate
|
|
89
|
-
* @param label - Human-readable label for error messages
|
|
90
|
-
* @throws {IdentifierError} If the value is not valid
|
|
91
|
-
*/
|
|
92
82
|
/**
|
|
93
83
|
* Validate that a string is a safe JSON field name for use in json_extract paths.
|
|
94
84
|
*
|
|
@@ -120,6 +110,16 @@ export function validateJsonFieldName(value: string, label = "JSON field name"):
|
|
|
120
110
|
}
|
|
121
111
|
}
|
|
122
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Validate that a string is a safe SQL identifier, allowing hyphens.
|
|
115
|
+
*
|
|
116
|
+
* Like `validateIdentifier` but also permits hyphens, which appear in
|
|
117
|
+
* plugin IDs (e.g., "my-plugin"). Matches `/^[a-z][a-z0-9_-]*$/`.
|
|
118
|
+
*
|
|
119
|
+
* @param value - The string to validate
|
|
120
|
+
* @param label - Human-readable label for error messages
|
|
121
|
+
* @throws {IdentifierError} If the value is not valid
|
|
122
|
+
*/
|
|
123
123
|
export function validatePluginIdentifier(value: string, label = "plugin identifier"): void {
|
|
124
124
|
if (!value || typeof value !== "string") {
|
|
125
125
|
throw new IdentifierError(`${label} must be a non-empty string`, String(value));
|
package/src/emdash-runtime.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { isSqlite } from "./database/dialect-helpers.js";
|
|
|
23
23
|
import { runMigrations } from "./database/migrations/runner.js";
|
|
24
24
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
25
25
|
import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
|
|
26
|
+
import { validateIdentifier } from "./database/validate.js";
|
|
26
27
|
import { normalizeMediaValue } from "./media/normalize.js";
|
|
27
28
|
import type { MediaProvider, MediaProviderCapabilities } from "./media/types.js";
|
|
28
29
|
import type { SandboxedPlugin, SandboxRunner } from "./plugins/sandbox/types.js";
|
|
@@ -36,8 +37,10 @@ import type {
|
|
|
36
37
|
PageMetadataContribution,
|
|
37
38
|
PageFragmentContribution,
|
|
38
39
|
} from "./plugins/types.js";
|
|
40
|
+
import { invalidateUrlPatternCache } from "./query.js";
|
|
39
41
|
import type { FieldType } from "./schema/types.js";
|
|
40
42
|
import { hashString } from "./utils/hash.js";
|
|
43
|
+
import { COMMIT, VERSION } from "./version.js";
|
|
41
44
|
|
|
42
45
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
43
46
|
|
|
@@ -55,6 +58,7 @@ const VALID_LINK_REL = new Set([
|
|
|
55
58
|
"alternate",
|
|
56
59
|
"author",
|
|
57
60
|
"license",
|
|
61
|
+
"nlweb",
|
|
58
62
|
"site.standard.document",
|
|
59
63
|
]);
|
|
60
64
|
|
|
@@ -1352,7 +1356,8 @@ export class EmDashRuntime {
|
|
|
1352
1356
|
: undefined;
|
|
1353
1357
|
|
|
1354
1358
|
return {
|
|
1355
|
-
version:
|
|
1359
|
+
version: VERSION,
|
|
1360
|
+
commit: COMMIT,
|
|
1356
1361
|
hash: manifestHash,
|
|
1357
1362
|
collections: manifestCollections,
|
|
1358
1363
|
plugins: manifestPlugins,
|
|
@@ -1364,11 +1369,12 @@ export class EmDashRuntime {
|
|
|
1364
1369
|
}
|
|
1365
1370
|
|
|
1366
1371
|
/**
|
|
1367
|
-
* Invalidate
|
|
1368
|
-
*
|
|
1372
|
+
* Invalidate cached data derived from the manifest/schema.
|
|
1373
|
+
* Called when collections are created, updated, or deleted.
|
|
1369
1374
|
*/
|
|
1370
1375
|
invalidateManifest(): void {
|
|
1371
|
-
//
|
|
1376
|
+
// Invalidate the URL pattern cache used by resolveEmDashPath
|
|
1377
|
+
invalidateUrlPatternCache();
|
|
1372
1378
|
}
|
|
1373
1379
|
|
|
1374
1380
|
// =========================================================================
|
|
@@ -1540,6 +1546,7 @@ export class EmDashRuntime {
|
|
|
1540
1546
|
});
|
|
1541
1547
|
|
|
1542
1548
|
// Update entry to point to new draft (metadata only, not data columns)
|
|
1549
|
+
validateIdentifier(collection, "collection");
|
|
1543
1550
|
const tableName = `ec_${collection}`;
|
|
1544
1551
|
await sql`
|
|
1545
1552
|
UPDATE ${sql.ref(tableName)}
|
|
@@ -1609,7 +1616,7 @@ export class EmDashRuntime {
|
|
|
1609
1616
|
|
|
1610
1617
|
// Run afterDelete hooks (fire-and-forget)
|
|
1611
1618
|
if (result.success) {
|
|
1612
|
-
this.runAfterDeleteHooks(id, collection);
|
|
1619
|
+
this.runAfterDeleteHooks(id, collection, false);
|
|
1613
1620
|
}
|
|
1614
1621
|
|
|
1615
1622
|
return result;
|
|
@@ -1631,7 +1638,14 @@ export class EmDashRuntime {
|
|
|
1631
1638
|
}
|
|
1632
1639
|
|
|
1633
1640
|
async handleContentPermanentDelete(collection: string, id: string) {
|
|
1634
|
-
|
|
1641
|
+
const result = await handleContentPermanentDelete(this.db, collection, id);
|
|
1642
|
+
|
|
1643
|
+
// Run afterDelete hooks so plugins (e.g. AI Search) can clean up
|
|
1644
|
+
if (result.success) {
|
|
1645
|
+
this.runAfterDeleteHooks(id, collection, true);
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
return result;
|
|
1635
1649
|
}
|
|
1636
1650
|
|
|
1637
1651
|
async handleContentCountTrashed(collection: string) {
|
|
@@ -2006,11 +2020,11 @@ export class EmDashRuntime {
|
|
|
2006
2020
|
}
|
|
2007
2021
|
}
|
|
2008
2022
|
|
|
2009
|
-
private runAfterDeleteHooks(id: string, collection: string): void {
|
|
2023
|
+
private runAfterDeleteHooks(id: string, collection: string, permanent: boolean): void {
|
|
2010
2024
|
// Trusted plugins
|
|
2011
2025
|
if (this.hooks.hasHooks("content:afterDelete")) {
|
|
2012
2026
|
this.hooks
|
|
2013
|
-
.runContentAfterDelete(id, collection)
|
|
2027
|
+
.runContentAfterDelete(id, collection, permanent)
|
|
2014
2028
|
.catch((err) => console.error("EmDash afterDelete hook error:", err));
|
|
2015
2029
|
}
|
|
2016
2030
|
|
|
@@ -2020,7 +2034,7 @@ export class EmDashRuntime {
|
|
|
2020
2034
|
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2021
2035
|
|
|
2022
2036
|
plugin
|
|
2023
|
-
.invokeHook("content:afterDelete", { id, collection })
|
|
2037
|
+
.invokeHook("content:afterDelete", { id, collection, permanent })
|
|
2024
2038
|
.catch((err) =>
|
|
2025
2039
|
console.error(`EmDash: Sandboxed plugin ${pluginId} afterDelete error:`, err),
|
|
2026
2040
|
);
|
package/src/index.ts
CHANGED
|
@@ -102,6 +102,7 @@ export type {
|
|
|
102
102
|
export { ulid } from "ulidx";
|
|
103
103
|
export { computeContentHash, hashString } from "./utils/hash.js";
|
|
104
104
|
export { sanitizeHref, isSafeHref } from "./utils/url.js";
|
|
105
|
+
export { decodeSlug } from "./utils/slugify.js";
|
|
105
106
|
|
|
106
107
|
// Live Collections query functions (loader is in emdash/runtime)
|
|
107
108
|
export {
|
|
@@ -212,6 +213,8 @@ export type {
|
|
|
212
213
|
ResolvedHook,
|
|
213
214
|
ResolvedPluginHooks,
|
|
214
215
|
ContentHookEvent,
|
|
216
|
+
ContentDeleteEvent,
|
|
217
|
+
ContentPublishStateChangeEvent,
|
|
215
218
|
MediaUploadEvent,
|
|
216
219
|
HookResult,
|
|
217
220
|
PluginRoute,
|
package/src/loader.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { Kysely, sql, type Dialect } from "kysely";
|
|
|
16
16
|
|
|
17
17
|
import { currentTimestampValue, isPostgres } from "./database/dialect-helpers.js";
|
|
18
18
|
import { decodeCursor, encodeCursor } from "./database/repositories/types.js";
|
|
19
|
+
import { validateIdentifier } from "./database/validate.js";
|
|
19
20
|
import type { Database } from "./index.js";
|
|
20
21
|
import { getRequestContext } from "./request-context.js";
|
|
21
22
|
|
|
@@ -50,6 +51,7 @@ const SYSTEM_COLUMNS = new Set([
|
|
|
50
51
|
* Get the table name for a collection type
|
|
51
52
|
*/
|
|
52
53
|
function getTableName(type: string): string {
|
|
54
|
+
validateIdentifier(type, "collection type");
|
|
53
55
|
return `ec_${type}`;
|
|
54
56
|
}
|
|
55
57
|
|