diff --git a/package.json b/package.json index 6f04fe1..d48ce7e 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dependencies": { "component-type": "^1.2.1", "fs-jetpack": "^0.10.5", + "generic-pool": "^3.1.6", "knex": "^0.12.6", "osom": "^2.1.2", "sql.js": "^0.4.0" diff --git a/src/helpers.js b/src/helpers.js index afb1eae..c9cb74d 100644 --- a/src/helpers.js +++ b/src/helpers.js @@ -87,21 +87,24 @@ export function runQuery (instance, query, needResponse) { return query.then(res => res ? res.length : 0) } - let response - - if (needResponse) { - response = parseResponse(instance.db.exec(query.toString())) - if (query._sequence && query._sequence[0].method === 'hasTable') { - response = !!response.length + return instance.pool.acquire().then(db => { + let response + + if (needResponse) { + response = parseResponse(db.exec(query.toString())) + if (query._sequence && query._sequence[0].method === 'hasTable') { + response = !!response.length + } + } else { + db.run(query.toString()) + + if (util.isOneOf(['insert', 'update', 'delete'], query._method)) { + response = db.getRowsModified() + } } - } else { - instance.db.run(query.toString()) - - if (util.isOneOf(['insert', 'update', 'delete'], query._method)) { - response = instance.db.getRowsModified() - } - } - writeDatabase(instance) - return Promise.resolve(response) + writeDatabase(instance, db) + instance.pool.release(db) + return response + }) } diff --git a/src/index.js b/src/index.js index c1d6834..869fa59 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,7 @@ import { resolve } from 'path' import Model from './model' import * as types from './types' -import { readDatabase } from './sqljs-handler' +import { connect } from './sqljs-handler' import { runQuery } from './helpers' import { setup } from './enforcers' import { invariant } from './util' @@ -25,7 +25,7 @@ class Trilogy { this.knex = knex({ ...config, connection: this.options.connection }) } else { this.knex = knex(config) - readDatabase(this, this.options.connection.filename) + this.pool = connect(this) } this.definitions = new Map() @@ -87,7 +87,7 @@ class Trilogy { if (this.isNative) { return this.knex.destroy() } else { - return this.db.close() + return this.pool.drain() } } diff --git a/src/sqljs-handler.js b/src/sqljs-handler.js index 70e8e4a..602e5da 100644 --- a/src/sqljs-handler.js +++ b/src/sqljs-handler.js @@ -1,34 +1,40 @@ import jetpack from 'fs-jetpack' +import pool from 'generic-pool' import SQL from 'sql.js' -import constants from './constants' - export function readDatabase (instance) { + let client + let atPath = instance.options.connection.filename if (jetpack.exists(atPath) === 'file') { let file = jetpack.read(atPath, 'buffer') - instance.db = new SQL.Database(file) + client = new SQL.Database(file) } else { - instance.db = new SQL.Database() - writeDatabase(instance) + client = new SQL.Database() + writeDatabase(instance, client) } + + return client } -export function writeDatabase (instance) { - if (!instance.db) { - throw new Error(constants.ERR_NO_DATABASE) - } +export function writeDatabase (instance, db) { + let data = db.export() + let buffer = new Buffer(data) - try { - let data = instance.db.export() - let buffer = new Buffer(data) + jetpack.file(instance.options.connection.filename, { + content: buffer, mode: '777' + }) +} - let atPath = instance.options.connection.filename +export function connect (instance) { + return pool.createPool({ + create () { + return Promise.resolve(readDatabase(instance)) + }, - jetpack.file(atPath, { - content: buffer, mode: '777' - }) - } catch (e) { - throw new Error(e.message) - } + destroy (client) { + client.close() + return Promise.resolve() + } + }, { min: 1, max: 1 }) } diff --git a/tests/create.js b/tests/create.js new file mode 100644 index 0000000..72086c1 --- /dev/null +++ b/tests/create.js @@ -0,0 +1,45 @@ +import Trilogy from '../dist/trilogy' + +import test from 'ava' +import { remove } from 'fs-jetpack' +import { join, basename } from 'path' + +const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) +const db = new Trilogy(filePath) + +const schema = { + first: String, + second: Number +} + +const tables = [ + { name: 'one', schema }, + { name: 'two', schema }, + { name: 'three', schema } +] + +test.before(() => { + return Promise.all(tables.map(table => { + return db.model(table.name, table.schema) + })) +}) + +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) + +test('inserts objects into the database', async t => { + let inserts = [ + { table: 'one', object: { first: 'hello', second: 1 } }, + { table: 'two', object: { first: 'hello', second: 2 } }, + { table: 'three', object: { first: 'hello', second: 3 } } + ] + + await Promise.all( + inserts.map(({ table, object }) => db.create(table, object)) + ) + + inserts.forEach(async ({ table, object }) => { + t.deepEqual(await db.find(table, object), [object]) + }) +}) diff --git a/tests/drop-model.js b/tests/drop-model.js new file mode 100644 index 0000000..3dce9a4 --- /dev/null +++ b/tests/drop-model.js @@ -0,0 +1,36 @@ +import Trilogy from '../dist/trilogy' + +import test from 'ava' +import { remove } from 'fs-jetpack' +import { join, basename } from 'path' + +const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) +const db = new Trilogy(filePath) + +const schema = { name: String } + +const tables = [ + { name: 'one', schema }, + { name: 'two', schema }, + { name: 'three', schema } +] + +test.before(() => { + return Promise.all(tables.map(table => { + db.model(table.name, table.schema) + })) +}) + +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) + +test('removes tables from the database', async t => { + let removals = await Promise.all( + tables.map(({ name }) => { + return db.dropModel(name).then(() => db.hasModel(name)) + }) + ) + + removals.forEach(v => t.false(v)) +}) diff --git a/tests/drop-table.js b/tests/drop-table.js deleted file mode 100644 index ce27519..0000000 --- a/tests/drop-table.js +++ /dev/null @@ -1,31 +0,0 @@ -import Trilogy from '../dist/trilogy' - -import test from 'ava' -import { remove } from 'fs-jetpack' -import { join, basename } from 'path' - -const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) -const db = new Trilogy(filePath) - -const columns = ['name'] - -const tables = [ - { name: 'one', columns }, - { name: 'two', columns }, - { name: 'three', columns } -] - -test.before(() => { - return Promise.all(tables.map(table => { - db.createTable(table.name, table.columns) - })) -}) - -test.after.always('remove test database file', () => remove(filePath)) - -test('removes tables from the database', t => { - tables.map(async table => { - await db.dropTable(table.name) - t.false(await db.hasTable(table.name)) - }) -}) diff --git a/tests/find-one.js b/tests/find-one.js new file mode 100644 index 0000000..9fcda89 --- /dev/null +++ b/tests/find-one.js @@ -0,0 +1,35 @@ +import Trilogy from '../dist/trilogy' + +import test from 'ava' +import { remove } from 'fs-jetpack' +import { join, basename } from 'path' + +const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) +const db = new Trilogy(filePath) + +test.before(async () => { + await db.model('first', { + first: String, + second: String + }) + + await db.create('first', { + first: 'fee', + second: 'blah' + }) +}) + +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) + +test('retrieves a single object', async t => { + let expected = { first: 'fee', second: 'blah' } + let res = await db.findOne('first') + t.deepEqual(res, expected) +}) + +test('allows retrieving a specific property', async t => { + let res = await db.findOne('first.second') + t.deepEqual(res, 'blah') +}) diff --git a/tests/select.js b/tests/find.js similarity index 64% rename from tests/select.js rename to tests/find.js index 434c70d..300a697 100644 --- a/tests/select.js +++ b/tests/find.js @@ -10,17 +10,22 @@ const db = new Trilogy(filePath) const arr = ['fee', 'fi', 'fo', 'fum'] test.before(async () => { - await db.createTable('select', ['first', 'second']) - arr.forEach(async v => await db.insert('select', { - first: v, - second: 'blah' - })) + await db.model('select', { + first: String, + second: String + }) + + await Promise.all( + arr.map(v => db.create('select', { first: v, second: 'blah' })) + ) }) -test.after.always('remove test database file', () => remove(filePath)) +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) test('retrieves rows as arrays of objects', async t => { - let res = await db.select('select') + let res = await db.find('select') t.true(Array.isArray(res)) res.forEach((obj, i) => t.is(obj.first, arr[i])) diff --git a/tests/first.js b/tests/first.js deleted file mode 100644 index 2410eb3..0000000 --- a/tests/first.js +++ /dev/null @@ -1,24 +0,0 @@ -import Trilogy from '../dist/trilogy' - -import test from 'ava' -import { remove } from 'fs-jetpack' -import { join, basename } from 'path' - -const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) -const db = new Trilogy(filePath) - -test.before(async () => { - await db.createTable('first', ['first', 'second']) - await db.insert('first', { - first: 'fee', - second: 'blah' - }) -}) - -test.after.always('remove test database file', () => remove(filePath)) - -test('retrieves a single row as an object', async t => { - let expected = { first: 'fee', second: 'blah' } - let res = await db.first('first') - t.deepEqual(res, expected) -}) diff --git a/tests/get-value.js b/tests/get-value.js deleted file mode 100644 index b0ce120..0000000 --- a/tests/get-value.js +++ /dev/null @@ -1,30 +0,0 @@ -import Trilogy from '../dist/trilogy' - -import test from 'ava' -import { remove } from 'fs-jetpack' -import { join, basename } from 'path' - -const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) -const db = new Trilogy(filePath) - -test.before(async () => { - await db.createTable('one', ['first', 'second']) - await db.insert('one', { - first: 'fee', - second: 'blah' - }) -}) - -test.after.always('remove test database file', () => remove(filePath)) - -test('retrieves the value at a single column & row', async t => { - let res = await db.getValue('one.second', { first: 'fee' }) - t.is(res, 'blah') -}) - -test('is undefined when no value at the path exists', async t => { - let noRow = await db.getValue('one.second', { first: 'worst' }) - let noColumn = await db.getValue('one.third', { first: 'fee' }) - t.is(noRow, undefined) - t.is(noColumn, undefined) -}) diff --git a/tests/get.js b/tests/get.js new file mode 100644 index 0000000..28fce86 --- /dev/null +++ b/tests/get.js @@ -0,0 +1,41 @@ +import Trilogy from '../dist/trilogy' + +import test from 'ava' +import { remove } from 'fs-jetpack' +import { join, basename } from 'path' + +const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) +const db = new Trilogy(filePath) + +test.before(async () => { + await db.model('one', { + first: String, + second: String + }) + + await db.create('one', { + first: 'fee', + second: 'blah' + }) +}) + +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) + +test('retrieves a specific property of the object', async t => { + let res = await db.get('one.second', { first: 'fee' }) + t.is(res, 'blah') +}) + +test('is undefined when no value at the path exists', async t => { + let noRow = await db.get('one.second', { first: 'worst' }) + let noColumn = await db.get('one.third', { first: 'fee' }) + t.is(noRow, undefined) + t.is(noColumn, undefined) +}) + +test('returns the provided default value when target is undefined', async t => { + let noRow = await db.get('one.second', { first: 'worst' }, 'nothing') + t.is(noRow, 'nothing') +}) diff --git a/tests/has-table.js b/tests/has-model.js similarity index 51% rename from tests/has-table.js rename to tests/has-model.js index 3254b3c..05f2f76 100644 --- a/tests/has-table.js +++ b/tests/has-model.js @@ -7,27 +7,33 @@ import { join, basename } from 'path' const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) const db = new Trilogy(filePath) -const columns = ['name'] +const schema = { name: String } const tables = [ - { name: 'one', columns }, - { name: 'two', columns }, - { name: 'three', columns } + { name: 'one', schema }, + { name: 'two', schema }, + { name: 'three', schema } ] test.before(() => { return Promise.all(tables.map(table => { - db.createTable(table.name, table.columns) + return db.model(table.name, table.schema) })) }) -test.after.always('remove test database file', () => remove(filePath)) +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) test('is true for existing tables', t => { - tables.map(async table => t.true(await db.hasTable(table.name))) + return Promise.all( + tables.map(async ({ name }) => t.true(await db.hasModel(name))) + ) }) test('is false for non-existent tables', async t => { let noTables = ['four', 'five', 'six'] - noTables.map(async table => t.false(await db.hasTable(table))) + return Promise.all( + noTables.map(async table => t.false(await db.hasModel(table))) + ) }) diff --git a/tests/inserts.js b/tests/inserts.js deleted file mode 100644 index 66ecfe8..0000000 --- a/tests/inserts.js +++ /dev/null @@ -1,41 +0,0 @@ -import Trilogy from '../dist/trilogy' - -import test from 'ava' -import { remove } from 'fs-jetpack' -import { join, basename } from 'path' - -const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) -const db = new Trilogy(filePath) - -const columns = [ - { name: 'first' }, - { name: 'second', type: 'integer' } -] - -const tables = [ - { name: 'one', columns }, - { name: 'two', columns }, - { name: 'three', columns } -] - -test.before(() => { - return Promise.all(tables.map(table => { - db.createTable(table.name, table.columns) - })) -}) - -test.after.always('remove test database file', () => remove(filePath)) - -test('inserts values into the database', async t => { - let inserts = [ - { name: 'one', value: { first: 'hello', second: 1 } }, - { name: 'two', value: { first: 'hello', second: 2 } }, - { name: 'three', value: { first: 'hello', second: 3 } } - ] - - await Promise.all(inserts.map(insert => db.insert(insert.name, insert.value))) - - inserts.forEach(async insert => { - t.deepEqual(await db.select(insert.name, insert.value), [insert.value]) - }) -}) diff --git a/tests/issues/boolean-to-string.js b/tests/issues/boolean-to-string.js deleted file mode 100644 index 8ef89fc..0000000 --- a/tests/issues/boolean-to-string.js +++ /dev/null @@ -1,56 +0,0 @@ -import Trilogy from '../../dist/trilogy' - -import test from 'ava' -import { remove } from 'fs-jetpack' -import { join, basename } from 'path' - -const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) -const db = new Trilogy(filePath) - -test.before(async () => { - await db.createTable('booleans', ['key', 'booleanField']) -}) - -test.after.always('remove test database file', () => remove(filePath)) - -test.serial('coerces booleans to strings on insert', async t => { - let x = `insert into "booleans" ("booleanField", "key") values ('true', 'one')` - db.verbose = y => t.is(y, x) - - await db.insert('booleans', { key: 'one', booleanField: true }) -}) - -test.serial('coerces booleans to strings on update', async t => { - let x = `update "booleans" set "booleanField" = 'false' where "key" = 'one'` - db.verbose = y => t.is(y, x, 'works for object syntax') - - await db.update('booleans', { booleanField: false }, { key: 'one' }) - - let z = `update "booleans" set "booleanField" = 'true' where "key" = 'one'` - db.verbose = y => t.is(y, z, 'works for array syntax') - - await db.update('booleans', ['booleanField', true], { key: 'one' }) -}) - -test.serial('returns boolean strings to boolean on selection', async t => { - await db.insert('booleans', { key: 'two', booleanField: false }) - - let res = await db.getValue('booleans.booleanField', { key: 'two' }) - t.is(res, false) -}) - -test.serial('does not coerce booleans when `options.coercion` is set to false', async t => { - db.coercion = false - - let x = `update "booleans" set "booleanField" = 1 where "key" = 'two'` - db.verbose = y => t.is(y, x, 'true becomes 1') - - await db.update('booleans', { booleanField: true }, { key: 'two' }) - - let z = `update "booleans" set "booleanField" = 0 where "key" = 'two'` - db.verbose = y => t.is(y, z, 'false becomes 0') - - await db.update('booleans', { booleanField: false }, { key: 'two' }) - - db.coercion = true -}) diff --git a/tests/model.js b/tests/model.js new file mode 100644 index 0000000..12b0c96 --- /dev/null +++ b/tests/model.js @@ -0,0 +1,34 @@ +import Trilogy from '../dist/trilogy' + +import test from 'ava' +import { remove } from 'fs-jetpack' +import { join, basename } from 'path' + +const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) +const db = new Trilogy(filePath) + +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) + +test('creates a new model definition', async t => { + await db.model('teams', { + name: String, + playoffs: { type: Boolean, defaultTo: false } + }) + + t.true(await db.hasModel('teams')) +}) + +test('defines a model with a uniquely constrained property', async t => { + await db.model('sodas', { + name: { type: String, unique: true }, + flavor: String + }) + + let object = { name: 'coke', flavor: 'awesome' } + await db.create('sodas', object) + + let duplicate = await db.create('sodas', object) + t.is(duplicate, 0) +}) diff --git a/tests/table-creation.js b/tests/table-creation.js deleted file mode 100644 index fbaebf6..0000000 --- a/tests/table-creation.js +++ /dev/null @@ -1,50 +0,0 @@ -import Trilogy from '../dist/trilogy' - -import test from 'ava' -import { remove } from 'fs-jetpack' -import { join, basename } from 'path' - -const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) -const db = new Trilogy(filePath) - -test.after.always('remove test database file', () => remove(filePath)) - -test('adds a table to the database (array syntax)', async t => { - await db.createTable('people', [ - { name: 'name' }, - { name: 'age', type: 'integer' } - ]) - - t.true(await db.hasTable('people')) -}) - -test('adds a table to the database (function syntax)', async t => { - let schema = function (table) { - table.text('title') - table.integer('release_year') - } - - await db.createTable('movies', schema) - - t.true(await db.hasTable('movies')) -}) - -test('adds a table to the database (object syntax)', async t => { - await db.createTable('teams', { - name: { type: 'text' }, - playoffs: { type: 'text', defaultTo: false } - }) - - t.true(await db.hasTable('people')) -}) - -test('adds a table with a uniquely constrained column', async t => { - await db.createTable('sodas', [ - { name: 'name', unique: true }, - { name: 'flavor' } - ]) - - await db.insert('sodas', { name: 'coke', flavor: 'awesome' }) - - t.throws(db.insert('sodas', { name: 'coke', flavor: 'awesome' }), Error) -}) diff --git a/tests/updates.js b/tests/update.js similarity index 54% rename from tests/updates.js rename to tests/update.js index a73465d..c7b7e5d 100644 --- a/tests/updates.js +++ b/tests/update.js @@ -8,17 +8,23 @@ const filePath = join(__dirname, `${basename(__filename, '.js')}.db`) const db = new Trilogy(filePath) test.before(async () => { - await db.createTable('one', ['first', 'second']) - await db.insert('one', { + await db.model('one', { + first: String, + second: String + }) + + await db.create('one', { first: 'fee', second: 'blah' }) }) -test.after.always('remove test database file', () => remove(filePath)) +test.after.always('remove test database file', () => { + return db.close().then(() => remove(filePath)) +}) test('changes the value of an existing key', async t => { - await db.update('one', { second: 'blurg' }, { first: 'fee' }) - let res = await db.getValue('one.second', { first: 'fee' }) + await db.update('one', { first: 'fee' }, { second: 'blurg' }) + let res = await db.get('one.second', { first: 'fee' }) t.is(res, 'blurg') })