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:
parent
0e19f0a1b2
commit
b6c60a297d
|
@ -81,8 +81,10 @@ module.exports = function compile(options) {
|
||||||
delete modelsConfig._meta;
|
delete modelsConfig._meta;
|
||||||
|
|
||||||
var modelSources = options.modelSources || modelsMeta.sources || ['./models'];
|
var modelSources = options.modelSources || modelsMeta.sources || ['./models'];
|
||||||
|
var modelDefinitions = options.modelDefinitions || undefined;
|
||||||
|
|
||||||
var modelInstructions = buildAllModelInstructions(
|
var modelInstructions = buildAllModelInstructions(
|
||||||
modelsRootDir, modelsConfig, modelSources);
|
modelsRootDir, modelsConfig, modelSources, modelDefinitions);
|
||||||
|
|
||||||
var mixinDirs = options.mixinDirs || [];
|
var mixinDirs = options.mixinDirs || [];
|
||||||
var mixinInstructions = buildAllMixinInstructions(
|
var mixinInstructions = buildAllMixinInstructions(
|
||||||
|
@ -192,8 +194,8 @@ function tryReadDir() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildAllModelInstructions(rootDir, modelsConfig, sources) {
|
function buildAllModelInstructions(rootDir, modelsConfig, sources, modelDefinitions) {
|
||||||
var registry = findModelDefinitions(rootDir, sources);
|
var registry = verifyModelDefinitions(rootDir, modelDefinitions) || findModelDefinitions(rootDir, sources);
|
||||||
|
|
||||||
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
|
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) {
|
function findModelDefinitions(rootDir, sources) {
|
||||||
var registry = {};
|
var registry = {};
|
||||||
|
|
||||||
|
|
|
@ -78,6 +78,193 @@ describe('compiler', function() {
|
||||||
it('has datasources definition', function() {
|
it('has datasources definition', function() {
|
||||||
expect(instructions.dataSources).to.eql(options.dataSources);
|
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() {
|
describe('from directory', function() {
|
||||||
|
|
Loading…
Reference in New Issue