diff --git a/lib/compiler.js b/lib/compiler.js index a8a0e88..5d1938e 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -1,6 +1,7 @@ var assert = require('assert'); var fs = require('fs'); var path = require('path'); +var toposort = require('toposort'); var ConfigLoader = require('./config-loader'); var debug = require('debug')('loopback:boot:compiler'); @@ -176,15 +177,15 @@ function addAllBaseModels(registry, modelNames) { while (modelNames.length) { var name = modelNames.shift(); + + if (visited[name]) continue; + visited[name] = true; result.push(name); var definition = registry[name] && registry[name].definition; if (!definition) continue; - var base = definition.base || definition.options && definition.options.base; - if (!base || base in visited) continue; - - visited[base] = true; + var base = getBaseModelName(definition); // ignore built-in models like User if (!registry[base]) continue; @@ -195,9 +196,37 @@ function addAllBaseModels(registry, modelNames) { return result; } +function getBaseModelName(modelDefinition) { + if (!modelDefinition) + return undefined; + + return modelDefinition.base || + modelDefinition.options && modelDefinition.options.base; +} + function sortByInheritance(instructions) { - // TODO implement topological sort - return instructions.reverse(); + // create edges Base name -> Model name + var edges = instructions + .map(function(inst) { + return [getBaseModelName(inst.definition), inst.name]; + }); + + var sortedNames = toposort(edges); + + var instructionsByModelName = {}; + instructions.forEach(function(inst) { + instructionsByModelName[inst.name] = inst; + }); + + return sortedNames + // convert to instructions + .map(function(name) { + return instructionsByModelName[name]; + }) + // remove built-in models + .filter(function(inst) { + return !!inst; + }); } function findModelDefinitions(rootDir, sources) { diff --git a/package.json b/package.json index ef0d12b..e4c8fdd 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,10 @@ "url": "https://github.com/strongloop/loopback-boot/blob/master/LICENSE" }, "dependencies": { - "underscore": "^1.6.0", + "commondir": "0.0.1", "debug": "^0.8.1", - "commondir": "0.0.1" + "toposort": "^0.2.10", + "underscore": "^1.6.0" }, "devDependencies": { "loopback": "^1.5.0", diff --git a/test/compiler.test.js b/test/compiler.test.js index b3823d6..77fa7b7 100644 --- a/test/compiler.test.js +++ b/test/compiler.test.js @@ -359,6 +359,48 @@ describe('compiler', function() { var modelNames = instructions.models.map(getNameProperty); expect(modelNames).to.eql(['Car']); }); + + it('sorts models, base models first', function() { + appdir.createConfigFilesSync({}, {}, { + Vehicle: { dataSource: 'db' }, + FlyingCar: { dataSource: 'db' }, + Car: { dataSource: 'db' } + }); + appdir.writeConfigFileSync('models/car.json', { + name: 'Car', + base: 'Vehicle' + }); + appdir.writeConfigFileSync('models/vehicle.json', { + name: 'Vehicle' + }); + appdir.writeConfigFileSync('models/flying-car.json', { + name: 'FlyingCar', + base: 'Car' + }); + + var instructions = boot.compile(appdir.PATH); + + var modelNames = instructions.models.map(getNameProperty); + expect(modelNames).to.eql(['Vehicle', 'Car', 'FlyingCar']); + }); + + it('detects circular Model dependencies', function() { + appdir.createConfigFilesSync({}, {}, { + Vehicle: { dataSource: 'db' }, + Car: { dataSource: 'db' } + }); + appdir.writeConfigFileSync('models/car.json', { + name: 'Car', + base: 'Vehicle' + }); + appdir.writeConfigFileSync('models/vehicle.json', { + name: 'Vehicle', + base: 'Car' + }); + + expect(function() { boot.compile(appdir.PATH); }) + .to.throw(/cyclic dependency/i); + }); }); });