b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
637 lines
20 KiB
TypeScript
637 lines
20 KiB
TypeScript
import { sql } from "kysely";
|
|
import { ulid } from "ulidx";
|
|
import { afterEach, beforeEach, expect, it } from "vitest";
|
|
|
|
import { RevisionRepository } from "../../../src/database/repositories/revision.js";
|
|
import { loadContentMediaUsageSnapshots } from "../../../src/media/usage/content-snapshots.js";
|
|
import { buildContentMediaUsageSourceKey } from "../../../src/media/usage/source-key.js";
|
|
import { SchemaRegistry } from "../../../src/schema/registry.js";
|
|
import {
|
|
describeEachDialect,
|
|
setupForDialect,
|
|
teardownForDialect,
|
|
type DialectTestContext,
|
|
} from "../../utils/test-db.js";
|
|
|
|
describeEachDialect("content media usage snapshots", (dialect) => {
|
|
let ctx: DialectTestContext;
|
|
let registry: SchemaRegistry;
|
|
let revisionRepo: RevisionRepository;
|
|
|
|
beforeEach(async () => {
|
|
ctx = await setupForDialect(dialect);
|
|
registry = new SchemaRegistry(ctx.db);
|
|
revisionRepo = new RevisionRepository(ctx.db);
|
|
|
|
await registry.createCollection({ slug: "posts", label: "Posts" });
|
|
await registry.createField("posts", { slug: "title", label: "Title", type: "string" });
|
|
await registry.createField("posts", { slug: "hero", label: "Hero", type: "image" });
|
|
await registry.createField("posts", { slug: "attachment", label: "Attachment", type: "file" });
|
|
await registry.createField("posts", {
|
|
slug: "sections",
|
|
label: "Sections",
|
|
type: "repeater",
|
|
validation: { subFields: [{ slug: "image", type: "image", label: "Image" }] },
|
|
});
|
|
await registry.createField("posts", { slug: "body", label: "Body", type: "portableText" });
|
|
await registry.createField("posts", { slug: "raw_data", label: "Raw Data", type: "json" });
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await teardownForDialect(ctx);
|
|
});
|
|
|
|
it("builds a columns snapshot from stored content fields", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "hello-world",
|
|
status: "published",
|
|
locale: "en",
|
|
data: {
|
|
title: "Hello World",
|
|
hero: { id: "media-hero", provider: "local", mimeType: "image/webp" },
|
|
attachment: { id: "media-file", provider: "local", mimeType: "application/pdf" },
|
|
sections: [{ image: { id: "media-section", provider: "local" } }],
|
|
body: [{ _type: "image", asset: { _ref: "media-body" } }],
|
|
raw_data: { id: "media-ignored" },
|
|
},
|
|
});
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
expect(result.snapshots).toHaveLength(1);
|
|
const snapshot = result.snapshots[0]!;
|
|
|
|
expect(snapshot.source).toEqual(
|
|
expect.objectContaining({
|
|
sourceKey: buildContentMediaUsageSourceKey({
|
|
collectionSlug: "posts",
|
|
contentId: item.id,
|
|
sourceVariant: "columns",
|
|
}),
|
|
sourceType: "content",
|
|
collectionSlug: "posts",
|
|
contentId: item.id,
|
|
sourceVariant: "columns",
|
|
locale: "en",
|
|
translationGroup: item.translationGroup,
|
|
contentSlug: "hello-world",
|
|
contentTitle: "Hello World",
|
|
contentStatus: "published",
|
|
contentScheduledAt: null,
|
|
contentDeletedAt: null,
|
|
revisionId: null,
|
|
sourceUpdatedAt: item.updatedAt,
|
|
sourceVersion: item.version,
|
|
}),
|
|
);
|
|
expect(snapshot.fields).toEqual([
|
|
{ slug: "attachment", type: "file" },
|
|
{ slug: "body", type: "portableText" },
|
|
{ slug: "hero", type: "image" },
|
|
{
|
|
slug: "sections",
|
|
type: "repeater",
|
|
validation: { subFields: [{ slug: "image", type: "image" }] },
|
|
},
|
|
]);
|
|
expect(snapshot.occurrences).toEqual([
|
|
expect.objectContaining({ fieldPath: "attachment", mediaId: "media-file" }),
|
|
expect.objectContaining({ fieldPath: "body[0].asset._ref", mediaId: "media-body" }),
|
|
expect.objectContaining({ fieldPath: "hero", mediaId: "media-hero" }),
|
|
expect.objectContaining({ fieldPath: "sections[0].image", mediaId: "media-section" }),
|
|
]);
|
|
expect(snapshot.occurrences).not.toEqual(
|
|
expect.arrayContaining([expect.objectContaining({ mediaId: "media-ignored" })]),
|
|
);
|
|
});
|
|
|
|
it("returns a typed not-found result for missing content", async () => {
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", "missing-content");
|
|
|
|
expect(result).toEqual({ success: false, error: "CONTENT_NOT_FOUND" });
|
|
});
|
|
|
|
it("keeps JSON-looking stored display strings as strings", async () => {
|
|
const title = '{"headline":"Columns"}';
|
|
const item = await insertPost(ctx, {
|
|
slug: "json-title",
|
|
status: "published",
|
|
data: { title },
|
|
});
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
expect(getSnapshot(result, "columns").source.contentTitle).toBe(title);
|
|
});
|
|
|
|
it("builds columns and draft overlay snapshots for a pending draft revision", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: item.id,
|
|
data: {
|
|
_slug: "draft-post",
|
|
title: "Draft Title",
|
|
hero: { id: "media-draft", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
expect(result.snapshots.map((snapshot) => snapshot.source.sourceVariant)).toEqual([
|
|
"columns",
|
|
"draft_overlay",
|
|
]);
|
|
|
|
const columns = result.snapshots.find(
|
|
(snapshot) => snapshot.source.sourceVariant === "columns",
|
|
)!;
|
|
const overlay = result.snapshots.find(
|
|
(snapshot) => snapshot.source.sourceVariant === "draft_overlay",
|
|
)!;
|
|
|
|
expect(columns.source).toEqual(
|
|
expect.objectContaining({
|
|
contentSlug: "live-post",
|
|
contentTitle: "Live Title",
|
|
revisionId: null,
|
|
sourceKey: buildContentMediaUsageSourceKey({
|
|
collectionSlug: "posts",
|
|
contentId: item.id,
|
|
sourceVariant: "columns",
|
|
}),
|
|
}),
|
|
);
|
|
expect(columns.occurrences).toEqual([
|
|
expect.objectContaining({ fieldPath: "hero", mediaId: "media-live" }),
|
|
]);
|
|
|
|
expect(overlay.source).toEqual(
|
|
expect.objectContaining({
|
|
contentSlug: "draft-post",
|
|
contentTitle: "Draft Title",
|
|
revisionId: draft.id,
|
|
sourceKey: buildContentMediaUsageSourceKey({
|
|
collectionSlug: "posts",
|
|
contentId: item.id,
|
|
sourceVariant: "draft_overlay",
|
|
}),
|
|
}),
|
|
);
|
|
expect(overlay.occurrences).toEqual([
|
|
expect.objectContaining({ fieldPath: "hero", mediaId: "media-draft" }),
|
|
]);
|
|
});
|
|
|
|
it("merges draft overlays over columns when the draft changes unrelated fields", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: item.id,
|
|
data: { title: "Draft Title" },
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
const overlay = result.snapshots.find(
|
|
(snapshot) => snapshot.source.sourceVariant === "draft_overlay",
|
|
)!;
|
|
expect(overlay.source.contentTitle).toBe("Draft Title");
|
|
expect(overlay.occurrences).toEqual([
|
|
expect.objectContaining({ fieldPath: "hero", mediaId: "media-live" }),
|
|
]);
|
|
});
|
|
|
|
it("preserves column display fields when a draft changes only media", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: item.id,
|
|
data: { hero: { id: "media-draft", provider: "local", mimeType: "image/webp" } },
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
const overlay = result.snapshots.find(
|
|
(snapshot) => snapshot.source.sourceVariant === "draft_overlay",
|
|
)!;
|
|
expect(overlay.source.contentTitle).toBe("Live Title");
|
|
expect(overlay.occurrences).toEqual([
|
|
expect.objectContaining({ fieldPath: "hero", mediaId: "media-draft" }),
|
|
]);
|
|
});
|
|
|
|
it("keeps JSON-looking revision display strings as strings", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: { title: "Live Title" },
|
|
});
|
|
const draftTitle = '{"headline":"Draft"}';
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: item.id,
|
|
data: { title: draftTitle },
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
const overlay = result.snapshots.find(
|
|
(snapshot) => snapshot.source.sourceVariant === "draft_overlay",
|
|
)!;
|
|
expect(overlay.source.contentTitle).toBe(draftTitle);
|
|
});
|
|
|
|
it("honors draft nulls that clear media fields", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: item.id,
|
|
data: { hero: null },
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result.success).toBe(true);
|
|
if (!result.success) throw new Error(result.error);
|
|
const overlay = result.snapshots.find(
|
|
(snapshot) => snapshot.source.sourceVariant === "draft_overlay",
|
|
)!;
|
|
expect(overlay.occurrences).toEqual([]);
|
|
});
|
|
|
|
it("fails when draft_revision_id belongs to another content row", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: { title: "Live Title" },
|
|
});
|
|
const other = await insertPost(ctx, {
|
|
slug: "other-post",
|
|
status: "published",
|
|
data: { title: "Other Title" },
|
|
});
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: other.id,
|
|
data: { title: "Other Draft" },
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: "DRAFT_REVISION_MISMATCH",
|
|
source: expect.objectContaining({ sourceVariant: "draft_overlay" }),
|
|
snapshots: [
|
|
expect.objectContaining({
|
|
source: expect.objectContaining({ sourceVariant: "columns" }),
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
});
|
|
|
|
it("fails with a columns snapshot when draft_revision_id is missing", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
await setDraftRevision(ctx, item.id, "missing_revision", { allowMissing: true });
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: "DRAFT_REVISION_NOT_FOUND",
|
|
source: expect.objectContaining({ sourceVariant: "draft_overlay" }),
|
|
snapshots: [
|
|
expect.objectContaining({
|
|
source: expect.objectContaining({ sourceVariant: "columns" }),
|
|
occurrences: [expect.objectContaining({ mediaId: "media-live" })],
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
expect(result.source).not.toHaveProperty("sourceFingerprint");
|
|
});
|
|
|
|
it("fails when draft revision data is invalid JSON", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: { title: "Live Title" },
|
|
});
|
|
const revisionId = ulid();
|
|
await sql`
|
|
INSERT INTO revisions (id, collection, entry_id, data, author_id)
|
|
VALUES (${revisionId}, ${"posts"}, ${item.id}, ${"{"}, ${null})
|
|
`.execute(ctx.db);
|
|
await setDraftRevision(ctx, item.id, revisionId);
|
|
|
|
const result = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(result).toEqual(
|
|
expect.objectContaining({
|
|
success: false,
|
|
error: "DRAFT_REVISION_INVALID",
|
|
source: expect.objectContaining({ sourceVariant: "draft_overlay" }),
|
|
snapshots: [
|
|
expect.objectContaining({
|
|
source: expect.objectContaining({ sourceVariant: "columns" }),
|
|
}),
|
|
],
|
|
}),
|
|
);
|
|
expect(result.source).not.toHaveProperty("sourceFingerprint");
|
|
});
|
|
|
|
it("adds stable source schema versions and fingerprints to snapshots", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
const draft = await revisionRepo.create({
|
|
collection: "posts",
|
|
entryId: item.id,
|
|
data: {
|
|
title: "Draft Title",
|
|
hero: { id: "media-draft", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
await setDraftRevision(ctx, item.id, draft.id);
|
|
|
|
const firstResult = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
const secondResult = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
|
|
expect(firstResult.success).toBe(true);
|
|
expect(secondResult.success).toBe(true);
|
|
if (!firstResult.success) throw new Error(firstResult.error);
|
|
if (!secondResult.success) throw new Error(secondResult.error);
|
|
const firstColumns = getSnapshot(firstResult, "columns");
|
|
const firstOverlay = getSnapshot(firstResult, "draft_overlay");
|
|
const secondColumns = getSnapshot(secondResult, "columns");
|
|
const secondOverlay = getSnapshot(secondResult, "draft_overlay");
|
|
|
|
expect(firstColumns.source.schemaVersion).toBe(1);
|
|
expect(firstOverlay.source.schemaVersion).toBe(1);
|
|
expect(firstColumns.source.sourceFingerprint).toEqual(expect.stringMatching(/^[a-f0-9]{16}$/));
|
|
expect(firstOverlay.source.sourceFingerprint).toEqual(expect.stringMatching(/^[a-f0-9]{16}$/));
|
|
expect(firstOverlay.source.sourceFingerprint).not.toBe(firstColumns.source.sourceFingerprint);
|
|
expect(secondColumns.source.sourceFingerprint).toBe(firstColumns.source.sourceFingerprint);
|
|
expect(secondOverlay.source.sourceFingerprint).toBe(firstOverlay.source.sourceFingerprint);
|
|
});
|
|
|
|
it("changes fingerprints when extraction-relevant values or fields change", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
},
|
|
});
|
|
const initial = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
expect(initial.success).toBe(true);
|
|
if (!initial.success) throw new Error(initial.error);
|
|
const initialFingerprint = getSnapshot(initial, "columns").source.sourceFingerprint;
|
|
|
|
await updatePostHero(ctx, item.id, {
|
|
id: "media-updated",
|
|
provider: "local",
|
|
mimeType: "image/webp",
|
|
});
|
|
const valueChanged = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
expect(valueChanged.success).toBe(true);
|
|
if (!valueChanged.success) throw new Error(valueChanged.error);
|
|
const valueChangedFingerprint = getSnapshot(valueChanged, "columns").source.sourceFingerprint;
|
|
|
|
await registry.createField("posts", {
|
|
slug: "thumbnail",
|
|
label: "Thumbnail",
|
|
type: "image",
|
|
});
|
|
const fieldChanged = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
expect(fieldChanged.success).toBe(true);
|
|
if (!fieldChanged.success) throw new Error(fieldChanged.error);
|
|
const fieldChangedFingerprint = getSnapshot(fieldChanged, "columns").source.sourceFingerprint;
|
|
|
|
expect(valueChangedFingerprint).not.toBe(initialFingerprint);
|
|
expect(fieldChangedFingerprint).not.toBe(valueChangedFingerprint);
|
|
});
|
|
|
|
it("keeps fingerprints stable for non-extraction schema metadata changes", async () => {
|
|
const item = await insertPost(ctx, {
|
|
slug: "live-post",
|
|
status: "published",
|
|
data: {
|
|
title: "Live Title",
|
|
hero: { id: "media-live", provider: "local", mimeType: "image/webp" },
|
|
attachment: { id: "file-live", provider: "local", mimeType: "application/pdf" },
|
|
},
|
|
});
|
|
const before = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
expect(before.success).toBe(true);
|
|
if (!before.success) throw new Error(before.error);
|
|
const beforeFingerprint = getSnapshot(before, "columns").source.sourceFingerprint;
|
|
|
|
await ctx.db
|
|
.updateTable("_emdash_fields")
|
|
.set({
|
|
label: "Hero Image",
|
|
required: 1,
|
|
validation: JSON.stringify({ allowedMimeTypes: ["image/png"] }),
|
|
sort_order: 999,
|
|
})
|
|
.where("slug", "=", "hero")
|
|
.execute();
|
|
await ctx.db
|
|
.updateTable("_emdash_fields")
|
|
.set({ sort_order: -999 })
|
|
.where("slug", "=", "attachment")
|
|
.execute();
|
|
|
|
const after = await loadContentMediaUsageSnapshots(ctx.db, "posts", item.id);
|
|
expect(after.success).toBe(true);
|
|
if (!after.success) throw new Error(after.error);
|
|
|
|
expect(getSnapshot(after, "columns").source.sourceFingerprint).toBe(beforeFingerprint);
|
|
});
|
|
});
|
|
|
|
interface TestPostInput {
|
|
slug: string;
|
|
status: string;
|
|
locale?: string;
|
|
data: Record<string, unknown>;
|
|
}
|
|
|
|
interface TestPost {
|
|
id: string;
|
|
slug: string;
|
|
status: string;
|
|
updatedAt: string;
|
|
version: number;
|
|
translationGroup: string;
|
|
}
|
|
|
|
async function insertPost(ctx: DialectTestContext, input: TestPostInput): Promise<TestPost> {
|
|
const id = ulid();
|
|
const now = new Date().toISOString();
|
|
await sql`
|
|
INSERT INTO ${sql.ref("ec_posts")} (
|
|
id,
|
|
slug,
|
|
status,
|
|
created_at,
|
|
updated_at,
|
|
version,
|
|
locale,
|
|
translation_group,
|
|
title,
|
|
hero,
|
|
attachment,
|
|
sections,
|
|
body,
|
|
raw_data
|
|
) VALUES (
|
|
${id},
|
|
${input.slug},
|
|
${input.status},
|
|
${now},
|
|
${now},
|
|
${1},
|
|
${input.locale ?? "en"},
|
|
${id},
|
|
${serializeFieldValue(input.data.title)},
|
|
${serializeFieldValue(input.data.hero)},
|
|
${serializeFieldValue(input.data.attachment)},
|
|
${serializeFieldValue(input.data.sections)},
|
|
${serializeFieldValue(input.data.body)},
|
|
${serializeFieldValue(input.data.raw_data)}
|
|
)
|
|
`.execute(ctx.db);
|
|
|
|
return {
|
|
id,
|
|
slug: input.slug,
|
|
status: input.status,
|
|
updatedAt: now,
|
|
version: 1,
|
|
translationGroup: id,
|
|
};
|
|
}
|
|
|
|
async function setDraftRevision(
|
|
ctx: DialectTestContext,
|
|
contentId: string,
|
|
revisionId: string,
|
|
options: { allowMissing?: boolean } = {},
|
|
): Promise<void> {
|
|
if (options.allowMissing) await allowMissingDraftRevision(ctx);
|
|
await sql`
|
|
UPDATE ${sql.ref("ec_posts")}
|
|
SET draft_revision_id = ${revisionId},
|
|
updated_at = ${new Date().toISOString()}
|
|
WHERE id = ${contentId}
|
|
`.execute(ctx.db);
|
|
}
|
|
|
|
async function allowMissingDraftRevision(ctx: DialectTestContext): Promise<void> {
|
|
if (ctx.dialect === "postgres") {
|
|
await sql`
|
|
ALTER TABLE ${sql.ref("ec_posts")}
|
|
DROP CONSTRAINT IF EXISTS ec_posts_draft_revision_id_fkey
|
|
`.execute(ctx.db);
|
|
return;
|
|
}
|
|
|
|
await sql`PRAGMA foreign_keys = OFF`.execute(ctx.db);
|
|
}
|
|
|
|
async function updatePostHero(
|
|
ctx: DialectTestContext,
|
|
contentId: string,
|
|
hero: Record<string, unknown>,
|
|
): Promise<void> {
|
|
await sql`
|
|
UPDATE ${sql.ref("ec_posts")}
|
|
SET hero = ${serializeFieldValue(hero)},
|
|
updated_at = ${new Date().toISOString()}
|
|
WHERE id = ${contentId}
|
|
`.execute(ctx.db);
|
|
}
|
|
|
|
function getSnapshot(
|
|
result: Extract<Awaited<ReturnType<typeof loadContentMediaUsageSnapshots>>, { success: true }>,
|
|
sourceVariant: "columns" | "draft_overlay",
|
|
) {
|
|
const snapshot = result.snapshots.find(
|
|
(candidate) => candidate.source.sourceVariant === sourceVariant,
|
|
);
|
|
if (!snapshot) throw new Error(`Missing ${sourceVariant} snapshot`);
|
|
return snapshot;
|
|
}
|
|
|
|
function serializeFieldValue(value: unknown): unknown {
|
|
if (value === null || value === undefined) return null;
|
|
if (typeof value === "object") return JSON.stringify(value);
|
|
return value;
|
|
}
|