diff --git a/lib/compiler.js b/lib/compiler.js index 70459ab..c6738c7 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -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 = {}; diff --git a/test/compiler.test.js b/test/compiler.test.js index 9ed42e3..1eb1469 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -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() {