From e3c037e7d9c345be8937c479386fe6c95921c8db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 10:10:05 +0200 Subject: [PATCH 1/9] package: add mocha as dev-dep and test script --- package.json | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 1f6b6e19..7bc8442f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,9 @@ { "version": "3.0.0", "main": "app.js", - "scripts": {}, + "scripts": { + "test": "mocha" + }, "dependencies": { "loopback": "1.x >=1.7.0", "async": "^0.9.0", @@ -21,11 +23,12 @@ }, "name": "loopback-workspace", "devDependencies": { - "loopback-testing": "~0.1.5", + "better-stack-traces": "^1.0.1", "chai": "~1.9.1", "grunt": "~0.4.5", - "grunt-loopback-angular": "~1.1.0", "grunt-docular": "~0.1.2", - "better-stack-traces": "^1.0.1" + "grunt-loopback-angular": "~1.1.0", + "loopback-testing": "~0.1.5", + "mocha": "^1.20.1" } } From 8028ec7f60e4ee2c601960bb2f7a4cef48441d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 10:34:52 +0200 Subject: [PATCH 2/9] ModelDefinition: include `name` in the json file Fix `ModelDefinition.getConfigData` to include model name in the generated data. --- models/model-definition.js | 2 ++ package.json | 2 +- test/model-definition.js | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/models/model-definition.js b/models/model-definition.js index 434a6e69..09d384b9 100644 --- a/models/model-definition.js +++ b/models/model-definition.js @@ -28,6 +28,8 @@ ModelDefinition.getConfigData = function(cache, modelDef) { var configData = {}; var relations = this.getEmbededRelations(); + configData.name = modelDef.name; + relations.forEach(function(relation) { var relatedData = getRelated(cache, modelDef.name, relation); configData[relation.as] = formatRelatedData(relation, relatedData); diff --git a/package.json b/package.json index 7bc8442f..4b36883d 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "grunt": "~0.4.5", "grunt-docular": "~0.1.2", "grunt-loopback-angular": "~1.1.0", - "loopback-testing": "~0.1.5", + "loopback-testing": "^0.2.0", "mocha": "^1.20.1" } } diff --git a/test/model-definition.js b/test/model-definition.js index 4851ce49..f6c1e541 100644 --- a/test/model-definition.js +++ b/test/model-definition.js @@ -70,4 +70,23 @@ describe('ModelDefinition', function() { }} } }); + + describe('ModelDefinition.getConfigData(cache, modelDef)', function() { + beforeEach(function() { + this.cache = app.dataSources.db.connector.cache; + }); + + it('includes `name` property', function(done) { + new TestDataBuilder() + .define('model', ModelDefinition, { + name: 'test-model' + }) + .buildTo(this, function(err) { + if (err) return done(err); + var data = ModelDefinition.getConfigData(this.cache, this.model); + expect(data).to.have.property('name', this.modelName); + done(); + }.bind(this)); + }); + }); }); From 886d1149b86f15a2df3e6bb06a7e386a59a67899 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 13:05:38 +0200 Subject: [PATCH 3/9] Workspace: implement `listAvailableConnectors` The list is maintained in the file `available-connectors.json` for now. --- available-connectors.json | 53 +++++++++++++++++++++++++++++++++++++++ models/workspace.js | 18 +++++++++++++ test/workspace.js | 15 +++++++++++ 3 files changed, 86 insertions(+) create mode 100644 available-connectors.json diff --git a/available-connectors.json b/available-connectors.json new file mode 100644 index 00000000..5e2a69af --- /dev/null +++ b/available-connectors.json @@ -0,0 +1,53 @@ +[ + { + "name": "memory", + "description": "In-memory db", + "supportedByStrongLoop": true + }, + { + "name": "mysql", + "description": "MySQL", + "supportedByStrongLoop": true + }, + { + "name": "postgresql", + "description": "PostgreSQL", + "supportedByStrongLoop": true + }, + { + "name": "oracle", + "description": "Oracle", + "supportedByStrongLoop": true + }, + { + "name": "mssql", + "description": "Microsoft SQL", + "supportedByStrongLoop": true + }, + { + "name": "mongodb", + "description": "MongoDB", + "supportedByStrongLoop": true + }, + { + "name": "soap", + "description": "SOAP webservices", + "supportedByStrongLoop": true + }, + { + "name": "rest", + "description": "REST services", + "supportedByStrongLoop": true + }, + + { + "name": "neo4j", + "description": "Neo4j", + "supportedByStrongLoop": false + }, + { + "name": "kafka", + "description": "Kafka", + "supportedByStrongLoop": false + } +] diff --git a/models/workspace.js b/models/workspace.js index fac76caf..fb84e712 100644 --- a/models/workspace.js +++ b/models/workspace.js @@ -165,3 +165,21 @@ Workspace.createFromTemplate = function(templateName, name, cb) { template: templateName }, cb); } + +/** + * @typedef {{name, description,supportedByStrongLoop}} ConnectorMeta + */ + +/** + * @type {Array.} + * @internal + */ +var staticConnectorList = require('../available-connectors'); + +/** + * List of connectors available on npm. + * @param {function(Error=,Array.=)} cb + */ +Workspace.listAvailableConnectors = function(cb) { + cb(null, staticConnectorList); +}; diff --git a/test/workspace.js b/test/workspace.js index 8e965db9..c8513f60 100644 --- a/test/workspace.js +++ b/test/workspace.js @@ -51,4 +51,19 @@ describe('Workspace', function() { describe('Workspace.listUseableConnectors(cb)', function () { it('should return a list of connectors in package.json'); }); + + + describe('project.listAvailableConnectors(cb)', function() { + before(function(done) { + Workspace.listAvailableConnectors(function(err, list) { + this.connectors = list; + done(err); + }.bind(this)); + }); + + it('should include Memory connector', function() { + var names = this.connectors.map(function(it) { return it.name; }); + expect(names).to.contain('memory'); + }); + }); }); From bcad05b775d3d8e6b230dfa08ca083526f68cc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 13:20:11 +0200 Subject: [PATCH 4/9] ModelProperty: implement `availableTypes` A list of types to offer when creating a new property. --- models/model-property.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/models/model-property.js b/models/model-property.js index 614e6039..875ff5f5 100644 --- a/models/model-property.js +++ b/models/model-property.js @@ -8,3 +8,18 @@ var app = require('../app'); */ var ModelProperty = app.models.ModelProperty; + +/** + * List of built-in types that can be used for `ModelProperty.type`. + * @type {string[]} + */ +ModelProperty.availableTypes = [ + 'string', + 'number', + 'boolean', + 'object', + 'array', + 'date', + 'buffer', + 'geopoint' +]; From dcd88eb4d6569fe0b0beaa63b919f7871ffeee90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 13:45:48 +0200 Subject: [PATCH 5/9] ModelAccessControl: improve metadata providers Rename the property `humanized` to `name`. Add `ModelAccessControl.getBuiltinRoles`. Add the `cb` argument where missing. --- models/model-access-control.js | 64 +++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/models/model-access-control.js b/models/model-access-control.js index 4c08eea9..ab5a1118 100644 --- a/models/model-access-control.js +++ b/models/model-access-control.js @@ -1,5 +1,6 @@ var app = require('../app'); var ACL = require('loopback').ACL; +var Role = require('loopback').Role; /** * Represents an Access Control configuration. @@ -19,18 +20,19 @@ var ModelAccessControl = app.models.ModelAccessControl; * ```js * { * value: 'the value', // may be string or number - * humanized: 'the humanized value' + * name: 'a short name' * } * ``` */ ModelAccessControl.getAccessTypes = function(cb) { cb(null, [ - {value: ACL.READ, humanized: 'Read'}, - {value: ACL.WRITE, humanized: 'Write'}, - {value: ACL.EXECUTE, humanized: 'Execute'} + { name: 'All (match all types)', value: ACL.ALL }, + { name: 'Read', value: ACL.READ }, + { name: 'Write', value: ACL.WRITE }, + { name: 'Execute', value: ACL.EXECUTE }, ]); -} +}; /** * Get the available permission types. @@ -41,20 +43,19 @@ ModelAccessControl.getAccessTypes = function(cb) { * ```js * { * value: 'the value', // may be string or number - * humanized: 'the humanized value' + * name: 'a descriptive name' * } * ``` */ -ModelAccessControl.getPermissionTypes = function() { +ModelAccessControl.getPermissionTypes = function(cb) { cb(null, [ - {value: ACL.DEFAULT, humanized: 'Default'}, - {value: ACL.ALLOW, humanized: 'Allow'}, - {value: ACL.ALARM, humanized: 'Alarm'}, - {value: ACL.AUDIT, humanized: 'Audit'}, - {value: ACL.DENY, humanized: 'Deny'} + { name: 'Explicitly grant access', value: ACL.ALLOW }, + { name: 'Explicitly deny access', value: ACL.DENY }, + { name: 'Generate an alarm of the access', value: ACL.ALARM }, + { name: 'Log the access', value: ACL.AUDIT }, ]); -} +}; /** * Get the available principal types. @@ -65,16 +66,39 @@ ModelAccessControl.getPermissionTypes = function() { * ```js * { * value: 'the value', // may be string or number - * humanized: 'the humanized value' + * name: 'a descriptive name' * } * ``` */ -ModelAccessControl.getPrincipalTypes = function() { +ModelAccessControl.getPrincipalTypes = function(cb) { cb(null, [ - {value: ACL.USER, humanized: 'User'}, - {value: ACL.APP, humanized: 'App'}, - {value: ACL.ROLE, humanized: 'Role'}, - {value: ACL.SCOPE, humanized: 'Scope'} + { name: 'User', value: ACL.USER }, + { name: 'App', value: ACL.APP }, + { name: 'Role', value: ACL.ROLE }, + { name: 'Scope', value: ACL.SCOPE }, ]); -} +}; + +/** + * Get the available built-in roles. + * + * @callback {Function} callback + * @param {Error} err + * @param {Array} types An array of objects with the following format: + * ```js + * { + * value: 'the value', // may be string or number + * name: 'a descriptive name' + * } + * ``` + */ +ModelAccessControl.getBuiltinRoles = function(cb) { + cb(null, [ + { name: 'All users', value: Role.EVERYONE }, + { name: 'Any unauthenticated user', value: Role.UNAUTHENTICATED }, + { name: 'Any authenticated user', value: Role.AUTHENTICATED }, + { name: 'Any user related to the object', value: Role.RELATED }, + { name: 'The user owning the object', value: Role.OWNER }, + ]); +}; From f27075c3ec9d9e2f253b07703608d59cfae051b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 16:06:50 +0200 Subject: [PATCH 6/9] Fix serialization of Model ACLs. Add a new `embedded` property `name` that overrides the serialized property name (`acls` instead of `accessControls`). Fix deserialization of embedded arrays: - use `id` from the data as an id property - this has to be fixed later. - fill `relatedModel[foreignKey]` with the model name to preserve entity relation --- models.json | 2 +- models/definition.js | 8 ++++++-- test/model-definition.js | 32 ++++++++++++++++++++++++++++++-- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/models.json b/models.json index 2a3b8c22..a6039a93 100644 --- a/models.json +++ b/models.json @@ -122,7 +122,7 @@ "foreignKey": "fromModel" }, "accessControls": { - "embed": {"as": "array"}, + "embed": {"name": "acls", "as": "array"}, "type": "hasMany", "model": "ModelAccessControl", "foreignKey": "model" diff --git a/models/definition.js b/models/definition.js index 09409eed..65c1b95d 100644 --- a/models/definition.js +++ b/models/definition.js @@ -1,6 +1,7 @@ var loopback = require('loopback'); var path = require('path'); var app = require('../app'); +var debug = require('debug')('workspace:definition'); var ConfigFile = app.models.ConfigFile; /** @@ -94,7 +95,7 @@ Definition.getEmbededRelations = function() { results.push({ embed: relation.embed, model: relation.model, - as: name, + as: relation.embed.name || name, type: relation.type, foreignKey: relation.foreignKey }); @@ -113,13 +114,16 @@ Definition.addRelatedToCache = function(cache, name, fileData) { if(Array.isArray(relatedData)) { relatedData.forEach(function(config) { - var id = config[relation.foreignKey]; + var id = config[relation.foreignKey] || config.id; + config[relation.foreignKey] = name; + debug('addRelatedToCache %s %s %j', relation.model, id, config); Entity.addToCache(cache, id, config); }); } else if(relatedData) { Object.keys(relatedData).forEach(function(id) { var config = relatedData[id]; config[Entity.dataSource.idName(Entity.modelName)] = id; + debug('addRelatedToCache %s %s %j', relation.model, id, config); Entity.addToCache(cache, id, config); }); } diff --git a/test/model-definition.js b/test/model-definition.js index f6c1e541..81369020 100644 --- a/test/model-definition.js +++ b/test/model-definition.js @@ -1,6 +1,8 @@ var app = require('../app'); var ModelDefinition = app.models.ModelDefinition; +var ModelAccessControl = app.models.ModelAccessControl; var TestDataBuilder = require('loopback-testing').TestDataBuilder; +var ref = TestDataBuilder.ref; var ConfigFile = app.models.ConfigFile; describe('ModelDefinition', function() { @@ -72,8 +74,14 @@ describe('ModelDefinition', function() { }); describe('ModelDefinition.getConfigData(cache, modelDef)', function() { - beforeEach(function() { - this.cache = app.dataSources.db.connector.cache; + beforeEach(givenEmptyWorkspace); + + before(function() { + Object.defineProperty(this, 'cache', { + get: function() { + return app.dataSources.db.connector.cache; + } + }); }); it('includes `name` property', function(done) { @@ -88,5 +96,25 @@ describe('ModelDefinition', function() { done(); }.bind(this)); }); + + it('includes access-control configuration', function(done) { + new TestDataBuilder() + .define('model', ModelDefinition, { + name: 'Car', + componentName: '.' + }) + .define('aclx', ModelAccessControl, { + method: 'ALL', + model: ref('model.name') + }) + .buildTo(this, function(err) { + if (err) return done(err); + var data = ModelDefinition.getConfigData(this.cache, this.model); + expect(data).to.have.property('acls'); + expect(data.acls, 'acls').to.have.length(1); + expect(data.acls[0], 'acls[0]').to.have.property('method', 'ALL'); + done(); + }.bind(this)); + }); }); }); From 30a29a94cecf406707b3a7734506594f654bd621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 16:47:27 +0200 Subject: [PATCH 7/9] connector: fix race condition in loadFromFile Move calls of `loader.emit` to the very last callback, to make sure the events are raised only once and only after all work has already been done. --- connector.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/connector.js b/connector.js index e3724f73..d6430cd1 100644 --- a/connector.js +++ b/connector.js @@ -26,6 +26,13 @@ connector.loadFromFile = function() { } loader = connector.loader = new EventEmitter(); + var done = function(err) { + if (err) + loader.emit('error', err); + else + loader.emit('complete'); + cb(err); + }; // reset the cache var cacheKeys = Object.keys(connector.cache); @@ -35,27 +42,25 @@ connector.loadFromFile = function() { }, {}); ConfigFile.findComponentFiles(function(err, components) { - if(err) return cb(err); + if(err) return done(err); var componentNames = Object.keys(components); - async.each(componentNames, function(component, cb) { + async.each(componentNames, function(component, next) { ComponentDefinition.loadIntoCache(cache, component, components, function(err) { if(err) { - loader.emit('error', err); - return cb(err); + return next(err); } // commit the cache connector.cache = cache; connector.loader = null; - loader.emit('complete'); if(debug.enabled) { Object.keys(cache).forEach(function(model) { debug('setting cache %s => %j', model, Object.keys(cache[model])); }); } - cb(); + next(); }); - }, cb); + }, done); }); } @@ -67,6 +72,7 @@ connector.find = function(model) { var cb = args[args.length - 1]; connector.loadFromFile(function(err) { if(err) return cb(err); + debug('reading from cache %s => %j', model, Object.keys(connector.cache[model])); originalFind.apply(connector, args); }); } From 99b28f53fb653482270345868463a459d4651e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 16:48:55 +0200 Subject: [PATCH 8/9] ModelAccessControl: rename 'method' to 'property' Fix ModelAccessControl model to match the ACL definition expected by loopback. --- models.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models.json b/models.json index a6039a93..d5435815 100644 --- a/models.json +++ b/models.json @@ -172,7 +172,7 @@ }, "ModelAccessControl": { "properties": { - "method": {"type": "string", "default": "ALL"}, + "property": {"type": "string"}, "route": {"type": "string"}, "principalId": {"type": "string"}, "principalType": {"type": "string"}, From b9eb40ebd88b693e517cb7598ccb257fbb3f0342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Mon, 30 Jun 2014 17:12:43 +0200 Subject: [PATCH 9/9] Fix deserialization of embedded key-value maps Fill `relatedModel[foreignKey]` with the model name to preserve entity relation. --- models/definition.js | 1 + test/model-property.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/models/definition.js b/models/definition.js index 65c1b95d..1613cb65 100644 --- a/models/definition.js +++ b/models/definition.js @@ -123,6 +123,7 @@ Definition.addRelatedToCache = function(cache, name, fileData) { Object.keys(relatedData).forEach(function(id) { var config = relatedData[id]; config[Entity.dataSource.idName(Entity.modelName)] = id; + config[relation.foreignKey] = name; debug('addRelatedToCache %s %s %j', relation.model, id, config); Entity.addToCache(cache, id, config); }); diff --git a/test/model-property.js b/test/model-property.js index 9b098e96..5a41b9b5 100644 --- a/test/model-property.js +++ b/test/model-property.js @@ -70,4 +70,19 @@ describe('ModelProperty', function() { expect(properties[this.propertyName]).to.eql({type: 'Boolean'}); }); }); + + describe('modelProperty.load()', function() { + it('should restore model relation', function(done) { + // every query triggers a reload + ModelProperty.all(function(err, list) { + if (err) return done(err); + expect(list[0].toObject()).to.eql(new ModelProperty({ + name: this.propertyName, + type: 'String', + modelName: 'user' + }).toObject()); + done(); + }.bind(this)); + }); + }); });