summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEuAndreh <eu@euandre.org>2024-02-23 11:11:38 -0300
committerEuAndreh <eu@euandre.org>2024-02-23 11:21:21 -0300
commitcd50a23dab8623f232da7d6b1b9511f4590e9788 (patch)
tree349e111763eac7853d9f1dd7a61e9bce117f3993
parentBig cleanup (diff)
downloadpapod-cd50a23dab8623f232da7d6b1b9511f4590e9788.tar.gz
papod-cd50a23dab8623f232da7d6b1b9511f4590e9788.tar.xz
Implement accretion.runMigrations() and wrappings of node-sqlite3
-rw-r--r--Makefile1
-rw-r--r--deps.mk2
-rw-r--r--src/accretion.mjs43
-rw-r--r--src/db.mjs87
-rw-r--r--src/sqlite.cjs1
-rw-r--r--tests/js/accretion.mjs35
-rw-r--r--tests/js/db.mjs126
-rw-r--r--tests/js/utils.mjs45
8 files changed, 298 insertions, 42 deletions
diff --git a/Makefile b/Makefile
index 1dbf13e..e19a81e 100644
--- a/Makefile
+++ b/Makefile
@@ -47,6 +47,7 @@ sources = \
installable = \
$(sources.sql) \
$(sources.mjs) \
+ src/sqlite.cjs \
src/package.json \
diff --git a/deps.mk b/deps.mk
index 076ad1a..320dc3d 100644
--- a/deps.mk
+++ b/deps.mk
@@ -1,4 +1,5 @@
sources.mjs = \
+ src/accretion.mjs \
src/api.mjs \
src/bin.mjs \
src/db.mjs \
@@ -9,6 +10,7 @@ sources.mjs = \
src/web.mjs \
tests.mjs = \
+ tests/js/accretion.mjs \
tests/js/db.mjs \
tests/js/escape.mjs \
tests/js/hero.mjs \
diff --git a/src/accretion.mjs b/src/accretion.mjs
new file mode 100644
index 0000000..117e291
--- /dev/null
+++ b/src/accretion.mjs
@@ -0,0 +1,43 @@
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+import url from "node:url";
+
+import sqlite from "./sqlite.cjs";
+
+import { difference, log } from "./utils.mjs";
+
+
+const DIRNAME = path.dirname(url.fileURLToPath(import.meta.url));
+const MIGRATIONS_DIR = DIRNAME + "/sql/migrations/";
+
+export const runMigrations = async db => {
+ assert(db);
+
+ await db.exec(`
+ BEGIN TRANSACTION;
+ CREATE TABLE IF NOT EXISTS migrations (
+ filename TEXT NOT NULL PRIMARY KEY
+ );
+ COMMIT TRANSACTION;
+ `);
+
+ const done = [...(await db.all("SELECT filename FROM migrations;"))]
+ .map(row => row.filename);
+ const allFiles = fs.readdirSync(MIGRATIONS_DIR, "UTF-8");
+ const pending = difference(new Set(allFiles), new Set(done));
+ const sortedPending = [...pending]
+ .sort((a, b) => a.localeCompare(b, "POSIX"));
+
+ for (const filename of sortedPending) {
+ log({ log: "exec-migration", filename });
+ const sql = fs.readFileSync(MIGRATIONS_DIR + filename, "UTF-8");
+ await db.exec("BEGIN TRANSACTION;");
+ await db.exec(sql);
+ await db.run(
+ "INSERT INTO migrations (filename) VALUES ($filename);",
+ { $filename: filename },
+ );
+ await db.exec("COMMIT TRANSACTION;")
+ }
+};
diff --git a/src/db.mjs b/src/db.mjs
index 59d6a0e..fdfd899 100644
--- a/src/db.mjs
+++ b/src/db.mjs
@@ -3,47 +3,56 @@ import fs from "node:fs";
import path from "node:path";
import url from "node:url";
-import { difference, log } from "./utils.mjs";
+import sqlite from "./sqlite.cjs";
+import { runMigrations } from "./accretion.mjs";
-const DIRNAME = path.dirname(url.fileURLToPath(import.meta.url));
-const MIGRATIONS_DIR = DIRNAME + "/sql/migrations/";
-const MIGRATION_FILENAMES = fs.readdirSync(MIGRATIONS_DIR, "UTF-8");
-let db = null;
+export const promisify = nativeFn => (...args) =>
+ new Promise((resolve, reject) =>
+ nativeFn(...args, (err, data) => err ? reject(err) : resolve(data)));
+
+export const promisifyDb = dbHandle => ({
+ ref: dbHandle,
+ all: promisify((...args) => dbHandle.all(...args)),
+ exec: promisify((...args) => dbHandle.exec(...args)),
+ run: promisify((...args) => dbHandle.run(...args)),
+ each: (...args) =>
+ new Promise((resolve, reject) => {
+ const cb = args[args.length - 1];
+ dbHandle.each(
+ ...args.slice(0, args.length - 1),
+ (err, row) => err ? reject(err) : cb(row),
+ (err, data) => err ? reject(err) : resolve(data),
+ );
+ }),
+ prepare: (...args) =>
+ new Promise((resolve, reject) => {
+ const mkStmt = stmtRef => ({
+ ref: stmtRef,
+ run: promisify((...args) => stmtRef.run(...args)),
+ finalize: promisify((...args) => stmtRef.finalize(...args)),
+ });
+ const ref = dbHandle.prepare(
+ ...args,
+ err => err ? reject(err) : resolve(mkStmt(ref)),
+ );
+ }),
+});
+
+export const open = (...args) =>
+ new Promise((resolve, reject) => {
+ const dbName = args[0];
+ assert(dbName);
+ const ref = promisifyDb(new sqlite.Database(
+ ...args,
+ err => err ? reject(err) : resolve(ref),
+ ));
+ });
+
+export let db = null;
+
export const init = async (dbName = process.env.PAPO_DB_PATH || ":memory:") => {
- assert(dbName);
-
- db = await sqlite.open(dbName);
- await db.exec(`
- BEGIN TRANSACTION;
- CREATE TABLE IF NOT EXISTS migrations (
- filename TEXT PRIMARY KEY
- );
- COMMIT TRANSACTION;
- `);
-
- const done = await db.all(`
- SELECT filename FROM migrations;
- `).map(row => row.filename);
- const pending = difference(
- new Set(MIGRATION_FILENAMES),
- new Set(done)
- );
- const sortedPending = [...pending].sort((a, b) => a.localeCompare(b));
-
- await db.exec("BEGIN TRANSACTION;");
- for (const filename of sortedPending) {
- log({
- log: "exec-migration",
- filename,
- });
- const sql = fs.readFileSync(MIGRATIONS_DIR + filename, "UTF-8");
- await db.exec(sql);
- await db.run(
- `INSERT INTO migrations (filename) VALUES ($filename);`,
- { filename },
- );
- }
- await db.exec("COMMIT TRANSACTION;");
+ db = await open(dbName);
+ await runMigrations(db);
};
diff --git a/src/sqlite.cjs b/src/sqlite.cjs
new file mode 100644
index 0000000..a1b8ab7
--- /dev/null
+++ b/src/sqlite.cjs
@@ -0,0 +1 @@
+module.exports = require("sqlite3");
diff --git a/tests/js/accretion.mjs b/tests/js/accretion.mjs
new file mode 100644
index 0000000..3ea90c3
--- /dev/null
+++ b/tests/js/accretion.mjs
@@ -0,0 +1,35 @@
+import assert from "node:assert/strict";
+
+import sqlite from "../../src/sqlite.cjs";
+
+import { runTests } from "../runner.mjs";
+import { promisifyDb, open } from "../../src/db.mjs";
+import { runMigrations } from "../../src/accretion.mjs";
+
+
+const test_runMigrations = t => {
+ t.start("runMigrations()");
+
+ t.test("running twice is a noop", async () => {
+ const db = await open(":memory:");
+ const migrationsFn = () => db.all("SELECT filename FROM migrations;");
+
+ assert.rejects(
+ migrationsFn,
+ { message: "SQLITE_ERROR: no such table: migrations" },
+ );
+
+ await runMigrations(db);
+ const filled = await migrationsFn();
+
+ await runMigrations(db);
+ const unchanged = await migrationsFn();
+
+ assert.deepEqual(filled, unchanged);
+ });
+};
+
+
+await runTests([
+ test_runMigrations,
+]);
diff --git a/tests/js/db.mjs b/tests/js/db.mjs
index e4a8a51..372f64a 100644
--- a/tests/js/db.mjs
+++ b/tests/js/db.mjs
@@ -1,15 +1,135 @@
+import assert from "node:assert/strict";
+
+import sqlite from "../../src/sqlite.cjs";
+
import { runTests } from "../runner.mjs";
-import { init } from "../../src/db.mjs";
+import { promisify, promisifyDb, open, db, init } from "../../src/db.mjs";
+
+
+const test_promisify = t => {
+ t.start("promisify()");
+
+ t.test("we wrap the callbacky function", async () => {
+ const okFn1 = (a, b, cb) =>
+ setTimeout(() => cb(null, { a, b, ok: true }));
+ const errFn1 = (a, b, c, cb) =>
+ setTimeout(() => cb({ err: true, a }, "ignored"));
+
+ const okFn2 = promisify(okFn1);
+ const errFn2 = promisify(errFn1);
+
+ assert.deepEqual(
+ await okFn2("a-value", "b-value"),
+ {
+ a: "a-value",
+ b: "b-value",
+ ok: true,
+ },
+ );
+
+ assert.rejects(
+ async () => await errFn2("aa", "bb", "cc"),
+ {
+ err: true,
+ a: "aa",
+ },
+ );
+ });
+};
+const test_promisifyDb = t => {
+ t.start("promisifyDb()");
+
+ const createTable = "CREATE TABLE table_ (column INTEGER NOT NULL PRIMARY KEY);";
+
+ t.test("we can access the underlying database and statement ref", async () => {
+ const db = await open(":memory:");
+ assert.ok(db.ref instanceof sqlite.Database);
+
+ const stmt = await db.prepare("SELECT 1;");
+ assert.ok(stmt.ref instanceof sqlite.Statement);
+ });
+
+ t.test("we can run the wrapped fns", async () => {
+ const db = await open(":memory:");
+
+ await db.exec(createTable);
+ const stmt = await db.prepare("INSERT INTO table_ (column) VALUES ($column)");
+ await stmt.run({ $column: 3 });
+ await stmt.run({ $column: 1 });
+ await stmt.finalize();
+ assert.rejects(
+ async () => await stmt.run({ $column: 2 }),
+ {
+ message: "SQLITE_MISUSE: Statement is already finalized",
+ code: "SQLITE_MISUSE",
+ },
+ );
+
+ const selectAll = "SELECT column FROM table_ ORDER BY column;";
+
+ const all = await db.all(selectAll);
+ assert.deepEqual(all, [{ column: 1 }, { column: 3 }]);
+
+ const contents = [];
+ const cb = row => contents.push(row);
+ await db.each(selectAll, cb);
+ assert.deepEqual(contents, [{ column: 1 }, { column: 3 }]);
+ });
+};
+
+const test_open = t => {
+ t.start("open()");
+
+ t.test("we must provide a name", () => {
+ assert.rejects(
+ async () => await open(),
+ assert.AssertionError,
+ );
+
+ assert.rejects(
+ async () => await open(undefined),
+ assert.AssertionError,
+ );
+
+ assert.rejects(
+ async () => await open(null),
+ assert.AssertionError,
+ );
+
+ assert.rejects(
+ async () => await open(""),
+ assert.AssertionError,
+ );
+ });
+
+ t.test("failure to open causes a promise rejection", () => {
+ assert.rejects(
+ async () => await open("tests/non/existing/directory/and/file"),
+ {
+ message: "SQLITE_CANTOPEN: unable to open database file",
+ code: "SQLITE_CANTOPEN",
+ },
+ );
+ });
+};
const test_init = t => {
t.start("init()");
- t.test("FIXME", () => {
- // init();
+ t.test("we only know how to deal with 1 database", async () => {
+ await init();
+ const ref1 = db;
+ await init();
+ const ref2 = db;
+
+ assert.notDeepEqual(ref1, ref2);
});
};
await runTests([
+ test_promisify,
+ test_promisifyDb,
+ test_open,
test_init,
]);
diff --git a/tests/js/utils.mjs b/tests/js/utils.mjs
index 1875ce5..94e8eae 100644
--- a/tests/js/utils.mjs
+++ b/tests/js/utils.mjs
@@ -4,6 +4,7 @@ import { runTests } from "../runner.mjs";
import {
eq,
keys,
+ difference,
assocIn,
getIn,
first,
@@ -126,6 +127,49 @@ const test_keys = t => {
});
};
+const test_difference = t => {
+ t.start("difference()");
+
+ t.test("empty values", () => {
+ assert.deepEqual(
+ difference(new Set(), new Set()),
+ new Set(),
+ );
+
+ assert.deepEqual(
+ difference(new Set(), new Set([1, 2])),
+ new Set(),
+ );
+
+ assert.deepEqual(
+ difference(new Set([1, 2]), new Set()),
+ new Set([1, 2]),
+ );
+ });
+
+ t.test("different subsets", () => {
+ assert.deepEqual(
+ difference(new Set([1, 2]), new Set([3, 4])),
+ new Set([1, 2]),
+ );
+
+ assert.deepEqual(
+ difference(new Set([1, 2, 3]), new Set([2, 4, 5])),
+ new Set([1, 3]),
+ );
+
+ assert.deepEqual(
+ difference(new Set([1]), new Set([1, 2, 3, 4, 5])),
+ new Set(),
+ );
+
+ assert.deepEqual(
+ difference(new Set([1, 2, 3]), new Set([1, 2, 3])),
+ new Set(),
+ );
+ });
+};
+
const test_assocIn = t => {
t.start("assocIn()");
@@ -218,6 +262,7 @@ const test_log = t => {
await runTests([
test_eq,
test_keys,
+ test_difference,
test_assocIn,
test_getIn,
test_first,