Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
*.pid
*.swp
*.swo
*.iml
node_modules
checkstyle.xml
loopback-boot-*.tgz
Expand Down
66 changes: 58 additions & 8 deletions lib/config-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,31 +112,81 @@ function mergeConfigurations(configObjects, mergeFn) {

function mergeDataSourceConfig(target, config, fileName) {
for (var ds in target) {
var err = applyCustomConfig(target[ds], config[ds]);
var err = mergeObjects(target[ds], config[ds]);
if (err) {
throw new Error('Cannot apply ' + fileName + ' to `' + ds + '`: ' + err);
}
}
}

function mergeAppConfig(target, config, fileName) {
var err = applyCustomConfig(target, config);
var err = mergeObjects(target, config);
if (err) {
throw new Error('Cannot apply ' + fileName + ': ' + err);
}
}

function applyCustomConfig(target, config) {
function mergeObjects(target, config, keyPrefix) {
for (var key in config) {
var value = config[key];
if (typeof value === 'object') {
return 'override for the option `' + key + '` is not a value type.';
}
target[key] = value;
var fullKey = keyPrefix ? keyPrefix + '.' + key : key;
var err = mergeSingleItemOrProperty(target, config, key, fullKey);
if (err) return err;
}
return null; // no error
}

function mergeSingleItemOrProperty(target, config, key, fullKey) {
var origValue = target[key];
var newValue = config[key];

if (!hasCompatibleType(origValue, newValue)) {
return 'Cannot merge values of incompatible types for the option `' +
fullKey + '`.';
}

if (Array.isArray(origValue)) {
return mergeArrays(origValue, newValue, fullKey);
}

if (typeof origValue === 'object') {
return mergeObjects(origValue, newValue, fullKey);
}

target[key] = newValue;
return null; // no error
}

function mergeArrays(target, config, keyPrefix) {
if (target.length !== config.length) {
return 'Cannot merge array values of different length' +
' for the option `' + keyPrefix + '`.';
}

// Use for(;;) to iterate over undefined items, for(in) would skip them.
for (var ix=0; ix < target.length; ix++) {
var fullKey = keyPrefix + '[' + ix + ']';
var err = mergeSingleItemOrProperty(target, config, ix, fullKey);
if (err) return err;
}

return null; // no error
}

function hasCompatibleType(origValue, newValue) {
if (origValue === null || origValue === undefined)
return true;

if (Array.isArray(origValue))
return Array.isArray(newValue);

if (typeof origValue === 'object')
return typeof newValue === 'object';

// Note: typeof Array() is 'object' too,
// we don't need to explicitly check array types
return typeof newValue !== 'object';
}

/**
* Try to read a config file with .json extension
* @param cwd Dirname of the file
Expand Down
158 changes: 151 additions & 7 deletions test/compiler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,24 +125,168 @@ describe('compiler', function() {
expect(db).to.have.property('fromJs', true);
});

it('refuses to merge Object properties', function() {
it('merges new Object values', function() {
var objectValue = { key: 'value' };
appdir.createConfigFilesSync();
appdir.writeConfigFileSync('datasources.local.json', {
db: { nested: { key: 'value' } }
db: { nested: objectValue }
});

expect(function() { boot.compile(appdir.PATH); })
.to.throw(/`nested` is not a value type/);
var instructions = boot.compile(appdir.PATH);

var db = instructions.dataSources.db;
expect(db).to.have.property('nested');
expect(db.nested).to.eql(objectValue);
});

it('refuses to merge Array properties', function() {
it('deeply merges Object values', function() {
appdir.createConfigFilesSync({}, {
email: {
transport: {
host: 'localhost'
}
}
});

appdir.writeConfigFileSync('datasources.local.json', {
email: {
transport: {
host: 'mail.example.com'
}
}
});

var instructions = boot.compile(appdir.PATH);
var email = instructions.dataSources.email;
expect(email.transport.host).to.equal('mail.example.com');
});

it('deeply merges Array values of the same length', function() {
appdir.createConfigFilesSync({}, {
rest: {
operations: [
{
template: {
method: 'POST',
url: 'http://localhost:12345'
}
}
]
}

});
appdir.writeConfigFileSync('datasources.local.json', {
rest: {
operations: [
{
template: {
url: 'http://api.example.com'
}
}
]
}
});

var instructions = boot.compile(appdir.PATH);

var rest = instructions.dataSources.rest;
expect(rest.operations[0].template).to.eql({
method: 'POST', // the value from datasources.json
url: 'http://api.example.com' // overriden in datasources.local.json
});
});

it('merges Array properties', function() {
var arrayValue = ['value'];
appdir.createConfigFilesSync();
appdir.writeConfigFileSync('datasources.local.json', {
db: { nested: ['value'] }
db: { nested: arrayValue }
});

var instructions = boot.compile(appdir.PATH);

var db = instructions.dataSources.db;
expect(db).to.have.property('nested');
expect(db.nested).to.eql(arrayValue);
});

it('refuses to merge Array properties of different length', function() {
appdir.createConfigFilesSync({
nest: {
array: []
}
});

appdir.writeConfigFileSync('config.local.json', {
nest: {
array: [
{
key: 'value'
}
]
}
});

expect(function() { boot.compile(appdir.PATH); })
.to.throw(/array values of different length.*nest\.array/);
});

it('refuses to merge Array of different length in Array', function() {
appdir.createConfigFilesSync({
key: [[]]
});

appdir.writeConfigFileSync('config.local.json', {
key: [['value']]
});

expect(function() { boot.compile(appdir.PATH); })
.to.throw(/array values of different length.*key\[0\]/);
});

it('returns full key of an incorrect Array value', function() {
appdir.createConfigFilesSync({
toplevel: [
{
nested: []
}
]
});

appdir.writeConfigFileSync('config.local.json', {
toplevel: [
{
nested: [ 'value' ]
}
]
});

expect(function() { boot.compile(appdir.PATH); })
.to.throw(/array values of different length.*toplevel\[0\]\.nested/);
});

it('refuses to merge incompatible object properties', function() {
appdir.createConfigFilesSync({
key: []
});
appdir.writeConfigFileSync('config.local.json', {
key: {}
});

expect(function() { boot.compile(appdir.PATH); })
.to.throw(/incompatible types.*key/);
});

it('refuses to merge incompatible array items', function() {
appdir.createConfigFilesSync({
key: [[]]
});
appdir.writeConfigFileSync('config.local.json', {
key: [{}]
});

expect(function() { boot.compile(appdir.PATH); })
.to.throw(/`nested` is not a value type/);
.to.throw(/incompatible types.*key\[0\]/);
});

it('merges app configs from multiple files', function() {
Expand Down