src/node.ts
98 lines and 26 functions in DrDustinEdwards/d1-dump, at commit e15f966 on 2026-10-23. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| directoryBucket | 12 to 40 | 1 | 100% | OkCRAP 1 | Ok100% |
| readOnly | 13 to 15 | 1 | 100% | OkCRAP 1 | Ok100% |
| keysUnder | 16 to 24 | 3 | 100% | OkCRAP 3 | Ok80% |
| <anonymous:18> | 18 to 18 | 1 | 50% | OkCRAP 1.1 | Watch60% |
| get | 26 to 34 | 3 | 100% | OkCRAP 3 | Ok100% |
| <anonymous:29> | 29 to 32 | 2 | 75% | OkCRAP 2.1 | Watch66.7% |
| text | 33 to 33 | 1 | 100% | OkCRAP 1 | Ok100% |
| list | 35 to 35 | 1 | 100% | OkCRAP 1 | Watch60% |
| <anonymous:35> | 35 to 35 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
| <anonymous:35> | 35 to 35 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
| pickBackend | 46 to 51 | 10 | 100% | WatchCRAP 10 | Ok100% |
| sqliteScratch | 58 to 104 | 1 | 100% | OkCRAP 1 | Ok100% |
| <anonymous:59> | 59 to 103 | 2 | 100% | OkCRAP 2 | Ok100% |
| sqlValues | 66 to 67 | 1 | 100% | OkCRAP 1 | Ok100% |
| <anonymous:67> | 67 to 67 | 2 | 100% | OkCRAP 2 | Ok100% |
| rowsOf | 68 to 72 | 1 | 100% | OkCRAP 1 | Ok100% |
| <anonymous:72> | 72 to 72 | 1 | 100% | OkCRAP 1 | Ok100% |
| statement | 74 to 79 | 1 | 100% | OkCRAP 1 | Ok100% |
| bind | 75 to 75 | 1 | 100% | OkCRAP 1 | Ok100% |
| <anonymous:76> | 76 to 76 | 1 | 100% | OkCRAP 1 | No data: no mutants in this code |
| run | 77 to 77 | 1 | 100% | OkCRAP 1 | Ok100% |
| exec | 78 to 78 | 1 | 100% | OkCRAP 1 | Ok100% |
| prepare | 81 to 81 | 1 | 100% | OkCRAP 1 | Ok100% |
| batch | 83 to 93 | 2 | 100% | OkCRAP 2 | Ok100% |
| <anonymous:86> | 86 to 86 | 1 | 100% | OkCRAP 1 | Ok100% |
| delete | 97 to 101 | 2 | 75% | OkCRAP 2.1 | Critical42.9% |
Imports
It imports src/drill.ts, src/types.ts. Imported by src/cli.ts.
Source
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { readFile, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, relative, sep } from "node:path";
import { DatabaseSync, type SQLInputValue } from "node:sqlite";
import { SCRATCH_PREFIX, type Scratch } from "./drill.js";
import type { D1Like, D1Statement, R2Like } from "./types.js";
// A downloaded dump as an R2Like, read only, for a drill run from Node. `root` holds the
// keys as paths: <root>/backups/json/<run id>/<table>.json. Not exported from the package
// entry, which has to stay free of node: imports so a Worker can bundle it.
export function directoryBucket(root: string): R2Like {
const readOnly = async (): Promise<never> => {
throw new Error("d1-dump: a directory bucket is read only");
};
const keysUnder = async (dir: string): Promise<string[]> => {
const out: string[] = [];
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
const path = join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await keysUnder(path)));
else out.push(relative(root, path).split(sep).join("/"));
}
return out;
};
return {
get: async (key) => {
const path = join(root, ...key.split("/"));
if (relative(root, path).startsWith("..")) throw new Error(`d1-dump: ${key} is outside ${root}`);
const text = await readFile(path, "utf8").catch((err: NodeJS.ErrnoException) => {
if (err.code === "ENOENT") return null;
throw err;
});
return text === null ? null : { text: async () => text };
},
list: async ({ prefix = "" }) => ({ objects: (await keysUnder(root)).filter((k) => k.startsWith(prefix)).sort().map((key) => ({ key })), truncated: false }),
put: readOnly,
delete: readOnly,
createMultipartUpload: readOnly,
};
}
type Backend = "sqlite" | "d1";
// sqlite unless Cloudflare credentials are set; either can be asked for. Says why, so the
// drill's output shows which one ran. asking for d1 without credentials is an error.
export function pickBackend(asked: string | undefined, credentials: boolean): { backend: Backend; why: string } | { error: string } {
if (asked !== undefined && asked !== "sqlite" && asked !== "d1") return { error: `--backend is sqlite or d1, not ${JSON.stringify(asked)}` };
const backend: Backend = asked ?? (credentials ? "d1" : "sqlite");
if (backend === "d1" && !credentials) return { error: "--backend d1 needs CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN in the environment" };
return { backend, why: asked ? "asked for" : credentials ? "Cloudflare credentials are set" : "no Cloudflare credentials are set" };
}
// A scratch database in a local SQLite file, for a drill that must hold no Cloudflare
// credential. node:sqlite is built into Node (unflagged since 22.13; this package
// already needs 24.14.1) and carries FTS5. The file lives in a directory of its own,
// named restore-drill-<date>-<pid>, and delete() closes it and removes the directory,
// then checks it is gone.
export function sqliteScratch(): (name: string) => Promise<Scratch> {
return async (name) => {
if (!name.startsWith(SCRATCH_PREFIX)) throw new Error(`d1-dump: refusing to create ${JSON.stringify(name)}: a scratch database starts with ${SCRATCH_PREFIX}`);
const dir = mkdtempSync(join(tmpdir(), `${name}-`));
const file = join(dir, `${name}.sqlite`);
const sqlite = new DatabaseSync(file);
// D1 binds a BLOB as an array of byte values and refuses undefined; SQLite takes bytes.
const sqlValues = (values: unknown[]): SQLInputValue[] =>
values.map((v) => (Array.isArray(v) ? Uint8Array.from(v as number[]) : (v as SQLInputValue)));
const rowsOf = (sql: string, values: unknown[]) =>
sqlite
.prepare(sql)
.all(...sqlValues(values))
.map((r) => ({ ...r }));
const statement = (sql: string, values: unknown[] = []): D1Statement & { exec(): { results: unknown[] } } => ({
bind: (...next) => statement(sql, next),
all: (async <T>() => ({ results: rowsOf(sql, values) as T[] })) as D1Statement["all"],
run: async () => sqlite.prepare(sql).run(...sqlValues(values)),
exec: () => ({ results: rowsOf(sql, values) }),
});
const db: D1Like = {
prepare: (sql) => statement(sql),
// One batch is one transaction, as D1's is: all of it lands or none of it does.
batch: async <T>(statements: D1Statement[]) => {
sqlite.exec("BEGIN");
try {
const out = statements.map((s) => (s as ReturnType<typeof statement>).exec() as { results: T[] });
sqlite.exec("COMMIT");
return out;
} catch (err) {
sqlite.exec("ROLLBACK");
throw err;
}
},
};
return {
db,
delete: async () => {
sqlite.close();
rmSync(dir, { recursive: true, force: true });
if (existsSync(dir)) throw new Error(`${dir} is still there after the delete`);
},
};
};
}