feat(housekeeping): complete article workflow
CI / check (pull_request) Failing after 11s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-31 21:42:57 +02:00
1 parent fc050bd3f8
commit fa33b4498e
9 files changed
+595 -3

No files matched your search

@@ -14,6 +14,7 @@ interface QueryDefinition {
readonly statement: string;
readonly href: string;
readonly appendId?: boolean;
readonly privatePayload?: (row: RawRow) => unknown;
}
export const CONTENT_QUERY_DEFINITIONS = {
@@ -25,9 +26,15 @@ export const CONTENT_QUERY_DEFINITIONS = {
},
"content.editorial.article-detail": {
statement:
"SELECT id, title, slug AS description, 'published' AS status, updated_at FROM website_articles ORDER BY created_at DESC",
"SELECT id, title, slug AS description, slug, image, short_story, full_story, 'published' AS status, updated_at FROM website_articles ORDER BY created_at DESC",
href: "/ase-next/content/editorial/articles/",
appendId: true,
privatePayload: (row) => ({
slug: value(row.slug),
image: value(row.image),
shortStory: value(row.short_story),
fullStory: value(row.full_story),
}),
},
"content.editorial.tags": {
statement:
@@ -188,6 +195,9 @@ function mapRows(
status: value(row.status) || undefined,
updatedAt: updatedAt(row.updated_at),
href: definition.appendId ? definition.href + id : definition.href,
...(definition.privatePayload
? { privatePayload: definition.privatePayload(row) }
: {}),
};
});
}
@@ -118,6 +118,41 @@ describe("Content query", () => {
expect(emailTemplates.statement).not.toMatch(/\bbody\b|private_payload/iu);
});
it("loads the editable payload only for an individual article", async () => {
queryMocks.execute
.mockResolvedValueOnce([[{ total: 1 }]])
.mockResolvedValueOnce([
[
{
id: 7,
title: "Launch day",
description: "launch-day",
status: "published",
updated_at: null,
slug: "launch-day",
image: "/media/launch.webp",
short_story: "Short launch summary",
full_story: "<p>Complete launch story</p>",
},
],
]);
const result = await loadProductionContentQuery({
routeId: "content.editorial.article-detail",
params: { id: "7" },
list: { search: "", pageSize: 25, offset: 0 },
});
expect(result.items[0]?.privatePayload).toEqual({
slug: "launch-day",
image: "/media/launch.webp",
shortStory: "Short launch summary",
fullStory: "<p>Complete launch story</p>",
});
expect(
CONTENT_QUERY_DEFINITIONS["content.editorial.articles"].statement,
).not.toMatch(/short_story|full_story/iu);
});
it("fails closed before production adapters are invoked", async () => {
const load = vi.fn(async () => ready);
const query = createContentQuery({ load });
@@ -162,6 +197,32 @@ describe("Content query", () => {
});
});
it("fails closed when an article detail payload is incomplete", async () => {
const query = createContentQuery({
load: async () => ({
kind: "editorial",
items: [
{
id: "7",
title: "Launch day",
privatePayload: { slug: "launch-day" },
},
],
total: 1,
partialDependencies: [],
}),
});
const result = await query.run(context([PERMS.NEWS_VIEW]), {
routeId: "content.editorial.article-detail",
params: { id: "7" },
});
expect(result).toMatchObject({
ok: false,
error: { code: "DEPENDENCY_UNAVAILABLE" },
});
});
it("rejects a fake partial result and maps complete adapter failure", async () => {
const malformed = createContentQuery({
load: async () =>
@@ -24,6 +24,26 @@ export interface ContentQueryInput {
readonly list?: ContentQueryListInput;
}
export interface ContentArticlePrivatePayload {
readonly slug: string;
readonly image: string;
readonly shortStory: string;
readonly fullStory: string;
}
export function isContentArticlePrivatePayload(
value: unknown,
): value is ContentArticlePrivatePayload {
if (value === null || typeof value !== "object") return false;
const payload = value as Record<string, unknown>;
return (
typeof payload.slug === "string" &&
typeof payload.image === "string" &&
typeof payload.shortStory === "string" &&
typeof payload.fullStory === "string"
);
}
export interface ContentQueryItem {
readonly id: string;
readonly title: string;
@@ -100,7 +120,7 @@ function isValidData(
) {
return false;
}
return data.items.every(
const validItems = data.items.every(
(item) =>
typeof item.id === "string" &&
item.id.length > 0 &&
@@ -108,6 +128,14 @@ function isValidData(
item.title.length > 0 &&
(item.href === undefined || isSafeHousekeepingHref(item.href)),
);
if (!validItems) return false;
if (input.routeId !== "content.editorial.article-detail") return true;
return (
data.items.length <= 1 &&
data.items.every((item) =>
isContentArticlePrivatePayload(item.privatePayload),
)
);
}
export function createContentQuery(