emdash 0.7.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-5uslYdUu.mjs → apply-x0eMK1lX.mjs} +18 -17
- 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 +86 -15
- 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 +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +259 -71
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +16 -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 +16 -12
- 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-D7J5y73J.mjs → content-BcQPYxdV.mjs} +13 -15
- package/dist/content-BcQPYxdV.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- 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-De6_Xv3v.d.mts → index-DIb-CzNx.d.mts} +157 -14
- package/dist/index-DIb-CzNx.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +22 -20
- 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-g4Ug-9j9.mjs → query-fqEdLFms.mjs} +9 -9
- package/dist/{query-g4Ug-9j9.mjs.map → query-fqEdLFms.mjs.map} +1 -1
- package/dist/{redirect-CN0Rt9Ob.mjs → redirect-D_pshWdf.mjs} +4 -4
- 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-BR2xKwhn.d.mts → runner-OURCaApa.d.mts} +2 -2
- package/dist/{runner-BR2xKwhn.d.mts.map → runner-OURCaApa.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +2 -2
- package/dist/{search-B0effn3j.mjs → search-BoZYFuUk.mjs} +227 -84
- 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-K2z0Uhnj.mjs → taxonomies-B4IAshV8.mjs} +5 -5
- package/dist/{taxonomies-K2z0Uhnj.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-C2v0c34j.d.mts → types-CS8FIX7L.d.mts} +1 -1
- package/dist/{types-C2v0c34j.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-kM8Pjuf7.d.mts → validate-DHxmpFJt.d.mts} +4 -4
- package/dist/{validate-kM8Pjuf7.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-BnTKdfam.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 +122 -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/setup.ts +8 -0
- package/src/api/schemas/widgets.ts +12 -10
- package/src/api/setup-complete.ts +40 -0
- package/src/astro/integration/index.ts +13 -2
- package/src/astro/integration/routes.ts +28 -0
- package/src/astro/integration/runtime.ts +19 -1
- 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/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/content/[collection]/[id]/translations.ts +1 -1
- package/src/astro/routes/api/content/[collection]/index.ts +1 -9
- 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/settings/email.ts +4 -9
- package/src/astro/routes/api/setup/admin.ts +8 -2
- package/src/astro/routes/api/setup/index.ts +2 -2
- 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 +9 -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/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/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 +40 -40
- package/src/database/repositories/index.ts +1 -1
- package/src/database/repositories/media.ts +10 -13
- package/src/database/repositories/plugin-storage.ts +4 -6
- package/src/database/repositories/redirect.ts +12 -16
- 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/emdash-runtime.ts +306 -90
- package/src/index.ts +5 -1
- package/src/loader.ts +6 -5
- package/src/mcp/server.ts +678 -105
- package/src/media/normalize.ts +1 -1
- package/src/media/url.ts +78 -0
- package/src/plugins/email-console.ts +10 -3
- package/src/plugins/hooks.ts +11 -0
- package/src/plugins/manifest-schema.ts +12 -0
- 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/storage/s3.ts +12 -6
- package/src/virtual-modules.d.ts +21 -1
- package/src/widgets/index.ts +1 -1
- package/dist/apply-5uslYdUu.mjs.map +0 -1
- package/dist/byline-C4OVd8b3.mjs.map +0 -1
- package/dist/content-D7J5y73J.mjs.map +0 -1
- package/dist/error-CiYn9yDu.mjs.map +0 -1
- package/dist/index-De6_Xv3v.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-CN0Rt9Ob.mjs.map +0 -1
- package/dist/registry-Ci3WxVAr.mjs.map +0 -1
- package/dist/request-cache-DiR961CV.mjs.map +0 -1
- package/dist/search-B0effn3j.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-BnTKdfam.mjs +0 -7
package/src/emdash-runtime.ts
CHANGED
|
@@ -46,6 +46,20 @@ import { COMMIT, VERSION } from "./version.js";
|
|
|
46
46
|
|
|
47
47
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Parse a JSON column expected to contain an array of strings.
|
|
51
|
+
*
|
|
52
|
+
* Throws on malformed JSON rather than returning []; callers are responsible
|
|
53
|
+
* for deciding how to handle/log the error. Empty string / null inputs return
|
|
54
|
+
* [] (they represent "no value"). Non-string array entries are filtered out.
|
|
55
|
+
*/
|
|
56
|
+
function parseStringArray(raw: string | null | undefined): string[] {
|
|
57
|
+
if (!raw) return [];
|
|
58
|
+
const parsed: unknown = JSON.parse(raw);
|
|
59
|
+
if (!Array.isArray(parsed)) return [];
|
|
60
|
+
return parsed.filter((v): v is string => typeof v === "string");
|
|
61
|
+
}
|
|
62
|
+
|
|
49
63
|
/** Combined result from a single-pass page contribution collection */
|
|
50
64
|
interface PageContributions {
|
|
51
65
|
metadata: PageMetadataContribution[];
|
|
@@ -237,6 +251,45 @@ export interface RuntimeDependencies {
|
|
|
237
251
|
createSandboxRunner: ((opts: { db: Kysely<Database> }) => SandboxRunner) | null;
|
|
238
252
|
}
|
|
239
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Constructor parameters for `EmDashRuntime`.
|
|
256
|
+
*
|
|
257
|
+
* Production code should use `EmDashRuntime.create()` which discovers and
|
|
258
|
+
* loads all parts (database, plugins, hooks, cron, etc.) and then calls the
|
|
259
|
+
* constructor. Direct construction is supported for callers that already
|
|
260
|
+
* have all the dependencies in hand — for example, integration tests that
|
|
261
|
+
* supply a pre-migrated database and an empty plugin set.
|
|
262
|
+
*
|
|
263
|
+
* Every field corresponds 1:1 to internal state set on the runtime — none of
|
|
264
|
+
* these are derived. If you don't have a value for one, see what `create()`
|
|
265
|
+
* passes for that field as the canonical default.
|
|
266
|
+
*/
|
|
267
|
+
export interface EmDashRuntimeParts {
|
|
268
|
+
db: Kysely<Database>;
|
|
269
|
+
storage: Storage | null;
|
|
270
|
+
configuredPlugins: ResolvedPlugin[];
|
|
271
|
+
sandboxedPlugins: Map<string, SandboxedPlugin>;
|
|
272
|
+
sandboxedPluginEntries: SandboxedPluginEntry[];
|
|
273
|
+
hooks: HookPipeline;
|
|
274
|
+
enabledPlugins: Set<string>;
|
|
275
|
+
pluginStates: Map<string, string>;
|
|
276
|
+
config: EmDashConfig;
|
|
277
|
+
mediaProviders: Map<string, MediaProvider>;
|
|
278
|
+
mediaProviderEntries: MediaProviderEntry[];
|
|
279
|
+
cronExecutor: CronExecutor | null;
|
|
280
|
+
cronScheduler: CronScheduler | null;
|
|
281
|
+
emailPipeline: EmailPipeline | null;
|
|
282
|
+
allPipelinePlugins: ResolvedPlugin[];
|
|
283
|
+
pipelineFactoryOptions: {
|
|
284
|
+
db: Kysely<Database>;
|
|
285
|
+
storage?: Storage;
|
|
286
|
+
siteInfo?: { siteName?: string; siteUrl?: string; locale?: string };
|
|
287
|
+
};
|
|
288
|
+
runtimeDeps: RuntimeDependencies;
|
|
289
|
+
pipelineRef: { current: HookPipeline };
|
|
290
|
+
manifestCacheKey: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
240
293
|
/**
|
|
241
294
|
* Convert a ContentItem to Record<string, unknown> for hook consumption.
|
|
242
295
|
* Hooks receive the full item as a flat record.
|
|
@@ -337,51 +390,27 @@ export class EmDashRuntime {
|
|
|
337
390
|
return this._db;
|
|
338
391
|
}
|
|
339
392
|
|
|
340
|
-
|
|
341
|
-
db
|
|
342
|
-
storage
|
|
343
|
-
configuredPlugins
|
|
344
|
-
sandboxedPlugins
|
|
345
|
-
sandboxedPluginEntries
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
runtimeDeps: RuntimeDependencies,
|
|
362
|
-
pipelineRef: { current: HookPipeline },
|
|
363
|
-
manifestCacheKey: string,
|
|
364
|
-
) {
|
|
365
|
-
this._db = db;
|
|
366
|
-
this.storage = storage;
|
|
367
|
-
this.configuredPlugins = configuredPlugins;
|
|
368
|
-
this.sandboxedPlugins = sandboxedPlugins;
|
|
369
|
-
this.sandboxedPluginEntries = sandboxedPluginEntries;
|
|
370
|
-
this.schemaRegistry = new SchemaRegistry(db);
|
|
371
|
-
this._hooks = hooks;
|
|
372
|
-
this.enabledPlugins = enabledPlugins;
|
|
373
|
-
this.pluginStates = pluginStates;
|
|
374
|
-
this.config = config;
|
|
375
|
-
this.mediaProviders = mediaProviders;
|
|
376
|
-
this.mediaProviderEntries = mediaProviderEntries;
|
|
377
|
-
this.cronExecutor = cronExecutor;
|
|
378
|
-
this.cronScheduler = cronScheduler;
|
|
379
|
-
this.email = emailPipeline;
|
|
380
|
-
this.allPipelinePlugins = allPipelinePlugins;
|
|
381
|
-
this.pipelineFactoryOptions = pipelineFactoryOptions;
|
|
382
|
-
this.runtimeDeps = runtimeDeps;
|
|
383
|
-
this.pipelineRef = pipelineRef;
|
|
384
|
-
this._manifestCacheKey = manifestCacheKey;
|
|
393
|
+
constructor(parts: EmDashRuntimeParts) {
|
|
394
|
+
this._db = parts.db;
|
|
395
|
+
this.storage = parts.storage;
|
|
396
|
+
this.configuredPlugins = parts.configuredPlugins;
|
|
397
|
+
this.sandboxedPlugins = parts.sandboxedPlugins;
|
|
398
|
+
this.sandboxedPluginEntries = parts.sandboxedPluginEntries;
|
|
399
|
+
this.schemaRegistry = new SchemaRegistry(parts.db);
|
|
400
|
+
this._hooks = parts.hooks;
|
|
401
|
+
this.enabledPlugins = parts.enabledPlugins;
|
|
402
|
+
this.pluginStates = parts.pluginStates;
|
|
403
|
+
this.config = parts.config;
|
|
404
|
+
this.mediaProviders = parts.mediaProviders;
|
|
405
|
+
this.mediaProviderEntries = parts.mediaProviderEntries;
|
|
406
|
+
this.cronExecutor = parts.cronExecutor;
|
|
407
|
+
this.cronScheduler = parts.cronScheduler;
|
|
408
|
+
this.email = parts.emailPipeline;
|
|
409
|
+
this.allPipelinePlugins = parts.allPipelinePlugins;
|
|
410
|
+
this.pipelineFactoryOptions = parts.pipelineFactoryOptions;
|
|
411
|
+
this.runtimeDeps = parts.runtimeDeps;
|
|
412
|
+
this.pipelineRef = parts.pipelineRef;
|
|
413
|
+
this._manifestCacheKey = parts.manifestCacheKey;
|
|
385
414
|
}
|
|
386
415
|
|
|
387
416
|
/**
|
|
@@ -858,16 +887,16 @@ export class EmDashRuntime {
|
|
|
858
887
|
].join("|"),
|
|
859
888
|
);
|
|
860
889
|
|
|
861
|
-
return new EmDashRuntime(
|
|
890
|
+
return new EmDashRuntime({
|
|
862
891
|
db,
|
|
863
892
|
storage,
|
|
864
|
-
deps.plugins,
|
|
893
|
+
configuredPlugins: deps.plugins,
|
|
865
894
|
sandboxedPlugins,
|
|
866
|
-
deps.sandboxedPluginEntries,
|
|
867
|
-
pipeline,
|
|
895
|
+
sandboxedPluginEntries: deps.sandboxedPluginEntries,
|
|
896
|
+
hooks: pipeline,
|
|
868
897
|
enabledPlugins,
|
|
869
898
|
pluginStates,
|
|
870
|
-
deps.config,
|
|
899
|
+
config: deps.config,
|
|
871
900
|
mediaProviders,
|
|
872
901
|
mediaProviderEntries,
|
|
873
902
|
cronExecutor,
|
|
@@ -875,10 +904,10 @@ export class EmDashRuntime {
|
|
|
875
904
|
emailPipeline,
|
|
876
905
|
allPipelinePlugins,
|
|
877
906
|
pipelineFactoryOptions,
|
|
878
|
-
deps,
|
|
907
|
+
runtimeDeps: deps,
|
|
879
908
|
pipelineRef,
|
|
880
909
|
manifestCacheKey,
|
|
881
|
-
);
|
|
910
|
+
});
|
|
882
911
|
}
|
|
883
912
|
|
|
884
913
|
/**
|
|
@@ -1489,7 +1518,7 @@ export class EmDashRuntime {
|
|
|
1489
1518
|
label: row.label,
|
|
1490
1519
|
labelSingular: row.label_singular ?? undefined,
|
|
1491
1520
|
hierarchical: row.hierarchical === 1,
|
|
1492
|
-
collections:
|
|
1521
|
+
collections: parseStringArray(row.collections).toSorted(),
|
|
1493
1522
|
}));
|
|
1494
1523
|
} catch (error) {
|
|
1495
1524
|
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
@@ -1615,11 +1644,75 @@ export class EmDashRuntime {
|
|
|
1615
1644
|
}
|
|
1616
1645
|
|
|
1617
1646
|
async handleContentGet(collection: string, id: string, locale?: string) {
|
|
1618
|
-
|
|
1647
|
+
const result = await handleContentGet(this.db, collection, id, locale);
|
|
1648
|
+
return this.hydrateDraftData(result);
|
|
1619
1649
|
}
|
|
1620
1650
|
|
|
1621
1651
|
async handleContentGetIncludingTrashed(collection: string, id: string, locale?: string) {
|
|
1622
|
-
|
|
1652
|
+
const result = await handleContentGetIncludingTrashed(this.db, collection, id, locale);
|
|
1653
|
+
return this.hydrateDraftData(result);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/**
|
|
1657
|
+
* If the response item has a `draftRevisionId`, replace `item.data` with
|
|
1658
|
+
* the draft revision's data and expose the original published values as
|
|
1659
|
+
* `liveData`. This makes the content_get / content_update round-trip
|
|
1660
|
+
* intuitive — read returns the latest content the caller has saved
|
|
1661
|
+
* (their pending draft), with the previously-published values still
|
|
1662
|
+
* accessible for compare-style flows.
|
|
1663
|
+
*
|
|
1664
|
+
* No-op when no draft exists or the response is an error.
|
|
1665
|
+
*/
|
|
1666
|
+
private async hydrateDraftData<T>(result: T): Promise<T> {
|
|
1667
|
+
if (!result || typeof result !== "object") return result;
|
|
1668
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape probed below
|
|
1669
|
+
const r = result as {
|
|
1670
|
+
success?: boolean;
|
|
1671
|
+
data?: { item?: Record<string, unknown> };
|
|
1672
|
+
};
|
|
1673
|
+
if (!r.success || !r.data?.item) return result;
|
|
1674
|
+
const item = r.data.item;
|
|
1675
|
+
const draftRevisionId = typeof item.draftRevisionId === "string" ? item.draftRevisionId : null;
|
|
1676
|
+
if (!draftRevisionId) return result;
|
|
1677
|
+
try {
|
|
1678
|
+
const revision = await new RevisionRepository(this.db).findById(draftRevisionId);
|
|
1679
|
+
if (!revision) return result;
|
|
1680
|
+
const liveData =
|
|
1681
|
+
item.data && typeof item.data === "object"
|
|
1682
|
+
? // eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- narrowed to object above
|
|
1683
|
+
(item.data as Record<string, unknown>)
|
|
1684
|
+
: {};
|
|
1685
|
+
// Strip leading-underscore keys (`_slug`, `_rev`, etc.) from the
|
|
1686
|
+
// revision data — those are handler-internal markers and don't
|
|
1687
|
+
// belong in the surfaced `data` field. Match syncDataColumns at
|
|
1688
|
+
// content.ts:~1119.
|
|
1689
|
+
const revisionData: Record<string, unknown> = {};
|
|
1690
|
+
for (const [key, value] of Object.entries(revision.data)) {
|
|
1691
|
+
if (!key.startsWith("_")) revisionData[key] = value;
|
|
1692
|
+
}
|
|
1693
|
+
const mergedData = { ...liveData, ...revisionData };
|
|
1694
|
+
// Return a clone rather than mutating in place. The response
|
|
1695
|
+
// object isn't retained by the runtime today, but a future
|
|
1696
|
+
// request-cache layer would observe stale-after-mutation bugs;
|
|
1697
|
+
// cloning closes that footgun.
|
|
1698
|
+
// `r.data` was narrowed to `{ item?: ... }` at the top of this
|
|
1699
|
+
// method; spread its other keys (e.g. `_rev`) alongside the
|
|
1700
|
+
// hydrated item without going back through `unknown`.
|
|
1701
|
+
return {
|
|
1702
|
+
...result,
|
|
1703
|
+
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- shape preserved; result has been narrowed to the {success,data:{item}} envelope
|
|
1704
|
+
data: {
|
|
1705
|
+
...r.data,
|
|
1706
|
+
item: { ...item, data: mergedData, liveData },
|
|
1707
|
+
},
|
|
1708
|
+
} as T;
|
|
1709
|
+
} catch (error) {
|
|
1710
|
+
// Non-fatal — fall back to the unhydrated response. Log so the
|
|
1711
|
+
// failure isn't completely silent (the response will look stale
|
|
1712
|
+
// to the caller but no error is raised).
|
|
1713
|
+
console.error("[emdash] draft hydration failed:", error);
|
|
1714
|
+
return result;
|
|
1715
|
+
}
|
|
1623
1716
|
}
|
|
1624
1717
|
|
|
1625
1718
|
async handleContentCreate(
|
|
@@ -1647,6 +1740,20 @@ export class EmDashRuntime {
|
|
|
1647
1740
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
1648
1741
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1649
1742
|
|
|
1743
|
+
// Validate against the collection schema. Hook output is validated
|
|
1744
|
+
// rather than `body.data` so plugins that mutate field values can't
|
|
1745
|
+
// sneak invalid data past.
|
|
1746
|
+
const { validateContentData } = await import("./api/handlers/validation.js");
|
|
1747
|
+
const validation = await validateContentData(this.db, collection, processedData, {
|
|
1748
|
+
partial: false,
|
|
1749
|
+
});
|
|
1750
|
+
if (!validation.ok) {
|
|
1751
|
+
return {
|
|
1752
|
+
success: false as const,
|
|
1753
|
+
error: validation.error,
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1650
1757
|
// Create the content
|
|
1651
1758
|
const result = await handleContentCreate(this.db, collection, {
|
|
1652
1759
|
...body,
|
|
@@ -1720,6 +1827,19 @@ export class EmDashRuntime {
|
|
|
1720
1827
|
|
|
1721
1828
|
// Normalize media fields (fill dimensions, storageKey, etc.)
|
|
1722
1829
|
processedData = await this.normalizeMediaFields(collection, processedData);
|
|
1830
|
+
|
|
1831
|
+
// Validate field-level shape BEFORE the draft-revision write so
|
|
1832
|
+
// invalid updates can't silently land in revision history.
|
|
1833
|
+
const { validateContentData } = await import("./api/handlers/validation.js");
|
|
1834
|
+
const validation = await validateContentData(this.db, collection, processedData, {
|
|
1835
|
+
partial: true,
|
|
1836
|
+
});
|
|
1837
|
+
if (!validation.ok) {
|
|
1838
|
+
return {
|
|
1839
|
+
success: false as const,
|
|
1840
|
+
error: validation.error,
|
|
1841
|
+
};
|
|
1842
|
+
}
|
|
1723
1843
|
}
|
|
1724
1844
|
|
|
1725
1845
|
// Draft-aware revision handling (if collection supports revisions)
|
|
@@ -1795,12 +1915,18 @@ export class EmDashRuntime {
|
|
|
1795
1915
|
bylines: bodyWithoutRev.bylines,
|
|
1796
1916
|
});
|
|
1797
1917
|
|
|
1918
|
+
// Hydrate draft data BEFORE firing afterSave hooks so the hook sees
|
|
1919
|
+
// the same effective data the response surfaces — for revision-
|
|
1920
|
+
// supporting collections, that's the just-saved draft, not the live
|
|
1921
|
+
// columns.
|
|
1922
|
+
const hydrated = await this.hydrateDraftData(result);
|
|
1923
|
+
|
|
1798
1924
|
// Run afterSave hooks (fire-and-forget)
|
|
1799
|
-
if (
|
|
1800
|
-
this.runAfterSaveHooks(contentItemToRecord(
|
|
1925
|
+
if (hydrated.success && hydrated.data) {
|
|
1926
|
+
this.runAfterSaveHooks(contentItemToRecord(hydrated.data.item), collection, false);
|
|
1801
1927
|
}
|
|
1802
1928
|
|
|
1803
|
-
return
|
|
1929
|
+
return hydrated;
|
|
1804
1930
|
}
|
|
1805
1931
|
|
|
1806
1932
|
async handleContentDelete(collection: string, id: string) {
|
|
@@ -1947,6 +2073,7 @@ export class EmDashRuntime {
|
|
|
1947
2073
|
contentHash?: string;
|
|
1948
2074
|
blurhash?: string;
|
|
1949
2075
|
dominantColor?: string;
|
|
2076
|
+
authorId?: string;
|
|
1950
2077
|
}) {
|
|
1951
2078
|
// Run beforeUpload hooks
|
|
1952
2079
|
let processedInput = input;
|
|
@@ -2010,7 +2137,74 @@ export class EmDashRuntime {
|
|
|
2010
2137
|
}
|
|
2011
2138
|
|
|
2012
2139
|
async handleRevisionRestore(revisionId: string, callerUserId: string) {
|
|
2013
|
-
|
|
2140
|
+
// Discover the parent entry up front so we can branch on whether
|
|
2141
|
+
// the collection uses draft revisions.
|
|
2142
|
+
const revisionRepo = new RevisionRepository(this.db);
|
|
2143
|
+
const revision = await revisionRepo.findById(revisionId);
|
|
2144
|
+
if (!revision) {
|
|
2145
|
+
return {
|
|
2146
|
+
success: false as const,
|
|
2147
|
+
error: {
|
|
2148
|
+
code: "NOT_FOUND",
|
|
2149
|
+
message: `Revision not found: ${revisionId}`,
|
|
2150
|
+
},
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
const collectionInfo = await this.schemaRegistry.getCollectionWithFields(revision.collection);
|
|
2155
|
+
const usesDraftRevisions = collectionInfo?.supports?.includes("revisions") ?? false;
|
|
2156
|
+
|
|
2157
|
+
// Non-revision collections: keep the legacy behavior of writing the
|
|
2158
|
+
// revision's data straight onto the live row. This preserves
|
|
2159
|
+
// behavior for collections that opt out of the draft model.
|
|
2160
|
+
if (!usesDraftRevisions) {
|
|
2161
|
+
const result = await handleRevisionRestore(this.db, revisionId, callerUserId);
|
|
2162
|
+
return this.hydrateDraftData(result);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
// Revision-capable collections: restore is "make this revision the
|
|
2166
|
+
// current draft". The live row's data columns are left untouched
|
|
2167
|
+
// (only `draft_revision_id` and `updated_at` change). The caller
|
|
2168
|
+
// must then `content_publish` to promote the restored draft to
|
|
2169
|
+
// live, matching the documented tool contract.
|
|
2170
|
+
try {
|
|
2171
|
+
const newDraft = await revisionRepo.create({
|
|
2172
|
+
collection: revision.collection,
|
|
2173
|
+
entryId: revision.entryId,
|
|
2174
|
+
data: revision.data,
|
|
2175
|
+
authorId: callerUserId,
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
validateIdentifier(revision.collection, "collection");
|
|
2179
|
+
const tableName = `ec_${revision.collection}`;
|
|
2180
|
+
await sql`
|
|
2181
|
+
UPDATE ${sql.ref(tableName)}
|
|
2182
|
+
SET draft_revision_id = ${newDraft.id},
|
|
2183
|
+
updated_at = ${new Date().toISOString()}
|
|
2184
|
+
WHERE id = ${revision.entryId}
|
|
2185
|
+
`.execute(this.db);
|
|
2186
|
+
|
|
2187
|
+
// Fire-and-forget: prune old revisions to prevent unbounded growth
|
|
2188
|
+
void revisionRepo
|
|
2189
|
+
.pruneOldRevisions(revision.collection, revision.entryId, 50)
|
|
2190
|
+
.catch(() => {});
|
|
2191
|
+
|
|
2192
|
+
// Return the freshly-fetched item with the new draft hydrated
|
|
2193
|
+
// onto `data`. Without this the response would echo the live
|
|
2194
|
+
// columns and the next `content_get` would surface different
|
|
2195
|
+
// values (the bug that motivated this rewrite).
|
|
2196
|
+
const refetched = await handleContentGet(this.db, revision.collection, revision.entryId);
|
|
2197
|
+
return this.hydrateDraftData(refetched);
|
|
2198
|
+
} catch (error) {
|
|
2199
|
+
console.error("[emdash] revision restore failed:", error);
|
|
2200
|
+
return {
|
|
2201
|
+
success: false as const,
|
|
2202
|
+
error: {
|
|
2203
|
+
code: "REVISION_RESTORE_ERROR",
|
|
2204
|
+
message: "Failed to restore revision",
|
|
2205
|
+
},
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2014
2208
|
}
|
|
2015
2209
|
|
|
2016
2210
|
// =========================================================================
|
|
@@ -2222,22 +2416,34 @@ export class EmDashRuntime {
|
|
|
2222
2416
|
collection: string,
|
|
2223
2417
|
isNew: boolean,
|
|
2224
2418
|
): void {
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
this.hooks
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
const [id] = pluginKey.split(":");
|
|
2235
|
-
if (!id || !this.isPluginEnabled(id)) continue;
|
|
2419
|
+
after(async () => {
|
|
2420
|
+
// Trusted plugins
|
|
2421
|
+
if (this.hooks.hasHooks("content:afterSave")) {
|
|
2422
|
+
try {
|
|
2423
|
+
await this.hooks.runContentAfterSave(content, collection, isNew);
|
|
2424
|
+
} catch (err) {
|
|
2425
|
+
console.error("EmDash afterSave hook error:", err);
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2236
2428
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2429
|
+
// Sandboxed plugins
|
|
2430
|
+
const tasks: Promise<void>[] = [];
|
|
2431
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
2432
|
+
const [id] = pluginKey.split(":");
|
|
2433
|
+
if (!id || !this.isPluginEnabled(id)) continue;
|
|
2434
|
+
|
|
2435
|
+
tasks.push(
|
|
2436
|
+
(async () => {
|
|
2437
|
+
try {
|
|
2438
|
+
await plugin.invokeHook("content:afterSave", { content, collection, isNew });
|
|
2439
|
+
} catch (err) {
|
|
2440
|
+
console.error(`EmDash: Sandboxed plugin ${id} afterSave error:`, err);
|
|
2441
|
+
}
|
|
2442
|
+
})(),
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
await Promise.allSettled(tasks);
|
|
2446
|
+
});
|
|
2241
2447
|
}
|
|
2242
2448
|
|
|
2243
2449
|
private runAfterDeleteHooks(id: string, collection: string, permanent: boolean): void {
|
|
@@ -2262,24 +2468,34 @@ export class EmDashRuntime {
|
|
|
2262
2468
|
}
|
|
2263
2469
|
|
|
2264
2470
|
private runAfterPublishHooks(content: Record<string, unknown>, collection: string): void {
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
this.hooks
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
const [pluginId] = pluginKey.split(":");
|
|
2275
|
-
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2471
|
+
after(async () => {
|
|
2472
|
+
// Trusted plugins
|
|
2473
|
+
if (this.hooks.hasHooks("content:afterPublish")) {
|
|
2474
|
+
try {
|
|
2475
|
+
await this.hooks.runContentAfterPublish(content, collection);
|
|
2476
|
+
} catch (err) {
|
|
2477
|
+
console.error("EmDash afterPublish hook error:", err);
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2276
2480
|
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2481
|
+
// Sandboxed plugins
|
|
2482
|
+
const tasks: Promise<void>[] = [];
|
|
2483
|
+
for (const [pluginKey, plugin] of this.sandboxedPlugins) {
|
|
2484
|
+
const [pluginId] = pluginKey.split(":");
|
|
2485
|
+
if (!pluginId || !this.isPluginEnabled(pluginId)) continue;
|
|
2486
|
+
|
|
2487
|
+
tasks.push(
|
|
2488
|
+
(async () => {
|
|
2489
|
+
try {
|
|
2490
|
+
await plugin.invokeHook("content:afterPublish", { content, collection });
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
console.error(`EmDash: Sandboxed plugin ${pluginId} afterPublish error:`, err);
|
|
2493
|
+
}
|
|
2494
|
+
})(),
|
|
2281
2495
|
);
|
|
2282
|
-
|
|
2496
|
+
}
|
|
2497
|
+
await Promise.allSettled(tasks);
|
|
2498
|
+
});
|
|
2283
2499
|
}
|
|
2284
2500
|
|
|
2285
2501
|
private runAfterUnpublishHooks(content: Record<string, unknown>, collection: string): void {
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
ContentRepository,
|
|
14
14
|
MediaRepository,
|
|
15
15
|
EmDashValidationError,
|
|
16
|
+
InvalidCursorError,
|
|
16
17
|
} from "./database/repositories/index.js";
|
|
17
18
|
export type {
|
|
18
19
|
ContentItem,
|
|
@@ -480,11 +481,14 @@ export type {
|
|
|
480
481
|
SearchStats,
|
|
481
482
|
} from "./search/index.js";
|
|
482
483
|
|
|
483
|
-
// Auth types (for platform-specific auth providers)
|
|
484
|
+
// Auth types (for platform-specific auth providers and pluggable login methods)
|
|
484
485
|
export type {
|
|
485
486
|
AuthDescriptor,
|
|
487
|
+
AuthProviderDescriptor,
|
|
488
|
+
AuthProviderAdminExports,
|
|
486
489
|
AuthProviderModule,
|
|
487
490
|
AuthResult,
|
|
491
|
+
AuthRouteDescriptor,
|
|
488
492
|
ExternalAuthConfig,
|
|
489
493
|
} from "./auth/types.js";
|
|
490
494
|
|
package/src/loader.ts
CHANGED
|
@@ -318,16 +318,17 @@ function buildOrderByClause(
|
|
|
318
318
|
/**
|
|
319
319
|
* Build a cursor WHERE condition for keyset pagination.
|
|
320
320
|
* Uses the primary sort field + id as tiebreaker for stable ordering.
|
|
321
|
+
*
|
|
322
|
+
* Throws `InvalidCursorError` if the cursor is malformed; callers should
|
|
323
|
+
* let this propagate so users see a real error rather than silently
|
|
324
|
+
* falling back to the first page.
|
|
321
325
|
*/
|
|
322
326
|
function buildCursorCondition(
|
|
323
327
|
cursor: string,
|
|
324
328
|
orderBy: OrderBySpec | undefined,
|
|
325
329
|
tablePrefix?: string,
|
|
326
|
-
): ReturnType<typeof sql>
|
|
327
|
-
const
|
|
328
|
-
if (!decoded) return null;
|
|
329
|
-
|
|
330
|
-
const { orderValue, id: cursorId } = decoded;
|
|
330
|
+
): ReturnType<typeof sql> {
|
|
331
|
+
const { orderValue, id: cursorId } = decodeCursor(cursor);
|
|
331
332
|
const primary = getPrimarySort(orderBy, tablePrefix);
|
|
332
333
|
const idField = tablePrefix ? `${tablePrefix}.id` : "id";
|
|
333
334
|
|