emdash 0.4.0 → 0.6.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-C2BzVy0p.d.mts → adapters-Di31kZ28.d.mts} +16 -1
- package/dist/adapters-Di31kZ28.d.mts.map +1 -0
- package/dist/{apply-Cma_PiF6.mjs → apply-B4MsLM-w.mjs} +27 -12
- package/dist/apply-B4MsLM-w.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 +208 -34
- 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 +34 -9
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware/request-context.d.mts.map +1 -1
- package/dist/astro/middleware/request-context.mjs +5 -3
- 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 +460 -180
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +8 -8
- package/dist/{byline-WuOq9MFJ.mjs → byline-C4OVd8b3.mjs} +3 -19
- package/dist/byline-C4OVd8b3.mjs.map +1 -0
- package/dist/{bylines-C_Wsnz4L.mjs → bylines-hPTW79hw.mjs} +20 -33
- package/dist/bylines-hPTW79hw.mjs.map +1 -0
- package/dist/{cache-E3Dts-yT.mjs → cache-BkKBuIvS.mjs} +1 -1
- package/dist/{cache-E3Dts-yT.mjs.map → cache-BkKBuIvS.mjs.map} +1 -1
- package/dist/chunks-HGz06Soa.mjs +19 -0
- package/dist/chunks-HGz06Soa.mjs.map +1 -0
- package/dist/cli/index.mjs +9 -8
- 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-DkxPrM9l.mjs → config-BXwuX8Bx.mjs} +1 -1
- package/dist/{config-DkxPrM9l.mjs.map → config-BXwuX8Bx.mjs.map} +1 -1
- package/dist/{connection-B4zVnQIa.mjs → connection-2igzM-AT.mjs} +19 -2
- package/dist/connection-2igzM-AT.mjs.map +1 -0
- package/dist/database/instrumentation.d.mts +45 -0
- package/dist/database/instrumentation.d.mts.map +1 -0
- package/dist/database/instrumentation.mjs +61 -0
- package/dist/database/instrumentation.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs.map +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 +41 -0
- package/dist/db-errors-D0UT85nC.mjs.map +1 -0
- package/dist/{default-PUx9RK6u.mjs → default-CME5YdZ3.mjs} +1 -1
- package/dist/{default-PUx9RK6u.mjs.map → default-CME5YdZ3.mjs.map} +1 -1
- package/dist/{error-HBeQbVhV.mjs → error-CiYn9yDu.mjs} +1 -1
- package/dist/{error-HBeQbVhV.mjs.map → error-CiYn9yDu.mjs.map} +1 -1
- package/dist/{index-CRg3PWfZ.d.mts → index-BYv0mB9g.d.mts} +135 -19
- package/dist/index-BYv0mB9g.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +20 -18
- package/dist/{load-BhSSm-TS.mjs → load-CBcmDIot.mjs} +1 -1
- package/dist/{load-BhSSm-TS.mjs.map → load-CBcmDIot.mjs.map} +1 -1
- package/dist/{loader-BYzwzORf.mjs → loader-DeiBJEMe.mjs} +18 -12
- package/dist/loader-DeiBJEMe.mjs.map +1 -0
- package/dist/{manifest-schema-BsXINkQD.mjs → manifest-schema-V30qsMft.mjs} +1 -1
- package/dist/{manifest-schema-BsXINkQD.mjs.map → manifest-schema-V30qsMft.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-CyPLdO3C.mjs → mode-CpNnGkPz.mjs} +1 -1
- package/dist/{mode-CyPLdO3C.mjs.map → mode-CpNnGkPz.mjs.map} +1 -1
- package/dist/page/index.d.mts +11 -2
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +23 -1
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-DntBEQo7.mjs → placeholder-C-fk5hYI.mjs} +1 -1
- package/dist/{placeholder-DntBEQo7.mjs.map → placeholder-C-fk5hYI.mjs.map} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts → placeholder-tzpqGWII.d.mts} +1 -1
- package/dist/{placeholder-BBCtpTES.d.mts.map → placeholder-tzpqGWII.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-B6Vu0d2i.mjs → query-Bk_3vKvU.mjs} +78 -11
- package/dist/query-Bk_3vKvU.mjs.map +1 -0
- package/dist/{registry-BgnP3ysR.mjs → registry-Ci3WxVAr.mjs} +133 -97
- package/dist/registry-Ci3WxVAr.mjs.map +1 -0
- package/dist/request-cache-DiR961CV.mjs +79 -0
- package/dist/request-cache-DiR961CV.mjs.map +1 -0
- package/dist/request-context.d.mts +19 -16
- package/dist/request-context.d.mts.map +1 -1
- package/dist/request-context.mjs.map +1 -1
- package/dist/{runner-DYv3rX8P.d.mts → runner-Fl2NcUUz.d.mts} +2 -2
- package/dist/{runner-DYv3rX8P.d.mts.map → runner-Fl2NcUUz.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-B5p9D36n.mjs → search-DI4bM2w9.mjs} +110 -209
- package/dist/search-DI4bM2w9.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +8 -7
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/taxonomies-DbrKzDju.mjs +308 -0
- package/dist/taxonomies-DbrKzDju.mjs.map +1 -0
- package/dist/{tokens-DKHiCYCB.mjs → tokens-BFPFx3CA.mjs} +1 -1
- package/dist/{tokens-DKHiCYCB.mjs.map → tokens-BFPFx3CA.mjs.map} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs → transport-BykRfpyy.mjs} +1 -1
- package/dist/{transport-BtcQ-Z7T.mjs.map → transport-BykRfpyy.mjs.map} +1 -1
- package/dist/{transport-CKQA_G44.d.mts → transport-H4Iwx7tC.d.mts} +1 -1
- package/dist/{transport-CKQA_G44.d.mts.map → transport-H4Iwx7tC.d.mts.map} +1 -1
- package/dist/{types-BmkQR1En.d.mts → types-6CUZRrZP.d.mts} +1 -1
- package/dist/{types-BmkQR1En.d.mts.map → types-6CUZRrZP.d.mts.map} +1 -1
- package/dist/{types-B6BzlZxx.d.mts → types-8xrvl_68.d.mts} +1 -1
- package/dist/{types-B6BzlZxx.d.mts.map → types-8xrvl_68.d.mts.map} +1 -1
- package/dist/{types-Dz9_WMS6.mjs → types-BH2L167P.mjs} +1 -1
- package/dist/{types-Dz9_WMS6.mjs.map → types-BH2L167P.mjs.map} +1 -1
- package/dist/{types-DNZpaCBk.d.mts → types-CFWjXmus.d.mts} +1 -1
- package/dist/{types-DNZpaCBk.d.mts.map → types-CFWjXmus.d.mts.map} +1 -1
- package/dist/{types-gLYVCXCQ.d.mts → types-CnZYHyLW.d.mts} +55 -5
- package/dist/types-CnZYHyLW.d.mts.map +1 -0
- package/dist/{types-xxCWI3j0.mjs → types-DDS4MxsT.mjs} +11 -3
- package/dist/types-DDS4MxsT.mjs.map +1 -0
- package/dist/{types-BYWYxLcp.d.mts → types-DgrIP0tF.d.mts} +9 -2
- package/dist/types-DgrIP0tF.d.mts.map +1 -0
- package/dist/{validate-CcNRWH6I.d.mts → validate-CaLH1Ia2.d.mts} +5 -52
- package/dist/validate-CaLH1Ia2.d.mts.map +1 -0
- package/dist/{validate-DuZDIxfy.mjs → validate-CqsNItbt.mjs} +2 -2
- package/dist/{validate-DuZDIxfy.mjs.map → validate-CqsNItbt.mjs.map} +1 -1
- package/dist/version-Uaf2ynPX.mjs +7 -0
- package/dist/{version-DlTDRdpv.mjs.map → version-Uaf2ynPX.mjs.map} +1 -1
- package/package.json +10 -5
- package/src/after.ts +62 -0
- package/src/api/handlers/oauth-authorization.ts +2 -32
- package/src/api/handlers/oauth-clients.ts +40 -4
- package/src/api/handlers/taxonomies.ts +13 -0
- package/src/api/oauth/redirect-uri.ts +34 -0
- package/src/api/openapi/document.ts +126 -118
- package/src/api/schemas/auth.ts +7 -0
- package/src/api/schemas/media.ts +26 -15
- package/src/api/schemas/schema.ts +1 -0
- package/src/astro/integration/font-provider.ts +176 -0
- package/src/astro/integration/index.ts +42 -0
- package/src/astro/integration/routes.ts +17 -1
- package/src/astro/integration/runtime.ts +63 -0
- package/src/astro/integration/virtual-modules.ts +41 -39
- package/src/astro/integration/vite-config.ts +16 -5
- package/src/astro/middleware/auth.ts +39 -6
- package/src/astro/middleware/request-context.ts +15 -3
- package/src/astro/middleware.ts +340 -263
- package/src/astro/routes/admin.astro +10 -5
- package/src/astro/routes/api/auth/invite/register-options.ts +78 -0
- package/src/astro/routes/api/auth/passkey/verify.ts +5 -1
- package/src/astro/routes/api/content/[collection]/[id]/terms/[taxonomy].ts +5 -0
- package/src/astro/routes/api/import/wordpress/execute.ts +1 -1
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +1 -1
- package/src/astro/routes/api/media/upload-url.ts +10 -2
- package/src/astro/routes/api/media.ts +10 -7
- package/src/astro/routes/api/oauth/register.ts +178 -0
- package/src/astro/routes/api/oauth/token.ts +15 -0
- package/src/astro/routes/api/openapi.json.ts +15 -5
- package/src/astro/routes/api/schema/collections/[slug]/fields/[fieldSlug].ts +2 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/index.ts +1 -0
- package/src/astro/routes/api/schema/collections/[slug]/fields/reorder.ts +1 -0
- package/src/astro/routes/api/search/index.ts +5 -0
- package/src/astro/routes/api/search/suggest.ts +3 -0
- package/src/astro/routes/api/taxonomies/index.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +6 -4
- package/src/bylines/index.ts +22 -45
- package/src/components/EmDashHead.astro +23 -7
- package/src/components/Table.astro +73 -41
- package/src/components/index.ts +2 -12
- package/src/components/marks.ts +20 -0
- package/src/database/connection.ts +23 -1
- package/src/database/instrumentation.ts +98 -0
- package/src/db/adapters.ts +15 -0
- package/src/emdash-runtime.ts +309 -91
- package/src/index.ts +6 -0
- package/src/loader.ts +19 -24
- package/src/menus/index.ts +6 -3
- package/src/page/index.ts +1 -1
- package/src/page/seo-contributions.ts +36 -0
- package/src/plugins/context.ts +1 -0
- package/src/plugins/email-console.ts +9 -2
- package/src/plugins/types.ts +8 -0
- package/src/query.ts +104 -7
- package/src/request-cache.ts +106 -0
- package/src/request-context.ts +19 -0
- package/src/schema/query.ts +5 -2
- package/src/schema/registry.ts +243 -166
- package/src/schema/types.ts +13 -2
- package/src/schema/zod-generator.ts +4 -0
- package/src/search/fts-manager.ts +19 -5
- package/src/search/query.ts +4 -3
- package/src/seed/apply.ts +15 -1
- package/src/settings/index.ts +24 -5
- package/src/taxonomies/index.ts +324 -124
- package/src/utils/db-errors.ts +46 -0
- package/src/virtual-modules.d.ts +31 -10
- package/src/widgets/index.ts +54 -25
- package/dist/adapters-C2BzVy0p.d.mts.map +0 -1
- package/dist/apply-Cma_PiF6.mjs.map +0 -1
- package/dist/byline-WuOq9MFJ.mjs.map +0 -1
- package/dist/bylines-C_Wsnz4L.mjs.map +0 -1
- package/dist/connection-B4zVnQIa.mjs.map +0 -1
- package/dist/index-CRg3PWfZ.d.mts.map +0 -1
- package/dist/loader-BYzwzORf.mjs.map +0 -1
- package/dist/query-B6Vu0d2i.mjs.map +0 -1
- package/dist/registry-BgnP3ysR.mjs.map +0 -1
- package/dist/search-B5p9D36n.mjs.map +0 -1
- package/dist/types-BYWYxLcp.d.mts.map +0 -1
- package/dist/types-gLYVCXCQ.d.mts.map +0 -1
- package/dist/types-xxCWI3j0.mjs.map +0 -1
- package/dist/validate-CcNRWH6I.d.mts.map +0 -1
- package/dist/version-DlTDRdpv.mjs +0 -7
package/src/emdash-runtime.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type {
|
|
|
20
20
|
import type { EmDashManifest, ManifestCollection } from "./astro/types.js";
|
|
21
21
|
import { getAuthMode } from "./auth/mode.js";
|
|
22
22
|
import { isSqlite } from "./database/dialect-helpers.js";
|
|
23
|
+
import { kyselyLogOption } from "./database/instrumentation.js";
|
|
23
24
|
import { runMigrations } from "./database/migrations/runner.js";
|
|
24
25
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
25
26
|
import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
|
|
@@ -88,6 +89,7 @@ function isValidMetadataContribution(c: unknown): c is PageMetadataContribution
|
|
|
88
89
|
}
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
import { after } from "./after.js";
|
|
91
93
|
import { loadBundleFromR2 } from "./api/handlers/marketplace.js";
|
|
92
94
|
import { runSystemCleanup } from "./cleanup.js";
|
|
93
95
|
import {
|
|
@@ -153,6 +155,7 @@ import { FTSManager } from "./search/fts-manager.js";
|
|
|
153
155
|
const FIELD_TYPE_TO_KIND: Record<FieldType, string> = {
|
|
154
156
|
string: "string",
|
|
155
157
|
slug: "string",
|
|
158
|
+
url: "url",
|
|
156
159
|
text: "richText",
|
|
157
160
|
number: "number",
|
|
158
161
|
integer: "number",
|
|
@@ -286,6 +289,18 @@ export class EmDashRuntime {
|
|
|
286
289
|
private enabledPlugins: Set<string>;
|
|
287
290
|
private pluginStates: Map<string, string>;
|
|
288
291
|
|
|
292
|
+
private _cachedManifest: EmDashManifest | null = null;
|
|
293
|
+
private _manifestPromise: Promise<EmDashManifest> | null = null;
|
|
294
|
+
private readonly _manifestCacheKey: string;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Set to true after FTS indexes have been verified for this worker
|
|
298
|
+
* lifetime so we don't re-scan on every admin request. See
|
|
299
|
+
* ensureSearchHealthy().
|
|
300
|
+
*/
|
|
301
|
+
private _searchHealthChecked = false;
|
|
302
|
+
private _searchHealthPromise: Promise<void> | null = null;
|
|
303
|
+
|
|
289
304
|
/** Current hook pipeline. Use the `hooks` getter for external access. */
|
|
290
305
|
get hooks(): HookPipeline {
|
|
291
306
|
return this._hooks;
|
|
@@ -344,6 +359,7 @@ export class EmDashRuntime {
|
|
|
344
359
|
},
|
|
345
360
|
runtimeDeps: RuntimeDependencies,
|
|
346
361
|
pipelineRef: { current: HookPipeline },
|
|
362
|
+
manifestCacheKey: string,
|
|
347
363
|
) {
|
|
348
364
|
this._db = db;
|
|
349
365
|
this.storage = storage;
|
|
@@ -364,6 +380,7 @@ export class EmDashRuntime {
|
|
|
364
380
|
this.pipelineFactoryOptions = pipelineFactoryOptions;
|
|
365
381
|
this.runtimeDeps = runtimeDeps;
|
|
366
382
|
this.pipelineRef = pipelineRef;
|
|
383
|
+
this._manifestCacheKey = manifestCacheKey;
|
|
367
384
|
}
|
|
368
385
|
|
|
369
386
|
/**
|
|
@@ -413,6 +430,7 @@ export class EmDashRuntime {
|
|
|
413
430
|
this.enabledPlugins.delete(pluginId);
|
|
414
431
|
await this.rebuildHookPipeline();
|
|
415
432
|
}
|
|
433
|
+
this.invalidateManifest();
|
|
416
434
|
}
|
|
417
435
|
|
|
418
436
|
/**
|
|
@@ -565,36 +583,46 @@ export class EmDashRuntime {
|
|
|
565
583
|
/**
|
|
566
584
|
* Create and initialize the runtime
|
|
567
585
|
*/
|
|
568
|
-
static async create(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
//
|
|
575
|
-
|
|
586
|
+
static async create(
|
|
587
|
+
deps: RuntimeDependencies,
|
|
588
|
+
timings?: Array<{ name: string; dur: number; desc?: string }>,
|
|
589
|
+
): Promise<EmDashRuntime> {
|
|
590
|
+
// Helper: time a phase and push into the shared timings array when
|
|
591
|
+
// provided. Uses performance.now() — monotonic across async boundaries.
|
|
592
|
+
// No-op when `timings` wasn't passed (preserves backwards compatibility
|
|
593
|
+
// with callers that don't care about per-phase breakdown).
|
|
594
|
+
const phase = async <T>(name: string, desc: string, fn: () => Promise<T>): Promise<T> => {
|
|
595
|
+
if (!timings) return fn();
|
|
596
|
+
const t0 = performance.now();
|
|
576
597
|
try {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
console.log(`Repaired ${repaired} corrupted FTS index(es) at startup`);
|
|
581
|
-
}
|
|
582
|
-
} catch {
|
|
583
|
-
// FTS tables may not exist yet (pre-setup). Non-fatal.
|
|
598
|
+
return await fn();
|
|
599
|
+
} finally {
|
|
600
|
+
timings.push({ name, dur: performance.now() - t0, desc });
|
|
584
601
|
}
|
|
585
|
-
}
|
|
602
|
+
};
|
|
586
603
|
|
|
587
|
-
// Initialize
|
|
604
|
+
// Initialize database (connects, runs migrations if needed)
|
|
605
|
+
const db = await phase("rt.db", "DB init + migrations", () => EmDashRuntime.getDatabase(deps));
|
|
606
|
+
|
|
607
|
+
// FTS verify/repair is deferred off the cold-start hot path.
|
|
608
|
+
// See EmDashRuntime.ensureSearchHealthy().
|
|
609
|
+
|
|
610
|
+
// Initialize storage (sync)
|
|
588
611
|
const storage = EmDashRuntime.getStorage(deps);
|
|
589
612
|
|
|
590
613
|
// Fetch plugin states from database
|
|
591
614
|
let pluginStates: Map<string, string> = new Map();
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
615
|
+
await phase("rt.plugins", "Plugin states", async () => {
|
|
616
|
+
try {
|
|
617
|
+
const states = await db
|
|
618
|
+
.selectFrom("_plugin_state")
|
|
619
|
+
.select(["plugin_id", "status"])
|
|
620
|
+
.execute();
|
|
621
|
+
pluginStates = new Map(states.map((s) => [s.plugin_id, s.status]));
|
|
622
|
+
} catch {
|
|
623
|
+
// Plugin state table may not exist yet
|
|
624
|
+
}
|
|
625
|
+
});
|
|
598
626
|
|
|
599
627
|
// Build set of enabled plugins
|
|
600
628
|
const enabledPlugins = new Set<string>();
|
|
@@ -605,21 +633,25 @@ export class EmDashRuntime {
|
|
|
605
633
|
}
|
|
606
634
|
}
|
|
607
635
|
|
|
608
|
-
// Load site info for plugin context extensions
|
|
636
|
+
// Load site info for plugin context extensions (1 batch query instead of 3)
|
|
609
637
|
let siteInfo: { siteName?: string; siteUrl?: string; locale?: string } | undefined;
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
638
|
+
await phase("rt.site", "Site info options", async () => {
|
|
639
|
+
try {
|
|
640
|
+
const optionsRepo = new OptionsRepository(db);
|
|
641
|
+
const siteOpts = await optionsRepo.getMany<string>([
|
|
642
|
+
"emdash:site_title",
|
|
643
|
+
"emdash:site_url",
|
|
644
|
+
"emdash:locale",
|
|
645
|
+
]);
|
|
646
|
+
siteInfo = {
|
|
647
|
+
siteName: siteOpts.get("emdash:site_title") ?? undefined,
|
|
648
|
+
siteUrl: siteOpts.get("emdash:site_url") ?? undefined,
|
|
649
|
+
locale: siteOpts.get("emdash:locale") ?? undefined,
|
|
650
|
+
};
|
|
651
|
+
} catch {
|
|
652
|
+
// Options table may not exist yet (pre-setup)
|
|
653
|
+
}
|
|
654
|
+
});
|
|
623
655
|
|
|
624
656
|
// Build the full list of pipeline-eligible plugins: all configured
|
|
625
657
|
// plugins (regardless of current enabled status) plus built-in plugins.
|
|
@@ -685,11 +717,15 @@ export class EmDashRuntime {
|
|
|
685
717
|
const pipeline = createHookPipeline(enabledPluginList, pipelineFactoryOptions);
|
|
686
718
|
|
|
687
719
|
// Load sandboxed plugins (build-time)
|
|
688
|
-
const sandboxedPlugins = await
|
|
720
|
+
const sandboxedPlugins = await phase("rt.sandbox", "Sandboxed plugins", () =>
|
|
721
|
+
EmDashRuntime.loadSandboxedPlugins(deps, db),
|
|
722
|
+
);
|
|
689
723
|
|
|
690
724
|
// Cold-start: load marketplace-installed plugins from site R2
|
|
691
725
|
if (deps.config.marketplace && storage) {
|
|
692
|
-
await
|
|
726
|
+
await phase("rt.market", "Marketplace plugins", () =>
|
|
727
|
+
EmDashRuntime.loadMarketplacePlugins(db, storage, deps, sandboxedPlugins),
|
|
728
|
+
);
|
|
693
729
|
}
|
|
694
730
|
|
|
695
731
|
// Initialize media providers
|
|
@@ -707,7 +743,9 @@ export class EmDashRuntime {
|
|
|
707
743
|
}
|
|
708
744
|
|
|
709
745
|
// Resolve exclusive hooks — auto-select providers and sync with DB
|
|
710
|
-
await
|
|
746
|
+
await phase("rt.hooks", "Exclusive hook resolution", () =>
|
|
747
|
+
EmDashRuntime.resolveExclusiveHooks(pipeline, db, deps),
|
|
748
|
+
);
|
|
711
749
|
|
|
712
750
|
// ── Email pipeline ───────────────────────────────────────────────
|
|
713
751
|
// The email pipeline orchestrates beforeSend → deliver → afterSend.
|
|
@@ -740,52 +778,84 @@ export class EmDashRuntime {
|
|
|
740
778
|
let cronExecutor: CronExecutor | null = null;
|
|
741
779
|
let cronScheduler: CronScheduler | null = null;
|
|
742
780
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
781
|
+
await phase("rt.cron", "Cron init (recovery deferred post-response)", async () => {
|
|
782
|
+
try {
|
|
783
|
+
cronExecutor = new CronExecutor(db, invokeCronHook);
|
|
784
|
+
|
|
785
|
+
// Recover stale locks from previous crashes. Pure bookkeeping
|
|
786
|
+
// against the _emdash_cron_tasks table — no request needs the
|
|
787
|
+
// result — so we defer it past the response via after(). On
|
|
788
|
+
// Cloudflare this goes into waitUntil (extending the worker
|
|
789
|
+
// lifetime); on Node it's fire-and-forget (the process stays
|
|
790
|
+
// up anyway). Saves one cold-start write per D1 isolate.
|
|
791
|
+
const executorForRecovery = cronExecutor;
|
|
792
|
+
after(async () => {
|
|
793
|
+
try {
|
|
794
|
+
const recovered = await executorForRecovery.recoverStaleLocks();
|
|
795
|
+
if (recovered > 0) {
|
|
796
|
+
console.log(`[cron] Recovered ${recovered} stale task lock(s)`);
|
|
797
|
+
}
|
|
798
|
+
} catch (error) {
|
|
799
|
+
// Keep the `[cron]` prefix so a failure is easy to trace back
|
|
800
|
+
// rather than surfacing as a generic deferred-task error.
|
|
801
|
+
console.error("[cron] Failed to recover stale task locks:", error);
|
|
802
|
+
}
|
|
803
|
+
});
|
|
751
804
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if (isWorkersRuntime) {
|
|
761
|
-
cronScheduler = new PiggybackScheduler(cronExecutor);
|
|
762
|
-
} else {
|
|
763
|
-
cronScheduler = new NodeCronScheduler(cronExecutor);
|
|
764
|
-
}
|
|
805
|
+
// Detect platform and create appropriate scheduler.
|
|
806
|
+
// On Cloudflare Workers, setTimeout is available but unreliable for
|
|
807
|
+
// long durations — use PiggybackScheduler as default.
|
|
808
|
+
// In Node/Bun, use NodeCronScheduler with real timers.
|
|
809
|
+
const isWorkersRuntime =
|
|
810
|
+
typeof globalThis.navigator !== "undefined" &&
|
|
811
|
+
globalThis.navigator.userAgent === "Cloudflare-Workers";
|
|
765
812
|
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
await runSystemCleanup(db, storage ?? undefined);
|
|
771
|
-
} catch (error) {
|
|
772
|
-
// Non-fatal -- individual cleanup failures are already logged
|
|
773
|
-
// by runSystemCleanup. This catches unexpected errors.
|
|
774
|
-
console.error("[cleanup] System cleanup failed:", error);
|
|
813
|
+
if (isWorkersRuntime) {
|
|
814
|
+
cronScheduler = new PiggybackScheduler(cronExecutor);
|
|
815
|
+
} else {
|
|
816
|
+
cronScheduler = new NodeCronScheduler(cronExecutor);
|
|
775
817
|
}
|
|
776
|
-
});
|
|
777
818
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
819
|
+
// Register system cleanup to run alongside each scheduler tick.
|
|
820
|
+
// Pass storage so cleanupPendingUploads can delete orphaned files.
|
|
821
|
+
cronScheduler.setSystemCleanup(async () => {
|
|
822
|
+
try {
|
|
823
|
+
await runSystemCleanup(db, storage ?? undefined);
|
|
824
|
+
} catch (error) {
|
|
825
|
+
// Non-fatal -- individual cleanup failures are already logged
|
|
826
|
+
// by runSystemCleanup. This catches unexpected errors.
|
|
827
|
+
console.error("[cleanup] System cleanup failed:", error);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
782
830
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
831
|
+
// Add cron reschedule callback (merges with existing factory options)
|
|
832
|
+
pipeline.setContextFactory({
|
|
833
|
+
cronReschedule: () => cronScheduler?.reschedule(),
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// Start the scheduler
|
|
837
|
+
await cronScheduler.start();
|
|
838
|
+
} catch (error) {
|
|
839
|
+
console.warn("[cron] Failed to initialize cron system:", error);
|
|
840
|
+
// Non-fatal — CMS works without cron
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
// SHA of emdash commit + user config that affects the manifest.
|
|
845
|
+
// COMMIT captures emdash code changes; plugin IDs/versions and i18n
|
|
846
|
+
// capture user astro.config changes (e.g. upgrading a plugin package).
|
|
847
|
+
// DB-driven changes (collections, fields, plugin toggle) go through
|
|
848
|
+
// invalidateManifest(). Sorted for stability across nondeterministic
|
|
849
|
+
// plugin ordering.
|
|
850
|
+
const manifestCacheKey = await hashString(
|
|
851
|
+
[
|
|
852
|
+
COMMIT,
|
|
853
|
+
...deps.plugins.map((p) => `${p.id}@${p.version ?? ""}`).toSorted(),
|
|
854
|
+
...deps.sandboxedPluginEntries.map((e) => `${e.id}@${e.version}`).toSorted(),
|
|
855
|
+
virtualConfig?.i18n?.defaultLocale ?? "",
|
|
856
|
+
(virtualConfig?.i18n?.locales ?? []).toSorted().join(","),
|
|
857
|
+
].join("|"),
|
|
858
|
+
);
|
|
789
859
|
|
|
790
860
|
return new EmDashRuntime(
|
|
791
861
|
db,
|
|
@@ -806,6 +876,7 @@ export class EmDashRuntime {
|
|
|
806
876
|
pipelineFactoryOptions,
|
|
807
877
|
deps,
|
|
808
878
|
pipelineRef,
|
|
879
|
+
manifestCacheKey,
|
|
809
880
|
);
|
|
810
881
|
}
|
|
811
882
|
|
|
@@ -837,12 +908,14 @@ export class EmDashRuntime {
|
|
|
837
908
|
* Get or create database instance
|
|
838
909
|
*/
|
|
839
910
|
private static async getDatabase(deps: RuntimeDependencies): Promise<Kysely<Database>> {
|
|
840
|
-
//
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
// the
|
|
911
|
+
// Only use the per-request `ctx.db` when it's an isolated instance
|
|
912
|
+
// (playground / DO preview). Plain D1 Sessions set `ctx.db` on every
|
|
913
|
+
// anonymous request — if we captured one of those session-bound
|
|
914
|
+
// Kyselys into the cached runtime, every request would accidentally
|
|
915
|
+
// share one request's session. The configured `deps.createDialect`
|
|
916
|
+
// path gives us a fresh singleton instead.
|
|
844
917
|
const ctx = getRequestContext();
|
|
845
|
-
if (ctx?.db) {
|
|
918
|
+
if (ctx?.dbIsIsolated && ctx.db) {
|
|
846
919
|
// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- db in context is typed as unknown to avoid circular deps
|
|
847
920
|
return ctx.db as Kysely<Database>;
|
|
848
921
|
}
|
|
@@ -878,9 +951,20 @@ export class EmDashRuntime {
|
|
|
878
951
|
|
|
879
952
|
dbInitPromise = (async () => {
|
|
880
953
|
const dialect = deps.createDialect(dbConfig.config);
|
|
881
|
-
const db = new Kysely<Database>({ dialect });
|
|
954
|
+
const db = new Kysely<Database>({ dialect, log: kyselyLogOption() });
|
|
882
955
|
|
|
883
|
-
await runMigrations(db);
|
|
956
|
+
const { applied } = await runMigrations(db);
|
|
957
|
+
|
|
958
|
+
// If migrations were applied, the schema changed — clear the
|
|
959
|
+
// DB-persisted manifest cache so getManifest() rebuilds it.
|
|
960
|
+
if (applied.length > 0) {
|
|
961
|
+
try {
|
|
962
|
+
const options = new OptionsRepository(db);
|
|
963
|
+
await options.delete("emdash:manifest_cache");
|
|
964
|
+
} catch {
|
|
965
|
+
// Non-fatal
|
|
966
|
+
}
|
|
967
|
+
}
|
|
884
968
|
|
|
885
969
|
// Auto-seed schema if no collections exist and setup hasn't run.
|
|
886
970
|
// This covers first-load on sites that skip the setup wizard.
|
|
@@ -1142,9 +1226,82 @@ export class EmDashRuntime {
|
|
|
1142
1226
|
// =========================================================================
|
|
1143
1227
|
|
|
1144
1228
|
/**
|
|
1145
|
-
*
|
|
1229
|
+
* Get the manifest, using an in-memory cache with a DB-persisted
|
|
1230
|
+
* fallback for cold starts. Avoids N+1 schema registry queries
|
|
1231
|
+
* on every request.
|
|
1232
|
+
*
|
|
1233
|
+
* Cache is invalidated by invalidateManifest(), called from schema
|
|
1234
|
+
* API routes, MCP server, plugin toggle, and taxonomy def changes.
|
|
1146
1235
|
*/
|
|
1147
1236
|
async getManifest(): Promise<EmDashManifest> {
|
|
1237
|
+
// When the DB is overridden by an isolated instance (playground /
|
|
1238
|
+
// DO-preview sessions), bypass the module-scoped manifest cache —
|
|
1239
|
+
// its schema may diverge from the configured DB. Plain D1 Sessions
|
|
1240
|
+
// routing does NOT set `dbIsIsolated`, so the cache still applies.
|
|
1241
|
+
if (getRequestContext()?.dbIsIsolated) {
|
|
1242
|
+
return this._buildManifest();
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
if (this._cachedManifest) return this._cachedManifest;
|
|
1246
|
+
|
|
1247
|
+
// DB-persisted cache (1 query instead of N+1 rebuild on cold start).
|
|
1248
|
+
// Keyed by SHA of commit + config to bust on deploys. DB-driven
|
|
1249
|
+
// changes (collections, fields, plugins, taxonomies) go through
|
|
1250
|
+
// invalidateManifest().
|
|
1251
|
+
try {
|
|
1252
|
+
const options = new OptionsRepository(this.db);
|
|
1253
|
+
const cached = await options.get<{ key: string; manifest: EmDashManifest }>(
|
|
1254
|
+
"emdash:manifest_cache",
|
|
1255
|
+
);
|
|
1256
|
+
if (cached && cached.key === this._manifestCacheKey && cached.manifest) {
|
|
1257
|
+
this._cachedManifest = cached.manifest;
|
|
1258
|
+
return cached.manifest;
|
|
1259
|
+
}
|
|
1260
|
+
} catch {
|
|
1261
|
+
// Options table may not exist yet
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Full rebuild, then persist. Track which promise is current so
|
|
1265
|
+
// an invalidation during the build can't be overwritten.
|
|
1266
|
+
if (!this._manifestPromise) {
|
|
1267
|
+
let manifestPromise: Promise<EmDashManifest>;
|
|
1268
|
+
const isCurrentLoad = () => this._manifestPromise === manifestPromise;
|
|
1269
|
+
manifestPromise = this._loadManifest(isCurrentLoad);
|
|
1270
|
+
this._manifestPromise = manifestPromise;
|
|
1271
|
+
}
|
|
1272
|
+
return this._manifestPromise;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
private async _loadManifest(isCurrentLoad: () => boolean): Promise<EmDashManifest> {
|
|
1276
|
+
try {
|
|
1277
|
+
const manifest = await this._buildManifest();
|
|
1278
|
+
|
|
1279
|
+
if (isCurrentLoad()) {
|
|
1280
|
+
this._cachedManifest = manifest;
|
|
1281
|
+
|
|
1282
|
+
try {
|
|
1283
|
+
const options = new OptionsRepository(this.db);
|
|
1284
|
+
await options.set("emdash:manifest_cache", {
|
|
1285
|
+
key: this._manifestCacheKey,
|
|
1286
|
+
manifest,
|
|
1287
|
+
});
|
|
1288
|
+
} catch {
|
|
1289
|
+
// Non-fatal — will just rebuild next time
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return manifest;
|
|
1294
|
+
} finally {
|
|
1295
|
+
if (isCurrentLoad()) {
|
|
1296
|
+
this._manifestPromise = null;
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Build the manifest from database (N+1 collection queries).
|
|
1303
|
+
*/
|
|
1304
|
+
private async _buildManifest(): Promise<EmDashManifest> {
|
|
1148
1305
|
// Build collections from database.
|
|
1149
1306
|
// Use this.db (ALS-aware getter) so playground mode picks up the
|
|
1150
1307
|
// per-session DO database instead of the hardcoded singleton.
|
|
@@ -1370,11 +1527,72 @@ export class EmDashRuntime {
|
|
|
1370
1527
|
|
|
1371
1528
|
/**
|
|
1372
1529
|
* Invalidate cached data derived from the manifest/schema.
|
|
1373
|
-
* Called when collections
|
|
1530
|
+
* Called when collections, fields, plugins, or taxonomy defs change.
|
|
1374
1531
|
*/
|
|
1375
1532
|
invalidateManifest(): void {
|
|
1376
|
-
|
|
1533
|
+
this._cachedManifest = null;
|
|
1534
|
+
this._manifestPromise = null;
|
|
1377
1535
|
invalidateUrlPatternCache();
|
|
1536
|
+
// Delete DB-persisted cache so the next cold start rebuilds.
|
|
1537
|
+
// Fire-and-forget: in-memory is already cleared for this worker,
|
|
1538
|
+
// DB delete is best-effort for the next cold start.
|
|
1539
|
+
try {
|
|
1540
|
+
const options = new OptionsRepository(this.db);
|
|
1541
|
+
options.delete("emdash:manifest_cache").catch((error) => {
|
|
1542
|
+
console.error("Failed to delete persisted manifest cache", error);
|
|
1543
|
+
});
|
|
1544
|
+
} catch (error) {
|
|
1545
|
+
console.error("Failed to initialize manifest cache invalidation", error);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
/**
|
|
1550
|
+
* Verify and repair FTS indexes on demand. Runs at most once per worker
|
|
1551
|
+
* lifetime.
|
|
1552
|
+
*
|
|
1553
|
+
* Originally called from `EmDashRuntime.create()`, but on a busy D1 link
|
|
1554
|
+
* (e.g. SIN replica ~80-150ms per query) it added ~1.5s to every cold
|
|
1555
|
+
* start for a modest-sized site — more than every other init phase
|
|
1556
|
+
* combined. Anonymous public reads never touch the search write path,
|
|
1557
|
+
* so the cost isn't paid back for the vast majority of requests.
|
|
1558
|
+
*
|
|
1559
|
+
* Instead, search endpoints call this lazily: the first request that
|
|
1560
|
+
* actually needs the index pays the verify cost (usually fast — no
|
|
1561
|
+
* rebuild needed), everyone else runs cold-free.
|
|
1562
|
+
*
|
|
1563
|
+
* Uses the runtime's singleton database (`this._db`) rather than the
|
|
1564
|
+
* request-scoped DB. Verify reads only, but `rebuildIndex` writes, and
|
|
1565
|
+
* a GET search request on D1 carries a `first-unconstrained` session
|
|
1566
|
+
* that's free to route at a read replica — unsafe for writes. The
|
|
1567
|
+
* singleton always goes through the default binding, which the D1
|
|
1568
|
+
* adapter will promote to `first-primary` for write statements.
|
|
1569
|
+
*
|
|
1570
|
+
* Safe to call concurrently: repeated callers share the same in-flight
|
|
1571
|
+
* promise. Errors are swallowed internally so callers don't need to
|
|
1572
|
+
* defend against FTS not existing yet (pre-setup).
|
|
1573
|
+
*/
|
|
1574
|
+
async ensureSearchHealthy(): Promise<void> {
|
|
1575
|
+
if (this._searchHealthChecked) return;
|
|
1576
|
+
if (this._searchHealthPromise) return this._searchHealthPromise;
|
|
1577
|
+
if (!isSqlite(this._db)) {
|
|
1578
|
+
this._searchHealthChecked = true;
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
this._searchHealthPromise = (async () => {
|
|
1582
|
+
try {
|
|
1583
|
+
const ftsManager = new FTSManager(this._db);
|
|
1584
|
+
const repaired = await ftsManager.verifyAndRepairAll();
|
|
1585
|
+
if (repaired > 0) {
|
|
1586
|
+
console.log(`Repaired ${repaired} corrupted FTS index(es)`);
|
|
1587
|
+
}
|
|
1588
|
+
} catch {
|
|
1589
|
+
// FTS tables may not exist yet (pre-setup). Non-fatal.
|
|
1590
|
+
} finally {
|
|
1591
|
+
this._searchHealthChecked = true;
|
|
1592
|
+
this._searchHealthPromise = null;
|
|
1593
|
+
}
|
|
1594
|
+
})();
|
|
1595
|
+
return this._searchHealthPromise;
|
|
1378
1596
|
}
|
|
1379
1597
|
|
|
1380
1598
|
// =========================================================================
|
package/src/index.ts
CHANGED
|
@@ -130,6 +130,10 @@ export type {
|
|
|
130
130
|
export { getRequestContext, runWithContext } from "./request-context.js";
|
|
131
131
|
export type { EmDashRequestContext } from "./request-context.js";
|
|
132
132
|
|
|
133
|
+
// Defer work past the response (waitUntil on workerd, fire-and-forget on Node)
|
|
134
|
+
export { after } from "./after.js";
|
|
135
|
+
export type { WaitUntilFn } from "./after.js";
|
|
136
|
+
|
|
133
137
|
// i18n configuration (from Astro config)
|
|
134
138
|
export { getI18nConfig, isI18nEnabled, getFallbackChain } from "./i18n/config.js";
|
|
135
139
|
export type { I18nConfig } from "./i18n/config.js";
|
|
@@ -400,7 +404,9 @@ export {
|
|
|
400
404
|
getTerm,
|
|
401
405
|
getEntryTerms,
|
|
402
406
|
getTermsForEntries,
|
|
407
|
+
getAllTermsForEntries,
|
|
403
408
|
getEntriesByTerm,
|
|
409
|
+
invalidateTermCache,
|
|
404
410
|
} from "./taxonomies/index.js";
|
|
405
411
|
export type {
|
|
406
412
|
TaxonomyDef,
|
package/src/loader.ts
CHANGED
|
@@ -15,10 +15,12 @@ import type { LiveLoader } from "astro/loaders";
|
|
|
15
15
|
import { Kysely, sql, type Dialect } from "kysely";
|
|
16
16
|
|
|
17
17
|
import { currentTimestampValue, isPostgres } from "./database/dialect-helpers.js";
|
|
18
|
+
import { kyselyLogOption } from "./database/instrumentation.js";
|
|
18
19
|
import { decodeCursor, encodeCursor } from "./database/repositories/types.js";
|
|
19
20
|
import { validateIdentifier } from "./database/validate.js";
|
|
20
21
|
import type { Database } from "./index.js";
|
|
21
22
|
import { getRequestContext } from "./request-context.js";
|
|
23
|
+
import { isMissingTableError } from "./utils/db-errors.js";
|
|
22
24
|
|
|
23
25
|
const FIELD_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
24
26
|
|
|
@@ -63,26 +65,29 @@ function getTableName(type: string): string {
|
|
|
63
65
|
let taxonomyNames: Set<string> | null = null;
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
|
-
* Get all taxonomy names (cached for primary DB,
|
|
68
|
+
* Get all taxonomy names (cached for the primary DB, bypassed only when
|
|
69
|
+
* the per-request DB is an isolated instance — playground / DO preview).
|
|
70
|
+
* Plain D1 Sessions routing shares schema with the singleton, so the
|
|
71
|
+
* module-scoped cache stays valid.
|
|
67
72
|
*/
|
|
68
73
|
async function getTaxonomyNames(db: Kysely<Database>): Promise<Set<string>> {
|
|
69
|
-
const
|
|
74
|
+
const hasIsolatedDb = getRequestContext()?.dbIsIsolated === true;
|
|
70
75
|
|
|
71
|
-
if (!
|
|
76
|
+
if (!hasIsolatedDb && taxonomyNames) {
|
|
72
77
|
return taxonomyNames;
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
try {
|
|
76
81
|
const defs = await db.selectFrom("_emdash_taxonomy_defs").select("name").execute();
|
|
77
82
|
const names = new Set(defs.map((d) => d.name));
|
|
78
|
-
if (!
|
|
83
|
+
if (!hasIsolatedDb) {
|
|
79
84
|
taxonomyNames = names;
|
|
80
85
|
}
|
|
81
86
|
return names;
|
|
82
87
|
} catch {
|
|
83
88
|
// Table doesn't exist yet, return empty set
|
|
84
89
|
const empty = new Set<string>();
|
|
85
|
-
if (!
|
|
90
|
+
if (!hasIsolatedDb) {
|
|
86
91
|
taxonomyNames = empty;
|
|
87
92
|
}
|
|
88
93
|
return empty;
|
|
@@ -406,7 +411,7 @@ export async function getDb(): Promise<Kysely<Database>> {
|
|
|
406
411
|
);
|
|
407
412
|
}
|
|
408
413
|
const dialect = virtualCreateDialect(virtualConfig.database.config);
|
|
409
|
-
dbInstance = new Kysely<Database>({ dialect });
|
|
414
|
+
dbInstance = new Kysely<Database>({ dialect, log: kyselyLogOption() });
|
|
410
415
|
}
|
|
411
416
|
return dbInstance;
|
|
412
417
|
}
|
|
@@ -617,18 +622,13 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
|
|
|
617
622
|
},
|
|
618
623
|
};
|
|
619
624
|
} catch (error) {
|
|
620
|
-
// Handle missing table gracefully - return empty collection
|
|
621
|
-
// This happens before migrations have run
|
|
622
|
-
|
|
623
|
-
const lowerMessage = message.toLowerCase();
|
|
624
|
-
if (
|
|
625
|
-
lowerMessage.includes("no such table") ||
|
|
626
|
-
(lowerMessage.includes("table") && lowerMessage.includes("does not exist")) ||
|
|
627
|
-
(lowerMessage.includes("relation") && lowerMessage.includes("does not exist"))
|
|
628
|
-
) {
|
|
625
|
+
// Handle missing table gracefully - return empty collection.
|
|
626
|
+
// This happens before migrations have run.
|
|
627
|
+
if (isMissingTableError(error)) {
|
|
629
628
|
return { entries: [] };
|
|
630
629
|
}
|
|
631
630
|
|
|
631
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
632
632
|
return {
|
|
633
633
|
error: new Error(`Failed to load collection: ${message}`),
|
|
634
634
|
};
|
|
@@ -751,18 +751,13 @@ export function emdashLoader(): LiveLoader<EntryData, EntryFilter, CollectionFil
|
|
|
751
751
|
},
|
|
752
752
|
};
|
|
753
753
|
} catch (error) {
|
|
754
|
-
// Handle missing table gracefully - return undefined (not found)
|
|
755
|
-
// This happens before migrations have run
|
|
756
|
-
|
|
757
|
-
const lowerMessage = message.toLowerCase();
|
|
758
|
-
if (
|
|
759
|
-
lowerMessage.includes("no such table") ||
|
|
760
|
-
(lowerMessage.includes("table") && lowerMessage.includes("does not exist")) ||
|
|
761
|
-
(lowerMessage.includes("relation") && lowerMessage.includes("does not exist"))
|
|
762
|
-
) {
|
|
754
|
+
// Handle missing table gracefully - return undefined (not found).
|
|
755
|
+
// This happens before migrations have run.
|
|
756
|
+
if (isMissingTableError(error)) {
|
|
763
757
|
return undefined;
|
|
764
758
|
}
|
|
765
759
|
|
|
760
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
766
761
|
return {
|
|
767
762
|
error: new Error(`Failed to load entry: ${message}`),
|
|
768
763
|
};
|