From dd0f409ced34f99af7b4d8c3ca9aef7413a0d5d3 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 30 Dec 2024 11:26:23 +0530 Subject: [PATCH 01/18] Fix for duration --- src/requests/consumption.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index b8ab67a5..2993d674 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -60,6 +60,11 @@ const publishProjectTemplates = function (templateData) { let template = formattedTemplate.template + //add duration key if consumption service is diksha + if (process.env.CONSUMPTION_SERVICE == common.DIKSHA && templateData.recommended_duration) { + template.duration = utils.convertDuration(templateData.recommended_duration) + } + // Process Categories if (templateData.categories?.length > 0) { let categoriesResponse = await processCategories(templateData.categories) @@ -145,7 +150,6 @@ const formatTemplate = (templateData) => { externalId: utils.generateExternalId(templateData.title), entityType: '', metaInformation: utils.formatProjectMetaInformation(templateData), - duration: utils.convertDuration(templateData.recommended_duration), recommendedFor: [], //Initially empty categories: [], //Initially empty tasks: [], // Initially empty From 82c71eeb8e64045698afe25e07eb27bb717edb2d Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 30 Dec 2024 12:11:45 +0530 Subject: [PATCH 02/18] fix for dates --- src/requests/consumption.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index 2993d674..67c9b2c7 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -154,6 +154,8 @@ const formatTemplate = (templateData) => { categories: [], //Initially empty tasks: [], // Initially empty taskSequence: [], // Initially empty + createdAt: new Date(), + updatedAt: new Date(), } return { success: true, template } From fc4e27955a257e79f29ca8af2e2e4da4d207999e Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 30 Dec 2024 12:33:25 +0530 Subject: [PATCH 03/18] deletable fix --- src/requests/consumption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index 67c9b2c7..e847618d 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -277,7 +277,7 @@ async function createTasks(tasks, templateId, templateExternalId, parentId = nul description: task.name, externalId: utils.generateExternalId(task.name), type: task.type, - isDeleted: !task.is_mandatory, + isDeleted: false, isDeletable: !task.is_mandatory, sequenceNumber: task.sequence_no, projectTemplateId: templateId, From 06c8a5dca01e2a54a6066218fb92c0e2a49c3a32 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 30 Dec 2024 15:18:13 +0530 Subject: [PATCH 04/18] fix task issue --- src/requests/consumption.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index e847618d..6ef6ae4f 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -312,6 +312,8 @@ async function createTasks(tasks, templateId, templateExternalId, parentId = nul ) } + taskIds.push(...childTaskResult.taskIds) + // Update task with child task sequence and children await taskCollection.updateOne( { _id: taskId }, From 64689cf94e851c6cce6c6aad7aeab6ae9ea8e219 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Mon, 30 Dec 2024 15:43:54 +0530 Subject: [PATCH 05/18] Reverting the language based validation --- ...ate-entity-types-validations-max-length.js | 8 ++--- ...-add-entity-types-for-tasks-validations.js | 2 +- ...23173032-update-entity-type-validations.js | 10 +++--- src/generics/utils.js | 32 ++----------------- 4 files changed, 13 insertions(+), 39 deletions(-) diff --git a/src/database/migrations/20240726043835-update-entity-types-validations-max-length.js b/src/database/migrations/20240726043835-update-entity-types-validations-max-length.js index b73f753f..9934774a 100644 --- a/src/database/migrations/20240726043835-update-entity-types-validations-max-length.js +++ b/src/database/migrations/20240726043835-update-entity-types-validations-max-length.js @@ -5,10 +5,10 @@ module.exports = { // as per the discussion with products , all text field length is set to 256 and text-area tp 2000 const validations = { - title: { required: true, regex: "^[a-zA-Z0-9 <>_&-' ]+$" }, - description: { required: true, regex: "^[a-zA-Z0-9 <>_&-' ]+$" }, - objective: { required: true, regex: "^[a-zA-Z0-9 <>_&-' ]+$" }, - name: { required: true, regex: "^[a-zA-Z0-9 <>_&-' ]+$" }, + title: { required: true, regex: "^[a-zA-Z0-9 <>_&'\\-]+$" }, + description: { required: true, regex: "^[a-zA-Z0-9 <>_&'\\-]+$" }, + objective: { required: true, regex: "^[a-zA-Z0-9 <>_&'\\-]+$" }, + name: { required: true, regex: "^[a-zA-Z0-9 <>_&'\\-]+$" }, keywords: { required: false, regex: '^[a-zA-Z0-9 <>_&-,]$+' }, learning_resources: { required: false, diff --git a/src/database/migrations/20240731090313-add-entity-types-for-tasks-validations.js b/src/database/migrations/20240731090313-add-entity-types-for-tasks-validations.js index 8698c6c8..6e82fd4c 100644 --- a/src/database/migrations/20240731090313-add-entity-types-for-tasks-validations.js +++ b/src/database/migrations/20240731090313-add-entity-types-for-tasks-validations.js @@ -53,7 +53,7 @@ module.exports = { { entityType: 'solution_details', has_entities: false, - validation: { required: false, regex: "^[a-zA-Z0-9 <>_&-' ]+$" }, + validation: { required: false, regex: "^[a-zA-Z0-9 <>_&'\\-]+$" }, model: ['tasks'], }, ] diff --git a/src/database/migrations/20241023173032-update-entity-type-validations.js b/src/database/migrations/20241023173032-update-entity-type-validations.js index 78339067..42179ca6 100644 --- a/src/database/migrations/20241023173032-update-entity-type-validations.js +++ b/src/database/migrations/20241023173032-update-entity-type-validations.js @@ -182,18 +182,18 @@ function transformValidation(validation, entityType) { function getNewMessage(entityType, validationType) { const messages = { title: { - regex: 'Project title can only include alphanumeric characters with spaces, -, _, &, <>', + regex: "Project title can only include alphanumeric characters with spaces, -, _, &, <>, '", required: 'Enter valid project title', max_length: 'Project title must not exceed 256 characters', }, categories: { required: 'Add project category' }, objective: { - regex: 'Objective can only include alphanumeric characters with spaces, -, _, &, <>', + regex: "Objective can only include alphanumeric characters with spaces, -, _, &, <>, '", required: 'Summarize the goal of the project', max_length: 'Objective must not exceed 2000 characters', }, keywords: { - regex: 'Keyword can only include alphanumeric characters with spaces, -, _, &, <>', + regex: "Keyword can only include alphanumeric characters with spaces, -, _, &, <>, '", required: 'Add a tag', max_length: 'Keyword must not exceed 256 characters', }, @@ -204,7 +204,7 @@ function getNewMessage(entityType, validationType) { required: 'Enter link to the resource', }, name: { - regex: 'Description can only include alphanumeric characters with spaces, -, _, &, <>', + regex: "Description can only include alphanumeric characters with spaces, -, _, &, <>, '", required: 'Enter description for task', max_length: 'Description title must not exceed 2000 characters', }, @@ -213,7 +213,7 @@ function getNewMessage(entityType, validationType) { allow_evidences: { required: 'allow_evidences field is required' }, min_no_of_evidences: { required: 'min_no_of_evidences field is required' }, solution_details: { - regex: 'Description can only include alphanumeric characters with spaces, -, _, &, <>', + regex: "Description can only include alphanumeric characters with spaces, -, _, &, <>, '", max_length: 'Name must not exceed 256 characters', }, } diff --git a/src/generics/utils.js b/src/generics/utils.js index c3cb4072..9098fc47 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -419,28 +419,13 @@ const checkRegexPattern = (entityType, entityData) => { } // Proceed if a regex validation object is found if (entityType && entityType.type === common.REGEX_VALIDATION) { - const isTextEnglish = isEnglish(entityData) - // Normalize the entityData - let normalizedValue = typeof entityData === 'number' ? entityData.toString() : entityData - - // If the language is not English, translate - if (!isTextEnglish) { - normalizedValue = tr(entityData) // Assuming translateText function exists - } - - const modifyPattern = (pattern) => { - // If the text is English, remove the apostrophe from the regex pattern - if (isTextEnglish) { - return pattern.replace(/'/g, '') - } - return pattern - } + let normalizedValue = typeof entityData === 'number' ? entityData.toString() : tr(entityData) // Handle array of regex patterns if (Array.isArray(entityType.regex)) { for (let pattern of entityType.regex) { - let regex = new RegExp(modifyPattern(pattern)) + let regex = new RegExp(pattern) if (regex.test(normalizedValue)) { return true } @@ -448,7 +433,7 @@ const checkRegexPattern = (entityType, entityData) => { return false } else { // Handle the case where regex is a single pattern - let regex = new RegExp(modifyPattern(entityType.value)) + let regex = new RegExp(entityType.value) return regex.test(normalizedValue) } } @@ -620,16 +605,6 @@ const isEmpty = (obj) => { return true } -function isEnglish(text) { - // Regex to match only English letters and numbers - var englishRegex = new RegExp('^[\x20-\x7E]*$') - if (englishRegex.test(text)) { - return true - } else { - return false - } -} - /** * Format Name to title case * @name formatToTitleCase @@ -779,7 +754,6 @@ module.exports = { sort, isEmpty, checkLength, - isEnglish, checkEndDate, formatToTitleCase, generateExternalId, From af99e6ff20208172d9517131a83679fec830a1ba Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 30 Dec 2024 17:59:24 +0530 Subject: [PATCH 06/18] consumption side changes --- src/.env.sample | 2 +- src/configs/kafka.js | 10 +- src/envVariables.js | 4 +- src/generics/kafka-communication.js | 20 + src/requests/consumption.js | 635 ++++++++++++++++++++++++++++ src/services/rollouts.js | 10 +- 6 files changed, 670 insertions(+), 11 deletions(-) diff --git a/src/.env.sample b/src/.env.sample index cdd73ccd..be5b02e9 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -187,7 +187,7 @@ DEFAULT_DATA_MANAGERS="program_manager,program_designer" DEFAULT_ROLLOUT_ROLES='rollout_manager' #kafka topic for publishing rollout -ROLLOUT_PUBLISH_KAFKA_TOPIC='dev.rolloutpublishtopic' +ROLLOUT_PUBLISH_KAFKA_TOPIC='dev.rolloutpublish' #API for create the project template in consumption PROJECT_PUBLISH_END_POINT=v1/scp/publishTemplateAndTasks diff --git a/src/configs/kafka.js b/src/configs/kafka.js index 9793945a..ebe9358b 100644 --- a/src/configs/kafka.js +++ b/src/configs/kafka.js @@ -47,10 +47,14 @@ module.exports = async () => { const subscribeToConsumer = async () => { try { await consumer.subscribe({ - topics: [process.env.CLEAR_INTERNAL_CACHE, process.env.PROJECT_PUBLISH_KAFKA_TOPIC], + topics: [ + process.env.CLEAR_INTERNAL_CACHE, + process.env.PROJECT_PUBLISH_KAFKA_TOPIC, + process.env.ROLLOUT_PUBLISH_KAFKA_TOPIC, + ], }) logger.info( - `Subscribed to topics: ${process.env.CLEAR_INTERNAL_CACHE} and ${process.env.PROJECT_PUBLISH_KAFKA_TOPIC}` + `Subscribed to topics: ${process.env.CLEAR_INTERNAL_CACHE} , ${process.env.PROJECT_PUBLISH_KAFKA_TOPIC} and ${process.env.ROLLOUT_PUBLISH_KAFKA_TOPIC}` ) await consumer.run({ eachMessage: async ({ topic, partition, message }) => { @@ -75,6 +79,8 @@ module.exports = async () => { utils.internalDel(streamingData) } else if (topic == process.env.PROJECT_PUBLISH_KAFKA_TOPIC) { await consumptionService.publishProjectTemplates(streamingData) + } else if (topic == process.env.ROLLOUT_PUBLISH_KAFKA_TOPIC) { + await consumptionService.publishProgram(streamingData) } } catch (error) { logger.error('Error processing Kafka message:', { error }) diff --git a/src/envVariables.js b/src/envVariables.js index a0d28b0c..2e361477 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -207,8 +207,8 @@ let environmentVariables = { ROLLOUT_PUBLISH_KAFKA_TOPIC: { message: 'Default Kafka topic for rollout publish required', optional: true, - default: 'dev.rolloutpublishtopic', - }, + default: 'dev.rolloutpublish', + }, CONSUMPTION_SERVICE_BASE_URL: { message: 'Consumption service base name required', optional: true, diff --git a/src/generics/kafka-communication.js b/src/generics/kafka-communication.js index b45abe49..0fd1c306 100644 --- a/src/generics/kafka-communication.js +++ b/src/generics/kafka-communication.js @@ -60,8 +60,28 @@ const pushResourceToKafka = async (message, resourceType) => { throw error } } +const pushRolloutToKafka = async (message, resourceType) => { + try { + let topic = process.env.ROLLOUT_PUBLISH_KAFKA_TOPIC + + if (!topic) { + console.log('Publishing rollout topic not fount.') + return + } + + const payload = { + topic: topic, + messages: [{ value: JSON.stringify(message) }], + } + + return await pushPayloadToKafka(payload) + } catch (error) { + throw error + } +} module.exports = { clearInternalCache, pushResourceToKafka, + pushRolloutToKafka, } diff --git a/src/requests/consumption.js b/src/requests/consumption.js index b8ab67a5..5ce60933 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -6,10 +6,12 @@ */ const common = require('@constants/common') const resourceService = require('@services/resource') +const rolloutService = require('@services/rollouts') const utils = require('@generics/utils') const interfaceBaseUrl = process.env.INTERFACE_SERVICE_HOST const requests = require('@generics/requests') const endpoints = require('@constants/endpoints') +const { ObjectId } = require('mongodb') const MongoClient = require('mongodb').MongoClient let mongoDb @@ -40,6 +42,9 @@ const COLLECTIONS = { TEMPLATES: 'projectTemplates', TASKS: 'projectTemplateTasks', USER_ROLES: 'userRoles', + PROGRAMS: 'programs', + SOLUTIONS: 'solutions', + CERTIFICATE_TEMPLATE: 'certificateTemplates', } /** @@ -377,6 +382,7 @@ const publishProject = function (templateData) { } }) } + /** * Converts the recommended roles for projects based on the consumption service type. * @name convertRecommendedRolesForProjects @@ -412,7 +418,636 @@ async function convertRecommendedRolesForProjects(recommendedFor) { return { success: false, error: `Failed to process recommeded for: ${error.message}` } } } + +const createSolutions = async (resourceDetails, programDetails) => { + try { + let solutionsToCreate = [] + let solutionCertificateMap = [] + let solutionRolloutMap = {} + resourceDetails.forEach((resource) => { + const solutionTemplate = { + resourceType: [common.SOLUTIONS_RESOURCE_TYPE[resource.type]], + language: resource?.languages ? resource?.languages.map((language) => language.label) : [], + keywords: resource?.keywords + ? Array.isArray(resource?.keywords) + ? resource?.keywords + : resource?.keywords.split(',') + : [], + concepts: resource?.concepts ? resource?.concepts : [], + themes: resource?.themes ? resource?.themes : [], + flattenedThemes: resource?.flattenedThemes ? resource?.flattenedThemes : [], + entities: resource?.entities ? resource?.entities : [], + registry: resource?.registry ? resource?.registry : [], + isRubricDriven: resource?.isRubricDriven ? true : false, + enableQuestionReadOut: resource?.enableQuestionReadOut ? true : false, + captureGpsLocationAtQuestionLevel: resource?.captureGpsLocationAtQuestionLevel ? true : false, + isAPrivateProgram: false, + allowMultipleAssessemts: resource?.allowMultipleAssessemts ? true : false, + isDeleted: false, + pageHeading: 'Domains', + minNoOfSubmissionsRequired: resource?.minNoOfSubmissionsRequired + ? resource?.minNoOfSubmissionsRequired + : 1, + rootOrganisations: resource?.organization + ? resource?.organization.map((organization) => organization.id) + : [], + createdFor: resource?.organization ? resource?.organization.map((organization) => organization.id) : [], + deleted: false, + name: resource?.title, + programExternalId: programDetails.externalId, + entityType: resource?.entityType ? resource?.entityType : null, + type: common.SOLUTIONS_TYPE[resource.type] ? common.SOLUTIONS_TYPE[resource.type] : null, + subType: common.SOLUTIONS_TYPE[resource.type] ? common.SOLUTIONS_TYPE[resource.type] : null, + isReusable: false, + externalId: utils.generateUniqueId(), + programId: programDetails._id, + programName: programDetails.name, + programDescription: programDetails.description, + status: common.STATUS_ACTIVE.toLowerCase(), + updatedAt: new Date(), + createdAt: new Date(), + scope: programDetails.scope, + projectTemplateId: resource._id, + updatedBy: 1, + endDate: programDetails.end_date, + startDate: programDetails.start_date, + } + + solutionRolloutMap[solutionTemplate.externalId] = resource.rolloutId + + // map resource externalId and certificate Data if it has certificate data + if ( + resource?.certificate && + typeof resource?.certificate === 'object' && + Object.keys(resource?.certificate).length != 0 + ) { + solutionCertificateMap.push({ + externalId: solutionTemplate.externalId, + certificate: resource?.certificate, + }) + } + solutionsToCreate.push(solutionTemplate) + }) + + const solutionCollection = mongoDb.collection(COLLECTIONS.SOLUTIONS) + await solutionCollection.insertMany(solutionsToCreate) + + const createdSolutions = await solutionCollection + .find({ + programId: programDetails._id, + }) + .toArray() + + if (solutionCertificateMap && solutionCertificateMap.length > 0) { + solutionCertificateMap.forEach((solutionMap) => { + const found = createdSolutions.find((solution) => solution.externalId == solutionMap.externalId) + insertCertificateTemplate(found.certificate, found._id, programDetails._id) + }) + } + + return createdSolutions.map((solution) => { + return { + ...solution, + rolloutId: solutionRolloutMap[solution.externalId], + } + }) + } catch (error) { + console.log(error) + } +} + +const duplicateResources = async (resourceDetails) => { + let projectTemplateIds = [] + let solutionTemplateIds = [] + + if (resourceDetails.type == common.PROJECT) projectTemplateIds.push(ObjectId(resourceDetails._id)) + else solutionTemplateIds.push(ObjectId(resourceDetails._id)) + + // handling only project creation now. Make changes here for observation , survey etc... + if (projectTemplateIds.length > 0) { + const projectsCollection = mongoDb.collection(COLLECTIONS.TEMPLATES) + const projectTemplates = await projectsCollection + .find({ + _id: { + $in: projectTemplateIds, + }, + }) + .toArray() + + //templateProjectsTaskMap = { + // projectExternalId : [ list of last ids] + // } + let templateProjectsTaskMap = {} + let templateProjectsIdMap = {} + let templateProjects = [] + let templateTaskIds = [] + let duplicateTasks = [] + + //taskMap = { + // projectTaskId : duplicateProjectTaskId + // } + let taskMap = {} + + if (projectTemplates) { + projectTemplates.forEach((project) => { + project.externalId = project.externalId + Date.now() + common.SUFFIX_CHILD + delete project._id + project.updatedAt = new Date() + project.createdAt = new Date() + project.isReusable = false + templateProjectsTaskMap[project.externalId] = project.tasks + templateProjectsIdMap[project.externalId] = { + resource_id: resourceDetails.resource_id, + rollout_id: resourceDetails.rolloutId, + } + templateProjects.push(project) + }) + + Object.keys(templateProjectsTaskMap).forEach(async (projectExtId) => { + templateTaskIds = [...templateTaskIds, ...templateProjectsTaskMap[projectExtId]] + }) + templateTaskIds = [...new Set(templateTaskIds)] + const projectsTaskCollection = mongoDb.collection(COLLECTIONS.TASKS) + const projectsTasksDetails = await projectsTaskCollection + .find({ + _id: { + $in: templateTaskIds, + }, + }) + .toArray() + + projectsTasksDetails.forEach((projectTask) => { + projectTask.externalId = utils.generateUniqueId() + taskMap[projectTask._id] = projectTask.externalId + projectTask.updatedAt = new Date() + projectTask.createdAt = new Date() + delete projectTask._id + duplicateTasks.push(projectTask) + }) + + await projectsTaskCollection.insertMany(duplicateTasks) + + const projectsTasksDetailsAfterInsert = await projectsTaskCollection + .find({ + externalId: { + $in: duplicateTasks.map((tasks) => tasks.externalId), + }, + }) + .toArray() + + taskMap = _.mapValues(taskMap, (externalId) => { + // Find the corresponding object from projectsTasksDetailsAfterInsert + const task = _.find(projectsTasksDetailsAfterInsert, { externalId: externalId }) + + // If found, replace externalId with _id; otherwise, keep the externalId + return task ? ObjectId(task._id) : externalId + }) + + templateProjects.forEach((project) => { + let projectTasks = [] + project.tasks.forEach((task) => { + projectTasks.push(taskMap[task]) + }) + project.tasks = projectTasks + }) + + await projectsCollection.insertMany(templateProjects) + } + + const projectTemplatesAfterInsert = await projectsCollection + .find({ + externalId: { + $in: templateProjects.map((projects) => projects.externalId), + }, + }) + .toArray() + + // Add a new 'type' key to each project + const updatedProjectTemplates = projectTemplatesAfterInsert.map((project) => ({ + ...project, // Spread the existing project fields + type: common.PROJECT, + resource_id: templateProjectsIdMap[project.externalId].resource_id, + rolloutId: templateProjectsIdMap[project.externalId].rollout_id, + })) + + return [...updatedProjectTemplates] + } +} + +/** + * Format Program Template + * @name formatProgramTemplate + * @param {Object} templateData - Program template data + * @returns {Object} - Response contains formatted template + */ +const formatProgramTemplate = (templateData) => { + try { + let language = templateData?.language + ? templateData?.resource.flatMap((resource) => { + return resource.languages.map((language) => { + return language.label + }) + }) + : [] + + language = [...new Set(language)] + let keywords = templateData?.keywords + ? templateData?.keywords + : templateData?.resource + ? templateData?.resource?.keywords?.split(',').map((keyword) => keyword.trim()) + : [] + keywords = [...new Set(keywords)] + + let resourceDetails = templateData?.resource + if (templateData?.resource?.published_id) resourceDetails._id = templateData?.resource?.published_id + + let scope = { + roles: [], + entityType: [], + } + let metaInformation = { + recommendedFor: [], + } + if (templateData?.targeting_criteria) { + templateData?.targeting_criteria.forEach((targeting) => { + const targeting_entity = targeting?.entity_targeting?.value + + if (!scope.entityType.includes(targeting_entity)) scope.entityType.push(targeting_entity) + if (targeting?.roles) { + targeting.roles.forEach((role) => { + if (!scope.roles.includes(role.value)) scope.roles.push(role.value) + if (!metaInformation.recommendedFor.includes(role.label)) + metaInformation.recommendedFor.push(role.label) + }) + } else { + scope.roles = [] + metaInformation.recommendedFor = [] + } + targeting[targeting_entity].forEach((target) => { + if (scope[targeting_entity] == undefined) scope[targeting_entity] = [] + if (metaInformation[targeting_entity] == undefined) metaInformation[targeting_entity] = [] + metaInformation[targeting_entity].push(target.name) + scope[targeting_entity].push(target._id) + }) + }) + } + + let template = { + scope, + metaInformation, + resourceType: [common.ROLLOUT_TYPE_PROGRAM], + language, + keywords, + concepts: templateData?.concepts ? templateData?.concepts : [], + components: [], + resourceDetails, + isAPrivateProgram: false, + isDeleted: false, + requestForPIIConsent: templateData?.requestForPIIConsent ? true : false, + rootOrganisations: [ + templateData?.rootOrganisations ? templateData?.rootOrganisations : templateData?.organization?.id, + ], + createdFor: [templateData?.createdFor ? templateData?.createdFor : templateData?.organization?.id], + deleted: false, + status: common.STATUS_ACTIVE.toLowerCase(), + owner: templateData?.created_by, + createdBy: templateData?.created_by, + updatedBy: templateData?.created_by, + externalId: utils.generateExternalId(templateData?.title), + name: templateData?.title.trim(), + description: templateData?.description ? templateData?.description : '', + updatedAt: new Date(), + createdAt: new Date(), + endDate: new Date(templateData?.end_date), + startDate: new Date(templateData?.start_date), + } + if (templateData.published_id) template._id = ObjectId(templateData.published_id) + return { success: true, template } + } catch (error) { + console.error('Error in formatTemplate:', error.message) + return { success: false, error: error.message } + } +} + +// function to replace special charecters +const escapeXml = (unsafe) => { + return unsafe + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} +/** + * create svg template by editing base template. + * @method + * @name createSvg + * @param {Object} certificateData - Certificate data for upload + */ + +async function createSvg(certificateData) { + return new Promise(async (resolve, reject) => { + try { + // fetch base template from cloud + let baseTemplate = await getBaseTemplate(certificateData?.base_template_url) + if (!baseTemplate.success) { + throw new Error('Base template download failed.') + } + + // Load SVG template using Cheerio with XML mode + const $ = cheerio.load(baseTemplate.result, { xmlMode: true }) + + // set issuer name + const issuerNameTag = 'stateTitle' + const issuerNameElement = $(`#${issuerNameTag}`) + issuerNameElement.text(escapeXml(certificateData.issuer)) + + // update signature + for (let index = 1; index <= certificateData.signature.no_of_signature; index++) { + const signatureNameTag = `signatureTitleName${index}` + const signatureDesignationTag = `signatureTitleDesignation${index}` + const signatureImgTag = `signatureImg${index}` + const imageData = await downloadAndConvertToBase64(certificateData.signature[signatureImgTag]) + const signatureNameElement = $(`#${signatureNameTag}`) + const signatureDesignationElement = $(`#${signatureDesignationTag}`) + const signatureImgElement = $(`#${signatureImgTag}`) + signatureImgElement.attr('xlink:href', escapeXml(imageData)) + signatureNameElement.text(escapeXml(certificateData.signature[signatureImgTag])) + signatureDesignationElement.text(escapeXml(certificateData.signature[signatureDesignationTag])) + } + + // update logos + for (let index = 1; index <= certificateData.logos.no_of_logos; index++) { + const logoTag = `stateLogo${index}` + const imageData = await downloadAndConvertToBase64(certificateData.logos[logoTag]) + const logoElement = $(`#${logoTag}`) + logoElement.attr('xlink:href', escapeXml(imageData)) + } + + // updated svg + let updatedSvg = $.xml() + + const uniqueId = utils.generateUniqueId() //generate a unique id for folder + let fileName = `./certificate_template_${uniqueId}.svg` //create a unique file name + const mainPath = path.join(__dirname, `../temp/certificate/`) //temporary folder path for certificate template + let dirPath = path.join(mainPath, `${uniqueId}/`) //create a directory path + fs.mkdirSync(dirPath, { recursive: true }) //create directory + fs.writeFileSync(path.join(dirPath, fileName), updatedSvg, { encoding: 'utf8' }) //create file + // create a file upload payload + let payloadData = { + cert: { + files: [fileName], + }, + ref: common.CERTIFICATE, + } + // generate signed url + const getSignedUrl = await filesService.getSignedUrl( + payloadData, + common.CERTIFICATE_TEMPLATE, + 'system', + false + ) + if (!getSignedUrl.result) { + throw new Error('FAILED_TO_GENERATE_SIGNED_URL') + } + + if (!getSignedUrl.result) { + throw new Error('FAILED_TO_GENERATE_SIGNED_URL') + } + + const fileUploadUrl = getSignedUrl.result['cert']['files'][0].url + let uploadedFilePath = getSignedUrl.result['cert']['files'][0].file + const fileData = fs.readFileSync(path.join(dirPath, fileName)) + //upload file + const fileUploadToSingedUrl = await request({ + url: fileUploadUrl, + method: 'put', + headers: { + 'Content-Type': 'application/multipart/form-data', + }, + body: fileData, + }) + console.log('fileUploadToSingedUrl : ', fileUploadToSingedUrl.status) + // delete folder after upload + await deleteFolderRecursive(path.join(mainPath, uniqueId)) + + resolve({ + message: 'Template edited successfully', + filePath: uploadedFilePath, + }) + } catch (error) { + reject(error) + } + }) +} + +async function insertCertificateTemplate(certificateData, solutionId, programId) { + const filePath = await createSvg(certificateData) + const certificateDocument = { + status: common.STATUS_ACTIVE.toLowerCase(), + deleted: false, + solutionId, + programId, + createdAt: new Date(), + updatedAt: new Date(), + templateUrl: filePath, + issuer: { name: certificateData.issuer }, + criteria: certificateData.criteria, + } + + // Insert the template into the database + const certificateTemplateCollection = mongoDb.collection(COLLECTIONS.CERTIFICATE_TEMPLATE) + const result = await certificateTemplateCollection.insertOne(certificateDocument) + + // Validate the result of the template creation + if (!result || !result.insertedId) { + throw new Error(`Failed to insert the template into the ${COLLECTIONS.CERTIFICATE_TEMPLATE} collection.`) + } + // Insert the template into the database + const solutionTemplateCollection = mongoDb.collection(COLLECTIONS.SOLUTIONS) + const resultUpdateSolution = await solutionTemplateCollection.updateOne( + ({ _id: solutionId }, + { + $set: { + certificateTemplateId: result.insertedId, + }, + }) + ) + // Validate the result of the template creation + if (!resultUpdateSolution) { + throw new Error(`Failed to update the template into the ${COLLECTIONS.SOLUTIONS} collection.`) + } + + return true +} +// function to recursively delete folder after upload +async function deleteFolderRecursive(folderPath) { + // Check if the folder exists + if (fs.existsSync(folderPath)) { + // Get all files and subdirectories in the folder + fs.readdirSync(folderPath).forEach((file) => { + const currentPath = path.join(folderPath, file) + + // If the item is a directory, recursively delete its contents + if (fs.lstatSync(currentPath).isDirectory()) { + deleteFolderRecursive(currentPath) + } else { + // Otherwise, delete the file + fs.unlinkSync(currentPath) + } + }) + // Delete the empty folder + fs.rmdirSync(folderPath) + console.log(`Folder and its contents deleted: ${folderPath}`) + } else { + console.log('Folder does not exist:', folderPath) + } +} + +// Function to fetch data information from cloud using downloadable Url +async function getBaseTemplate(templateUrl) {} + +// download file from cloud and convert it into base64 +async function downloadAndConvertToBase64(url) { + try { + // Download the image file as a binary buffer + const response = await axios({ + url, + method: 'GET', + responseType: 'arraybuffer', // Ensures we receive raw binary data + timeout: 40000, + headers: { + Connection: 'close', // Ensures the socket is closed after the request + }, + }) + + // Convert the binary data to a Base64 string + const base64 = Buffer.from(response.data, 'binary').toString('base64') + + // Get the content type (e.g., image/jpeg) from the response headers + const contentType = response.headers['content-type'] + + // Create the Base64 Data URL + const base64DataUrl = `data:${contentType};base64,${base64}` + + return base64DataUrl + } catch (error) { + console.error('Error downloading or converting file:', error.message) + throw error + } +} + +/** + * Publish the Program + * @name publishProjectTemplates + * @param {Object} programData - Program template data + * @returns {Object} - Response of Program creation + */ +const publishProgram = function (programData) { + return new Promise(async (resolve, reject) => { + const result = { success: false, templateId: null, error: null } + try { + // Format the program template + let formattedTemplate = formatProgramTemplate(programData) + if (!formattedTemplate.success) { + throw new Error('FAILED_TO_FORMAT_TEMPLATE') + } + + let template = formattedTemplate.template + // fetch the resource details to create + const resourceDetailsCreate = template.resourceDetails + const programScope = template.scope + delete template.resourceDetails + let result = {} + let programId = template?._id + + // Insert the template into the database + const programsCollection = mongoDb.collection(COLLECTIONS.PROGRAMS) + // if program is already created , update scope , start and end dates else create a new program + if (programId) { + const updateTemplate = { + scope: formattedTemplate.template.scope, + endDate: formattedTemplate.template.endDate, + startDate: formattedTemplate.template.startDate, + } + + result = await programsCollection.updateOne( + { _id: template?._id }, + { + $set: updateTemplate, + } + ) + + programId = template?._id + } else { + result = await programsCollection.insertOne(template) + // Validate the result of the template creation + if (!result || !result.insertedId) { + throw new Error('Failed to insert the template into the database.') + } + programId = result.insertedId + } + let solutions = [] + if (resourceDetailsCreate?.published_id) { + const updateTemplate = { + scope: formattedTemplate.template.scope, + endDate: formattedTemplate.template.endDate, + startDate: formattedTemplate.template.startDate, + } + const solutionsCollection = mongoDb.collection(COLLECTIONS.SOLUTIONS) + result = await solutionsCollection.updateOne( + { _id: resourceDetailsCreate?.published_id }, + { + $set: updateTemplate, + } + ) + + solutions.push(resourceDetailsCreate?.published_id) + } else { + const duplicateResource = await duplicateResources(resourceDetailsCreate) + solutions = await createSolutions(duplicateResource, { + _id: programId, + scope: programScope, + externalId: template.externalId, + name: template.name, + description: template.description ? template.description : '', + end_date: template.endDate, + start_date: template.startDate, + }) + const solutionIds = solutions.map((solution) => solution._id) + // Update Template with tasks and sequence + await programsCollection.updateOne( + { _id: programId }, + { + $set: { + components: solutionIds, + }, + } + ) + } + + await rolloutService.publishCallback(programData.id, programId.toString()) + solutions.forEach(async (solution) => { + await rolloutService.publishCallback( + solution.rolloutId, + solution._id.toString(), + solution.projectTemplateId.toString() + ) + }) + + //return result + result.success = true + result.programId = programId + + return resolve(result) + } catch (error) { + result.error = `Error: ${error.message}` + return reject(error) + } + }) +} module.exports = { publishProjectTemplates, publishProject, + publishProgram, } diff --git a/src/services/rollouts.js b/src/services/rollouts.js index 40922389..09bdeac4 100644 --- a/src/services/rollouts.js +++ b/src/services/rollouts.js @@ -600,18 +600,19 @@ module.exports = class RolloutsHelper { const resourceDetails = await resourceService.getDetails(rolloutDetailsResult?.resource_id, orgId) let resourceDetailsResult = resourceDetails?.result + resourceDetailsResult.resource_id = resourceDetailsResult?.id // check if resource is present or not if (resourceDetails?.statusCode != httpStatusCode.ok) return resourceDetails let solutionRollout = await rolloutQueries.findOne({ - resource_id: resourceDetailsResult?.resource_id, + resource_id: resourceDetailsResult?.id, type: common.ROLLOUT_TYPE_SOLUTION, parent_id: rolloutId, organization_id: orgId, }) - if (!solutionRollout && resourceDetailsResult?.resource_type != common.ROLLOUT_TYPE_PROGRAM) { + if (!solutionRollout && resourceDetailsResult?.type != common.ROLLOUT_TYPE_PROGRAM) { resourceDetailsResult.parent_id = rolloutId const resultCreateRollout = await this.create(resourceDetailsResult, loggedInUserId, orgId, true) solutionRolloutId = resultCreateRollout?.result?.id @@ -635,14 +636,11 @@ module.exports = class RolloutsHelper { } if (process.env.CONSUMPTION_SERVICE != common.SELF) { - // seperating it in another pr - // await kafkaCommunication.pushRolloutToKafka(rolloutKafkaPayload, common.ROLL_OUT) + await kafkaCommunication.pushRolloutToKafka(rolloutKafkaPayload, common.ROLL_OUT) } else { // implement API based publish } - await this.publishCallback(rolloutId, '', '') - return responses.successResponse({ statusCode: httpStatusCode.accepted, message: 'ROLLOUT_PUBLISHED', From 6130b42dd7dd8c27f150e10284fa98b6a6829f8e Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 30 Dec 2024 18:31:37 +0530 Subject: [PATCH 07/18] consumption side changes --- src/requests/consumption.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index d4b10322..b3d73b1a 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -963,7 +963,8 @@ const publishProgram = function (programData) { let template = formattedTemplate.template // fetch the resource details to create - const resourceDetailsCreate = template.resourceDetails + const resourceDetailsCreateResponse = await rolloutService.details(template?.resourceDetails?.rolloutId) + const resourceDetailsCreate = resourceDetailsCreateResponse.result const programScope = template.scope delete template.resourceDetails let result = {} @@ -996,7 +997,8 @@ const publishProgram = function (programData) { programId = result.insertedId } let solutions = [] - if (resourceDetailsCreate?.published_id) { + + if (resourceDetailsCreate?.status == common.ROLLOUT_STATUS_PENDING && resourceDetailsCreate?.published_id) { const updateTemplate = { scope: formattedTemplate.template.scope, endDate: formattedTemplate.template.endDate, From 13870955badbbb1a4f48d2d89f9b6e6ad9340fcb Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 30 Dec 2024 19:39:36 +0530 Subject: [PATCH 08/18] consumption side changes --- src/requests/consumption.js | 15 ++++++++++++--- src/services/rollouts.js | 7 +++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index b3d73b1a..9d3d2f3a 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -7,6 +7,7 @@ const common = require('@constants/common') const resourceService = require('@services/resource') const rolloutService = require('@services/rollouts') +const rolloutQueries = require('@database/queries/rollouts') const utils = require('@generics/utils') const interfaceBaseUrl = process.env.INTERFACE_SERVICE_HOST const requests = require('@generics/requests') @@ -963,8 +964,13 @@ const publishProgram = function (programData) { let template = formattedTemplate.template // fetch the resource details to create - const resourceDetailsCreateResponse = await rolloutService.details(template?.resourceDetails?.rolloutId) - const resourceDetailsCreate = resourceDetailsCreateResponse.result + const resourceDetailsCreate = template.resourceDetails + const resourceStatus = await rolloutQueries.findOne( + { + id: template?.resourceDetails?.rolloutId, + }, + { attributes: ['status', 'published_id'] } + ) const programScope = template.scope delete template.resourceDetails let result = {} @@ -998,7 +1004,10 @@ const publishProgram = function (programData) { } let solutions = [] - if (resourceDetailsCreate?.status == common.ROLLOUT_STATUS_PENDING && resourceDetailsCreate?.published_id) { + if ( + resourceStatus?.status == common.ROLLOUT_STATUS_ROLLED_OUT && + resourceStatus?.published_id != undefined + ) { const updateTemplate = { scope: formattedTemplate.template.scope, endDate: formattedTemplate.template.endDate, diff --git a/src/services/rollouts.js b/src/services/rollouts.js index 09bdeac4..2da0ea64 100644 --- a/src/services/rollouts.js +++ b/src/services/rollouts.js @@ -614,10 +614,17 @@ module.exports = class RolloutsHelper { if (!solutionRollout && resourceDetailsResult?.type != common.ROLLOUT_TYPE_PROGRAM) { resourceDetailsResult.parent_id = rolloutId + // add the start date and end date of program for single roll out + resourceDetailsResult.start_date = rolloutDetailsResult.start_date + resourceDetailsResult.end_date = rolloutDetailsResult.end_date const resultCreateRollout = await this.create(resourceDetailsResult, loggedInUserId, orgId, true) solutionRolloutId = resultCreateRollout?.result?.id } else { + // update the start date and end date of program for single roll out solutionRolloutId = solutionRollout.id + bodyData.start_date = rolloutDetailsResult.start_date + bodyData.end_date = rolloutDetailsResult.end_date + await this.update(solutionRolloutId, bodyData, loggedInUserId, orgId) } // publish the resource if not published From 722d5e01fd816c63b912b7fa13929b868c4c4119 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Mon, 30 Dec 2024 23:01:05 +0530 Subject: [PATCH 09/18] consumption side changes --- src/envVariables.js | 5 ++ src/generics/utils.js | 15 ++++ src/requests/consumption.js | 146 ++++++++++++++++++++++++++---------- src/services/rollouts.js | 2 + 4 files changed, 129 insertions(+), 39 deletions(-) diff --git a/src/envVariables.js b/src/envVariables.js index 2e361477..92d26818 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -208,6 +208,11 @@ let environmentVariables = { message: 'Default Kafka topic for rollout publish required', optional: true, default: 'dev.rolloutpublish', + requiredIf: { + key: 'CONSUMPTION_SERVICE_BASE_URL', + operator: 'NOT_EQUALS', + value: 'self', + }, }, CONSUMPTION_SERVICE_BASE_URL: { message: 'Consumption service base name required', diff --git a/src/generics/utils.js b/src/generics/utils.js index 9098fc47..9c6d21d4 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -718,6 +718,21 @@ function convertDuration(durationObj) { } } +/** + * Converts charachters which can cause issues in xml file to accepted values + * name escapeXml + * @param {String} inputElement - The input duration object. + * @returns {String} - Converted xml with accepted charecters. + */ +const escapeXml = (inputElement) => { + return inputElement + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + module.exports = { composeEmailBody, internalSet, diff --git a/src/requests/consumption.js b/src/requests/consumption.js index 9d3d2f3a..d8038ca4 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -428,20 +428,27 @@ async function convertRecommendedRolesForProjects(recommendedFor) { } } +/** + * Create solutions for resources + * @name createSolutions + * @param {Object} resourceDetails - Object of resource details + * @param {Object} programDetails - Object of program details + * @returns {Array} Array of objects of solutions + */ const createSolutions = async (resourceDetails, programDetails) => { try { + // array to have objects of solutions to create let solutionsToCreate = [] + // solution to certificate mapping let solutionCertificateMap = [] + // solution to rollout if map let solutionRolloutMap = {} resourceDetails.forEach((resource) => { + // create solutions template const solutionTemplate = { resourceType: [common.SOLUTIONS_RESOURCE_TYPE[resource.type]], language: resource?.languages ? resource?.languages.map((language) => language.label) : [], - keywords: resource?.keywords - ? Array.isArray(resource?.keywords) - ? resource?.keywords - : resource?.keywords.split(',') - : [], + keywords: resource?.keywords ? utils.formatKeywords(resource?.keywords) : [], concepts: resource?.concepts ? resource?.concepts : [], themes: resource?.themes ? resource?.themes : [], flattenedThemes: resource?.flattenedThemes ? resource?.flattenedThemes : [], @@ -477,7 +484,8 @@ const createSolutions = async (resourceDetails, programDetails) => { createdAt: new Date(), scope: programDetails.scope, projectTemplateId: resource._id, - updatedBy: 1, + updatedBy: programDetails.loggedInUserId, + author: programDetails.loggedInUserId, endDate: programDetails.end_date, startDate: programDetails.start_date, } @@ -487,7 +495,7 @@ const createSolutions = async (resourceDetails, programDetails) => { // map resource externalId and certificate Data if it has certificate data if ( resource?.certificate && - typeof resource?.certificate === 'object' && + typeof resource?.certificate === common.OBJECT && Object.keys(resource?.certificate).length != 0 ) { solutionCertificateMap.push({ @@ -525,10 +533,19 @@ const createSolutions = async (resourceDetails, programDetails) => { } } +/** + * Create a duplicate solution from the given resource details + * @name duplicateResources + * @param {Object} resourceDetails - Object of resource details + * @returns {Array} Array of objects of duplicate templates + */ const duplicateResources = async (resourceDetails) => { + // initialise list of project templates to create let projectTemplateIds = [] + //initialise list of solution templates to create let solutionTemplateIds = [] + // seggregate templates based on type , all projects should be created in projectTemplates and others in solutions collection if (resourceDetails.type == common.PROJECT) projectTemplateIds.push(ObjectId(resourceDetails._id)) else solutionTemplateIds.push(ObjectId(resourceDetails._id)) @@ -547,9 +564,16 @@ const duplicateResources = async (resourceDetails) => { // projectExternalId : [ list of last ids] // } let templateProjectsTaskMap = {} + //templateProjectsIdMap = { + // resource_id: resource id in the resource table, + // rollout_id: rollout id in the rollout table, + // } let templateProjectsIdMap = {} + // array of project templates to create let templateProjects = [] + // array of template tasks to create let templateTaskIds = [] + // array of created template tasks let duplicateTasks = [] //taskMap = { @@ -557,7 +581,8 @@ const duplicateResources = async (resourceDetails) => { // } let taskMap = {} - if (projectTemplates) { + if (projectTemplates.length > 0) { + // create project duplicate template to create projectTemplates.forEach((project) => { project.externalId = project.externalId + Date.now() + common.SUFFIX_CHILD delete project._id @@ -571,7 +596,7 @@ const duplicateResources = async (resourceDetails) => { } templateProjects.push(project) }) - + // array of tasks to create Object.keys(templateProjectsTaskMap).forEach(async (projectExtId) => { templateTaskIds = [...templateTaskIds, ...templateProjectsTaskMap[projectExtId]] }) @@ -584,7 +609,7 @@ const duplicateResources = async (resourceDetails) => { }, }) .toArray() - + // duplicate project task details to create projectsTasksDetails.forEach((projectTask) => { projectTask.externalId = utils.generateUniqueId() taskMap[projectTask._id] = projectTask.externalId @@ -611,15 +636,19 @@ const duplicateResources = async (resourceDetails) => { // If found, replace externalId with _id; otherwise, keep the externalId return task ? ObjectId(task._id) : externalId }) - + // update the project template after tasks created templateProjects.forEach((project) => { let projectTasks = [] + let taskSequence = [] project.tasks.forEach((task) => { projectTasks.push(taskMap[task]) + let seqNum = task.sequenceNumber - 1 < 0 ? 0 : task.sequenceNumber - 1 + taskSequence[seqNum] = task.externalId }) project.tasks = projectTasks + project.taskSequence = taskSequence }) - + // create project templates await projectsCollection.insertMany(templateProjects) } @@ -631,7 +660,7 @@ const duplicateResources = async (resourceDetails) => { }) .toArray() - // Add a new 'type' key to each project + // Add a new 'type', 'resource_id' , 'rolloutId' keys to each project const updatedProjectTemplates = projectTemplatesAfterInsert.map((project) => ({ ...project, // Spread the existing project fields type: common.PROJECT, @@ -663,7 +692,7 @@ const formatProgramTemplate = (templateData) => { let keywords = templateData?.keywords ? templateData?.keywords : templateData?.resource - ? templateData?.resource?.keywords?.split(',').map((keyword) => keyword.trim()) + ? utils.formatKeywords(templateData?.resource?.keywords) : [] keywords = [...new Set(keywords)] @@ -677,14 +706,20 @@ const formatProgramTemplate = (templateData) => { let metaInformation = { recommendedFor: [], } + // check if targeting criteria is given in the template or not if (templateData?.targeting_criteria) { + // iterate through the targeting_criteria given templateData?.targeting_criteria.forEach((targeting) => { + // fetch the targeting entity const targeting_entity = targeting?.entity_targeting?.value - + // check if entity type in scope is added or not. if not added , add if (!scope.entityType.includes(targeting_entity)) scope.entityType.push(targeting_entity) + // check of roles if (targeting?.roles) { + // iterate through the roles object and add the value of role if its not added targeting.roles.forEach((role) => { if (!scope.roles.includes(role.value)) scope.roles.push(role.value) + // iterate through the roles object and add the value of role in metaInformation.recommendedFor if its not added if (!metaInformation.recommendedFor.includes(role.label)) metaInformation.recommendedFor.push(role.label) }) @@ -693,6 +728,7 @@ const formatProgramTemplate = (templateData) => { metaInformation.recommendedFor = [] } targeting[targeting_entity].forEach((target) => { + // add the entity ids in scope and metaInformation if (scope[targeting_entity] == undefined) scope[targeting_entity] = [] if (metaInformation[targeting_entity] == undefined) metaInformation[targeting_entity] = [] metaInformation[targeting_entity].push(target.name) @@ -700,8 +736,8 @@ const formatProgramTemplate = (templateData) => { }) }) } - - let template = { + // prepare the program template + let programTemplate = { scope, metaInformation, resourceType: [common.ROLLOUT_TYPE_PROGRAM], @@ -730,23 +766,15 @@ const formatProgramTemplate = (templateData) => { endDate: new Date(templateData?.end_date), startDate: new Date(templateData?.start_date), } - if (templateData.published_id) template._id = ObjectId(templateData.published_id) - return { success: true, template } + // if the program is already published , update _id from the published_id + if (templateData.published_id) programTemplate._id = ObjectId(templateData.published_id) + return { success: true, programTemplate } } catch (error) { console.error('Error in formatTemplate:', error.message) return { success: false, error: error.message } } } -// function to replace special charecters -const escapeXml = (unsafe) => { - return unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') -} /** * create svg template by editing base template. * @method @@ -769,7 +797,7 @@ async function createSvg(certificateData) { // set issuer name const issuerNameTag = 'stateTitle' const issuerNameElement = $(`#${issuerNameTag}`) - issuerNameElement.text(escapeXml(certificateData.issuer)) + issuerNameElement.text(utils.escapeXml(certificateData.issuer)) // update signature for (let index = 1; index <= certificateData.signature.no_of_signature; index++) { @@ -780,9 +808,9 @@ async function createSvg(certificateData) { const signatureNameElement = $(`#${signatureNameTag}`) const signatureDesignationElement = $(`#${signatureDesignationTag}`) const signatureImgElement = $(`#${signatureImgTag}`) - signatureImgElement.attr('xlink:href', escapeXml(imageData)) - signatureNameElement.text(escapeXml(certificateData.signature[signatureImgTag])) - signatureDesignationElement.text(escapeXml(certificateData.signature[signatureDesignationTag])) + signatureImgElement.attr('xlink:href', utils.escapeXml(imageData)) + signatureNameElement.text(utils.escapeXml(certificateData.signature[signatureImgTag])) + signatureDesignationElement.text(utils.escapeXml(certificateData.signature[signatureDesignationTag])) } // update logos @@ -790,7 +818,7 @@ async function createSvg(certificateData) { const logoTag = `stateLogo${index}` const imageData = await downloadAndConvertToBase64(certificateData.logos[logoTag]) const logoElement = $(`#${logoTag}`) - logoElement.attr('xlink:href', escapeXml(imageData)) + logoElement.attr('xlink:href', utils.escapeXml(imageData)) } // updated svg @@ -850,6 +878,14 @@ async function createSvg(certificateData) { }) } +/** + * Insert certificate templates + * @method + * @name insertCertificateTemplate + * @param {Object} certificateData - Certificate data for upload + * @param {String} solutionId - solutionId of the created solution + * @param {String} programId - programId of the created program + */ async function insertCertificateTemplate(certificateData, solutionId, programId) { const filePath = await createSvg(certificateData) const certificateDocument = { @@ -872,7 +908,7 @@ async function insertCertificateTemplate(certificateData, solutionId, programId) if (!result || !result.insertedId) { throw new Error(`Failed to insert the template into the ${COLLECTIONS.CERTIFICATE_TEMPLATE} collection.`) } - // Insert the template into the database + // update the solution with the certificate template id const solutionTemplateCollection = mongoDb.collection(COLLECTIONS.SOLUTIONS) const resultUpdateSolution = await solutionTemplateCollection.updateOne( ({ _id: solutionId }, @@ -889,7 +925,13 @@ async function insertCertificateTemplate(certificateData, solutionId, programId) return true } -// function to recursively delete folder after upload + +/** + * function to recursively delete folder after upload + * @method + * @name deleteFolderRecursive + * @param {String} folderPath - folder path to delete + */ async function deleteFolderRecursive(folderPath) { // Check if the folder exists if (fs.existsSync(folderPath)) { @@ -913,10 +955,34 @@ async function deleteFolderRecursive(folderPath) { } } -// Function to fetch data information from cloud using downloadable Url -async function getBaseTemplate(templateUrl) {} +/** + * Function to fetch data information from cloud using downloadable Url + * @method + * @name getBaseTemplate + * @param {String} templateUrl - cloud path to download + */ +async function getBaseTemplate(templateUrl) { + try { + const response = await axios.get(templateUrl) + if (response.status === 200) { + return { + success: true, + result: response.data, + } + } else { + throw new Error(`Unexpected response status: ${response.status}`) + } + } catch (error) { + return Promise.reject(new Error(`Failed to fetch base template: ${error.message}`)) + } +} -// download file from cloud and convert it into base64 +/** + * download file from cloud and convert it into base64 + * @method + * @name downloadAndConvertToBase64 + * @param {String} templateUrl - cloud path to download + */ async function downloadAndConvertToBase64(url) { try { // Download the image file as a binary buffer @@ -963,7 +1029,7 @@ const publishProgram = function (programData) { } let template = formattedTemplate.template - // fetch the resource details to create + // fetch the resource details to create solutions const resourceDetailsCreate = template.resourceDetails const resourceStatus = await rolloutQueries.findOne( { @@ -1032,6 +1098,8 @@ const publishProgram = function (programData) { description: template.description ? template.description : '', end_date: template.endDate, start_date: template.startDate, + loggedInUserId: programData.loggedInUserId, + orgId: programData.orgId, }) const solutionIds = solutions.map((solution) => solution._id) // Update Template with tasks and sequence diff --git a/src/services/rollouts.js b/src/services/rollouts.js index 2da0ea64..85ff3c6b 100644 --- a/src/services/rollouts.js +++ b/src/services/rollouts.js @@ -636,6 +636,8 @@ module.exports = class RolloutsHelper { } const rolloutKafkaPayload = { ...rolloutDetails.result, + loggedInUserId, + orgId, resource: { ...resourceDetails?.result, rolloutId: solutionRolloutId, From 1285cdf4c13c7e07701d19b7f54d9bde97b83c44 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 31 Dec 2024 11:21:09 +0530 Subject: [PATCH 10/18] consumption side changes --- src/generics/utils.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/generics/utils.js b/src/generics/utils.js index 9c6d21d4..44ca3709 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -776,4 +776,5 @@ module.exports = { formatKeywords, formatProjectMetaInformation, convertDuration, + escapeXml, } From e5a9176befdc72d5db82d451634c2077230854f9 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Tue, 31 Dec 2024 12:31:21 +0530 Subject: [PATCH 11/18] added logs --- .circleci/config.yml | 40 +++++++++---------- .../integration_test.self_creation_portal.env | 4 +- src/envVariables.js | 8 ++-- src/integration-tests/commonTests.js | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e44e2240..1101f243 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -151,35 +151,35 @@ jobs: - run: name: Get User container ID and print logs command: | - echo "Getting SCP container ID..." + echo "Getting User container ID..." USER_CONTAINER_ID=$(docker ps -q --filter "name=user") - echo "SCP container ID: $USER_CONTAINER_ID" + echo "User container ID: $USER_CONTAINER_ID" - # Check if the SCP container is running + # Check if the User container is running if [ -z "$USER_CONTAINER_ID" ]; then + echo "User container is not running. Cannot fetch logs." + exit 1 # Exit with an error if User container is not running + fi + + # Print logs of the SCP container + echo "Printing SCP container logs..." + docker logs $USER_CONTAINER_ID --tail=20000 + - run: + name: Get SCP container ID and print logs + command: | + echo "Getting SCP container ID..." + SCP_CONTAINER_ID=$(docker ps -q --filter "name=scp") + echo "SCP container ID: $SCP_CONTAINER_ID" + + # Check if the SCP container is running + if [ -z "$SCP_CONTAINER_ID" ]; then echo "SCP container is not running. Cannot fetch logs." exit 1 # Exit with an error if SCP container is not running fi # Print logs of the SCP container echo "Printing SCP container logs..." - docker logs $USER_CONTAINER_ID --tail=10000 - # - run: - # name: Get SCP container ID and print logs - # command: | - # echo "Getting SCP container ID..." - # SCP_CONTAINER_ID=$(docker ps -q --filter "name=scp") - # echo "SCP container ID: $SCP_CONTAINER_ID" - - # # Check if the SCP container is running - # if [ -z "$SCP_CONTAINER_ID" ]; then - # echo "SCP container is not running. Cannot fetch logs." - # exit 1 # Exit with an error if SCP container is not running - # fi - - # # Print logs of the SCP container - # echo "Printing SCP container logs..." - # docker logs $SCP_CONTAINER_ID --tail=10000 + docker logs $SCP_CONTAINER_ID --tail=20000 - store_test_results: path: ./dev-ops/report diff --git a/dev-ops/integration_test.self_creation_portal.env b/dev-ops/integration_test.self_creation_portal.env index 7e1ac6ac..f719beed 100644 --- a/dev-ops/integration_test.self_creation_portal.env +++ b/dev-ops/integration_test.self_creation_portal.env @@ -15,7 +15,7 @@ KAFKA_GROUP_ID=scp NOTIFICATION_KAFKA_TOPIC=dev.notifications PROJECT_PUBLISH_KAFKA_TOPIC=projectpublishtopic KAFKA_COMMUNICATIONS_ON_OFF=OFF -RESOURCE_KAFKA_PUSH_ON_OFF=ON +RESOURCE_KAFKA_PUSH_ON_OFF=OFF # Cloud Storage Configuration CLOUD_STORAGE_PROVIDER=aws @@ -55,7 +55,7 @@ MIN_APPROVAL=1 REVIEW_TYPE=SEQUENTIAL MAX_PROJECT_TASK_COUNT=10 RESOURCE_TYPES="project,observation,observation_with_rubric,survey,program" -CONSUMPTION_SERVICE=elevate-project +CONSUMPTION_SERVICE=self MAX_BODY_LENGTH_FOR_UPLOAD=5242880 RESOURCE_AUTO_SAVE_TIMER=1000 MAX_RESOURCE_NOTE_LENGTH=2000 diff --git a/src/envVariables.js b/src/envVariables.js index a0d28b0c..9c213b40 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -208,14 +208,14 @@ let environmentVariables = { message: 'Default Kafka topic for rollout publish required', optional: true, default: 'dev.rolloutpublishtopic', - }, + }, CONSUMPTION_SERVICE_BASE_URL: { message: 'Consumption service base name required', optional: true, requiredIf: { - key: 'RESOURCE_KAFKA_PUSH_ON_OFF', - operator: 'EQUALS', - value: 'OFF', + key: 'CONSUMPTION_SERVICE', + operator: 'NOT_EQUALS', + value: 'self', }, }, PROJECT_PUBLISH_END_POINT: { diff --git a/src/integration-tests/commonTests.js b/src/integration-tests/commonTests.js index 2d1f33c6..3372d293 100644 --- a/src/integration-tests/commonTests.js +++ b/src/integration-tests/commonTests.js @@ -40,7 +40,7 @@ const verifyUserRole = async () => { // Create a new user let email = 'orgadmin' + crypto.randomBytes(5).toString('hex') + '@shikshalokam.com' - let password = 'Welcome@123' + let password = 'Welco@Me#123!' try { let res = await request.post('/user/v1/account/create').send({ From c00b0d02dcd0f5e7e06d17dc1a28620528b91d61 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Tue, 31 Dec 2024 12:55:07 +0530 Subject: [PATCH 12/18] add artifacts --- .circleci/config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1101f243..923c780a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -148,6 +148,8 @@ jobs: name: Running test cases command: | cd src/ && npm run test:integration || true + - store_artifacts: + path: ./dev-ops/report - run: name: Get User container ID and print logs command: | From ff252c9ddef50d0ab339601416d7854691ce260d Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Tue, 31 Dec 2024 13:13:25 +0530 Subject: [PATCH 13/18] result dir change --- .circleci/config.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 923c780a..522c3868 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -148,8 +148,6 @@ jobs: name: Running test cases command: | cd src/ && npm run test:integration || true - - store_artifacts: - path: ./dev-ops/report - run: name: Get User container ID and print logs command: | @@ -185,6 +183,10 @@ jobs: - store_test_results: path: ./dev-ops/report + # path: test-results + - store_artifacts: + path: ./dev-ops/report + destination: test-results/ - run: name: Stop the docker containers - Unmap volumes command: |- From b8a81b460ba282f62dc25658920f976ae44a885b Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Tue, 31 Dec 2024 15:11:59 +0530 Subject: [PATCH 14/18] rollout --- .../rollouts/responseSchema.js | 39 ++++++++++++++++--- .../rollouts/rollouts.spec.js | 27 +++++++++++-- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/integration-tests/rollouts/responseSchema.js b/src/integration-tests/rollouts/responseSchema.js index 51c0e45a..47bb001d 100644 --- a/src/integration-tests/rollouts/responseSchema.js +++ b/src/integration-tests/rollouts/responseSchema.js @@ -111,7 +111,7 @@ const getDataManagersEmptyResponseSchema = { }, required: ['responseCode', 'message', 'result', 'meta'], } -const getRolloutsListSchema = { +const listSchema = { type: 'object', properties: { responseCode: { @@ -360,7 +360,7 @@ const getRolloutsListSchema = { }, required: ['responseCode', 'message', 'result', 'meta'], } -const getRolloutsListEmptyResponseSchema = { +const listEmptyResponseSchema = { type: 'object', properties: { responseCode: { @@ -398,7 +398,33 @@ const getRolloutsListEmptyResponseSchema = { }, required: ['responseCode', 'message', 'result', 'meta'], } -const rolloutDetailResponseSchema = { +const detailResponseSchema = { + type: 'object', + properties: { + responseCode: { + type: 'string', + }, + error: { + type: 'array', + items: {}, + }, + meta: { + type: 'object', + properties: { + correlation: { + type: 'string', + }, + }, + required: [], + }, + message: { + type: 'string', + }, + }, + required: ['responseCode', 'error', 'meta', 'message'], +} + +const createSchema = { type: 'object', properties: { responseCode: { @@ -426,7 +452,8 @@ const rolloutDetailResponseSchema = { module.exports = { getDataManagersSchema, getDataManagersEmptyResponseSchema, - getRolloutsListSchema, - getRolloutsListEmptyResponseSchema, - rolloutDetailResponseSchema, + listSchema, + listEmptyResponseSchema, + detailResponseSchema, + createSchema, } diff --git a/src/integration-tests/rollouts/rollouts.spec.js b/src/integration-tests/rollouts/rollouts.spec.js index 160054a0..a85ac4eb 100644 --- a/src/integration-tests/rollouts/rollouts.spec.js +++ b/src/integration-tests/rollouts/rollouts.spec.js @@ -26,23 +26,44 @@ describe('Rollout APIs', function () { expect(res.body).toMatchSchema(schema.getDataManagersSchema) }) + it('Create Rollout', async () => { + let res = await request.post('/scp/v1/rollouts/update').send(insertRolloutData()) + expect(res.statusCode).toBe(400) + expect(res.body).toMatchSchema(schema.createSchema) + }) + it('Get list of Rollouts', async () => { let res = await request.get('/scp/v1/rollouts/list').query({ page: 1, limit: 10 }) expect(res.statusCode).toBe(200) if (res.body?.result.length == 0) { - expect(res.body).toMatchSchema(schema.getRolloutsListEmptyResponseSchema) + expect(res.body).toMatchSchema(schema.listEmptyResponseSchema) } - expect(res.body).toMatchSchema(schema.getRolloutsListSchema) + expect(res.body).toMatchSchema(schema.listSchema) }) it('Get Rollout Details', async () => { let res = await request.get('/scp/v1/rollouts/details/1') expect(res.statusCode).toBe(400) - expect(res.body).toMatchSchema(schema.rolloutDetailResponseSchema) + expect(res.body).toMatchSchema(schema.detailResponseSchema) }) it('Delete Rollout', async () => { const res = await request.delete('/scp/v1/rollouts/update/999999') expect(res.statusCode).toBe(400) }) + + it('Publish Rollout', async () => { + let res = await request.get('/scp/v1/rollouts/publish/1').send() + expect(res.statusCode).toBe(400) + expect(res.body).toMatchSchema(schema.createSchema) + }) }) + +function insertRolloutData(resource_id) { + return { + title: faker.random.alpha(5), + resource_id: resource_id, + start_date: '2024-11-29T11:36:31.117Z', + end_date: '2024-12-30T11:36:31.117Z', + } +} From 009f19991e70680f7b28c03ffbdc683d93d5bc10 Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Tue, 31 Dec 2024 15:40:30 +0530 Subject: [PATCH 15/18] commeted logs --- .circleci/config.yml | 66 ++++++++++++++++++++++---------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 522c3868..a420a82f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -147,39 +147,39 @@ jobs: - run: name: Running test cases command: | - cd src/ && npm run test:integration || true - - run: - name: Get User container ID and print logs - command: | - echo "Getting User container ID..." - USER_CONTAINER_ID=$(docker ps -q --filter "name=user") - echo "User container ID: $USER_CONTAINER_ID" - - # Check if the User container is running - if [ -z "$USER_CONTAINER_ID" ]; then - echo "User container is not running. Cannot fetch logs." - exit 1 # Exit with an error if User container is not running - fi - - # Print logs of the SCP container - echo "Printing SCP container logs..." - docker logs $USER_CONTAINER_ID --tail=20000 - - run: - name: Get SCP container ID and print logs - command: | - echo "Getting SCP container ID..." - SCP_CONTAINER_ID=$(docker ps -q --filter "name=scp") - echo "SCP container ID: $SCP_CONTAINER_ID" - - # Check if the SCP container is running - if [ -z "$SCP_CONTAINER_ID" ]; then - echo "SCP container is not running. Cannot fetch logs." - exit 1 # Exit with an error if SCP container is not running - fi - - # Print logs of the SCP container - echo "Printing SCP container logs..." - docker logs $SCP_CONTAINER_ID --tail=20000 + cd src/ && npm run test:integration + # - run: + # name: Get User container ID and print logs + # command: | + # echo "Getting User container ID..." + # USER_CONTAINER_ID=$(docker ps -q --filter "name=user") + # echo "User container ID: $USER_CONTAINER_ID" + + # # Check if the User container is running + # if [ -z "$USER_CONTAINER_ID" ]; then + # echo "User container is not running. Cannot fetch logs." + # exit 1 # Exit with an error if User container is not running + # fi + + # # Print logs of the SCP container + # echo "Printing SCP container logs..." + # docker logs $USER_CONTAINER_ID --tail=20000 + # - run: + # name: Get SCP container ID and print logs + # command: | + # echo "Getting SCP container ID..." + # SCP_CONTAINER_ID=$(docker ps -q --filter "name=scp") + # echo "SCP container ID: $SCP_CONTAINER_ID" + + # # Check if the SCP container is running + # if [ -z "$SCP_CONTAINER_ID" ]; then + # echo "SCP container is not running. Cannot fetch logs." + # exit 1 # Exit with an error if SCP container is not running + # fi + + # # Print logs of the SCP container + # echo "Printing SCP container logs..." + # docker logs $SCP_CONTAINER_ID --tail=20000 - store_test_results: path: ./dev-ops/report From 78f44eff143bcdb3a3a228568133e03b30cb46bb Mon Sep 17 00:00:00 2001 From: priyanka-TL Date: Tue, 31 Dec 2024 15:42:08 +0530 Subject: [PATCH 16/18] remove fix --- src/integration-tests/rollouts/rollouts.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/integration-tests/rollouts/rollouts.spec.js b/src/integration-tests/rollouts/rollouts.spec.js index a85ac4eb..1a90d110 100644 --- a/src/integration-tests/rollouts/rollouts.spec.js +++ b/src/integration-tests/rollouts/rollouts.spec.js @@ -59,10 +59,10 @@ describe('Rollout APIs', function () { }) }) -function insertRolloutData(resource_id) { +function insertRolloutData() { return { title: faker.random.alpha(5), - resource_id: resource_id, + resource_id: 1, start_date: '2024-11-29T11:36:31.117Z', end_date: '2024-12-30T11:36:31.117Z', } From 3c22ddefcf8b70c19874d4ba85f312fd1ef1a00a Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 31 Dec 2024 16:42:40 +0530 Subject: [PATCH 17/18] consumption side changes --- src/requests/consumption.js | 256 ++++++++++++++++++++---------------- src/services/rollouts.js | 30 +++-- 2 files changed, 163 insertions(+), 123 deletions(-) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index d8038ca4..e9e11a89 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -484,8 +484,8 @@ const createSolutions = async (resourceDetails, programDetails) => { createdAt: new Date(), scope: programDetails.scope, projectTemplateId: resource._id, - updatedBy: programDetails.loggedInUserId, - author: programDetails.loggedInUserId, + updatedBy: programDetails.created_by, + author: programDetails.created_by, endDate: programDetails.end_date, startDate: programDetails.start_date, } @@ -537,17 +537,18 @@ const createSolutions = async (resourceDetails, programDetails) => { * Create a duplicate solution from the given resource details * @name duplicateResources * @param {Object} resourceDetails - Object of resource details + * @param {String} created_by - created by user id * @returns {Array} Array of objects of duplicate templates */ -const duplicateResources = async (resourceDetails) => { +const duplicateResources = async (resourceDetails, created_by) => { // initialise list of project templates to create let projectTemplateIds = [] //initialise list of solution templates to create let solutionTemplateIds = [] // seggregate templates based on type , all projects should be created in projectTemplates and others in solutions collection - if (resourceDetails.type == common.PROJECT) projectTemplateIds.push(ObjectId(resourceDetails._id)) - else solutionTemplateIds.push(ObjectId(resourceDetails._id)) + if (resourceDetails.type == common.PROJECT) projectTemplateIds.push(ObjectId(resourceDetails.published_id)) + else solutionTemplateIds.push(ObjectId(resourceDetails.published_id)) // handling only project creation now. Make changes here for observation , survey etc... if (projectTemplateIds.length > 0) { @@ -588,6 +589,8 @@ const duplicateResources = async (resourceDetails) => { delete project._id project.updatedAt = new Date() project.createdAt = new Date() + project.createdBy = created_by + project.updatedBy = created_by project.isReusable = false templateProjectsTaskMap[project.externalId] = project.tasks templateProjectsIdMap[project.externalId] = { @@ -615,6 +618,8 @@ const duplicateResources = async (resourceDetails) => { taskMap[projectTask._id] = projectTask.externalId projectTask.updatedAt = new Date() projectTask.createdAt = new Date() + projectTask.createdBy = created_by + projectTask.updatedBy = created_by delete projectTask._id duplicateTasks.push(projectTask) }) @@ -628,10 +633,12 @@ const duplicateResources = async (resourceDetails) => { }, }) .toArray() - + let seqCounter = 0 + let taskSequence = [] taskMap = _.mapValues(taskMap, (externalId) => { // Find the corresponding object from projectsTasksDetailsAfterInsert const task = _.find(projectsTasksDetailsAfterInsert, { externalId: externalId }) + taskSequence[seqCounter++] = externalId // If found, replace externalId with _id; otherwise, keep the externalId return task ? ObjectId(task._id) : externalId @@ -639,11 +646,10 @@ const duplicateResources = async (resourceDetails) => { // update the project template after tasks created templateProjects.forEach((project) => { let projectTasks = [] - let taskSequence = [] project.tasks.forEach((task) => { projectTasks.push(taskMap[task]) - let seqNum = task.sequenceNumber - 1 < 0 ? 0 : task.sequenceNumber - 1 - taskSequence[seqNum] = task.externalId + // let seqNum = task.sequenceNumber - 1 < 0 ? 0 : task.sequenceNumber - 1 + // taskSequence[seqNum] = task.externalId }) project.tasks = projectTasks project.taskSequence = taskSequence @@ -672,103 +678,133 @@ const duplicateResources = async (resourceDetails) => { } } +/** + * Process targeting criteria + * @name processTargetingCriteria + * @param {Object} targetingData - Program template data + * @returns {Object} - Response contains scope and metaInformation + */ +const processTargetingCriteria = (targetingData) => { + let scope = { + roles: [], + entityType: [], + } + let metaInformation = { + recommendedFor: [], + } + + if (targetingData) { + // Iterate through each targeting criterion + targetingData.forEach((targeting) => { + const targetingEntity = targeting?.entity_targeting?.value + + scope.entityType.push(targetingEntity) + + if (targeting?.roles?.length) { + // Add unique roles to scope and metaInformation + targeting.roles.forEach(({ value, label }) => { + scope.roles.push(value) + metaInformation.recommendedFor.push(label) + }) + } else { + // Reset roles and recommendedFor if no roles are present + scope.roles = [] + metaInformation.recommendedFor = [] + } + + // Add entity-specific targets to scope and metaInformation + targeting[targetingEntity]?.forEach(({ name, _id }) => { + scope[targetingEntity] = scope[targetingEntity] || [] + metaInformation[targetingEntity] = metaInformation[targetingEntity] || [] + scope[targetingEntity].push(_id) + metaInformation[targetingEntity].push(name) + }) + }) + } + // refactor scope to remove duplicates + Object.keys(scope).forEach((key) => { + if (Array.isArray(scope[key] && scope[key].length > 0)) { + scope[key] = [...new Set(scope[key])] // Remove duplicates while preserving array structure + } + }) + // refactor metaInformation to remove duplicates + Object.keys(metaInformation).forEach((key) => { + if (Array.isArray(metaInformation[key] && metaInformation[key].length > 0)) { + metaInformation[key] = [...new Set(metaInformation[key])] // Remove duplicates while preserving array structure + } + }) + + return { scope, metaInformation } +} + /** * Format Program Template * @name formatProgramTemplate - * @param {Object} templateData - Program template data + * @param {Object} programData - Program template data * @returns {Object} - Response contains formatted template */ -const formatProgramTemplate = (templateData) => { +const formatProgramTemplate = (programData) => { try { - let language = templateData?.language - ? templateData?.resource.flatMap((resource) => { - return resource.languages.map((language) => { - return language.label - }) - }) - : [] - - language = [...new Set(language)] - let keywords = templateData?.keywords - ? templateData?.keywords - : templateData?.resource - ? utils.formatKeywords(templateData?.resource?.keywords) - : [] - keywords = [...new Set(keywords)] - - let resourceDetails = templateData?.resource - if (templateData?.resource?.published_id) resourceDetails._id = templateData?.resource?.published_id - - let scope = { - roles: [], - entityType: [], - } - let metaInformation = { - recommendedFor: [], - } - // check if targeting criteria is given in the template or not - if (templateData?.targeting_criteria) { - // iterate through the targeting_criteria given - templateData?.targeting_criteria.forEach((targeting) => { - // fetch the targeting entity - const targeting_entity = targeting?.entity_targeting?.value - // check if entity type in scope is added or not. if not added , add - if (!scope.entityType.includes(targeting_entity)) scope.entityType.push(targeting_entity) - // check of roles - if (targeting?.roles) { - // iterate through the roles object and add the value of role if its not added - targeting.roles.forEach((role) => { - if (!scope.roles.includes(role.value)) scope.roles.push(role.value) - // iterate through the roles object and add the value of role in metaInformation.recommendedFor if its not added - if (!metaInformation.recommendedFor.includes(role.label)) - metaInformation.recommendedFor.push(role.label) - }) - } else { - scope.roles = [] - metaInformation.recommendedFor = [] - } - targeting[targeting_entity].forEach((target) => { - // add the entity ids in scope and metaInformation - if (scope[targeting_entity] == undefined) scope[targeting_entity] = [] - if (metaInformation[targeting_entity] == undefined) metaInformation[targeting_entity] = [] - metaInformation[targeting_entity].push(target.name) - scope[targeting_entity].push(target._id) - }) - }) - } - // prepare the program template - let programTemplate = { - scope, - metaInformation, - resourceType: [common.ROLLOUT_TYPE_PROGRAM], - language, - keywords, - concepts: templateData?.concepts ? templateData?.concepts : [], - components: [], - resourceDetails, - isAPrivateProgram: false, - isDeleted: false, - requestForPIIConsent: templateData?.requestForPIIConsent ? true : false, - rootOrganisations: [ - templateData?.rootOrganisations ? templateData?.rootOrganisations : templateData?.organization?.id, - ], - createdFor: [templateData?.createdFor ? templateData?.createdFor : templateData?.organization?.id], - deleted: false, - status: common.STATUS_ACTIVE.toLowerCase(), - owner: templateData?.created_by, - createdBy: templateData?.created_by, - updatedBy: templateData?.created_by, - externalId: utils.generateExternalId(templateData?.title), - name: templateData?.title.trim(), - description: templateData?.description ? templateData?.description : '', - updatedAt: new Date(), - createdAt: new Date(), - endDate: new Date(templateData?.end_date), - startDate: new Date(templateData?.start_date), + let programDocument = {} + if (programData?.targeting_criteria) { + const targeting = processTargetingCriteria(programData?.targeting_criteria) + programDocument.scope = targeting.scope + programDocument.metaInformation = targeting.metaInformation } + programDocument.updatedAt = new Date() + programDocument.endDate = new Date(programData?.end_date) + programDocument.startDate = new Date(programData?.start_date) // if the program is already published , update _id from the published_id - if (templateData.published_id) programTemplate._id = ObjectId(templateData.published_id) - return { success: true, programTemplate } + if (programData?.published_id) { + programDocument._id = ObjectId(programData.published_id) + } else { + let language = programData?.language + ? programData?.resource.flatMap((resource) => { + return resource.languages.map((language) => { + return language.label + }) + }) + : [] + + language = [...new Set(language)] + let keywords = programData?.keywords + ? programData?.keywords + : programData?.resource + ? utils.formatKeywords(programData?.resource?.keywords) + : [] + keywords = [...new Set(keywords)] + + let resourceDetails = programData?.resource // prepare the program template + programDocument = { + ...programDocument, + ...{ + resourceType: [common.ROLLOUT_TYPE_PROGRAM], + language, + keywords, + concepts: programData?.concepts ? programData?.concepts : [], + components: [], + resourceDetails, + isAPrivateProgram: false, + isDeleted: false, + requestForPIIConsent: programData?.requestForPIIConsent ? true : false, + rootOrganisations: [ + programData?.rootOrganisations ? programData?.rootOrganisations : programData?.organization?.id, + ], + createdFor: [programData?.createdFor ? programData?.createdFor : programData?.organization?.id], + deleted: false, + status: common.STATUS_ACTIVE.toLowerCase(), + owner: programData?.created_by, + createdBy: programData?.created_by, + updatedBy: programData?.created_by, + externalId: utils.generateExternalId(programData?.title), + name: programData?.title.trim(), + description: programData?.description ? programData?.description.trim() : '', + createdAt: new Date(), + }, + } + } + + return { success: true, programDocument } } catch (error) { console.error('Error in formatTemplate:', error.message) return { success: false, error: error.message } @@ -1028,17 +1064,18 @@ const publishProgram = function (programData) { throw new Error('FAILED_TO_FORMAT_TEMPLATE') } - let template = formattedTemplate.template + let template = formattedTemplate.programDocument // fetch the resource details to create solutions - const resourceDetailsCreate = template.resourceDetails + const resourceDetailsCreate = programData.resource + delete template.resourceDetails const resourceStatus = await rolloutQueries.findOne( { - id: template?.resourceDetails?.rolloutId, + id: resourceDetailsCreate?.rolloutId, }, { attributes: ['status', 'published_id'] } ) const programScope = template.scope - delete template.resourceDetails + let result = {} let programId = template?._id @@ -1046,16 +1083,13 @@ const publishProgram = function (programData) { const programsCollection = mongoDb.collection(COLLECTIONS.PROGRAMS) // if program is already created , update scope , start and end dates else create a new program if (programId) { - const updateTemplate = { - scope: formattedTemplate.template.scope, - endDate: formattedTemplate.template.endDate, - startDate: formattedTemplate.template.startDate, - } + let updateData = template + delete updateData._id result = await programsCollection.updateOne( { _id: template?._id }, { - $set: updateTemplate, + $set: updateData, } ) @@ -1089,7 +1123,7 @@ const publishProgram = function (programData) { solutions.push(resourceDetailsCreate?.published_id) } else { - const duplicateResource = await duplicateResources(resourceDetailsCreate) + const duplicateResource = await duplicateResources(resourceDetailsCreate, programData.created_by) solutions = await createSolutions(duplicateResource, { _id: programId, scope: programScope, @@ -1098,8 +1132,8 @@ const publishProgram = function (programData) { description: template.description ? template.description : '', end_date: template.endDate, start_date: template.startDate, - loggedInUserId: programData.loggedInUserId, - orgId: programData.orgId, + created_by: programData.created_by, + orgId: programData.organization_id, }) const solutionIds = solutions.map((solution) => solution._id) // Update Template with tasks and sequence diff --git a/src/services/rollouts.js b/src/services/rollouts.js index 85ff3c6b..200b46d2 100644 --- a/src/services/rollouts.js +++ b/src/services/rollouts.js @@ -612,19 +612,27 @@ module.exports = class RolloutsHelper { organization_id: orgId, }) - if (!solutionRollout && resourceDetailsResult?.type != common.ROLLOUT_TYPE_PROGRAM) { - resourceDetailsResult.parent_id = rolloutId - // add the start date and end date of program for single roll out - resourceDetailsResult.start_date = rolloutDetailsResult.start_date - resourceDetailsResult.end_date = rolloutDetailsResult.end_date - const resultCreateRollout = await this.create(resourceDetailsResult, loggedInUserId, orgId, true) - solutionRolloutId = resultCreateRollout?.result?.id + const parentRollout = await rolloutQueries.findOne({ + id: rolloutId, + organization_id: orgId, + }) + + if (solutionRollout?.id == undefined && resourceDetailsResult?.type != common.ROLLOUT_TYPE_PROGRAM) { + let childRollout = parentRollout + childRollout.parent_id = rolloutId + childRollout.type = common.ROLLOUT_TYPE_SOLUTION + delete childRollout.id + const resultCreateRollout = await rolloutQueries.create(childRollout) + solutionRolloutId = resultCreateRollout.id } else { + let childRollout = { + blob_path: parentRollout.blob_path, + } // update the start date and end date of program for single roll out solutionRolloutId = solutionRollout.id - bodyData.start_date = rolloutDetailsResult.start_date - bodyData.end_date = rolloutDetailsResult.end_date - await this.update(solutionRolloutId, bodyData, loggedInUserId, orgId) + childRollout.start_date = parentRollout.start_date + childRollout.end_date = parentRollout.end_date + await rolloutQueries.updateOne(childRollout, { id: solutionRolloutId }) } // publish the resource if not published @@ -636,8 +644,6 @@ module.exports = class RolloutsHelper { } const rolloutKafkaPayload = { ...rolloutDetails.result, - loggedInUserId, - orgId, resource: { ...resourceDetails?.result, rolloutId: solutionRolloutId, From d5abfba6cb8f528d497ca47c73398c22ed508879 Mon Sep 17 00:00:00 2001 From: adithya_dinesh Date: Tue, 31 Dec 2024 17:43:36 +0530 Subject: [PATCH 18/18] consumption side changes --- src/requests/consumption.js | 20 ++++++++++++------- src/services/rollouts.js | 38 ++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/requests/consumption.js b/src/requests/consumption.js index e9e11a89..f3e07719 100644 --- a/src/requests/consumption.js +++ b/src/requests/consumption.js @@ -581,11 +581,14 @@ const duplicateResources = async (resourceDetails, created_by) => { // projectTaskId : duplicateProjectTaskId // } let taskMap = {} + let taskSeqMap = {} + const externalId_suffixing = `${Date.now()}${common.SUFFIX_CHILD}` if (projectTemplates.length > 0) { // create project duplicate template to create projectTemplates.forEach((project) => { - project.externalId = project.externalId + Date.now() + common.SUFFIX_CHILD + project.externalId = project.externalId + externalId_suffixing + taskSeqMap[project.externalId] = project.taskSequence delete project._id project.updatedAt = new Date() project.createdAt = new Date() @@ -597,6 +600,7 @@ const duplicateResources = async (resourceDetails, created_by) => { resource_id: resourceDetails.resource_id, rollout_id: resourceDetails.rolloutId, } + templateProjects.push(project) }) // array of tasks to create @@ -614,12 +618,18 @@ const duplicateResources = async (resourceDetails, created_by) => { .toArray() // duplicate project task details to create projectsTasksDetails.forEach((projectTask) => { + let oldTaskExtId = projectTask.externalId projectTask.externalId = utils.generateUniqueId() + // replace old task id by new task id in sequence + _.update(taskSeqMap, projectTask.projectTemplateExternalId + externalId_suffixing, (tasks) => + tasks.map((task) => (task === oldTaskExtId ? projectTask.externalId : task)) + ) taskMap[projectTask._id] = projectTask.externalId projectTask.updatedAt = new Date() projectTask.createdAt = new Date() projectTask.createdBy = created_by projectTask.updatedBy = created_by + projectTask.projectTemplateExternalId = projectTask.projectTemplateExternalId + externalId_suffixing delete projectTask._id duplicateTasks.push(projectTask) }) @@ -633,12 +643,10 @@ const duplicateResources = async (resourceDetails, created_by) => { }, }) .toArray() - let seqCounter = 0 - let taskSequence = [] + taskMap = _.mapValues(taskMap, (externalId) => { // Find the corresponding object from projectsTasksDetailsAfterInsert const task = _.find(projectsTasksDetailsAfterInsert, { externalId: externalId }) - taskSequence[seqCounter++] = externalId // If found, replace externalId with _id; otherwise, keep the externalId return task ? ObjectId(task._id) : externalId @@ -648,11 +656,9 @@ const duplicateResources = async (resourceDetails, created_by) => { let projectTasks = [] project.tasks.forEach((task) => { projectTasks.push(taskMap[task]) - // let seqNum = task.sequenceNumber - 1 < 0 ? 0 : task.sequenceNumber - 1 - // taskSequence[seqNum] = task.externalId }) project.tasks = projectTasks - project.taskSequence = taskSequence + project.taskSequence = taskSeqMap[project.externalId] }) // create project templates await projectsCollection.insertMany(templateProjects) diff --git a/src/services/rollouts.js b/src/services/rollouts.js index 200b46d2..c9c6a5de 100644 --- a/src/services/rollouts.js +++ b/src/services/rollouts.js @@ -141,7 +141,7 @@ module.exports = class RolloutsHelper { * @param {String} loggedInUserId - User id * @returns {JSON} - Rollout Details */ - static async details(rolloutId, orgId, loggedInUserId) { + static async details(rolloutId, orgId, loggedInUserId, returnBlobPath = false) { try { let result = { organization: {}, @@ -174,7 +174,9 @@ module.exports = class RolloutsHelper { ...rollout, } - delete resultData['blob_path'] + if (!returnBlobPath) { + delete resultData['blob_path'] + } resultData.viewers = [] // fetch the user if viewer is present @@ -579,7 +581,7 @@ module.exports = class RolloutsHelper { static async publish(rolloutId, loggedInUserId, orgId) { try { // fetch rollout details - const rolloutDetails = await this.details(rolloutId, orgId, loggedInUserId) + const rolloutDetails = await this.details(rolloutId, orgId, loggedInUserId, true) let solutionRolloutId const rolloutDetailsResult = rolloutDetails?.result @@ -612,26 +614,28 @@ module.exports = class RolloutsHelper { organization_id: orgId, }) - const parentRollout = await rolloutQueries.findOne({ - id: rolloutId, - organization_id: orgId, - }) - - if (solutionRollout?.id == undefined && resourceDetailsResult?.type != common.ROLLOUT_TYPE_PROGRAM) { - let childRollout = parentRollout - childRollout.parent_id = rolloutId + if (!solutionRollout?.id) { + let childRollout = _.pick(rolloutDetailsResult, [ + 'title', + 'blob_path', + 'start_date', + 'end_date', + 'resource_id', + 'created_by', + 'updated_by', + 'status', + 'organization_id', + 'user_id', + 'resource_type', + ]) childRollout.type = common.ROLLOUT_TYPE_SOLUTION - delete childRollout.id + childRollout.parent_id = rolloutId const resultCreateRollout = await rolloutQueries.create(childRollout) solutionRolloutId = resultCreateRollout.id } else { - let childRollout = { - blob_path: parentRollout.blob_path, - } + let childRollout = _.pick(rolloutDetailsResult, ['blob_path', 'start_date', 'end_date']) // update the start date and end date of program for single roll out solutionRolloutId = solutionRollout.id - childRollout.start_date = parentRollout.start_date - childRollout.end_date = parentRollout.end_date await rolloutQueries.updateOne(childRollout, { id: solutionRolloutId }) }