diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8571456..3f8d145 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "24" - name: Cache node_modules uses: actions/cache@v4 @@ -33,9 +33,9 @@ jobs: ~/.nvm ~/work/better-sqlite3-wrapper/better-sqlite3-wrapper/node_modules ~/work/better-sqlite3-wrapper/better-sqlite3-wrapper/package-lock.json - key: ${{ runner.os }}-node_modules-cache-v4-${{ hashFiles('**/package-lock.json') }} + key: ${{ runner.os }}-24-node_modules-cache-v4-${{ hashFiles('package.json') }} restore-keys: | - ${{ runner.os }}-node_modules-cache-v4- + ${{ runner.os }}-24-node_modules-cache-v4- - name: Extract Tag Name run: echo "TAG_NAME=$(echo ${GITHUB_REF##*/})" >> $GITHUB_ENV diff --git a/.github/workflows/ci_bun.yml b/.github/workflows/ci_bun.yml index ad72331..d0c4eb1 100644 --- a/.github/workflows/ci_bun.yml +++ b/.github/workflows/ci_bun.yml @@ -11,6 +11,7 @@ on: jobs: build: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Git checkout uses: actions/checkout@v4 diff --git a/.github/workflows/ci_matrix.yml b/.github/workflows/ci_matrix.yml new file mode 100644 index 0000000..b2759e1 --- /dev/null +++ b/.github/workflows/ci_matrix.yml @@ -0,0 +1,66 @@ +name: Node Matrix CI + +on: + push: + paths-ignore: + - "*.md" + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + strategy: + matrix: + os: + - ubuntu-latest + - macos-latest + - windows-2019 + node: + - 16 + - 18 + - 20 + - 22 + name: Testing Node ${{ matrix.node }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + steps: + - name: Git checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + + - name: Cache node_modules on non-windows os + if: ${{ !startsWith(matrix.os, 'windows') }} + uses: actions/cache@v4 + with: + path: | + ~/.npm + ~/.nvm + ~/work/better-sqlite3-wrapper/better-sqlite3-wrapper/node_modules + ~/work/better-sqlite3-wrapper/better-sqlite3-wrapper/package-lock.json + key: ${{ runner.os }}-${{ matrix.node }}-node_modules-cache-v1-${{ hashFiles('package.json') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.node }}-node_modules-cache-v1- + + - name: Cache node_modules on windows os + if: ${{ startsWith(matrix.os, 'windows') }} + uses: actions/cache@v4 + with: + path: | + ~/AppData/Roaming/npm + D:/a/better-sqlite3-wrapper/better-sqlite3-wrapper/node_modules + D:/a/better-sqlite3-wrapper/better-sqlite3-wrapper/package-lock.json + key: ${{ runner.os }}-${{ matrix.node }}-node_modules-cache-v1-${{ hashFiles('package.json') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.node }}-node_modules-cache-v1- + + - name: Run tests + run: | + npm i + node ./index.test.js diff --git a/README.md b/README.md index bc15aad..e2ac3bf 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,27 @@ - [![CI](https://github.com/farjs/better-sqlite3-wrapper/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/farjs/better-sqlite3-wrapper/actions/workflows/ci.yml?query=workflow%3Aci+branch%3Amain) [![npm version](https://img.shields.io/npm/v/@farjs/better-sqlite3-wrapper)](https://www.npmjs.com/package/@farjs/better-sqlite3-wrapper) ## @farjs/better-sqlite3-wrapper -Thin api wrapper around [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) -and [bun:sqlite](https://bun.sh/docs/api/sqlite) to allow cross- runtime/engine usage. +Thin api wrapper around: + +- [better-sqlite3](https://github.com/WiseLibs/better-sqlite3) on node version < 22 +- [node:sqlite](https://nodejs.org/docs/latest-v22.x/api/sqlite.html) on node version >= 22 +- and [bun:sqlite](https://bun.sh/docs/api/sqlite) + +to allow cross- runtime/engine usage. It exposes only [a subset](./index.d.ts) of `better-sqlite3` api because not everything -is available in `bun:sqlite`. +is available in `bun:sqlite`/`node:sqlite`. For example, since `run` returns `undefined` you can use the following queries to get - `changes` and/or `lastInsertRowid`: +`changes` and/or `lastInsertRowid`: ```javascript const changes = db.prepare("SELECT changes() AS changes;").get().changes; -const lastInsertRowId = db.prepare("SELECT last_insert_rowid() AS id;").get().id; +const lastInsertRowId = db + .prepare("SELECT last_insert_rowid() AS id;") + .get().id; ``` Also, since `db.pragma` is not available in `bun:sqlite`, you can run: diff --git a/index.js b/index.js index 3d72a37..c647ddd 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,70 @@ const wrapper = function () { return require("bun:sqlite").Database; } - return require("better-sqlite3"); + if (isModuleAvailable("better-sqlite3")) { + return require("better-sqlite3"); + } + + const { DatabaseSync } = require("node:sqlite"); + function dbCtor(path, options) { + if (options) { + return nodeDbWrapper(new DatabaseSync(path, options)); + } + + return nodeDbWrapper(new DatabaseSync(path)); + } + return dbCtor; }; +function isModuleAvailable(name) { + try { + require.resolve(name); + return true; + } catch {} + + return false; +} + +function nodeDbWrapper(db) { + //check if node supports it + //see: https://github.com/nodejs/node/issues/57431 + if (db.transaction) { + return db; + } + + //see: https://github.com/WiseLibs/better-sqlite3/blob/master/docs/api.md#transactionfunction---function + db.transaction = (f) => { + const doTx = (...args) => { + try { + const res = f(...args); + db.prepare("COMMIT").run(); + return res; + } catch (error) { + db.prepare("ROLLBACK").run(); + throw error; + } + }; + + const txFn = (...args) => { + db.prepare("BEGIN").run(); + return doTx(...args); + }; + txFn.deferred = (...args) => { + db.prepare("BEGIN DEFERRED").run(); + return doTx(...args); + }; + txFn.immediate = (...args) => { + db.prepare("BEGIN IMMEDIATE").run(); + return doTx(...args); + }; + txFn.exclusive = (...args) => { + db.prepare("BEGIN EXCLUSIVE").run(); + return doTx(...args); + }; + + return txFn; + }; + return db; +} + module.exports = wrapper(); diff --git a/index.test.js b/index.test.js index eca6ea3..3d71270 100644 --- a/index.test.js +++ b/index.test.js @@ -14,12 +14,17 @@ it("should open in-memory database", () => { const query = db.prepare("select 'Hello world' as message;"); //then - assert.deepEqual(query.get(), { message: "Hello world" }); + assert.deepEqual({ ...query.get() }, { message: "Hello world" }); //when & then const results = query.all(); assert.deepEqual(Array.isArray(results), true); - assert.deepEqual(results, [{ message: "Hello world" }]); + assert.deepEqual( + results.map((r) => { + return { ...r }; + }), + [{ message: "Hello world" }] + ); //when & then db.close(); @@ -62,19 +67,73 @@ it("should create file-based database", () => { const results = db.prepare("select * from test order by id;").all(); //then - assert.deepEqual(results, [ - { - id: 1, - name: "test1", - }, - { - id: 2, - name: "test2", - }, - ]); + assert.deepEqual( + results.map((r) => { + return { ...r }; + }), + [ + { + id: 1, + name: "test1", + }, + { + id: 2, + name: "test2", + }, + ] + ); //cleanup db.close(); fs.unlinkSync(file); fs.rmdirSync(tmpDir); }); + +it("should rollback failed transaction", () => { + //when + const db = new Database(":memory:"); + const changesQuery = db.prepare("SELECT changes() AS changes;"); + const lastInsertRowIdQuery = db.prepare("SELECT last_insert_rowid() AS id;"); + db.prepare( + ` + create table test( + id integer primary key, + name text not null + ); + ` + ).run(); + assert.deepEqual(changesQuery.get().changes, 0); + assert.deepEqual(lastInsertRowIdQuery.get().id, 0); + + const error = Error("test error"); + const txFn = db.transaction((name) => { + const insert = db.prepare("insert into test (name) values (?);"); + insert.run(name); + throw error; + }); + let caughtError = null; + + //when + try { + txFn("test1"); + } catch (err) { + caughtError = err; + } + + //then + assert.deepEqual(caughtError === error, true); + assert.deepEqual(changesQuery.get().changes, 1); + assert.deepEqual(lastInsertRowIdQuery.get().id, 1); + + //when & then + const query = db.prepare("select id, name from test;"); + assert.deepEqual( + query.all().map((r) => { + return { ...r }; + }), + [] + ); + + //cleanup + db.close(); +}); diff --git a/package.json b/package.json index d8fe301..472fb58 100644 --- a/package.json +++ b/package.json @@ -38,11 +38,11 @@ }, "browserslist": "maintained node versions", "engines": { - "node": ">=18", + "node": ">=16", "bun": ">=0.8" }, "optionalDependencies": { - "better-sqlite3": "^11.5.0" + "better-sqlite3": "8.7.0" }, "devDependencies": { "prettier": "^2.8.8",