import type { Kysely } from "kysely"; import { sql } from "kysely"; import { ulid } from "ulidx"; import { describe, it, expect, beforeEach, afterEach } from "vitest"; import type { Database } from "../../../src/database/types.js"; import { getMenuWithDb } from "../../../src/menus/index.js"; import { SchemaRegistry } from "../../../src/schema/registry.js"; import { applySeed } from "../../../src/seed/apply.js"; import type { SeedFile } from "../../../src/seed/types.js"; import { setupTestDatabase, teardownTestDatabase } from "../../utils/test-db.js"; describe("urlPattern", () => { let db: Kysely; let registry: SchemaRegistry; beforeEach(async () => { db = await setupTestDatabase(); registry = new SchemaRegistry(db); }); afterEach(async () => { await teardownTestDatabase(db); }); describe("schema registry", () => { it("should store urlPattern on create", async () => { const collection = await registry.createCollection({ slug: "pages", label: "Pages", urlPattern: "/{slug}", }); expect(collection.urlPattern).toBe("/{slug}"); }); it("should default urlPattern to undefined when not provided", async () => { const collection = await registry.createCollection({ slug: "posts", label: "Posts", }); expect(collection.urlPattern).toBeUndefined(); }); it("should persist urlPattern in the database", async () => { await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); const row = await db .selectFrom("_emdash_collections") .select("url_pattern") .where("slug", "=", "posts") .executeTakeFirst(); expect(row?.url_pattern).toBe("/blog/{slug}"); }); it("should return urlPattern from getCollection", async () => { await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); const collection = await registry.getCollection("posts"); expect(collection?.urlPattern).toBe("/blog/{slug}"); }); it("should update urlPattern", async () => { await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); const updated = await registry.updateCollection("posts", { urlPattern: "/articles/{slug}", }); expect(updated.urlPattern).toBe("/articles/{slug}"); }); it("should clear urlPattern when set to undefined", async () => { await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); // Setting to undefined in the update should clear it const updated = await registry.updateCollection("posts", { urlPattern: undefined, }); // urlPattern was not in the update input, so it should keep the old value expect(updated.urlPattern).toBe("/blog/{slug}"); }); it("should clear urlPattern when explicitly set to null-ish", async () => { await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); // Explicitly passing null (via the update interface) should clear it const updated = await registry.updateCollection("posts", { urlPattern: "" as any, // empty string to clear }); // Empty string is falsy but still a defined value expect(updated.urlPattern).toBe(""); }); it("should include urlPattern in listCollections", async () => { await registry.createCollection({ slug: "pages", label: "Pages", urlPattern: "/{slug}", }); await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); const collections = await registry.listCollections(); const patterns = collections.map((c) => ({ slug: c.slug, urlPattern: c.urlPattern })); expect(patterns).toEqual([ { slug: "pages", urlPattern: "/{slug}" }, { slug: "posts", urlPattern: "/blog/{slug}" }, ]); }); }); describe("menu URL resolution", () => { it("should use urlPattern for content URL resolution", async () => { // Create a pages collection with urlPattern await registry.createCollection({ slug: "pages", label: "Pages", urlPattern: "/{slug}", }); await registry.createField("pages", { slug: "title", label: "Title", type: "string", }); // Insert a page const pageId = ulid(); await sql` INSERT INTO ec_pages (id, slug, status) VALUES (${pageId}, ${"about"}, ${"published"}) `.execute(db); // Create a menu with a page reference const menuId = ulid(); await db .insertInto("_emdash_menus") .values({ id: menuId, name: "primary", label: "Primary" }) .execute(); await db .insertInto("_emdash_menu_items") .values({ id: ulid(), menu_id: menuId, sort_order: 0, type: "page", reference_collection: "pages", reference_id: pageId, label: "About", }) .execute(); const menu = await getMenuWithDb("primary", db); expect(menu).not.toBeNull(); expect(menu!.items).toHaveLength(1); expect(menu!.items[0].url).toBe("/about"); }); it("should fall back to /{collection}/{slug} when no urlPattern", async () => { // Create a posts collection without urlPattern await registry.createCollection({ slug: "posts", label: "Posts", }); await registry.createField("posts", { slug: "title", label: "Title", type: "string", }); // Insert a post const postId = ulid(); await sql` INSERT INTO ec_posts (id, slug, status) VALUES (${postId}, ${"hello"}, ${"published"}) `.execute(db); // Create a menu with a post reference const menuId = ulid(); await db .insertInto("_emdash_menus") .values({ id: menuId, name: "primary", label: "Primary" }) .execute(); await db .insertInto("_emdash_menu_items") .values({ id: ulid(), menu_id: menuId, sort_order: 0, type: "post", reference_collection: "posts", reference_id: postId, label: "Hello", }) .execute(); const menu = await getMenuWithDb("primary", db); expect(menu!.items[0].url).toBe("/posts/hello"); }); it("should interpolate {slug} in urlPattern", async () => { await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); await registry.createField("posts", { slug: "title", label: "Title", type: "string", }); const postId = ulid(); await sql` INSERT INTO ec_posts (id, slug, status) VALUES (${postId}, ${"my-post"}, ${"published"}) `.execute(db); const menuId = ulid(); await db .insertInto("_emdash_menus") .values({ id: menuId, name: "primary", label: "Primary" }) .execute(); await db .insertInto("_emdash_menu_items") .values({ id: ulid(), menu_id: menuId, sort_order: 0, type: "post", reference_collection: "posts", reference_id: postId, label: "My Post", }) .execute(); const menu = await getMenuWithDb("primary", db); expect(menu!.items[0].url).toBe("/blog/my-post"); }); it("should handle multiple collections with different patterns", async () => { // Pages: /{slug} await registry.createCollection({ slug: "pages", label: "Pages", urlPattern: "/{slug}", }); await registry.createField("pages", { slug: "title", label: "Title", type: "string", }); // Posts: /blog/{slug} await registry.createCollection({ slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", }); await registry.createField("posts", { slug: "title", label: "Title", type: "string", }); const pageId = ulid(); const postId = ulid(); await sql`INSERT INTO ec_pages (id, slug, status) VALUES (${pageId}, ${"about"}, ${"published"})`.execute( db, ); await sql`INSERT INTO ec_posts (id, slug, status) VALUES (${postId}, ${"hello"}, ${"published"})`.execute( db, ); const menuId = ulid(); await db .insertInto("_emdash_menus") .values({ id: menuId, name: "nav", label: "Nav" }) .execute(); await db .insertInto("_emdash_menu_items") .values([ { id: ulid(), menu_id: menuId, sort_order: 0, type: "page", reference_collection: "pages", reference_id: pageId, label: "About", }, { id: ulid(), menu_id: menuId, sort_order: 1, type: "post", reference_collection: "posts", reference_id: postId, label: "Hello", }, ]) .execute(); const menu = await getMenuWithDb("nav", db); expect(menu!.items[0].url).toBe("/about"); expect(menu!.items[1].url).toBe("/blog/hello"); }); }); describe("seed", () => { it("should persist urlPattern from seed", async () => { const seed: SeedFile = { version: "1", collections: [ { slug: "pages", label: "Pages", urlPattern: "/{slug}", fields: [{ slug: "title", label: "Title", type: "string" }], }, { slug: "posts", label: "Posts", urlPattern: "/blog/{slug}", fields: [{ slug: "title", label: "Title", type: "string" }], }, ], }; await applySeed(db, seed); const pages = await registry.getCollection("pages"); const posts = await registry.getCollection("posts"); expect(pages?.urlPattern).toBe("/{slug}"); expect(posts?.urlPattern).toBe("/blog/{slug}"); }); it("should handle seed without urlPattern", async () => { const seed: SeedFile = { version: "1", collections: [ { slug: "posts", label: "Posts", fields: [{ slug: "title", label: "Title", type: "string" }], }, ], }; await applySeed(db, seed); const posts = await registry.getCollection("posts"); expect(posts?.urlPattern).toBeUndefined(); }); }); });