// @ts-check
import { renderHtml } from "./render-html.js";
/** @param {{ args: string[], accept: string[], host: { database: D1Database } }} context */
export default async function network({ args, accept, host }) {
if (args.includes("--help")) return "The Wimble invitation tree.\n\nUsage: run.js\n";
const tree = await invitationTree(host.database);
for (const type of accept) {
if (type === "text/html") {
return new Response(renderHtml(tree), { headers: { "content-type": "text/html; charset=utf-8" } });
}
if (type === "application/json") return tree;
if (type === "text/plain") return renderText(tree);
}
}
/** @param {D1Database} database */
async function invitationTree(database) {
const [usersResult, systemResult, userResult] = await Promise.all([
database.prepare(`
SELECT users.username
FROM users
JOIN computers ON computers.owner_account_id = users.id AND computers.status = 'active'
WHERE users.status = 'active'
ORDER BY users.username COLLATE NOCASE
`).all(),
database.prepare("SELECT accepted_username FROM system_invite_codes WHERE accepted_username IS NOT NULL ORDER BY accepted_at, code").all(),
database.prepare(`
SELECT inviter.username AS inviter, invited.username AS invited
FROM invite_codes
JOIN users inviter ON inviter.id = invite_codes.inviter_id
JOIN users invited ON invited.id = invite_codes.accepted_user_id
WHERE inviter.status = 'active' AND invited.status = 'active'
ORDER BY invite_codes.accepted_at, invite_codes.code
`).all(),
]);
const usernames = /** @type {{ username: string }[]} */ (usersResult.results ?? []).map((row) => row.username);
const active = new Set(usernames);
const parentByUsername = new Map();
for (const row of /** @type {{ accepted_username: string }[]} */ (systemResult.results ?? [])) {
if (active.has(row.accepted_username)) parentByUsername.set(row.accepted_username, "wimble");
}
for (const row of /** @type {{ inviter: string, invited: string }[]} */ (userResult.results ?? [])) {
if (active.has(row.invited) && active.has(row.inviter) && !parentByUsername.has(row.invited)) parentByUsername.set(row.invited, row.inviter);
}
const root = { label: "wimble", path: "/", children: /** @type {any[]} */ ([]) };
const nodes = new Map(usernames.map((username) => [username, {
label: username,
path: `/${username}`,
children: /** @type {any[]} */ ([]),
}]));
for (const username of usernames) {
const node = nodes.get(username);
const parent = nodes.get(parentByUsername.get(username) ?? "") ?? root;
if (node) parent.children.push(node);
}
return root;
}
/** @param {{ label: string, children: any[] }} node @param {string} prefix @param {string} connector */
function textLines(node, prefix = "", connector = "") {
const lines = [`${prefix}${connector}${node.label}`];
const childPrefix = `${prefix}${connector === "├── " ? "│ " : connector === "└── " ? " " : ""}`;
node.children.forEach((child, index) => lines.push(...textLines(child, childPrefix, index === node.children.length - 1 ? "└── " : "├── ")));
return lines;
}
/** @param {{ label: string, children: any[] }} tree */
function renderText(tree) {
return `${textLines(tree).join("\n")}\n`;
}