317 lines
8.8 KiB
JavaScript
317 lines
8.8 KiB
JavaScript
// Copyright IBM Corp. 2014,2016. All Rights Reserved.
|
|
// Node module: loopback-boot
|
|
// This file is licensed under the MIT License.
|
|
// License text available at https://opensource.org/licenses/MIT
|
|
|
|
'use strict';
|
|
|
|
var assert = require('assert');
|
|
var util = require('util');
|
|
var PluginBase = require('../plugin-base');
|
|
var path = require('path');
|
|
var debug = require('debug')('loopback:boot:model');
|
|
var _ = require('lodash');
|
|
var toposort = require('toposort');
|
|
var utils = require('../utils');
|
|
|
|
var tryReadDir = utils.tryReadDir;
|
|
var assertIsValidConfig = utils.assertIsValidConfig;
|
|
var tryResolveAppPath = utils.tryResolveAppPath;
|
|
var fixFileExtension = utils.fixFileExtension;
|
|
var g = require('../globalize');
|
|
|
|
module.exports = function(options) {
|
|
return new Model(options);
|
|
};
|
|
|
|
function Model(options) {
|
|
PluginBase.call(this, options, 'models', 'model-config');
|
|
}
|
|
|
|
util.inherits(Model, PluginBase);
|
|
|
|
Model.prototype.getRootDir = function() {
|
|
return this.options.modelsRootDir;
|
|
};
|
|
|
|
Model.prototype.load = function(context) {
|
|
var config = PluginBase.prototype.load.apply(this, arguments);
|
|
assertIsValidModelConfig(config);
|
|
return config;
|
|
};
|
|
|
|
Model.prototype.buildInstructions = function(context, rootDir, modelsConfig) {
|
|
var modelsMeta = modelsConfig._meta || {};
|
|
delete modelsConfig._meta;
|
|
context.configurations.mixins._meta = modelsMeta;
|
|
|
|
var modelSources = this.options.modelSources || modelsMeta.sources ||
|
|
['./models'];
|
|
var modelInstructions = buildAllModelInstructions(
|
|
rootDir, modelsConfig, modelSources, this.options.modelDefinitions,
|
|
this.options.scriptExtensions
|
|
);
|
|
return modelInstructions;
|
|
};
|
|
|
|
function buildAllModelInstructions(rootDir, modelsConfig, sources,
|
|
modelDefinitions, scriptExtensions) {
|
|
var registry = verifyModelDefinitions(rootDir, modelDefinitions,
|
|
scriptExtensions);
|
|
if (!registry) {
|
|
registry = findModelDefinitions(rootDir, sources, scriptExtensions);
|
|
}
|
|
|
|
var modelNamesToBuild = addAllBaseModels(registry, Object.keys(modelsConfig));
|
|
|
|
var instructions = modelNamesToBuild
|
|
.map(function createModelInstructions(name) {
|
|
var config = modelsConfig[name];
|
|
var definition = registry[name] || {};
|
|
|
|
debug('Using model "%s"\nConfiguration: %j\nDefinition %j',
|
|
name, config, definition.definition);
|
|
|
|
return {
|
|
name: name,
|
|
config: config,
|
|
definition: definition.definition,
|
|
sourceFile: definition.sourceFile,
|
|
};
|
|
});
|
|
|
|
return sortByInheritance(instructions);
|
|
}
|
|
|
|
function addAllBaseModels(registry, modelNames) {
|
|
var result = [];
|
|
var visited = {};
|
|
|
|
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 = getBaseModelName(definition);
|
|
|
|
// ignore built-in models like User
|
|
if (!registry[base]) continue;
|
|
|
|
modelNames.push(base);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function getBaseModelName(modelDefinition) {
|
|
if (!modelDefinition)
|
|
return undefined;
|
|
|
|
return modelDefinition.base ||
|
|
modelDefinition.options && modelDefinition.options.base;
|
|
}
|
|
|
|
function sortByInheritance(instructions) {
|
|
// 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 verifyModelDefinitions(rootDir, modelDefinitions, scriptExtensions) {
|
|
if (!modelDefinitions || modelDefinitions.length < 1) {
|
|
return undefined;
|
|
}
|
|
|
|
var registry = {};
|
|
modelDefinitions.forEach(function(definition, idx) {
|
|
if (definition.sourceFile) {
|
|
var fullPath = path.resolve(rootDir, definition.sourceFile);
|
|
definition.sourceFile = fixFileExtension(
|
|
fullPath,
|
|
tryReadDir(path.dirname(fullPath)),
|
|
scriptExtensions
|
|
);
|
|
|
|
if (!definition.sourceFile) {
|
|
debug('Model source code not found: %s - %s', definition.sourceFile);
|
|
}
|
|
}
|
|
|
|
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, scriptExtensions) {
|
|
var registry = {};
|
|
|
|
sources.forEach(function(src) {
|
|
var srcDir = tryResolveAppPath(rootDir, src, {strict: false});
|
|
if (!srcDir) {
|
|
debug('Skipping unknown module source dir %j', src);
|
|
return;
|
|
}
|
|
|
|
var files = tryReadDir(srcDir);
|
|
|
|
files
|
|
.filter(function(f) {
|
|
return f[0] !== '_' && path.extname(f) === '.json';
|
|
})
|
|
.forEach(function(f) {
|
|
var fullPath = path.resolve(srcDir, f);
|
|
var entry = loadModelDefinition(rootDir, fullPath, files,
|
|
scriptExtensions);
|
|
var modelName = entry.definition.name;
|
|
if (!modelName) {
|
|
debug('Skipping model definition without Model name: %s',
|
|
path.relative(srcDir, fullPath));
|
|
return;
|
|
}
|
|
registry[modelName] = entry;
|
|
});
|
|
});
|
|
|
|
return registry;
|
|
}
|
|
|
|
function loadModelDefinition(rootDir, jsonFile, allFiles, scriptExtensions) {
|
|
var definition = require(jsonFile);
|
|
var basename = path.basename(jsonFile, path.extname(jsonFile));
|
|
definition.name = definition.name || _.upperFirst(_.camelCase(basename));
|
|
|
|
// find a matching file with a supported extension like `.js` or `.coffee`
|
|
var sourceFile = fixFileExtension(jsonFile, allFiles, scriptExtensions);
|
|
|
|
if (sourceFile === undefined) {
|
|
debug('Model source code not found: %s', sourceFile);
|
|
}
|
|
|
|
debug('Found model "%s" - %s %s', definition.name,
|
|
path.relative(rootDir, jsonFile),
|
|
sourceFile ? path.relative(rootDir, sourceFile) : '(no source file)');
|
|
|
|
return {
|
|
definition: definition,
|
|
sourceFile: sourceFile,
|
|
};
|
|
}
|
|
|
|
function assertIsValidModelConfig(config) {
|
|
assertIsValidConfig('model', config);
|
|
for (var name in config) {
|
|
var entry = config[name];
|
|
var options = entry.options || {};
|
|
var unsupported = entry.properties ||
|
|
entry.base || options.base ||
|
|
entry.plural || options.plural;
|
|
|
|
if (unsupported) {
|
|
throw new Error(g.f(
|
|
'The data in {{model-config.json}} ' +
|
|
'is in the unsupported {{1.x}} format.'
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Regular expression to match built-in loopback models
|
|
var LOOPBACK_MODEL_REGEXP = new RegExp(
|
|
['', 'node_modules', 'loopback', '[^\\/\\\\]+', 'models', '[^\\/\\\\]+\\.js$']
|
|
.join('\\' + path.sep)
|
|
);
|
|
|
|
function isBuiltinLoopBackModel(app, data) {
|
|
// 1. Built-in models are exposed on the loopback object
|
|
if (!app.loopback[data.name]) return false;
|
|
|
|
// 2. Built-in models have a script file `loopback/{facet}/models/{name}.js`
|
|
var srcFile = data.sourceFile;
|
|
return srcFile &&
|
|
LOOPBACK_MODEL_REGEXP.test(srcFile);
|
|
}
|
|
|
|
Model.prototype.start = function(context) {
|
|
var app = context.app;
|
|
var instructions = context.instructions[this.name];
|
|
|
|
var registry = app.registry || app.loopback;
|
|
instructions.forEach(function(data) {
|
|
var name = data.name;
|
|
var model;
|
|
|
|
if (!data.definition) {
|
|
model = registry.getModel(name);
|
|
if (!model) {
|
|
throw new Error(g.f('Cannot configure unknown model %s', name));
|
|
}
|
|
debug('Configuring existing model %s', name);
|
|
} else if (isBuiltinLoopBackModel(app, data)) {
|
|
model = registry.getModel(name);
|
|
assert(model, 'Built-in model ' + name + ' should have been defined');
|
|
debug('Configuring built-in LoopBack model %s', name);
|
|
} else {
|
|
debug('Creating new model %s %j', name, data.definition);
|
|
model = registry.createModel(data.definition);
|
|
if (data.sourceFile) {
|
|
debug('Loading customization script %s', data.sourceFile);
|
|
var code = require(data.sourceFile);
|
|
if (typeof code === 'function') {
|
|
debug('Customizing model %s', name);
|
|
code(model);
|
|
} else {
|
|
debug('Skipping model file %s - `module.exports` is not a function',
|
|
data.sourceFile);
|
|
}
|
|
}
|
|
}
|
|
data._model = model;
|
|
});
|
|
|
|
instructions.forEach(function(data) {
|
|
// Skip base models that are not exported to the app
|
|
if (!data.config) return;
|
|
|
|
app.model(data._model, data.config);
|
|
});
|
|
};
|