diff --git a/.eslintrc.yml b/.eslintrc.yml index d48c246d..9d8eff76 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -174,7 +174,7 @@ rules: no-restricted-properties: error no-restricted-syntax: error no-return-assign: 'off' - no-return-await: error + no-return-await: 'off' no-script-url: error no-self-assign: - error diff --git a/app/controllers/references-controller.js b/app/controllers/references-controller.js index 741866c4..7ba2e259 100644 --- a/app/controllers/references-controller.js +++ b/app/controllers/references-controller.js @@ -2,6 +2,7 @@ const referencesService = require('../services/references-service'); const logger = require('../lib/logger'); +const { DuplicateIdError } = require('../exceptions'); exports.retrieveAll = async function(req, res) { const options = { @@ -44,7 +45,7 @@ exports.create = async function(req, res) { return res.status(201).send(reference); } catch(err) { - if (err.message === referencesService.errors.duplicateId) { + if (err instanceof DuplicateIdError) { logger.warn("Duplicate source_name"); return res.status(409).send('Unable to create reference. Duplicate source_name.'); } @@ -60,11 +61,12 @@ exports.update = async function(req, res) { const referenceData = req.body; // Create the reference - try { + try { const reference = await referencesService.update(referenceData); if (!reference) { return res.status(404).send('Reference not found.'); - } else { + } + else { logger.debug('Success: Updated reference with source_name ' + reference.source_name); return res.status(200).send(reference); } diff --git a/app/repository/_base.repository.js b/app/repository/_base.repository.js index f2da0120..60cef2aa 100644 --- a/app/repository/_base.repository.js +++ b/app/repository/_base.repository.js @@ -147,7 +147,6 @@ class BaseRepository extends AbstractRepository { // eslint-disable-next-line class-methods-use-this async save(data) { - try { const document = new this.model(data); return await document.save(); diff --git a/app/repository/references-repository.js b/app/repository/references-repository.js new file mode 100644 index 00000000..b5fd32fe --- /dev/null +++ b/app/repository/references-repository.js @@ -0,0 +1,110 @@ +'use strict'; + + const Reference = require('../models/reference-model'); + const { BadlyFormattedParameterError, DuplicateIdError, DatabaseError } = require('../exceptions'); + +class ReferencesRepository { + constructor(model) { + this.model = model; + } + + async retrieveAll(options) { + // Build the text search + let textSearch; + if (typeof options.search !== 'undefined') { + textSearch = { $text: { $search: options.search }}; + } + + // Build the query + const query = {}; + if (typeof options.sourceName !== 'undefined') { + query['source_name'] = options.sourceName; + } + + // Build the aggregation + const aggregation = []; + if (textSearch) { + aggregation.push({ $match: textSearch }); + } + + aggregation.push({ $sort: { 'source_name': 1 }}); + aggregation.push({ $match: query }); + + const facet = { + $facet: { + totalCount: [ { $count: 'totalCount' }], + documents: [ ] + } + }; + if (options.offset) { + facet.$facet.documents.push({ $skip: options.offset }); + } + else { + facet.$facet.documents.push({ $skip: 0 }); + } + if (options.limit) { + facet.$facet.documents.push({ $limit: options.limit }); + } + aggregation.push(facet); + + // Retrieve the documents + return await this.model.aggregate(aggregation); + } + + async save(data) { + // Create the document + const reference = new this.model(data); + + // Save the document in the database + try { + const savedReference = await reference.save(); + return savedReference; + } + catch(err) { + if (err.name === 'MongoServerError' && err.code === 11000) { + // 11000 = Duplicate index + throw new DuplicateIdError({ + details: `Reference with source_name '${ data.source_name }' already exists.` + }); + } + else { + throw new DatabaseError(err); + } + } + } + + async updateAndSave(data) { + try { + const document = await this.model.findOne({ 'source_name': data.source_name }); + if (!document) { + // document not found + return null; + } + else { + // Copy data to found document and save + Object.assign(document, data); + const savedDocument = await document.save(); + return savedDocument; + } + } + catch(err) { + if (err.name === 'CastError') { + throw new BadlyFormattedParameterError({ parameterName: 'source_name' }); + } + else { + throw new DatabaseError(err); + } + } + } + + async findOneAndRemove(sourceName) { + try { + return await this.model.findOneAndRemove({ 'source_name': sourceName }); + } + catch(err) { + throw new DatabaseError(err); + } + } +} + +module.exports = new ReferencesRepository(Reference); \ No newline at end of file diff --git a/app/services/_base.service.js b/app/services/_base.service.js index 41a907fb..dde05567 100644 --- a/app/services/_base.service.js +++ b/app/services/_base.service.js @@ -53,7 +53,7 @@ class BaseService extends AbstractService { try { results = await this.repository.retrieveAll(options); } catch (err) { - const databaseError = new DatabaseError(err); // Let the DatabaseError buddle up + const databaseError = new DatabaseError(err); // Let the DatabaseError bubble up if (callback) { return callback(databaseError); } diff --git a/app/services/references-service.js b/app/services/references-service.js index 09ab3b2f..5fb41603 100644 --- a/app/services/references-service.js +++ b/app/services/references-service.js @@ -1,142 +1,41 @@ 'use strict'; -const Reference = require('../models/reference-model'); +const baseService = require('./_base.service'); +const ReferencesRepository = require('../repository/references-repository'); +const { MissingParameterError } = require('../exceptions'); -const errors = { - missingParameter: 'Missing required parameter', - badlyFormattedParameter: 'Badly formatted parameter', - duplicateId: 'Duplicate id', - notFound: 'Document not found', - invalidQueryStringParameter: 'Invalid query string parameter' -}; -exports.errors = errors; - -exports.retrieveAll = async function(options) { - // Build the text search - let textSearch; - if (typeof options.search !== 'undefined') { - textSearch = { $text: { $search: options.search }}; - } - - // Build the query - const query = {}; - if (typeof options.sourceName !== 'undefined') { - query['source_name'] = options.sourceName; - } - - // Build the aggregation - const aggregation = []; - if (textSearch) { - aggregation.push({ $match: textSearch }); - } - - aggregation.push({ $sort: { 'source_name': 1 }}); - aggregation.push({ $match: query }); - - const facet = { - $facet: { - totalCount: [ { $count: 'totalCount' }], - documents: [ ] - } - }; - if (options.offset) { - facet.$facet.documents.push({ $skip: options.offset }); - } - else { - facet.$facet.documents.push({ $skip: 0 }); - } - if (options.limit) { - facet.$facet.documents.push({ $limit: options.limit }); +class ReferencesService { + constructor() { + this.repository = ReferencesRepository; } - aggregation.push(facet); - // Retrieve the documents - const results = await Reference.aggregate(aggregation); + async retrieveAll(options) { + const results = await this.repository.retrieveAll(options); + const paginatedResults = baseService.paginate(options, results); - if (options.includePagination) { - let derivedTotalCount = 0; - if (results[0].totalCount.length > 0) { - derivedTotalCount = results[0].totalCount[0].totalCount; - } - const returnValue = { - pagination: { - total: derivedTotalCount, - offset: options.offset, - limit: options.limit - }, - data: results[0].documents - }; - return returnValue; - } - else { - return results[0].documents; + return paginatedResults; } -}; - -exports.create = async function(data) { - // Create the document - const reference = new Reference(data); - - // Save the document in the database - try { - const savedReference = await reference.save(); - return savedReference; + + async create(data) { + return await this.repository.save(data); } - catch(err) { - if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - throw new Error(errors.duplicateId); - } else { - console.log(`name: ${err.name} code: ${err.code}`); - throw err; + + async update(data) { + // Note: source_name is used as the key and cannot be updated + if (!data.source_name) { + throw new MissingParameterError({ parameterName: 'source_name' }); } - } -}; -exports.update = async function(data, callback) { - // Note: source_name is used as the key and cannot be updated - if (!data.source_name) { - const error = new Error(errors.missingParameter); - error.parameterName = 'source_name'; - throw error; + return await this.repository.updateAndSave(data); } - try { - const document = await Reference.findOne({ 'source_name': data.source_name }); - if (!document) { - // document not found - return null; - } - else { - // Copy data to found document and save - Object.assign(document, data); - const savedDocument = await document.save(); - return savedDocument; + async deleteBySourceName(sourceName) { + if (!sourceName) { + throw new MissingParameterError({ parameterName: 'source_name' }); } - } - catch(err) { - if (err.name === 'CastError') { - const error = new Error(errors.badlyFormattedParameter); - error.parameterName = 'source_name'; - return callback(error); - } - else if (err.name === 'MongoServerError' && err.code === 11000) { - // 11000 = Duplicate index - throw new Error(errors.duplicateId); - } else { - throw err; - } - } -}; -exports.deleteBySourceName = async function (sourceName) { - if (!sourceName) { - const error = new Error(errors.missingParameter); - error.parameterName = 'sourceName'; - throw error; + return await this.repository.findOneAndRemove(sourceName); } +} - const deletedReference = await Reference.findOneAndRemove({ 'source_name': sourceName }); - return deletedReference; -}; - +module.exports = new ReferencesService(ReferencesRepository); \ No newline at end of file diff --git a/app/tests/api/references/references.spec.js b/app/tests/api/references/references.spec.js index 545cc03e..5ed8bcfa 100644 --- a/app/tests/api/references/references.spec.js +++ b/app/tests/api/references/references.spec.js @@ -52,369 +52,245 @@ describe('References API', function () { passportCookie = await login.loginAnonymous(app); }); - it('GET /api/references returns an empty array of references', function (done) { - request(app) - .get('/api/references') - .set('Accept', 'application/json') - .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .expect('Content-Type', /json/) - .end(function (err, res) { - if (err) { - done(err); - } else { - // We expect to get an empty array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(0); - done(); - } - }) + it('GET /api/references returns an empty array of references', async function () { + const res = await request(app) + .get('/api/references') + .set('Accept', 'application/json') + .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) + .expect(200) + .expect('Content-Type', /json/); + + // We expect to get an empty array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(0); }); - it('POST /api/references does not create an empty reference', function (done) { + it('POST /api/references does not create an empty reference', async function () { const body = { }; - request(app) + await request(app) .post('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(400); }); let reference1; - it('POST /api/references creates a reference', function (done) { + it('POST /api/references creates a reference', async function () { const body = initialObjectData1; - request(app) + const res = await request(app) .post('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created reference - reference1 = res.body; - expect(reference1).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created reference + reference1 = res.body; + expect(reference1).toBeDefined(); }); let reference2; - it('POST /api/references creates a second reference', function (done) { + it('POST /api/references creates a second reference', async function () { const body = initialObjectData2; - request(app) + const res = await request(app) .post('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created reference - reference2 = res.body; - expect(reference2).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created reference + reference2 = res.body; + expect(reference2).toBeDefined(); + }); let reference3; - it('POST /api/references creates a third reference', function (done) { + it('POST /api/references creates a third reference', async function () { const body = initialObjectData3; - request(app) + const res = await request(app) .post('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(201) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the created reference - reference3 = res.body; - expect(reference3).toBeDefined(); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the created reference + reference3 = res.body; + expect(reference3).toBeDefined(); }); - it('GET /api/references returns the added references', function (done) { - request(app) + it('GET /api/references returns the added references', async function () { + const res = await request(app) .get('/api/references') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(3); - done(); - } - }); + .expect('Content-Type', /json/); + + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(3); }); - it('GET /api/references should return an empty array of references when the source_name cannot be found', function (done) { - request(app) + it('GET /api/references should return an empty array of references when the source_name cannot be found', async function () { + const res = await request(app) .get('/api/references?sourceName=notasourcename') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(200) - .end(function (err, res) { - if (err) { - done(err); - } - else { - // We expect to get an empty array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(0); - done(); - } - }); + .expect(200); + + // We expect to get an empty array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(0); + }); - it('GET /api/references returns the first added reference', function (done) { - request(app) + it('GET /api/references returns the first added reference', async function () { + const res = await request(app) .get('/api/references?sourceName=' + encodeURIComponent(reference1.source_name)) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - const reference = references[0]; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference1.source_name); - expect(reference.description).toBe(reference1.description); - expect(reference.url).toBe(reference1.url); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + const reference = references[0]; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference1.source_name); + expect(reference.description).toBe(reference1.description); + expect(reference.url).toBe(reference1.url); }); - it('GET /api/references uses the search parameter to return the third added reference', function (done) { - request(app) + it('GET /api/references uses the search parameter to return the third added reference', async function () { + const res = await request(app) .get('/api/references?search=' + encodeURIComponent('#3')) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - const reference = references[0]; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference3.source_name); - expect(reference.description).toBe(reference3.description); - expect(reference.url).toBe(reference3.url); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + const reference = references[0]; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference3.source_name); + expect(reference.description).toBe(reference3.description); + expect(reference.url).toBe(reference3.url); }); - it('GET /api/references uses the search parameter to return the third added reference searching fields in the source_name', function (done) { - request(app) + it('GET /api/references uses the search parameter to return the third added reference searching fields in the source_name', async function () { + const res = await request(app) .get('/api/references?search=' + encodeURIComponent('unique')) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get one reference in an array - const references = res.body; - expect(references).toBeDefined(); - expect(Array.isArray(references)).toBe(true); - expect(references.length).toBe(1); - - const reference = references[0]; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference3.source_name); - expect(reference.description).toBe(reference3.description); - expect(reference.url).toBe(reference3.url); - - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get one reference in an array + const references = res.body; + expect(references).toBeDefined(); + expect(Array.isArray(references)).toBe(true); + expect(references.length).toBe(1); + + const reference = references[0]; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference3.source_name); + expect(reference.description).toBe(reference3.description); + expect(reference.url).toBe(reference3.url); + }); - it('PUT /api/references does not update a reference when the source_name is missing', function (done) { + it('PUT /api/references does not update a reference when the source_name is missing', async function () { const body = { description: 'This reference does not have a source_name', url: '' }; - request(app) + await request(app) .put('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function(err) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(400); }); - it('PUT /api/references does not update a reference when the source_name is not in the database', function (done) { + it('PUT /api/references does not update a reference when the source_name is not in the database', async function () { const body = { source_name: 'not-a-reference', description: 'This reference is not in the database', url: '' }; - request(app) + await request(app) .put('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function(err) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(404); }); - it('PUT /api/references updates a reference', function (done) { + it('PUT /api/references updates a reference', async function () { reference1.description = 'This is a new description'; const body = reference1; - request(app) + const res = await request(app) .put('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) .expect(200) - .expect('Content-Type', /json/) - .end(function(err, res) { - if (err) { - done(err); - } - else { - // We expect to get the updated reference - const reference = res.body; - expect(reference).toBeDefined(); - expect(reference.source_name).toBe(reference1.source_name); - expect(reference.description).toBe(reference1.description); - done(); - } - }); + .expect('Content-Type', /json/); + + // We expect to get the updated reference + const reference = res.body; + expect(reference).toBeDefined(); + expect(reference.source_name).toBe(reference1.source_name); + expect(reference.description).toBe(reference1.description); }); - it('POST /api/references does not create a reference with a duplicate source_name', function (done) { + it('POST /api/references does not create a reference with a duplicate source_name', async function () { const body = reference1; - request(app) + await request(app) .post('/api/references') .send(body) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(409) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(409); }); - it('DELETE /api/references does not delete a reference when the source_name is omitted', function (done) { - request(app) + it('DELETE /api/references does not delete a reference when the source_name is omitted', async function () { + await request(app) .delete('/api/references') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(400) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(400); }); - it('DELETE /api/references does not delete a reference with a non-existent source_name', function (done) { - request(app) + it('DELETE /api/references does not delete a reference with a non-existent source_name', async function () { + await request(app) .delete('/api/references?sourceName=not-a-reference') .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(404) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(404); }); - it('DELETE /api/references deletes a reference', function (done) { - request(app) + it('DELETE /api/references deletes a reference', async function () { + await request(app) .delete(`/api/references?sourceName=${ reference1.source_name }`) .set('Accept', 'application/json') .set('Cookie', `${ login.passportCookieName }=${ passportCookie.value }`) - .expect(204) - .end(function(err, res) { - if (err) { - done(err); - } - else { - done(); - } - }); + .expect(204); }); after(async function() {