Initial import of LogDarioSeq baseline

Establishes version control for the personal Logseq DB fork before
starting the server-client architecture migration and domain features
described in docs/agent-guide and the approved implementation plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-25 15:32:15 +02:00
commit 40631362b3
2016 changed files with 543898 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
#!/usr/bin/env node
const path = require("node:path");
const readline = require("node:readline/promises");
const { stdin, stdout } = require("node:process");
const { parseArgs } = require("node:util");
const {
buildAdminGraphDeleteUrl,
buildUserGraphsSql,
buildWranglerArgs,
defaultConfigPath,
fail,
formatUserGraphsResult,
parseWranglerResults,
printUserGraphsTable,
runWranglerQuery,
} = require("./graph_user_lib");
function escapeSqlValue(value) {
return value.replaceAll("'", "''");
}
function ensureMutationSuccess(output, context) {
if (!Array.isArray(output) || output.length === 0) {
fail(`Unexpected empty response from wrangler while ${context}.`);
}
output.forEach((statement, index) => {
if (!statement.success) {
fail(`Wrangler mutation failed while ${context} (statement ${index + 1}).`);
}
});
}
function deleteGraphAesKeys(options, graphId) {
const sql = `delete from graph_aes_keys where graph_id = '${escapeSqlValue(graphId)}'`;
const wranglerArgs = buildWranglerArgs({
database: options.database,
config: options.config,
env: options.env,
sql,
});
ensureMutationSuccess(runWranglerQuery(wranglerArgs), `deleting graph_aes_keys for ${graphId}`);
}
function printHelp() {
console.log(`Delete db-sync graphs owned by a user from a remote D1 environment.
Usage:
node worker/scripts/delete_graphs_for_user.js --username <username> [--env prod]
node worker/scripts/delete_graphs_for_user.js --user-id <user-id> [--env prod]
Options:
--username <username> Look up the target user by username.
--user-id <user-id> Look up the target user by user id.
--env <env> Wrangler environment to use. Defaults to "prod".
--database <name> D1 binding or database name. Defaults to "DB".
--config <path> Wrangler config path. Defaults to worker/wrangler.toml.
--base-url <url> Worker base URL. Defaults to DB_SYNC_BASE_URL.
--admin-token <token> Admin delete token. Defaults to DB_SYNC_ADMIN_TOKEN.
--help Show this message.
`);
}
function parseCliArgs(argv) {
const { values } = parseArgs({
args: argv,
options: {
username: { type: "string" },
"user-id": { type: "string" },
env: { type: "string", default: "prod" },
database: { type: "string", default: "DB" },
config: { type: "string", default: defaultConfigPath },
"base-url": { type: "string", default: process.env.DB_SYNC_BASE_URL },
"admin-token": { type: "string", default: process.env.DB_SYNC_ADMIN_TOKEN },
help: { type: "boolean", default: false },
},
strict: true,
allowPositionals: false,
});
if (values.help) {
printHelp();
process.exit(0);
}
const lookupCount = Number(Boolean(values.username)) + Number(Boolean(values["user-id"]));
if (lookupCount !== 1) {
fail("Pass exactly one of --username or --user-id.");
}
return {
lookupField: values.username ? "username" : "id",
lookupLabel: values.username ? "username" : "user-id",
lookupValue: values.username ?? values["user-id"],
env: values.env,
database: values.database,
config: path.resolve(values.config),
baseUrl: values["base-url"],
adminToken: values["admin-token"],
};
}
async function confirmDeletion(result) {
const rl = readline.createInterface({ input: stdin, output: stdout });
try {
const answer = await rl.question(
`Type DELETE to remove ${result.graphs.length} owned graph(s) for ${result.user.user_id}: `,
);
return answer.trim() === "DELETE";
} finally {
rl.close();
}
}
async function main() {
const options = parseCliArgs(process.argv.slice(2));
const lookupSql = buildUserGraphsSql({ ...options, ownedOnly: true });
const lookupArgs = buildWranglerArgs({
database: options.database,
config: options.config,
env: options.env,
sql: lookupSql,
});
const lookupRows = parseWranglerResults(runWranglerQuery(lookupArgs));
const result = formatUserGraphsResult(lookupRows);
if (!result) {
fail(`No user found for ${options.lookupLabel}=${options.lookupValue}.`);
}
printUserGraphsTable(result, "Owned graphs to delete");
if (result.graphs.length === 0) {
console.log("No owned graphs found. Nothing to delete.");
return;
}
if (!options.baseUrl) {
fail("Missing worker base URL. Pass --base-url or set DB_SYNC_BASE_URL.");
}
if (!options.adminToken) {
fail("Missing admin token. Pass --admin-token or set DB_SYNC_ADMIN_TOKEN.");
}
const confirmed = await confirmDeletion(result);
if (!confirmed) {
console.log("Aborted.");
return;
}
for (const graph of result.graphs) {
const response = await fetch(buildAdminGraphDeleteUrl(options.baseUrl, graph.graph_id), {
method: "DELETE",
headers: {
"x-db-sync-admin-token": options.adminToken,
},
});
if (!response.ok) {
const body = await response.text();
fail(`Delete failed for ${graph.graph_id}: ${response.status} ${body}`);
}
deleteGraphAesKeys(options, graph.graph_id);
}
console.log(`Deleted ${result.graphs.length} owned graph(s).`);
}
if (require.main === module) {
main().catch((error) => {
fail(error instanceof Error ? error.message : String(error));
});
}
module.exports = {
confirmDeletion,
parseCliArgs,
};

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
const path = require("node:path");
const readline = require("node:readline/promises");
const { stdin, stdout } = require("node:process");
const { parseArgs } = require("node:util");
const {
buildAdminGraphDeleteUrl,
buildUserGraphsSql,
buildWranglerArgs,
defaultConfigPath,
fail,
formatUserGraphsResult,
parseWranglerResults,
printUserGraphsTable,
runWranglerQuery,
} = require("./graph_user_lib");
function printHelp() {
console.log(`Delete a db-sync user and all related data from a remote D1 environment.
Usage:
node worker/scripts/delete_user_totally.js --username <username> [--env prod]
node worker/scripts/delete_user_totally.js --user-id <user-id> [--env prod]
Options:
--username <username> Look up the target user by username.
--user-id <user-id> Look up the target user by user id.
--env <env> Wrangler environment to use. Defaults to "prod".
--database <name> D1 binding or database name. Defaults to "DB".
--config <path> Wrangler config path. Defaults to worker/wrangler.toml.
--base-url <url> Worker base URL. Defaults to DB_SYNC_BASE_URL.
--admin-token <token> Admin delete token. Defaults to DB_SYNC_ADMIN_TOKEN.
--help Show this message.
`);
}
function parseCliArgs(argv) {
const { values } = parseArgs({
args: argv,
options: {
username: { type: "string" },
"user-id": { type: "string" },
env: { type: "string", default: "prod" },
database: { type: "string", default: "DB" },
config: { type: "string", default: defaultConfigPath },
"base-url": { type: "string", default: process.env.DB_SYNC_BASE_URL },
"admin-token": { type: "string", default: process.env.DB_SYNC_ADMIN_TOKEN },
help: { type: "boolean", default: false },
},
strict: true,
allowPositionals: false,
});
if (values.help) {
printHelp();
process.exit(0);
}
const lookupCount = Number(Boolean(values.username)) + Number(Boolean(values["user-id"]));
if (lookupCount !== 1) {
fail("Pass exactly one of --username or --user-id.");
}
return {
lookupField: values.username ? "username" : "id",
lookupLabel: values.username ? "username" : "user-id",
lookupValue: values.username ?? values["user-id"],
env: values.env,
database: values.database,
config: path.resolve(values.config),
baseUrl: values["base-url"],
adminToken: values["admin-token"],
};
}
function escapeSqlValue(value) {
return value.replaceAll("'", "''");
}
function runSelectQuery(options, sql) {
const wranglerArgs = buildWranglerArgs({
database: options.database,
config: options.config,
env: options.env,
sql,
});
return parseWranglerResults(runWranglerQuery(wranglerArgs));
}
function runMutationQuery(options, sql) {
const wranglerArgs = buildWranglerArgs({
database: options.database,
config: options.config,
env: options.env,
sql,
});
const output = runWranglerQuery(wranglerArgs);
if (!Array.isArray(output) || output.length === 0) {
throw new Error("Unexpected empty response from wrangler.");
}
output.forEach((statement, index) => {
if (!statement.success) {
throw new Error(`Wrangler reported an unsuccessful mutation (statement ${index + 1}).`);
}
});
return output.reduce((sum, statement) => sum + Number(statement?.meta?.changes ?? 0), 0);
}
function sqlCountToNumber(value) {
const numericValue = Number(value);
return Number.isFinite(numericValue) ? numericValue : 0;
}
function isDeleteConfirmationAccepted(answer, userId) {
const normalizedAnswer = answer.trim();
return normalizedAnswer === "DELETE" || normalizedAnswer === `DELETE USER ${userId}`;
}
async function confirmDeletion({ user, ownedGraphsCount, memberGraphsCount }) {
const rl = readline.createInterface({ input: stdin, output: stdout });
try {
const answer = await rl.question(
`Type DELETE to permanently delete this user (${user.user_id}; ${ownedGraphsCount} owned graph(s), ${memberGraphsCount} membership(s)): `,
);
return isDeleteConfirmationAccepted(answer, user.user_id);
} finally {
rl.close();
}
}
async function deleteOwnedGraphs(options, ownedGraphs) {
for (const graph of ownedGraphs) {
const response = await fetch(buildAdminGraphDeleteUrl(options.baseUrl, graph.graph_id), {
method: "DELETE",
headers: {
"x-db-sync-admin-token": options.adminToken,
},
});
if (!response.ok) {
const payload = await response.text();
fail(`Delete failed for owned graph ${graph.graph_id}: ${response.status} ${payload}`);
}
}
}
async function main() {
const options = parseCliArgs(process.argv.slice(2));
const graphRows = runSelectQuery(options, buildUserGraphsSql({ ...options, ownedOnly: false }));
const result = formatUserGraphsResult(graphRows);
if (!result) {
fail(`No user found for ${options.lookupLabel}=${options.lookupValue}.`);
}
const ownedGraphs = result.graphs.filter((graph) => graph.access_role === "owner");
const memberGraphs = result.graphs.filter((graph) => graph.access_role !== "owner");
printUserGraphsTable(result, "Graphs linked to user");
console.log(`Owned graphs: ${ownedGraphs.length}`);
console.log(`Member graphs: ${memberGraphs.length}`);
if (ownedGraphs.length > 0 && !options.baseUrl) {
fail("Missing worker base URL. Pass --base-url or set DB_SYNC_BASE_URL.");
}
if (ownedGraphs.length > 0 && !options.adminToken) {
fail("Missing admin token. Pass --admin-token or set DB_SYNC_ADMIN_TOKEN.");
}
const confirmed = await confirmDeletion({
user: result.user,
ownedGraphsCount: ownedGraphs.length,
memberGraphsCount: memberGraphs.length,
});
if (!confirmed) {
console.log("Aborted.");
return;
}
if (ownedGraphs.length > 0) {
await deleteOwnedGraphs(options, ownedGraphs);
}
const escapedUserId = escapeSqlValue(result.user.user_id);
const remainingOwnedGraphRows = runSelectQuery(
options,
`select count(1) as owned_graph_count from graphs where user_id = '${escapedUserId}'`,
);
const remainingOwnedGraphCount = sqlCountToNumber(remainingOwnedGraphRows[0]?.owned_graph_count);
if (remainingOwnedGraphCount > 0) {
fail(
`Owned graph cleanup incomplete: ${remainingOwnedGraphCount} graph(s) still owned by ${result.user.user_id}.`,
);
}
const deletedGraphAesKeys = runMutationQuery(
options,
`delete from graph_aes_keys where user_id = '${escapedUserId}'`,
);
const deletedGraphMembers = runMutationQuery(
options,
`delete from graph_members where user_id = '${escapedUserId}'`,
);
const clearedInvitedBy = runMutationQuery(
options,
`update graph_members set invited_by = null where invited_by = '${escapedUserId}'`,
);
const deletedUserRsaKeys = runMutationQuery(
options,
`delete from user_rsa_keys where user_id = '${escapedUserId}'`,
);
const deletedUsers = runMutationQuery(options, `delete from users where id = '${escapedUserId}'`);
if (deletedUsers !== 1) {
fail(`Expected to delete exactly one user row, but deleted ${deletedUsers}.`);
}
const userRowsAfterDelete = runSelectQuery(
options,
`select id from users where id = '${escapedUserId}' limit 1`,
);
if (userRowsAfterDelete.length > 0) {
fail(`User ${result.user.user_id} still exists after deletion.`);
}
console.table([
{ step: "owned graphs deleted", rows: ownedGraphs.length },
{ step: "graph_aes_keys deleted", rows: deletedGraphAesKeys },
{ step: "graph_members deleted", rows: deletedGraphMembers },
{ step: "graph_members invited_by cleared", rows: clearedInvitedBy },
{ step: "user_rsa_keys deleted", rows: deletedUserRsaKeys },
{ step: "users deleted", rows: deletedUsers },
]);
console.log(`Deleted user ${result.user.user_id} successfully.`);
}
if (require.main === module) {
main().catch((error) => {
fail(error instanceof Error ? error.message : String(error));
});
}
module.exports = {
confirmDeletion,
isDeleteConfirmationAccepted,
parseCliArgs,
};

View File

@@ -0,0 +1,59 @@
const assert = require("node:assert/strict");
const { spawnSync } = require("node:child_process");
const path = require("node:path");
const test = require("node:test");
const { isDeleteConfirmationAccepted, parseCliArgs } = require("./delete_user_totally");
const { defaultConfigPath } = require("./graph_user_lib");
function runCli(args) {
return spawnSync(process.execPath, [path.join(__dirname, "delete_user_totally.js"), ...args], {
encoding: "utf8",
});
}
test("parseCliArgs accepts --username", () => {
const parsed = parseCliArgs(["--username", "alice"]);
assert.equal(parsed.lookupField, "username");
assert.equal(parsed.lookupLabel, "username");
assert.equal(parsed.lookupValue, "alice");
assert.equal(parsed.env, "prod");
assert.equal(parsed.database, "DB");
assert.equal(parsed.config, path.resolve(defaultConfigPath));
});
test("parseCliArgs accepts --user-id", () => {
const parsed = parseCliArgs(["--user-id", "user-123"]);
assert.equal(parsed.lookupField, "id");
assert.equal(parsed.lookupLabel, "user-id");
assert.equal(parsed.lookupValue, "user-123");
});
test("CLI --help exits successfully", () => {
const result = runCli(["--help"]);
assert.equal(result.status, 0);
assert.match(result.stdout, /Delete a db-sync user and all related data/);
});
test("CLI rejects passing both --username and --user-id", () => {
const result = runCli(["--username", "alice", "--user-id", "user-123"]);
assert.equal(result.status, 1);
assert.match(result.stderr, /Pass exactly one of --username or --user-id\./);
});
test("confirmation accepts DELETE", () => {
assert.equal(isDeleteConfirmationAccepted("DELETE", "user-123"), true);
});
test("confirmation accepts legacy DELETE USER <id>", () => {
assert.equal(isDeleteConfirmationAccepted("DELETE USER user-123", "user-123"), true);
});
test("confirmation rejects unrelated input", () => {
assert.equal(isDeleteConfirmationAccepted("DELETE USER other-user", "user-123"), false);
assert.equal(isDeleteConfirmationAccepted("yes", "user-123"), false);
});

View File

@@ -0,0 +1,246 @@
#!/usr/bin/env node
const Database = require("better-sqlite3");
const fs = require("node:fs");
const path = require("node:path");
const zlib = require("node:zlib");
const { parseArgs } = require("node:util");
const transit = require("transit-js");
const { fail } = require("./graph_user_lib");
const defaultBaseUrl = "https://api.logseq.com";
function printHelp() {
console.log(`Download a graph snapshot and store it as a local sqlite debug DB.
Usage:
node worker/scripts/download_graph_db.js --graph-id <graph-id> --admin-token <token>
node worker/scripts/download_graph_db.js --graph-id <graph-id> --output ./tmp/my-graph.sqlite
Options:
--graph-id <id> Target graph id. Required.
--admin-token <token> Admin token. Defaults to DB_SYNC_ADMIN_TOKEN.
--base-url <url> Worker base URL. Defaults to DB_SYNC_BASE_URL or https://api.logseq.com.
--output <path> SQLite output path. Defaults to ./tmp/graph-<graph-id>.snapshot.sqlite.
--help Show this message.
Notes:
The output sqlite matches local graph DB schema and contains only:
kvs(addr, content, addresses).
`);
}
function sanitizeGraphIdForFilename(graphId) {
return graphId.replaceAll(/[^a-zA-Z0-9.-]/g, "_");
}
function parseCliArgs(argv) {
const { values } = parseArgs({
args: argv,
options: {
"graph-id": { type: "string" },
"admin-token": { type: "string", default: process.env.DB_SYNC_ADMIN_TOKEN },
"base-url": { type: "string", default: process.env.DB_SYNC_BASE_URL || defaultBaseUrl },
output: { type: "string" },
help: { type: "boolean", default: false },
},
strict: true,
allowPositionals: false,
});
if (values.help) {
printHelp();
process.exit(0);
}
if (!values["graph-id"]) {
fail("Missing required --graph-id.");
}
if (!values["admin-token"]) {
fail("Missing admin token. Pass --admin-token or set DB_SYNC_ADMIN_TOKEN.");
}
const output = values.output
? path.resolve(values.output)
: path.resolve("tmp", `graph-${sanitizeGraphIdForFilename(values["graph-id"])}.snapshot.sqlite`);
return {
graphId: values["graph-id"],
adminToken: values["admin-token"],
baseUrl: values["base-url"],
output,
};
}
function authHeaders(adminToken) {
return {
"x-db-sync-admin-token": adminToken,
};
}
function normalizeBaseUrl(baseUrl) {
return baseUrl.replace(/\/+$/, "");
}
async function fetchJson(url, adminToken) {
const response = await fetch(url, {
method: "GET",
headers: authHeaders(adminToken),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Request failed (${response.status}) for ${url}: ${body}`);
}
return response.json();
}
async function fetchSnapshotDescriptor(options) {
const baseUrl = normalizeBaseUrl(options.baseUrl);
const url = `${baseUrl}/sync/${encodeURIComponent(options.graphId)}/snapshot/download`;
return fetchJson(url, options.adminToken);
}
async function fetchSnapshotBytes(url, adminToken) {
const response = await fetch(url, {
method: "GET",
headers: authHeaders(adminToken),
});
if (!response.ok) {
const body = await response.text();
throw new Error(`Snapshot download failed (${response.status}) for ${url}: ${body}`);
}
const buffer = Buffer.from(await response.arrayBuffer());
const contentEncoding = response.headers.get("content-encoding");
return {
buffer,
contentEncoding,
};
}
function hasGzipMagic(buffer) {
return buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
}
function maybeDecompressBuffer(buffer, contentEncoding) {
if (contentEncoding === "gzip" && hasGzipMagic(buffer)) {
return zlib.gunzipSync(buffer);
}
return buffer;
}
function parseFramedRows(buffer) {
const rows = [];
const reader = transit.reader("json");
let offset = 0;
while (offset < buffer.length) {
if (buffer.length - offset < 4) {
throw new Error("Invalid snapshot payload: incomplete frame header");
}
const frameLength = buffer.readUInt32BE(offset);
offset += 4;
if (buffer.length - offset < frameLength) {
throw new Error("Invalid snapshot payload: incomplete frame payload");
}
const payload = buffer.subarray(offset, offset + frameLength);
offset += frameLength;
const batch = reader.read(payload.toString("utf8"));
if (!Array.isArray(batch)) {
throw new Error("Invalid snapshot payload: decoded frame is not an array");
}
for (const row of batch) {
if (!Array.isArray(row) || row.length < 2) {
throw new Error("Invalid snapshot payload: row must be [addr, content, addresses?]");
}
rows.push(row);
}
}
return rows;
}
function writeSnapshotSqlite({
outputPath,
rows,
}) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
if (fs.existsSync(outputPath)) {
fs.rmSync(outputPath);
}
const db = new Database(outputPath);
try {
db.exec(`
create table if not exists kvs (
addr INTEGER primary key,
content TEXT,
addresses JSON
);
`);
const upsertKvs = db.prepare(
"insert into kvs (addr, content, addresses) values (?, ?, ?) on conflict(addr) do update set content = excluded.content, addresses = excluded.addresses",
);
const writeAll = db.transaction(() => {
for (const row of rows) {
const [addr, content, addresses] = row;
upsertKvs.run(addr, content, addresses ?? null);
}
});
writeAll();
} finally {
db.close();
}
}
async function main() {
const options = parseCliArgs(process.argv.slice(2));
const descriptor = await fetchSnapshotDescriptor(options);
if (!descriptor || !descriptor.url) {
fail("Snapshot download response missing URL.");
}
const snapshot = await fetchSnapshotBytes(descriptor.url, options.adminToken);
const effectiveEncoding = descriptor["content-encoding"] || snapshot.contentEncoding || "";
const decompressed = maybeDecompressBuffer(snapshot.buffer, effectiveEncoding);
const rows = parseFramedRows(decompressed);
writeSnapshotSqlite({
outputPath: options.output,
rows,
});
console.log(`Saved graph snapshot sqlite to ${options.output}`);
console.log(`Graph: ${options.graphId}`);
console.log(`Rows: ${rows.length}`);
if (descriptor.key) {
console.log(`Snapshot key: ${descriptor.key}`);
}
}
if (require.main === module) {
main().catch((error) => {
fail(error instanceof Error ? error.message : String(error));
});
}
module.exports = {
parseCliArgs,
parseFramedRows,
sanitizeGraphIdForFilename,
writeSnapshotSqlite,
};

View File

@@ -0,0 +1,117 @@
const assert = require("node:assert/strict");
const { spawnSync } = require("node:child_process");
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const test = require("node:test");
const transit = require("transit-js");
const Database = require("better-sqlite3");
const {
parseFramedRows,
parseCliArgs,
sanitizeGraphIdForFilename,
writeSnapshotSqlite,
} = require("./download_graph_db");
function runCli(args, env = {}) {
return spawnSync(process.execPath, [path.join(__dirname, "download_graph_db.js"), ...args], {
encoding: "utf8",
env: { ...process.env, ...env },
});
}
test("parseCliArgs accepts required args and defaults output", () => {
const parsed = parseCliArgs([
"--graph-id",
"graph-1",
"--admin-token",
"admin-token-value",
]);
assert.equal(parsed.graphId, "graph-1");
assert.equal(parsed.baseUrl, "https://api.logseq.com");
assert.equal(parsed.adminToken, "admin-token-value");
assert.equal(parsed.output, path.resolve("tmp", "graph-graph-1.snapshot.sqlite"));
});
test("sanitizeGraphIdForFilename replaces unsafe chars", () => {
assert.equal(sanitizeGraphIdForFilename("us-east-1:abc/def"), "us-east-1_abc_def");
});
test("CLI --help exits successfully", () => {
const result = runCli(["--help"]);
assert.equal(result.status, 0);
assert.match(result.stdout, /Download a graph snapshot and store it as a local sqlite debug DB/);
});
test("CLI rejects missing --graph-id", () => {
const result = runCli(["--admin-token", "admin-token-value"]);
assert.equal(result.status, 1);
assert.match(result.stderr, /Missing required --graph-id\./);
});
test("CLI rejects missing admin-token when env is absent", () => {
const result = runCli(["--graph-id", "graph-1"], { DB_SYNC_ADMIN_TOKEN: "" });
assert.equal(result.status, 1);
assert.match(result.stderr, /Missing admin token/);
});
test("parseFramedRows decodes framed transit batches", () => {
const writer = transit.writer("json");
const rowsA = [
[1, "line-1", null],
[2, "line-2", null],
];
const rowsB = [
[1000606, "line-1000606", "{\"1\":[2,3]}"],
];
const encodeFrame = (rows) => {
const payload = Buffer.from(writer.write(rows), "utf8");
const frame = Buffer.allocUnsafe(4 + payload.length);
frame.writeUInt32BE(payload.length, 0);
payload.copy(frame, 4);
return frame;
};
const framed = Buffer.concat([encodeFrame(rowsA), encodeFrame(rowsB)]);
const parsed = parseFramedRows(framed);
assert.deepEqual(parsed, [...rowsA, ...rowsB]);
});
test("writeSnapshotSqlite writes kvs-only sqlite like local dbs", () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "db-sync-download-test-"));
const dbPath = path.join(tmpDir, "graph-debug.sqlite");
const rows = [
[1, "line-1", null],
[2, "line-2", null],
[1000606, "line-1000606", "{\"1\":[2,3]}"],
];
writeSnapshotSqlite({
outputPath: dbPath,
rows,
});
const db = new Database(dbPath, { readonly: true });
const tableNames = db
.prepare("select name from sqlite_master where type = 'table' order by name")
.all()
.map((row) => row.name);
const rowCount = db.prepare("select count(1) as count from kvs").get().count;
const row1000606 = db.prepare("select addr, content, addresses from kvs where addr = 1000606").get();
assert.deepEqual(tableNames, ["kvs"]);
assert.equal(rowCount, 3);
assert.equal(row1000606.addr, 1000606);
assert.equal(row1000606.content, "line-1000606");
assert.equal(row1000606.addresses, "{\"1\":[2,3]}");
db.close();
});

View File

@@ -0,0 +1,7 @@
import { readFileSync, writeFileSync } from "node:fs";
const html = readFileSync(new URL("../dist/api-docs.html", import.meta.url), "utf8");
writeFileSync(
new URL("../dist/api-docs.generated.mjs", import.meta.url),
`export default ${JSON.stringify(html)};\n`,
);

View File

@@ -0,0 +1,196 @@
const { execFileSync } = require("node:child_process");
const path = require("node:path");
const repoRoot = path.resolve(__dirname, "..", "..");
const defaultConfigPath = path.join(repoRoot, "worker", "wrangler.toml");
function fail(message) {
console.error(message);
process.exit(1);
}
function escapeSqlValue(value) {
return value.replaceAll("'", "''");
}
function sqlBooleanToBool(value) {
if (value === null || value === undefined) return null;
return Number(value) === 1;
}
function sqlTimestampToIso(value) {
if (value === null || value === undefined || value === "") return null;
const timestamp = Number(value);
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : null;
}
function buildUserGraphsSql({ lookupField, lookupValue, ownedOnly = false }) {
const escapedValue = escapeSqlValue(lookupValue);
const memberUnion = ownedOnly
? ""
: `
union all
select g.graph_id,
g.graph_name,
g.user_id as owner_user_id,
m.role as access_role,
m.invited_by,
g.schema_version,
g.graph_e2ee,
g.graph_ready_for_use,
g.created_at,
g.updated_at
from graph_members m
join graphs g on g.graph_id = m.graph_id
join target_user u on m.user_id = u.id
where g.user_id <> u.id`;
return `with target_user as (
select id, email, username
from users
where ${lookupField} = '${escapedValue}'
limit 1
),
matching_graphs as (
select g.graph_id,
g.graph_name,
g.user_id as owner_user_id,
'owner' as access_role,
null as invited_by,
g.schema_version,
g.graph_e2ee,
g.graph_ready_for_use,
g.created_at,
g.updated_at
from graphs g
join target_user u on g.user_id = u.id${memberUnion}
)
select u.id as user_id,
u.email as user_email,
u.username as user_username,
g.graph_id,
g.graph_name,
g.access_role,
g.invited_by,
g.owner_user_id,
owner.email as owner_email,
owner.username as owner_username,
g.schema_version,
g.graph_e2ee,
g.graph_ready_for_use,
g.created_at,
g.updated_at
from target_user u
left join matching_graphs g on 1 = 1
left join users owner on owner.id = g.owner_user_id
order by g.updated_at desc;`;
}
function buildWranglerArgs({ database, config, env, sql, local = false }) {
const useLocal = local || env === "local";
const args = [
"dlx",
"wrangler",
"d1",
"execute",
database,
"--config",
config,
];
if (useLocal) {
args.push("--local");
} else {
args.push("--env", env, "--remote");
}
args.push("--json", "--command", sql);
return args;
}
function runWranglerQuery(args) {
const output = execFileSync("pnpm", args, {
cwd: repoRoot,
encoding: "utf8",
stdio: ["ignore", "pipe", "inherit"],
});
return JSON.parse(output);
}
function parseWranglerResults(output) {
if (!Array.isArray(output) || output.length === 0) {
throw new Error("Unexpected empty response from wrangler.");
}
const [statement] = output;
if (!statement.success) {
throw new Error("Wrangler reported an unsuccessful D1 query.");
}
return Array.isArray(statement.results) ? statement.results : [];
}
function formatUserGraphsResult(rows) {
if (rows.length === 0) {
return null;
}
const [firstRow] = rows;
const graphs = rows
.filter((row) => row.graph_id)
.map((row) => ({
graph_id: row.graph_id,
graph_name: row.graph_name,
access_role: row.access_role,
invited_by: row.invited_by ?? null,
owner_user_id: row.owner_user_id,
owner_username: row.owner_username ?? null,
owner_email: row.owner_email ?? null,
schema_version: row.schema_version ?? null,
graph_e2ee: sqlBooleanToBool(row.graph_e2ee),
graph_ready_for_use: sqlBooleanToBool(row.graph_ready_for_use),
created_at: sqlTimestampToIso(row.created_at),
updated_at: sqlTimestampToIso(row.updated_at),
}));
return {
user: {
user_id: firstRow.user_id,
username: firstRow.user_username ?? null,
email: firstRow.user_email ?? null,
},
graphs,
};
}
function printUserGraphsTable(result, countLabel = "Graphs") {
console.log(
`User: ${result.user.user_id}` +
(result.user.username ? ` (${result.user.username})` : "") +
(result.user.email ? ` <${result.user.email}>` : ""),
);
console.log(`${countLabel}: ${result.graphs.length}`);
if (result.graphs.length > 0) {
console.table(result.graphs);
}
}
function buildAdminGraphDeleteUrl(baseUrl, graphId) {
const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
return `${normalizedBaseUrl}/admin/graphs/${encodeURIComponent(graphId)}`;
}
module.exports = {
buildAdminGraphDeleteUrl,
buildUserGraphsSql,
buildWranglerArgs,
defaultConfigPath,
fail,
formatUserGraphsResult,
parseWranglerResults,
printUserGraphsTable,
repoRoot,
runWranglerQuery,
};

View File

@@ -0,0 +1,104 @@
#!/usr/bin/env node
const path = require("node:path");
const { parseArgs } = require("node:util");
const {
buildUserGraphsSql,
buildWranglerArgs,
defaultConfigPath,
fail,
formatUserGraphsResult,
parseWranglerResults,
printUserGraphsTable,
runWranglerQuery,
} = require("./graph_user_lib");
function printHelp() {
console.log(`Show db-sync graphs for a user from a remote D1 environment.
Usage:
node worker/scripts/show_graphs_for_user.js --username <username> [--env prod] [--json]
node worker/scripts/show_graphs_for_user.js --user-id <user-id> [--env prod] [--json]
Options:
--username <username> Look up the target user by username.
--user-id <user-id> Look up the target user by user id.
--env <env> Wrangler environment to use. Defaults to "prod".
--database <name> D1 binding or database name. Defaults to "DB".
--config <path> Wrangler config path. Defaults to worker/wrangler.toml.
--json Print JSON instead of a table.
--help Show this message.
`);
}
function parseCliArgs(argv) {
const { values } = parseArgs({
args: argv,
options: {
username: { type: "string" },
"user-id": { type: "string" },
env: { type: "string", default: "prod" },
database: { type: "string", default: "DB" },
config: { type: "string", default: defaultConfigPath },
json: { type: "boolean", default: false },
help: { type: "boolean", default: false },
},
strict: true,
allowPositionals: false,
});
if (values.help) {
printHelp();
process.exit(0);
}
const lookupCount = Number(Boolean(values.username)) + Number(Boolean(values["user-id"]));
if (lookupCount !== 1) {
fail("Pass exactly one of --username or --user-id.");
}
return {
lookupField: values.username ? "username" : "id",
lookupLabel: values.username ? "username" : "user-id",
lookupValue: values.username ?? values["user-id"],
env: values.env,
database: values.database,
config: path.resolve(values.config),
json: values.json,
};
}
function main() {
const options = parseCliArgs(process.argv.slice(2));
const sql = buildUserGraphsSql(options);
const wranglerArgs = buildWranglerArgs({
database: options.database,
config: options.config,
env: options.env,
sql,
});
const rows = parseWranglerResults(runWranglerQuery(wranglerArgs));
const result = formatUserGraphsResult(rows);
if (!result) {
fail(`No user found for ${options.lookupLabel}=${options.lookupValue}.`);
}
if (options.json) {
console.log(JSON.stringify(result, null, 2));
} else {
printUserGraphsTable(result);
}
}
if (require.main === module) {
try {
main();
} catch (error) {
fail(error instanceof Error ? error.message : String(error));
}
}
module.exports = {
parseCliArgs,
};

View File

@@ -0,0 +1,188 @@
#!/usr/bin/env node
const path = require("node:path");
const { parseArgs } = require("node:util");
const {
buildWranglerArgs,
defaultConfigPath,
fail,
parseWranglerResults,
runWranglerQuery,
} = require("./graph_user_lib");
function printHelp() {
console.log(`Show db-sync usage stats from a D1 environment.
Usage:
node worker/scripts/show_usage_stats.js [--env prod|local] [--days 7] [--json]
Options:
--env <env> Wrangler environment to use. Defaults to "prod"; use "local" to query the local D1 database.
--days <n> Active window size in days. Defaults to 1.
--database <name> D1 binding or database name. Defaults to "DB".
--config <path> Wrangler config path. Defaults to worker/wrangler.toml.
--json Print JSON instead of a table.
--help Show this message.
Output:
days
active_since_utc
active_users_last_n_days
active_graphs_last_n_days
total_users
total_graphs
users_created_today
graphs_created_today
today_utc
`);
}
function parseDays(value) {
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed < 1) {
fail("--days must be an integer >= 1.");
}
return parsed;
}
function parseCliArgs(argv) {
const { values } = parseArgs({
args: argv,
options: {
env: { type: "string", default: "prod" },
days: { type: "string", default: "1" },
database: { type: "string", default: "DB" },
config: { type: "string", default: defaultConfigPath },
json: { type: "boolean", default: false },
help: { type: "boolean", default: false },
},
strict: true,
allowPositionals: false,
});
if (values.help) {
printHelp();
process.exit(0);
}
return {
env: values.env,
days: parseDays(values.days),
database: values.database,
config: path.resolve(values.config),
json: values.json,
};
}
function buildUsageStatsSql(days) {
const sinceDays = days - 1;
return `with bounds as (
select
cast(strftime('%s', 'now', 'start of day') as integer) * 1000 as today_start_ms,
cast(strftime('%s', 'now', 'start of day', '+1 day') as integer) * 1000 as tomorrow_start_ms,
date('now') as today_utc,
date('now', '-${sinceDays} days') as active_since_utc
)
select
${days} as days,
(select active_since_utc from bounds) as active_since_utc,
(select count(distinct entity_id)
from daily_active_entities a
join bounds b on 1 = 1
where a.entity_type = 'user'
and a.day_utc >= b.active_since_utc
and a.day_utc <= b.today_utc) as active_users_last_n_days,
(select count(distinct entity_id)
from daily_active_entities a
join bounds b on 1 = 1
where a.entity_type = 'graph'
and a.day_utc >= b.active_since_utc
and a.day_utc <= b.today_utc) as active_graphs_last_n_days,
(select count(1) from users) as total_users,
(select count(1) from graphs) as total_graphs,
(select count(1)
from users u
join bounds b on 1 = 1
where u.created_at is not null
and u.created_at >= b.today_start_ms
and u.created_at < b.tomorrow_start_ms) as users_created_today,
(select count(1)
from graphs g
join bounds b on 1 = 1
where g.created_at is not null
and g.created_at >= b.today_start_ms
and g.created_at < b.tomorrow_start_ms) as graphs_created_today,
(select today_utc from bounds) as today_utc;`;
}
function sqlCountToNumber(value) {
const numeric = Number(value);
return Number.isFinite(numeric) ? numeric : 0;
}
function formatUsageStats(rows) {
if (!rows.length) {
return null;
}
const [row] = rows;
return {
days: sqlCountToNumber(row.days),
active_since_utc: typeof row.active_since_utc === "string" ? row.active_since_utc : null,
active_users_last_n_days: sqlCountToNumber(row.active_users_last_n_days),
active_graphs_last_n_days: sqlCountToNumber(row.active_graphs_last_n_days),
total_users: sqlCountToNumber(row.total_users),
total_graphs: sqlCountToNumber(row.total_graphs),
users_created_today: sqlCountToNumber(row.users_created_today),
graphs_created_today: sqlCountToNumber(row.graphs_created_today),
today_utc: typeof row.today_utc === "string" ? row.today_utc : null,
};
}
function printUsageStatsTable(stats) {
console.table([stats]);
}
function main() {
const options = parseCliArgs(process.argv.slice(2));
const sql = buildUsageStatsSql(options.days);
const wranglerArgs = buildWranglerArgs({
database: options.database,
config: options.config,
env: options.env,
sql,
});
const rows = parseWranglerResults(runWranglerQuery(wranglerArgs));
const stats = formatUsageStats(rows);
if (!stats) {
fail("No stats returned from D1.");
}
if (options.json) {
console.log(JSON.stringify(stats, null, 2));
} else {
printUsageStatsTable(stats);
}
}
if (require.main === module) {
try {
main();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (/no such table: daily_active_entities/i.test(message)) {
fail("Missing daily_active_entities. Apply worker migration 0006_add_daily_active_entities.sql.");
}
if (/no such column: created_at/i.test(message)) {
fail("Missing users.created_at. Apply worker migration 0005_add_user_created_at.sql.");
}
fail(message);
}
}
module.exports = {
buildUsageStatsSql,
formatUsageStats,
parseCliArgs,
};

32
deps/db-sync/worker/scripts/ws_test.js vendored Normal file
View File

@@ -0,0 +1,32 @@
const baseUrl = process.env.BASE_URL || "http://127.0.0.1:8787";
const graphId = process.env.GRAPH_ID || "dev-graph";
const wsUrl = baseUrl.replace(/^http/, "ws") + `/sync/${graphId}`;
const WebSocketCtor = globalThis.WebSocket;
if (!WebSocketCtor) {
console.error("WebSocket not available. Use node>=20 or provide a WebSocket polyfill.");
process.exit(1);
}
const ws = new WebSocketCtor(wsUrl);
let pending = 0;
ws.addEventListener("open", () => {
ws.send(JSON.stringify({ type: "hello", client: "ws-test" }));
ws.send(JSON.stringify({ type: "ping" }));
ws.send(JSON.stringify({ type: "pull", since: 0 }));
pending = 3;
});
ws.addEventListener("message", (event) => {
console.log(String(event.data));
pending -= 1;
if (pending <= 0) {
ws.close();
}
});
ws.addEventListener("error", (event) => {
console.error("ws error", event);
});