fix(housekeeping): complete people read fidelity
CI / check (pull_request) Successful in 31s
CI / deploy (pull_request) Skipped
CI / release (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-29 10:34:44 +02:00
1 parent d1382c839e
commit 7920d4f46c
8 files changed
+564 -37

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -1,5 +1,6 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { import {
assertPeopleSerializable,
createPeoplePage, createPeoplePage,
normalizePeopleListInput, normalizePeopleListInput,
normalizePeopleUser, normalizePeopleUser,
@@ -171,5 +172,12 @@ describe("People canonical models", () => {
expect(() => peopleUserHref(Number.MAX_SAFE_INTEGER + 1)).toThrow(); expect(() => peopleUserHref(Number.MAX_SAFE_INTEGER + 1)).toThrow();
expect(() => peopleGuildHref(-4)).toThrow(); expect(() => peopleGuildHref(-4)).toThrow();
expect(() => toPeopleIsoDate("not-a-date")).toThrow(); expect(() => toPeopleIsoDate("not-a-date")).toThrow();
expect(() =>
assertPeopleSerializable({
id: 7,
userId: 0,
href: "/ase/people/users/7",
}),
).toThrow();
}); });
}); });
@@ -92,15 +92,15 @@ export interface PeopleGuildSummary {
readonly id: number; readonly id: number;
readonly name: string; readonly name: string;
readonly description: string; readonly description: string;
readonly ownerId: number; readonly userId: number;
readonly ownerUsername: string | null; readonly ownerUsername: string | null;
readonly roomId: number;
readonly memberCount: number; readonly memberCount: number;
readonly createdAt: string | null; readonly createdAt: string | null;
readonly href: `/ase/people/community/guilds/${number}`; readonly href: `/ase/people/community/guilds/${number}`;
} }
export interface PeopleGuildDetail extends PeopleGuildSummary { export interface PeopleGuildDetail extends PeopleGuildSummary {
readonly roomId: number;
readonly threadCount: number; readonly threadCount: number;
readonly members: readonly { readonly members: readonly {
readonly id: number; readonly id: number;
@@ -166,6 +166,13 @@ export interface PeopleTicketSummary {
| `/ase/people/support/help-tickets/${number}`; | `/ase/people/support/help-tickets/${number}`;
} }
export interface PeopleTicketDeskSummary extends PeopleTicketSummary {
readonly source: "cms";
readonly category: string;
readonly assigneeId: number | null;
readonly messageCount: number;
}
export interface PeopleTicketDetail extends PeopleTicketSummary { export interface PeopleTicketDetail extends PeopleTicketSummary {
readonly category: string; readonly category: string;
readonly assigneeId: number | null; readonly assigneeId: number | null;
@@ -188,6 +195,7 @@ export interface PeopleHelpTicketSummary {
readonly userId: number | null; readonly userId: number | null;
readonly username: string | null; readonly username: string | null;
readonly updatedAt: string | null; readonly updatedAt: string | null;
readonly replyCount: number;
readonly href: `/ase/people/support/help-tickets/${number}`; readonly href: `/ase/people/support/help-tickets/${number}`;
} }
@@ -462,7 +470,25 @@ const DATE_KEYS = new Set([
]); ]);
export function assertPeopleSerializable(value: unknown): void { export function assertPeopleSerializable(value: unknown): void {
function visit(current: unknown, key = ""): void { function permitsZeroSentinel(parent: unknown, key: string): boolean {
if (typeof parent !== "object" || parent === null) return false;
const href = Reflect.get(parent, "href");
if (typeof href !== "string") return false;
if (href.startsWith("/ase/people/community/guilds/")) {
return key === "userId" || key === "roomId";
}
if (href.startsWith("/ase/people/moderation/cfh/")) {
return (
key === "senderId" ||
key === "reportedId" ||
key === "roomId" ||
key === "moderatorId"
);
}
return false;
}
function visit(current: unknown, key = "", parent?: unknown): void {
if ( if (
current === null || current === null ||
typeof current === "string" || typeof current === "string" ||
@@ -480,13 +506,17 @@ export function assertPeopleSerializable(value: unknown): void {
if (typeof current === "number") { if (typeof current === "number") {
if (!Number.isSafeInteger(current)) if (!Number.isSafeInteger(current))
throw new Error("invalid People number"); throw new Error("invalid People number");
if ((key === "id" || key.endsWith("Id")) && current <= 0) { if (
(key === "id" || key.endsWith("Id")) &&
current <= 0 &&
!(current === 0 && permitsZeroSentinel(parent, key))
) {
throw new Error("invalid People identifier"); throw new Error("invalid People identifier");
} }
return; return;
} }
if (Array.isArray(current)) { if (Array.isArray(current)) {
for (const item of current) visit(item); for (const item of current) visit(item, "", current);
return; return;
} }
if (typeof current !== "object" || current instanceof Date) { if (typeof current !== "object" || current instanceof Date) {
@@ -494,7 +524,7 @@ export function assertPeopleSerializable(value: unknown): void {
} }
for (const [childKey, child] of Object.entries(current)) { for (const [childKey, child] of Object.entries(current)) {
if (child === undefined) throw new Error("undefined People value"); if (child === undefined) throw new Error("undefined People value");
visit(child, childKey); visit(child, childKey, current);
} }
} }
visit(value); visit(value);
@@ -183,14 +183,16 @@ const peopleCommunityAdapters: PeopleCommunityAdapters = {
: sql``; : sql``;
const [rowsResult, countResult] = await Promise.all([ const [rowsResult, countResult] = await Promise.all([
db.execute(sql` db.execute(sql`
SELECT g.id, g.name, g.description, g.user_id AS ownerId, SELECT g.id, g.name, g.description, g.user_id AS userId,
u.username AS ownerUsername, g.date_created AS createdAt, u.username AS ownerUsername, g.room_id AS roomId,
g.date_created AS createdAt,
COUNT(gm.id) AS memberCount COUNT(gm.id) AS memberCount
FROM guilds g FROM guilds g
LEFT JOIN users u ON u.id = g.user_id LEFT JOIN users u ON u.id = g.user_id
LEFT JOIN guilds_members gm ON gm.guild_id = g.id LEFT JOIN guilds_members gm ON gm.guild_id = g.id
${where} ${where}
GROUP BY g.id, g.name, g.description, g.user_id, u.username, g.date_created GROUP BY g.id, g.name, g.description, g.user_id, u.username,
g.room_id, g.date_created
ORDER BY ${input.sort === "name" ? sql`g.name` : sql`g.id`} ${ ORDER BY ${input.sort === "name" ? sql`g.name` : sql`g.id`} ${
input.order === "desc" ? sql`DESC` : sql`ASC` input.order === "desc" ? sql`DESC` : sql`ASC`
}, g.id ASC }, g.id ASC
@@ -206,16 +208,18 @@ const peopleCommunityAdapters: PeopleCommunityAdapters = {
id: number; id: number;
name: string; name: string;
description: string; description: string;
ownerId: number; userId: number;
ownerUsername: string | null; ownerUsername: string | null;
roomId: number;
memberCount: number; memberCount: number;
createdAt: number; createdAt: number;
}>(rowsResult).map((row) => ({ }>(rowsResult).map((row) => ({
id: Number(row.id), id: Number(row.id),
name: row.name, name: row.name,
description: row.description, description: row.description,
ownerId: Number(row.ownerId), userId: Number(row.userId),
ownerUsername: row.ownerUsername, ownerUsername: row.ownerUsername,
roomId: Number(row.roomId),
memberCount: Number(row.memberCount), memberCount: Number(row.memberCount),
createdAt: toPeopleIsoDate(row.createdAt), createdAt: toPeopleIsoDate(row.createdAt),
href: peopleGuildHref(Number(row.id)), href: peopleGuildHref(Number(row.id)),
@@ -232,7 +236,7 @@ const peopleCommunityAdapters: PeopleCommunityAdapters = {
]); ]);
const [guildResult, membersResult, threadsResult] = await Promise.all([ const [guildResult, membersResult, threadsResult] = await Promise.all([
db.execute(sql` db.execute(sql`
SELECT g.id, g.name, g.description, g.user_id AS ownerId, SELECT g.id, g.name, g.description, g.user_id AS userId,
u.username AS ownerUsername, g.room_id AS roomId, u.username AS ownerUsername, g.room_id AS roomId,
g.date_created AS createdAt, COUNT(gm.id) AS memberCount g.date_created AS createdAt, COUNT(gm.id) AS memberCount
FROM guilds g FROM guilds g
@@ -257,7 +261,7 @@ const peopleCommunityAdapters: PeopleCommunityAdapters = {
id: number; id: number;
name: string; name: string;
description: string; description: string;
ownerId: number; userId: number;
ownerUsername: string | null; ownerUsername: string | null;
roomId: number; roomId: number;
createdAt: number; createdAt: number;
@@ -268,7 +272,7 @@ const peopleCommunityAdapters: PeopleCommunityAdapters = {
id: Number(row.id), id: Number(row.id),
name: row.name, name: row.name,
description: row.description, description: row.description,
ownerId: Number(row.ownerId), userId: Number(row.userId),
ownerUsername: row.ownerUsername, ownerUsername: row.ownerUsername,
memberCount: Number(row.memberCount), memberCount: Number(row.memberCount),
createdAt: toPeopleIsoDate(row.createdAt), createdAt: toPeopleIsoDate(row.createdAt),
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -385,16 +385,16 @@ const peopleUsersAdapters: PeopleUsersAdapters = {
const sortColumn = const sortColumn =
input.sort === "accountCount" ? sql`accountCount` : sql`ipCurrent`; input.sort === "accountCount" ? sql`accountCount` : sql`ipCurrent`;
const direction = input.order === "desc" ? sql`DESC` : sql`ASC`; const direction = input.order === "desc" ? sql`DESC` : sql`ASC`;
const result = await db.execute(sql` const [result, countResult] = await Promise.all([
db.execute(sql`
WITH grouped AS ( WITH grouped AS (
SELECT ip_current AS ipCurrent, COUNT(*) AS accountCount, SELECT ip_current AS ipCurrent, COUNT(*) AS accountCount
COUNT(*) OVER () AS total
FROM users FROM users
WHERE ip_current <> '' ${search} WHERE ip_current <> '' ${search}
GROUP BY ip_current GROUP BY ip_current
HAVING COUNT(*) >= 2 HAVING COUNT(*) >= 2
), paged AS ( ), paged AS (
SELECT ipCurrent, accountCount, total SELECT ipCurrent, accountCount
FROM grouped FROM grouped
ORDER BY ${sortColumn} ${direction}, ipCurrent ASC ORDER BY ${sortColumn} ${direction}, ipCurrent ASC
LIMIT ${input.pageSize} OFFSET ${input.offset} LIMIT ${input.pageSize} OFFSET ${input.offset}
@@ -404,17 +404,26 @@ const peopleUsersAdapters: PeopleUsersAdapters = {
FROM users u FROM users u
INNER JOIN paged p ON p.ipCurrent = u.ip_current INNER JOIN paged p ON p.ipCurrent = u.ip_current
) )
SELECT p.ipCurrent, p.accountCount, p.total, SELECT p.ipCurrent, p.accountCount,
a.id, a.username, a.rank, a.online a.id, a.username, a.rank, a.online
FROM paged p FROM paged p
INNER JOIN ranked_accounts a ON a.ipCurrent = p.ipCurrent INNER JOIN ranked_accounts a ON a.ipCurrent = p.ipCurrent
WHERE a.accountPosition <= 100 WHERE a.accountPosition <= 100
ORDER BY ${sortColumn} ${direction}, p.ipCurrent ASC, a.id ASC ORDER BY ${sortColumn} ${direction}, p.ipCurrent ASC, a.id ASC
`); `),
db.execute(sql`
SELECT COUNT(*) AS total FROM (
SELECT ip_current
FROM users
WHERE ip_current <> '' ${search}
GROUP BY ip_current
HAVING COUNT(*) >= 2
) AS matching_clusters
`),
]);
const records = resultRows<{ const records = resultRows<{
ipCurrent: string; ipCurrent: string;
accountCount: number | bigint; accountCount: number | bigint;
total: number | bigint;
id: number; id: number;
username: string; username: string;
rank: number; rank: number;
@@ -440,9 +449,14 @@ const peopleUsersAdapters: PeopleUsersAdapters = {
}); });
} }
} }
const totalRow = resultRows<{ total: number | bigint }>(countResult)[0];
const total = Number(totalRow?.total);
if (!Number.isSafeInteger(total) || total < 0) {
throw new Error("invalid People multi-account total");
}
return { return {
rows: [...clusters.values()], rows: [...clusters.values()],
total: Number(records[0]?.total ?? 0), total,
}; };
}, },
async loadSanctions(userId) { async loadSanctions(userId) {