diff --git a/connector.js b/connector.js index a5fd824f..8555f1d1 100644 --- a/connector.js +++ b/connector.js @@ -1,4 +1,5 @@ var app = require('./app'); +var loopback = require('loopback'); var connector = app.dataSources.db.connector; var ComponentDefinition = app.models.ComponentDefinition; var ConfigFile = app.models.ConfigFile; @@ -89,3 +90,34 @@ connector.all = function(model) { originalAll.apply(connector, args); }); } + +connector.getIdValue = function(model, data) { + var Entity = loopback.getModel(model); + var entity = new Entity(data); + return entity.getUniqueId(); +} + +connector.create = function create(model, data, callback) { + var Entity = loopback.getModel(model); + var entity = new Entity(data); + var id = entity.getUniqueId(); + + this.setIdValue(model, data, id); + + if(!this.cache[model]) { + this.cache[model] = {}; + } + + this.cache[model][id] = serialize(data); + this.saveToFile(id, function(err) { + if(err) return callback(err); + callback(null, id); + }); +}; + +function serialize(obj) { + if(obj === null || obj === undefined) { + return obj; + } + return JSON.stringify(obj); +} diff --git a/models.json b/models.json index 965ba551..1e53e19d 100644 --- a/models.json +++ b/models.json @@ -16,7 +16,8 @@ }, "PackageDefinition": { "properties": { - "componentName": {"type": "string", "required": true} + "componentName": {"type": "string", "required": true}, + "name": {"id": true, "type": "string"} }, "public": false, "dataSource": "db", @@ -30,6 +31,7 @@ }, "ComponentDefinition": { "properties": { + "name": {"id": true, "type": "string"}, "defaultPermission": {"type": "string"} }, "public": true, @@ -62,7 +64,9 @@ }, "ComponentModel": { "properties": { - "name": {"type": "string", "id": true} + "id": {"type": "string", "id": true}, + "name": {"type": "string"}, + "componentName": {"type": "string", "required": true} }, "dataSource": "db", "options": { @@ -90,6 +94,8 @@ }, "ModelDefinition": { "properties": { + "id": {"type": "string", "id": true}, + "name": "string", "plural": "string", "strict": "boolean", "scopes": "array", @@ -109,46 +115,53 @@ ], "configExtensions": ["json"], "relations": { + "component": { + "type": "belongsTo", + "model": "ComponentDefinition", + "foreignKey": "componentName" + }, "properties": { "embed": {"as": "object", "key": "name"}, "type": "hasMany", "model": "ModelProperty", - "foreignKey": "modelName" + "foreignKey": "modelId" }, "validations": { "embed": {"as": "array"}, "type": "hasMany", "model": "PropertyValidation", - "foreignKey": "modelName" + "foreignKey": "modelId" }, "relations": { "embed": {"as": "object", "key": "as"}, "type": "hasMany", "model": "ModelRelation", - "foreignKey": "fromModel" + "foreignKey": "fromModelId" }, "accessControls": { "embed": {"name": "acls", "as": "array"}, "type": "hasMany", "model": "ModelAccessControl", - "foreignKey": "model" + "foreignKey": "modelId" }, "methods": { "embed": {"as": "array"}, "type": "hasMany", "model": "ModelMethod", - "foreignKey": "model" + "foreignKey": "modelId" }, "views": { "type": "hasMany", "model": "ViewDefinition", - "foreignKey": "model" + "foreignKey": "modelId" } } } }, "ModelMethod": { "properties": { + "id": {"type": "string", "id": true}, + "modelId": {"type": "string", "required": true}, "aliases": {"type": "array"}, "isStatic": {"type": "boolean"}, "accepts": {"type": "array"}, @@ -165,6 +178,8 @@ }, "ModelRelation": { "properties": { + "id": {"type": "string", "id": true}, + "modelId": {"type": "string", "required": true}, "type": {"type": "string"}, "as": {"type": "string"}, "foreignKey": {"type": "string"}, @@ -173,11 +188,20 @@ "public": true, "dataSource": "db", "options": { - "base": "WorkspaceEntity" + "base": "WorkspaceEntity", + "relations": { + "model": { + "type": "belongsTo", + "model": "ModelDefinition", + "foreignKey": "modelId" + } + } } }, "ModelAccessControl": { "properties": { + "id": {"type": "string", "id": true}, + "modelId": {"type": "string", "required": true}, "property": {"type": "string"}, "route": {"type": "string"}, "principalId": {"type": "string"}, @@ -187,17 +211,25 @@ "public": true, "dataSource": "db", "options": { - "base": "WorkspaceEntity" + "base": "WorkspaceEntity", + "relations": { + "model": { + "type": "belongsTo", + "model": "ModelDefinition", + "foreignKey": "modelId" + } + } } }, "ModelProperty": { "properties": { + "id": {"type": "string", "id": true}, + "modelId": {"type": "string", "required": true}, "type": {"type": "string"}, - "name": {"type": "string", "id": true}, + "name": {"type": "string"}, "generated": {"type": "boolean"}, "required": {"type": "boolean"}, "index": {"type": "boolean"}, - "modelName": "string", "desc": {"type": "string"} }, "public": true, @@ -209,16 +241,23 @@ "type": "hasMany", "model": "PropertyValid", "foreignKey": "property" + }, + "model": { + "type": "belongsTo", + "model": "ModelDefinition", + "foreignKey": "modelId" } } } }, "DatabaseColumn": { - "connector": {"type": "string"}, - "columnName": {"type": "string"}, - "dataType": {"type": "string"}, - "dataLength": {"type": "string"}, - "nullable": {"type": "boolean"}, + "properties": { + "connector": {"type": "string"}, + "columnName": {"type": "string"}, + "dataType": {"type": "string"}, + "dataLength": {"type": "string"}, + "nullable": {"type": "boolean"} + }, "options": { "base": "WorkspaceEntity" }, @@ -227,6 +266,8 @@ }, "PropertyValidation": { "properties": { + "id": {"type": "string", "id": true}, + "propertyId": {"type": "string", "required": true}, "type": {"type": "string"}, "message": {"type": "string"}, "min": {"type": "number"}, @@ -238,7 +279,14 @@ "public": true, "dataSource": "db", "options": { - "base": "WorkspaceEntity" + "base": "WorkspaceEntity", + "relations": { + "model": { + "type": "belongsTo", + "model": "ModelProperty", + "foreignKey": "propertyId" + } + } } }, "ViewDefinition": { @@ -265,12 +313,14 @@ }, "DataSourceDefinition": { "properties": { + "id": {"type": "string", "id": true}, "host": {"type": "string"}, "port": {"type": "number"}, "url": {"type": "string"}, "database": {"type": "string"}, "username": {"type": "string"}, - "password": {"type": "string"} + "password": {"type": "string"}, + "componentName": {"type": "string", "required": true} }, "public": true, "dataSource": "db", diff --git a/models/component-definition.js b/models/component-definition.js index 6b67b87f..918a1bb9 100644 --- a/models/component-definition.js +++ b/models/component-definition.js @@ -44,7 +44,7 @@ ComponentDefinition.loadIntoCache = function(cache, componentName, components, c packageFile.load(cb); }, function(cb) { packageFile.data.componentName = componentName; - PackageDefinition.addToCache(cache, componentName, packageFile.data || {}); + PackageDefinition.addToCache(cache, packageFile.data || {}); cb(); }); } @@ -57,7 +57,7 @@ ComponentDefinition.loadIntoCache = function(cache, componentName, components, c component.data.configFile = component.path; component.data.name = componentName; debug('adding to cache component file [%s]', component.path); - ComponentDefinition.addToCache(cache, componentName, component.data); + ComponentDefinition.addToCache(cache, component.data); cb(); }); } else { @@ -67,7 +67,7 @@ ComponentDefinition.loadIntoCache = function(cache, componentName, components, c configFile: path.join(componentName, 'config.json') }; debug('adding to cache component entry [%s]', componentData.configFile); - ComponentDefinition.addToCache(cache, componentName, componentData); + ComponentDefinition.addToCache(cache, componentData); cb(); }); } @@ -86,7 +86,7 @@ ComponentDefinition.loadIntoCache = function(cache, componentName, components, c var componentModel = modelDefs[modelName]; componentModel.componentName = componentName; componentModel.name = modelName; - ComponentModel.addToCache(cache, modelName, componentModel); + ComponentModel.addToCache(cache, componentModel); }); cb(); @@ -103,11 +103,13 @@ ComponentDefinition.loadIntoCache = function(cache, componentName, components, c var def = configFile.data || {}; def.componentName = componentName; def.configFile = configFile.path; + var modelDef = new ModelDefinition(def); debug('loading [%s] model definition into cache', def.name); - ModelDefinition.addToCache(cache, def.name, def); - ModelDefinition.addRelatedToCache(cache, def.name, def); + ModelDefinition.addToCache(cache, def); + ModelDefinition.addRelatedToCache(cache, def, componentName + , modelDef.getUniqueId()); }); cb(); }); @@ -126,7 +128,7 @@ ComponentDefinition.loadIntoCache = function(cache, componentName, components, c def.name = dataSourceName; def.componentName = componentName; debug('loading [%s] dataSource into cache', dataSourceName); - DataSourceDefinition.addToCache(cache, dataSourceName, def); + DataSourceDefinition.addToCache(cache, def); }); cb(); @@ -184,11 +186,11 @@ ComponentDefinition.saveToFs = function(cache, componentDef, cb) { var cachedDataSources = DataSourceDefinition.allFromCache(cache); cachedDataSources.forEach(function(dataSourceDef) { - if (dataSourceDef.componentName === componentName) { + if(dataSourceDef.componentName === componentName) { dataSourcePath = DataSourceDefinition.getPath(componentName, dataSourceDef); dataSoureConfig[dataSourceDef.name] = dataSourceDef; - // remove extra data that shouldn't be persisted to the fs delete dataSourceDef.name; + delete dataSourceDef.id; delete dataSourceDef.componentName; } }); @@ -204,11 +206,12 @@ ComponentDefinition.saveToFs = function(cache, componentDef, cb) { var componentModelsConfig = componentModelFile.data = {}; cachedComponentModels.forEach(function(componentModel) { - if (componentModel.componentName !== componentName) return; - componentModelsConfig[componentModel.name] = componentModel; - // remove extra data that shouldn't be persisted to the fs - delete componentModel.name; - delete componentModel.componentName; + if(componentModel.componentName === componentName) { + componentModelsConfig[componentModel.name] = componentModel; + delete componentModel.name; + delete componentModel.id; + delete componentModel.componentName; + } }); filesToSave.push(componentModelFile); @@ -222,6 +225,8 @@ ComponentDefinition.saveToFs = function(cache, componentDef, cb) { delete modelDef.dataSource; var modelConfigFile = ModelDefinition.getConfigFile(componentName, modelDef); modelConfigFile.data = ModelDefinition.getConfigData(cache, modelDef); + delete modelDef.componentName; + delete modelDef.id; filesToSave.push(modelConfigFile); } }); @@ -244,3 +249,7 @@ ComponentDefinition.hasApp = function(componentDef) { // e.g. package.json > loopback-workspace > app: true|false return componentDef.name !== '.'; }; + +ComponentDefinition.getUniqueId = function(data) { + return data.name || null; +} diff --git a/models/definition.js b/models/definition.js index 1613cb65..9e39ee5c 100644 --- a/models/definition.js +++ b/models/definition.js @@ -15,7 +15,6 @@ var Definition = app.model('Definition', { "properties": { "name": { "type": "string", - "id": true, "required": true }, "dir": { @@ -106,26 +105,28 @@ Definition.getEmbededRelations = function() { return results; } -Definition.addRelatedToCache = function(cache, name, fileData) { +Definition.addRelatedToCache = function(cache, fileData, componentName, fk) { var Definition = this; this.getEmbededRelations().forEach(function(relation) { var relatedData = fileData[relation.as]; var Entity = loopback.getModel(relation.model); + var entity; if(Array.isArray(relatedData)) { relatedData.forEach(function(config) { - 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); + config[relation.foreignKey] = fk; + config.componentName = componentName; + debug('addRelatedToCache %s %j', relation.model, config); + Entity.addToCache(cache, 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); + Object.keys(relatedData).forEach(function(embedId) { + var config = relatedData[embedId]; + config[relation.foreignKey] = fk; + config[relation.embed.key] = embedId; + config.componentName = componentName; + debug('addRelatedToCache %s %j', relation.model, config); + Entity.addToCache(cache, config); }); } }); diff --git a/models/model-definition.js b/models/model-definition.js index 09d384b9..d0ebd93d 100644 --- a/models/model-definition.js +++ b/models/model-definition.js @@ -31,7 +31,7 @@ ModelDefinition.getConfigData = function(cache, modelDef) { configData.name = modelDef.name; relations.forEach(function(relation) { - var relatedData = getRelated(cache, modelDef.name, relation); + var relatedData = getRelated(cache, modelDef.id, relation); configData[relation.as] = formatRelatedData(relation, relatedData); }); @@ -51,7 +51,6 @@ function getRelated(cache, id, relation) { function formatRelatedData(relation, relatedData) { var result; assert(relation.embed && relation.embed.as, 'embed requires "as"'); - cleanRelatedData(relatedData, relation); switch(relation.embed.as) { case 'object': assert(relation.embed.key, 'embed as object requires "key"'); @@ -59,11 +58,12 @@ function formatRelatedData(relation, relatedData) { relatedData.forEach(function(related) { var key = related[relation.embed.key]; result[key] = related; - delete related[relation.embed.key]; }); + cleanRelatedData(relatedData, relation); return result; break; case 'array': + cleanRelatedData(relatedData, relation); return relatedData; break; } @@ -95,6 +95,11 @@ ModelDefinition.toFilename = function(name) { function cleanRelatedData(relatedData, relation) { relatedData.forEach(function(obj) { assert(relation.foreignKey, 'embeded relation must have foreignKey'); - delete obj[relation.foreignKey]; + delete obj[relation.foreignKey]; + delete obj[relation.embed.key]; + // TODO(ritch) we can probably generalize these + delete obj.id; + delete obj.componentName; + delete obj.modelName; }); } diff --git a/models/package-definition.js b/models/package-definition.js new file mode 100644 index 00000000..c0c4200e --- /dev/null +++ b/models/package-definition.js @@ -0,0 +1,5 @@ +var PackageDefinition = require('../app').models.PackageDefinition; + +PackageDefinition.prototype.getUniqueId = function() { + return this.name || null; +} diff --git a/models/workspace-entity.js b/models/workspace-entity.js index d95cf65d..553c6665 100644 --- a/models/workspace-entity.js +++ b/models/workspace-entity.js @@ -2,28 +2,63 @@ var path = require('path'); var app = require('../app'); var WorkspaceEntity = app.model('WorkspaceEntity', { "properties": { - "configFile": {"type": "string"}, - "scriptFile": {"type": "string"}, - "configLineNum": {"type": "number"}, - "locked": {"type": "boolean"} + "configFile": {"type": "string"} }, "public": false, "dataSource": "db" }); -/** - * Get the file location of the entity. - * - * @returns {String} location For example - * `"/foo/bar/bat/baz.json:237"` - */ +WorkspaceEntity.getUniqueId = function(data) { + var sep = this.settings.idSeparator || '.'; + var parts = this.getUniqueIdParts(data); + if(parts.length >= 1) { + return parts.join(sep); + } + return null; +} -WorkspaceEntity.prototype.getLocation = function() { - throwMustImplement('getLocation', this.constructor); +WorkspaceEntity.prototype.getUniqueId = function() { + return this.constructor.getUniqueId(this); } -function throwMustImplement(name, constructor) { - throw new Error('must be implemented by ', constructor.name); +WorkspaceEntity.getUniqueIdParts = function(data) { + var settings = this.settings; + var parentPropertyName = this.getParentPropertyName(); + var parts = []; + var parentId = parentPropertyName && data[parentPropertyName]; + var splitParentId = parentId && parentId.split('.'); + var parentIdIsNotRootComponent = parentId !== '.'; + var name = data.name; + + if(parentPropertyName) { + if(parentId) { + if(parentIdIsNotRootComponent) { + parts.push.apply(parts, splitParentId); + } + } else { + // cannot construct the id without the parent id + return []; + } + } + + if(name) parts.push(name); + + return parts; +} + +WorkspaceEntity.getParentPropertyName = function() { + var relations = this.relations; + if(!relations) return; + + var relationNames = Object.keys(relations); + var relation; + + for(var i = 0; i < relationNames.length; i++) { + relation = relations[relationNames[i]]; + if(relation.type === 'belongsTo') { + return relation.keyFrom; + } + } } /** @@ -41,7 +76,10 @@ WorkspaceEntity.clearCache = function(cache) { cache[this.modelName] = {}; } -WorkspaceEntity.addToCache = function(cache, id, val) { +WorkspaceEntity.addToCache = function(cache, val) { + var Entity = this; + var id = Entity.getUniqueId(val); + val[this.dataSource.idName(Entity.modelName)] = id; cache[this.modelName][id] = JSON.stringify(val); } diff --git a/models/workspace.js b/models/workspace.js index ae057767..d0cc3db5 100644 --- a/models/workspace.js +++ b/models/workspace.js @@ -129,6 +129,7 @@ Workspace.addComponent = function(options, cb) { } if(template.relations) { + setComponentName(template.relations); steps.push(function(cb) { async.each(template.relations, ModelRelation.create.bind(ModelRelation), cb); diff --git a/templates/rest/component.js b/templates/rest/component.js index 441a2c4d..977a6702 100644 --- a/templates/rest/component.js +++ b/templates/rest/component.js @@ -38,6 +38,20 @@ template.componentModels = [ ]; template.relations = [ + { + fromModel: 'user', + model: 'access-token', + type: 'hasMany', + foreignKey: 'userId', + modelId: 'rest.user' + }, + { + fromModel: 'role', + type: 'hasMany', + model: 'roleMapping', + foreignKey: 'roleId', + modelId: 'rest.role' + } ]; template.datasources = [ diff --git a/test/component-definition.js b/test/component-definition.js index a17513e9..f1f7af74 100644 --- a/test/component-definition.js +++ b/test/component-definition.js @@ -1,8 +1,21 @@ var fs = require('fs-extra'); +var ComponentDefinition = require('../app').models.ComponentDefinition; -describe('ComponentDefinition', function() { - describe('componentDefinition.saveToFs', function() { +describe('ComponentDefinition', function () { + describe('ComponentDefinition.create(def, cb)', function () { beforeEach(givenBasicWorkspace); + + it('should use name as the id', function (done) { + ComponentDefinition.create({ + name: 'foo' + }, function(err, def) { + expect(err).to.not.exist; + expect(def).to.not.have.property('id'); + expect(def.name).to.equal('foo'); + done(); + }); + }); + it('omits `name` in config.json', function() { var content = fs.readJsonFileSync(SANDBOX + '/rest/config.json'); expect(content).to.not.have.property('name'); diff --git a/test/data-source-definition.js b/test/data-source-definition.js index 3317421b..8098bdbc 100644 --- a/test/data-source-definition.js +++ b/test/data-source-definition.js @@ -40,11 +40,19 @@ describe('DataSourceDefinition', function() { done(); }); }); - it('should create a config file', function(done) { - this.configFile.exists(function(err, exists) { - expect(err).to.not.exist; - expect(exists).to.equal(true); - done(); + describe('config file', function () { + it('should be created', function(done) { + this.configFile.exists(function(err, exists) { + expect(err).to.not.exist; + expect(exists).to.equal(true); + done(); + }); + }); + it('should not contain id properties', function () { + var configData = this.configFile.data; + var dsConfig = configData.foo; + expect(dsConfig).to.not.have.property('id'); + expect(dsConfig).to.not.have.property('componentName'); }); }); it('shoulb be persist multiple to the config file', function (done) { diff --git a/test/model-definition.js b/test/model-definition.js index c78f0efb..0098ac67 100644 --- a/test/model-definition.js +++ b/test/model-definition.js @@ -105,7 +105,7 @@ describe('ModelDefinition', function() { }) .define('aclx', ModelAccessControl, { method: 'ALL', - model: ref('model.name') + modelId: ref('model.id') }) .buildTo(this, function(err) { if (err) return done(err); diff --git a/test/model-property.js b/test/model-property.js index 5a41b9b5..093e850b 100644 --- a/test/model-property.js +++ b/test/model-property.js @@ -18,7 +18,7 @@ describe('ModelProperty', function() { var property = { name: test.propertyName, type: 'String', - modelName: 'user' + modelId: 'rest.user' }; ModelProperty.create(property, function(err, property) { if(err) return done(err); @@ -36,6 +36,9 @@ describe('ModelProperty', function() { expect(properties).to.have.property(this.propertyName); expect(properties[this.propertyName]).to.eql({type: type}); }); + it('should have the correct id', function () { + expect(this.property.id).to.equal('rest.user.myProperty'); + }); }); describe('ModelProperty.find(filter, cb)', function (done) { @@ -76,11 +79,16 @@ describe('ModelProperty', function() { // every query triggers a reload ModelProperty.all(function(err, list) { if (err) return done(err); - expect(list[0].toObject()).to.eql(new ModelProperty({ + var actual = list[0].toObject(); + var expected = new ModelProperty({ name: this.propertyName, type: 'String', - modelName: 'user' - }).toObject()); + componentName: 'rest', + id: 'rest.user.myProperty', + modelId: 'rest.user' + }).toObject(); + + expect(actual).to.eql(expected); done(); }.bind(this)); }); diff --git a/test/workspace-entity.js b/test/workspace-entity.js index 2027b5e3..a1cf3cdc 100644 --- a/test/workspace-entity.js +++ b/test/workspace-entity.js @@ -1,17 +1,25 @@ var app = require('../app'); var WorkspaceEntity = app.models.WorkspaceEntity; +var ComponentDefinition = app.models.ComponentDefinition; var TestDataBuilder = require('loopback-testing').TestDataBuilder; describe('WorkspaceEntity', function() { - describe('workspaceEntity.getLocation()', function() { - it('Get the file location of the entity.', function() { - - }); - }); - - describe('WorkspaceEntity.getWorkspaceDir()', function() { - it('Get the Workspace directory.', function() { - + describe('workspaceEntity.getUniqueId()', function() { + it('gets the unique identifier of the entity', function() { + var MyWorkspaceEntity = WorkspaceEntity.extend('MyWorkspaceEntity'); + MyWorkspaceEntity.attachTo(app.dataSources.db); + MyWorkspaceEntity.belongsTo(ComponentDefinition, { + as: 'component', + foreignKey: 'componentName' + }); + var bar = new MyWorkspaceEntity({ + componentName: 'foo', + name: 'bar' + }); + var expected = 'foo.bar'; + expect(bar.getUniqueId()).to.equal(expected); + expect(MyWorkspaceEntity.getUniqueId(bar)).to.equal(expected); + expect(MyWorkspaceEntity.getUniqueId(bar.toObject())).to.equal(expected); }); }); }); diff --git a/test/workspace.js b/test/workspace.js index 92692359..303fa2f4 100644 --- a/test/workspace.js +++ b/test/workspace.js @@ -65,18 +65,18 @@ describe('Workspace', function() { }); }); - it('it should create a set of component definitions', function() { + it('should create a set of component definitions', function() { var componentNames = toNames(this.components); expect(componentNames).to.contain('rest'); expect(componentNames).to.contain('.'); expect(componentNames).to.contain('server'); }); - it('it should not create a set of model definitions', function() { + it('should not create a set of model definitions', function() { expect(this.models).to.be.empty; }); - it('it should create a set of data source definitions', function() { + it('should create a set of data source definitions', function() { var dataSourceNames = toNames(this.dataSources); expect(dataSourceNames).to.contain('db'); }); @@ -88,10 +88,6 @@ 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) {