diff --git a/CHANGELOG.md b/CHANGELOG.md index a2e4340..036640d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +### 1.11.0 Jan 9, 2025 +* Added external link model, service, and swagger definitions. Modified search service. [DESENG-751](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-751) + ### 1.10.0 Nov 26, 2024 * Modified project definition to accomodate shape file colours. [DESENG-743](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-743) * Modified project definition to accomodate project type multiselect. [DESENG-745](https://apps.itsm.gov.bc.ca/jira/browse/DESENG-745) diff --git a/api/controllers/externalLink.js b/api/controllers/externalLink.js new file mode 100644 index 0000000..1b199f4 --- /dev/null +++ b/api/controllers/externalLink.js @@ -0,0 +1,279 @@ +const { remove, indexOf, assignIn } = require('lodash'); +const defaultLog = require('winston').loggers.get('defaultLog'); +const mongoose = require('mongoose'); +const Actions = require('../helpers/actions'); +const Utils = require('../helpers/utils'); + +const getSanitizedFields = (fields) => { + return remove(fields, (f) => { + return (indexOf([ + '_addedBy', + 'project', + 'displayName', + 'externalLink', + 'section', + 'dateAdded', + 'dateUpdated', + 'description', + 'projectPhase', + 'checkbox', + 'read', + ], f) !== -1); + }); +}; + +exports.protectedOptions = (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED OPTIONS'); + res.status(200).send(); +}; + +exports.publicGet = async (args, res) => { + defaultLog.info('EXTERNAL LINK PUBLIC GET'); + // Build match query if on exLinkId route + let query = {}; + + if (args.swagger.params.exLinkId?.value) { + query = Utils.buildQuery("_id", args.swagger.params.exLinkId.value, query); + } else if (args.swagger.params.exLinkIds?.value?.length > 0) { + query = Utils.buildQuery("_id", args.swagger.params.exLinkIds.value, query); + } + + if (args.swagger.params.project?.value) { + query = Utils.buildQuery("project", args.swagger.params.project.value, query); + } + + // Set query type + assignIn(query, { "_schemaName": "ExternalLink" }); + + try { + const data = await Utils.runDataQuery( + 'ExternalLink', + ['public'], + null, + query, + getSanitizedFields(args.swagger.params.fields.value), // Fields + null, // sort warmup + null, // sort + null, // skip + null, // limit + false); // count + defaultLog.info('Got external link file(s):', data); + Utils.recordAction('Get', 'ExternalLink', 'public', args.swagger.params.exLinkId?.value || null); + return Actions.sendResponse(res, 200, data); + } catch (e) { + defaultLog.error(e); + return Actions.sendResponse(res, 400, e); + } +}; + +exports.protectedHead = (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED HEAD'); + // Build match query if on exLinkId route + let query = {}; + if (args.swagger.params.exLinkId?.value) { + query = Utils.buildQuery("_id", args.swagger.params.exLinkId.value, query); + } + if (args.swagger.params._application?.value) { + query = Utils.buildQuery('_application', args.swagger.params._application.value, query); + } + if (args.swagger.params._comment?.value) { + query = Utils.buildQuery('_comment', args.swagger.params._comment.value, query); + } + // Set query type + assignIn(query, { "_schemaName": "ExternalLink" }); + + Utils.runDataQuery('ExternalLink', + args.swagger.params.auth_payload.client_roles, + args.swagger.params.auth_payload.idir_user_guid, + query, + ['_id', + 'read'], // Fields + null, // sort warmup + null, // sort + null, // skip + null, // limit + true) // count + .then((data) => { + Utils.recordAction('Head', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, args.swagger.params.exLinkId?.value || null); + if (!(args.swagger.params.exLinkId && args.swagger.params.exLinkId.value) || (data && data.length > 0)) { + res.setHeader('x-total-count', data?.length > 0 ? data[0].total_items : 0); + return Actions.sendResponse(res, 200, data); + } else { + return Actions.sendResponse(res, 404, data); + } + }); +} + +exports.protectedGet = async (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED GET'); + let query = {}, sort = {}, skip = null, limit = null, count = false; + + // Build match query if on exLinkId route + if (args.swagger.params.exLinkId?.value) { + assignIn(query, { _id: mongoose.Types.ObjectId(args.swagger.params.exLinkId.value) }); + } else if (args.swagger.params.exLinkIds?.value?.length > 0) { + query = Utils.buildQuery("_id", args.swagger.params.exLinkIds.value); + } + if (args.swagger.params.project?.value) { + query = Utils.buildQuery("project", args.swagger.params.project.value, query); + } + // Set query type + assignIn(query, { "_schemaName": "ExternalLink" }); + + try { + const data = await Utils.runDataQuery('ExternalLink', + args.swagger.params.auth_payload.client_roles, + args.swagger.params.auth_payload.idir_user_guid, + query, + getSanitizedFields(args.swagger.params.fields.value), // Fields + null, // sort warmup + sort, // sort + skip, // skip + limit, // limit + count); // count + Utils.recordAction('Get', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, args.swagger.params.exLinkId?.value || null); + defaultLog.info('Got external file(s):', data); + return Actions.sendResponse(res, 200, data); + } catch (e) { + defaultLog.error(e); + return Actions.sendResponse(res, 400, e); + } +}; + +exports.protectedPost = async (args, res, next) => { + defaultLog.info('EXTERNAL LINK PROTECTED POST'); + try { + const project = args.swagger.params.project?.value; + defaultLog.info('Section value:', args.swagger.params.section?.value); + Promise.resolve() + .then(async () => { + const ExternalLink = mongoose.model('ExternalLink'); + const extLink = new ExternalLink(); + // Define security tag defaults + extLink.read = ['sysadmin', 'staff']; + extLink.write = ['sysadmin', 'staff']; + extLink.delete = ['sysadmin', 'staff']; + + // Map the form values + extLink.project = mongoose.Types.ObjectId(project); + extLink._addedBy = args.swagger.params.auth_payload.preferred_username; + extLink._createdDate = new Date(); + extLink.displayName = args.swagger.params.displayName.value; + extLink.externalLink = args.swagger.params.externalLink.value; + extLink.section = args.swagger.params.section?.value; + extLink.dateAdded = args.swagger.params.dateAdded.value; + extLink.dateUpdated = args.swagger.params.dateUpdated.value; + extLink.description = args.swagger.params.description.value; + extLink.projectPhase = args.swagger.params.projectPhase.value; + extLink.checkbox = 'true' === args.body.checkbox ? true : false; + extLink.save() + .then((exl) => { + Utils.recordAction('Post', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, exl._id); + return Actions.sendResponse(res, 200, exl); + }) + .catch((error) => { + defaultLog.error(error); + return Actions.sendResponse(res, 400, error); + }); + }) + .catch(error => defaultLog.error(error)); + } catch (e) { + defaultLog.error(e); + // Delete the path details before we return to the caller. + delete e['path']; + return Actions.sendResponse(res, 500, e); + } +}; + +exports.protectedPublish = async (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED PUBLISH'); + const objId = args.swagger.params.exLinkId.value; + defaultLog.info("Publish External Link:", objId); + + const ExternalLink = require('mongoose').model('ExternalLink'); + try { + const exLink = await ExternalLink.findOne({ _id: objId }); + if (exLink) { + defaultLog.info("External Link:", exLink); + const published = await Actions.publish(await exLink.save()); + Utils.recordAction('Publish', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, objId); + return Actions.sendResponse(res, 200, published); + } else { + defaultLog.info("Couldn't find that external link!"); + return Actions.sendResponse(res, 404, e); + } + } catch (e) { + return Actions.sendResponse(res, 400, e); + } +}; + +exports.protectedUnPublish = async (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED UNPUBLISH'); + const objId = args.swagger.params.exLinkId.value; + defaultLog.info("Unpublish External Link:", objId); + const ExternalLink = require('mongoose').model('ExternalLink'); + try { + const exLink = await ExternalLink.findOne({ _id: objId }); + if (exLink) { + const unPublished = await Actions.unPublish(await exLink.save()); + Utils.recordAction('Unpublish', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, objId); + defaultLog.info("Published external link:", objId); + return Actions.sendResponse(res, 200, unPublished); + } else { + defaultLog.info("Couldn't find that external link!"); + return Actions.sendResponse(res, 404, e); + } + } catch (e) { + defaultLog.error(e); + return Actions.sendResponse(res, 400, e); + } +}; + +exports.protectedPut = async (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED PUT'); + const objId = args.swagger.params.exLinkId.value; + let obj = {}; + defaultLog.info('Put external link:', objId); + + obj._updatedBy = args.swagger.params.auth_payload.preferred_username; + obj.displayName = args.swagger.params.displayName.value; + obj.externalLink = args.swagger.params.externalLink.value; + obj.section = args.swagger.params.section.value; + obj.projectPhase = args.swagger.params.projectPhase.value; + obj.dateAdded = args.swagger.params.dateAdded.value; + obj.dateUpdated = args.swagger.params.dateUpdated.value; + obj.description = args.swagger.params.description.value; + obj.section = "null" === args.swagger.params.section.value ? null : args.swagger.params.section.value; + const ExternalLink = mongoose.model('ExternalLink'); + + try { + const exLink = await ExternalLink.findOneAndUpdate({ _id: objId }, obj, { upsert: false, new: true }); + if (exLink) { + Utils.recordAction('put', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, objId); + defaultLog.info('External link updated:', objId); + return Actions.sendResponse(res, 200, exLink); + } else { + defaultLog.info("Couldn't find that external link!"); + return Actions.sendResponse(res, 404, {}); + } + } catch (e) { + defaultLog.error(e); + return Actions.sendResponse(res, 400, e); + } +} + +exports.protectedDelete = async (args, res) => { + defaultLog.info('EXTERNAL LINK PROTECTED DELETE'); + const objId = args.swagger.params.exLinkId.value; + defaultLog.info("Delete External Link:", objId); + const ExternalLink = require('mongoose').model('ExternalLink'); + + try { + await ExternalLink.findOneAndRemove({ _id: objId }); + Utils.recordAction('Delete', 'ExternalLink', args.swagger.params.auth_payload.preferred_username, objId); + return Actions.sendResponse(res, 200, {}); + } catch (e) { + defaultLog.error("Error:", e); + return Actions.sendResponse(res, 400, e); + } +}; diff --git a/api/controllers/search.js b/api/controllers/search.js index 29bad79..a8c2117 100644 --- a/api/controllers/search.js +++ b/api/controllers/search.js @@ -411,17 +411,14 @@ var executeQuery = async function (args, res, next) { }); if (dataset !== 'Item') { - var data = await searchCollection(roles, userProjectPermissions, keywords, dataset, pageNum, pageSize, project, sortField, sortDirection, caseSensitive, populate, and, or) - if (dataset === 'Comment') { - // Filter - each(data[0].searchResults, function (item) { - if (item.isAnonymous === true) { - delete item.author; - } - }); - } + var data = await searchCollection(roles, userProjectPermissions, keywords, dataset, pageNum, pageSize, project, sortField, sortDirection, caseSensitive, populate, and, or); + // Filter + each(data[0].searchResults, function (item) { + if (item.isAnonymous === true) { + delete item.author; + } + }); return Actions.sendResponse(res, 200, data); - } else if (dataset === 'Item') { var collectionObj = mongoose.model(args.swagger.params._schemaName.value); diff --git a/api/helpers/models/externalLink.js b/api/helpers/models/externalLink.js new file mode 100644 index 0000000..b630c24 --- /dev/null +++ b/api/helpers/models/externalLink.js @@ -0,0 +1,22 @@ +module.exports = require('../models')('ExternalLink', { + + _createdDate: { type: Date, default: Date.now() }, + _updatedDate: { type: Date, default: Date.now() }, + _addedBy: { type: String, default: 'system' }, + _updatedBy: { type: String, default: 'system' }, + _deletedBy: { type: String, default: 'system' }, + + read: [{ type: String, trim: true, default: 'sysadmin' }], + write: [{ type: String, trim: true, default: 'sysadmin' }], + delete: [{ type: String, trim: true, default: 'sysadmin' }], + + project: { type: 'ObjectId', ref: 'Project', default: null }, + section: { type: 'ObjectId', ref: 'DocumentSection', default: null } | null, + displayName: { type: String, default:'' }, + externalLink: { type: String, default: '' }, + dateAdded: { type: Date, default: Date.now() }, + dateUpdated: { type: Date, default: Date.now() }, + description: { type: String, default: '' }, + projectPhase: { type: String, default: '' }, + checkbox: { type: Boolean, default: false }, +}, 'lup'); diff --git a/api/swagger/swagger.yaml b/api/swagger/swagger.yaml index 9366fa8..75d13bd 100644 --- a/api/swagger/swagger.yaml +++ b/api/swagger/swagger.yaml @@ -216,6 +216,32 @@ definitions: - internalMime - internalSize +### External Link Definitions + ExLinkId: + type: object + properties: + displayName: + type: string + example: "A cool external link" + ExLinkObject: + type: object + properties: + displayName: + type: string + example: "A cool external link" + ExternalLinkFields: + type: string + description: "Optional fields to return." + example: name|type + enum: &externalLinkFields + - displayName + - externalLink + - dateAdded + - dateUpdated + - description + - section + - projectPhase + ### Document Section Definitions DocumentSectionId: type: object @@ -3433,6 +3459,524 @@ paths: schema: $ref: "#/definitions/Error" +## External Links + /link: + x-swagger-router-controller: externalLink + options: + tags: + - link + summary: "Pre-flight request" + operationId: protectedOptions + description: "Options on External Link Route" + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + post: + tags: + - link + summary: "Create a new external link" + operationId: protectedPost + description: "Create a new external link object." + summary: "Create a new external link object." + security: + - Bearer: [] + x-security-scopes: + - staff + - sysadmin + consumes: + - multipart/form-data + parameters: + - name: project + in: formData + description: "The application to bind this file to." + required: false + type: string + - name: section + in: formData + required: false + type: string + - name: projectPhase + in: formData + description: "The project phase of the file." + required: false + type: string + - name: dateAdded + in: formData + description: "The dateAdded of the file." + required: false + type: string + - name: dateUpdated + in: formData + description: "The dateUpdated of the file." + required: false + type: string + - name: description + in: formData + description: "The description of the file." + required: false + type: string + - name: externalLink + in: formData + description: "The external link of the file." + required: false + type: string + - name: displayName + in: formData + description: "The displayName of the file." + required: false + type: string + - name: checkbox + in: formData + description: "The checkbox state of the file." + required: false + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + get: + tags: + - link + summary: "Get a list of external links" + operationId: protectedGet + description: "Authenticated access to retreiving an external link." + security: + - Bearer: [] + x-security-scopes: + - sysadmin + - staff + parameters: + - in: query + name: exLinkIds + description: "Search using an array of external link ids." + type: array + required: false + collectionFormat: pipes + items: + type: string + - in: query + name: project + description: "External Links relating to (a) particular Project(s)" + type: array + required: false + collectionFormat: pipes + items: + type: string + - in: query + name: _comment + description: "External Links relating to a particular Comment(s)" + type: array + required: false + collectionFormat: pipes + items: + type: string + - in: query + name: fields + description: "External Links fields to return." + required: false + type: array + collectionFormat: pipes + items: + type: string + enum: *externalLinkFields + - in: query + name: isDeleted + type: boolean + required: false + description: "External Links that are deleted or not" + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + /link/{exLinkId}: + x-swagger-router-controller: externalLink + options: + tags: + - link + summary: "Pre-flight request" + operationId: protectedOptions + description: "Options on External Link GET Route" + parameters: + - name: exLinkId + in: path + description: "ID of external link to get" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + get: + tags: + - link + summary: "Get an external link" + operationId: protectedGet + description: "Authenticated access to retreiving an external link." + security: + - Bearer: [] + x-security-scopes: + - staff + - sysadmin + parameters: + - name: exLinkId + in: path + description: "ID of external link to get" + required: true + type: string + - in: query + name: fields + description: "External link fields to return." + required: false + type: array + collectionFormat: pipes + items: + type: string + enum: *externalLinkFields + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + put: + tags: + - link + summary: "Update/Upload an external link" + operationId: protectedPut + description: "Update an external link object." + security: + - Bearer: [] + x-security-scopes: + - sysadmin + consumes: + - multipart/form-data + parameters: + - name: exLinkId + in: path + description: "ID of external link to update" + required: true + type: string + - name: project + in: formData + description: "The application to bind this document to." + required: false + type: string + - name: section + in: formData + description: "The section to organize the document under." + required: false + type: string + - name: projectPhase + in: formData + description: "The project phase of the file." + required: false + type: string + - name: dateAdded + in: formData + description: "The date on which the file was added." + required: false + type: string + - name: dateUpdated + in: formData + description: "The date on which the file was updated last." + required: false + type: string + - name: description + in: formData + description: "The description of the file." + required: false + type: string + - name: displayName + in: formData + description: "The displayName of the file." + required: false + type: string + - name: externalLink + in: formData + description: "The external link of the file." + required: false + type: string + - name: checkbox + in: formData + description: "The checkbox state of the file." + required: false + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + delete: + tags: + - link + summary: "Delete an external link" + operationId: protectedDelete + description: "Delete an external link object." + security: + - Bearer: [] + x-security-scopes: + - sysadmin + consumes: + - application/json + parameters: + - name: exLinkId + in: path + description: "ID of external link to delete" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + + /link/{exLinkId}/publish: + x-swagger-router-controller: externalLink + options: + tags: + - link + summary: "Pre-flight request" + operationId: protectedOptions + description: "Options on External Link GET Route" + parameters: + - name: exLinkId + in: path + description: "ID of external link to get" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + put: + tags: + - link + summary: "Publish an external link" + operationId: protectedPublish + description: "Adds the singular instance of the 'public' role to a external link." + security: + - Bearer: [] + x-security-scopes: + - staff + - sysadmin + # TODO: Define publish roles. + parameters: + - name: exLinkId + in: path + description: "ID of external link to update" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + + /link/{exLinkId}/unpublish: + x-swagger-router-controller: externalLink + options: + tags: + - link + summary: "Pre-flight request" + operationId: protectedOptions + description: "Options on ExternalLink GET Route" + parameters: + - name: exLinkId + in: path + description: "ID of external link to get" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + put: + tags: + - link + summary: "UnPublish an external link" + operationId: protectedUnPublish + description: "Removes the singular instance of the 'public' role on an external link." + security: + - Bearer: [] + x-security-scopes: + - staff + - sysadmin + # TODO: Define unpublish roles. + parameters: + - name: exLinkId + in: path + description: "ID of external link to update" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + + /public/link: + x-swagger-router-controller: externalLink + options: + tags: + - link + summary: "Pre-flight request" + operationId: protectedOptions + description: "Options on External Link Route" + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + get: + tags: + - link + summary: "Get a list of external links" + operationId: publicGet + description: "Retreiving public external links." + parameters: + - in: query + name: exLinkIds + description: "Search using an array of external link ids." + type: array + required: false + collectionFormat: pipes + items: + type: string + - in: query + name: project + description: "Documents relating to a particular Project(s)" + type: array + required: false + collectionFormat: pipes + items: + type: string + - in: query + name: fields + description: "Document fields to return." + required: false + type: array + collectionFormat: pipes + items: + type: string + enum: *externalLinkFields + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + + /public/link/{exLinkId}: + x-swagger-router-controller: externalLink + options: + tags: + - link + summary: "Pre-flight request" + operationId: protectedOptions + description: "Options on ExternalLink GET Route" + parameters: + - name: exLinkId + in: path + description: "ID of external link to get" + required: true + type: string + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + get: + tags: + - link + summary: "Get an external link" + operationId: publicGet + description: "Public access to retreiving external links." + parameters: + - name: exLinkId + in: path + description: "ID of external link to get" + required: true + type: string + - in: query + name: fields + description: "External link fields to return." + required: false + type: array + collectionFormat: pipes + items: + type: string + enum: *externalLinkFields + responses: + "200": + description: "Success" + schema: + $ref: "#/definitions/ExLinkObject" + "403": + description: "Access Denied" + schema: + $ref: "#/definitions/Error" + ## Documents Section /documentSection: x-swagger-router-controller: documentSection diff --git a/app.js b/app.js index 08fb3b3..4ef9080 100644 --- a/app.js +++ b/app.js @@ -121,6 +121,7 @@ swaggerTools.initializeMiddleware(swaggerConfig, function(middleware) { require('./api/helpers/models/surveyQuestionAnswer'); require('./api/helpers/models/surveyResponse'); require('./api/helpers/models/document'); + require('./api/helpers/models/externalLink'); require('./api/helpers/models/documentSection'); require('./api/helpers/models/comment'); require('./api/helpers/models/commentperiod'); diff --git a/package.json b/package.json index ed812a4..eb3071c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "landuseplanning-api", - "version": "1.10.0", + "version": "1.11.0", "author": "Mark Lisé", "contributors": [ "Mark Lisé ",