Fixes: 1. media.ts: wrap placeholder generation in try-catch 2. toolbar.ts: check r.ok, display error message in popover
1047 lines
26 KiB
TypeScript
1047 lines
26 KiB
TypeScript
import type { Kysely } from "kysely";
|
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
|
|
import { BylineRepository } from "../../../src/database/repositories/byline.js";
|
|
import { ContentRepository } from "../../../src/database/repositories/content.js";
|
|
import { RedirectRepository } from "../../../src/database/repositories/redirect.js";
|
|
import { TaxonomyRepository } from "../../../src/database/repositories/taxonomy.js";
|
|
import type { Database } from "../../../src/database/types.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("applySeed", () => {
|
|
let db: Kysely<Database>;
|
|
|
|
beforeEach(async () => {
|
|
db = await setupTestDatabase();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await teardownTestDatabase(db);
|
|
});
|
|
|
|
describe("validation", () => {
|
|
it("should reject invalid seed file", async () => {
|
|
const invalidSeed = { version: "99" } as SeedFile;
|
|
|
|
await expect(applySeed(db, invalidSeed)).rejects.toThrow("Invalid seed file");
|
|
});
|
|
|
|
it("should accept minimal valid seed", async () => {
|
|
const seed: SeedFile = { version: "1" };
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.collections.created).toBe(0);
|
|
expect(result.settings.applied).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("settings", () => {
|
|
it("should apply site settings", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
settings: {
|
|
siteTitle: "Test Site",
|
|
tagline: "A test site",
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.settings.applied).toBe(2);
|
|
|
|
// Verify settings were saved
|
|
const row = await db
|
|
.selectFrom("options")
|
|
.selectAll()
|
|
.where("name", "=", "site:siteTitle")
|
|
.executeTakeFirst();
|
|
|
|
expect(row?.value).toBe('"Test Site"');
|
|
});
|
|
});
|
|
|
|
describe("collections", () => {
|
|
it("should create collections and fields", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
collections: [
|
|
{
|
|
slug: "posts",
|
|
label: "Posts",
|
|
labelSingular: "Post",
|
|
fields: [
|
|
{ slug: "title", label: "Title", type: "string", required: true },
|
|
{ slug: "content", label: "Content", type: "portableText" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.collections.created).toBe(1);
|
|
expect(result.fields.created).toBe(2);
|
|
|
|
// Verify collection exists
|
|
const registry = new SchemaRegistry(db);
|
|
const collection = await registry.getCollection("posts");
|
|
expect(collection).not.toBeNull();
|
|
expect(collection?.label).toBe("Posts");
|
|
});
|
|
|
|
it("should skip existing collections", async () => {
|
|
// Create collection first
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({
|
|
slug: "posts",
|
|
label: "Existing Posts",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
collections: [
|
|
{
|
|
slug: "posts",
|
|
label: "New Posts",
|
|
fields: [{ slug: "title", label: "Title", type: "string" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.collections.created).toBe(0);
|
|
expect(result.collections.skipped).toBe(1);
|
|
expect(result.fields.skipped).toBe(1);
|
|
|
|
// Original label should be preserved
|
|
const collection = await registry.getCollection("posts");
|
|
expect(collection?.label).toBe("Existing Posts");
|
|
});
|
|
|
|
it("should create multiple collections", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
collections: [
|
|
{ slug: "posts", label: "Posts", fields: [] },
|
|
{ slug: "pages", label: "Pages", fields: [] },
|
|
{ slug: "products", label: "Products", fields: [] },
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.collections.created).toBe(3);
|
|
});
|
|
});
|
|
|
|
describe("taxonomies", () => {
|
|
it("should create taxonomy definitions", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
taxonomies: [
|
|
{
|
|
name: "topics",
|
|
label: "Topics",
|
|
hierarchical: true,
|
|
collections: ["posts"],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.taxonomies.created).toBe(1);
|
|
|
|
// Verify taxonomy exists
|
|
const row = await db
|
|
.selectFrom("_emdash_taxonomy_defs")
|
|
.selectAll()
|
|
.where("name", "=", "topics")
|
|
.executeTakeFirst();
|
|
|
|
expect(row).not.toBeNull();
|
|
expect(row?.label).toBe("Topics");
|
|
expect(row?.hierarchical).toBe(1);
|
|
});
|
|
|
|
it("should create flat taxonomy terms", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
taxonomies: [
|
|
{
|
|
name: "tags",
|
|
label: "Tags",
|
|
hierarchical: false,
|
|
collections: ["posts"],
|
|
terms: [
|
|
{ slug: "javascript", label: "JavaScript" },
|
|
{ slug: "typescript", label: "TypeScript" },
|
|
{ slug: "rust", label: "Rust" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.taxonomies.created).toBe(1);
|
|
expect(result.taxonomies.terms).toBe(3);
|
|
});
|
|
|
|
it("should create hierarchical taxonomy terms with parents", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
taxonomies: [
|
|
{
|
|
name: "topics",
|
|
label: "Topics",
|
|
hierarchical: true,
|
|
collections: ["posts"],
|
|
terms: [
|
|
{ slug: "tech", label: "Technology" },
|
|
{ slug: "web", label: "Web Development", parent: "tech" },
|
|
{ slug: "mobile", label: "Mobile Development", parent: "tech" },
|
|
{ slug: "react", label: "React", parent: "web" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.taxonomies.terms).toBe(4);
|
|
|
|
// Verify parent-child relationship
|
|
const termRepo = new TaxonomyRepository(db);
|
|
const webTerm = await termRepo.findBySlug("topics", "web");
|
|
const techTerm = await termRepo.findBySlug("topics", "tech");
|
|
|
|
expect(webTerm?.parentId).toBe(techTerm?.id);
|
|
});
|
|
|
|
it("should skip existing terms", async () => {
|
|
// Create taxonomy and term first
|
|
await db
|
|
.insertInto("_emdash_taxonomy_defs")
|
|
.values({
|
|
id: "def-1",
|
|
name: "tags",
|
|
label: "Tags",
|
|
hierarchical: 0,
|
|
collections: JSON.stringify(["posts"]),
|
|
})
|
|
.execute();
|
|
|
|
const termRepo = new TaxonomyRepository(db);
|
|
await termRepo.create({
|
|
name: "tags",
|
|
slug: "javascript",
|
|
label: "Existing JS",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
taxonomies: [
|
|
{
|
|
name: "tags",
|
|
label: "Tags",
|
|
hierarchical: false,
|
|
collections: ["posts"],
|
|
terms: [
|
|
{ slug: "javascript", label: "New JavaScript" },
|
|
{ slug: "typescript", label: "TypeScript" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
// Definition already exists, so not created
|
|
expect(result.taxonomies.created).toBe(0);
|
|
// Only typescript is new
|
|
expect(result.taxonomies.terms).toBe(1);
|
|
|
|
// Original label should be preserved
|
|
const term = await termRepo.findBySlug("tags", "javascript");
|
|
expect(term?.label).toBe("Existing JS");
|
|
});
|
|
});
|
|
|
|
describe("menus", () => {
|
|
it("should create menus with items", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
menus: [
|
|
{
|
|
name: "main",
|
|
label: "Main Navigation",
|
|
items: [
|
|
{ type: "custom", label: "Home", url: "/" },
|
|
{ type: "custom", label: "About", url: "/about" },
|
|
{ type: "custom", label: "Contact", url: "/contact" },
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.menus.created).toBe(1);
|
|
expect(result.menus.items).toBe(3);
|
|
|
|
// Verify menu exists
|
|
const menu = await db
|
|
.selectFrom("_emdash_menus")
|
|
.selectAll()
|
|
.where("name", "=", "main")
|
|
.executeTakeFirst();
|
|
|
|
expect(menu).not.toBeNull();
|
|
expect(menu?.label).toBe("Main Navigation");
|
|
});
|
|
|
|
it("should create nested menu items", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
menus: [
|
|
{
|
|
name: "main",
|
|
label: "Main",
|
|
items: [
|
|
{
|
|
type: "custom",
|
|
label: "Products",
|
|
url: "/products",
|
|
children: [
|
|
{
|
|
type: "custom",
|
|
label: "Software",
|
|
url: "/products/software",
|
|
},
|
|
{
|
|
type: "custom",
|
|
label: "Hardware",
|
|
url: "/products/hardware",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.menus.items).toBe(3); // 1 parent + 2 children
|
|
|
|
// Verify parent-child relationship
|
|
const items = await db.selectFrom("_emdash_menu_items").selectAll().execute();
|
|
|
|
const parent = items.find((i) => i.label === "Products");
|
|
const child = items.find((i) => i.label === "Software");
|
|
|
|
expect(child?.parent_id).toBe(parent?.id);
|
|
});
|
|
|
|
it("should replace items in existing menu", async () => {
|
|
// Create menu with items first
|
|
await db
|
|
.insertInto("_emdash_menus")
|
|
.values({
|
|
id: "menu-1",
|
|
name: "main",
|
|
label: "Main",
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
})
|
|
.execute();
|
|
|
|
await db
|
|
.insertInto("_emdash_menu_items")
|
|
.values({
|
|
id: "item-1",
|
|
menu_id: "menu-1",
|
|
parent_id: null,
|
|
sort_order: 0,
|
|
type: "custom",
|
|
label: "Old Item",
|
|
custom_url: "/old",
|
|
created_at: new Date().toISOString(),
|
|
})
|
|
.execute();
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
menus: [
|
|
{
|
|
name: "main",
|
|
label: "Main",
|
|
items: [{ type: "custom", label: "New Item", url: "/new" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
// Menu not created (existed), but items are replaced
|
|
expect(result.menus.created).toBe(0);
|
|
expect(result.menus.items).toBe(1);
|
|
|
|
// Old item should be gone
|
|
const items = await db
|
|
.selectFrom("_emdash_menu_items")
|
|
.selectAll()
|
|
.where("menu_id", "=", "menu-1")
|
|
.execute();
|
|
|
|
expect(items).toHaveLength(1);
|
|
expect(items[0].label).toBe("New Item");
|
|
});
|
|
});
|
|
|
|
describe("widget areas", () => {
|
|
it("should create widget areas with widgets", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
widgetAreas: [
|
|
{
|
|
name: "sidebar",
|
|
label: "Sidebar",
|
|
description: "The main sidebar",
|
|
widgets: [
|
|
{
|
|
type: "content",
|
|
title: "About",
|
|
content: [{ _type: "block", children: [{ text: "About us" }] }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.widgetAreas.created).toBe(1);
|
|
expect(result.widgetAreas.widgets).toBe(1);
|
|
|
|
// Verify area exists
|
|
const area = await db
|
|
.selectFrom("_emdash_widget_areas")
|
|
.selectAll()
|
|
.where("name", "=", "sidebar")
|
|
.executeTakeFirst();
|
|
|
|
expect(area).not.toBeNull();
|
|
expect(area?.description).toBe("The main sidebar");
|
|
});
|
|
|
|
it("should create menu widgets", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
widgetAreas: [
|
|
{
|
|
name: "footer",
|
|
label: "Footer",
|
|
widgets: [{ type: "menu", title: "Footer Nav", menuName: "footer-menu" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.widgetAreas.widgets).toBe(1);
|
|
|
|
const widget = await db.selectFrom("_emdash_widgets").selectAll().executeTakeFirst();
|
|
|
|
expect(widget?.type).toBe("menu");
|
|
expect(widget?.menu_name).toBe("footer-menu");
|
|
});
|
|
|
|
it("should create component widgets", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
widgetAreas: [
|
|
{
|
|
name: "sidebar",
|
|
label: "Sidebar",
|
|
widgets: [
|
|
{
|
|
type: "component",
|
|
componentId: "recent-posts",
|
|
props: { count: 5, showDate: true },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.widgetAreas.widgets).toBe(1);
|
|
|
|
const widget = await db.selectFrom("_emdash_widgets").selectAll().executeTakeFirst();
|
|
|
|
expect(widget?.type).toBe("component");
|
|
expect(widget?.component_id).toBe("recent-posts");
|
|
expect(JSON.parse(widget?.component_props ?? "{}")).toEqual({
|
|
count: 5,
|
|
showDate: true,
|
|
});
|
|
});
|
|
|
|
it("should replace widgets in existing area", async () => {
|
|
// Create area with widget first
|
|
await db
|
|
.insertInto("_emdash_widget_areas")
|
|
.values({
|
|
id: "area-1",
|
|
name: "sidebar",
|
|
label: "Sidebar",
|
|
description: null,
|
|
})
|
|
.execute();
|
|
|
|
await db
|
|
.insertInto("_emdash_widgets")
|
|
.values({
|
|
id: "widget-1",
|
|
area_id: "area-1",
|
|
sort_order: 0,
|
|
type: "content",
|
|
title: "Old Widget",
|
|
content: null,
|
|
menu_name: null,
|
|
component_id: null,
|
|
component_props: null,
|
|
})
|
|
.execute();
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
widgetAreas: [
|
|
{
|
|
name: "sidebar",
|
|
label: "Sidebar",
|
|
widgets: [{ type: "content", title: "New Widget" }],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.widgetAreas.created).toBe(0);
|
|
expect(result.widgetAreas.widgets).toBe(1);
|
|
|
|
// Old widget should be gone
|
|
const widgets = await db
|
|
.selectFrom("_emdash_widgets")
|
|
.selectAll()
|
|
.where("area_id", "=", "area-1")
|
|
.execute();
|
|
|
|
expect(widgets).toHaveLength(1);
|
|
expect(widgets[0]!.title).toBe("New Widget");
|
|
});
|
|
});
|
|
|
|
describe("redirects", () => {
|
|
it("should create redirects", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
redirects: [
|
|
{ source: "/old-about", destination: "/about" },
|
|
{
|
|
source: "/temp",
|
|
destination: "/new-temp",
|
|
type: 302,
|
|
enabled: false,
|
|
groupName: "migration",
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.redirects.created).toBe(2);
|
|
expect(result.redirects.skipped).toBe(0);
|
|
|
|
const redirects = await db
|
|
.selectFrom("_emdash_redirects")
|
|
.selectAll()
|
|
.orderBy("source", "asc")
|
|
.execute();
|
|
|
|
expect(redirects).toHaveLength(2);
|
|
expect(redirects[0]!.source).toBe("/old-about");
|
|
expect(redirects[0]!.destination).toBe("/about");
|
|
expect(redirects[0]!.type).toBe(301);
|
|
expect(redirects[0]!.enabled).toBe(1);
|
|
expect(redirects[1]!.source).toBe("/temp");
|
|
expect(redirects[1]!.type).toBe(302);
|
|
expect(redirects[1]!.enabled).toBe(0);
|
|
expect(redirects[1]!.group_name).toBe("migration");
|
|
});
|
|
|
|
it("should skip redirects when source already exists", async () => {
|
|
const redirectRepo = new RedirectRepository(db);
|
|
await redirectRepo.create({
|
|
source: "/old-about",
|
|
destination: "/existing-about",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
redirects: [
|
|
{ source: "/old-about", destination: "/about" },
|
|
{ source: "/old-contact", destination: "/contact" },
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.redirects.created).toBe(1);
|
|
expect(result.redirects.skipped).toBe(1);
|
|
|
|
const existing = await redirectRepo.findBySource("/old-about");
|
|
expect(existing?.destination).toBe("/existing-about");
|
|
|
|
const created = await redirectRepo.findBySource("/old-contact");
|
|
expect(created?.destination).toBe("/contact");
|
|
});
|
|
});
|
|
|
|
describe("content", () => {
|
|
it("should create bylines and assign ordered credits to content", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
bylines: [
|
|
{ id: "editorial", slug: "editorial", displayName: "Editorial" },
|
|
{ id: "guest", slug: "guest-writer", displayName: "Guest Writer", isGuest: true },
|
|
],
|
|
content: {
|
|
posts: [
|
|
{
|
|
id: "post-1",
|
|
slug: "hello",
|
|
data: { title: "Hello World" },
|
|
bylines: [{ byline: "editorial" }, { byline: "guest", roleLabel: "Guest essay" }],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed, { includeContent: true });
|
|
|
|
expect(result.bylines.created).toBe(2);
|
|
expect(result.content.created).toBe(1);
|
|
|
|
const contentRepo = new ContentRepository(db);
|
|
const bylineRepo = new BylineRepository(db);
|
|
const entry = await contentRepo.findBySlug("posts", "hello");
|
|
expect(entry).not.toBeNull();
|
|
|
|
const credits = await bylineRepo.getContentBylines("posts", entry!.id);
|
|
expect(credits).toHaveLength(2);
|
|
expect(credits[0]?.byline.slug).toBe("editorial");
|
|
expect(credits[1]?.byline.slug).toBe("guest-writer");
|
|
expect(credits[1]?.roleLabel).toBe("Guest essay");
|
|
expect(entry?.primaryBylineId).toBe(credits[0]?.byline.id);
|
|
});
|
|
|
|
it("should not create content by default", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
content: {
|
|
posts: [{ id: "post-1", slug: "hello", data: { title: "Hello World" } }],
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.content.created).toBe(0);
|
|
|
|
const contentRepo = new ContentRepository(db);
|
|
const entries = await contentRepo.findMany("posts", {});
|
|
expect(entries.items).toHaveLength(0);
|
|
});
|
|
|
|
it("should create content when includeContent is true", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
content: {
|
|
posts: [
|
|
{ id: "post-1", slug: "hello", data: { title: "Hello World" } },
|
|
{ id: "post-2", slug: "goodbye", data: { title: "Goodbye World" } },
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed, { includeContent: true });
|
|
|
|
expect(result.content.created).toBe(2);
|
|
|
|
const contentRepo = new ContentRepository(db);
|
|
const entry = await contentRepo.findBySlug("posts", "hello");
|
|
expect(entry?.data.title).toBe("Hello World");
|
|
});
|
|
|
|
it("should skip existing content entries", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
|
|
const contentRepo = new ContentRepository(db);
|
|
await contentRepo.create({
|
|
type: "posts",
|
|
slug: "hello",
|
|
data: { title: "Existing" },
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
content: {
|
|
posts: [
|
|
{ id: "post-1", slug: "hello", data: { title: "New Title" } },
|
|
{ id: "post-2", slug: "world", data: { title: "World" } },
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed, { includeContent: true });
|
|
|
|
expect(result.content.created).toBe(1);
|
|
expect(result.content.skipped).toBe(1);
|
|
|
|
// Original should be preserved
|
|
const entry = await contentRepo.findBySlug("posts", "hello");
|
|
expect(entry?.data.title).toBe("Existing");
|
|
});
|
|
|
|
it("should resolve $ref: references between content", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
await registry.createField("posts", {
|
|
slug: "related_post",
|
|
label: "Related Post",
|
|
type: "reference",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
content: {
|
|
posts: [
|
|
{ id: "post-1", slug: "first", data: { title: "First" } },
|
|
{
|
|
id: "post-2",
|
|
slug: "second",
|
|
data: { title: "Second", related_post: "$ref:post-1" },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed, { includeContent: true });
|
|
|
|
expect(result.content.created).toBe(2);
|
|
|
|
const contentRepo = new ContentRepository(db);
|
|
const first = await contentRepo.findBySlug("posts", "first");
|
|
const second = await contentRepo.findBySlug("posts", "second");
|
|
|
|
// The reference should be resolved to the real ID
|
|
expect(second?.data.related_post).toBe(first?.id);
|
|
});
|
|
|
|
it("should assign taxonomy terms to content", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
|
|
// Create taxonomy
|
|
await db
|
|
.insertInto("_emdash_taxonomy_defs")
|
|
.values({
|
|
id: "def-1",
|
|
name: "tags",
|
|
label: "Tags",
|
|
hierarchical: 0,
|
|
collections: JSON.stringify(["posts"]),
|
|
})
|
|
.execute();
|
|
|
|
const termRepo = new TaxonomyRepository(db);
|
|
await termRepo.create({ name: "tags", slug: "javascript", label: "JS" });
|
|
await termRepo.create({ name: "tags", slug: "typescript", label: "TS" });
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
content: {
|
|
posts: [
|
|
{
|
|
id: "post-1",
|
|
slug: "hello",
|
|
data: { title: "Hello" },
|
|
taxonomies: { tags: ["javascript", "typescript"] },
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
const result = await applySeed(db, seed, { includeContent: true });
|
|
|
|
expect(result.content.created).toBe(1);
|
|
|
|
// Check taxonomy assignments
|
|
const contentRepo = new ContentRepository(db);
|
|
const entry = await contentRepo.findBySlug("posts", "hello");
|
|
|
|
const assignments = await db
|
|
.selectFrom("content_taxonomies")
|
|
.selectAll()
|
|
.where("entry_id", "=", entry!.id)
|
|
.execute();
|
|
|
|
expect(assignments).toHaveLength(2);
|
|
});
|
|
});
|
|
|
|
describe("apply order", () => {
|
|
it("should create content before menus so refs resolve", async () => {
|
|
const registry = new SchemaRegistry(db);
|
|
await registry.createCollection({ slug: "pages", label: "Pages" });
|
|
await registry.createField("pages", {
|
|
slug: "title",
|
|
label: "Title",
|
|
type: "string",
|
|
});
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
content: {
|
|
pages: [{ id: "about-page", slug: "about", data: { title: "About Us" } }],
|
|
},
|
|
menus: [
|
|
{
|
|
name: "main",
|
|
label: "Main",
|
|
items: [
|
|
{
|
|
type: "page",
|
|
label: "About",
|
|
ref: "about-page",
|
|
collection: "pages",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed, { includeContent: true });
|
|
|
|
expect(result.content.created).toBe(1);
|
|
expect(result.menus.items).toBe(1);
|
|
|
|
// Menu item should reference the content
|
|
const contentRepo = new ContentRepository(db);
|
|
const aboutPage = await contentRepo.findBySlug("pages", "about");
|
|
|
|
const menuItem = await db.selectFrom("_emdash_menu_items").selectAll().executeTakeFirst();
|
|
|
|
expect(menuItem?.reference_id).toBe(aboutPage?.id);
|
|
});
|
|
});
|
|
|
|
describe("sections", () => {
|
|
it("should create sections", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
sections: [
|
|
{
|
|
slug: "hero-centered",
|
|
title: "Centered Hero",
|
|
description: "A centered hero section",
|
|
keywords: ["hero", "banner"],
|
|
content: [
|
|
{
|
|
_type: "block",
|
|
style: "h1",
|
|
children: [{ _type: "span", text: "Welcome" }],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.sections.created).toBe(1);
|
|
expect(result.sections.skipped).toBe(0);
|
|
|
|
// Verify section exists
|
|
const section = await db
|
|
.selectFrom("_emdash_sections")
|
|
.selectAll()
|
|
.where("slug", "=", "hero-centered")
|
|
.executeTakeFirst();
|
|
|
|
expect(section).not.toBeNull();
|
|
expect(section?.title).toBe("Centered Hero");
|
|
expect(section?.description).toBe("A centered hero section");
|
|
expect(section?.source).toBe("theme");
|
|
expect(JSON.parse(section?.keywords ?? "[]")).toEqual(["hero", "banner"]);
|
|
});
|
|
|
|
it("should skip existing sections", async () => {
|
|
// Create section first
|
|
await db
|
|
.insertInto("_emdash_sections")
|
|
.values({
|
|
id: "sec-1",
|
|
slug: "hero-centered",
|
|
title: "Existing Hero",
|
|
description: null,
|
|
keywords: null,
|
|
content: "[]",
|
|
preview_media_id: null,
|
|
source: "theme",
|
|
theme_id: "hero-centered",
|
|
created_at: new Date().toISOString(),
|
|
updated_at: new Date().toISOString(),
|
|
})
|
|
.execute();
|
|
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
sections: [
|
|
{
|
|
slug: "hero-centered",
|
|
title: "New Hero",
|
|
content: [],
|
|
},
|
|
{
|
|
slug: "cta-newsletter",
|
|
title: "Newsletter CTA",
|
|
content: [],
|
|
},
|
|
],
|
|
};
|
|
|
|
const result = await applySeed(db, seed);
|
|
|
|
expect(result.sections.created).toBe(1);
|
|
expect(result.sections.skipped).toBe(1);
|
|
|
|
// Original title should be preserved
|
|
const section = await db
|
|
.selectFrom("_emdash_sections")
|
|
.selectAll()
|
|
.where("slug", "=", "hero-centered")
|
|
.executeTakeFirst();
|
|
|
|
expect(section?.title).toBe("Existing Hero");
|
|
});
|
|
});
|
|
|
|
describe("idempotency", () => {
|
|
it("should be safe to run multiple times", async () => {
|
|
const seed: SeedFile = {
|
|
version: "1",
|
|
settings: { siteTitle: "Test Site" },
|
|
collections: [
|
|
{
|
|
slug: "posts",
|
|
label: "Posts",
|
|
fields: [{ slug: "title", label: "Title", type: "string" }],
|
|
},
|
|
],
|
|
taxonomies: [
|
|
{
|
|
name: "tags",
|
|
label: "Tags",
|
|
hierarchical: false,
|
|
collections: ["posts"],
|
|
terms: [{ slug: "test", label: "Test" }],
|
|
},
|
|
],
|
|
menus: [
|
|
{
|
|
name: "main",
|
|
label: "Main",
|
|
items: [{ type: "custom", label: "Home", url: "/" }],
|
|
},
|
|
],
|
|
widgetAreas: [
|
|
{
|
|
name: "sidebar",
|
|
label: "Sidebar",
|
|
widgets: [{ type: "content", title: "About" }],
|
|
},
|
|
],
|
|
redirects: [{ source: "/legacy-post", destination: "/posts/test" }],
|
|
};
|
|
|
|
// First application
|
|
const result1 = await applySeed(db, seed);
|
|
expect(result1.collections.created).toBe(1);
|
|
expect(result1.taxonomies.created).toBe(1);
|
|
expect(result1.menus.created).toBe(1);
|
|
expect(result1.widgetAreas.created).toBe(1);
|
|
expect(result1.redirects.created).toBe(1);
|
|
|
|
// Second application - should skip existing
|
|
const result2 = await applySeed(db, seed);
|
|
expect(result2.collections.created).toBe(0);
|
|
expect(result2.collections.skipped).toBe(1);
|
|
expect(result2.taxonomies.created).toBe(0);
|
|
// Menus and widgets replace items but don't duplicate
|
|
expect(result2.menus.created).toBe(0);
|
|
expect(result2.widgetAreas.created).toBe(0);
|
|
expect(result2.redirects.created).toBe(0);
|
|
expect(result2.redirects.skipped).toBe(1);
|
|
});
|
|
});
|
|
});
|