diff --git a/.changeset/cute-news-follow.md b/.changeset/cute-news-follow.md new file mode 100644 index 0000000..2d88f0a --- /dev/null +++ b/.changeset/cute-news-follow.md @@ -0,0 +1,5 @@ +--- +"emdash": patch +--- + +Fixes migration 033 failing with "index already exists" on databases where the schema registry had already created composite indexes on content tables. diff --git a/packages/core/src/database/migrations/033_optimize_content_indexes.ts b/packages/core/src/database/migrations/033_optimize_content_indexes.ts index aa395be..442e7f8 100644 --- a/packages/core/src/database/migrations/033_optimize_content_indexes.ts +++ b/packages/core/src/database/migrations/033_optimize_content_indexes.ts @@ -28,19 +28,19 @@ export async function up(db: Kysely): Promise { // Composite index for listing queries: WHERE deleted_at IS NULL ORDER BY updated_at DESC await sql` - CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_updated_id`)} + CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_updated_id`)} ON ${sql.ref(table.name)} (deleted_at, updated_at DESC, id DESC) `.execute(db); // Composite index for count-by-status queries: WHERE deleted_at IS NULL AND status = ? await sql` - CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_status`)} + CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_status`)} ON ${sql.ref(table.name)} (deleted_at, status) `.execute(db); // Composite index for created-at ordering: WHERE deleted_at IS NULL ORDER BY created_at DESC await sql` - CREATE INDEX ${sql.ref(`idx_${table.name}_deleted_created_id`)} + CREATE INDEX IF NOT EXISTS ${sql.ref(`idx_${table.name}_deleted_created_id`)} ON ${sql.ref(table.name)} (deleted_at, created_at DESC, id DESC) `.execute(db); } @@ -48,25 +48,25 @@ export async function up(db: Kysely): Promise { // Add partial indexes for efficient comment status counting // Each index contains only rows for one status, enabling fast COUNT queries await sql` - CREATE INDEX idx_comments_pending + CREATE INDEX IF NOT EXISTS idx_comments_pending ON _emdash_comments (id) WHERE status = 'pending' `.execute(db); await sql` - CREATE INDEX idx_comments_approved + CREATE INDEX IF NOT EXISTS idx_comments_approved ON _emdash_comments (id) WHERE status = 'approved' `.execute(db); await sql` - CREATE INDEX idx_comments_spam + CREATE INDEX IF NOT EXISTS idx_comments_spam ON _emdash_comments (id) WHERE status = 'spam' `.execute(db); await sql` - CREATE INDEX idx_comments_trash + CREATE INDEX IF NOT EXISTS idx_comments_trash ON _emdash_comments (id) WHERE status = 'trash' `.execute(db); diff --git a/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts b/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts index 217c896..ef6fe86 100644 --- a/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts +++ b/packages/core/tests/integration/smoke/site-matrix-smoke.test.ts @@ -7,11 +7,10 @@ import { describe, expect, it } from "vitest"; import { ensureBuilt } from "../server.js"; -interface RuntimeSiteCase { +interface SiteCase { name: string; dir: string; port: number; - mode: "runtime"; startupTimeoutMs: number; waitPath?: string; setupPath?: string | null; @@ -20,121 +19,61 @@ interface RuntimeSiteCase { requireDoctype?: boolean; } -interface TypecheckSiteCase { - name: string; - dir: string; - mode: "typecheck"; -} - -type SiteCase = RuntimeSiteCase | TypecheckSiteCase; - const WORKSPACE_ROOT = resolve(import.meta.dirname, "../../../../.."); const execAsync = promisify(execFile); const SITE_MATRIX: SiteCase[] = [ - // Demos - { - name: "demos/simple", - dir: resolve(WORKSPACE_ROOT, "demos/simple"), - port: 4601, - mode: "runtime", - startupTimeoutMs: 60_000, - }, - { - name: "demos/cloudflare", - dir: resolve(WORKSPACE_ROOT, "demos/cloudflare"), - port: 4602, - mode: "runtime", - startupTimeoutMs: 120_000, - }, { name: "demos/playground", dir: resolve(WORKSPACE_ROOT, "demos/playground"), port: 4603, - mode: "runtime", startupTimeoutMs: 120_000, waitPath: "/playground", frontendPath: "/playground", requireDoctype: false, }, - { - name: "demos/preview", - dir: resolve(WORKSPACE_ROOT, "demos/preview"), - port: 4604, - mode: "runtime", - startupTimeoutMs: 120_000, - setupPath: null, - frontendStatuses: [400], - requireDoctype: false, - }, - // Postgres demo requires DATABASE_URL — skip when not available - ...(process.env.DATABASE_URL - ? [ - { - name: "demos/postgres", - dir: resolve(WORKSPACE_ROOT, "demos/postgres"), - port: 4605, - mode: "runtime" as const, - startupTimeoutMs: 90_000, - }, - ] - : []), - { - name: "demos/plugins-demo", - dir: resolve(WORKSPACE_ROOT, "demos/plugins-demo"), - port: 4606, - mode: "runtime", - startupTimeoutMs: 90_000, - }, // Templates { name: "templates/blank", dir: resolve(WORKSPACE_ROOT, "templates/blank"), port: 4611, - mode: "runtime", startupTimeoutMs: 60_000, }, { name: "templates/blog", dir: resolve(WORKSPACE_ROOT, "templates/blog"), port: 4612, - mode: "runtime", startupTimeoutMs: 60_000, }, { name: "templates/blog-cloudflare", dir: resolve(WORKSPACE_ROOT, "templates/blog-cloudflare"), port: 4613, - mode: "runtime", startupTimeoutMs: 120_000, }, { name: "templates/marketing", dir: resolve(WORKSPACE_ROOT, "templates/marketing"), port: 4614, - mode: "runtime", startupTimeoutMs: 90_000, }, { name: "templates/marketing-cloudflare", dir: resolve(WORKSPACE_ROOT, "templates/marketing-cloudflare"), port: 4615, - mode: "runtime", startupTimeoutMs: 120_000, }, { name: "templates/portfolio", dir: resolve(WORKSPACE_ROOT, "templates/portfolio"), port: 4616, - mode: "runtime", startupTimeoutMs: 90_000, }, { name: "templates/portfolio-cloudflare", dir: resolve(WORKSPACE_ROOT, "templates/portfolio-cloudflare"), port: 4617, - mode: "runtime", startupTimeoutMs: 120_000, }, ]; @@ -181,18 +120,26 @@ async function fetchWithRetry(url: string, retries = 10, delayMs = 1500): Promis } // --------------------------------------------------------------------------- -// Build verification — runs a single recursive `pnpm build` across all demos -// and templates in parallel, then verifies each site produced output. +// Build verification — runs a single recursive `pnpm build` across templates +// and the playground demo in parallel. // --------------------------------------------------------------------------- describe("Site build verification", () => { - it("all demos and templates build successfully", { timeout: 300_000 }, async () => { + it("all templates and playground build successfully", { timeout: 300_000 }, async () => { await ensureBuilt(); try { await execAsync( "pnpm", - ["run", "--recursive", "--filter", "{./demos/*}", "--filter", "{./templates/*}", "build"], + [ + "run", + "--recursive", + "--filter", + "{./templates/*}", + "--filter", + "@emdash-cms/playground", + "build", + ], { cwd: WORKSPACE_ROOT, timeout: 240_000, @@ -221,16 +168,6 @@ describe("Site build verification", () => { describe.sequential("Site runtime verification", () => { for (const site of SITE_MATRIX) { - if (site.mode === "typecheck") { - it(`${site.name} typechecks`, { timeout: 120_000 }, async () => { - await execAsync("pnpm", ["run", "typecheck"], { - cwd: site.dir, - timeout: 120_000, - }); - }); - continue; - } - const waitPath = site.waitPath ?? "/_emdash/admin/"; const setupPath = site.setupPath ?? "/_emdash/api/setup/dev-bypass?redirect=/"; const frontendPath = site.frontendPath ?? "/"; diff --git a/templates/marketing/emdash-env.d.ts b/templates/marketing/emdash-env.d.ts index abb2626..d1bf261 100644 --- a/templates/marketing/emdash-env.d.ts +++ b/templates/marketing/emdash-env.d.ts @@ -17,23 +17,8 @@ export interface Page { bylines?: ContentBylineCredit[]; } -export interface Post { - id: string; - slug: string | null; - status: string; - title: string; - featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number }; - content?: PortableTextBlock[]; - excerpt?: string; - createdAt: Date; - updatedAt: Date; - publishedAt: Date | null; - bylines?: ContentBylineCredit[]; -} - declare module "emdash" { interface EmDashCollections { pages: Page; - posts: Post; } } \ No newline at end of file diff --git a/templates/portfolio-cloudflare/emdash-env.d.ts b/templates/portfolio-cloudflare/emdash-env.d.ts index 59c9083..c9a5d4b 100644 --- a/templates/portfolio-cloudflare/emdash-env.d.ts +++ b/templates/portfolio-cloudflare/emdash-env.d.ts @@ -17,20 +17,6 @@ export interface Page { bylines?: ContentBylineCredit[]; } -export interface Post { - id: string; - slug: string | null; - status: string; - title: string; - featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number }; - content?: PortableTextBlock[]; - excerpt?: string; - createdAt: Date; - updatedAt: Date; - publishedAt: Date | null; - bylines?: ContentBylineCredit[]; -} - export interface Project { id: string; slug: string | null; @@ -52,7 +38,6 @@ export interface Project { declare module "emdash" { interface EmDashCollections { pages: Page; - posts: Post; projects: Project; } } \ No newline at end of file diff --git a/templates/portfolio/emdash-env.d.ts b/templates/portfolio/emdash-env.d.ts index 59c9083..c9a5d4b 100644 --- a/templates/portfolio/emdash-env.d.ts +++ b/templates/portfolio/emdash-env.d.ts @@ -17,20 +17,6 @@ export interface Page { bylines?: ContentBylineCredit[]; } -export interface Post { - id: string; - slug: string | null; - status: string; - title: string; - featured_image?: { id: string; src?: string; alt?: string; width?: number; height?: number }; - content?: PortableTextBlock[]; - excerpt?: string; - createdAt: Date; - updatedAt: Date; - publishedAt: Date | null; - bylines?: ContentBylineCredit[]; -} - export interface Project { id: string; slug: string | null; @@ -52,7 +38,6 @@ export interface Project { declare module "emdash" { interface EmDashCollections { pages: Page; - posts: Post; projects: Project; } } \ No newline at end of file