From 32165a8cd73b684b25370654679e99e316dd9cfd Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 23 Jun 2025 08:13:17 +1000 Subject: [PATCH 01/14] See if this helps with schema usage --- migrations/umzug/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/migrations/umzug/index.js b/migrations/umzug/index.js index 7a807c262..a876293e2 100644 --- a/migrations/umzug/index.js +++ b/migrations/umzug/index.js @@ -5,6 +5,11 @@ const { Umzug, SequelizeStorage } = require('umzug'); // Initialize Sequelize const sequelize = new Sequelize(config.get('dbConfig.masterUrl'), { dialect: 'postgres', + schema: config.get('dbConfig.schema'), + dialectOptions: { + prependSearchPath: true + }, + searchPath: config.get('dbConfig.schema'), }); // Initialize Umzug From 5c39688da61afb656cb0fcd44c1f6410f3e509a6 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 25 Aug 2025 16:35:07 +0530 Subject: [PATCH 02/14] Allow non admins to see Applications --- src/routes/copilotOpportunityApply/list.js | 33 +++++++++++----------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 6051b0279..158cadfdc 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -1,17 +1,12 @@ import _ from 'lodash'; -import { middleware as tcMiddleware } from 'tc-core-library-js'; import models from '../../models'; import { ADMIN_ROLES } from '../../constants'; import util from '../../util'; -const permissions = tcMiddleware.permissions; - module.exports = [ - permissions('copilotApplications.view'), (req, res, next) => { const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); - const userId = req.authUser.userId; const opportunityId = _.parseInt(req.params.id); let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; @@ -24,17 +19,15 @@ module.exports = [ } const sortParams = sort.split(' '); - // Admin can see all requests and the PM can only see requests created by them const whereCondition = _.assign({ opportunityId, }, - canAccessAllApplications ? {} : { createdBy: userId }, ); return models.CopilotOpportunity.findOne({ where: { id: opportunityId, - } + }, }).then((opportunity) => { if (!opportunity) { const err = new Error('No opportunity found'); @@ -51,13 +44,13 @@ module.exports = [ ], order: [[sortParams[0], sortParams[1]]], }) - .then(copilotApplications => { + .then((copilotApplications) => { req.log.debug(`CopilotApplications ${JSON.stringify(copilotApplications)}`); return models.ProjectMember.getActiveProjectMembers(opportunity.projectId).then((members) => { req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); - const enrichedApplications = copilotApplications.map(application => { - const m = members.find(m => m.userId === application.userId); + const enrichedApplications = copilotApplications.map((application) => { + const m = members.find(member => member.userId === application.userId); // Using spread operator fails in lint check // While Object.assign fails silently during run time @@ -86,13 +79,21 @@ module.exports = [ return enriched; }); + const response = canAccessAllApplications + ? enrichedApplications + : enrichedApplications.map(app => ({ + userId: app.userId, + status: app.status, + createdAt: app.createdAt, + })); + req.log.debug(`Enriched Applications ${JSON.stringify(enrichedApplications)}`); - res.status(200).send(enrichedApplications); + res.status(200).send(response); }); - }) + }); }) - .catch((err) => { - util.handleError('Error fetching copilot applications', err, req, next); - }); + .catch((err) => { + util.handleError('Error fetching copilot applications', err, req, next); + }); }, ]; From f2b6f56b74ae84db38b105dd47c7b492b0db38a7 Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 25 Aug 2025 16:36:58 +0530 Subject: [PATCH 03/14] deploy PM-1612 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 763508b11..8f5877148 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1611_1'] + only: ['develop', 'migration-setup', 'PM-1612'] - deployProd: context : org-global filters: From eff16df9167ec694ad81f531071f6e984362286c Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Mon, 25 Aug 2025 16:38:27 +0530 Subject: [PATCH 04/14] deploy PM-1612 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 268aff933..8f5877148 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'pm-1510'] + only: ['develop', 'migration-setup', 'PM-1612'] - deployProd: context : org-global filters: From 429dc5a4821126f5d7cb27280a6003c418c92ddb Mon Sep 17 00:00:00 2001 From: himaniraghav3 Date: Tue, 26 Aug 2025 13:32:41 +0530 Subject: [PATCH 05/14] PM-1612 AI review feedback --- src/routes/copilotOpportunityApply/list.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/copilotOpportunityApply/list.js b/src/routes/copilotOpportunityApply/list.js index 158cadfdc..0275d3059 100644 --- a/src/routes/copilotOpportunityApply/list.js +++ b/src/routes/copilotOpportunityApply/list.js @@ -6,7 +6,7 @@ import util from '../../util'; module.exports = [ (req, res, next) => { - const canAccessAllApplications = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); + const isAdminOrPM = util.hasRoles(req, ADMIN_ROLES) || util.hasProjectManagerRole(req); const opportunityId = _.parseInt(req.params.id); let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'createdAt desc'; @@ -50,7 +50,7 @@ module.exports = [ req.log.debug(`Fetched existing active members ${JSON.stringify(members)}`); req.log.debug(`Applications ${JSON.stringify(copilotApplications)}`); const enrichedApplications = copilotApplications.map((application) => { - const m = members.find(member => member.userId === application.userId); + const member = members.find(memberItem => memberItem.userId === application.userId); // Using spread operator fails in lint check // While Object.assign fails silently during run time @@ -70,8 +70,8 @@ module.exports = [ copilotOpportunity: application.copilotOpportunity, }; - if (m) { - enriched.existingMembership = m; + if (member) { + enriched.existingMembership = member; } req.log.debug(`Existing member to application ${JSON.stringify(enriched)}`); @@ -79,7 +79,7 @@ module.exports = [ return enriched; }); - const response = canAccessAllApplications + const response = isAdminOrPM ? enrichedApplications : enrichedApplications.map(app => ({ userId: app.userId, From 158e8d932ccc43b71b50901718d0d657d130235a Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 15:46:26 +0200 Subject: [PATCH 06/14] fix: send project info only for PM and admin role users --- src/routes/copilotOpportunity/list.js | 10 ++++++++-- src/routes/copilotRequest/list.js | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 9a806290f..264805b1a 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -2,7 +2,7 @@ import _ from 'lodash'; import models from '../../models'; import util from '../../util'; -import DEFAULT_PAGE_SIZE from '../../constants'; +import DEFAULT_PAGE_SIZE, { USER_ROLE } from '../../constants'; module.exports = [ (req, res, next) => { @@ -15,6 +15,7 @@ module.exports = [ return util.handleError('Invalid sort criteria', null, req, next); } const sortParams = sort.split(' '); + const isAdminOrManager = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN, USER_ROLE.PROJECT_MANAGER]); // Extract pagination parameters const page = parseInt(req.query.page, 10) || 1; @@ -42,7 +43,7 @@ module.exports = [ baseOrder.push([sortParams[0], sortParams[1]]); return models.CopilotOpportunity.findAll({ - include: [ + include: isAdminOrManager ?[ { model: models.CopilotRequest, as: 'copilotRequest', @@ -52,6 +53,11 @@ module.exports = [ as: 'project', attributes: ['name'], }, + ] : [ + { + model: models.CopilotRequest, + as: 'copilotRequest', + } ], order: baseOrder, limit, diff --git a/src/routes/copilotRequest/list.js b/src/routes/copilotRequest/list.js index ef36d26bc..92b736149 100644 --- a/src/routes/copilotRequest/list.js +++ b/src/routes/copilotRequest/list.js @@ -4,7 +4,7 @@ import { Op, Sequelize } from 'sequelize'; import models from '../../models'; import util from '../../util'; import { PERMISSION } from '../../permissions/constants'; -import { DEFAULT_PAGE_SIZE } from '../../constants'; +import { DEFAULT_PAGE_SIZE, USER_ROLE } from '../../constants'; module.exports = [ (req, res, next) => { @@ -17,6 +17,8 @@ module.exports = [ return next(err); } + const isAdminOrManager = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN, USER_ROLE.PROJECT_MANAGER]); + const page = parseInt(req.query.page, 10) || 1; const pageSize = parseInt(req.query.pageSize, 10) || DEFAULT_PAGE_SIZE; const offset = (page - 1) * pageSize; @@ -46,7 +48,7 @@ module.exports = [ let order = [[sortParams[0], sortParams[1]]]; const relationBasedSortParams = ['projectName']; const jsonBasedSortParams = ['opportunityTitle', 'projectType']; - if (relationBasedSortParams.includes(sortParams[0])) { + if (relationBasedSortParams.includes(sortParams[0]) && isAdminOrManager) { order = [ [{model: models.Project, as: 'project'}, 'name', sortParams[1]], ['id', 'DESC'] @@ -64,9 +66,11 @@ module.exports = [ return models.CopilotRequest.findAndCountAll({ where: whereCondition, - include: [ + include: isAdminOrManager ? [ { model: models.CopilotOpportunity, as: 'copilotOpportunity', required: false }, { model: models.Project, as: 'project', required: false }, + ] : [ + { model: models.CopilotOpportunity, as: 'copilotOpportunity', required: false }, ], order, limit: pageSize, From def3f5c9cd004bf9305b9c27bbeef4c1c61adf52 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 15:47:01 +0200 Subject: [PATCH 07/14] fix: send project info only for PM and admin role users --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 8f5877148..e2ac728e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -149,7 +149,7 @@ workflows: context : org-global filters: branches: - only: ['develop', 'migration-setup', 'PM-1612'] + only: ['develop', 'migration-setup', 'PM-1612', 'fix-project-exposing'] - deployProd: context : org-global filters: From 344671c19eab736e0adca32cd836752afc8f1c2f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 15:59:00 +0200 Subject: [PATCH 08/14] fix: send project info only for PM and admin role users --- src/routes/copilotOpportunity/list.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index 264805b1a..e64577f25 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -66,6 +66,11 @@ module.exports = [ .then((copilotOpportunities) => { const formattedOpportunities = copilotOpportunities.map((opportunity) => { const plainOpportunity = opportunity.get({ plain: true }); + // For users who are not admin or manager, we dont want to expose + // the project id + if (!isAdminOrManager) { + delete plainOpportunity.projectId; + } return Object.assign({}, plainOpportunity, plainOpportunity.copilotRequest ? plainOpportunity.copilotRequest.data : {}, { copilotRequest: undefined }, From 11ae715c6e2e2e9360bb7b19d77703be19b49dbb Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 17:28:28 +0200 Subject: [PATCH 09/14] fix: project object optional --- src/routes/copilotOpportunity/list.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index e64577f25..d4871e9d2 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -66,6 +66,8 @@ module.exports = [ .then((copilotOpportunities) => { const formattedOpportunities = copilotOpportunities.map((opportunity) => { const plainOpportunity = opportunity.get({ plain: true }); + + req.debug.info(isAdminOrManager, 'admin or manager', plainOpportunity); // For users who are not admin or manager, we dont want to expose // the project id if (!isAdminOrManager) { From 3d2216a89791b6d8c8f910488811d6e0aa3199ae Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 17:50:55 +0200 Subject: [PATCH 10/14] fix: project object optional --- src/routes/copilotOpportunity/list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index d4871e9d2..cf3796abd 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -67,7 +67,7 @@ module.exports = [ const formattedOpportunities = copilotOpportunities.map((opportunity) => { const plainOpportunity = opportunity.get({ plain: true }); - req.debug.info(isAdminOrManager, 'admin or manager', plainOpportunity); + req.log.debug(isAdminOrManager, 'admin or manager', plainOpportunity); // For users who are not admin or manager, we dont want to expose // the project id if (!isAdminOrManager) { From 6fb8f34a75dabc30935fe32d3a1ab062a91a649f Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 20:50:26 +0200 Subject: [PATCH 11/14] fix: project object optional --- src/routes/copilotOpportunity/list.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index cf3796abd..c0db026b4 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -68,15 +68,18 @@ module.exports = [ const plainOpportunity = opportunity.get({ plain: true }); req.log.debug(isAdminOrManager, 'admin or manager', plainOpportunity); + + const formatted = Object.assign({}, plainOpportunity, + plainOpportunity.copilotRequest ? plainOpportunity.copilotRequest.data : {}, + { copilotRequest: undefined }, + ); + // For users who are not admin or manager, we dont want to expose // the project id if (!isAdminOrManager) { delete plainOpportunity.projectId; } - return Object.assign({}, plainOpportunity, - plainOpportunity.copilotRequest ? plainOpportunity.copilotRequest.data : {}, - { copilotRequest: undefined }, - ); + return formatted; }); return util.setPaginationHeaders(req, res, { count: copilotOpportunities.count, From f8f52e0fc9d749b5484f24510cc185fcf50ced99 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 20:53:06 +0200 Subject: [PATCH 12/14] fix: project object optional --- src/routes/copilotOpportunity/get.js | 23 +++++++++++++++++++---- src/routes/copilotOpportunity/list.js | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/src/routes/copilotOpportunity/get.js b/src/routes/copilotOpportunity/get.js index e4bad5c8d..f71394fdf 100644 --- a/src/routes/copilotOpportunity/get.js +++ b/src/routes/copilotOpportunity/get.js @@ -1,3 +1,4 @@ +import { USER_ROLE } from '../../constants'; import models from '../../models'; import util from '../../util'; @@ -8,9 +9,11 @@ module.exports = [ return util.handleError('Invalid opportunity ID', null, req, next, 400); } + const isAdminOrManager = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN, USER_ROLE.PROJECT_MANAGER]); + return models.CopilotOpportunity.findOne({ where: { id }, - include: [ + include: isAdminOrManager ? [ { model: models.CopilotRequest, as: 'copilotRequest', @@ -27,17 +30,25 @@ module.exports = [ }, ] }, + ]: [ + { + model: models.CopilotRequest, + as: 'copilotRequest', + }, ], }) .then((copilotOpportunity) => { const plainOpportunity = copilotOpportunity.get({ plain: true }); - const memberIds = plainOpportunity.project.members && plainOpportunity.project.members.map((member) => member.userId); + const memberIds = (plainOpportunity.project && plainOpportunity.project.members && plainOpportunity.project.members.map((member) => member.userId)) || []; let canApplyAsCopilot = false; if (req.authUser) { canApplyAsCopilot = !memberIds.includes(req.authUser.userId) } - // This shouldn't be exposed to the clientside - delete plainOpportunity.project.members; + + if (plainOpportunity.project) { + // This shouldn't be exposed to the clientside + delete plainOpportunity.project.members; + } const formattedOpportunity = Object.assign({ members: memberIds, canApplyAsCopilot, @@ -45,6 +56,10 @@ module.exports = [ plainOpportunity.copilotRequest ? plainOpportunity.copilotRequest.data : {}, { copilotRequest: undefined }, ); + + if (!isAdminOrManager) { + delete formattedOpportunity.projectId; + } res.json(formattedOpportunity); }) .catch((err) => { diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index c0db026b4..fb4065fd6 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -73,11 +73,11 @@ module.exports = [ plainOpportunity.copilotRequest ? plainOpportunity.copilotRequest.data : {}, { copilotRequest: undefined }, ); - + // For users who are not admin or manager, we dont want to expose // the project id if (!isAdminOrManager) { - delete plainOpportunity.projectId; + delete formatted.projectId; } return formatted; }); From d81d76347b176c09cfcac3cbca19ac5727762110 Mon Sep 17 00:00:00 2001 From: Hentry Martin Date: Tue, 2 Sep 2025 21:14:42 +0200 Subject: [PATCH 13/14] fix: removed debug log --- src/routes/copilotOpportunity/list.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/routes/copilotOpportunity/list.js b/src/routes/copilotOpportunity/list.js index fb4065fd6..9206157c5 100644 --- a/src/routes/copilotOpportunity/list.js +++ b/src/routes/copilotOpportunity/list.js @@ -66,8 +66,6 @@ module.exports = [ .then((copilotOpportunities) => { const formattedOpportunities = copilotOpportunities.map((opportunity) => { const plainOpportunity = opportunity.get({ plain: true }); - - req.log.debug(isAdminOrManager, 'admin or manager', plainOpportunity); const formatted = Object.assign({}, plainOpportunity, plainOpportunity.copilotRequest ? plainOpportunity.copilotRequest.data : {}, From 56de0585415bfe441a3d9f19a5ab20a4b0e7de2a Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Wed, 24 Sep 2025 07:09:40 +1000 Subject: [PATCH 14/14] Updates for returning large attachments immediately and continuing the S3 copy in the background --- src/routes/attachments/create.js | 174 ++++++++++++++----------------- 1 file changed, 78 insertions(+), 96 deletions(-) diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js index 669613e36..cb4e3c6c2 100644 --- a/src/routes/attachments/create.js +++ b/src/routes/attachments/create.js @@ -39,7 +39,7 @@ module.exports = [ * Add project attachment * In development mode we have to mock the ec2 file transfer and file service calls */ - (req, res, next) => { + async (req, res, next) => { const data = req.body; // default values const projectId = req.params.projectId; @@ -53,64 +53,22 @@ module.exports = [ // extract file name const fileName = Path.parse(data.path).base; // create file path - const path = _.join([ + const attachmentPath = _.join([ config.get('projectAttachmentPathPrefix'), data.projectId, config.get('projectAttachmentPathPrefix'), fileName, ], '/'); - let newAttachment = null; const sourceBucket = data.s3Bucket; const sourceKey = data.path; const destBucket = config.get('attachmentsS3Bucket'); - const destKey = path; + const destKey = attachmentPath; - if (data.type === ATTACHMENT_TYPES.LINK) { - // We create the record in the db and return (i.e. no need to handle transferring file between S3 buckets) - Promise.resolve(models.ProjectAttachment.create({ - projectId, - allowedUsers, - createdBy: req.authUser.userId, - updatedBy: req.authUser.userId, - title: data.title, - size: data.size, - category: data.category || null, - description: data.description, - contentType: data.contentType, - path: data.path, - type: data.type, - tags: data.tags, - })).then((_link) => { - const link = _link.get({ plain: true }); - req.log.debug('New Link Attachment record: ', link); - - // emit the Kafka event - util.sendResourceToKafkaBus( - req, - EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED, - RESOURCES.ATTACHMENT, - link); - - res.status(201).json(link); - return Promise.resolve(); - }) - .catch((error) => { - req.log.error('Error adding link attachment', error); - const rerr = error; - rerr.status = rerr.status || 500; - next(rerr); - }); - } else { - // don't actually transfer file in development mode if file uploading is disabled, so we can test this endpoint - const fileTransferPromise = (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true') - ? util.s3FileTransfer(req, sourceBucket, sourceKey, destBucket, destKey) - : Promise.resolve(); - - fileTransferPromise.then(() => { - // file copied to final destination, create DB record - req.log.debug('creating db file record'); - return models.ProjectAttachment.create({ + try { + if (data.type === ATTACHMENT_TYPES.LINK) { + // Create the record and return immediately (no file transfer needed) + const linkInstance = await models.ProjectAttachment.create({ projectId, allowedUsers, createdBy: req.authUser.userId, @@ -120,60 +78,84 @@ module.exports = [ category: data.category || null, description: data.description, contentType: data.contentType, - path, + path: data.path, type: data.type, tags: data.tags, }); - }).then((_newAttachment) => { - newAttachment = _newAttachment.get({ plain: true }); - req.log.debug('New Attachment record: ', newAttachment); - if (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true') { - // retrieve download url for the response - req.log.debug('retrieving download url'); - return getDownloadUrl(destBucket, path); - } - return Promise.resolve(); - }).then((url) => { - if ( - process.env.NODE_ENV !== 'development' || - config.get('enableFileUpload') === 'true' - ) { - req.log.debug('Retreiving Presigned Url resp: ', url); - let response = _.cloneDeep(newAttachment); - response = _.omit(response, ['path', 'deletedAt']); - - response.downloadUrl = url; - - // emit the event - util.sendResourceToKafkaBus( - req, - EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED, - RESOURCES.ATTACHMENT, - newAttachment, - ); - res.status(201).json(response); - return Promise.resolve(); - } - let response = _.cloneDeep(newAttachment); - response = _.omit(response, ['path', 'deletedAt']); - // only in development mode - response.downloadUrl = path; - // emit the event + const link = linkInstance.get({ plain: true }); + req.log.debug('New Link Attachment record: ', link); + util.sendResourceToKafkaBus( req, EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED, RESOURCES.ATTACHMENT, - newAttachment); - - res.status(201).json(response); - return Promise.resolve(); - }) - .catch((error) => { - req.log.error('Error adding file attachment', error); - const rerr = error; - rerr.status = rerr.status || 500; - next(rerr); - }); + link, + ); + + res.status(201).json(link); + return; + } + + const shouldTransfer = process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true'; + const downloadUrlPromise = shouldTransfer + ? getDownloadUrl(destBucket, destKey) + : Promise.resolve(destKey); + + req.log.debug('creating db file record'); + const attachmentInstance = await models.ProjectAttachment.create({ + projectId, + allowedUsers, + createdBy: req.authUser.userId, + updatedBy: req.authUser.userId, + title: data.title, + size: data.size, + category: data.category || null, + description: data.description, + contentType: data.contentType, + path: destKey, + type: data.type, + tags: data.tags, + }); + + const newAttachment = attachmentInstance.get({ plain: true }); + req.log.debug('New Attachment record: ', newAttachment); + + const downloadUrl = await downloadUrlPromise; + req.log.debug('Retrieved presigned url for new attachment'); + + let response = _.cloneDeep(newAttachment); + response = _.omit(response, ['path', 'deletedAt']); + response.downloadUrl = downloadUrl; + + util.sendResourceToKafkaBus( + req, + EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED, + RESOURCES.ATTACHMENT, + newAttachment, + ); + + res.status(201).json(response); + + if (shouldTransfer) { + util.s3FileTransfer(req, sourceBucket, sourceKey, destBucket, destKey) + .then(() => { + req.log.debug('File attachment copied asynchronously', { attachmentId: newAttachment.id }); + }) + .catch((error) => { + req.log.error('Async S3 file transfer failed', { + error: error.message, + stack: error.stack, + attachmentId: newAttachment.id, + source: `${sourceBucket}/${sourceKey}`, + destination: `${destBucket}/${destKey}`, + }); + }); + } + } catch (error) { + req.log.error('Error adding attachment', error); + const rerr = error; + rerr.status = rerr.status || 500; + next(rerr); } }, ];