fix(housekeeping): address task 14 review round 1

This commit is contained in:
Simo committed 2026-08-30 10:53:50 +02:00
1 parent fd68819d9b
commit d09eaa33d6
23 files changed
+2503 -488

No files matched your search

@@ -134,7 +134,11 @@ function rows(result: unknown): readonly RawRow[] {
}
function value(value: unknown, fallback = ""): string {
return typeof value === "string" ? value : value == null ? fallback : String(value);
return typeof value === "string"
? value
: value == null
? fallback
: String(value);
}
function updatedAt(value: unknown): string | null {
@@ -157,7 +161,10 @@ function response(
};
}
function matches(input: NormalizedContentQueryInput, item: ContentQueryItem): boolean {
function matches(
input: NormalizedContentQueryInput,
item: ContentQueryItem,
): boolean {
const needle = input.list.search.toLocaleLowerCase();
return (
needle.length === 0 ||
@@ -171,18 +178,20 @@ function mapRows(
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));
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);
}
@@ -200,7 +209,11 @@ async function databaseRoute(
const items = mapRows(
input,
definition,
rows(await db.execute(sql.raw(definition.statement))),
rows(
await db.execute(
sql.raw(definition.statement.replace(/\s+LIMIT 500$/u, "")),
),
),
);
return response(input, items, items.length);
}
@@ -219,54 +232,67 @@ async function mediaLibrary(
/[.](png|jpe?g|gif|webp|svg|bmp)$/iu.test(name),
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return response(input, []);
if ((error as NodeJS.ErrnoException).code === "ENOENT")
return response(input, []);
throw error;
}
const entries = await Promise.all(
names.map(async (name) => ({ name, metadata: await stat(resolve(MEDIA_ROOT, name)) })),
names.map(async (name) => ({
name,
metadata: await stat(resolve(MEDIA_ROOT, name)),
})),
);
entries.sort((left, right) => right.metadata.mtimeMs - left.metadata.mtimeMs);
const items = entries.map(({ name, metadata }) => ({
id: name,
title: name,
description: String(metadata.size) + " bytes",
status: "stored",
updatedAt: metadata.mtime.toISOString(),
href: "/ase/content/media/library",
})).filter((item) => matches(input, item));
const items = entries
.map(({ name, metadata }) => ({
id: name,
title: name,
description: String(metadata.size) + " bytes",
status: "stored",
updatedAt: metadata.mtime.toISOString(),
href: "/ase/content/media/library",
}))
.filter((item) => matches(input, item));
return response(input, items, items.length);
}
async function brand(input: NormalizedContentQueryInput): Promise<ContentQueryData> {
async function brand(
input: NormalizedContentQueryInput,
): Promise<ContentQueryData> {
const [{ siteSettings }, { listCustomThemes }] = await Promise.all([
import("@/lib/services/site-settings"),
import("@/lib/theme-custom-store"),
]);
if (input.routeId === "content.brand.favicon") {
const favicon = await siteSettings.get("cms_favicon", null);
return response(input, [{
id: "favicon",
title: favicon || "Default favicon",
status: favicon ? "custom" : "default",
href: "/ase/content/brand/favicon",
}]);
return response(input, [
{
id: "favicon",
title: favicon || "Default favicon",
status: favicon ? "custom" : "default",
href: "/ase/content/brand/favicon",
},
]);
}
const [preset, customThemes] = await Promise.all([
siteSettings.get("theme_preset", "Atom (golden)"),
listCustomThemes(),
]);
return response(input, [{
id: "active-theme",
title: preset || "Atom (golden)",
status: "active",
href: "/ase/content/brand/theme",
}, ...customThemes.map((theme) => ({
id: theme.id,
title: theme.name,
status: "saved",
updatedAt: new Date(theme.createdAt).toISOString(),
href: "/ase/content/brand/theme",
}))]);
return response(input, [
{
id: "active-theme",
title: preset || "Atom (golden)",
status: "active",
href: "/ase/content/brand/theme",
},
...customThemes.map((theme) => ({
id: theme.id,
title: theme.name,
status: "saved",
updatedAt: new Date(theme.createdAt).toISOString(),
href: "/ase/content/brand/theme",
})),
]);
}
async function localizationFiles(
@@ -276,13 +302,16 @@ async function localizationFiles(
const { CLIENT_TRANSLATION_FILES } = await import(
"@/lib/client-translation-files"
);
return response(input, CLIENT_TRANSLATION_FILES.map((file) => ({
id: file.id,
title: file.id,
description: file.relPath,
status: file.readOnly ? "read-only" : "editable",
href: "/ase/content/localization/client",
})));
return response(
input,
CLIENT_TRANSLATION_FILES.map((file) => ({
id: file.id,
title: file.id,
description: file.relPath,
status: file.readOnly ? "read-only" : "editable",
href: "/ase/content/localization/client",
})),
);
}
const [{ readdir }, { join }] = await Promise.all([
import("node:fs/promises"),
@@ -319,12 +348,15 @@ async function localizationFiles(
},
]);
}
return response(input, locales.map((locale) => ({
id: locale,
title: locale,
status: "editable",
href: "/ase/content/localization/cms",
})));
return response(
input,
locales.map((locale) => ({
id: locale,
title: locale,
status: "editable",
href: "/ase/content/localization/cms",
})),
);
}
export async function loadProductionContentQuery(
@@ -340,9 +372,11 @@ export async function loadProductionContentQuery(
) {
return localizationFiles(input);
}
const definition = (CONTENT_QUERY_DEFINITIONS as Partial<
Record<ContentRouteId, QueryDefinition>
>)[input.routeId];
const definition = (
CONTENT_QUERY_DEFINITIONS as Partial<
Record<ContentRouteId, QueryDefinition>
>
)[input.routeId];
if (!definition) throw new Error("missing Content query adapter");
return databaseRoute(input, definition);
}
@@ -7,7 +7,16 @@ import {
type ContentQueryInput,
createContentQuery,
} from "./content-queries";
import { CONTENT_QUERY_DEFINITIONS } from "./content-queries-production";
import {
CONTENT_QUERY_DEFINITIONS,
loadProductionContentQuery,
} from "./content-queries-production";
const queryMocks = vi.hoisted(() => ({ execute: vi.fn() }));
vi.mock("drizzle-orm", () => ({
sql: { raw: (statement: string) => statement },
}));
vi.mock("@/lib/db", () => ({ db: { execute: queryMocks.execute } }));
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
@@ -35,6 +44,25 @@ const ready: ContentQueryData = {
};
describe("Content query", () => {
it("reports totals beyond the former 500-row adapter cap", async () => {
const allRows = Array.from({ length: 600 }, (_, 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,
]);
const result = await loadProductionContentQuery({
routeId: "content.editorial.articles",
params: {},
list: { search: "", pageSize: 25, offset: 0 },
});
expect(result.total).toBe(600);
expect(result.items).toHaveLength(25);
});
it("never selects email template bodies into summary query results", () => {
const emailTemplates =
CONTENT_QUERY_DEFINITIONS["content.help.email-templates"];