fix: migration 033 idempotency & smoke test cleanup (#365)
* fix(core): add IF NOT EXISTS to migration 033 index creation The schema registry's createContentTable() creates composite indexes on content tables at creation time. When migration 033 runs on a database where tables were created after the registry added these indexes, it fails with "index already exists". Adding IF NOT EXISTS makes the migration idempotent. * refactor(smoke): trim site matrix to templates + playground, remove typechecks The smoke test matrix included every demo and template, plus typecheck cases. This was slow and redundant — typechecks are already covered by pnpm typecheck, and demos are dev targets not release artifacts. Trim to templates + playground only, and replace the sequential per-site astro build with a single recursive pnpm build. Remove the TypecheckSiteCase type and handling since smoke tests should only verify runtime behavior. * Update env * chore: add changeset for migration 033 fix
This commit is contained in:
5
.changeset/cute-news-follow.md
Normal file
5
.changeset/cute-news-follow.md
Normal file
@@ -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.
|
||||||
@@ -28,19 +28,19 @@ export async function up(db: Kysely<unknown>): Promise<void> {
|
|||||||
|
|
||||||
// Composite index for listing queries: WHERE deleted_at IS NULL ORDER BY updated_at DESC
|
// Composite index for listing queries: WHERE deleted_at IS NULL ORDER BY updated_at DESC
|
||||||
await sql`
|
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)
|
ON ${sql.ref(table.name)} (deleted_at, updated_at DESC, id DESC)
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
|
|
||||||
// Composite index for count-by-status queries: WHERE deleted_at IS NULL AND status = ?
|
// Composite index for count-by-status queries: WHERE deleted_at IS NULL AND status = ?
|
||||||
await sql`
|
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)
|
ON ${sql.ref(table.name)} (deleted_at, status)
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
|
|
||||||
// Composite index for created-at ordering: WHERE deleted_at IS NULL ORDER BY created_at DESC
|
// Composite index for created-at ordering: WHERE deleted_at IS NULL ORDER BY created_at DESC
|
||||||
await sql`
|
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)
|
ON ${sql.ref(table.name)} (deleted_at, created_at DESC, id DESC)
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
}
|
}
|
||||||
@@ -48,25 +48,25 @@ export async function up(db: Kysely<unknown>): Promise<void> {
|
|||||||
// Add partial indexes for efficient comment status counting
|
// Add partial indexes for efficient comment status counting
|
||||||
// Each index contains only rows for one status, enabling fast COUNT queries
|
// Each index contains only rows for one status, enabling fast COUNT queries
|
||||||
await sql`
|
await sql`
|
||||||
CREATE INDEX idx_comments_pending
|
CREATE INDEX IF NOT EXISTS idx_comments_pending
|
||||||
ON _emdash_comments (id)
|
ON _emdash_comments (id)
|
||||||
WHERE status = 'pending'
|
WHERE status = 'pending'
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
|
|
||||||
await sql`
|
await sql`
|
||||||
CREATE INDEX idx_comments_approved
|
CREATE INDEX IF NOT EXISTS idx_comments_approved
|
||||||
ON _emdash_comments (id)
|
ON _emdash_comments (id)
|
||||||
WHERE status = 'approved'
|
WHERE status = 'approved'
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
|
|
||||||
await sql`
|
await sql`
|
||||||
CREATE INDEX idx_comments_spam
|
CREATE INDEX IF NOT EXISTS idx_comments_spam
|
||||||
ON _emdash_comments (id)
|
ON _emdash_comments (id)
|
||||||
WHERE status = 'spam'
|
WHERE status = 'spam'
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
|
|
||||||
await sql`
|
await sql`
|
||||||
CREATE INDEX idx_comments_trash
|
CREATE INDEX IF NOT EXISTS idx_comments_trash
|
||||||
ON _emdash_comments (id)
|
ON _emdash_comments (id)
|
||||||
WHERE status = 'trash'
|
WHERE status = 'trash'
|
||||||
`.execute(db);
|
`.execute(db);
|
||||||
|
|||||||
@@ -7,11 +7,10 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import { ensureBuilt } from "../server.js";
|
import { ensureBuilt } from "../server.js";
|
||||||
|
|
||||||
interface RuntimeSiteCase {
|
interface SiteCase {
|
||||||
name: string;
|
name: string;
|
||||||
dir: string;
|
dir: string;
|
||||||
port: number;
|
port: number;
|
||||||
mode: "runtime";
|
|
||||||
startupTimeoutMs: number;
|
startupTimeoutMs: number;
|
||||||
waitPath?: string;
|
waitPath?: string;
|
||||||
setupPath?: string | null;
|
setupPath?: string | null;
|
||||||
@@ -20,121 +19,61 @@ interface RuntimeSiteCase {
|
|||||||
requireDoctype?: boolean;
|
requireDoctype?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TypecheckSiteCase {
|
|
||||||
name: string;
|
|
||||||
dir: string;
|
|
||||||
mode: "typecheck";
|
|
||||||
}
|
|
||||||
|
|
||||||
type SiteCase = RuntimeSiteCase | TypecheckSiteCase;
|
|
||||||
|
|
||||||
const WORKSPACE_ROOT = resolve(import.meta.dirname, "../../../../..");
|
const WORKSPACE_ROOT = resolve(import.meta.dirname, "../../../../..");
|
||||||
const execAsync = promisify(execFile);
|
const execAsync = promisify(execFile);
|
||||||
|
|
||||||
const SITE_MATRIX: SiteCase[] = [
|
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",
|
name: "demos/playground",
|
||||||
dir: resolve(WORKSPACE_ROOT, "demos/playground"),
|
dir: resolve(WORKSPACE_ROOT, "demos/playground"),
|
||||||
port: 4603,
|
port: 4603,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 120_000,
|
startupTimeoutMs: 120_000,
|
||||||
waitPath: "/playground",
|
waitPath: "/playground",
|
||||||
frontendPath: "/playground",
|
frontendPath: "/playground",
|
||||||
requireDoctype: false,
|
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
|
// Templates
|
||||||
{
|
{
|
||||||
name: "templates/blank",
|
name: "templates/blank",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/blank"),
|
dir: resolve(WORKSPACE_ROOT, "templates/blank"),
|
||||||
port: 4611,
|
port: 4611,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 60_000,
|
startupTimeoutMs: 60_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "templates/blog",
|
name: "templates/blog",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/blog"),
|
dir: resolve(WORKSPACE_ROOT, "templates/blog"),
|
||||||
port: 4612,
|
port: 4612,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 60_000,
|
startupTimeoutMs: 60_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "templates/blog-cloudflare",
|
name: "templates/blog-cloudflare",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/blog-cloudflare"),
|
dir: resolve(WORKSPACE_ROOT, "templates/blog-cloudflare"),
|
||||||
port: 4613,
|
port: 4613,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 120_000,
|
startupTimeoutMs: 120_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "templates/marketing",
|
name: "templates/marketing",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/marketing"),
|
dir: resolve(WORKSPACE_ROOT, "templates/marketing"),
|
||||||
port: 4614,
|
port: 4614,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 90_000,
|
startupTimeoutMs: 90_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "templates/marketing-cloudflare",
|
name: "templates/marketing-cloudflare",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/marketing-cloudflare"),
|
dir: resolve(WORKSPACE_ROOT, "templates/marketing-cloudflare"),
|
||||||
port: 4615,
|
port: 4615,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 120_000,
|
startupTimeoutMs: 120_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "templates/portfolio",
|
name: "templates/portfolio",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/portfolio"),
|
dir: resolve(WORKSPACE_ROOT, "templates/portfolio"),
|
||||||
port: 4616,
|
port: 4616,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 90_000,
|
startupTimeoutMs: 90_000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "templates/portfolio-cloudflare",
|
name: "templates/portfolio-cloudflare",
|
||||||
dir: resolve(WORKSPACE_ROOT, "templates/portfolio-cloudflare"),
|
dir: resolve(WORKSPACE_ROOT, "templates/portfolio-cloudflare"),
|
||||||
port: 4617,
|
port: 4617,
|
||||||
mode: "runtime",
|
|
||||||
startupTimeoutMs: 120_000,
|
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
|
// Build verification — runs a single recursive `pnpm build` across templates
|
||||||
// and templates in parallel, then verifies each site produced output.
|
// and the playground demo in parallel.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
describe("Site build verification", () => {
|
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();
|
await ensureBuilt();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await execAsync(
|
await execAsync(
|
||||||
"pnpm",
|
"pnpm",
|
||||||
["run", "--recursive", "--filter", "{./demos/*}", "--filter", "{./templates/*}", "build"],
|
[
|
||||||
|
"run",
|
||||||
|
"--recursive",
|
||||||
|
"--filter",
|
||||||
|
"{./templates/*}",
|
||||||
|
"--filter",
|
||||||
|
"@emdash-cms/playground",
|
||||||
|
"build",
|
||||||
|
],
|
||||||
{
|
{
|
||||||
cwd: WORKSPACE_ROOT,
|
cwd: WORKSPACE_ROOT,
|
||||||
timeout: 240_000,
|
timeout: 240_000,
|
||||||
@@ -221,16 +168,6 @@ describe("Site build verification", () => {
|
|||||||
|
|
||||||
describe.sequential("Site runtime verification", () => {
|
describe.sequential("Site runtime verification", () => {
|
||||||
for (const site of SITE_MATRIX) {
|
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 waitPath = site.waitPath ?? "/_emdash/admin/";
|
||||||
const setupPath = site.setupPath ?? "/_emdash/api/setup/dev-bypass?redirect=/";
|
const setupPath = site.setupPath ?? "/_emdash/api/setup/dev-bypass?redirect=/";
|
||||||
const frontendPath = site.frontendPath ?? "/";
|
const frontendPath = site.frontendPath ?? "/";
|
||||||
|
|||||||
15
templates/marketing/emdash-env.d.ts
vendored
15
templates/marketing/emdash-env.d.ts
vendored
@@ -17,23 +17,8 @@ export interface Page {
|
|||||||
bylines?: ContentBylineCredit[];
|
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" {
|
declare module "emdash" {
|
||||||
interface EmDashCollections {
|
interface EmDashCollections {
|
||||||
pages: Page;
|
pages: Page;
|
||||||
posts: Post;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
templates/portfolio-cloudflare/emdash-env.d.ts
vendored
15
templates/portfolio-cloudflare/emdash-env.d.ts
vendored
@@ -17,20 +17,6 @@ export interface Page {
|
|||||||
bylines?: ContentBylineCredit[];
|
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 {
|
export interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
@@ -52,7 +38,6 @@ export interface Project {
|
|||||||
declare module "emdash" {
|
declare module "emdash" {
|
||||||
interface EmDashCollections {
|
interface EmDashCollections {
|
||||||
pages: Page;
|
pages: Page;
|
||||||
posts: Post;
|
|
||||||
projects: Project;
|
projects: Project;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
15
templates/portfolio/emdash-env.d.ts
vendored
15
templates/portfolio/emdash-env.d.ts
vendored
@@ -17,20 +17,6 @@ export interface Page {
|
|||||||
bylines?: ContentBylineCredit[];
|
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 {
|
export interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
slug: string | null;
|
slug: string | null;
|
||||||
@@ -52,7 +38,6 @@ export interface Project {
|
|||||||
declare module "emdash" {
|
declare module "emdash" {
|
||||||
interface EmDashCollections {
|
interface EmDashCollections {
|
||||||
pages: Page;
|
pages: Page;
|
||||||
posts: Post;
|
|
||||||
projects: Project;
|
projects: Project;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user