Fix relation race condition in model glob

Globs working depended on the order that models were imported.
Remote sharing is now re-calculated whenever a new model is remoted.
This commit is contained in:
Zak Barbuto 2017-08-15 11:04:46 +09:30
parent 30f3161c65
commit d405432b2d
5 changed files with 118 additions and 77 deletions

View File

@ -534,85 +534,12 @@ function configureModel(ModelCtor, config, app) {
config.dataSource = dataSource; config.dataSource = dataSource;
app.registry.configureModel(ModelCtor, config); app.registry.configureModel(ModelCtor, config);
setSharedMethodSharedProperties(ModelCtor, app, config);
}
function setSharedMethodSharedProperties(model, app, modelConfigs) {
var settings = {};
// apply config.json settings
var config = app.get('remoting');
var configHasSharedMethodsSettings = config &&
config.sharedMethods &&
typeof config.sharedMethods === 'object';
if (configHasSharedMethodsSettings)
util._extend(settings, config.sharedMethods);
// apply model-config.json settings
var modelConfig = modelConfigs.options;
var modelConfigHasSharedMethodsSettings = modelConfig &&
modelConfig.remoting &&
modelConfig.remoting.sharedMethods &&
typeof modelConfig.remoting.sharedMethods === 'object';
if (modelConfigHasSharedMethodsSettings)
util._extend(settings, modelConfig.remoting.sharedMethods);
// validate setting values
Object.keys(settings).forEach(function(setting) {
var settingValue = settings[setting];
var settingValueType = typeof settingValue;
if (settingValueType !== 'boolean')
throw new TypeError(g.f('Expected boolean, got %s', settingValueType));
});
// set sharedMethod.shared using the merged settings
var sharedMethods = model.sharedClass.methods({includeDisabled: true});
// re-map glob style values to regular expressions
var tests = Object
.keys(settings)
.filter(function(setting) {
return settings.hasOwnProperty(setting) && setting.indexOf('*') >= 0;
})
.map(function(setting) {
// Turn * into an testable regexp string
var glob = escapeRegExp(setting).replace(/\*/g, '(.)*');
return {regex: new RegExp(glob), setting: settings[setting]};
}) || [];
sharedMethods.forEach(function(sharedMethod) {
// use the specific setting if it exists
var methodName = sharedMethod.isStatic ? sharedMethod.name : 'prototype.' + sharedMethod.name;
var hasSpecificSetting = settings.hasOwnProperty(methodName);
if (hasSpecificSetting) {
if (settings[methodName] === false) {
sharedMethod.sharedClass.disableMethodByName(methodName);
} else {
sharedMethod.shared = true;
}
} else {
tests.forEach(function(glob) {
if (glob.regex.test(methodName)) {
if (glob.setting === false) {
sharedMethod.sharedClass.disableMethodByName(methodName);
} else {
sharedMethod.shared = true;
}
}
});
}
});
} }
function clearHandlerCache(app) { function clearHandlerCache(app) {
app._handlers = undefined; app._handlers = undefined;
} }
// Sanitize all RegExp reserved characters except * for pattern gobbing
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&');
}
/** /**
* Listen for connections and update the configured port. * Listen for connections and update the configured port.
* *

View File

@ -0,0 +1,75 @@
'use strict';
var util = require('util');
var extend = require('util')._extend;
var g = require('./globalize');
module.exports = function(modelCtor, remotingConfig, modelConfig) {
var settings = {};
// apply config.json settings
var configHasSharedMethodsSettings = remotingConfig &&
remotingConfig.sharedMethods &&
typeof remotingConfig.sharedMethods === 'object';
if (configHasSharedMethodsSettings)
util._extend(settings, remotingConfig.sharedMethods);
// apply model-config.json settings
const options = modelConfig.options;
var modelConfigHasSharedMethodsSettings = options &&
options.remoting &&
options.remoting.sharedMethods &&
typeof options.remoting.sharedMethods === 'object';
if (modelConfigHasSharedMethodsSettings)
util._extend(settings, options.remoting.sharedMethods);
// validate setting values
Object.keys(settings).forEach(function(setting) {
var settingValue = settings[setting];
var settingValueType = typeof settingValue;
if (settingValueType !== 'boolean')
throw new TypeError(g.f('Expected boolean, got %s', settingValueType));
});
// set sharedMethod.shared using the merged settings
var sharedMethods = modelCtor.sharedClass.methods({includeDisabled: true});
// re-map glob style values to regular expressions
var tests = Object
.keys(settings)
.filter(function(setting) {
return settings.hasOwnProperty(setting) && setting.indexOf('*') >= 0;
})
.map(function(setting) {
// Turn * into an testable regexp string
var glob = escapeRegExp(setting).replace(/\*/g, '(.)*');
return {regex: new RegExp(glob), setting: settings[setting]};
}) || [];
sharedMethods.forEach(function(sharedMethod) {
// use the specific setting if it exists
var methodName = sharedMethod.isStatic ? sharedMethod.name : 'prototype.' + sharedMethod.name;
var hasSpecificSetting = settings.hasOwnProperty(methodName);
if (hasSpecificSetting) {
if (settings[methodName] === false) {
sharedMethod.sharedClass.disableMethodByName(methodName);
} else {
sharedMethod.shared = true;
}
} else {
tests.forEach(function(glob) {
if (glob.regex.test(methodName)) {
if (glob.setting === false) {
sharedMethod.sharedClass.disableMethodByName(methodName);
} else {
sharedMethod.shared = true;
}
}
});
}
});
};
// Sanitize all RegExp reserved characters except * for pattern gobbing
function escapeRegExp(str) {
return str.replace(/[\-\[\]\/\{\}\(\)\+\?\.\\\^\$\|]/g, '\\$&');
}

View File

@ -18,6 +18,7 @@ var merge = require('util')._extend;
var assert = require('assert'); var assert = require('assert');
var Registry = require('./registry'); var Registry = require('./registry');
var juggler = require('loopback-datasource-juggler'); var juggler = require('loopback-datasource-juggler');
var configureSharedMethods = require('./configure-shared-methods');
/** /**
* LoopBack core module. It provides static properties and * LoopBack core module. It provides static properties and
@ -78,6 +79,13 @@ function createApplication(options) {
app.loopback = loopback; app.loopback = loopback;
app.on('modelRemoted', function() {
app.models().forEach(function(Model) {
if (!Model.config) return;
configureSharedMethods(Model, app.get('remoting'), Model.config);
});
});
// Create a new instance of models registry per each app instance // Create a new instance of models registry per each app instance
app.models = function() { app.models = function() {
return proto.models.apply(this, arguments); return proto.models.apply(this, arguments);

View File

@ -179,6 +179,8 @@ Registry.prototype.configureModel = function(ModelCtor, config) {
var settings = ModelCtor.settings; var settings = ModelCtor.settings;
var modelName = ModelCtor.modelName; var modelName = ModelCtor.modelName;
ModelCtor.config = config;
// Relations // Relations
if (typeof config.relations === 'object' && config.relations !== null) { if (typeof config.relations === 'object' && config.relations !== null) {
var relations = settings.relations = settings.relations || {}; var relations = settings.relations = settings.relations || {};

View File

@ -832,9 +832,38 @@ describe('loopback', function() {
expect(publicMethods).to.be.empty(); expect(publicMethods).to.be.empty();
}); });
it('hides methods for related models using globs', function() { it('hides methods for related models using globs (model configured first)', function() {
var TestModel = app.registry.createModel(uniqueModelName); const TestModel = app.registry.createModel('TestModel');
var RelatedModel = app.registry.createModel(uniqueModelName); const RelatedModel = app.registry.createModel('RelatedModel');
app.dataSource('test', {connector: 'memory'});
app.model(TestModel, {
dataSource: 'test',
relations: {
related: {
type: 'hasOne',
model: RelatedModel,
},
},
options: {
remoting: {
sharedMethods: {
'*__related': false,
},
},
},
});
app.model(RelatedModel, {dataSource: 'test'});
const publicMethods = getSharedMethods(TestModel);
expect(publicMethods).to.not.include.members([
'prototype.__create__related',
]);
});
it('hides methods for related models using globs (related model configured first)', function() {
const TestModel = app.registry.createModel('TestModel');
const RelatedModel = app.registry.createModel('RelatedModel');
app.dataSource('test', {connector: 'memory'}); app.dataSource('test', {connector: 'memory'});
app.model(RelatedModel, {dataSource: 'test'}); app.model(RelatedModel, {dataSource: 'test'});
app.model(TestModel, { app.model(TestModel, {
@ -854,7 +883,7 @@ describe('loopback', function() {
}, },
}); });
var publicMethods = getSharedMethods(TestModel); const publicMethods = getSharedMethods(TestModel);
expect(publicMethods).to.not.include.members([ expect(publicMethods).to.not.include.members([
'prototype.__create__related', 'prototype.__create__related',