diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..7d298c5 --- /dev/null +++ b/.npmignore @@ -0,0 +1,109 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port +.vscode/ +package-lock.json +test/ +.github/ +.eslintrc diff --git a/README.md b/README.md index 2520b01..c0de975 100644 --- a/README.md +++ b/README.md @@ -40,14 +40,17 @@ main() Checkout the [sqlite3 documentation](https://github.com/TryGhost/node-sqlite3/wiki/API) to see all the available methods. -_Note that Promise is not supported by the `sqlite3` module._ +Note that Promise APIs are not supported by the `sqlite3` module by default. +By using the `promiseApi` option, the [`sqlite`](https://github.com/kriasoft/node-sqlite) wrapper will be used +to enhance the Database instance. It has many convenient utilities such as `migration` support. ## Options You can pass the following options to the plugin: ```js -app.register(require('fastify-sqlite'), { +await app.register(require('fastify-sqlite'), { + promiseApi: true, // the DB instance supports the Promise API. Default false name: 'mydb', // optional decorator name. Default null verbose: true, // log sqlite3 queries as trace. Default false dbFile: ':memory:', // select the database file. Default ':memory:' @@ -56,9 +59,7 @@ app.register(require('fastify-sqlite'), { }) // usage WITH name option -app.sqlite.myDb.all('SELECT * FROM myTable', (err, rows) => { - // do something -}) +await app.sqlite.myDb.all('SELECT * FROM myTable') ``` ## License diff --git a/index.js b/index.js index 407f8ca..1f7a226 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,7 @@ const fp = require('fastify-plugin') const sqlite3 = require('sqlite3') +const { open } = require('sqlite') function fastifySqlite (fastify, opts, next) { const Sqlite = (opts.verbose === true) @@ -11,10 +12,22 @@ function fastifySqlite (fastify, opts, next) { const filename = opts.dbFile || ':memory:' const mode = opts.mode || (sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE | sqlite3.OPEN_FULLMUTEX) - const db = new Sqlite.Database(filename, mode, (err) => { - if (err) { + if (opts.promiseApi === true) { + open({ + filename, + mode, + driver: Sqlite.Database + }).then(setupDatabase, setupDatabase) + } else { + // eslint-disable-next-line + new Sqlite.Database(filename, mode, setupDatabase) + } + + function setupDatabase (err) { + if (err && err instanceof Error) { return next(err) } + const db = err || this if (opts.verbose === true) { db.on('trace', function (trace) { @@ -23,7 +36,7 @@ function fastifySqlite (fastify, opts, next) { } decorateFastifyInstance(fastify, db, opts, next) - }) + } } function decorateFastifyInstance (fastify, db, opts, next) { @@ -52,7 +65,10 @@ function decorateFastifyInstance (fastify, db, opts, next) { } function close (instance, done) { - this.close(done) + const isProm = this.close(done) + if (isProm?.then) { + isProm.then(done, done) + } } module.exports = fp(fastifySqlite, { diff --git a/package.json b/package.json index 5b9aa20..7c610ec 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "fastify-plugin": "^4.2.1", + "sqlite": "^4.1.2", "sqlite3": "^5.0.11" } } diff --git a/test/migrations/001-init.sql b/test/migrations/001-init.sql new file mode 100644 index 0000000..cbe16f2 --- /dev/null +++ b/test/migrations/001-init.sql @@ -0,0 +1,35 @@ +-------------------------------------------------------------------------------- +-- Up +-------------------------------------------------------------------------------- + +CREATE TABLE Family ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL +); + +CREATE TABLE Person ( + id INTEGER PRIMARY KEY, + familyId INTEGER NOT NULL, + name TEXT NOT NULL, + nick TEXT, + FOREIGN KEY(familyId) REFERENCES Family(id) +); + +CREATE INDEX Person_ix_familyId ON Person (familyId); + +CREATE TABLE Friend ( + personId INTEGER NOT NULL, + friendId INTEGER NOT NULL, + isTheBest NUMERIC NOT NULL DEFAULT 0, + PRIMARY KEY (personId, friendId), + CONSTRAINT Friend_ck_isTheBest CHECK (isTheBest IN (0, 1)) +); + +-------------------------------------------------------------------------------- +-- Down +-------------------------------------------------------------------------------- + +DROP INDEX Person_ix_familyId; +DROP TABLE Friend; +DROP TABLE Person; +DROP TABLE Family; \ No newline at end of file diff --git a/test/migrations/002-data.sql b/test/migrations/002-data.sql new file mode 100644 index 0000000..27e194d --- /dev/null +++ b/test/migrations/002-data.sql @@ -0,0 +1,26 @@ +-------------------------------------------------------------------------------- +-- Up +-------------------------------------------------------------------------------- +INSERT INTO Family (id, name) VALUES (1, 'Foo'); +INSERT INTO Family (id, name) VALUES (2, 'Bar'); +INSERT INTO Family (id, name) VALUES (3, 'Baz'); + +INSERT INTO Person (id, familyId, name) VALUES (1, 1, 'John'); +INSERT INTO Person (id, familyId, name) VALUES (2, 1, 'Jakie'); +INSERT INTO Person (id, familyId, name) VALUES (3, 1, 'Jessie'); + +INSERT INTO Person (id, familyId, name) VALUES (4, 2, 'Micky'); +INSERT INTO Person (id, familyId, name) VALUES (5, 2, 'Lory'); +INSERT INTO Person (id, familyId, name) VALUES (6, 2, 'Sara'); +INSERT INTO Person (id, familyId, name) VALUES (7, 2, 'Jenny'); + +INSERT INTO Person (id, familyId, name) VALUES (8, 3, 'Brian'); +INSERT INTO Person (id, familyId, name) VALUES (9, 3, 'Brown'); +INSERT INTO Person (id, familyId, name) VALUES (10, 3, 'Fuzzy'); + +INSERT INTO Friend (personId, friendId, isTheBest) VALUES (1, 8, 0); +INSERT INTO Friend (personId, friendId, isTheBest) VALUES (1, 6, 0); +INSERT INTO Friend (personId, friendId, isTheBest) VALUES (1, 9, 1); + +INSERT INTO Friend (personId, friendId, isTheBest) VALUES (2, 4, 0); +INSERT INTO Friend (personId, friendId, isTheBest) VALUES (2, 6, 0); diff --git a/test/promise.test.js b/test/promise.test.js new file mode 100644 index 0000000..518e931 --- /dev/null +++ b/test/promise.test.js @@ -0,0 +1,129 @@ +'use strict' + +const { test } = require('tap') +const fastify = require('fastify') +const plugin = require('../index') + +test('basic test', async t => { + const app = fastify() + app.register(plugin, { promiseApi: true }) + t.teardown(app.close.bind(app)) + + await app.ready() + + t.ok(app.sqlite) + t.ok(app.sqlite.migrate) +}) + +test('promise api', async t => { + const app = fastify() + app.register(plugin, { promiseApi: true }) + t.teardown(app.close.bind(app)) + + await app.ready() + + await app.sqlite.migrate({ + migrationsPath: 'test/migrations' + }) + + const johnFriends = await app.sqlite.all(` + SELECT * FROM Person + JOIN Friend ON Person.id = Friend.personId + JOIN Person AS FriendPerson ON Friend.friendId = FriendPerson.id + WHERE Person.name = 'John' + `) + + t.equal(johnFriends.length, 3) +}) + +test('verbose mode', async t => { + const createSql = 'CREATE TABLE foo (id INT, txt TEXT)' + + let waitLogResolve + const waitLog = new Promise(resolve => { waitLogResolve = resolve }) + + function Logger (...args) { this.args = args } + Logger.prototype.info = function (msg) { t.fail() } + Logger.prototype.error = function (msg) { t.fail() } + Logger.prototype.debug = function (msg) { t.fail() } + Logger.prototype.fatal = function (msg) { t.fail() } + Logger.prototype.warn = function (msg) { t.fail() } + Logger.prototype.trace = function (msg) { + t.same(msg, { sql: createSql }) + waitLogResolve() + } + Logger.prototype.child = function () { return new Logger() } + + const myLogger = new Logger() + + const app = fastify({ + logger: myLogger + }) + app.register(plugin, { + promiseApi: true, + verbose: true + }) + t.teardown(app.close.bind(app)) + + await app.ready() + await app.sqlite.exec(createSql) + t.pass('table created') + await waitLog +}) + +test('multiple register', async t => { + const app = fastify() + app.register(plugin, { promiseApi: true, name: 'db1' }) + app.register(plugin, { promiseApi: true, name: 'db2' }) + t.teardown(app.close.bind(app)) + + await app.ready() + + t.ok(app.sqlite.db1) + t.ok(app.sqlite.db2) +}) + +test('multiple register same name error', async t => { + const app = fastify() + app.register(plugin, { promiseApi: true, name: 'db1' }) + app.register(plugin, { promiseApi: true, name: 'db1' }) + t.teardown(app.close.bind(app)) + + try { + await app.ready() + t.fail('should throw') + } catch (error) { + t.match(error.message, 'Connection name [db1] already registered') + } +}) + +test('multiple register error', async t => { + const app = fastify() + app.register(plugin, { promiseApi: true }) + app.register(plugin, { promiseApi: true }) + t.teardown(app.close.bind(app)) + + try { + await app.ready() + t.fail('should throw') + } catch (error) { + t.match(error.message, 'fastify-sqlite has been already registered') + } +}) + +test('sql connection error', async t => { + const app = fastify() + app.register(plugin, { + promiseApi: true, + dbFile: 'foobar.db', + mode: plugin.sqlite3.OPEN_READONLY + }) + t.teardown(app.close.bind(app)) + + try { + await app.ready() + t.fail('should throw') + } catch (error) { + t.match(error.message, 'SQLITE_CANTOPEN: unable to open database file') + } +})