diff --git a/.travis.yml b/.travis.yml index f559271..f14c848 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,21 @@ node_js: - "7" - "8" sudo: false + +before_script: + - mysql -e "CREATE USER grind_test;" -u root + - mysql -e "GRANT ALL PRIVILEGES ON *.* TO grind_test;" -u root + - mysql -e "CREATE DATABASE grind_orm_test;" -u root + - psql -c "CREATE USER grind_test SUPERUSER;" -U postgres + - psql -c "CREATE DATABASE grind_orm_test;" -U postgres + script: - "npm run lint" - "npm test" + +services: + - mysql + - postgresql + +addons: + postgresql: '9.5' diff --git a/README.markdown b/README.markdown index 5a9640a..b099a9d 100644 --- a/README.markdown +++ b/README.markdown @@ -16,6 +16,22 @@ Grind ORM provides an integrated ORM for [Grind](https://github.com/grindjs/fram Full documentation for Grind ORM is available on the [Grind website](https://grind.rocks/docs/guides/orm). +## Testing MySQL and PostgreSQL +Install [MySQL](https://dev.mysql.com/downloads/mysql/) and [PostgreSQL](https://www.postgresql.org/download/). Then, create users and databases: + +MySQL: +```bash +> "CREATE USER grind_test;" +> "GRANT ALL PRIVILEGES ON `grind_orm_test`.* TO 'grind_test'@'%';" +> "CREATE DATABASE grind_orm_test;" +``` + +PostgreSQL: +```bash +> "CREATE USER grind_test SUPERUSER;" +> "CREATE DATABASE grind_orm_test;" +``` + ## License Grind was created by [Shaun Harrison](https://github.com/shnhrrsn) and is made available under the [MIT license](LICENSE). diff --git a/bin/ava b/bin/ava index 36e264f..006a5c3 100755 --- a/bin/ava +++ b/bin/ava @@ -6,4 +6,4 @@ if [ "$NODE_VERSION" == "7" ]; then FLAGS="--harmony-async-await" fi -NODE_ENV=test node $FLAGS node_modules/.bin/ava "$@" +NODE_ENV=test node $FLAGS node_modules/.bin/ava "$@" --serial diff --git a/package.json b/package.json index b541af4..7b61755 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,8 @@ "grind-cli": "^0.7.0", "grind-db": "^0.7.0", "grind-framework": "^0.7.1", + "mysql": "^2.15.0", + "pg": "^7.3.0", "sqlite": "^2.8.0" }, "engines": { diff --git a/src/MakeModelCommand.js b/src/MakeModelCommand.js index a2e8e11..b58d944 100644 --- a/src/MakeModelCommand.js +++ b/src/MakeModelCommand.js @@ -3,6 +3,7 @@ import './Inflect' import path from 'path' export class MakeModelCommand extends Command { + name = 'make:model' description = 'Create a model class' diff --git a/src/Model.js b/src/Model.js index ffdb6a3..7288f7e 100644 --- a/src/Model.js +++ b/src/Model.js @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { Model as ObjectionModel } from 'objection' import './Inflect' @@ -8,6 +9,7 @@ import './RelationValidator' const as = require('as-type') export class Model extends ObjectionModel { + static descriptiveName = null static eager = null static eagerFilters = null diff --git a/src/QueryBuilder.js b/src/QueryBuilder.js index b749b9b..3cc499f 100644 --- a/src/QueryBuilder.js +++ b/src/QueryBuilder.js @@ -3,6 +3,7 @@ import { QueryBuilder as ObjectionQueryBuilder } from 'objection' import './ModelNotFoundError' export class QueryBuilder extends ObjectionQueryBuilder { + static registeredFilters = { } _cyclicalEagerProtection = [ ] diff --git a/src/RelationSynchronizer.js b/src/RelationSynchronizer.js index f87db5a..342d0b3 100644 --- a/src/RelationSynchronizer.js +++ b/src/RelationSynchronizer.js @@ -1,4 +1,5 @@ export class RelationSynchronizer { + model = null modelClass = null relation = null diff --git a/test/QueryBuilder.js b/test/QueryBuilder.js index 5512ba8..5d80079 100644 --- a/test/QueryBuilder.js +++ b/test/QueryBuilder.js @@ -19,7 +19,7 @@ test('orFail', async t => { } try { - await t.context.UserModel.query().where('id', Date.now()).orFail() + await t.context.UserModel.query().where('id', Math.round(Date.now() / 10000)).orFail() t.fail('Should have thrown an error') } catch(err) { if(err instanceof ModelNotFoundError) { diff --git a/test/RelationValidator.js b/test/RelationValidator.js index b4a0999..34d7f8b 100644 --- a/test/RelationValidator.js +++ b/test/RelationValidator.js @@ -4,7 +4,7 @@ import { transaction } from 'objection' test('validate missing', async t => { try { await t.context.UserAvatarModel.query().insert({ - user_id: Date.now(), + user_id: Math.round(Date.now() / 10000), url: 'test' }) diff --git a/test/fixtures/config/database.json b/test/fixtures/config/database.json index 0a7a7f2..0dadeef 100644 --- a/test/fixtures/config/database.json +++ b/test/fixtures/config/database.json @@ -8,6 +8,24 @@ "driver": "sqlite3", "filename": "./database.sqlite", "useNullAsDefault": true + }, + + "mysql": { + "driver": "mysql", + "host": "127.0.0.1", + "user": "grind_test", + "database": "grind_orm_test", + "charset": "utf8", + "pool": { + "min": 1, + "max": 1 + } + }, + + "pg": { + "driver": "pg", + "host": "127.0.0.1", + "database": "grind_orm_test" } } diff --git a/test/fixtures/database/migrations/01-create_users_table.js b/test/fixtures/database/migrations/01-create_users_table.js index 1b5fcfc..6250c63 100644 --- a/test/fixtures/database/migrations/01-create_users_table.js +++ b/test/fixtures/database/migrations/01-create_users_table.js @@ -1,6 +1,6 @@ export function up(db) { return db.schema.createTable('users', table => { - table.integer('id').unsigned().primary() + table.increments('id').unsigned().primary() table.string('name', 128).notNullable().index() table.timestamps() }) diff --git a/test/fixtures/database/migrations/02-create_user_avatars_table.js b/test/fixtures/database/migrations/02-create_user_avatars_table.js index c7e6aad..aada610 100644 --- a/test/fixtures/database/migrations/02-create_user_avatars_table.js +++ b/test/fixtures/database/migrations/02-create_user_avatars_table.js @@ -1,6 +1,6 @@ export function up(db) { return db.schema.createTable('user_avatars', table => { - table.integer('id').unsigned().primary() + table.increments('id').unsigned().primary() table.integer('user_id').unsigned().nullable().index().references('id').inTable('users').onDelete('CASCADE') table.string('url', 128).notNullable().index() table.timestamps() diff --git a/test/helpers/Databases/BaseDatabase.js b/test/helpers/Databases/BaseDatabase.js new file mode 100644 index 0000000..4910a87 --- /dev/null +++ b/test/helpers/Databases/BaseDatabase.js @@ -0,0 +1,37 @@ +export class BaseDatabase { + + static seedTables = [ 'users', 'user_avatars' ] + + static ready(app) { + app.config.set('database.default', this.dbName) + } + + static async runMigration(app) { + await app.db.migrate.latest() + await app.db.seed.run() + + // Reset autoincrementing, which can break when primary keys are set explicitly in seed files + return app.db.transaction(trx => { + return Promise.all(this.seedTables.map(async table => { + try { + let maxId = await trx.max('id as max').from(table) + maxId = maxId[0].max || 10 + + return this.resequence(trx, table, maxId) + } catch(err) { + throw new Error(err) + } + })) + }) + } + + static async resequence(/* db */) { + throw new Error('Subclass must implement') + } + + static shutdown(/* app */) { + throw new Error('Subclass must implement') + } + + +} diff --git a/test/helpers/Databases/Mysql.js b/test/helpers/Databases/Mysql.js new file mode 100644 index 0000000..6266ef4 --- /dev/null +++ b/test/helpers/Databases/Mysql.js @@ -0,0 +1,29 @@ +import './BaseDatabase' + +export class Mysql extends BaseDatabase { + + static dbName = 'mysql' + + static runMigration(app) { + return app.db.migrate.currentVersion().then(version => { + if(version === 'none') { + return super.runMigration(app) + } + + return app.db.migrate.rollback().then(() => super.runMigration(app)) + }) + } + + static resequence(trx, table, maxId) { + return trx.raw(`ALTER TABLE ${table} AUTO_INCREMENT = ${maxId + 1}`) + } + + static async shutdown(app) { + try { + await app.db.destroy() + } catch(err) { + Log.error('Unable to destroy test db', err) + } + } + +} diff --git a/test/helpers/Databases/Postgres.js b/test/helpers/Databases/Postgres.js new file mode 100644 index 0000000..65b1c03 --- /dev/null +++ b/test/helpers/Databases/Postgres.js @@ -0,0 +1,29 @@ +import './BaseDatabase' + +export class Postgres extends BaseDatabase { + + static dbName = 'pg' + + static runMigration(app) { + return app.db.migrate.currentVersion().then(version => { + if(version === 'none') { + return super.runMigration(app) + } + + return app.db.migrate.rollback().then(() => super.runMigration(app)) + }) + } + + static resequence(trx, table, maxId) { + return trx.raw(`ALTER SEQUENCE "${table}_id_seq" RESTART WITH ${maxId + 1}`) + } + + static async shutdown(app) { + try { + await app.db.destroy() + } catch(err) { + Log.error('Unable to destroy test db', err) + } + } + +} diff --git a/test/helpers/Databases/Sqlite3.js b/test/helpers/Databases/Sqlite3.js new file mode 100644 index 0000000..809a974 --- /dev/null +++ b/test/helpers/Databases/Sqlite3.js @@ -0,0 +1,30 @@ +import './BaseDatabase' + +const path = require('path') +const crypto = require('crypto') +const fs = require('fs') + +export class Sqlite3 extends BaseDatabase { + + static dbName = 'sqlite' + static dbPath = path.join(__dirname, `../../fixtures/database/database-${crypto.randomBytes(4).toString('hex')}.sqlite`) + + static ready(app) { + super.ready(app) + app.config.set('database.connections.sqlite.filename', this.dbPath) + } + + static resequence(trx, table, maxId) { + return trx.raw(`UPDATE sqlite_sequence SET seq = ${maxId} WHERE name = '${table}'`) + } + + static shutdown(/* app */) { + try { + // eslint-disable-next-line no-sync + fs.unlinkSync(this.dbPath) + } catch(err) { + Log.error('Unable to remove test db', err) + } + } + +} diff --git a/test/helpers/Databases/index.js b/test/helpers/Databases/index.js new file mode 100644 index 0000000..90e365c --- /dev/null +++ b/test/helpers/Databases/index.js @@ -0,0 +1,9 @@ +import './Postgres' +import './Sqlite3' +import './Mysql' + +export const Databases = { + [Postgres.dbName]: Postgres, + [Sqlite3.dbName]: Sqlite3, + [Mysql.dbName]: Mysql +} diff --git a/test/helpers/Models/UserAvatarModel.js b/test/helpers/Models/UserAvatarModel.js index 12021f1..423aebf 100644 --- a/test/helpers/Models/UserAvatarModel.js +++ b/test/helpers/Models/UserAvatarModel.js @@ -2,6 +2,7 @@ import { Model } from '../../../src' import './UserModel' export class UserAvatarModel extends Model { + static tableName = 'user_avatars' static eager = '[user]' @@ -9,7 +10,7 @@ export class UserAvatarModel extends Model { type: 'object', properties: { - id: { type: 'number' }, + id: { type: 'integer' }, user_id: { type: 'integer', relation: 'user' }, url: { type: 'string', maxLength: 128 }, created_at: { type: 'string', format: 'date-time' }, @@ -22,4 +23,5 @@ export class UserAvatarModel extends Model { user: this.belongsTo(UserModel) } } + } diff --git a/test/helpers/Models/UserModel.js b/test/helpers/Models/UserModel.js index a9cf1c5..f9bdb73 100644 --- a/test/helpers/Models/UserModel.js +++ b/test/helpers/Models/UserModel.js @@ -2,6 +2,7 @@ import { Model } from '../../../src' import './UserAvatarModel' export class UserModel extends Model { + static tableName = 'users' static buildRelations() { diff --git a/test/helpers/makeApp.js b/test/helpers/makeApp.js index 3d42e25..e04460e 100644 --- a/test/helpers/makeApp.js +++ b/test/helpers/makeApp.js @@ -3,16 +3,13 @@ require('babel-polyfill') import './Grind' import { DatabaseProvider } from 'grind-db' import { OrmProvider } from '../../src' +import { Databases } from './Databases' -const path = require('path') -const crypto = require('crypto') -const fs = require('fs') - -export async function makeApp(boot = () => { }) { +export async function makeApp(dbName, boot = () => { }) { const app = new Grind - const dbPath = path.join(__dirname, `../fixtures/database/database-${crypto.randomBytes(4).toString('hex')}.sqlite`) - app.config.set('database.connections.sqlite.filename', dbPath) + const db = Databases[dbName] + await db.ready(app) app.providers.add(DatabaseProvider) app.providers.add(OrmProvider) @@ -20,17 +17,8 @@ export async function makeApp(boot = () => { }) { await app.boot() await boot() - await app.db.migrate.latest() - await app.db.seed.run() - - app.on('shutdown', () => { - try { - // eslint-disable-next-line no-sync - fs.unlinkSync(dbPath) - } catch(err) { - Log.error('Unable to remove test db', err) - } - }) + await db.runMigration(app) + app.on('shutdown', async () => db.shutdown(app)) return app } diff --git a/test/helpers/test.js b/test/helpers/test.js index f3890dc..bcacb1c 100644 --- a/test/helpers/test.js +++ b/test/helpers/test.js @@ -4,8 +4,7 @@ import '../helpers/Models/UserModel' import '../helpers/Models/UserAvatarModel' test.beforeEach(async t => { - t.context.app = await makeApp() - + t.context.app = await makeApp(t.title.match(/\*(sqlite|mysql|pg)\*/)[1]) UserModel.app(t.context.app) UserModel.knex(t.context.app.db) t.context.UserModel = UserModel @@ -17,6 +16,12 @@ test.beforeEach(async t => { test.afterEach.always(t => t.context.app.shutdown()) +export function testDBs(name, cb, dbs = [ 'sqlite', 'mysql', 'pg' ]) { + for(const db of dbs) { + test.serial(`*${db}*: ${name}`, cb) + } +} + module.exports = { - test: test.serial + test: testDBs }