diff --git a/api/v1/controllers/samples.js b/api/v1/controllers/samples.js index ad2dd20c2c..97b9a4c784 100644 --- a/api/v1/controllers/samples.js +++ b/api/v1/controllers/samples.js @@ -250,19 +250,26 @@ module.exports = { deleteSampleRelatedLinks(req, res, next) { const resultObj = { reqStartTime: new Date() }; const params = req.swagger.params; - u.findByKey(helper, params) - .then((o) => u.isWritable(req, o, - featureToggles.isFeatureEnabled('enforceWritePermission'))) - .then((o) => { - let jsonData = []; - if (params.relName) { - jsonData = - u.deleteAJsonArrayElement(o.relatedLinks, params.relName.value); - } + let delRlinksPromise; + if (featureToggles.isFeatureEnabled(constants.featureName) && + helper.modelName === 'Sample') { + delRlinksPromise = redisModelSample.deleteSampleRelatedLinks(params); + } else { + delRlinksPromise = u.findByKey(helper, params) + .then((o) => u.isWritable(req, o, + featureToggles.isFeatureEnabled('enforceWritePermission'))) + .then((o) => { + let jsonData = []; + if (params.relName) { + jsonData = + u.deleteAJsonArrayElement(o.relatedLinks, params.relName.value); + } - return o.update({ relatedLinks: jsonData }); - }) - .then((o) => { + return o.update({ relatedLinks: jsonData }); + }); + } + + delRlinksPromise.then((o) => { resultObj.dbTime = new Date() - resultObj.reqStartTime; const retval = u.responsify(o, helper, req.method); diff --git a/api/v1/controllers/subjects.js b/api/v1/controllers/subjects.js index 359cc4019b..d5cdbbd37a 100644 --- a/api/v1/controllers/subjects.js +++ b/api/v1/controllers/subjects.js @@ -225,7 +225,7 @@ module.exports = { const findByKeyPromise = featureToggles.isFeatureEnabled(sampleStoreFeature) ? - u.findByKey(helper, params, ['hierarchy']) : + u.findByKey(helper, params, ['subjectHierarchy']) : u.findByKey(helper, params, ['hierarchy', 'samples']); findByKeyPromise .then((o) => { diff --git a/api/v1/helpers/nouns/samples.js b/api/v1/helpers/nouns/samples.js index 06654df827..53885fefeb 100644 --- a/api/v1/helpers/nouns/samples.js +++ b/api/v1/helpers/nouns/samples.js @@ -12,6 +12,9 @@ 'use strict'; const Sample = require('../../../../db/index').Sample; +const Aspect = require('../../../../db/index').Aspect; +const Subject = require('../../../../db/index').Subject; + const m = 'sample'; const fieldsWithJsonArrayType = ['relatedLinks']; @@ -33,4 +36,10 @@ module.exports = { fieldsWithJsonArrayType, fieldsWithEnum, fieldsToExclude, + publishEvents: true, + associatedModels: { + aspect: Aspect, + subject: Subject, + }, + }; // exports diff --git a/api/v1/helpers/nouns/subjects.js b/api/v1/helpers/nouns/subjects.js index c82f7fa814..f1d7b8f13a 100644 --- a/api/v1/helpers/nouns/subjects.js +++ b/api/v1/helpers/nouns/subjects.js @@ -159,6 +159,7 @@ module.exports = { fieldScopeMap: { samples: 'withSamples', hierarchy: 'hierarchy', + subjectHierarchy: 'subjectHierarchy', }, model: Subject, modelName: 'Subject', diff --git a/api/v1/helpers/verbs/doDelete.js b/api/v1/helpers/verbs/doDelete.js index 60bdcd9efb..29af1e806c 100644 --- a/api/v1/helpers/verbs/doDelete.js +++ b/api/v1/helpers/verbs/doDelete.js @@ -12,6 +12,8 @@ 'use strict'; // eslint-disable-line strict const u = require('./utils'); +const publisher = u.publisher; +const event = u.realtimeEvents; const httpStatus = require('../../constants').httpStatus; const featureToggles = require('feature-toggles'); const constants = require('../../../../cache/sampleStore').constants; @@ -58,6 +60,12 @@ function doDelete(req, res, next, props) { ); } + // publish the delete event to the redis channel + if (props.publishEvents) { + publisher.publishSample(o, props.associatedModels.subject, + event.sample.del); + } + // when a resource is deleted, delete all its associations too u.deleteAllAssociations(o, assocNames); u.logAPI(req, resultObj, o); diff --git a/api/v1/helpers/verbs/doPatch.js b/api/v1/helpers/verbs/doPatch.js index dc253a495d..56acd60814 100644 --- a/api/v1/helpers/verbs/doPatch.js +++ b/api/v1/helpers/verbs/doPatch.js @@ -13,6 +13,8 @@ const featureToggles = require('feature-toggles'); const u = require('./utils'); +const publisher = u.publisher; +const event = u.realtimeEvents; const httpStatus = require('../../constants').httpStatus; const constants = require('../../../../cache/sampleStore').constants; const redisModelSample = require('../../../../cache/models/samples'); @@ -66,6 +68,13 @@ function doPatch(req, res, next, props) { .then((retVal) => { resultObj.dbTime = new Date() - resultObj.reqStartTime; u.logAPI(req, resultObj, retVal); + + // publish the update event to the redis channel + if (props.publishEvents) { + publisher.publishSample(retVal, + props.associatedModels.subject, event.sample.upd); + } + return res.status(httpStatus.OK) .json(u.responsify(retVal, props, req.method)); }) diff --git a/api/v1/helpers/verbs/doPost.js b/api/v1/helpers/verbs/doPost.js index c5e6eef55e..069aedd847 100644 --- a/api/v1/helpers/verbs/doPost.js +++ b/api/v1/helpers/verbs/doPost.js @@ -12,6 +12,8 @@ 'use strict'; // eslint-disable-line strict const u = require('./utils'); +const publisher = u.publisher; +const event = u.realtimeEvents; const httpStatus = require('../../constants').httpStatus; const constants = require('../../../../cache/sampleStore').constants; const redisModelSample = require('../../../../cache/models/samples'); @@ -47,6 +49,13 @@ function doPost(req, res, next, props) { postPromise.then((o) => { resultObj.dbTime = new Date() - resultObj.reqStartTime; u.logAPI(req, resultObj, o); + + // publish the update event to the redis channel + if (props.publishEvents) { + publisher.publishSample(o, props.associatedModels.subject, + event.sample.add, props.associatedModels.aspect); + } + return res.status(httpStatus.CREATED) .json(u.responsify(o, props, req.method)); }) diff --git a/api/v1/helpers/verbs/doPut.js b/api/v1/helpers/verbs/doPut.js index 140848e9db..fd6605c201 100644 --- a/api/v1/helpers/verbs/doPut.js +++ b/api/v1/helpers/verbs/doPut.js @@ -13,6 +13,8 @@ const featureToggles = require('feature-toggles'); const u = require('./utils'); +const publisher = u.publisher; +const event = u.realtimeEvents; const httpStatus = require('../../constants').httpStatus; const constants = require('../../../../cache/sampleStore').constants; const redisModelSample = require('../../../../cache/models/samples'); @@ -75,6 +77,13 @@ function doPut(req, res, next, props) { putPromise.then((o) => { resultObj.dbTime = new Date() - resultObj.reqStartTime; u.logAPI(req, resultObj, o); + + // publish the update event to the redis channel + if (props.publishEvents) { + publisher.publishSample(o, props.associatedModels.subject, + event.sample.upd); + } + res.status(httpStatus.OK).json(u.responsify(o, props, req.method)); }) .catch((err) => u.handleError(next, err, props.modelName)); diff --git a/api/v1/helpers/verbs/utils.js b/api/v1/helpers/verbs/utils.js index a157347245..e06f723c18 100644 --- a/api/v1/helpers/verbs/utils.js +++ b/api/v1/helpers/verbs/utils.js @@ -714,6 +714,7 @@ function checkDuplicateRLinks(rLinkArr) { // ---------------------------------------------------------------------------- module.exports = { + realtimeEvents, publisher, diff --git a/cache/models/samples.js b/cache/models/samples.js index b9a301ecdb..3477ee7694 100644 --- a/cache/models/samples.js +++ b/cache/models/samples.js @@ -526,6 +526,65 @@ module.exports = { }); }, + /** + * Delete sample related links + * @param {Object} params - Request parameters + * @returns {Promise} - Resolves to a sample object + */ + deleteSampleRelatedLinks(params) { + const sampleName = params.key.value; + let currSampObj; + let aspectObj; + + return redisOps.getHashPromise(sampleType, sampleName) + .then((sampObj) => { + if (!sampObj) { + throw new redisErrors.ResourceNotFoundError({ + explanation: 'Sample not found.', + }); + } + + currSampObj = sampObj; + const aspectName = sampleName.split('|')[ONE]; + return redisOps.getHashPromise(aspectType, aspectName); + }) + .then((aspObj) => { + if (!aspObj) { + throw new redisErrors.ResourceNotFoundError({ + explanation: 'Aspect not found.', + }); + } + + let updatedRlinks = []; + if (params.relName) { // delete only this related link + const currRlinks = JSON.parse(currSampObj.relatedLinks); + updatedRlinks = u.deleteAJsonArrayElement( + currRlinks, params.relName.value + ); + } + + // if no change in related links, then return the object. + if (JSON.stringify(updatedRlinks) === + JSON.stringify(currSampObj.relatedLinks)) { + Promise.resolve(cleanAddAspectToSample(currSampObj, aspObj)); + } + + const hmsetObj = {}; + hmsetObj.relatedLinks = updatedRlinks; + hmsetObj.updatedAt = new Date().toString(); + + // stringify arrays + constants.fieldsToStringify.sample.forEach((field) => { + if (hmsetObj[field]) { + hmsetObj[field] = JSON.stringify(hmsetObj[field]); + } + }); + return redisOps.setHashMultiPromise(sampleType, sampleName, hmsetObj); + }) + .then(() => redisOps.getHashPromise(sampleType, sampleName)) + .then((updatedSamp) => cleanAddAspectToSample(updatedSamp, aspectObj)); + }, + /** * Patch sample. First get sample, if not found, throw error, else get aspect. * Update request body with required fields based on value and related links diff --git a/db/model/aspect.js b/db/model/aspect.js index bd2a4fd3c0..107af6daf3 100644 --- a/db/model/aspect.js +++ b/db/model/aspect.js @@ -270,7 +270,7 @@ module.exports = function aspect(seq, dataTypes) { * on aspectStore and the aspect hash. * 2. if the aspect is updated to published, add an entry to the * aspectStore and create the aspect hash - * 3. if the aspect is updated to unpublihsed, delete the entry in the + * 3. if the aspect is updated to unpublished, delete the entry in the * aspectStore, delete the aspect hash and delete the related samples * 4. if the aspect that is updated is already published, update the * the aspect with the new values. diff --git a/db/model/sample.js b/db/model/sample.js index 6c07519583..ddfa0cc31c 100644 --- a/db/model/sample.js +++ b/db/model/sample.js @@ -224,8 +224,10 @@ module.exports = function sample(seq, dataTypes) { .then((o) => resolve(o)) .catch((err) => { if (isBulk) { - /* adding isFailed:true to differentiate failed results from - success results in bulk upsert */ + /* + * adding isFailed:true to differentiate failed results from + * success results in bulk upsert + */ resolve({ explanation: err, isFailed: true }); } else { reject(err); @@ -319,52 +321,6 @@ module.exports = function sample(seq, dataTypes) { ); }, // hooks.beforeCreate - /** - * Publishes the created sample to redis channel *including* the values - * from its aspect association. - * - * @param {Sample} inst - The newly-created instance - */ - afterCreate(inst /* , opts */) { - let samp; - Sample.findOne({ - where: { - name: { - $iLike: inst.getDataValue('name'), - }, - }, - }) - .then((found) => { - samp = found; - return common.sampleAspectAndSubjectArePublished(seq, samp); - }) - .then((published) => { - if (published) { - // augment the sample instance with the subject instance to enable - // filtering by subjecttags in the realtime socketio module - common.augmentSampleWithSubjectAspectInfo(seq, samp) - .then(() => common.publishChange(samp, eventName.add)); - } - }); - }, - - /** - * Publishes the delete to redis subscriber - * - * @param {Sample} inst - The Sample instance which was just deleted - */ - afterDelete(inst /* , opts */) { - return common.sampleAspectAndSubjectArePublished(seq, inst) - .then((published) => { - if (published) { - // augument the sample instance with the subject instance to enable - // filtering by subjecttags in the realtime socketio module - common.augmentSampleWithSubjectAspectInfo(seq, inst) - .then(() => common.publishChange(inst, eventName.del)); - } - }); - }, // hooks.afterDelete - /** * Update isDeleted. * Publishes the deleted sample to redis channel. diff --git a/db/model/subject.js b/db/model/subject.js index 48712b2774..5620776d5c 100644 --- a/db/model/subject.js +++ b/db/model/subject.js @@ -206,6 +206,22 @@ module.exports = function subject(seq, dataTypes) { }, ], }); + Subject.addScope('subjectHierarchy', { + where: { + isPublished: true, + }, + include: [ + { + model: models.Subject, + as: 'descendents', + hierarchy: true, + required: false, + where: { + isPublished: true, + }, + }, + ], + }); }, }, hooks: { diff --git a/realtime/redisPublisher.js b/realtime/redisPublisher.js index 58b0aacbc8..bb6277870b 100644 --- a/realtime/redisPublisher.js +++ b/realtime/redisPublisher.js @@ -92,32 +92,49 @@ function publishObject(inst, event, changedKeys, ignoreAttributes) { * a absolutePath field added to it before the sample is published to the redis * channel. * @param {Object} sampleInst - The sample instance to be published - * @param {Model} model - The subject model used to get the related subject - * instance + * @param {Model} subjectModel - The subject model to get the related + * subject instance * @param {String} event - Type of the event that is being published + * @param {Model} aspectModel - The aspect model to get the related + * aspect instance * @returns {Promise} - which resolves to a sample object */ -function publishSample(sampleInst, model, event) { +function publishSample(sampleInst, subjectModel, event, aspectModel) { const eventType = event || getSampleEventType(sampleInst); const sample = sampleInst.get ? sampleInst.get() : sampleInst; - const subName = sample.name.split('|')[0]; - const options = {}; - options.where = { absolutePath: subName }; - return model.findOne(options) + const nameParts = sample.name.split('|'); + const subName = nameParts[0]; + const aspName = nameParts[1]; + const subOpts = { + where: { + absolutePath: subName, + }, + }; + const aspOpts = { + where: { + name: aspName, + }, + }; + const getAspect = aspectModel ? aspectModel.findOne(aspOpts) : + Promise.resolve(sample.aspect); + return getAspect + .then((asp) => { + sample.aspect = asp.get ? asp.get() : asp; + return subjectModel.findOne(subOpts); + }) .then((sub) => { if (sub) { + /* *pass the sample instance to the publishObject function only if the *aspect and subject are published */ if (sample.aspect && sample.aspect.isPublished && sub.isPublished) { - // attach subject to the sample sample.subject = sub.get(); // attach absolutePath field to the sample sample.absolutePath = subName; - publishObject(sample, eventType); } } diff --git a/tests/cache/models/aspects/aspectCRUD.js b/tests/cache/models/aspects/aspectCRUD.js index 37529610ab..9c7ff12f19 100644 --- a/tests/cache/models/aspects/aspectCRUD.js +++ b/tests/cache/models/aspects/aspectCRUD.js @@ -7,7 +7,7 @@ */ /** - * tests/cache/models/aspect/aspectCRUD.js + * tests/cache/models/aspects/aspectCRUD.js */ 'use strict'; // eslint-disable-line strict diff --git a/tests/cache/models/aspects/deleteRelatedLinks.js b/tests/cache/models/aspects/deleteRelatedLinks.js new file mode 100644 index 0000000000..d2e4f06755 --- /dev/null +++ b/tests/cache/models/aspects/deleteRelatedLinks.js @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2017, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/cache/models/aspects/deleteRelatedLinks.js + */ +'use strict'; // eslint-disable-line strict + +const supertest = require('supertest'); +const api = supertest(require('../../../../index').app); +const constants = require('../../../../api/v1/constants'); +const tu = require('../../../testUtils'); +const rtu = require('../redisTestUtil'); +const expect = require('chai').expect; +const Aspect = tu.db.Aspect; +const allDeletePath = '/v1/aspects/{key}/relatedLinks'; +const oneDeletePath = '/v1/aspects/{key}/relatedLinks/{akey}'; +const redisOps = require('../../../../cache/redisOps'); +const objectType = require('../../../../cache/sampleStore') + .constants.objectType; +const samstoinit = rtu.samstoinit; +const ZERO = 0; +const ONE = 1; + +describe('api: aspects: DELETE RelatedLinks', () => { + let token; + let i; + let name; + + const n = { + name: `${tu.namePrefix}ASPECTNAME`, + timeout: '110s', + relatedLinks: [ + { name: 'rlink0', url: 'https://samples.com' }, + { name: 'rlink1', url: 'https://samples.com' }, + ], + isPublished: true, + }; + + before((done) => { + tu.toggleOverride('enableRedisSampleStore', true); + tu.createToken() + .then((returnedToken) => { + token = returnedToken; + done(); + }) + .catch(done); + }); + + beforeEach((done) => { + Aspect.create(n) + .then((asp) => { + i = asp.id; + name = asp.name; + return samstoinit.eradicate(); + }) + .then(() => samstoinit.init()) + .then(() => done()) + .catch(done); + }); + afterEach(rtu.forceDelete); + after(() => tu.toggleOverride('enableRedisSampleStore', false)); + + it('delete all related links', (done) => { + api.delete(allDeletePath.replace('{key}', i)) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + + expect(res.body.relatedLinks).to.have.length(ZERO); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + expect(JSON.parse(aspect.relatedLinks)).to.have.length(ZERO); + done(); + }); + }); + }); + + it('delete one relatedLink', (done) => { + api.delete(oneDeletePath.replace('{key}', i).replace('{akey}', 'rlink0')) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + + expect(res.body.relatedLinks).to.have.length(ONE); + expect(res.body.relatedLinks).to.have.deep.property('[0].name', 'rlink1'); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + const rlinks = JSON.parse(aspect.relatedLinks); + expect(rlinks).to.have.length(ONE); + expect(rlinks).to.have.deep.property('[0].name', 'rlink1'); + done(); + }); + }); + }); + + it('delete related link by name', (done) => { + api.delete(oneDeletePath.replace('{key}', name).replace('{akey}', 'rlink0')) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + + expect(res.body.relatedLinks).to.have.length(ONE); + expect(res.body.relatedLinks).to.have.deep.property('[0].name', 'rlink1'); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + const rlinks = JSON.parse(aspect.relatedLinks); + expect(rlinks).to.have.length(ONE); + expect(rlinks).to.have.deep.property('[0].name', 'rlink1'); + done(); + }); + }); + }); +}); diff --git a/tests/cache/models/aspects/deleteTags.js b/tests/cache/models/aspects/deleteTags.js new file mode 100644 index 0000000000..f9cee01a6c --- /dev/null +++ b/tests/cache/models/aspects/deleteTags.js @@ -0,0 +1,141 @@ +/** + * Copyright (c) 2017, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or + * https://opensource.org/licenses/BSD-3-Clause + */ + +/** + * tests/cache/models/aspects/deleteTags.js + */ +'use strict'; // eslint-disable-line strict + +const supertest = require('supertest'); +const api = supertest(require('../../../../index').app); +const constants = require('../../../../api/v1/constants'); +const tu = require('../../../testUtils'); +const rtu = require('../redisTestUtil'); +const expect = require('chai').expect; +const Aspect = tu.db.Aspect; +const allDeletePath = '/v1/aspects/{key}/tags'; +const oneDeletePath = '/v1/aspects/{key}/tags/{akey}'; +const redisOps = require('../../../../cache/redisOps'); +const objectType = require('../../../../cache/sampleStore') + .constants.objectType; +const samstoinit = rtu.samstoinit; + +describe(`api: redisStore: aspects: DELETE tags`, () => { + let token; + let aspId; + let aspName; + const tag0 = 'tag0'; + + const n = { + name: `${tu.namePrefix}ASPECTNAME`, + timeout: '110s', + tags: ['tag0', 'tag1'], + isPublished: true, + }; + + before((done) => { + tu.toggleOverride('enableRedisSampleStore', true); + tu.createToken() + .then((returnedToken) => { + token = returnedToken; + done(); + }) + .catch(done); + }); + + beforeEach((done) => { + Aspect.create(n) + .then((asp) => { + aspId = asp.id; + aspName = asp.name; + return samstoinit.eradicate(); + }) + .then(() => samstoinit.init()) + .then(() => done()) + .catch(done); + }); + afterEach(rtu.forceDelete); + after(() => tu.toggleOverride('enableRedisSampleStore', false)); + + it('delete all tags', (done) => { + api.delete(allDeletePath.replace('{key}', aspId)) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + expect(res.body.tags).to.have.length(0); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + expect(JSON.parse(aspect.tags)).to.have.length(0); + done(); + }); + }); + }); + + it('delete one tag', (done) => { + api.delete(oneDeletePath.replace('{key}', aspId).replace('{akey}', tag0)) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + expect(res.body.tags).to.have.length(1); + expect(res.body.tags).to.have.members(['tag1']); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + const tags = JSON.parse(aspect.tags); + expect(tags).to.have.length(1); + expect(tags).to.have.members(['tag1']); + done(); + }); + }); + }); + + it('delete tag by name', (done) => { + api.delete(oneDeletePath.replace('{key}', aspName).replace('{akey}', tag0)) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + expect(res.body.tags).to.have.length(1); + expect(res.body.tags).to.have.members(['tag1']); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + const tags = JSON.parse(aspect.tags); + expect(tags).to.have.length(1); + expect(tags).to.have.members(['tag1']); + done(); + }); + }); + }); + + it('no error if tag not found, no update on tags', (done) => { + api.delete(oneDeletePath.replace('{key}', aspId).replace('{akey}', 'x')) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + expect(res.body.tags).to.have.length(2); + expect(res.body.tags).to.have.members(['tag1', 'tag0']); + redisOps.getHashPromise(objectType.aspect, n.name) + .then((aspect) => { + const tags = JSON.parse(aspect.tags); + expect(tags).to.have.length(2); + expect(tags).to.have.members(['tag1', 'tag0']); + done(); + }); + }); + }); +}); diff --git a/tests/cache/models/samples/delete.js b/tests/cache/models/samples/delete.js index b3ffcee5a3..189e79fb16 100644 --- a/tests/cache/models/samples/delete.js +++ b/tests/cache/models/samples/delete.js @@ -21,6 +21,11 @@ const redisOps = require('../../../../cache/redisOps'); const objectType = require('../../../../cache/sampleStore') .constants.objectType; const expect = require('chai').expect; +const u = require('./utils'); +const allDeletePath = '/v1/samples/{key}/relatedLinks'; +const oneDeletePath = '/v1/samples/{key}/relatedLinks/{akey}'; +const samstoinit = require('../../../../cache/sampleStoreInit'); +const Sample = tu.db.Sample; const ZERO = 0; describe(`api: redisStore: DELETE ${path}`, () => { @@ -101,3 +106,99 @@ describe(`api: redisStore: DELETE ${path}`, () => { }); }); +describe('api: redisStore: samples: DELETE RelatedLinks', () => { + let token; + let sampleName; + + before((done) => { + tu.toggleOverride('enableRedisSampleStore', true); + tu.createToken() + .then((returnedToken) => { + token = returnedToken; + done(); + }) + .catch((err) => done(err)); + }); + + beforeEach((done) => { + u.doSetup() + .then((samp) => { + samp.relatedLinks = [ + { + name: 'rlink0', + url: 'https://samples.com', + }, + { + name: 'rlink1', + url: 'https://samples.com', + }, + ]; + return Sample.create( + samp + ); + }) + .then((samp) => { + sampleName = samp.name; + return samstoinit.eradicate(); + }) + .then(() => samstoinit.init()) + .then(() => done()) + .catch((err) => done(err)); + }); + + afterEach(rtu.forceDelete); + after(() => tu.toggleOverride('enableRedisSampleStore', false)); + + it('delete all related links', (done) => { + api.delete(allDeletePath.replace('{key}', sampleName)) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .end((err, res) => { + if (err) { + done(err); + } + + expect(res.body.relatedLinks).to.have.length(ZERO); + done(); + }); + }); + + it('delete one relatedLink', (done) => { + api.delete( + oneDeletePath.replace('{key}', sampleName).replace('{akey}', 'rlink0') + ) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .expect((res) => { + expect(res.body.relatedLinks).to.have.length(1); + expect(res.body.relatedLinks) + .to.have.deep.property('[0].name', 'rlink1'); + }) + .end((err /* , res */) => { + if (err) { + done(err); + } + + done(); + }); + }); + + it('delete related link by name', (done) => { + api.delete(oneDeletePath.replace('{key}', sampleName) + .replace('{akey}', 'rlink0')) + .set('Authorization', token) + .expect(constants.httpStatus.OK) + .expect((res) => { + expect(res.body.relatedLinks).to.have.length(1); + expect(res.body.relatedLinks).to.have.deep.property('[0].name', 'rlink1'); + }) + .end((err /* , res */) => { + if (err) { + done(err); + } + + done(); + }); + }); +}); + diff --git a/tests/db/model/subject/hierarchy.js b/tests/db/model/subject/hierarchy.js index c4414f861a..b63415580f 100644 --- a/tests/db/model/subject/hierarchy.js +++ b/tests/db/model/subject/hierarchy.js @@ -178,6 +178,28 @@ describe('db: subject: get hierarchy: ', () => { .catch(done); }); + it('using "subject hierarchy" scope: sample should not be ' + + ' included in any level of the hierarchy', (done) => { + Subject.scope('subjectHierarchy').findById(ipar) + .then((o) => { + const gp = o.get({ plain: true }); + expect(gp).to.not.have.property('samples'); + expect(gp.children).to.have.length(1); + expect(gp.childCount).to.equal(1); + const pa = gp.children[0].get(); + expect(pa.children).to.have.length(1); + expect(pa.childCount).to.equal(1); + expect(pa).to.not.have.property('samples'); + const ch = pa.children[0].get(); + expect(ch.parentAbsolutePath).to.equal(pa.absolutePath); + expect(ch.childCount).to.equal(0); + expect(ch).to.not.have.property('children'); + expect(ch).to.not.have.property('samples'); + done(); + }) + .catch(done); + }); + describe('db: subject: get hierarchy: with children', () => { const howManyChildren = 7; diff --git a/tests/realtime/redisPublisher.js b/tests/realtime/redisPublisher.js index 80141308b0..423bfb74be 100644 --- a/tests/realtime/redisPublisher.js +++ b/tests/realtime/redisPublisher.js @@ -7,7 +7,7 @@ */ /** - * tests/realtime/setupSocketIO.js + * tests/realtime/redisPublisher.js */ 'use strict'; @@ -81,6 +81,29 @@ describe('redis Publisher', () => { }) .catch(done); }); + + it('when tried to publish sample without aspect,'+ + ' aspect should be attached', (done) => { + Sample.findById(sampId) + .then((sam) => { + const sampInst = sam.get(); + delete sampInst.aspect; + return publisher.publishSample(sam, Subject, sampleEvent.upd, Aspect); + }) + .then((pubObj) => { + expect(pubObj.aspect).to.not.equal(null); + expect(pubObj.aspect.name).to.equal(humidity.name); + expect(pubObj.aspect.tags.length).to.equal(0); + expect(pubObj.subject).to.not.equal(null); + expect(pubObj.subject.name).to.equal(subjectNA.name); + expect(pubObj.subject.tags.length).to.equal(0); + expect(pubObj.absolutePath).to.equal(subjectNA.name); + expect(pubObj.aspect.tags.length).to.equal(0); + + done(); + }) + .catch(done); + }); }); describe('getSampleEventType function tests: ', () => { @@ -115,5 +138,4 @@ describe('redis Publisher', () => { .catch(done); }); }); - });