Skip to content

Commit

Permalink
feat: support config.datasources to define more database
Browse files Browse the repository at this point in the history
  • Loading branch information
dead-horse authored and fengmk2 committed Aug 13, 2018
1 parent bfd0c26 commit 86d660d
Show file tree
Hide file tree
Showing 18 changed files with 367 additions and 77 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ services:
- mysql
before_install:
- mysql -e 'CREATE DATABASE IF NOT EXISTS test;'
- mysql -e 'CREATE DATABASE IF NOT EXISTS test1;'
7 changes: 0 additions & 7 deletions app/extend/context.js

This file was deleted.

144 changes: 80 additions & 64 deletions lib/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

const path = require('path');
const Sequelize = require('sequelize');
const MODELS = Symbol('loadedModels');
const AUTH_RETRIES = Symbol('authenticateRetries');
const sleep = require('mz-modules/sleep');

module.exports = app => {
const defaultConfig = {
delegate: 'model',
baseDir: 'model',
logging: app.logger.info.bind(app.logger),
host: 'localhost',
port: 3306,
Expand All @@ -19,76 +20,91 @@ module.exports = app => {
},
};

const config = Object.assign(defaultConfig, app.config.sequelize);

const config = app.config.sequelize;
app.Sequelize = Sequelize;

const sequelize = new Sequelize(config.database, config.username, config.password, config);

// app.sequelize
Object.defineProperty(app, 'model', {
value: sequelize,
writable: false,
configurable: false,
});

loadModel(app);

app.beforeStart(async function() {
await authenticate(app);
});
};

/**
* Authenticate to test Database connection.
*
* This method will retry 3 times when database connect fail in temporary, to avoid Egg start failed.
* @param {Application} app instance of Egg Application
*/
async function authenticate(app) {
app.model[AUTH_RETRIES] = app.model[AUTH_RETRIES] || 0;

try {
await app.model.authenticate();
} catch (e) {
if (e.name !== 'SequelizeConnectionRefusedError') throw e;
if (app.model[AUTH_RETRIES] >= 3) throw e;

// sleep 2s to retry, max 3 times
app.model[AUTH_RETRIES] += 1;
app.logger.warn(`Sequelize Error: ${e.message}, sleep 2 seconds to retry...`);
await sleep(2000);
await authenticate(app);
const databases = [];
if (!config.datasources) {
databases.push(loadDatabase(Object.assign({}, defaultConfig, config)));
} else {
config.datasources.forEach(datasource => {
databases.push(loadDatabase(Object.assign({}, defaultConfig, datasource)));
});
}
}

function loadModel(app) {
const modelDir = path.join(app.baseDir, 'app/model');
app.loader.loadToApp(modelDir, MODELS, {
inject: app,
caseStyle: 'upper',
ignore: 'index.js',
});

for (const name of Object.keys(app[MODELS])) {
const klass = app[MODELS][name];

// only this Sequelize Model class
if ('sequelize' in klass) {
app.model[name] = klass;
app.beforeStart(async () => {
await Promise.all(databases.map(database => authenticate(database)));
});

if ('classMethods' in klass.options || 'instanceMethods' in klass.options) {
app.logger.error(`${name} model has classMethods/instanceMethods, but it was removed supports in Sequelize V4.\
see: http://docs.sequelizejs.com/manual/tutorial/models-definition.html#expansion-of-models`);
}
/**
* load databse to app[config.delegate
* @param {Object} config config for load
* - delegate: load model to app[delegate]
* - baeDir: where model located
* - other sequelize configures(databasem username, password, etc...)
* @return {Object} sequelize instance
*/
function loadDatabase(config) {
const sequelize = new app.Sequelize(config.database, config.username, config.password, config);

if (app[config.delegate] || app.context[config.delegate]) {
throw new Error(`[egg-sequelize] app[${config.delegate}] or ctx[${config.delegate}] is already defined`);
}
}

for (const name of Object.keys(app[MODELS])) {
const klass = app[MODELS][name];
Object.defineProperty(app, config.delegate, {
value: sequelize,
writable: false,
configurable: false,
});

Object.defineProperty(app.context, config.delegate, {
get() {
return app[config.delegate];
},
configurable: false,
});

const modelDir = path.join(app.baseDir, 'app', config.baseDir);

const models = [];
const target = Symbol(config.delegate);
app.loader.loadToApp(modelDir, target, {
caseStyle: 'upper',
filter(model) {
if (!model || !model.sequelize) return false;
models.push(model);
return true;
},
});
Object.assign(app[config.delegate], app[target]);

models.forEach(model => {
typeof model.associate === 'function' && model.associate();
});

return app[config.delegate];
}

if ('associate' in klass) {
klass.associate();
/**
* Authenticate to test Database connection.
*
* This method will retry 3 times when database connect fail in temporary, to avoid Egg start failed.
* @param {Application} database instance of sequelize
*/
async function authenticate(database) {
database[AUTH_RETRIES] = database[AUTH_RETRIES] || 0;

try {
await database.authenticate();
} catch (e) {
if (e.name !== 'SequelizeConnectionRefusedError') throw e;
if (app.model[AUTH_RETRIES] >= 3) throw e;

// sleep 2s to retry, max 3 times
database[AUTH_RETRIES] += 1;
app.logger.warn(`Sequelize Error: ${e.message}, sleep 2 seconds to retry...`);
await sleep(2000);
await authenticate(app, database);
}
}
}
};
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,6 @@
"eslint": "^5.3.0",
"eslint-config-egg": "^7.0.0",
"mysql2": "^1.3.4",
"power-assert": "^1.4.2",
"should": "^11.2.0",
"supertest": "^3.1.0",
"webstorm-disable-index": "^1.2.0"
},
"engines": {
Expand Down
83 changes: 83 additions & 0 deletions test/datasources.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
'use strict';

const assert = require('assert');
const mm = require('egg-mock');

describe('test/datasources.test.js', () => {
let app;

before(() => {
app = mm.app({
baseDir: 'apps/datasources',
});
return app.ready();
});
before(async () => {
await app.model.sync({ force: true });
await app.sequelize.sync({ force: true });
});

after(mm.restore);

describe('Base', () => {
it('sequelize init success', () => {
assert(app.model);
assert(app.sequelize);
assert(app.Sequelize);
});

it('ctx model property getter', () => {
const ctx = app.mockContext();
assert.ok(ctx.model);
assert.ok(ctx.model.User);
assert.ok(ctx.model.Monkey);
assert.ok(ctx.model.Person);
assert.ok(ctx.sequelize);
assert.ok(ctx.sequelize.User);
assert.ok(ctx.sequelize.Monkey);
assert.ok(ctx.sequelize.Person);
assert(ctx.model.User !== ctx.sequelize.User);
});

it('has right tableName', () => {
assert(app.model.Person.tableName === 'people');
assert(app.model.User.tableName === 'users');
assert(app.model.Monkey.tableName === 'the_monkeys');
});
});

describe('Test model', () => {
it('User.test method work', async function() {
await app.model.User.test();
});

it('should work timestramp', async function() {
let user = await app.model.User.create({ name: 'huacnlee' });
assert(user.isNewRecord === false);
assert(user.name === 'huacnlee');
assert(user.created_at !== null);
assert(user.updated_at !== null);

user = await app.sequelize.User.create({ name: 'huacnlee' });
assert(user.isNewRecord === false);
assert(user.name === 'huacnlee');
assert(user.created_at !== null);
assert(user.updated_at !== null);
});
});

describe('Associate', () => {
it('ctx model associate init success', () => {
const ctx = app.mockContext();
assert.ok(ctx.model);
assert.ok(ctx.model.User);
assert.ok(ctx.model.User.prototype.hasPosts);
assert.ok(ctx.model.Post);

assert.ok(ctx.sequelize);
assert.ok(ctx.sequelize.User);
assert.ok(ctx.sequelize.User.prototype.hasPosts);
assert.ok(ctx.sequelize.Post);
});
});
});
10 changes: 10 additions & 0 deletions test/fixtures/apps/datasources/app/model/Person.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

module.exports = app => {
const { STRING } = app.Sequelize;
const Person = app.model.define('person', {
name: STRING(30),
});

return Person;
};
28 changes: 28 additions & 0 deletions test/fixtures/apps/datasources/app/model/monkey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

module.exports = app => {
const { STRING, INTEGER, DATE } = app.Sequelize;
const Monkey = app.model.define('monkey', {
name: {
type: STRING,
allowNull: false,
},
user_id: INTEGER,
created_at: DATE,
updated_at: DATE,
}, {
tableName: 'the_monkeys',

classMethods: {
},

instanceMethods: {
},
});

Monkey.findUser = async function() {
return app.model.User.find({ id: 1 });
};

return Monkey;
};
4 changes: 4 additions & 0 deletions test/fixtures/apps/datasources/app/model/other.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
'use strict';

module.exports = {
};
19 changes: 19 additions & 0 deletions test/fixtures/apps/datasources/app/model/post.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const assert = require('assert');

module.exports = app => {
const { INTEGER, STRING } = app.Sequelize;
const Post = app.model.define('post', {
user_id: INTEGER,
name: STRING(30),
});

Post.associate = function() {
assert.ok(app.model.User);
assert.ok(app.model.Post);
app.model.Post.belongsTo(app.model.User, { as: 'user', foreignKey: 'user_id' });
};

return Post;
};
28 changes: 28 additions & 0 deletions test/fixtures/apps/datasources/app/model/user.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

const assert = require('assert');

module.exports = app => {
const { STRING, INTEGER } = app.Sequelize;
const User = app.model.define('user', {
name: STRING(30),
age: INTEGER,
});

User.associate = function() {
assert.ok(app.model.User);
assert.ok(app.model.Post);
app.model.User.hasMany(app.model.Post, { as: 'posts', foreignKey: 'user_id' });
};

User.test = async function() {
assert(app.config);
assert(app.model.User === this);
const monkey = await app.model.Monkey.create({ name: 'The Monkey' });
assert(monkey.id);
assert(monkey.isNewRecord === false);
assert(monkey.name === 'The Monkey');
};

return User;
};
10 changes: 10 additions & 0 deletions test/fixtures/apps/datasources/app/sequelize/Person.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
'use strict';

module.exports = app => {
const { STRING } = app.Sequelize;
const Person = app.sequelize.define('person', {
name: STRING(30),
});

return Person;
};

0 comments on commit 86d660d

Please sign in to comment.