Files
EpicNext-Cms/src/lib/services/ticket-replies.ts
T
Simo 3fa1119f5a
CI / check (pull_request) Failing after 9s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
fix(housekeeping): address people moderation review
2026-08-29 23:53:38 +02:00

88 lines
2.4 KiB
TypeScript

export interface TicketReplyRecord {
id: bigint;
userId: number;
content: string;
createdAt: Date | null;
}
export interface TicketReplyDb {
findTicket(
ticketId: bigint,
): Promise<{ id: bigint; userId: number | null } | null>;
createReply(data: {
ticketId: bigint;
userId: number;
content: string;
createdAt: Date;
updatedAt: Date;
}): Promise<TicketReplyRecord>;
touchTicket(ticketId: bigint, updatedAt: Date): Promise<unknown>;
}
export const MAX_UNSIGNED_TICKET_ID = 18_446_744_073_709_551_615n;
function boundedDecimalPattern(maximum: string): RegExp {
const alternatives = [`[1-9]\\d{0,${maximum.length - 2}}`];
for (let index = 0; index < maximum.length; index += 1) {
const maximumDigit = Number(maximum[index]);
const minimumDigit = index === 0 ? 1 : 0;
const upperDigit = maximumDigit - 1;
if (upperDigit < minimumDigit) continue;
const digit =
upperDigit === minimumDigit
? String(minimumDigit)
: `[${minimumDigit}-${upperDigit}]`;
alternatives.push(
`${maximum.slice(0, index)}${digit}\\d{${maximum.length - index - 1}}`,
);
}
alternatives.push(maximum);
return new RegExp(`^(?:${alternatives.join("|")})$`, "u");
}
export const CANONICAL_TICKET_ID_PATTERN = boundedDecimalPattern(
MAX_UNSIGNED_TICKET_ID.toString(),
);
// Canonicalize a SQL BIGINT identifier without passing through Number.
export function canonicalTicketId(value: string | number | bigint): bigint {
let parsed: bigint;
try {
if (typeof value === "bigint") {
parsed = value;
} else if (typeof value === "string") {
if (!CANONICAL_TICKET_ID_PATTERN.test(value)) {
throw new Error("noncanonical identifier");
}
parsed = BigInt(value);
} else if (Number.isSafeInteger(value) && value > 0) {
parsed = BigInt(value);
} else {
throw new Error("unsafe identifier");
}
} catch {
throw new Error("invalid ticket identifier");
}
if (parsed <= 0n || parsed > MAX_UNSIGNED_TICKET_ID) {
throw new Error("invalid ticket identifier");
}
return parsed;
}
export async function createOwnedTicketReply(
db: TicketReplyDb,
input: { ticketId: bigint; userId: number; content: string },
): Promise<TicketReplyRecord | null> {
const ticket = await db.findTicket(input.ticketId);
if (!ticket || ticket.userId !== input.userId) return null;
const now = new Date();
const reply = await db.createReply({
...input,
createdAt: now,
updatedAt: now,
});
await db.touchTicket(input.ticketId, now);
return reply;
}