fix(housekeeping): address task 14 review round 2

This commit is contained in:
Simo committed 2026-08-30 11:30:17 +02:00
1 parent d09eaa33d6
commit c524b305d7
17 files changed
+614 -130

No files matched your search

@@ -1,5 +1,6 @@
import "server-only";
import type { SQL } from "drizzle-orm";
import { type ContentRouteId, contentRouteGroup } from "../routes";
import type {
ContentQueryData,
@@ -18,102 +19,102 @@ interface QueryDefinition {
export const CONTENT_QUERY_DEFINITIONS = {
"content.editorial.articles": {
statement:
"SELECT id, title, slug AS description, 'published' AS status, updated_at FROM website_articles ORDER BY created_at DESC LIMIT 500",
"SELECT id, title, slug AS description, 'published' AS status, updated_at FROM website_articles ORDER BY created_at DESC",
href: "/ase/content/editorial/articles/",
appendId: true,
},
"content.editorial.article-detail": {
statement:
"SELECT id, title, slug AS description, 'published' AS status, updated_at FROM website_articles ORDER BY created_at DESC LIMIT 500",
"SELECT id, title, slug AS description, 'published' AS status, updated_at FROM website_articles ORDER BY created_at DESC",
href: "/ase/content/editorial/articles/",
appendId: true,
},
"content.editorial.tags": {
statement:
"SELECT id, name AS title, background_color AS status, updated_at FROM tags ORDER BY name LIMIT 500",
"SELECT id, name AS title, background_color AS status, updated_at FROM tags ORDER BY name",
href: "/ase/content/editorial/tags",
},
"content.editorial.writeable-boxes": {
statement:
"SELECT id, title, CASE WHEN is_active = 1 THEN 'active' ELSE 'hidden' END AS status, updated_at FROM website_writeable_boxes ORDER BY position, id LIMIT 500",
"SELECT id, title, CASE WHEN is_active = 1 THEN 'active' ELSE 'hidden' END AS status, updated_at FROM website_writeable_boxes ORDER BY position, id",
href: "/ase/content/editorial/writeable-boxes",
},
"content.media.photos": {
statement:
"SELECT id, url AS title, 'published' AS status, FROM_UNIXTIME(timestamp) AS updated_at FROM camera_web ORDER BY id DESC LIMIT 500",
"SELECT id, url AS title, 'published' AS status, FROM_UNIXTIME(timestamp) AS updated_at FROM camera_web ORDER BY id DESC",
href: "/ase/content/media/photos",
},
"content.media.banners": {
statement:
"SELECT id, title, CASE WHEN is_active = 1 THEN 'active' ELSE 'hidden' END AS status, NULL AS updated_at FROM website_banners ORDER BY sort_order, id LIMIT 500",
"SELECT id, title, CASE WHEN is_active = 1 THEN 'active' ELSE 'hidden' END AS status, NULL AS updated_at FROM website_banners ORDER BY sort_order, id",
href: "/ase/content/media/banners",
},
"content.media.ads": {
statement:
"SELECT id, image AS title, 'active' AS status, updated_at FROM website_ads ORDER BY created_at DESC LIMIT 500",
"SELECT id, image AS title, 'active' AS status, updated_at FROM website_ads ORDER BY created_at DESC",
href: "/ase/content/media/ads/",
appendId: true,
},
"content.media.ad-detail": {
statement:
"SELECT id, image AS title, 'active' AS status, updated_at FROM website_ads ORDER BY created_at DESC LIMIT 500",
"SELECT id, image AS title, 'active' AS status, updated_at FROM website_ads ORDER BY created_at DESC",
href: "/ase/content/media/ads/",
appendId: true,
},
"content.engagement.events": {
statement:
"SELECT id, title, status, updated_at FROM website_events ORDER BY starts_at DESC LIMIT 500",
"SELECT id, title, status, updated_at FROM website_events ORDER BY starts_at DESC",
href: "/ase/content/engagement/events/",
appendId: true,
},
"content.engagement.event-detail": {
statement:
"SELECT id, title, status, updated_at FROM website_events ORDER BY starts_at DESC LIMIT 500",
"SELECT id, title, status, updated_at FROM website_events ORDER BY starts_at DESC",
href: "/ase/content/engagement/events/",
appendId: true,
},
"content.engagement.event-types": {
statement:
"SELECT id, name AS title, CASE WHEN is_active = 1 THEN 'active' ELSE 'inactive' END AS status, NULL AS updated_at FROM website_event_types ORDER BY name LIMIT 500",
"SELECT id, name AS title, CASE WHEN is_active = 1 THEN 'active' ELSE 'inactive' END AS status, NULL AS updated_at FROM website_event_types ORDER BY name",
href: "/ase/content/engagement/events/types",
},
"content.engagement.polls": {
statement:
"SELECT id, title, status, updated_at FROM website_polls ORDER BY created_at DESC LIMIT 500",
"SELECT id, title, status, updated_at FROM website_polls ORDER BY created_at DESC",
href: "/ase/content/engagement/polls/",
appendId: true,
},
"content.engagement.poll-detail": {
statement:
"SELECT id, title, status, updated_at FROM website_polls ORDER BY created_at DESC LIMIT 500",
"SELECT id, title, status, updated_at FROM website_polls ORDER BY created_at DESC",
href: "/ase/content/engagement/polls/",
appendId: true,
},
"content.engagement.prefixes": {
statement:
"SELECT id, text AS title, color AS description, CASE WHEN active = 1 THEN 'active' ELSE 'inactive' END AS status, NULL AS updated_at FROM custom_prefixes ORDER BY id DESC LIMIT 500",
"SELECT id, text AS title, color AS description, CASE WHEN active = 1 THEN 'active' ELSE 'inactive' END AS status, NULL AS updated_at FROM custom_prefixes ORDER BY id DESC",
href: "/ase/content/engagement/prefixes",
},
"content.help.questions": {
statement:
"SELECT id, name AS title, CONCAT('position-', position) AS status, NULL AS updated_at FROM website_help_center_categories ORDER BY position, id LIMIT 500",
"SELECT id, name AS title, CONCAT('position-', position) AS status, NULL AS updated_at FROM website_help_center_categories ORDER BY position, id",
href: "/ase/content/help/questions/",
appendId: true,
},
"content.help.question-detail": {
statement:
"SELECT id, name AS title, CONCAT('position-', position) AS status, NULL AS updated_at FROM website_help_center_categories ORDER BY position, id LIMIT 500",
"SELECT id, name AS title, CONCAT('position-', position) AS status, NULL AS updated_at FROM website_help_center_categories ORDER BY position, id",
href: "/ase/content/help/questions/",
appendId: true,
},
"content.help.email-templates": {
statement:
"SELECT id, name AS title, subject AS description, CASE WHEN is_active = 1 THEN 'active' ELSE 'inactive' END AS status, updated_at FROM email_templates ORDER BY name LIMIT 500",
"SELECT id, name AS title, subject AS description, CASE WHEN is_active = 1 THEN 'active' ELSE 'inactive' END AS status, updated_at FROM email_templates ORDER BY name",
href: "/ase/content/help/email-templates",
},
"content.localization.emulator": {
statement:
"SELECT emulator_settings.key AS id, emulator_settings.key AS title, LEFT(emulator_settings.value, 120) AS description, 'configured' AS status, NULL AS updated_at FROM emulator_settings ORDER BY emulator_settings.key LIMIT 500",
"SELECT emulator_settings.key AS id, emulator_settings.key AS title, LEFT(emulator_settings.value, 120) AS description, 'configured' AS status, NULL AS updated_at FROM emulator_settings ORDER BY emulator_settings.key",
href: "/ase/content/localization/emulator",
},
} as const satisfies Partial<Record<ContentRouteId, QueryDefinition>>;
@@ -161,7 +162,7 @@ function response(
};
}
function matches(
function matchesListSearch(
input: NormalizedContentQueryInput,
item: ContentQueryItem,
): boolean {
@@ -174,28 +175,38 @@ function matches(
}
function mapRows(
input: NormalizedContentQueryInput,
definition: QueryDefinition,
rawRows: readonly RawRow[],
): readonly ContentQueryItem[] {
let items = rawRows
.map((row) => {
const id = value(row.id);
if (!id) throw new Error("invalid Content identifier");
return {
id,
title: value(row.title, "Untitled content"),
description: value(row.description) || undefined,
status: value(row.status) || undefined,
updatedAt: updatedAt(row.updated_at),
href: definition.appendId ? definition.href + id : definition.href,
};
})
.filter((item) => matches(input, item));
if (input.params.id) {
items = items.filter((item) => item.id === input.params.id);
}
return items;
return rawRows.map((row) => {
const id = value(row.id);
if (!id) throw new Error("invalid Content identifier");
return {
id,
title: value(row.title, "Untitled content"),
description: value(row.description) || undefined,
status: value(row.status) || undefined,
updatedAt: updatedAt(row.updated_at),
href: definition.appendId ? definition.href + id : definition.href,
};
});
}
function statementParts(statement: string): {
readonly selection: string;
readonly ordering: string;
} {
const match = /^(.*?)(\s+ORDER BY\s+.+)$/iu.exec(statement);
return match
? { selection: match[1], ordering: match[2] }
: { selection: statement, ordering: "" };
}
function totalFromRows(rawRows: readonly RawRow[]): number {
const total = Number(rawRows[0]?.total ?? 0);
if (!Number.isSafeInteger(total) || total < 0)
throw new Error("invalid Content query total");
return total;
}
async function databaseRoute(
@@ -206,16 +217,41 @@ async function databaseRoute(
import("drizzle-orm"),
import("@/lib/db"),
]);
const items = mapRows(
input,
definition,
const { selection, ordering } = statementParts(definition.statement);
const filters: SQL[] = [];
if (input.list.search) {
const pattern = `%${input.list.search.toLocaleLowerCase()}%`;
filters.push(
/\bAS\s+description\b/iu.test(selection)
? sql`(LOWER(COALESCE(title, '')) LIKE ${pattern} OR LOWER(COALESCE(description, '')) LIKE ${pattern})`
: sql`LOWER(COALESCE(title, '')) LIKE ${pattern}`,
);
}
if (input.params.id) filters.push(sql`CAST(id AS CHAR) = ${input.params.id}`);
const filtered = filters.length
? sql`${sql.raw(selection)} HAVING ${sql.join(filters, sql` AND `)}`
: sql.raw(selection);
const total = totalFromRows(
rows(
await db.execute(
sql.raw(definition.statement.replace(/\s+LIMIT 500$/u, "")),
sql`SELECT COUNT(*) AS total FROM (${filtered}) AS content_rows`,
),
),
);
return response(input, items, items.length);
const items = mapRows(
definition,
rows(
await db.execute(
sql`${filtered} ${sql.raw(ordering)} LIMIT ${input.list.pageSize} OFFSET ${input.list.offset}`,
),
),
);
return {
kind: contentRouteGroup(input.routeId),
items,
total,
partialDependencies: [],
};
}
async function mediaLibrary(
@@ -252,7 +288,7 @@ async function mediaLibrary(
updatedAt: metadata.mtime.toISOString(),
href: "/ase/content/media/library",
}))
.filter((item) => matches(input, item));
.filter((item) => matchesListSearch(input, item));
return response(input, items, items.length);
}
@@ -12,9 +12,23 @@ import {
loadProductionContentQuery,
} from "./content-queries-production";
const queryMocks = vi.hoisted(() => ({ execute: vi.fn() }));
const queryMocks = vi.hoisted(() => ({
execute: vi.fn(),
join: vi.fn((chunks: unknown[], separator: unknown) => ({
chunks,
separator,
})),
raw: vi.fn((statement: string) => ({ statement })),
tagged: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({
strings: Array.from(strings),
values,
})),
}));
vi.mock("drizzle-orm", () => ({
sql: { raw: (statement: string) => statement },
sql: Object.assign(queryMocks.tagged, {
join: queryMocks.join,
raw: queryMocks.raw,
}),
}));
vi.mock("@/lib/db", () => ({ db: { execute: queryMocks.execute } }));
@@ -45,15 +59,15 @@ const ready: ContentQueryData = {
describe("Content query", () => {
it("reports totals beyond the former 500-row adapter cap", async () => {
const allRows = Array.from({ length: 600 }, (_, index) => ({
const pageRows = Array.from({ length: 25 }, (_, index) => ({
id: index + 1,
title: `Article ${index + 1}`,
status: "published",
updated_at: null,
}));
queryMocks.execute.mockImplementationOnce(async (statement: string) => [
statement.includes("LIMIT 500") ? allRows.slice(0, 500) : allRows,
]);
queryMocks.execute
.mockResolvedValueOnce([[{ total: 600 }]])
.mockResolvedValueOnce([pageRows]);
const result = await loadProductionContentQuery({
routeId: "content.editorial.articles",
params: {},
@@ -61,6 +75,41 @@ describe("Content query", () => {
});
expect(result.total).toBe(600);
expect(result.items).toHaveLength(25);
expect(queryMocks.execute).toHaveBeenCalledTimes(2);
const pageQuery = queryMocks.execute.mock.calls[1]?.[0];
expect(JSON.stringify(pageQuery)).toContain("25");
expect(JSON.stringify(pageQuery)).toContain("0");
});
it("binds search, limit, and offset into the bounded page query", async () => {
queryMocks.execute
.mockResolvedValueOnce([[{ total: 1 }]])
.mockResolvedValueOnce([
[{ id: 9, title: "Launch", status: "published", updated_at: null }],
]);
await loadProductionContentQuery({
routeId: "content.editorial.articles",
params: {},
list: { search: "Launch", pageSize: 7, offset: 14 },
});
const serialized = JSON.stringify(queryMocks.execute.mock.calls);
expect(serialized).toContain("%launch%");
expect(serialized).toContain("7");
expect(serialized).toContain("14");
});
it("searches only projected aliases for routes without descriptions", async () => {
queryMocks.execute
.mockResolvedValueOnce([[{ total: 0 }]])
.mockResolvedValueOnce([[]]);
await loadProductionContentQuery({
routeId: "content.engagement.events",
params: {},
list: { search: "launch", pageSize: 25, offset: 0 },
});
const serialized = JSON.stringify(queryMocks.execute.mock.calls.slice(-2));
expect(serialized).toContain("COALESCE(title");
expect(serialized).not.toContain("COALESCE(description");
});
it("never selects email template bodies into summary query results", () => {