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/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); }); } diff --git a/models.json b/models.json index 2a3b8c22..d5435815 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" @@ -172,7 +172,7 @@ }, "ModelAccessControl": { "properties": { - "method": {"type": "string", "default": "ALL"}, + "property": {"type": "string"}, "route": {"type": "string"}, "principalId": {"type": "string"}, "principalType": {"type": "string"}, diff --git a/models/definition.js b/models/definition.js index 09409eed..1613cb65 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,17 @@ 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; + config[relation.foreignKey] = name; + debug('addRelatedToCache %s %s %j', relation.model, id, config); Entity.addToCache(cache, id, config); }); } 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 }, + ]); +}; 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/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' +]; 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/package.json b/package.json index 1f6b6e19..4b36883d 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.2.0", + "mocha": "^1.20.1" } } diff --git a/test/model-definition.js b/test/model-definition.js index 4851ce49..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() { @@ -70,4 +72,49 @@ describe('ModelDefinition', function() { }} } }); + + describe('ModelDefinition.getConfigData(cache, modelDef)', function() { + beforeEach(givenEmptyWorkspace); + + before(function() { + Object.defineProperty(this, 'cache', { + get: function() { + return 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)); + }); + + 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)); + }); + }); }); 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)); + }); + }); }); 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'); + }); + }); });