Extend options arg to support custom model definitions

Extend the options argument of boot/compile to allow the developer
to supply custom model definitions (to replace the result of
findModelDefinitions). Together with the existing options.models,
it allows to specify all model-related data and still use the benefits
which loopback-boot provides (most notably loading models in order
defined by inheritance, i.e. base models are loaded before models that
are extending them).
This commit is contained in:
Shlomi Assaf 2015-02-24 21:18:08 +02:00 committed by Miroslav Bajtoš
parent 0e19f0a1b2
commit b6c60a297d
2 changed files with 254 additions and 3 deletions

View File

@ -81,8 +81,10 @@ module.exports = function compile(options) {
delete modelsConfig._meta;
var modelSources = options.modelSources || modelsMeta.sources || ['./models'];
var modelDefinitions = options.modelDefinitions || undefined;
var modelInstructions = buildAllModelInstructions(
modelsRootDir, modelsConfig, modelSources);
modelsRootDir, modelsConfig, modelSources, modelDefinitions);
var mixinDirs = options.mixinDirs || [];
var mixinInstructions = buildAllMixinInstructions(
@ -192,8 +194,8 @@ function tryReadDir() {
}
}
function buildAllModelInstructions(rootDir, modelsConfig, sources) {
var registry = findModelDefinitions(rootDir, sources);
function buildAllModelInstructions(rootDir, modelsConfig, sources, modelDefinitions) {
var registry = verifyModelDefinitions(rootDir, modelDefinitions) || findModelDefinitions(rootDir, sources);
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
@ -274,6 +276,68 @@ function sortByInheritance(instructions) {
});
}
/**
* Returns matching files with a supported `sourceFile` extension like `.js` or `.coffee`
* @param allFiles {Array}
* @param basename {string}
* @returns {Array}
*/
function filterValidSourceFiles(allFiles, basename) {
var base, ext, validFileType;
return allFiles
.filter(function(f) {
ext = path.extname(f);
base = path.basename(f, ext);
validFileType = (ext !== '.node') && (ext !== '.json') &&
((typeof require.extensions[ext]) === 'function');
return validFileType && (base === basename);
});
}
function verifyModelDefinitions(rootDir, modelDefinitions) {
if (!modelDefinitions || modelDefinitions.length < 1) {
return undefined;
}
var registry = {};
modelDefinitions.forEach(function(definition, idx) {
if (definition.sourceFile) {
try {
var fullPath = path.resolve(rootDir, definition.sourceFile);
var basename = path.basename(fullPath, path.extname(fullPath));
var files = tryReadDir(path.dirname(fullPath));
var sourceFile = filterValidSourceFiles(files, basename)[0];
definition.sourceFile = path.join(path.dirname(fullPath), sourceFile);
definition.sourceFile = require.resolve(definition.sourceFile);
} catch (err) {
debug('Model source code not found: %s - %s',
definition.sourceFile,
err.code || err);
definition.sourceFile = undefined;
}
} else {
definition.sourceFile = undefined;
}
debug('Found model "%s" - %s %s',
definition.definition.name,
'from options',
definition.sourceFile ?
path.relative(rootDir, definition.sourceFile) :
'(no source file)');
var modelName = definition.definition.name;
if (!modelName) {
debug('Skipping model definition without Model name (from options.modelDefinitions @ index %s)', idx);
return;
}
registry[modelName] = definition;
});
return registry;
}
function findModelDefinitions(rootDir, sources) {
var registry = {};

View File

@ -78,6 +78,193 @@ describe('compiler', function() {
it('has datasources definition', function() {
expect(instructions.dataSources).to.eql(options.dataSources);
});
describe('with custom model definitions', function() {
var dataSources = {
'the-db': {
connector: 'memory',
defaultForType: 'db'
}
};
it('should loads model without definition.', function() {
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'model-without-definitions': {
dataSource: 'the-db'
}
},
modelDefinitions: [],
dataSources: dataSources
});
expect(instruction.models[0].name).to.equal('model-without-definitions');
expect(instruction.models[0].definition).to.equal(undefined);
expect(instruction.models[0].sourceFile).to.equal(undefined);
});
it('should load coffeescript models', function() {
require('coffee-script/register');
appdir.writeFileSync('custom-models/coffee-model-with-definitions.coffee', '');
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'coffee-model-with-definitions': {
dataSource: 'the-db'
}
},
modelDefinitions: [
{
definition: {
name: 'coffee-model-with-definitions'
},
sourceFile: path.join(appdir.PATH, 'custom-models', 'coffee-model-with-definitions.coffee')
}
],
dataSources: dataSources
});
expect(instruction.models[0].name).to.equal('coffee-model-with-definitions');
expect(instruction.models[0].definition).not.to.equal(undefined);
expect(instruction.models[0].sourceFile).not.to.equal(undefined);
});
it('should handle sourceFile path without extension.', function() {
require('coffee-script/register');
appdir.writeFileSync('custom-models/coffee-model-without-ext.coffee', '');
appdir.writeFileSync('custom-models/js-model-without-ext.js', '');
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'coffee-model-without-ext': {
dataSource: 'the-db'
},
'js-model-without-ext': {
dataSource: 'the-db'
}
},
modelDefinitions: [
{
definition: {
name: 'coffee-model-without-ext'
},
sourceFile: path.join(appdir.PATH, 'custom-models', 'coffee-model-without-ext')
},
{
definition: {
name: 'js-model-without-ext'
},
sourceFile: path.join(appdir.PATH, 'custom-models', 'js-model-without-ext')
}
],
dataSources: dataSources
});
expect(instruction.models[0].name).to.equal('coffee-model-without-ext');
expect(instruction.models[0].sourceFile)
.to.equal(path.join(appdir.PATH, 'custom-models', 'coffee-model-without-ext.coffee'));
expect(instruction.models[1].name).to.equal('js-model-without-ext');
expect(instruction.models[1].sourceFile)
.to.equal(path.join(appdir.PATH, 'custom-models', 'js-model-without-ext.js'));
});
it('should set source file path if the file exist.', function() {
appdir.writeFileSync('custom-models/model-with-definitions.js', '');
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'model-with-definitions': {
dataSource: 'the-db'
}
},
modelDefinitions: [
{
definition: {
name: 'model-with-definitions'
},
sourceFile: path.join(appdir.PATH, 'custom-models', 'model-with-definitions.js')
}
],
dataSources: dataSources
});
expect(instruction.models[0].name).to.equal('model-with-definitions');
expect(instruction.models[0].definition).not.to.equal(undefined);
expect(instruction.models[0].sourceFile)
.to.equal(path.join(appdir.PATH, 'custom-models', 'model-with-definitions.js'));
});
it('should not set source file path if the file does not exist.', function() {
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'model-with-definitions-with-falsey-source-file': {
dataSource: 'the-db'
}
},
modelDefinitions: [
{
definition: { // sourceFile is a not existing file.
name: 'model-with-definitions-with-falsey-source-file'
},
sourceFile: path.join(appdir.PATH, 'custom-models', 'model-with-definitions-with-falsey-source-file.js')
}
],
dataSources: dataSources
});
expect(instruction.models[0].name).to.equal('model-with-definitions-with-falsey-source-file');
expect(instruction.models[0].definition).not.to.equal(undefined);
expect(instruction.models[0].sourceFile).to.equal(undefined);
});
it('should not set source file path if no source file supplied.', function() {
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'model-with-definitions-without-source-file-property': {
dataSource: 'the-db'
}
},
modelDefinitions: [
{
definition: { // sourceFile is a not existing file.
name: 'model-with-definitions-without-source-file-property'
}
}
],
dataSources: dataSources
});
expect(instruction.models[0].name).to.equal('model-with-definitions-without-source-file-property');
expect(instruction.models[0].definition).not.to.equal(undefined);
expect(instruction.models[0].sourceFile).to.equal(undefined);
});
it('should loads models defined in `models` only.', function() {
var instruction = boot.compile({
appRootDir: appdir.PATH,
models: {
'some-model': {
dataSource: 'the-db'
}
},
modelDefinitions: [
{
definition: {
name: 'some-model'
}
},
{
definition: {
name: 'some-fictional-model'
}
}
],
dataSources: dataSources
});
expect(instruction.models).to.have.length(1);
});
});
});
describe('from directory', function() {