fix(housekeeping): complete people read fidelity
This commit is contained in:
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
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) {
|
||||||
|
|||||||
Reference in new issue
Block a user