diff --git a/lib/entity.js b/lib/entity.js index 417c449..24e3d75 100644 --- a/lib/entity.js +++ b/lib/entity.js @@ -40,9 +40,13 @@ class Entity { const type = clazz.type; logger.enabledLevels.trace && log.trace('searching entity "%s" matching index "%s" with "%s"', type, name, value); return clazz.findIdsByIndex(name, value) - .then(result => Promise.map(result, clazz.load.bind(clazz)) - .then(result => _.sortBy(result, 'value.' + (sort || ohm.getSchemaId(type)))) - ); + .then(result => Promise.map(result, clazz.load.bind(clazz))) + .then(result => { + if (!sort) { + return result; + } + return _.sortBy(result, `value.${sort}`); + }); } static findIdsByIndex(name, value) { @@ -116,7 +120,12 @@ class Entity { return clazz.load(id); })); }) - .then(result => _.sortBy(result, 'value.' + (sort || ohm.getSchemaId(type)))); + .then(result => { + if (!sort) { + return result; + } + return _.sortBy(result, `value.${sort}`); + }); } static load(id) { @@ -234,7 +243,7 @@ class Entity { delete(multi) { const entity = this; let id, type; - const localMulti = ohm.createLocalMulti.apply(null, arguments); + const localMulti = ohm.createLocalMulti(multi); return Promise.resolve() .then(() => { type = entity.type; @@ -266,21 +275,7 @@ class Entity { if (typeof entity[idName] !== 'undefined') { return; } - if (schema.idGenerator !== null) { - if (typeof schema.idGenerator === 'function') { - return schema.idGenerator() - .then(id => { - entity.value[idName] = id.toString(); - }); - } - if (schema.idGenerator === 'increment') { - return ohm.exec('incr', ohm.toHash(ohm.config.idsHashPrefix, type/*_.kebabCase(type)*/)) - .then(id => { - entity.value[idName] = id.toString(); - }); - } - entity.value[idName] = ohm.generateId(schema.idGenerator); - } + entity.value[idName] = ohm.generateId(schema.idGenerator); }); } @@ -354,14 +349,14 @@ class Entity { return; } return Promise.each(Array.isArray(value) ? value : [value], value => { - const k = util.format.call(null, link.key, value); - const reverseK = link.reverseKey && util.format.call(null, link.reverseKey, id); + const k = util.format(link.key, value); + const reverseK = link.reverseKey && util.format(link.reverseKey, id); return iterator(link, name, k, reverseK, value); }); } return Promise.resolve() .then(() => { - const k = util.format.call(null, link.reverseKey, id); + const k = util.format(link.reverseKey, id); return iterator(link, name, k); }); }); diff --git a/lib/ohm.js b/lib/ohm.js index 5afd12a..cc1ff1e 100644 --- a/lib/ohm.js +++ b/lib/ohm.js @@ -18,6 +18,7 @@ Promise.promisifyAll(redis.Multi.prototype); const idPattern = '^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'; const idSchema = {type: 'string', pattern: idPattern}; +const idNullableSchema = {type: ['string', 'null'], pattern: idPattern}; let Entity; @@ -37,6 +38,7 @@ const ohm = { entityClasses: {}, idPattern, idSchema, + idNullableSchema, init: (opt = {}) => { logger.enabledLevels.debug && log.debug('initializing redis ohm client'); _.extend(ohm.config, opt); @@ -49,7 +51,6 @@ const ohm = { ohm.entityClasses = {}; ohm.schemas = {}; _.forIn(ohm.config.schemas, (schemaSpec, schemaName) => { - let linkNames, key; if (!schemaSpec[ohm.config.schemaMetaPrefix]) { logger.enabledLevels.debug && log.debug('schema "%s" has no meta : ignore', schemaName); return; @@ -64,12 +65,16 @@ const ohm = { }); _.forIn(meta.operations, (value, type) => { _.forIn(value, (value, operation) => { - key = ['operations', type, operation, 'title'].join('.'); - logger.enabledLevels.trace && log.trace('setting property "%s" to schema "%s"', key, schemaName); - _.set(meta, key, util.format(schemaSpec.title, type, operation)); - key = ['operations', type, operation, 'type'].join('.'); - logger.enabledLevels.trace && log.trace('setting property "%s" to schema "%s"', key, schemaName); - _.set(meta, key, schemaSpec.type); + { + const key = ['operations', type, operation, 'title'].join('.'); + logger.enabledLevels.trace && log.trace('setting property "%s" to schema "%s"', key, schemaName); + _.set(meta, key, util.format(schemaSpec.title, type, operation)); + } + { + const key = ['operations', type, operation, 'type'].join('.'); + logger.enabledLevels.trace && log.trace('setting property "%s" to schema "%s"', key, schemaName); + _.set(meta, key, schemaSpec.type); + } }); }); const schemaId = ohm.getSchemaId(schemaName); @@ -83,7 +88,6 @@ const ohm = { }); logger.enabledLevels.trace && log.trace('setting meta datas to schema "%s"', schemaName); if (schemaSpec[ohm.config.schemaMetaPrefix].links) { - linkNames = schemaSpec[ohm.config.schemaMetaPrefix].links.map(link => link.as); schemaSpec[ohm.config.schemaMetaPrefix].links.forEach(link => { if (typeof schema.properties[link.as] === 'undefined') { logger.enabledLevels.trace && log.trace('setting link property "%s" to schema "%s"', link.as, schemaName); @@ -93,30 +97,34 @@ const ohm = { items: idSchema }; } else { - schema.properties[link.as] = idSchema; + schema.properties[link.as] = idNullableSchema; } } }); } - key = 'operations.db.new.excludeProperties'; - if (typeof _.get(meta, key) === 'undefined') { - logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); - _.set(meta, key, [schemaId]); - } - key = 'operations.db.save.required'; - if (typeof _.get(meta, key) === 'undefined') { - logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); - _.set(meta, key, [schemaId]); - } - key = 'operations.db.save.excludeProperties'; - if (typeof _.get(meta, key) === 'undefined' && linkNames && linkNames.length) { - logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); - _.set(meta, key, linkNames); + /*key = 'operations.db.new.excludeProperties'; + if (typeof _.get(meta, key) === 'undefined') { + logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); + _.set(meta, key, [schemaId]); + }*/ + { + const key = 'operations.db.save.required'; + if (typeof _.get(meta, key) === 'undefined') { + logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); + _.set(meta, key, [schemaId]); + } } - key = 'operations.db.save.minProperties'; - if (typeof _.get(meta, key) === 'undefined') { - logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); - _.set(meta, key, 2); + /*key = 'operations.db.save.excludeProperties'; + if (typeof _.get(meta, key) === 'undefined' && linkNames && linkNames.length) { + logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); + _.set(meta, key, linkNames); + }*/ + { + const key = 'operations.db.save.minProperties'; + if (typeof _.get(meta, key) === 'undefined') { + logger.enabledLevels.trace && log.trace('setting meta "%s" to schema "%s"', key, schemaName); + _.set(meta, key, 2); + } } _.forIn(meta.operations, (value, type) => { _.forIn(value, (value, operation) => { @@ -124,9 +132,8 @@ const ohm = { if (value.includeProperties) { _.set(meta, key, _.pick(schema.properties, value.includeProperties)); delete value.includeProperties; - } else { - _.set(meta, key, _.cloneDeep(schema.properties)); } + _.set(meta, key, _.cloneDeep(schema.properties)); if (value.excludeProperties) { _.set(meta, key, _.omit(_.get(meta, key), value.excludeProperties)); delete value.excludeProperties; diff --git a/package.json b/package.json index e78a893..7021222 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hw-redis-ohm", - "version": "0.5.7", + "version": "0.5.8", "description": "Redis Object Hash Mapping", "main": "lib/ohm.js", "scripts": { diff --git a/spec/ohmSpec.js b/spec/ohmSpec.js index fe9a1de..8b6eaab 100644 --- a/spec/ohmSpec.js +++ b/spec/ohmSpec.js @@ -8,6 +8,7 @@ const logger = require('hw-logger'); const ohm = require('../lib/ohm'); const tUtil = require('./test-util'); const expect = chai.expect; +//const log = logger.log; describe('hw-redis-ohm', () => { @@ -62,8 +63,8 @@ describe('hw-redis-ohm', () => { describe('ohm features', () => { before(() => ohm.start() - .then(() => tUtil.cleanStore() - )); + .then(() => tUtil.cleanStore()) + ); after(() => ohm.stop()); @@ -311,21 +312,21 @@ describe('hw-redis-ohm', () => { }; before(() => ohm.start({schemas}) - .then(() => tUtil.cleanStore() - )); + .then(() => tUtil.cleanStore()) + ); after(() => ohm.stop()); afterEach(() => tUtil.cleanStore()); - it.only('should have schemas', () => { + it('should have schemas', () => { expect(ohm.schemas).to.be.ok; expect(ohm.schemas).to.have.property('group').that.eql({ title: 'Group JSON schema main default', type: 'object', properties: { - id: ohm.idSchema, value: {type: 'string'}, + id: ohm.idSchema, contactIds: {type: 'array', items: ohm.idSchema} }, meta: { @@ -339,6 +340,7 @@ describe('hw-redis-ohm', () => { type: 'object', properties: { value: {type: 'string'}, + id: ohm.idSchema, contactIds: {type: 'array', items: ohm.idSchema} } }, @@ -348,16 +350,17 @@ describe('hw-redis-ohm', () => { title: 'Group JSON schema db save', type: 'object', properties: { + value: {type: 'string'}, id: ohm.idSchema, - value: {type: 'string'} + contactIds: {type: 'array', items: ohm.idSchema} } }, get: { title: 'Group JSON schema db get', type: 'object', properties: { - id: ohm.idSchema, value: {type: 'string'}, + id: ohm.idSchema, contactIds: {type: 'array', items: ohm.idSchema} } } @@ -369,15 +372,15 @@ describe('hw-redis-ohm', () => { title: 'Contact JSON schema main default', type: 'object', properties: { - id: ohm.idSchema, firstname: {type: ['string', 'null']}, lastname: {type: ['string', 'null']}, username: {type: 'string'}, password: {type: 'string'}, email: {type: 'string', format: 'email'}, - dogId: ohm.idSchema, + id: ohm.idSchema, groupIds: {type: 'array', items: ohm.idSchema}, - friendIds: {type: 'array', items: ohm.idSchema} + friendIds: {type: 'array', items: ohm.idSchema}, + dogId: ohm.idNullableSchema }, meta: { indexes: [{name: 'email', unique: true}, {name: 'lastname'}], @@ -410,23 +413,24 @@ describe('hw-redis-ohm', () => { username: {type: 'string'}, password: {type: 'string'}, email: {type: 'string', format: 'email'}, - dogId: ohm.idSchema, + id: ohm.idSchema, groupIds: {type: 'array', items: ohm.idSchema}, - friendIds: {type: 'array', items: ohm.idSchema} + friendIds: {type: 'array', items: ohm.idSchema}, + dogId: ohm.idNullableSchema } }, get: { title: 'Contact JSON schema db get', type: 'object', properties: { - id: ohm.idSchema, firstname: {type: ['string', 'null']}, lastname: {type: ['string', 'null']}, username: {type: 'string'}, email: {type: 'string', format: 'email'}, - dogId: ohm.idSchema, + id: ohm.idSchema, groupIds: {type: 'array', items: ohm.idSchema}, - friendIds: {type: 'array', items: ohm.idSchema} + friendIds: {type: 'array', items: ohm.idSchema}, + dogId: ohm.idNullableSchema } }, save: { @@ -435,12 +439,15 @@ describe('hw-redis-ohm', () => { required: ['id'], minProperties: 2, properties: { - id: ohm.idSchema, firstname: {type: ['string', 'null']}, lastname: {type: ['string', 'null']}, username: {type: 'string'}, password: {type: 'string'}, - email: {type: 'string', format: 'email'} + email: {type: 'string', format: 'email'}, + id: ohm.idSchema, + groupIds: {type: 'array', items: ohm.idSchema}, + friendIds: {type: 'array', items: ohm.idSchema}, + dogId: ohm.idNullableSchema } } } @@ -451,9 +458,9 @@ describe('hw-redis-ohm', () => { title: 'Dog JSON schema main default', type: 'object', properties: { - id: ohm.idSchema, value: {type: 'string'}, - masterId: ohm.idSchema + id: ohm.idSchema, + masterId: ohm.idNullableSchema }, meta: { indexes: [{name: 'value', unique: true}], @@ -472,7 +479,9 @@ describe('hw-redis-ohm', () => { type: 'object', properties: { value: {type: 'string'}, - description: {type: 'string'} + description: {type: 'string'}, + id: ohm.idSchema, + masterId: ohm.idNullableSchema } }, save: { @@ -481,17 +490,18 @@ describe('hw-redis-ohm', () => { required: ['id'], minProperties: 2, properties: { + value: {type: 'string'}, id: ohm.idSchema, - value: {type: 'string'} + masterId: ohm.idNullableSchema } }, get: { title: 'Dog JSON schema db get', type: 'object', properties: { - id: ohm.idSchema, value: {type: 'string'}, - masterId: ohm.idSchema + id: ohm.idSchema, + masterId: ohm.idNullableSchema } } } @@ -537,9 +547,20 @@ describe('hw-redis-ohm', () => { return entity.save() .then(result => { expect(result).to.eql(entity); - expect(entity.getId()).to.match(new RegExp(ohm.patterns.id)); + expect(entity.getId()).to.match(new RegExp(ohm.idPattern)); }); }) + .then(() => new Promise( + resolve => { + groupEntities[0].save().asCallback(err => { + expect(err).to.be.an.instanceof(ohm.e.EntityConflictError); + expect(err).to.have.deep.property('extra.type', 'group'); + expect(err).to.have.deep.property('extra.attrName', 'value'); + expect(err).to.have.deep.property('extra.attrValue', 'vip'); + resolve(); + }); + }) + ) .then(() => Promise .map(groupEntities, groupEntity => ohm.entityClasses.Group .load(groupEntity.getId()) @@ -547,7 +568,8 @@ describe('hw-redis-ohm', () => { groupEntity.value.contactIds = result.value.contactIds; expect(result).to.eql(groupEntity); }) - )) + ) + ) .then(() => { const groupEntity = groupEntities[0]; groupEntity.value.value = 'VIP'; @@ -572,13 +594,14 @@ describe('hw-redis-ohm', () => { return entity.save() .then(result => { expect(result).to.eql(entity); - expect(entity.getId()).to.match(new RegExp(ohm.patterns.id)); + expect(entity.getId()).to.match(new RegExp(ohm.idPattern)); }); - })) + }) + ) .then(() => { const entity = ohm.entityClasses.Contact.create(contacts[0]); return new Promise(resolve => { - entity.save().nodeify(err => { + entity.save().asCallback(err => { expect(err).to.have.property('name', 'EntityConflictError'); expect(err).to.have.deep.property('extra.type', 'contact'); expect(err).to.have.deep.property('extra.attrName', 'email'); @@ -601,13 +624,14 @@ describe('hw-redis-ohm', () => { .then(result => { contactEntities[index].value.dogId = result.getId(); expect(result).to.eql(entity); - expect(entity.getId()).to.match(new RegExp(ohm.patterns.id)); + expect(entity.getId()).to.match(new RegExp(ohm.idPattern)); }); - })) + }) + ) .then(() => { const entity = ohm.entityClasses.Dog.create({value: 'ted', masterId: contactEntities[0].getId()}); return new Promise(resolve => { - entity.save().nodeify(err => { + entity.save().asCallback(err => { expect(err).to.have.property('name', 'EntityConflictError'); expect(err).to.have.deep.property('extra.type', 'dog'); expect(err).to.have.deep.property('extra.attrName', 'masterId'); @@ -621,17 +645,19 @@ describe('hw-redis-ohm', () => { }); }); }) - .then(() => new Promise(resolve => { - ohm.entityClasses.Group.load('badid').nodeify(err => { - expect(err).to.have.property('name', 'EntityNotFoundError'); - expect(err).to.have.deep.property('extra.type', 'group'); - expect(err).to.have.deep.property('extra.attrName', 'id'); - expect(err).to.have.deep.property('extra.attrValue', 'badid'); - expect(err.toString()).to - .equal('EntityNotFoundError: entity "group" not found for "id" with value "badid"'); - resolve(); - }); - })) + .then(() => new Promise( + resolve => { + ohm.entityClasses.Group.load('badid').asCallback(err => { + expect(err).to.have.property('name', 'EntityNotFoundError'); + expect(err).to.have.deep.property('extra.type', 'group'); + expect(err).to.have.deep.property('extra.attrName', 'id'); + expect(err).to.have.deep.property('extra.attrValue', 'badid'); + expect(err.toString()).to + .equal('EntityNotFoundError: entity "group" not found for "id" with value "badid"'); + resolve(); + }); + }) + ) .then(() => Promise .map(groupEntities, groupEntity => ohm.entityClasses.Group .load(groupEntity.getId()) @@ -639,49 +665,64 @@ describe('hw-redis-ohm', () => { groupEntity.value.contactIds = result.value.contactIds; expect(result).to.eql(groupEntity); }) - )) - .then(() => ohm.entityClasses.Group.list('id') - .then(result => { - expect(result).to.eql(groupEntities); - }) + ) ) + .then(() => ohm.entityClasses.Group.list('id')) + .then(result => { + expect(result).to.eql(_.sortBy(groupEntities, 'value.id')); + }) .then(() => { const entity = _.first(contactEntities); - return ohm.entityClasses.Contact.load(entity.getId()).then(result => { - expect(result).to.eql(entity); - }); + return ohm.entityClasses.Contact.load(entity.getId()) + .then(result => { + expect(result).to.eql(entity); + }); }) - .then(() => ohm.entityClasses.Contact.findByIndex('dogId', dogEntities[0].getId()).then(result => { + .then(() => ohm.entityClasses.Contact.findByIndex('dogId', dogEntities[0].getId())) + .then(result => { expect(result).to.be.an('array').of.length(1); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('unknown', 'hello').then(result => { + }) + .then(() => ohm.entityClasses.Contact.findByIndex('unknown', 'hello')) + .then(result => { expect(result).to.be.an('array').of.length(0); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('email', 'unknown@doe.com').then(result => { + }) + .then(() => ohm.entityClasses.Contact.findByIndex('email', 'unknown@doe.com')) + .then(result => { expect(result).to.be.an('array').of.length(0); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('email', 'john@doe.com').then(result => { + }) + .then(() => ohm.entityClasses.Contact.findByIndex('email', 'john@doe.com')) + .then(result => { expect(result).to.be.an('array').of.length(1); expect(_.first(result)).to.eql(_.first(contactEntities)); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('lastname', 'doe').then(result => { + }) + .then(() => ohm.entityClasses.Contact.findByIndex('lastname', 'doe')) + .then(result => { expect(result).to.be.an('array').of.length(2); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('groupIds', groupEntities[0].getId()).then(result => { + }) + .then(() => ohm.entityClasses.Contact.findByIndex('groupIds', groupEntities[0].getId())) + .then(result => { expect(result).to.be.an('array').of.length(2); - expect(_.first(result)).to.eql(contactEntities[0]); - expect(result[1]).to.eql(contactEntities[1]); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('groupIds', 'badid').then(result => { - expect(result).to.be.an('array').of.length(0); - })) - .then(() => ohm.entityClasses.Contact.findByIndex('groupIds', groupEntities[1].getId()).then(result => { - expect(result).to.be.an('array').of.length(1); - expect(_.first(result)).to.eql(contactEntities[1]); - })) + result = _.sortBy(result, 'value.id'); + const expectedContacts = _.sortBy(contactEntities, 'value.id'); + expect(_.first(result)).to.eql(expectedContacts[0]); + expect(result[1]).to.eql(expectedContacts[1]); + }) + .then(() => ohm.entityClasses.Contact + .findByIndex('groupIds', 'badid') + .then(result => { + expect(result).to.be.an('array').of.length(0); + }) + ) + .then(() => ohm.entityClasses.Contact + .findByIndex('groupIds', groupEntities[1].getId()) + .then(result => { + expect(result).to.be.an('array').of.length(1); + expect(_.first(result)).to.eql(contactEntities[1]); + }) + ) .then(() => new Promise(resolve => { - ohm.entityClasses.Group.delete('badid').nodeify(err => { + ohm.entityClasses.Group.delete('badid').asCallback(err => { expect(err).to.have.property('name', 'EntityNotFoundError'); expect(err).to.have.deep.property('extra.type', 'group'); expect(err).to.have.deep.property('extra.attrName', 'id'); @@ -698,18 +739,20 @@ describe('hw-redis-ohm', () => { .then(() => Promise .map(contactEntities, entity => ohm.entityClasses.Contact.delete(entity.getId())) ) - .then(() => new Promise(resolve => { - delete groupEntities[0].value.id; - groupEntities[0].delete().nodeify(err => { - expect(err).to.have.property('name', 'EntityValidationError'); - expect(err).to.have.deep.property('extra.type', 'group'); - expect(err).to.have.deep.property('extra.attrName', 'id'); - expect(err).to.have.deep.property('extra.attrValue').that.is.undefined; - expect(err.toString()).to - .equal('EntityValidationError: entity "group" validation failed for "id" with value "undefined"'); - resolve(); - }); - })) + .then(() => new Promise( + resolve => { + delete groupEntities[0].value.id; + groupEntities[0].delete().asCallback(err => { + expect(err).to.have.property('name', 'EntityValidationError'); + expect(err).to.have.deep.property('extra.type', 'group'); + expect(err).to.have.deep.property('extra.attrName', 'id'); + expect(err).to.have.deep.property('extra.attrValue').that.is.undefined; + expect(err.toString()).to + .equal('EntityValidationError: entity "group" validation failed for "id" with value "undefined"'); + resolve(); + }); + }) + ) .then(() => Promise .map(dogEntities, entity => ohm.entityClasses.Dog.delete(entity.getId())) );