diff --git a/app-routes.js b/app-routes.js index 2e869e6..15d5a9e 100644 --- a/app-routes.js +++ b/app-routes.js @@ -66,7 +66,6 @@ module.exports = (app) => { actions.push((req, res, next) => { authenticator(_.pick(config, ['AUTH_SECRET', 'VALID_ISSUERS']))(req, res, next) }) - actions.push((req, res, next) => { if (!req.authUser) { return next(new errors.UnauthorizedError('Action is not allowed for invalid token')) @@ -95,6 +94,16 @@ module.exports = (app) => { next() } }) + } else { + // Allow public access, but process the jwt token, if one is passed + // This is for GET requests where admin users can access soft deleted records + actions.push((req, res, next) => { + if (req.headers.authorization) { + authenticator(_.pick(config, ['AUTH_SECRET', 'VALID_ISSUERS']))(req, res, next) + } else { + next() + } + }) } actions.push(method) diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1a51c77..cdd49c6 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -50,6 +50,7 @@ paths: parameters: - $ref: '#/parameters/page' - $ref: '#/parameters/perPage' + - $ref: '#/parameters/includeSoftDeleted' - name: type in: query description: Filter by type, case insensitive, partial match is used @@ -121,6 +122,7 @@ paths: parameters: - $ref: '#/parameters/page' - $ref: '#/parameters/perPage' + - $ref: '#/parameters/includeSoftDeleted' - name: type in: query description: Filter by type, case insensitive, partial match is used @@ -225,6 +227,7 @@ paths: - Device description: Retrieve the device by id parameters: + - $ref: '#/parameters/includeSoftDeleted' - name: id in: path required: true @@ -253,6 +256,7 @@ paths: - Device description: Retrieve the device head by id parameters: + - $ref: '#/parameters/includeSoftDeleted' - name: id in: path required: true @@ -380,6 +384,7 @@ paths: security: - bearer: [] parameters: + - $ref: '#/parameters/destroy' - name: id in: path required: true @@ -465,6 +470,7 @@ paths: parameters: - $ref: '#/parameters/page' - $ref: '#/parameters/perPage' + - $ref: '#/parameters/includeSoftDeleted' - name: name in: query description: Filter by name, case insensitive, partial match is used @@ -520,6 +526,7 @@ paths: parameters: - $ref: '#/parameters/page' - $ref: '#/parameters/perPage' + - $ref: '#/parameters/includeSoftDeleted' - name: name in: query description: Filter by name, case insensitive, partial match is used @@ -609,6 +616,7 @@ paths: - Country description: Retrieve the country by id parameters: + - $ref: '#/parameters/includeSoftDeleted' - name: id in: path required: true @@ -637,6 +645,7 @@ paths: - Country description: Retrieve the country head by id parameters: + - $ref: '#/parameters/includeSoftDeleted' - name: id in: path required: true @@ -764,6 +773,7 @@ paths: security: - bearer: [] parameters: + - $ref: '#/parameters/destroy' - name: id in: path required: true @@ -803,6 +813,7 @@ paths: parameters: - $ref: '#/parameters/page' - $ref: '#/parameters/perPage' + - $ref: '#/parameters/includeSoftDeleted' - name: name in: query description: Filter by name, case insensitive, partial match is used @@ -853,6 +864,7 @@ paths: parameters: - $ref: '#/parameters/page' - $ref: '#/parameters/perPage' + - $ref: '#/parameters/includeSoftDeleted' - name: name in: query description: Filter by name, case insensitive, partial match is used @@ -937,6 +949,7 @@ paths: - Educational Institution description: Retrieve the educational institution by id parameters: + - $ref: '#/parameters/includeSoftDeleted' - name: id in: path required: true @@ -965,6 +978,7 @@ paths: - Educational Institution description: Retrieve the educational institution head by id parameters: + - $ref: '#/parameters/includeSoftDeleted' - name: id in: path required: true @@ -1092,6 +1106,7 @@ paths: security: - bearer: [] parameters: + - $ref: '#/parameters/destroy' - name: id in: path required: true @@ -1158,6 +1173,18 @@ parameters: type: integer default: 20 maximum: 100 + includeSoftDeleted: + name: includeSoftDeleted + in: query + description: Include items that have been soft deleted (Only allowed for admin) + type: boolean + default: false + destroy: + name: destroy + in: query + description: Hard delete the item (Only allowed for admin) + type: boolean + default: false definitions: Country: type: object @@ -1241,4 +1268,4 @@ definitions: operatingSystem: type: string operatingSystemVersion: - type: string \ No newline at end of file + type: string diff --git a/docs/tc-lookup-api.postman_collection.json b/docs/tc-lookup-api.postman_collection.json index cdcc4f5..db42e6e 100644 --- a/docs/tc-lookup-api.postman_collection.json +++ b/docs/tc-lookup-api.postman_collection.json @@ -1,6 +1,6 @@ { "info": { - "_postman_id": "4802b022-4a66-43f5-b223-cbbcfd91ad9e", + "_postman_id": "95dabfdf-acad-402b-9a40-3457a4dcdcd7", "name": "tc-lookup-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, @@ -196,7 +196,7 @@ "raw": "" }, "url": { - "raw": "{{URL}}/lookups/countries?name=Testing", + "raw": "{{URL}}/lookups/countries", "host": [ "{{URL}}" ], @@ -207,7 +207,71 @@ "query": [ { "key": "name", - "value": "Testing" + "value": "Testing", + "disabled": true + } + ] + } + }, + "response": [] + }, + { + "name": "list countries (include soft deleted)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/countries?includeSoftDeleted=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "countries" + ], + "query": [ + { + "key": "name", + "value": "Testing", + "disabled": true + }, + { + "key": "includeSoftDeleted", + "value": "true" } ] } @@ -325,6 +389,65 @@ }, "response": [] }, + { + "name": "get country (include soft deleted)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/countries/{{COUNTRY_ID}}?includeSoftDeleted=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "countries", + "{{COUNTRY_ID}}" + ], + "query": [ + { + "key": "includeSoftDeleted", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "get country head", "event": [ @@ -877,6 +1000,62 @@ } }, "response": [] + }, + { + "name": "delete country (destroy)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 204\", function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/countries/{{COUNTRY_ID}}?destroy=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "countries", + "{{COUNTRY_ID}}" + ], + "query": [ + { + "key": "destroy", + "value": "true" + } + ] + } + }, + "response": [] } ], "event": [ @@ -1128,6 +1307,64 @@ }, "response": [] }, + { + "name": "list devices (inlcude soft deleted)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/devices?includeSoftDeleted=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "devices" + ], + "query": [ + { + "key": "includeSoftDeleted", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "list devices head", "event": [ @@ -1447,6 +1684,65 @@ }, "response": [] }, + { + "name": "get devices (include soft deleted)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/devices/{{DEVICE_ID}}?includeSoftDeleted=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "devices", + "{{DEVICE_ID}}" + ], + "query": [ + { + "key": "includeSoftDeleted", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "get country head", "event": [ @@ -2049,6 +2345,62 @@ } }, "response": [] + }, + { + "name": "delete device (destroy)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 204\", function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/devices/{{DEVICE_ID}}?destroy=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "devices", + "{{DEVICE_ID}}" + ], + "query": [ + { + "key": "destroy", + "value": "true" + } + ] + } + }, + "response": [] } ], "event": [ @@ -2284,6 +2636,64 @@ }, "response": [] }, + { + "name": "list educational institutions (include soft deleted)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/educationalInstitutions?includeSoftDeleted=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "educationalInstitutions" + ], + "query": [ + { + "key": "includeSoftDeleted", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "list educational institutions head", "event": [ @@ -2395,6 +2805,65 @@ }, "response": [] }, + { + "name": "get educational institution (include soft deleted)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "method": "GET", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{ADMIN_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/educationalInstitutions/{{EDUCATIONAL_INSTITUTION_ID}}?includeSoftDeleted=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "educationalInstitutions", + "{{EDUCATIONAL_INSTITUTION_ID}}" + ], + "query": [ + { + "key": "includeSoftDeleted", + "value": "true" + } + ] + } + }, + "response": [] + }, { "name": "get educational institution head", "event": [ @@ -2597,6 +3066,62 @@ } }, "response": [] + }, + { + "name": "delete educational institution (destroy)", + "event": [ + { + "listen": "test", + "script": { + "id": "fb283d16-ca1b-4b47-8e95-ed76324d2174", + "exec": [ + "pm.test(\"Status code is 204\", function () {", + " pm.response.to.have.status(204);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "DELETE", + "header": [ + { + "key": "Accept", + "value": "application/json" + }, + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Authorization", + "value": "Bearer {{M2M_UPDATE_ACCESS_TOKEN}}" + } + ], + "body": { + "mode": "raw", + "raw": "" + }, + "url": { + "raw": "{{URL}}/lookups/educationalInstitutions/{{EDUCATIONAL_INSTITUTION_ID}}?destroy=true", + "host": [ + "{{URL}}" + ], + "path": [ + "lookups", + "educationalInstitutions", + "{{EDUCATIONAL_INSTITUTION_ID}}" + ], + "query": [ + { + "key": "destroy", + "value": "true" + } + ] + } + }, + "response": [] } ], "event": [ diff --git a/resources/countries.json b/resources/countries.json index 483a777..c1bbe78 100644 --- a/resources/countries.json +++ b/resources/countries.json @@ -4,10 +4,14 @@ "countryFlag": "Italy Flag", "countryCode": "005" }, + { + "name": "Spain", + "countryFlag": "Spain Flag", + "countryCode": "004" + }, { "name": "England", "countryFlag": "England Flag", "countryCode": "007" } ] - diff --git a/resources/countries_with_errors.json b/resources/countries_with_errors.json deleted file mode 100644 index 58a360d..0000000 --- a/resources/countries_with_errors.json +++ /dev/null @@ -1,11 +0,0 @@ -[ - { - "countryFlag": "Italy Flag", - "countryCode": "005" - }, - { - "name": "England", - "countryCode": "007" - } -] - diff --git a/resources/devices.json b/resources/devices.json index aa8c97d..8ec5485 100644 --- a/resources/devices.json +++ b/resources/devices.json @@ -6,6 +6,13 @@ "operatingSystem": "OSX", "operatingSystemVersion": "10.14" }, + { + "type": "Mobile", + "manufacturer": "Apple", + "model": "iPhone XS", + "operatingSystem": "iOS", + "operatingSystemVersion": "10.14" + }, { "type": "Mobile", "manufacturer": "Samsung", diff --git a/resources/devices_with_duplicates.json b/resources/devices_with_duplicates.json deleted file mode 100644 index a83c767..0000000 --- a/resources/devices_with_duplicates.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "type": "Desktop", - "manufacturer": "Apple", - "model": "Macbook Pro", - "operatingSystem": "OSX", - "operatingSystemVersion": "10.14" - }, - { - "type": "Desktop", - "manufacturer": "Apple", - "model": "Macbook Pro", - "operatingSystem": "OSX", - "operatingSystemVersion": "10.14" - }, - { - "type": "Mobile", - "manufacturer": "Samsung", - "model": "Note8", - "operatingSystem": "Android", - "operatingSystemVersion": "10.x" - } -] \ No newline at end of file diff --git a/resources/devices_with_errors.json b/resources/devices_with_errors.json deleted file mode 100644 index 574e1f2..0000000 --- a/resources/devices_with_errors.json +++ /dev/null @@ -1,21 +0,0 @@ -[ - { - "type": "Desktop", - "manufacturer": "Apple", - "model": "Macbook Pro", - "operatingSystem": "OSX", - "operatingSystemVersion": "10.14" - }, - { - "manufacturer": "Apple", - "model": "Macbook Pro", - "operatingSystem": "OSX", - "operatingSystemVersion": "10.14" - }, - { - "type": "Mobile", - "manufacturer": "Samsung", - "operatingSystem": "Android", - "operatingSystemVersion": "10.x" - } -] \ No newline at end of file diff --git a/resources/educational_institutions.json b/resources/educational_institutions.json index 8f0a837..845d690 100644 --- a/resources/educational_institutions.json +++ b/resources/educational_institutions.json @@ -4,6 +4,8 @@ }, { "name": "Educational Insitution 2" + }, + { + "name": "Educational Insitution 3" } ] - diff --git a/resources/educational_institutions_errors.json b/resources/educational_institutions_errors.json deleted file mode 100644 index bc95796..0000000 --- a/resources/educational_institutions_errors.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "WRONG_ENTITY": "Educational Insitution 1" - }, - { - "WRONG_ENTITY2": "Educational Insitution 2" - } -] - diff --git a/resources/wrong_json.json b/resources/wrong_json.json deleted file mode 100644 index 38244a0..0000000 --- a/resources/wrong_json.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - - "type": "Desktop", - "manufacturer": "Apple", - "model": "Macbook Pro", - "operatingSystem": "OSX", - "operatingSystemVersion": "10.14" - }, - { - "type": "Desktop", - "manufacturer": "Apple", - "model": "Macbook Pro", - "operatingSystem": "OSX", - "operatingSystemVersion": "10.14" - }, - { - "type": "Mobile", - "manufacturer": "Samsung", - "model": "Note8", - "operatingSystem": "Android", - "operatingSystemVersion": "10.x" - } -] \ No newline at end of file diff --git a/scripts/createTables.js b/scripts/createTables.js index 5d30910..086a547 100644 --- a/scripts/createTables.js +++ b/scripts/createTables.js @@ -12,11 +12,11 @@ logger.info('Create DynamoDB tables.') const createTables = async () => { const names = [ config.AMAZON.DYNAMODB_COUNTRY_TABLE, - // 'test_' + config.AMAZON.DYNAMODB_COUNTRY_TABLE, + 'test_' + config.AMAZON.DYNAMODB_COUNTRY_TABLE, config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, - // 'test_' + config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, - config.AMAZON.DYNAMODB_DEVICE_TABLE - // 'test_' + config.AMAZON.DYNAMODB_DEVICE_TABLE + 'test_' + config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, + config.AMAZON.DYNAMODB_DEVICE_TABLE, + 'test_' + config.AMAZON.DYNAMODB_DEVICE_TABLE ] for (const name of names) { logger.info(`Create table: ${name}`) diff --git a/scripts/loadData.js b/scripts/loadData.js index 7470d2e..aa7dfcd 100644 --- a/scripts/loadData.js +++ b/scripts/loadData.js @@ -40,6 +40,7 @@ const loadData = async (lookupName, lookupFilePath) => { entity.operatingSystemVersion = 'ANY' } } + entity.isDeleted = false const res = await helper.create(getTableName, entity) // create record in es diff --git a/src/common/helper.js b/src/common/helper.js index feedab7..674b6ba 100644 --- a/src/common/helper.js +++ b/src/common/helper.js @@ -25,6 +25,18 @@ AWS.config.update({ region: config.AMAZON.AWS_REGION }) +const MODEL_TO_ES_INDEX_MAP = { + [config.AMAZON.DYNAMODB_DEVICE_TABLE]: config.ES.DEVICE_INDEX, + [config.AMAZON.DYNAMODB_COUNTRY_TABLE]: config.ES.COUNTRY_INDEX, + [config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE]: config.ES.EDUCATIONAL_INSTITUTION_INDEX +} + +const MODEL_TO_ES_TYPE_MAP = { + [config.AMAZON.DYNAMODB_DEVICE_TABLE]: config.ES.DEVICE_TYPE, + [config.AMAZON.DYNAMODB_COUNTRY_TABLE]: config.ES.COUNTRY_TYPE, + [config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE]: config.ES.EDUCATIONAL_INSTITUTION_TYPE +} + /** * Wrap async function to standard express function * @param {Function} fn the async function @@ -112,6 +124,10 @@ async function deleteTable (tableName) { }) } +function getNotFoundError (modelName, id) { + return new errors.NotFoundError(`${modelName} with id: ${id} doesn't exist`) +} + /** * Get Data by model id * @param {String} modelName The dynamoose model name @@ -126,7 +142,7 @@ async function getById (modelName, id) { } else if (result.length > 0) { resolve(result[0]) } else { - reject(new errors.NotFoundError(`${modelName} with id: ${id} doesn't exist`)) + reject(getNotFoundError(modelName, id)) } }) }) @@ -172,11 +188,19 @@ async function update (dbItem, data) { }) } +async function remove (dbItem, destroy) { + if (destroy) { + return deleteItem(dbItem) + } else { + return update(dbItem, { isDeleted: true }) + } +} + /** - * Remove item in database + * Delete item in database * @param {Object} dbItem The Dynamo database item to remove */ -async function remove (dbItem) { +async function deleteItem (dbItem) { return new Promise((resolve, reject) => { dbItem.delete((err) => { if (err) { @@ -263,6 +287,68 @@ function getESClient () { return esClient } +async function getEntity (modelName, id, query, authUser) { + let recordIsSoftDeleted = false + let result + // first try to get from ES + const isAdminUser = isAdmin(authUser) + + if (!_.isNil(query.includeSoftDeleted) && query.includeSoftDeleted) { + if (!isAdminUser) { + throw new errors.ForbiddenError('You are not allowed to perform that action') + } + } + + try { + const client = await getESClient() + const sourceParams = { + index: MODEL_TO_ES_INDEX_MAP[modelName], + type: MODEL_TO_ES_TYPE_MAP[modelName], + id + } + + result = await client.getSource(sourceParams) + + if ( + !isAdminUser || + _.isNil(query.includeSoftDeleted) || + (isAdmin && !query.includeSoftDeleted)) { + // We should not return the record if the record is soft deleted + if (result.isDeleted) { + recordIsSoftDeleted = true + } + } else if (!(isAdmin && !_.isNil(query.includeSoftDeleted))) { + delete result.isDeleted + } + } catch (e) { + // log and ignore + logger.logFullError(e) + } + + if (recordIsSoftDeleted) { + throw new errors.NotFoundError(`${modelName} with id: ${id} doesn't exist`) + } else if (result) { + return result + } + + result = await getById(modelName, id) + + if ( + !isAdminUser || + _.isNil(query.includeSoftDeleted) || + (isAdmin && !query.includeSoftDeleted)) { + // We should not return the record if the record is soft deleted + if (result.isDeleted) { + throw new errors.NotFoundError(`${modelName} with id: ${id} doesn't exist`) + } + } else if (!(isAdmin && !_.isNil(query.includeSoftDeleted))) { + delete result.isDeleted + } + + // then try to get from DB + return result +} + /** * Create Elasticsearch index, it will be deleted and re-created if present. * @param {String} indexName the ES index name @@ -372,12 +458,54 @@ async function postEvent (topic, payload) { await busApiClient.postEvent(message) } +/** + * Throws error if user is not admin + * @param {Object} authUser The user making the request + */ +function isAdmin (authUser) { + if (!authUser) { + return false + } else if (!authUser.scopes) { + // Not a machine user + const admin = _.filter(authUser.roles, role => role.toLowerCase() === 'Administrator'.toLowerCase()) + + if (admin.length === 0) { + return false + } + } + + return true +} + +/** + * Removes the attribute `isDeleted` from the result + * @param {Object|Array} result The result data set + * @param {Boolean} fromDB Is the result from database + */ +function sanitizeResult (result, fromDB) { + if (fromDB) { + // Dynamoose returns the result as an array hash of the models type + result = JSON.parse(JSON.stringify(result)) + } + + if (_.isPlainObject(result)) { + delete result.isDeleted + } else if (_.isArray(result)) { + for (let i = 0; i < result.length; i++) { + delete result[i].isDeleted + } + } + + return result +} + module.exports = { wrapExpress, autoWrapExpress, createTable, deleteTable, getById, + getEntity, create, update, remove, @@ -386,5 +514,7 @@ module.exports = { getESClient, createESIndex, setResHeaders, - postEvent + postEvent, + isAdmin, + sanitizeResult } diff --git a/src/controllers/CountryController.js b/src/controllers/CountryController.js index 660a02b..638b0d8 100644 --- a/src/controllers/CountryController.js +++ b/src/controllers/CountryController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param {Object} res the response */ async function list (req, res) { - const result = await service.list(req.query) + const result = await service.list(req.query, req.authUser) helper.setResHeaders(req, res, result) res.send(result.result) } @@ -22,7 +22,7 @@ async function list (req, res) { * @param {Object} res the response */ async function listHead (req, res) { - const result = await service.list(req.query) + const result = await service.list(req.query, req.authUser) helper.setResHeaders(req, res, result) res.end() } @@ -43,7 +43,7 @@ async function create (req, res) { * @param {Object} res the response */ async function getEntity (req, res) { - const result = await service.getEntity(req.params.id) + const result = await service.getEntity(req.params.id, req.query, req.authUser) res.send(result) } @@ -53,7 +53,7 @@ async function getEntity (req, res) { * @param {Object} res the response */ async function getEntityHead (req, res) { - await service.getEntity(req.params.id) + await service.getEntity(req.params.id, req.query, req.authUser) res.end() } @@ -83,7 +83,7 @@ async function partiallyUpdate (req, res) { * @param {Object} res the response */ async function remove (req, res) { - await service.remove(req.params.id) + await service.remove(req.params.id, req.query) res.status(HttpStatus.NO_CONTENT).end() } diff --git a/src/controllers/DeviceController.js b/src/controllers/DeviceController.js index d137114..b234143 100644 --- a/src/controllers/DeviceController.js +++ b/src/controllers/DeviceController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param {Object} res the response */ async function list (req, res) { - const result = await service.list(req.query) + const result = await service.list(req.query, req.authUser) helper.setResHeaders(req, res, result) res.send(result.result) } @@ -22,7 +22,7 @@ async function list (req, res) { * @param {Object} res the response */ async function listHead (req, res) { - const result = await service.list(req.query) + const result = await service.list(req.query, req.authUser) helper.setResHeaders(req, res, result) res.end() } @@ -43,7 +43,7 @@ async function create (req, res) { * @param {Object} res the response */ async function getEntity (req, res) { - const result = await service.getEntity(req.params.id) + const result = await service.getEntity(req.params.id, req.query, req.authUser) res.send(result) } @@ -53,7 +53,7 @@ async function getEntity (req, res) { * @param {Object} res the response */ async function getEntityHead (req, res) { - await service.getEntity(req.params.id) + await service.getEntity(req.params.id, req.query, req.authUser) res.end() } @@ -83,7 +83,7 @@ async function partiallyUpdate (req, res) { * @param {Object} res the response */ async function remove (req, res) { - await service.remove(req.params.id) + await service.remove(req.params.id, req.query) res.status(HttpStatus.NO_CONTENT).end() } diff --git a/src/controllers/EducationalInstitutionController.js b/src/controllers/EducationalInstitutionController.js index 7841abb..55b076d 100644 --- a/src/controllers/EducationalInstitutionController.js +++ b/src/controllers/EducationalInstitutionController.js @@ -11,7 +11,7 @@ const helper = require('../common/helper') * @param {Object} res the response */ async function list (req, res) { - const result = await service.list(req.query) + const result = await service.list(req.query, req.authUser) helper.setResHeaders(req, res, result) res.send(result.result) } @@ -22,7 +22,7 @@ async function list (req, res) { * @param {Object} res the response */ async function listHead (req, res) { - const result = await service.list(req.query) + const result = await service.list(req.query, req.authUser) helper.setResHeaders(req, res, result) res.end() } @@ -43,7 +43,7 @@ async function create (req, res) { * @param {Object} res the response */ async function getEntity (req, res) { - const result = await service.getEntity(req.params.id) + const result = await service.getEntity(req.params.id, req.query, req.authUser) res.send(result) } @@ -53,7 +53,7 @@ async function getEntity (req, res) { * @param {Object} res the response */ async function getEntityHead (req, res) { - await service.getEntity(req.params.id) + await service.getEntity(req.params.id, req.query, req.authUser) res.end() } @@ -83,7 +83,7 @@ async function partiallyUpdate (req, res) { * @param {Object} res the response */ async function remove (req, res) { - await service.remove(req.params.id) + await service.remove(req.params.id, req.query) res.status(HttpStatus.NO_CONTENT).end() } diff --git a/src/models/Country.js b/src/models/Country.js index 373ec75..caf833b 100644 --- a/src/models/Country.js +++ b/src/models/Country.js @@ -23,6 +23,10 @@ const schema = new Schema({ countryCode: { type: String, required: true + }, + isDeleted: { + type: Boolean, + default: false } }, { diff --git a/src/models/Device.js b/src/models/Device.js index dd6e5bb..7202d6a 100644 --- a/src/models/Device.js +++ b/src/models/Device.js @@ -31,6 +31,10 @@ const schema = new Schema({ operatingSystemVersion: { type: String, required: false + }, + isDeleted: { + type: Boolean, + default: false } }, { diff --git a/src/models/EducationalInstitution.js b/src/models/EducationalInstitution.js index 89215a6..84dafee 100644 --- a/src/models/EducationalInstitution.js +++ b/src/models/EducationalInstitution.js @@ -15,6 +15,10 @@ const schema = new Schema({ name: { type: String, required: true + }, + isDeleted: { + type: Boolean, + default: false } }, { diff --git a/src/services/CountryService.js b/src/services/CountryService.js index d1904d3..0d23b28 100644 --- a/src/services/CountryService.js +++ b/src/services/CountryService.js @@ -8,8 +8,9 @@ const uuid = require('uuid/v4') const helper = require('../common/helper') const logger = require('../common/logger') const { Resources } = require('../../app-constants') +const error = require('../common/errors') -var esClient +let esClient (async function () { esClient = await helper.getESClient() })() @@ -17,9 +18,10 @@ var esClient /** * List countries in Elasticsearch. * @param {Object} criteria the search criteria + * @param {Boolean} isAdmin Is the user an admin * @returns {Object} the search result */ -async function listES (criteria) { +async function listES (criteria, isAdmin) { // construct ES query const esQuery = { index: config.ES.COUNTRY_INDEX, @@ -33,7 +35,8 @@ async function listES (criteria) { must: [] } } - } + }, + _source_excludes: (isAdmin && !_.isNil(criteria.includeSoftDeleted)) ? [] : ['isDeleted'] } // filtering for name if (criteria.name) { @@ -52,6 +55,24 @@ async function listES (criteria) { }) } + // If user is not an admin or user has not specified + // whether they need soft deleted records, do not return + // soft deleted records + if ( + !isAdmin || + _.isNil(criteria.includeSoftDeleted) || + (isAdmin && !criteria.includeSoftDeleted)) { + esQuery.body.query.bool.must.push({ + bool: { + must_not: [{ + term: { + isDeleted: true + } + }] + } + }) + } + // Search with constructed query const docs = await esClient.search(esQuery) // Extract data from hits @@ -66,13 +87,24 @@ async function listES (criteria) { /** * List countries. * @param {Object} criteria the search criteria + * @param {Object} authUser the user making the request * @returns {Object} the search result */ -async function list (criteria) { +async function list (criteria, authUser) { // first try to get from ES let result + + const isAdmin = helper.isAdmin(authUser) + + if (!_.isNil(criteria.includeSoftDeleted) && criteria.includeSoftDeleted) { + // Only admin can request for deleted records + if (!isAdmin) { + throw new error.ForbiddenError('You are not allowed to perform that action') + } + } + try { - result = await listES(criteria) + result = await listES(criteria, isAdmin) } catch (e) { // log and ignore logger.logFullError(e) @@ -89,8 +121,15 @@ async function list (criteria) { if (criteria.countryCode) { options.countryCode = { eq: criteria.countryCode } } + if (!criteria.includeSoftDeleted) { + options.isDeleted = { ne: true } + } // ignore pagination, scan all matched records result = await helper.scan(config.AMAZON.DYNAMODB_COUNTRY_TABLE, options) + + if (!criteria.includeSoftDeleted) { + result = helper.sanitizeResult(result, true) + } // return fromDB:true to indicate it is got from db, // and response headers ('X-Total', 'X-Page', etc.) are not set in this case return { fromDB: true, result } @@ -102,34 +141,29 @@ list.schema = { perPage: Joi.perPage(), name: Joi.string(), countryFlag: Joi.string(), - countryCode: Joi.string() - }) + countryCode: Joi.string(), + includeSoftDeleted: Joi.boolean() + }), + authUser: Joi.object() } /** * Get country entity by id. * @param {String} id the country id + * @param {Object} query The query params + * @param {Object} authUser The user making the request * @returns {Object} the country of given id */ -async function getEntity (id) { - // first try to get from ES - try { - return await esClient.getSource({ - index: config.ES.COUNTRY_INDEX, - type: config.ES.COUNTRY_TYPE, - id - }) - } catch (e) { - // log and ignore - logger.logFullError(e) - } - - // then try to get from DB - return helper.getById(config.AMAZON.DYNAMODB_COUNTRY_TABLE, id) +async function getEntity (id, query, authUser) { + return helper.getEntity(config.AMAZON.DYNAMODB_COUNTRY_TABLE, id, query, authUser) } getEntity.schema = { - id: Joi.id() + id: Joi.id(), + query: Joi.object().keys({ + includeSoftDeleted: Joi.boolean() + }), + authUser: Joi.object() } /** @@ -140,13 +174,14 @@ getEntity.schema = { async function create (data) { await helper.validateDuplicate(config.AMAZON.DYNAMODB_COUNTRY_TABLE, 'name', data.name) data.id = uuid() + data.isDeleted = false // create record in db const res = await helper.create(config.AMAZON.DYNAMODB_COUNTRY_TABLE, data) // Send Kafka message using bus api await helper.postEvent(config.LOOKUP_CREATE_TOPIC, _.assign({ resource: Resources.Country }, res)) - return res + return helper.sanitizeResult(res) } create.schema = { @@ -179,10 +214,10 @@ async function partiallyUpdate (id, data) { // Send Kafka message using bus api await helper.postEvent(config.LOOKUP_UPDATE_TOPIC, _.assign({ resource: Resources.Country, id }, data)) - return res + return helper.sanitizeResult(res) } else { // data are not changed - return country + return helper.sanitizeResult(country) } } @@ -217,18 +252,22 @@ update.schema = { /** * Remove country. * @param {String} id the country id to remove + * @param {Object} query the query param */ -async function remove (id) { +async function remove (id, query) { // remove data in DB const country = await helper.getById(config.AMAZON.DYNAMODB_COUNTRY_TABLE, id) - await helper.remove(country) + await helper.remove(country, query.destroy) // Send Kafka message using bus api - await helper.postEvent(config.LOOKUP_DELETE_TOPIC, { resource: Resources.Country, id }) + await helper.postEvent(config.LOOKUP_DELETE_TOPIC, { resource: Resources.Country, id, isSoftDelete: !query.destroy }) } remove.schema = { - id: Joi.id() + id: Joi.id(), + query: Joi.object().keys({ + destroy: Joi.boolean() + }) } module.exports = { diff --git a/src/services/DeviceService.js b/src/services/DeviceService.js index dc8df86..2d496c2 100644 --- a/src/services/DeviceService.js +++ b/src/services/DeviceService.js @@ -8,8 +8,9 @@ const uuid = require('uuid/v4') const helper = require('../common/helper') const logger = require('../common/logger') const { Resources } = require('../../app-constants') +const error = require('../common/errors') -var esClient +let esClient (async function () { esClient = await helper.getESClient() })() @@ -17,9 +18,10 @@ var esClient /** * List devices in Elasticsearch. * @param {Object} criteria the search criteria + * @param {Boolean} isAdmin Is the user an admin * @returns {Object} the search result */ -async function listES (criteria) { +async function listES (criteria, isAdmin) { // construct ES query const esQuery = { index: config.ES.DEVICE_INDEX, @@ -33,7 +35,8 @@ async function listES (criteria) { must: [] } } - } + }, + _source_excludes: (isAdmin && !_.isNil(criteria.includeSoftDeleted)) ? [] : ['isDeleted'] } // filtering for type @@ -81,6 +84,23 @@ async function listES (criteria) { }) } + // If user is not an admin or user has not specified + // whether they need soft deleted records, do not return + // soft deleted records + if ( + !isAdmin || + _.isNil(criteria.includeSoftDeleted) || + (isAdmin && !criteria.includeSoftDeleted)) { + esQuery.body.query.bool.must.push({ + bool: { + must_not: [{ + term: { + isDeleted: true + } + }] + } + }) + } // Search with constructed query const docs = await esClient.search(esQuery) // Extract data from hits @@ -95,13 +115,24 @@ async function listES (criteria) { /** * List devices. * @param {Object} criteria the search criteria + * @param {Object} authUser the user making the request * @returns {Object} the search result */ -async function list (criteria) { +async function list (criteria, authUser) { // first try to get from ES let result + + const isAdmin = helper.isAdmin(authUser) + + if (!_.isNil(criteria.includeSoftDeleted) && criteria.includeSoftDeleted) { + // Only admin can request for deleted records + if (!isAdmin) { + throw new error.ForbiddenError('You are not allowed to perform that action') + } + } + try { - result = await listES(criteria) + result = await listES(criteria, isAdmin) } catch (e) { // log and ignore logger.logFullError(e) @@ -127,8 +158,15 @@ async function list (criteria) { if (criteria.operatingSystemVersion) { options.operatingSystemVersion = { eq: criteria.operatingSystemVersion } } + if (!criteria.includeSoftDeleted) { + options.isDeleted = { ne: true } + } // ignore pagination, scan all matched records result = await helper.scan(config.AMAZON.DYNAMODB_DEVICE_TABLE, options) + + if (!criteria.includeSoftDeleted) { + result = helper.sanitizeResult(result, true) + } // return fromDB:true to indicate it is got from db, // and response headers ('X-Total', 'X-Page', etc.) are not set in this case return { fromDB: true, result } @@ -142,34 +180,29 @@ list.schema = { manufacturer: Joi.string(), model: Joi.string(), operatingSystem: Joi.string(), - operatingSystemVersion: Joi.string() - }) + operatingSystemVersion: Joi.string(), + includeSoftDeleted: Joi.boolean() + }), + authUser: Joi.object() } /** * Get device entity by id. * @param {String} id the device id + * @param {Object} query The query params + * @param {Object} authUser The user making the request * @returns {Object} the device of given id */ -async function getEntity (id) { - // first try to get from ES - try { - return await esClient.getSource({ - index: config.ES.DEVICE_INDEX, - type: config.ES.DEVICE_TYPE, - id - }) - } catch (e) { - // log and ignore - logger.logFullError(e) - } - - // then try to get from DB - return helper.getById(config.AMAZON.DYNAMODB_DEVICE_TABLE, id) +async function getEntity (id, query, authUser) { + return helper.getEntity(config.AMAZON.DYNAMODB_DEVICE_TABLE, id, query, authUser) } getEntity.schema = { - id: Joi.id() + id: Joi.id(), + query: Joi.object().keys({ + includeSoftDeleted: Joi.boolean() + }), + authUser: Joi.object() } /** @@ -183,13 +216,14 @@ async function create (data) { [data.type, data.manufacturer, data.model, data.operatingSystem, data.operatingSystemVersion]) data.id = uuid() + data.isDeleted = false // create record in db const res = await helper.create(config.AMAZON.DYNAMODB_DEVICE_TABLE, data) // Send Kafka message using bus api await helper.postEvent(config.LOOKUP_CREATE_TOPIC, _.assign({ resource: Resources.Device }, res)) - return res + return helper.sanitizeResult(res) } create.schema = { @@ -231,10 +265,10 @@ async function partiallyUpdate (id, data) { // Send Kafka message using bus api await helper.postEvent(config.LOOKUP_UPDATE_TOPIC, _.assign({ resource: Resources.Device, id }, data)) - return res + return helper.sanitizeResult(res) } else { // data are not changed - return device + return helper.sanitizeResult(device) } } @@ -273,18 +307,22 @@ update.schema = { /** * Remove device. * @param {String} id the device id to remove + * @param {Object} query the query param */ -async function remove (id) { +async function remove (id, query) { // remove data in DB const device = await helper.getById(config.AMAZON.DYNAMODB_DEVICE_TABLE, id) - await helper.remove(device) + await helper.remove(device, query.destroy) // Send Kafka message using bus api - await helper.postEvent(config.LOOKUP_DELETE_TOPIC, { resource: Resources.Device, id }) + await helper.postEvent(config.LOOKUP_DELETE_TOPIC, { resource: Resources.Device, id, isSoftDelete: !query.destroy }) } remove.schema = { - id: Joi.id() + id: Joi.id(), + query: Joi.object().keys({ + destroy: Joi.boolean() + }) } /** diff --git a/src/services/EducationalInstitutionService.js b/src/services/EducationalInstitutionService.js index 7abe8fb..dec5d37 100644 --- a/src/services/EducationalInstitutionService.js +++ b/src/services/EducationalInstitutionService.js @@ -8,8 +8,9 @@ const uuid = require('uuid/v4') const helper = require('../common/helper') const logger = require('../common/logger') const { Resources } = require('../../app-constants') +const error = require('../common/errors') -var esClient +let esClient (async function () { esClient = await helper.getESClient() })() @@ -17,9 +18,10 @@ var esClient /** * List educational institutions in Elasticsearch. * @param {Object} criteria the search criteria + * @param {Boolean} isAdmin Is the user an admin * @returns {Object} the search result */ -async function listES (criteria) { +async function listES (criteria, isAdmin) { // construct ES query const esQuery = { index: config.ES.EDUCATIONAL_INSTITUTION_INDEX, @@ -33,7 +35,8 @@ async function listES (criteria) { must: [] } } - } + }, + _source_excludes: (isAdmin && !_.isNil(criteria.includeSoftDeleted)) ? [] : ['isDeleted'] } // filtering for name if (criteria.name) { @@ -44,6 +47,24 @@ async function listES (criteria) { }) } + // If user is not an admin or user has not specified + // whether they need soft deleted records, do not return + // soft deleted records + if ( + !isAdmin || + _.isNil(criteria.includeSoftDeleted) || + (isAdmin && !criteria.includeSoftDeleted)) { + esQuery.body.query.bool.must.push({ + bool: { + must_not: [{ + term: { + isDeleted: true + } + }] + } + }) + } + // Search with constructed query const docs = await esClient.search(esQuery) // Extract data from hits @@ -58,13 +79,24 @@ async function listES (criteria) { /** * List educational institutions. * @param {Object} criteria the search criteria + * @param {Object} authUser the user making the request * @returns {Object} the search result */ -async function list (criteria) { +async function list (criteria, authUser) { // first try to get from ES let result + + const isAdmin = helper.isAdmin(authUser) + + if (!_.isNil(criteria.includeSoftDeleted) && criteria.includeSoftDeleted) { + // Only admin can request for deleted records + if (!isAdmin) { + throw new error.ForbiddenError('You are not allowed to perform that action') + } + } + try { - result = await listES(criteria) + result = await listES(criteria, isAdmin) } catch (e) { // log and ignore logger.logFullError(e) @@ -74,14 +106,19 @@ async function list (criteria) { } // then try to get from DB - let options + const options = {} if (criteria.name) { - options = { - name: { eq: criteria.name } - } + options.name = { eq: criteria.name } + } + if (!criteria.includeSoftDeleted) { + options.isDeleted = { ne: true } } // ignore pagination, scan all matched records result = await helper.scan(config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, options) + + if (!criteria.includeSoftDeleted) { + result = helper.sanitizeResult(result, true) + } // return fromDB:true to indicate it is got from db, // and response headers ('X-Total', 'X-Page', etc.) are not set in this case return { fromDB: true, result } @@ -91,34 +128,29 @@ list.schema = { criteria: Joi.object().keys({ page: Joi.page(), perPage: Joi.perPage(), - name: Joi.string() - }) + name: Joi.string(), + includeSoftDeleted: Joi.boolean() + }), + authUser: Joi.object() } /** * Get educational institution entity by id. * @param {String} id the educational institution id + * @param {Object} query The query params + * @param {Object} authUser The user making the request * @returns {Object} the educational institution of given id */ -async function getEntity (id) { - // first try to get from ES - try { - return await esClient.getSource({ - index: config.ES.EDUCATIONAL_INSTITUTION_INDEX, - type: config.ES.EDUCATIONAL_INSTITUTION_TYPE, - id - }) - } catch (e) { - // log and ignore - logger.logFullError(e) - } - - // then try to get from DB - return helper.getById(config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, id) +async function getEntity (id, query, authUser) { + return helper.getEntity(config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, id, query, authUser) } getEntity.schema = { - id: Joi.id() + id: Joi.id(), + query: Joi.object().keys({ + includeSoftDeleted: Joi.boolean() + }), + authUser: Joi.object() } /** @@ -129,13 +161,14 @@ getEntity.schema = { async function create (data) { await helper.validateDuplicate(config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, 'name', data.name) data.id = uuid() + data.isDeleted = false // create record in db const res = await helper.create(config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, data) // Send Kafka message using bus api await helper.postEvent(config.LOOKUP_CREATE_TOPIC, _.assign({ resource: Resources.EducationalInstitution }, res)) - return res + return helper.sanitizeResult(res) } create.schema = { @@ -163,10 +196,10 @@ async function partiallyUpdate (id, data) { // Send Kafka message using bus api await helper.postEvent(config.LOOKUP_UPDATE_TOPIC, _.assign({ resource: Resources.EducationalInstitution, id }, data)) - return res + return helper.sanitizeResult(res) } else { // data are not changed - return ei + return helper.sanitizeResult(ei) } } @@ -197,18 +230,22 @@ update.schema = { /** * Remove educational institution. * @param {String} id the educational institution id to remove + * @param {Object} query the query param */ -async function remove (id) { +async function remove (id, query) { // remove data in DB const ei = await helper.getById(config.AMAZON.DYNAMODB_EDUCATIONAL_INSTITUTION_TABLE, id) - await helper.remove(ei) + await helper.remove(ei, query.destroy) // Send Kafka message using bus api - await helper.postEvent(config.LOOKUP_DELETE_TOPIC, { resource: Resources.EducationalInstitution, id }) + await helper.postEvent(config.LOOKUP_DELETE_TOPIC, { resource: Resources.EducationalInstitution, id, isSoftDelete: !query.destroy }) } remove.schema = { - id: Joi.id() + id: Joi.id(), + query: Joi.object().keys({ + destroy: Joi.boolean() + }) } module.exports = {