emdash 0.40.0 → 0.40.1
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/api/route-utils.mjs +6 -6
- package/dist/api/schemas/index.d.mts +1 -1
- package/dist/{api-cFFZpyS0.mjs → api-CuyNEHZG.mjs} +3 -3
- package/dist/{api-cFFZpyS0.mjs.map → api-CuyNEHZG.mjs.map} +1 -1
- package/dist/{apply-CoT603H3.mjs → apply-npY1TAhG.mjs} +237 -34
- package/dist/apply-npY1TAhG.mjs.map +1 -0
- package/dist/astro/index.d.mts +3 -3
- package/dist/astro/index.mjs +9 -9
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/redirect.mjs +1 -1
- package/dist/astro/middleware.d.mts +3 -3
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +19 -18
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/routes/api/admin/media-usage/progress.mjs +2 -2
- package/dist/astro/routes/api/admin/media-usage/repair.mjs +2 -2
- package/dist/astro/routes/api/admin/plugins/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/settings.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/uninstall.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/_id_/update.mjs +11 -11
- package/dist/astro/routes/api/admin/plugins/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/marketplace/_id_/install.mjs +11 -11
- package/dist/astro/routes/api/admin/plugins/marketplace/index.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/registry/_id_/uninstall.mjs +8 -8
- package/dist/astro/routes/api/admin/plugins/registry/_id_/update.mjs +12 -12
- package/dist/astro/routes/api/admin/plugins/registry/install.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/registry/verify.mjs +9 -9
- package/dist/astro/routes/api/admin/plugins/updates.mjs +8 -8
- package/dist/astro/routes/api/admin/themes/marketplace/_id_/index.mjs +8 -8
- package/dist/astro/routes/api/admin/themes/marketplace/index.mjs +8 -8
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/approve.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/approvals/_id_/deny.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/approvals/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/capabilities.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/advance.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/archive.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/files/_...path_.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/_id_/manifest.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/exports/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/abandon.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/advance.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/analyze.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/cancel.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/execute.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/files/_...path_.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/index.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/missing.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/plan.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/_id_/receipt.mjs +3 -3
- package/dist/astro/routes/api/admin/transfer/imports/index.mjs +3 -3
- package/dist/astro/routes/api/dashboard.mjs +2 -2
- package/dist/astro/routes/api/health.mjs +1 -1
- package/dist/astro/routes/api/import/probe.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/media.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress/rewrite-urls.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/analyze.mjs +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.d.mts +3 -3
- package/dist/astro/routes/api/import/wordpress-plugin/execute.mjs +3 -3
- package/dist/astro/routes/api/manifest.mjs +1 -1
- package/dist/astro/routes/api/mcp.mjs +13 -33
- package/dist/astro/routes/api/mcp.mjs.map +1 -1
- package/dist/astro/routes/api/media/_id_/confirm.mjs +1 -1
- package/dist/astro/routes/api/media/_id_/upload.mjs +1 -1
- package/dist/astro/routes/api/media/_id_/usage.mjs +2 -2
- package/dist/astro/routes/api/media/_id_.mjs +2 -2
- package/dist/astro/routes/api/media/asset/_id_/_filename_.mjs +1 -1
- package/dist/astro/routes/api/media/upload-url.mjs +1 -1
- package/dist/astro/routes/api/media.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/index.mjs +2 -2
- package/dist/astro/routes/api/redirects/404s/summary.mjs +2 -2
- package/dist/astro/routes/api/redirects/_id_.mjs +2 -2
- package/dist/astro/routes/api/redirects/index.mjs +2 -2
- package/dist/astro/routes/api/schema/block-types/_slug_/index.mjs +8 -8
- package/dist/astro/routes/api/schema/block-types/_slug_/versions/_version_/activate.mjs +8 -8
- package/dist/astro/routes/api/schema/block-types/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/_fieldSlug_.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/fields/reorder.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/_slug_/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/index.mjs +8 -8
- package/dist/astro/routes/api/schema/collections/reorder.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/_slug_.mjs +8 -8
- package/dist/astro/routes/api/schema/orphans/index.mjs +8 -8
- package/dist/astro/routes/api/settings/backups/archives/_name_.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/archives/index.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/export.mjs +2 -2
- package/dist/astro/routes/api/settings/backups/index.mjs +2 -2
- package/dist/astro/routes/api/settings.mjs +3 -3
- package/dist/astro/routes/api/setup/dev-bypass.mjs +4 -4
- package/dist/astro/routes/api/setup/index.d.mts.map +1 -1
- package/dist/astro/routes/api/setup/index.mjs +35 -10
- package/dist/astro/routes/api/setup/index.mjs.map +1 -1
- package/dist/astro/routes/api/snapshot.mjs +2 -2
- package/dist/astro/routes/api/snapshot.mjs.map +1 -1
- package/dist/astro/routes/robots.txt.mjs +2 -2
- package/dist/astro/routes/sitemap-_collection_.xml.mjs +2 -2
- package/dist/astro/routes/sitemap.xml.mjs +2 -2
- package/dist/astro/types.d.mts +7 -3
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{backup-CBu0LrXf.mjs → backup-BQRj3afm.mjs} +3 -3
- package/dist/{backup-CBu0LrXf.mjs.map → backup-BQRj3afm.mjs.map} +1 -1
- package/dist/cli/index.mjs +6 -6
- package/dist/client/index.d.mts +14 -14
- package/dist/{config-5ii2haYu.d.mts → config-BjEm_3xL.d.mts} +25 -16
- package/dist/config-BjEm_3xL.d.mts.map +1 -0
- package/dist/{context-BeGccdUX.mjs → context-BV_7t-Ua.mjs} +6 -6
- package/dist/{context-BeGccdUX.mjs.map → context-BV_7t-Ua.mjs.map} +1 -1
- package/dist/{dashboard-DjrB-xxZ.mjs → dashboard-BP-iJqpd.mjs} +2 -2
- package/dist/{dashboard-DjrB-xxZ.mjs.map → dashboard-BP-iJqpd.mjs.map} +1 -1
- package/dist/db/libsql-migrations.mjs +2 -2
- package/dist/db/postgres-migrations.mjs +2 -2
- package/dist/db/sqlite-migrations.mjs +2 -2
- package/dist/{direct-executor-BmuFJJZV.mjs → direct-executor-D2lNadwo.mjs} +2 -2
- package/dist/{direct-executor-BmuFJJZV.mjs.map → direct-executor-D2lNadwo.mjs.map} +1 -1
- package/dist/{emdash-runtime-DAjghyEG.mjs → emdash-runtime-CtfnZmPi.mjs} +56 -56
- package/dist/emdash-runtime-CtfnZmPi.mjs.map +1 -0
- package/dist/{entry-lock-DVhCt49A.d.mts → entry-lock-Cf5OoIgN.d.mts} +18 -18
- package/dist/{entry-lock-DVhCt49A.d.mts.map → entry-lock-Cf5OoIgN.d.mts.map} +1 -1
- package/dist/{hreflang-DMFBL2eb.mjs → hreflang-CbVI4paR.mjs} +2 -2
- package/dist/{hreflang-DMFBL2eb.mjs.map → hreflang-CbVI4paR.mjs.map} +1 -1
- package/dist/{identity-IyNWwXr6.mjs → identity-Be5kzutJ.mjs} +2 -2
- package/dist/{identity-IyNWwXr6.mjs.map → identity-Be5kzutJ.mjs.map} +1 -1
- package/dist/{import-C3JZplWq.mjs → import-YzfKCaco.mjs} +2 -2
- package/dist/{import-C3JZplWq.mjs.map → import-YzfKCaco.mjs.map} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +16 -16
- package/dist/{manifest-builder-B8zR-73J.mjs → manifest-builder-BmSAOOEF.mjs} +2 -2
- package/dist/{manifest-builder-B8zR-73J.mjs.map → manifest-builder-BmSAOOEF.mjs.map} +1 -1
- package/dist/media/local-runtime.d.mts +3 -3
- package/dist/media/local-runtime.mjs +3 -3
- package/dist/{media-Qp5S3eH3.mjs → media-CPXTbb1R.mjs} +20 -2
- package/dist/media-CPXTbb1R.mjs.map +1 -0
- package/dist/{media-h7tOiDLj.mjs → media-JXRWDhw3.mjs} +77 -4
- package/dist/media-JXRWDhw3.mjs.map +1 -0
- package/dist/{media-usage-My-Vi-ex.mjs → media-usage-De-zeR9E.mjs} +2 -2
- package/dist/{media-usage-My-Vi-ex.mjs.map → media-usage-De-zeR9E.mjs.map} +1 -1
- package/dist/{menus-TsSGmSR4.mjs → menus-Cm68rsAq.mjs} +4 -4
- package/dist/{menus-TsSGmSR4.mjs.map → menus-Cm68rsAq.mjs.map} +1 -1
- package/dist/migrations/index.mjs +2 -2
- package/dist/page/index.mjs +2 -2
- package/dist/plugin-test-runtime.d.mts +3 -3
- package/dist/plugin-test-runtime.mjs +18 -18
- package/dist/plugin-utils.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/host.d.mts +1 -1
- package/dist/plugins/host.mjs +6 -6
- package/dist/{redirect-iJf5fltD.mjs → redirect-B29UjdUn.mjs} +10 -14
- package/dist/redirect-B29UjdUn.mjs.map +1 -0
- package/dist/{redirects-CYu1u1aK.mjs → redirects-Ca2MPfEG.mjs} +2 -2
- package/dist/{redirects-CYu1u1aK.mjs.map → redirects-Ca2MPfEG.mjs.map} +1 -1
- package/dist/{routes-CUsM40Cc.mjs → routes-vrR95WR6.mjs} +2 -2
- package/dist/{routes-CUsM40Cc.mjs.map → routes-vrR95WR6.mjs.map} +1 -1
- package/dist/runtime.d.mts +3 -3
- package/dist/seed/index.mjs +4 -4
- package/dist/seo/index.mjs +3 -3
- package/dist/{settings-CgjRWGW9.mjs → settings-B4vDl1fK.mjs} +2 -2
- package/dist/{settings-CgjRWGW9.mjs.map → settings-B4vDl1fK.mjs.map} +1 -1
- package/dist/{settings-2xNYr90o.mjs → settings-Dyj6v8C4.mjs} +2 -2
- package/dist/{settings-2xNYr90o.mjs.map → settings-Dyj6v8C4.mjs.map} +1 -1
- package/dist/{snapshot-CugwUqKQ.mjs → snapshot-PawoX-WG.mjs} +180 -3
- package/dist/snapshot-PawoX-WG.mjs.map +1 -0
- package/dist/{storage-query-Bdv7A62k.d.mts → storage-query-FXJH5pI8.d.mts} +6 -1
- package/dist/storage-query-FXJH5pI8.d.mts.map +1 -0
- package/dist/{transfer-BhUCrgDc.mjs → transfer-DgeCvHnj.mjs} +4 -4
- package/dist/{transfer-BhUCrgDc.mjs.map → transfer-DgeCvHnj.mjs.map} +1 -1
- package/dist/validate-CM79QXYE.d.mts.map +1 -1
- package/dist/version-DXsxRrob.mjs +7 -0
- package/dist/{version-DUQDHgrf.mjs.map → version-DXsxRrob.mjs.map} +1 -1
- package/package.json +5 -5
- package/src/api/handlers/index.ts +1 -0
- package/src/api/handlers/media-upload.ts +17 -71
- package/src/api/handlers/media.ts +97 -3
- package/src/api/handlers/snapshot.ts +257 -3
- package/src/astro/integration/vite-config.ts +10 -4
- package/src/astro/middleware.ts +1 -0
- package/src/astro/routes/api/setup/index.ts +66 -23
- package/src/astro/routes/api/snapshot.ts +2 -1
- package/src/astro/types.ts +5 -0
- package/src/cleanup.ts +10 -0
- package/src/components/Columns.astro +3 -1
- package/src/components/Cover.astro +3 -1
- package/src/database/repositories/media.ts +40 -1
- package/src/database/repositories/redirect.ts +17 -34
- package/src/emdash-runtime.ts +28 -0
- package/src/index.ts +1 -0
- package/src/mcp/server.ts +13 -48
- package/src/seed/apply.ts +322 -41
- package/dist/apply-CoT603H3.mjs.map +0 -1
- package/dist/config-5ii2haYu.d.mts.map +0 -1
- package/dist/emdash-runtime-DAjghyEG.mjs.map +0 -1
- package/dist/media-Qp5S3eH3.mjs.map +0 -1
- package/dist/media-h7tOiDLj.mjs.map +0 -1
- package/dist/redirect-iJf5fltD.mjs.map +0 -1
- package/dist/snapshot-CugwUqKQ.mjs.map +0 -1
- package/dist/storage-query-Bdv7A62k.d.mts.map +0 -1
- package/dist/version-DUQDHgrf.mjs +0 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as validateIdentifier } from "./validate-DANX1-Qi.mjs";
|
|
2
2
|
import { t as OptionsRepository } from "./options-DhrI7eMv.mjs";
|
|
3
3
|
import { t as ContentRepository } from "./content-DFaIEDj9.mjs";
|
|
4
|
-
import { t as MediaRepository } from "./media-
|
|
4
|
+
import { t as MediaRepository } from "./media-CPXTbb1R.mjs";
|
|
5
5
|
import { t as UserRepository } from "./user-DDSL-sGw.mjs";
|
|
6
6
|
import { r as isScheduledPolicyRejection, t as SCHEDULED_POLICY_REJECTION_PREFIX } from "./content-policy-DFNw91-d.mjs";
|
|
7
7
|
import { sql } from "kysely";
|
|
@@ -148,4 +148,4 @@ async function fetchRecentItems(db, collections) {
|
|
|
148
148
|
|
|
149
149
|
//#endregion
|
|
150
150
|
export { recordSchedulerHeartbeatSafely as n, handleDashboardStats as t };
|
|
151
|
-
//# sourceMappingURL=dashboard-
|
|
151
|
+
//# sourceMappingURL=dashboard-BP-iJqpd.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard-DjrB-xxZ.mjs","names":[],"sources":["../src/scheduler-health.ts","../src/api/handlers/dashboard.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport type { Database } from \"./database/types.js\";\n\nexport const SCHEDULER_HEARTBEAT_OPTION = \"system:scheduler:last_completed_at\";\nexport const SCHEDULER_STALE_AFTER_MS = 5 * 60 * 1000;\n\nexport interface SchedulerHealth {\n\tstatus: \"healthy\" | \"stale\" | \"unknown\";\n\tlastCompletedAt: string | null;\n}\n\nexport async function recordSchedulerHeartbeat(\n\tdb: Kysely<Database>,\n\tcompletedAt = new Date(),\n): Promise<void> {\n\tawait new OptionsRepository(db).set(SCHEDULER_HEARTBEAT_OPTION, completedAt.toISOString());\n}\n\nexport async function recordSchedulerHeartbeatSafely(\n\tdb: Kysely<Database>,\n\tcompletedAt = new Date(),\n): Promise<void> {\n\ttry {\n\t\tawait recordSchedulerHeartbeat(db, completedAt);\n\t} catch (error) {\n\t\tconsole.error(\"[scheduler] Failed to record heartbeat:\", error);\n\t}\n}\n\nexport async function getSchedulerHealth(\n\tdb: Kysely<Database>,\n\tnow = new Date(),\n): Promise<SchedulerHealth> {\n\tconst lastCompletedAt = await new OptionsRepository(db).get<string>(SCHEDULER_HEARTBEAT_OPTION);\n\tconst lastCompletedAtMs = lastCompletedAt ? Date.parse(lastCompletedAt) : Number.NaN;\n\tif (!lastCompletedAt || Number.isNaN(lastCompletedAtMs)) {\n\t\treturn { status: \"unknown\", lastCompletedAt: null };\n\t}\n\n\treturn {\n\t\tstatus: now.getTime() - lastCompletedAtMs > SCHEDULER_STALE_AFTER_MS ? \"stale\" : \"healthy\",\n\t\tlastCompletedAt,\n\t};\n}\n","/**\n * Dashboard stats handler\n *\n * Returns summary data for the admin dashboard in a single request:\n * collection content counts, media count, user count, and recent\n * content across all collections.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { MediaRepository } from \"../../database/repositories/media.js\";\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport { UserRepository } from \"../../database/repositories/user.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport {\n\tSCHEDULED_POLICY_REJECTION_PREFIX,\n\tisScheduledPolicyRejection,\n\ttype ScheduledPolicyRejection,\n\ttype VersionedScheduledPolicyRejection,\n} from \"../../plugins/content-policy.js\";\nimport { getSchedulerHealth, type SchedulerHealth } from \"../../scheduler-health.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface CollectionStats {\n\tslug: string;\n\tlabel: string;\n\ttotal: number;\n\tpublished: number;\n\tdraft: number;\n\tscheduled: number;\n\toverdueScheduled: number;\n}\n\nexport interface RecentItem {\n\tid: string;\n\tcollection: string;\n\tcollectionLabel: string;\n\ttitle: string;\n\tslug: string | null;\n\tstatus: string;\n\tupdatedAt: string;\n\tauthorId: string | null;\n}\n\nexport interface DashboardStats {\n\tcollections: CollectionStats[];\n\tmediaCount: number;\n\tuserCount: number;\n\trecentItems: RecentItem[];\n\tschedulerHealth: SchedulerHealth;\n\tpolicyRejectedScheduled: number;\n\tpolicyRejections: VersionedScheduledPolicyRejection[];\n}\n\nconst POLICY_REJECTION_PREVIEW_LIMIT = 20;\n\n/**\n * Fetch dashboard statistics.\n *\n * Queries are intentionally lightweight — counts use indexed columns,\n * and recent items are capped at 10.\n */\nexport async function handleDashboardStats(\n\tdb: Kysely<Database>,\n\tnow = new Date(),\n): Promise<ApiResult<DashboardStats>> {\n\ttry {\n\t\t// Discover collections from the system table\n\t\tconst collections = await db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\"slug\", \"label\"])\n\t\t\t.orderBy(\"slug\", \"asc\")\n\t\t\t.execute();\n\n\t\t// Gather per-collection counts in parallel\n\t\tconst contentRepo = new ContentRepository(db);\n\t\tconst collectionStats: CollectionStats[] = await Promise.all(\n\t\t\tcollections.map(async (col) => {\n\t\t\t\tconst stats = await contentRepo.getStats(col.slug, now);\n\t\t\t\treturn {\n\t\t\t\t\tslug: col.slug,\n\t\t\t\t\tlabel: col.label,\n\t\t\t\t\ttotal: stats.total,\n\t\t\t\t\tpublished: stats.published,\n\t\t\t\t\tdraft: stats.draft,\n\t\t\t\t\tscheduled: stats.scheduled,\n\t\t\t\t\toverdueScheduled: stats.overdueScheduled,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\n\t\t// Media and user counts\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst userRepo = new UserRepository(db);\n\t\tconst optionsRepo = new OptionsRepository(db);\n\t\tconst [\n\t\t\tmediaCount,\n\t\t\tuserCount,\n\t\t\tschedulerHealth,\n\t\t\tpolicyRejectedScheduled,\n\t\t\tversionedPolicyRejectionOptions,\n\t\t] = await Promise.all([\n\t\t\tmediaRepo.count(),\n\t\t\tuserRepo.count(),\n\t\t\tgetSchedulerHealth(db, now),\n\t\t\toptionsRepo.countByPrefix(SCHEDULED_POLICY_REJECTION_PREFIX),\n\t\t\toptionsRepo.getVersionedByPrefix<ScheduledPolicyRejection>(\n\t\t\t\tSCHEDULED_POLICY_REJECTION_PREFIX,\n\t\t\t\t{\n\t\t\t\t\tlimit: POLICY_REJECTION_PREVIEW_LIMIT,\n\t\t\t\t},\n\t\t\t),\n\t\t]);\n\t\tconst policyRejections = [...versionedPolicyRejectionOptions.values()]\n\t\t\t.filter(({ value }) => isScheduledPolicyRejection(value))\n\t\t\t.map(({ value, revision }) => ({ ...value, _rev: revision }));\n\n\t\t// Recent items across all collections (last 10 updated, any status)\n\t\tconst recentItems = await fetchRecentItems(db, collections);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tcollections: collectionStats,\n\t\t\t\tmediaCount,\n\t\t\t\tuserCount,\n\t\t\t\trecentItems,\n\t\t\t\tschedulerHealth,\n\t\t\t\tpolicyRejectedScheduled,\n\t\t\t\tpolicyRejections,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Dashboard stats error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"DASHBOARD_STATS_ERROR\",\n\t\t\t\tmessage: \"Failed to load dashboard statistics\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/** Raw row shape from the UNION ALL query — all snake_case. */\ninterface RecentItemRow {\n\tid: string;\n\tcollection: string;\n\tcollection_label: string;\n\ttitle: string;\n\tslug: string | null;\n\tstatus: string;\n\tupdated_at: string;\n\tauthor_id: string | null;\n}\n\n/**\n * Fetch the 10 most recently updated items across all collections.\n *\n * Uses UNION ALL over each ec_* table. The query is safe because\n * collection slugs come from the system table and are validated.\n *\n * `title` is not a standard column — it's a user-defined field. We query\n * `_emdash_fields` to discover which collections have one and fall back\n * to `slug` (which is always present) otherwise.\n */\nasync function fetchRecentItems(\n\tdb: Kysely<Database>,\n\tcollections: Array<{ slug: string; label: string }>,\n): Promise<RecentItem[]> {\n\tif (collections.length === 0) return [];\n\n\t// Discover which collections have a \"title\" column\n\tconst titleFields = await db\n\t\t.selectFrom(\"_emdash_fields as f\")\n\t\t.innerJoin(\"_emdash_collections as c\", \"c.id\", \"f.collection_id\")\n\t\t.select([\"c.slug as collection_slug\"])\n\t\t.where(\"f.slug\", \"=\", \"title\")\n\t\t.execute();\n\n\tconst collectionsWithTitle = new Set(titleFields.map((r) => r.collection_slug));\n\n\t// Issue one query per collection in parallel, then merge in JS.\n\t// A single UNION ALL across N collections trips D1's\n\t// SQLITE_LIMIT_COMPOUND_SELECT cap when N is large enough (#895);\n\t// per-collection queries side-step that. Each query fetches at most\n\t// 10 rows, so the merge handles at most N * 10 rows before slicing.\n\tconst perCollection = await Promise.all(\n\t\tcollections.map(async (col) => {\n\t\t\tvalidateIdentifier(col.slug);\n\t\t\tconst table = `ec_${col.slug}`;\n\t\t\tconst hasTitle = collectionsWithTitle.has(col.slug);\n\n\t\t\t// Use title column if it exists, otherwise fall back to slug, id.\n\t\t\t// All output uses snake_case to avoid SQLite quoting issues on D1.\n\t\t\tconst titleExpr = hasTitle ? sql`COALESCE(title, slug, id)` : sql`COALESCE(slug, id)`;\n\n\t\t\tconst result = await sql<RecentItemRow>`\n\t\t\t\tSELECT\n\t\t\t\t\tid,\n\t\t\t\t\t${sql.lit(col.slug)} AS collection,\n\t\t\t\t\t${sql.lit(col.label)} AS collection_label,\n\t\t\t\t\t${titleExpr} AS title,\n\t\t\t\t\tslug,\n\t\t\t\t\tstatus,\n\t\t\t\t\tupdated_at,\n\t\t\t\t\tauthor_id\n\t\t\t\tFROM ${sql.ref(table)}\n\t\t\t\tWHERE deleted_at IS NULL\n\t\t\t\tORDER BY updated_at DESC\n\t\t\t\tLIMIT 10\n\t\t\t`.execute(db);\n\t\t\treturn result.rows;\n\t\t}),\n\t);\n\n\t// Merge across collections, sort by updated_at desc, take top 10.\n\tconst merged = perCollection\n\t\t.flat()\n\t\t.toSorted((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0))\n\t\t.slice(0, 10);\n\n\t// Map snake_case DB rows to camelCase API shape\n\treturn merged.map((row) => ({\n\t\tid: row.id,\n\t\tcollection: row.collection,\n\t\tcollectionLabel: row.collection_label,\n\t\ttitle: row.title,\n\t\tslug: row.slug,\n\t\tstatus: row.status,\n\t\tupdatedAt: row.updated_at,\n\t\tauthorId: row.author_id,\n\t}));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dashboard-BP-iJqpd.mjs","names":[],"sources":["../src/scheduler-health.ts","../src/api/handlers/dashboard.ts"],"sourcesContent":["import type { Kysely } from \"kysely\";\n\nimport { OptionsRepository } from \"./database/repositories/options.js\";\nimport type { Database } from \"./database/types.js\";\n\nexport const SCHEDULER_HEARTBEAT_OPTION = \"system:scheduler:last_completed_at\";\nexport const SCHEDULER_STALE_AFTER_MS = 5 * 60 * 1000;\n\nexport interface SchedulerHealth {\n\tstatus: \"healthy\" | \"stale\" | \"unknown\";\n\tlastCompletedAt: string | null;\n}\n\nexport async function recordSchedulerHeartbeat(\n\tdb: Kysely<Database>,\n\tcompletedAt = new Date(),\n): Promise<void> {\n\tawait new OptionsRepository(db).set(SCHEDULER_HEARTBEAT_OPTION, completedAt.toISOString());\n}\n\nexport async function recordSchedulerHeartbeatSafely(\n\tdb: Kysely<Database>,\n\tcompletedAt = new Date(),\n): Promise<void> {\n\ttry {\n\t\tawait recordSchedulerHeartbeat(db, completedAt);\n\t} catch (error) {\n\t\tconsole.error(\"[scheduler] Failed to record heartbeat:\", error);\n\t}\n}\n\nexport async function getSchedulerHealth(\n\tdb: Kysely<Database>,\n\tnow = new Date(),\n): Promise<SchedulerHealth> {\n\tconst lastCompletedAt = await new OptionsRepository(db).get<string>(SCHEDULER_HEARTBEAT_OPTION);\n\tconst lastCompletedAtMs = lastCompletedAt ? Date.parse(lastCompletedAt) : Number.NaN;\n\tif (!lastCompletedAt || Number.isNaN(lastCompletedAtMs)) {\n\t\treturn { status: \"unknown\", lastCompletedAt: null };\n\t}\n\n\treturn {\n\t\tstatus: now.getTime() - lastCompletedAtMs > SCHEDULER_STALE_AFTER_MS ? \"stale\" : \"healthy\",\n\t\tlastCompletedAt,\n\t};\n}\n","/**\n * Dashboard stats handler\n *\n * Returns summary data for the admin dashboard in a single request:\n * collection content counts, media count, user count, and recent\n * content across all collections.\n */\n\nimport { sql, type Kysely } from \"kysely\";\n\nimport { ContentRepository } from \"../../database/repositories/content.js\";\nimport { MediaRepository } from \"../../database/repositories/media.js\";\nimport { OptionsRepository } from \"../../database/repositories/options.js\";\nimport { UserRepository } from \"../../database/repositories/user.js\";\nimport type { Database } from \"../../database/types.js\";\nimport { validateIdentifier } from \"../../database/validate.js\";\nimport {\n\tSCHEDULED_POLICY_REJECTION_PREFIX,\n\tisScheduledPolicyRejection,\n\ttype ScheduledPolicyRejection,\n\ttype VersionedScheduledPolicyRejection,\n} from \"../../plugins/content-policy.js\";\nimport { getSchedulerHealth, type SchedulerHealth } from \"../../scheduler-health.js\";\nimport type { ApiResult } from \"../types.js\";\n\nexport interface CollectionStats {\n\tslug: string;\n\tlabel: string;\n\ttotal: number;\n\tpublished: number;\n\tdraft: number;\n\tscheduled: number;\n\toverdueScheduled: number;\n}\n\nexport interface RecentItem {\n\tid: string;\n\tcollection: string;\n\tcollectionLabel: string;\n\ttitle: string;\n\tslug: string | null;\n\tstatus: string;\n\tupdatedAt: string;\n\tauthorId: string | null;\n}\n\nexport interface DashboardStats {\n\tcollections: CollectionStats[];\n\tmediaCount: number;\n\tuserCount: number;\n\trecentItems: RecentItem[];\n\tschedulerHealth: SchedulerHealth;\n\tpolicyRejectedScheduled: number;\n\tpolicyRejections: VersionedScheduledPolicyRejection[];\n}\n\nconst POLICY_REJECTION_PREVIEW_LIMIT = 20;\n\n/**\n * Fetch dashboard statistics.\n *\n * Queries are intentionally lightweight — counts use indexed columns,\n * and recent items are capped at 10.\n */\nexport async function handleDashboardStats(\n\tdb: Kysely<Database>,\n\tnow = new Date(),\n): Promise<ApiResult<DashboardStats>> {\n\ttry {\n\t\t// Discover collections from the system table\n\t\tconst collections = await db\n\t\t\t.selectFrom(\"_emdash_collections\")\n\t\t\t.select([\"slug\", \"label\"])\n\t\t\t.orderBy(\"slug\", \"asc\")\n\t\t\t.execute();\n\n\t\t// Gather per-collection counts in parallel\n\t\tconst contentRepo = new ContentRepository(db);\n\t\tconst collectionStats: CollectionStats[] = await Promise.all(\n\t\t\tcollections.map(async (col) => {\n\t\t\t\tconst stats = await contentRepo.getStats(col.slug, now);\n\t\t\t\treturn {\n\t\t\t\t\tslug: col.slug,\n\t\t\t\t\tlabel: col.label,\n\t\t\t\t\ttotal: stats.total,\n\t\t\t\t\tpublished: stats.published,\n\t\t\t\t\tdraft: stats.draft,\n\t\t\t\t\tscheduled: stats.scheduled,\n\t\t\t\t\toverdueScheduled: stats.overdueScheduled,\n\t\t\t\t};\n\t\t\t}),\n\t\t);\n\n\t\t// Media and user counts\n\t\tconst mediaRepo = new MediaRepository(db);\n\t\tconst userRepo = new UserRepository(db);\n\t\tconst optionsRepo = new OptionsRepository(db);\n\t\tconst [\n\t\t\tmediaCount,\n\t\t\tuserCount,\n\t\t\tschedulerHealth,\n\t\t\tpolicyRejectedScheduled,\n\t\t\tversionedPolicyRejectionOptions,\n\t\t] = await Promise.all([\n\t\t\tmediaRepo.count(),\n\t\t\tuserRepo.count(),\n\t\t\tgetSchedulerHealth(db, now),\n\t\t\toptionsRepo.countByPrefix(SCHEDULED_POLICY_REJECTION_PREFIX),\n\t\t\toptionsRepo.getVersionedByPrefix<ScheduledPolicyRejection>(\n\t\t\t\tSCHEDULED_POLICY_REJECTION_PREFIX,\n\t\t\t\t{\n\t\t\t\t\tlimit: POLICY_REJECTION_PREVIEW_LIMIT,\n\t\t\t\t},\n\t\t\t),\n\t\t]);\n\t\tconst policyRejections = [...versionedPolicyRejectionOptions.values()]\n\t\t\t.filter(({ value }) => isScheduledPolicyRejection(value))\n\t\t\t.map(({ value, revision }) => ({ ...value, _rev: revision }));\n\n\t\t// Recent items across all collections (last 10 updated, any status)\n\t\tconst recentItems = await fetchRecentItems(db, collections);\n\n\t\treturn {\n\t\t\tsuccess: true,\n\t\t\tdata: {\n\t\t\t\tcollections: collectionStats,\n\t\t\t\tmediaCount,\n\t\t\t\tuserCount,\n\t\t\t\trecentItems,\n\t\t\t\tschedulerHealth,\n\t\t\t\tpolicyRejectedScheduled,\n\t\t\t\tpolicyRejections,\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconsole.error(\"Dashboard stats error:\", error);\n\t\treturn {\n\t\t\tsuccess: false,\n\t\t\terror: {\n\t\t\t\tcode: \"DASHBOARD_STATS_ERROR\",\n\t\t\t\tmessage: \"Failed to load dashboard statistics\",\n\t\t\t},\n\t\t};\n\t}\n}\n\n/** Raw row shape from the UNION ALL query — all snake_case. */\ninterface RecentItemRow {\n\tid: string;\n\tcollection: string;\n\tcollection_label: string;\n\ttitle: string;\n\tslug: string | null;\n\tstatus: string;\n\tupdated_at: string;\n\tauthor_id: string | null;\n}\n\n/**\n * Fetch the 10 most recently updated items across all collections.\n *\n * Uses UNION ALL over each ec_* table. The query is safe because\n * collection slugs come from the system table and are validated.\n *\n * `title` is not a standard column — it's a user-defined field. We query\n * `_emdash_fields` to discover which collections have one and fall back\n * to `slug` (which is always present) otherwise.\n */\nasync function fetchRecentItems(\n\tdb: Kysely<Database>,\n\tcollections: Array<{ slug: string; label: string }>,\n): Promise<RecentItem[]> {\n\tif (collections.length === 0) return [];\n\n\t// Discover which collections have a \"title\" column\n\tconst titleFields = await db\n\t\t.selectFrom(\"_emdash_fields as f\")\n\t\t.innerJoin(\"_emdash_collections as c\", \"c.id\", \"f.collection_id\")\n\t\t.select([\"c.slug as collection_slug\"])\n\t\t.where(\"f.slug\", \"=\", \"title\")\n\t\t.execute();\n\n\tconst collectionsWithTitle = new Set(titleFields.map((r) => r.collection_slug));\n\n\t// Issue one query per collection in parallel, then merge in JS.\n\t// A single UNION ALL across N collections trips D1's\n\t// SQLITE_LIMIT_COMPOUND_SELECT cap when N is large enough (#895);\n\t// per-collection queries side-step that. Each query fetches at most\n\t// 10 rows, so the merge handles at most N * 10 rows before slicing.\n\tconst perCollection = await Promise.all(\n\t\tcollections.map(async (col) => {\n\t\t\tvalidateIdentifier(col.slug);\n\t\t\tconst table = `ec_${col.slug}`;\n\t\t\tconst hasTitle = collectionsWithTitle.has(col.slug);\n\n\t\t\t// Use title column if it exists, otherwise fall back to slug, id.\n\t\t\t// All output uses snake_case to avoid SQLite quoting issues on D1.\n\t\t\tconst titleExpr = hasTitle ? sql`COALESCE(title, slug, id)` : sql`COALESCE(slug, id)`;\n\n\t\t\tconst result = await sql<RecentItemRow>`\n\t\t\t\tSELECT\n\t\t\t\t\tid,\n\t\t\t\t\t${sql.lit(col.slug)} AS collection,\n\t\t\t\t\t${sql.lit(col.label)} AS collection_label,\n\t\t\t\t\t${titleExpr} AS title,\n\t\t\t\t\tslug,\n\t\t\t\t\tstatus,\n\t\t\t\t\tupdated_at,\n\t\t\t\t\tauthor_id\n\t\t\t\tFROM ${sql.ref(table)}\n\t\t\t\tWHERE deleted_at IS NULL\n\t\t\t\tORDER BY updated_at DESC\n\t\t\t\tLIMIT 10\n\t\t\t`.execute(db);\n\t\t\treturn result.rows;\n\t\t}),\n\t);\n\n\t// Merge across collections, sort by updated_at desc, take top 10.\n\tconst merged = perCollection\n\t\t.flat()\n\t\t.toSorted((a, b) => (a.updated_at < b.updated_at ? 1 : a.updated_at > b.updated_at ? -1 : 0))\n\t\t.slice(0, 10);\n\n\t// Map snake_case DB rows to camelCase API shape\n\treturn merged.map((row) => ({\n\t\tid: row.id,\n\t\tcollection: row.collection,\n\t\tcollectionLabel: row.collection_label,\n\t\ttitle: row.title,\n\t\tslug: row.slug,\n\t\tstatus: row.status,\n\t\tupdatedAt: row.updated_at,\n\t\tauthorId: row.author_id,\n\t}));\n}\n"],"mappings":"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"}
|
|
@@ -2,8 +2,8 @@ import "../runner-1y4J9fJB.mjs";
|
|
|
2
2
|
import "../dialect-helpers-7_IqPdh9.mjs";
|
|
3
3
|
import "../config-CvmjJcVw.mjs";
|
|
4
4
|
import "../datetime-normalization-PeiEdcRY.mjs";
|
|
5
|
-
import "../identity-
|
|
6
|
-
import { t as createDirectMigrationExecutor } from "../direct-executor-
|
|
5
|
+
import "../identity-Be5kzutJ.mjs";
|
|
6
|
+
import { t as createDirectMigrationExecutor } from "../direct-executor-D2lNadwo.mjs";
|
|
7
7
|
import { n as requireMigrationEnvironment, t as createMigrationTarget } from "../target-_7pn2Gbg.mjs";
|
|
8
8
|
import { createDialect } from "./libsql.mjs";
|
|
9
9
|
import { pathToFileURL } from "node:url";
|
|
@@ -2,8 +2,8 @@ import "../runner-1y4J9fJB.mjs";
|
|
|
2
2
|
import "../dialect-helpers-7_IqPdh9.mjs";
|
|
3
3
|
import "../config-CvmjJcVw.mjs";
|
|
4
4
|
import "../datetime-normalization-PeiEdcRY.mjs";
|
|
5
|
-
import "../identity-
|
|
6
|
-
import { t as createDirectMigrationExecutor } from "../direct-executor-
|
|
5
|
+
import "../identity-Be5kzutJ.mjs";
|
|
6
|
+
import { t as createDirectMigrationExecutor } from "../direct-executor-D2lNadwo.mjs";
|
|
7
7
|
import { n as requireMigrationEnvironment, t as createMigrationTarget } from "../target-_7pn2Gbg.mjs";
|
|
8
8
|
import { createDialect } from "./postgres.mjs";
|
|
9
9
|
|
|
@@ -2,9 +2,9 @@ import "../runner-1y4J9fJB.mjs";
|
|
|
2
2
|
import "../dialect-helpers-7_IqPdh9.mjs";
|
|
3
3
|
import "../config-CvmjJcVw.mjs";
|
|
4
4
|
import "../datetime-normalization-PeiEdcRY.mjs";
|
|
5
|
-
import "../identity-
|
|
5
|
+
import "../identity-Be5kzutJ.mjs";
|
|
6
6
|
import { createDialect } from "./sqlite.mjs";
|
|
7
|
-
import { t as createDirectMigrationExecutor } from "../direct-executor-
|
|
7
|
+
import { t as createDirectMigrationExecutor } from "../direct-executor-D2lNadwo.mjs";
|
|
8
8
|
import { t as createMigrationTarget } from "../target-_7pn2Gbg.mjs";
|
|
9
9
|
import { isAbsolute, resolve } from "node:path";
|
|
10
10
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { i as getExactMigrationStatus, s as runMigrations } from "./runner-1y4J9fJB.mjs";
|
|
2
2
|
import { clearMigrationLock, readMigrationLock } from "./database/migration-lock.mjs";
|
|
3
3
|
import { c as setI18nConfig, r as getI18nConfig } from "./config-CvmjJcVw.mjs";
|
|
4
|
-
import { r as getCoreMigrationIdentity } from "./identity-
|
|
4
|
+
import { r as getCoreMigrationIdentity } from "./identity-Be5kzutJ.mjs";
|
|
5
5
|
import { Kysely } from "kysely";
|
|
6
6
|
|
|
7
7
|
//#region src/migrations/direct-executor.ts
|
|
@@ -101,4 +101,4 @@ function createDirectMigrationExecutor(options) {
|
|
|
101
101
|
|
|
102
102
|
//#endregion
|
|
103
103
|
export { createDirectMigrationExecutor as t };
|
|
104
|
-
//# sourceMappingURL=direct-executor-
|
|
104
|
+
//# sourceMappingURL=direct-executor-D2lNadwo.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"direct-executor-
|
|
1
|
+
{"version":3,"file":"direct-executor-D2lNadwo.mjs","names":[],"sources":["../src/migrations/direct-executor.ts"],"sourcesContent":["import { type Dialect, Kysely } from \"kysely\";\n\nimport { clearMigrationLock, readMigrationLock } from \"../database/migration-lock.js\";\nimport {\n\tgetExactMigrationStatus,\n\trunMigrations,\n\ttype ExactMigrationStatus,\n} from \"../database/migrations/runner.js\";\nimport type { Database } from \"../database/types.js\";\nimport { getI18nConfig, setI18nConfig } from \"../i18n/config.js\";\nimport { getCoreMigrationIdentity } from \"./identity.js\";\nimport type {\n\tMigrationExecutor,\n\tMigrationReport,\n\tMigrationRequest,\n\tMigrationTarget,\n} from \"./protocol.js\";\n\nexport interface DirectMigrationExecutorOptions {\n\ttarget: MigrationTarget;\n\tcreateDialect: () => Dialect | Promise<Dialect>;\n}\n\nconst LOCK_ID_PATTERN = /^[1-9]\\d{0,15}$/;\n\nfunction createReport(\n\ttarget: MigrationTarget,\n\tstatus: ExactMigrationStatus,\n\texecuted: readonly string[],\n\tlockHeldSince: number | null = null,\n): MigrationReport {\n\tconst report: MigrationReport = {\n\t\ttarget,\n\t\tknownApplied: [...status.knownApplied],\n\t\tpending: [...status.pending],\n\t\tunknownApplied: [...status.unknownApplied],\n\t\texecuted: [...executed],\n\t};\n\tif (lockHeldSince !== null) {\n\t\treport.lock = {\n\t\t\tid: String(lockHeldSince),\n\t\t\theldSince: new Date(lockHeldSince).toISOString(),\n\t\t};\n\t}\n\treturn report;\n}\n\nasync function releaseLock(db: Kysely<Database>, lockId: string | undefined): Promise<void> {\n\tif (!lockId || !LOCK_ID_PATTERN.test(lockId)) {\n\t\tthrow new Error(\"Releasing the migration lock requires the lock id reported by status.\");\n\t}\n\tconst lock = Number(lockId);\n\t// Read before writing: on Postgres the lock column is a 32-bit integer that\n\t// a lock id does not fit, and no Postgres lock is ever held in it.\n\tconst heldSince = await readMigrationLock(db);\n\tif (heldSince === lock && (await clearMigrationLock(db, lock))) return;\n\tconst current = heldSince === lock ? await readMigrationLock(db) : heldSince;\n\t// No lock ids here: the CLI's redaction can rewrite digits that match an\n\t// environment value.\n\tthrow new Error(\n\t\tcurrent === null\n\t\t\t? \"The migration lock is not held.\"\n\t\t\t: \"The migration lock has a different id now. Check the status again before releasing it.\",\n\t);\n}\n\nasync function verifyRequest(request: MigrationRequest): Promise<void> {\n\tconst identity = await getCoreMigrationIdentity();\n\tif (\n\t\trequest.artifact.emdashVersion !== identity.emdashVersion ||\n\t\trequest.artifact.migrationSetFingerprint !== identity.fingerprint\n\t) {\n\t\tthrow new Error(\n\t\t\t\"Migration artifact does not match the loaded EmDash version and migration registry.\",\n\t\t);\n\t}\n}\n\nasync function executeRequest(\n\tdb: Kysely<Database>,\n\ttarget: MigrationTarget,\n\trequest: MigrationRequest,\n): Promise<MigrationReport> {\n\tconst initialStatus = await getExactMigrationStatus(db);\n\tif (request.action === \"check\") {\n\t\treturn createReport(target, initialStatus, [], await readMigrationLock(db));\n\t}\n\tif (request.action === \"release-lock\") {\n\t\tawait releaseLock(db, request.lockId);\n\t\treturn createReport(target, initialStatus, []);\n\t}\n\n\tif (initialStatus.unknownApplied.length > 0) {\n\t\tthrow new Error(\n\t\t\t`Cannot apply migrations with unknown applied migrations: ${initialStatus.unknownApplied.join(\", \")}`,\n\t\t);\n\t}\n\tif (initialStatus.pending.length === 0) {\n\t\treturn createReport(target, initialStatus, []);\n\t}\n\n\tconst result = await runMigrations(db);\n\tconst finalStatus = await getExactMigrationStatus(db);\n\treturn createReport(target, finalStatus, result.applied);\n}\n\nexport function createDirectMigrationExecutor(\n\toptions: DirectMigrationExecutorOptions,\n): MigrationExecutor {\n\tconst target = Object.freeze({ ...options.target });\n\tlet used = false;\n\tlet disposed = false;\n\tlet activeDb: Kysely<Database> | undefined;\n\tlet closePromise: Promise<void> | undefined;\n\n\tconst closeActiveDb = (): Promise<void> => {\n\t\tif (!activeDb) return Promise.resolve();\n\t\tclosePromise ??= activeDb.destroy();\n\t\treturn closePromise;\n\t};\n\n\treturn {\n\t\ttarget,\n\t\tasync execute(request) {\n\t\t\tif (disposed) {\n\t\t\t\tthrow new Error(\"Migration executor has been disposed.\");\n\t\t\t}\n\t\t\tif (used) {\n\t\t\t\tthrow new Error(\"Migration executors are single-use.\");\n\t\t\t}\n\t\t\tused = true;\n\n\t\t\tif (\n\t\t\t\trequest.action !== \"check\" &&\n\t\t\t\trequest.action !== \"apply\" &&\n\t\t\t\trequest.action !== \"release-lock\"\n\t\t\t) {\n\t\t\t\tthrow new Error(\"Unsupported migration action.\");\n\t\t\t}\n\t\t\tawait verifyRequest(request);\n\n\t\t\tconst dialect = await options.createDialect();\n\t\t\tconst db = new Kysely<Database>({ dialect });\n\t\t\tactiveDb = db;\n\t\t\tif (disposed) {\n\t\t\t\tawait closeActiveDb();\n\t\t\t\tthrow new Error(\"Migration executor has been disposed.\");\n\t\t\t}\n\t\t\tconst previousI18n = getI18nConfig();\n\t\t\tlet executionFailed = false;\n\t\t\tlet executionError: unknown;\n\t\t\tlet report: MigrationReport | undefined;\n\t\t\tsetI18nConfig(request.i18n);\n\n\t\t\ttry {\n\t\t\t\treport = await executeRequest(db, target, request);\n\t\t\t} catch (error) {\n\t\t\t\texecutionFailed = true;\n\t\t\t\texecutionError = error;\n\t\t\t} finally {\n\t\t\t\tsetI18nConfig(previousI18n);\n\t\t\t\ttry {\n\t\t\t\t\tawait closeActiveDb();\n\t\t\t\t} catch {\n\t\t\t\t\tconsole.error(\"[migrations] Database close failed.\");\n\t\t\t\t\tclosePromise = Promise.resolve();\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (executionFailed) throw executionError;\n\t\t\tif (!report) throw new Error(\"Migration execution did not produce a report.\");\n\t\t\treturn report;\n\t\t},\n\t\tdispose() {\n\t\t\tdisposed = true;\n\t\t\treturn closeActiveDb();\n\t\t},\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { r as MIGRATION_RACE_WAIT_MS, t as ConcurrentMigrationTimeoutError } fro
|
|
|
2
2
|
import { n as isMissingTableError } from "./db-errors-CBD5yMro.mjs";
|
|
3
3
|
import { d as isSqlite, m as listTablesLike } from "./dialect-helpers-7_IqPdh9.mjs";
|
|
4
4
|
import { r as getI18nConfig, s as resolveContentCreateLocale } from "./config-CvmjJcVw.mjs";
|
|
5
|
-
import { S as definePlugin, _ as isContentSaveRejection, b as getActiveContentSaveHookName, c as inspectSandboxHookResult, g as ContentSaveRejectedError, h as EmailPipeline, x as resolveExclusiveHooks, y as createHookPipeline } from "./menus-
|
|
5
|
+
import { S as definePlugin, _ as isContentSaveRejection, b as getActiveContentSaveHookName, c as inspectSandboxHookResult, g as ContentSaveRejectedError, h as EmailPipeline, x as resolveExclusiveHooks, y as createHookPipeline } from "./menus-Cm68rsAq.mjs";
|
|
6
6
|
import { t as after } from "./after-PcEvMY_6.mjs";
|
|
7
7
|
import { getRequestContext } from "./request-context.mjs";
|
|
8
8
|
import { t as OptionsRepository } from "./options-DhrI7eMv.mjs";
|
|
@@ -12,10 +12,11 @@ import { r as singleFlightCached, t as createSingleFlightCache } from "./single-
|
|
|
12
12
|
import { o as validateEncryptionKeyAtStartup } from "./secrets-CIi0nFiR.mjs";
|
|
13
13
|
import { i as RevisionRepository, o as keepKnownFields, s as staleStoredKeys, t as ContentRepository } from "./content-DFaIEDj9.mjs";
|
|
14
14
|
import { n as ContentMutationConflictError } from "./types-BkUg2yHx.mjs";
|
|
15
|
-
import { t as MediaRepository } from "./media-
|
|
15
|
+
import { t as MediaRepository } from "./media-CPXTbb1R.mjs";
|
|
16
16
|
import { t as CommentRepository } from "./comment-CrBl22OF.mjs";
|
|
17
17
|
import { t as AuditRepository } from "./audit-RfCugiSB.mjs";
|
|
18
|
-
import { d as createTaxonomyAccessWithWrite, l as createSiteInfo, r as createCommentAccess, y as updatePluginMediaMetadata } from "./context-
|
|
18
|
+
import { d as createTaxonomyAccessWithWrite, l as createSiteInfo, r as createCommentAccess, y as updatePluginMediaMetadata } from "./context-BV_7t-Ua.mjs";
|
|
19
|
+
import { t as RedirectRepository } from "./redirect-B29UjdUn.mjs";
|
|
19
20
|
import { r as requestCached } from "./request-cache-DzLYKm_x.mjs";
|
|
20
21
|
import { n as normalizeMediaValue } from "./normalize-CO2P8-iE.mjs";
|
|
21
22
|
import { n as normalizeMime, t as matchesMimeAllowlist } from "./mime-ChLqYwDk.mjs";
|
|
@@ -25,22 +26,21 @@ import { t as FTSManager } from "./fts-manager-S-K0laoS.mjs";
|
|
|
25
26
|
import { m as selectTaxonomyDefs, s as getDb } from "./loader-BzqeR1ZN.mjs";
|
|
26
27
|
import { kyselyLogOption } from "./database/instrumentation.mjs";
|
|
27
28
|
import { r as primeRegisteredCollections } from "./collection-slugs-cache-CjGk__zp.mjs";
|
|
28
|
-
import { A as handleContentDiscardDraft, B as handleContentTranslations, D as handleContentCountTrashed, E as handleContentCountScheduled, F as handleContentListTrashed, G as validateRev, H as handleContentUnschedule, I as handleContentPermanentDelete, L as handleContentPublish, M as handleContentGet, N as handleContentGetIncludingTrashed, O as handleContentCreate, P as handleContentList, R as handleContentRestore, S as buildManifestCollections, T as handleContentCompare, U as handleContentUpdate, V as handleContentUnpublish, W as handleScheduledPolicyRejection, b as handleRevisionList, h as syncDeclaredStorageIndexes, j as handleContentDuplicate, k as handleContentDelete, p as loadBundleFromR2, w as handleContentAuthors, x as handleRevisionRestore, y as handleRevisionGet, z as handleContentSchedule } from "./api-
|
|
29
|
+
import { A as handleContentDiscardDraft, B as handleContentTranslations, D as handleContentCountTrashed, E as handleContentCountScheduled, F as handleContentListTrashed, G as validateRev, H as handleContentUnschedule, I as handleContentPermanentDelete, L as handleContentPublish, M as handleContentGet, N as handleContentGetIncludingTrashed, O as handleContentCreate, P as handleContentList, R as handleContentRestore, S as buildManifestCollections, T as handleContentCompare, U as handleContentUpdate, V as handleContentUnpublish, W as handleScheduledPolicyRejection, b as handleRevisionList, h as syncDeclaredStorageIndexes, j as handleContentDuplicate, k as handleContentDelete, p as loadBundleFromR2, w as handleContentAuthors, x as handleRevisionRestore, y as handleRevisionGet, z as handleContentSchedule } from "./api-CuyNEHZG.mjs";
|
|
29
30
|
import { r as activateMediaUsageCapture } from "./activation-LOkDM5WT.mjs";
|
|
30
31
|
import { A as refreshContentMediaUsageAfterWrite, H as markContentMediaUsageCollectionStale, O as deleteContentMediaUsage, W as MediaUsageRepository, a as SchemaRegistry, i as SchemaError, k as findNonTranslatableSiblingContentIds } from "./block-type-registry-DUI_3KxT.mjs";
|
|
31
32
|
import { n as computeContentHash, r as hashString } from "./hash-SDAW6zC-.mjs";
|
|
32
|
-
import { n as recordSchedulerHeartbeatSafely } from "./dashboard-
|
|
33
|
+
import { n as recordSchedulerHeartbeatSafely } from "./dashboard-BP-iJqpd.mjs";
|
|
33
34
|
import { i as resolveBlockTypes, r as normalizeBlocksData } from "./block-values-C_VIPr5t.mjs";
|
|
34
35
|
import { t as removeUploadAttempt } from "./upload-attempts-B68ZGNu9.mjs";
|
|
35
|
-
import { a as
|
|
36
|
-
import { s as processMediaUsageWorkAfterWrite } from "./media-usage-
|
|
36
|
+
import { a as handleMediaRegisterUpload, i as handleMediaList, n as handleMediaDelete, o as handleMediaReplaceMetadata, r as handleMediaGet, s as handleMediaUpdate, t as handleMediaCreate } from "./media-JXRWDhw3.mjs";
|
|
37
|
+
import { s as processMediaUsageWorkAfterWrite } from "./media-usage-De-zeR9E.mjs";
|
|
37
38
|
import { t as ErrorCode } from "./errors-BF0_LFhW.mjs";
|
|
38
39
|
import { t as PluginStateRepository } from "./state-DDiPySSG.mjs";
|
|
39
|
-
import { s as invalidateSiteSettingsCache } from "./settings-
|
|
40
|
+
import { s as invalidateSiteSettingsCache } from "./settings-B4vDl1fK.mjs";
|
|
40
41
|
import { i as normalizeManifestRoute$1, r as normalizeManifestHook, t as HOOK_NAMES } from "./manifest-schema-CY3zng_M.mjs";
|
|
41
42
|
import { i as getSandboxRouteErrorDetails } from "./types-D7HUsguf.mjs";
|
|
42
43
|
import { s as normalizePluginCapabilities } from "./types-BZakh7Ym.mjs";
|
|
43
|
-
import { a as ssrfSafeFetch, t as SsrfError } from "./ssrf-CJUnJymJ.mjs";
|
|
44
44
|
import { c as resolveRegistryConfigForSandbox, r as getRegistryConfigInput, s as resolveManifestRegistryConfig } from "./config-IhybvkIQ.mjs";
|
|
45
45
|
import { n as DEFAULT_MAX_UPLOAD_SIZE, r as formatFileSize, t as CONTENT_TYPE_RE } from "./media-Pu1L6xir.mjs";
|
|
46
46
|
import { t as getTrustedProxyHeaders } from "./trusted-proxy-tKiVtyTD.mjs";
|
|
@@ -51,13 +51,13 @@ import { n as CronExecutor } from "./cron-7DCJwPXq.mjs";
|
|
|
51
51
|
import { r as devConsoleEmailDeliver, t as DEV_CONSOLE_EMAIL_PLUGIN_ID } from "./email-console-bJn3ctqA.mjs";
|
|
52
52
|
import { n as assertSiteWriteAllowed } from "./fence-CMDXJYc_.mjs";
|
|
53
53
|
import { t as PluginRouteRequestError } from "./route-wire--1hLuACz.mjs";
|
|
54
|
-
import { a as toRouteCallerInfo, n as buildRouteMeta, r as parseRouteInput, t as PluginRouteRegistry } from "./routes-
|
|
54
|
+
import { a as toRouteCallerInfo, n as buildRouteMeta, r as parseRouteInput, t as PluginRouteRegistry } from "./routes-vrR95WR6.mjs";
|
|
55
55
|
import { n as enforceRuntimeMigrationPolicy, t as PendingMigrationsError } from "./policy-CUvwAARc.mjs";
|
|
56
|
-
import { n as VERSION, t as COMMIT } from "./version-
|
|
56
|
+
import { n as VERSION, t as COMMIT } from "./version-DXsxRrob.mjs";
|
|
57
57
|
import { t as getSiteBaseUrl } from "./site-url-BU_bbxwj.mjs";
|
|
58
58
|
import { t as getAuthMode } from "./mode-CCuKXUci.mjs";
|
|
59
59
|
import { n as lookupContentAuthor, r as sendCommentNotification, t as submitPublicComment } from "./public-submission-BFBUrqst.mjs";
|
|
60
|
-
import { u as maybeRunScheduledBackup } from "./backup-
|
|
60
|
+
import { u as maybeRunScheduledBackup } from "./backup-BQRj3afm.mjs";
|
|
61
61
|
import { t as cleanupExpiredChallenges } from "./challenge-store-ppU6Uv13.mjs";
|
|
62
62
|
import { n as createComment, r as moderateComment, t as CommentStatusConflictError } from "./service-DuykXPyW.mjs";
|
|
63
63
|
import { isRecord } from "./plugin-utils.mjs";
|
|
@@ -77,9 +77,8 @@ import { createKyselyAdapter } from "@emdash-cms/auth/adapters/kysely";
|
|
|
77
77
|
/**
|
|
78
78
|
* Programmatic media upload handler (MCP `media_upload` tool).
|
|
79
79
|
*
|
|
80
|
-
* Accepts file bytes as base64
|
|
81
|
-
*
|
|
82
|
-
* upload route: allowlist + size validation, content-hash deduplication,
|
|
80
|
+
* Accepts file bytes as base64, then runs the same pipeline as the multipart
|
|
81
|
+
* REST upload route: allowlist + size validation, content-hash deduplication,
|
|
83
82
|
* storage upload, image metadata enrichment, and record creation.
|
|
84
83
|
*/
|
|
85
84
|
function fail(code, message) {
|
|
@@ -98,48 +97,20 @@ function withUrl(item) {
|
|
|
98
97
|
url: `/_emdash/api/media/file/${item.storageKey}`
|
|
99
98
|
};
|
|
100
99
|
}
|
|
101
|
-
/**
|
|
102
|
-
function bareMime(headerValue) {
|
|
103
|
-
return (headerValue.split(";")[0] ?? "").trim();
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Acquire the file bytes and MIME type from either the base64 payload or
|
|
107
|
-
* the external URL. Returns an error result on any validation failure.
|
|
108
|
-
*/
|
|
100
|
+
/** Decode the file bytes after rejecting an oversized encoded payload. */
|
|
109
101
|
async function acquireBytes(input, maxUploadSize) {
|
|
110
|
-
if (input.base64) {
|
|
111
|
-
if (!input.contentType) return fail("VALIDATION_ERROR", "contentType is required when uploading base64 data");
|
|
112
|
-
if (input.base64.length * 3 / 4 > maxUploadSize) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`);
|
|
113
|
-
try {
|
|
114
|
-
return {
|
|
115
|
-
bytes: decodeBase64Bytes(input.base64),
|
|
116
|
-
mimeType: input.contentType
|
|
117
|
-
};
|
|
118
|
-
} catch {
|
|
119
|
-
return fail("VALIDATION_ERROR", "Invalid base64 data");
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const url = input.url;
|
|
123
|
-
if (!url) return fail("VALIDATION_ERROR", "Provide exactly one of 'base64' or 'url'");
|
|
124
|
-
let response;
|
|
102
|
+
if (input.base64.length * 3 / 4 > maxUploadSize) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`);
|
|
125
103
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
if (contentLength && parseInt(contentLength, 10) > maxUploadSize) return fail("PAYLOAD_TOO_LARGE", `File exceeds maximum size of ${formatFileSize(maxUploadSize)}`);
|
|
134
|
-
const mimeType = input.contentType ?? bareMime(response.headers.get("Content-Type") ?? "");
|
|
135
|
-
if (!mimeType) return fail("VALIDATION_ERROR", "Could not determine MIME type — pass contentType explicitly");
|
|
136
|
-
return {
|
|
137
|
-
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
138
|
-
mimeType
|
|
139
|
-
};
|
|
104
|
+
return {
|
|
105
|
+
bytes: decodeBase64Bytes(input.base64),
|
|
106
|
+
mimeType: input.contentType
|
|
107
|
+
};
|
|
108
|
+
} catch {
|
|
109
|
+
return fail("VALIDATION_ERROR", "Invalid base64 data");
|
|
110
|
+
}
|
|
140
111
|
}
|
|
141
112
|
/**
|
|
142
|
-
* Upload a media file from base64 data
|
|
113
|
+
* Upload a media file from base64 data.
|
|
143
114
|
*
|
|
144
115
|
* Mirrors the REST `POST /_emdash/api/media` route: global MIME allowlist,
|
|
145
116
|
* size limit, content-hash dedupe (returns the existing item with
|
|
@@ -147,7 +118,7 @@ async function acquireBytes(input, maxUploadSize) {
|
|
|
147
118
|
* image metadata enrichment (dimensions, blurhash, dominant color).
|
|
148
119
|
*/
|
|
149
120
|
async function handleMediaUpload(db, storage, input, hooks = {}) {
|
|
150
|
-
if (!input.base64
|
|
121
|
+
if (!input.base64 || !input.contentType) return fail("VALIDATION_ERROR", "base64 and contentType are required");
|
|
151
122
|
const rawMax = input.maxUploadSize ?? DEFAULT_MAX_UPLOAD_SIZE;
|
|
152
123
|
if (!Number.isFinite(rawMax) || rawMax <= 0) return fail("CONFIGURATION_ERROR", "Invalid maxUploadSize configuration");
|
|
153
124
|
const acquired = await acquireBytes(input, rawMax);
|
|
@@ -625,6 +596,7 @@ async function runSystemCleanup(db, storage) {
|
|
|
625
596
|
uploadAttempts: -1,
|
|
626
597
|
revisionsPruned: -1,
|
|
627
598
|
mediaUsage: -1,
|
|
599
|
+
notFoundLog: -1,
|
|
628
600
|
transferStaging: -1
|
|
629
601
|
};
|
|
630
602
|
try {
|
|
@@ -678,6 +650,11 @@ async function runSystemCleanup(db, storage) {
|
|
|
678
650
|
} catch (error) {
|
|
679
651
|
console.error("[cleanup] Failed to clean media usage:", error);
|
|
680
652
|
}
|
|
653
|
+
try {
|
|
654
|
+
result.notFoundLog = await new RedirectRepository(db).cleanup404Log();
|
|
655
|
+
} catch (error) {
|
|
656
|
+
console.error("[cleanup] Failed to cap the 404 log:", error);
|
|
657
|
+
}
|
|
681
658
|
if (storage) try {
|
|
682
659
|
const { collectTransferStaging } = await import("./gc-DjKY0Ay1.mjs");
|
|
683
660
|
result.transferStaging = (await collectTransferStaging(db, storage)).collected;
|
|
@@ -1688,7 +1665,7 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
1688
1665
|
const seedHolder = getSeedHolder();
|
|
1689
1666
|
try {
|
|
1690
1667
|
await initWithLock(seedHolder.lock, () => seedHolder.done.has(seedKey) ? true : void 0, async () => {
|
|
1691
|
-
const { applySeed } = await import("./apply-
|
|
1668
|
+
const { applySeed } = await import("./apply-npY1TAhG.mjs").then((n) => n.r);
|
|
1692
1669
|
const { loadSeed } = await import("./load-XgJiE6_3.mjs").then((n) => n.r);
|
|
1693
1670
|
const { validateSeed } = await import("./validate-CHosPEhs.mjs").then((n) => n.r);
|
|
1694
1671
|
const seed = await loadSeed();
|
|
@@ -3216,6 +3193,29 @@ var EmDashRuntime = class EmDashRuntime {
|
|
|
3216
3193
|
}
|
|
3217
3194
|
return result;
|
|
3218
3195
|
}
|
|
3196
|
+
async handleMediaRegisterUpload(input) {
|
|
3197
|
+
if (!this.storage) return {
|
|
3198
|
+
success: false,
|
|
3199
|
+
error: {
|
|
3200
|
+
code: "NO_STORAGE",
|
|
3201
|
+
message: "Storage not configured"
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
const result = await handleMediaRegisterUpload(this.db, this.storage, input);
|
|
3205
|
+
if (result.success && this.hooks.hasHooks("media:afterUpload")) {
|
|
3206
|
+
const item = result.data.item;
|
|
3207
|
+
const mediaItem = {
|
|
3208
|
+
id: item.id,
|
|
3209
|
+
filename: item.filename,
|
|
3210
|
+
mimeType: item.mimeType,
|
|
3211
|
+
size: item.size,
|
|
3212
|
+
url: `/media/${item.id}/${item.filename}`,
|
|
3213
|
+
createdAt: item.createdAt
|
|
3214
|
+
};
|
|
3215
|
+
this.hooks.runMediaAfterUpload(mediaItem).catch((err) => console.error("EmDash afterUpload hook error:", err));
|
|
3216
|
+
}
|
|
3217
|
+
return result;
|
|
3218
|
+
}
|
|
3219
3219
|
async handleMediaCreate(input) {
|
|
3220
3220
|
let processedInput = input;
|
|
3221
3221
|
if (this.hooks.hasHooks("media:beforeUpload")) {
|
|
@@ -4183,4 +4183,4 @@ async function normalizePrimaryImageValue(value, getProvider) {
|
|
|
4183
4183
|
|
|
4184
4184
|
//#endregion
|
|
4185
4185
|
export { EmDashRuntime as n, DB_INIT_DEADLINE_MS as t };
|
|
4186
|
-
//# sourceMappingURL=emdash-runtime-
|
|
4186
|
+
//# sourceMappingURL=emdash-runtime-CtfnZmPi.mjs.map
|