diff --git a/.gitignore b/.gitignore index 38bd082..f0cbe09 100644 --- a/.gitignore +++ b/.gitignore @@ -78,3 +78,5 @@ gcp1.json dev-ops/report +config.json + diff --git a/LICENSE b/LICENSE index 13dc8e9..8872c82 100644 --- a/LICENSE +++ b/LICENSE @@ -7,3 +7,4 @@ Permission is hereby granted, free of charge, to any person obtaining a copy of The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md index 6111290..656baac 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ + + +# entity-management
# Entity Management @@ -228,3 +231,4 @@ Several open source dependencies that have aided Mentoring's development: ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) ![MongoDB](https://img.shields.io/badge/MongoDB-%234ea94b.svg?style=for-the-badge&logo=mongodb&logoColor=white) ![Git](https://img.shields.io/badge/git-%23F05033.svg?style=for-the-badge&logo=git&logoColor=white) + diff --git a/deployment/ansible.yml b/deployment/ansible.yml index 59bb8a5..7d66557 100644 --- a/deployment/ansible.yml +++ b/deployment/ansible.yml @@ -11,14 +11,12 @@ #- debug: msg="{{ slurpfile['content'] | b64decode }}" #- debug: msg="the value of foo.txt is {{ contents }}" - name: Get vault credentials - shell: "curl --location --request GET '{{ vaultAddress }}elevate-entity-management' --header 'X-Vault-Token: {{ slurpfile['content'] | b64decode }}' | jq '.data' > '{{ project_path -}}/data2.json'" + shell: "curl --location --request GET '{{ vaultAddress }}elevate-entity-management' --header 'X-Vault-Token: {{ slurpfile['content'] | b64decode }}' | jq '.data' > '{{ project_path }}/data2.json'" register: credentials - debug: msg="{{ credentials }}" - name: Get gcp credentials - shell: "curl --location --request GET '{{ vaultAddress }}gcp' --header 'X-Vault-Token: {{ slurpfile['content'] | b64decode }}' | jq '.data.data' > '{{ project_path -}}/gcp.json'" + shell: "curl --location --request GET '{{ vaultAddress }}gcp' --header 'X-Vault-Token: {{ slurpfile['content'] | b64decode }}' | jq '.data.data' > '{{ project_path }}/gcp.json'" - name: Set some variable set_fact: @@ -44,7 +42,10 @@ update: yes version: "{{ gitBranch }}" - name: Update npm - shell: cd {{release_path}}/src && npm i && npm i redoc-cli + shell: cd {{release_path}}/src && npm i && npm i redoc-cli + + - name: Get config.json + shell: "curl --location --request GET '{{ vaultAddress }}projectSurveyEntityConfig' --header 'X-Vault-Token: {{ slurpfile['content'] | b64decode }}' | jq '.data.data' > '{{ release_path }}/config.json'" - name: Delete Old Folder shell: rm -rf {{ current_path }} && cd {{ project_path }} && mkdir entity-management @@ -84,4 +85,4 @@ - name: debug info debug: - msg: "Pm2 log {{pm2Info}}" + msg: "Pm2 log {{pm2Info}}" \ No newline at end of file diff --git a/src/.env.sample b/src/.env.sample index 160f342..e137e98 100644 --- a/src/.env.sample +++ b/src/.env.sample @@ -18,4 +18,9 @@ API_DOC_URL = "/entity-management/api-doc" IS_AUTH_TOKEN_BEARER=false AUTH_METHOD = native #or keycloak_public_key -KEYCLOAK_PUBLIC_KEY_PATH = path to the pem/secret file \ No newline at end of file +KEYCLOAK_PUBLIC_KEY_PATH = path to the pem/secret file +ADMIN_TOKEN_HEADER_NAME = admin-access-token // admin access token header name +ADMIN_ACCESS_TOKEN = ivopeiovcie-----------lvkkdvkdm // admin access token + +INTERFACE_SERVICE_URL = http://localhost:5000/interface-management // interface service url +USER_SERVICE_BASE_URL = user // user service base url \ No newline at end of file diff --git a/src/api-doc/api-doc.yaml b/src/api-doc/api-doc.yaml index ee5aa45..0ed9333 100644 --- a/src/api-doc/api-doc.yaml +++ b/src/api-doc/api-doc.yaml @@ -890,6 +890,25 @@ paths: items: type: object properties: + metaInformation: + type: object + properties: + targetedEntityTypes: + type: array + items: + type: object + properties: + entityType: + type: string + entityTypeId: + type: string + required: + - entityType + - entityTypeId + externalId: + type: string + name: + type: string childHierarchyPath: type: array items: @@ -898,6 +917,10 @@ paths: type: string updatedBy: type: string + orgIds: + type: array + items: + type: string _id: type: string deleted: @@ -913,11 +936,10 @@ paths: type: string code: type: string - metaInformation: - type: object - properties: - externalId: - type: string + userId: + type: string + tenantId: + type: string updatedAt: type: string createdAt: @@ -930,25 +952,34 @@ paths: message: ENTITY_ADDED status: 200 result: - - childHierarchyPath: + - metaInformation: + targetedEntityTypes: + - entityType: state + entityTypeId: 683953548f365ab56c8022e9 + - entityType: block + entityTypeId: 6839535b8f365ab56c8022ed + externalId: KA28 + name: Karnataka + childHierarchyPath: - district - - beat + - block - cluster - school - - block - createdBy: SYSTEM - updatedBy: SYSTEM - _id: 6634b0767411a605fdbaca71 + createdBy: '208' + updatedBy: '208' + orgIds: + - ALL + _id: 683996e732316a1ac4f6f150 deleted: false - entityTypeId: 5f32d8228e0dc83124040567 - entityType: school + entityTypeId: 683953808f365ab56c8022f5 + entityType: professional_role registryDetails: - locationId: entity123 - code: entity123 - metaInformation: - externalId: entity123 - updatedAt: '2024-05-03T09:37:58.425Z' - createdAt: '2024-05-03T09:37:58.425Z' + locationId: KA28 + code: KA28 + userId: '208' + tenantId: '24' + updatedAt: '2025-05-30T11:30:47.909Z' + createdAt: '2025-05-30T11:30:47.909Z' __v: 0 '400': description: Bad Request. @@ -979,22 +1010,26 @@ paths: type: string name: type: string - entityType: - type: string childHierarchyPath: type: array items: type: string + targetedEntityTypes: + type: array + items: + type: string examples: sampleBodyData: value: - externalId: entity123 - name: entityName + externalId: KA28 + name: Karnataka childHierarchyPath: - district - - beat + - block - cluster - school + targetedEntityTypes: + - state - block /v1/entities/details/{_id}?&language={code}: get: @@ -1148,8 +1183,29 @@ paths: metaInformation: type: object properties: + targetedEntityTypes: + type: array + items: + type: object + properties: + entityType: + type: string + entityTypeId: + type: string + required: + - entityType + - entityTypeId externalId: type: string + name: + type: string + registryDetails: + type: object + properties: + locationId: + type: string + code: + type: string childHierarchyPath: type: array items: @@ -1158,6 +1214,10 @@ paths: type: string updatedBy: type: string + orgIds: + type: array + items: + type: string deleted: type: boolean _id: @@ -1166,6 +1226,10 @@ paths: type: string entityType: type: string + userId: + type: string + tenantId: + type: string updatedAt: type: string createdAt: @@ -1175,24 +1239,33 @@ paths: examples: SuccessResponse: value: - message: ENTITY_UPDATATED + message: ENTITY_UPDATED status: 200 result: metaInformation: - externalId: SCH - name: school + targetedEntityTypes: + - entityType: state + entityTypeId: 683953548f365ab56c8022e9 + - entityType: block + entityTypeId: 6839535b8f365ab56c8022ed + externalId: KA28 + name: Karnataka registryDetails: - locationId: rajAPSTATEDummy4 - code: rajAPSTATEDummy4 + locationId: KA28 + code: KA28 childHierarchyPath: [] - createdBy: user123 - updatedBy: user123 + createdBy: '208' + updatedBy: '208' + orgIds: + - ALL deleted: false - _id: 66ea64fa68cd063346a10365 - entityTypeId: 6672ce0fc05aa58f89ba12f1 - entityType: state - updatedAt: '2024-09-18T10:07:39.407Z' - createdAt: '2024-09-18T05:28:26.148Z' + _id: 68395a55e5af17c215ce3408 + entityTypeId: 683953808f365ab56c8022f5 + entityType: professional_role + userId: '208' + tenantId: '24' + updatedAt: '2025-05-30T11:32:35.296Z' + createdAt: '2025-05-30T07:12:21.457Z' __v: 0 '400': description: Bad Request. @@ -1216,26 +1289,34 @@ paths: schema: type: object properties: - metaInformation.externalId: - type: string + metaInformation.targetedEntityTypes: + type: array + items: + type: object + properties: + entityType: + type: string + entityTypeId: + type: string + required: + - entityType + - entityTypeId metaInformation.name: type: string childHierarchyPath: type: array items: type: string - createdBy: - type: string - updatedBy: - type: string examples: sampleBodyData: value: - metaInformation.externalId: SCH - metaInformation.name: school + metaInformation.targetedEntityTypes: + - entityType: state + entityTypeId: 683953548f365ab56c8022e9 + - entityType: block + entityTypeId: 6839535b8f365ab56c8022ed + metaInformation.name: 'Karnataka ' childHierarchyPath: [] - createdBy: user123 - updatedBy: user123 /v1/entities/mappingUpload: post: summary: This endpoint will map its childEntity to its parentEntity @@ -1628,6 +1709,34 @@ paths: - location: body param: query msg: required query + parameters: + - in: query + name: page + description: Page number for pagination + schema: &ref_4 + type: number + - in: query + name: limit + description: Number of documents to return per page + schema: *ref_4 + - in: query + name: search + description: Optional search string for text-based filtering + schema: &ref_5 + type: string + - in: query + name: aggregateValue + description: Field path to be used for aggregation + schema: *ref_5 + - in: query + name: aggregateStaging + description: Whether to apply aggregation stages in the pipeline + schema: &ref_6 + type: boolean + - in: query + name: aggregateSort + description: Whether to apply sorting within the aggregation pipeline + schema: *ref_6 /v1/entities/list/_id?page={page_no}&limit={page_limit}&type={entity_type}: get: summary: List entities based on entity id diff --git a/src/controllers/v1/entities.js b/src/controllers/v1/entities.js index bc9a3f5..a8b8a87 100644 --- a/src/controllers/v1/entities.js +++ b/src/controllers/v1/entities.js @@ -42,6 +42,18 @@ module.exports = class Entities extends Abstract { * @api {POST} /v1/entities/find all the API based on projection * @apiVersion 1.0.0 * @apiName find + * @param {Object} req - The request object. + * @param {Object} req.body.query - MongoDB filter query to match specific entity documents. + * @param {Object} req.body.projection - Fields to include or exclude in the result set. + * @param {Number} req.pageNo - Page number for pagination. + * @param {Number} req.pageSize - Number of documents to return per page. + * @param {String} req.searchText - Optional search string for text-based filtering. + * @param {String|null} req.query.aggregateValue - Field path to be used for aggregation (e.g., "groups.school"); set to `null` if not used. + * @param {Boolean} req.query.aggregateStaging - Whether to apply aggregation stages in the pipeline. + * @param {Boolean} req.query.aggregateSort - Whether to apply sorting within the aggregation pipeline. + * @param {Array} req.body.aggregateProjection - Optional array of projection stages for aggregation. + * + * @returns {Promise} - A Promise resolving to a list of matched entity documents with pagination. * @apiGroup Entities * @apiSampleRequest { "query" : { @@ -67,10 +79,22 @@ module.exports = class Entities extends Abstract { */ find(req) { + console.log('Reached in find controller') return new Promise(async (resolve, reject) => { try { // Calls the 'find' function from 'entitiesHelper' to retrieve entity data - let entityData = await entitiesHelper.find(req.body.query, req.body.projection) + req.body.query = UTILS.stripOrgIds(req.body.query) + let entityData = await entitiesHelper.find( + req.body.query, + req.body.projection, + req.pageNo, + req.pageSize, + req.searchText, + req.query.aggregateValue ? req.query.aggregateValue : null, + req.query.aggregateStaging == 'true' ? true : false, + req.query.aggregateSort == 'true' ? true : false, + req.body.aggregateProjection ? req.body.aggregateProjection : [] + ) return resolve(entityData) } catch (error) { return reject({ @@ -87,6 +111,7 @@ module.exports = class Entities extends Abstract { * @apiVersion 1.0.0 * @apiName Get Related Entities * @apiGroup Entities + * @param {Object} req - The request object. * @apiSampleRequest v1/entities/relatedEntities/5c0bbab881bdbe330655da7f * @apiUse successBody * @apiUse errorBody @@ -128,7 +153,11 @@ module.exports = class Entities extends Abstract { 'entityTypeId', 'entityType', ] - let entityDocument = await entitiesQueries.entityDocuments({ _id: req.params._id }, projection) + let tenantId = req.userDetails.userInformation.tenantId + let entityDocument = await entitiesQueries.entityDocuments( + { _id: req.params._id, tenantId: tenantId }, + projection + ) if (entityDocument.length < 1) { throw { @@ -141,7 +170,8 @@ module.exports = class Entities extends Abstract { entityDocument[0]._id, entityDocument[0].entityTypeId, entityDocument[0].entityType, - projection + projection, + tenantId ) _.merge(result, entityDocument[0]) result['relatedEntities'] = relatedEntities.length > 0 ? relatedEntities : [] @@ -166,6 +196,7 @@ module.exports = class Entities extends Abstract { * @apiVersion 1.0.0 * @apiName entityListBasedOnEntityType * @apiGroup Entities + * @param {Object} req - The request object. * @apiUse successBody * @apiUse errorBody * @apiParamExample {json} Response: @@ -195,7 +226,8 @@ module.exports = class Entities extends Abstract { req.pageNo, req.pageSize, req?.query?.paginate?.toLowerCase() == 'true' ? true : false, - req.query.language ? req.query.language : '' + req.query.language ? req.query.language : '', + req.userDetails ) return resolve(entityData) } catch (error) { @@ -215,6 +247,7 @@ module.exports = class Entities extends Abstract { * @apiName createMappingCsv * @apiGroup Entities * @apiParam {File} entityCSV Mandatory entity mapping file of type CSV. + * @param {Object} req - The request object. * @apiUse successBody * @apiUse errorBody * @param {Object} req - The request object containing the uploaded CSV file in `req.files.entityCSV`. @@ -262,11 +295,12 @@ module.exports = class Entities extends Abstract { createMappingCsv(req) { return new Promise(async (resolve, reject) => { try { + let tenantId = req.userDetails.tenantAndOrgInfo.tenantId // Parse CSV data from the uploaded file in the request body let entityCSVData = await csv().fromString(req.files.entityCSV.data.toString()) // Process the entity mapping upload data using 'entitiesHelper.createMappingCsv' - let mappedEntities = await entitiesHelper.createMappingCsv(entityCSVData) + let mappedEntities = await entitiesHelper.createMappingCsv(entityCSVData, tenantId) return resolve({ message: CONSTANTS.apiResponses.MAPPING_CSV_GENERATED, @@ -288,6 +322,7 @@ module.exports = class Entities extends Abstract { * @apiName mappingUpload * @apiGroup Entities * @apiParam {File} entityMap Mandatory entity mapping file of type CSV. + * @param {Object} req - The request object. * @apiUse successBody * @apiUse errorBody * @param {Array} req.files.entityMap - Array of entityMap data. @@ -340,8 +375,6 @@ module.exports = class Entities extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - The request object containing parameters and user details. - * @param {Object} req.params - The request parameters. - * @param {string} req.params._id - The entity ID to filter roles. * @returns {Promise} A promise that resolves to the response containing the fetched roles or an error object. * * @returns {JSON} - Message of successfully response. * @@ -386,7 +419,9 @@ module.exports = class Entities extends Abstract { req.pageNo, req.pageSize, req?.query?.paginate?.toLowerCase() == 'true' ? true : false, - req.query.entityType ? req.query.entityType : '' + req.query.entityType ? req.query.entityType : '', + req.query.language ? req.query.language : '', + req.userDetails.userInformation.tenantId ) // Resolves the promise with the retrieved entity data return resolve(userRoleDetails) @@ -407,6 +442,7 @@ module.exports = class Entities extends Abstract { * @apiName details * @apiGroup Entities * @apiHeader {String} X-authenticated-user-token Authenticity token + * @param {Object} req - The request object. * @apiSampleRequest v1/entities/details/67dcf90f97174bab15241faa?&language=hi * @apiUse successBody * @apiUse errorBody @@ -492,7 +528,8 @@ module.exports = class Entities extends Abstract { let result = await entitiesHelper.details( req.params._id ? req.params._id : '', req.body ? req.body : {}, - req.query.language ? req.query.language : '' + req.query.language ? req.query.language : '', + req.userDetails ) return resolve(result) @@ -517,9 +554,6 @@ module.exports = class Entities extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - requested entity data. - * @param {String} req.query.type - entity type. - * @param {String} req.params._id - entity id. - * @param {Object} req.body - entity information that need to be updated. * @returns {JSON} - Updated entity information. * * @@ -549,7 +583,7 @@ module.exports = class Entities extends Abstract { return new Promise(async (resolve, reject) => { try { // Call 'entitiesHelper.update' to perform the entity update operation - let result = await entitiesHelper.update(req.params._id, req.body) + let result = await entitiesHelper.update(req.params._id, req.body, req.userDetails) return resolve(result) } catch (error) { @@ -573,7 +607,6 @@ module.exports = class Entities extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - All requested Data. - * @param {Object} req.files - requested files. * @returns {JSON} - Added entities information. * * "result": [ @@ -602,8 +635,6 @@ module.exports = class Entities extends Abstract { // Prepare query parameters for adding the entity let queryParams = { type: req.query.type, - // programId: req.query.programId, - // solutionId: req.query.solutionId, parentEntityId: req.query.parentEntityId, } // Call 'entitiesHelper.add' to perform the entity addition operation @@ -634,7 +665,6 @@ module.exports = class Entities extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - requested data. - * @param {Object} req.body.locationIds - registry data. * @returns {Object} - * * "result": [ @@ -660,7 +690,7 @@ module.exports = class Entities extends Abstract { return new Promise(async (resolve, reject) => { try { // Call 'entitiesHelper.listByLocationIds' to retrieve entities based on location IDs - let entitiesData = await entitiesHelper.listByLocationIds(req.body.locationIds) + let entitiesData = await entitiesHelper.listByLocationIds(req.body.locationIds, req.userDetails) entitiesData.result = entitiesData.data @@ -683,6 +713,7 @@ module.exports = class Entities extends Abstract { * @apiGroup Entities * @apiHeader {String} X-authenticated-user-token Authenticity token * @apiSampleRequest v1/entities/subEntityListBasedOnRoleAndLocation + * @param {Object} req - The request object. * @apiUse successBody * @apiUse errorBody * @param {String} req.params._id - entityId. @@ -716,7 +747,10 @@ module.exports = class Entities extends Abstract { return new Promise(async (resolve, reject) => { try { // Call 'entitiesHelper.subEntityListBasedOnRoleAndLocation' to retrieve sub-entity list - const entityTypeMappingData = await entitiesHelper.subEntityListBasedOnRoleAndLocation(req.params._id) + const entityTypeMappingData = await entitiesHelper.subEntityListBasedOnRoleAndLocation( + req.params._id, + req.userDetails + ) return resolve(entityTypeMappingData) } catch (error) { return reject({ @@ -737,7 +771,6 @@ module.exports = class Entities extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - requested data. - * @param {String} req.params._id - requested entity type. * @returns {JSON} - Array of entities. "result": [ @@ -782,12 +815,7 @@ module.exports = class Entities extends Abstract { * @apiSampleRequest /v1/entities/list * @apiUse successBody * @apiUse errorBody - * @param {String} req.query.type - type of entity requested. - * @param {String} req.params._id - requested entity id. - * @param {Number} req.pageSize - total size of the page. - * @param {Number} req.pageNo - page number. - * @param {string} req.query.schoolTypes - comma seperated school types. - * @param {string} req.query.administrationTypes - comma seperated administration types. + * @param {Object} req - The request object. * @apiParamExample {json} Response: * "result": [ { @@ -811,7 +839,8 @@ module.exports = class Entities extends Abstract { req.pageSize, req.pageSize * (req.pageNo - 1), req.schoolTypes, - req.administrationTypes + req.administrationTypes, + req.userDetails ) return resolve(result) @@ -881,7 +910,8 @@ module.exports = class Entities extends Abstract { req.searchText, req.pageSize, req.pageNo, - req.query.language ? req.query.language : '' + req.query.language ? req.query.language : '', + req.userDetails ) return resolve(entityDocuments) } catch (error) { @@ -916,8 +946,7 @@ module.exports = class Entities extends Abstract { * List of entities. * @method * @name listByIds - * @param {Object} req - requested data. - * @param {String} req.params._id - requested entity type. + * @param {Object} req - requested data. * @returns {JSON} - Array of entities. */ @@ -925,7 +954,11 @@ module.exports = class Entities extends Abstract { return new Promise(async (resolve, reject) => { try { // Call 'entitiesHelper.listByEntityIds' to retrieve entities based on provided entity IDs and fields - const entities = await entitiesHelper.listByEntityIds(req.body.entities, req.body.fields) + const entities = await entitiesHelper.listByEntityIds( + req.body.entities, + req.body.fields, + req.userDetails + ) return resolve(entities) } catch (error) { return reject({ @@ -948,10 +981,6 @@ module.exports = class Entities extends Abstract { * @apiUse errorBody * @apiParamExample {json} Response: * @param {Object} req - requested data. - * @param {String} req.query.type - requested entity type. - * @param {Object} req.userDetails - logged in user details. - * @param {Object} req.files.entities - entities data. - * @param {Object} req.files.translationFile - translation data. * @returns {CSV} - A CSV with name Entity-Upload is saved inside the folder * public/reports/currentDate * @@ -1025,8 +1054,6 @@ module.exports = class Entities extends Abstract { * @apiUse errorBody * @apiParamExample {json} Response: * @param {Object} req - requested data. - * @param {Object} req.files.entities - entities data. - * @param {Object} req.files.translationFile - entities data. * @returns {CSV} - A CSV with name Entity-Upload is saved inside the folder * public/reports/currentDate * @@ -1049,7 +1076,7 @@ module.exports = class Entities extends Abstract { translationFile = JSON.parse(req.files.translationFile.data.toString()) } // Call 'entitiesHelper.bulkUpdate' to update entities based on CSV data and user details - let newEntityData = await entitiesHelper.bulkUpdate(entityCSVData, translationFile) + let newEntityData = await entitiesHelper.bulkUpdate(entityCSVData, translationFile, req.userDetails) // Check if entities were updated successfully if (newEntityData.length > 0) { diff --git a/src/controllers/v1/entityTypes.js b/src/controllers/v1/entityTypes.js index 21bea33..26fb12a 100644 --- a/src/controllers/v1/entityTypes.js +++ b/src/controllers/v1/entityTypes.js @@ -57,12 +57,23 @@ module.exports = class EntityTypes extends Abstract { } ] */ - async list() { + async list(req) { return new Promise(async (resolve, reject) => { try { - // Call 'entityTypesHelper.list' to retrieve a list of entity types - // 'all' parameter retrieves all entity types, and { name: 1 } specifies projection to include only 'name' field - let result = await entityTypesHelper.list('all', { name: 1 }) + let organizationId + let query = {} + + // create query to fetch assets + query['tenantId'] = req.userDetails.tenantAndOrgInfo + ? req.userDetails.tenantAndOrgInfo.tenantId + : req.userDetails.userInformation.tenantId + + // handle currentOrgOnly filter + if (req.query['currentOrgOnly'] && req.query['currentOrgOnly'] == 'true') { + organizationId = req.userDetails.userInformation.organizationId + query['orgId'] = { $in: [organizationId] } + } + let result = await entityTypesHelper.list(query, ['name'], req.pageNo, req.pageSize, req.searchText) return resolve(result) } catch (error) { @@ -99,8 +110,15 @@ module.exports = class EntityTypes extends Abstract { async find(req) { return new Promise(async (resolve, reject) => { try { + req.body.query = UTILS.stripOrgIds(req.body.query) // Call 'entityTypesHelper.list' to find entity types based on provided query, projection, and skipFields - let result = await entityTypesHelper.list(req.body.query, req.body.projection, req.body.skipFields) + let result = await entityTypesHelper.list( + req.body.query, + req.body.projection, + req.pageNo, + req.pageSize, + req.searchText + ) return resolve(result) } catch (error) { @@ -123,7 +141,6 @@ module.exports = class EntityTypes extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req -request data. - * @param {Object} req.files.entityTypes -entityTypes data. * @returns {CSV} create single entity Types data. * * "result": { @@ -166,9 +183,6 @@ module.exports = class EntityTypes extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - requested entityType data. - * @param {String} req.query.type - entityType type. - * @param {String} req.params._id - entityType id. - * @param {Object} req.body - entityType information that need to be updated. * @returns {JSON} - Updated entityType information. * "result": { "profileForm": [], @@ -199,7 +213,7 @@ module.exports = class EntityTypes extends Abstract { return new Promise(async (resolve, reject) => { try { // Call 'entityTypesHelper.update' to update an existing entity type - let result = await entityTypesHelper.update(req.params._id, req.body, req.userDetails.userInformation) + let result = await entityTypesHelper.update(req.params._id, req.body, req.userDetails) return resolve(result) } catch (error) { @@ -223,7 +237,6 @@ module.exports = class EntityTypes extends Abstract { * @apiUse errorBody * @apiParamExample {json} Response: * @param {Object} req -request data. - * @param {Object} req.files.entityTypes -entityTypes data. * @returns {CSV} Bulk create entity Types data. */ async bulkCreate(req) { @@ -288,7 +301,6 @@ module.exports = class EntityTypes extends Abstract { * @apiUse errorBody * @apiParamExample {json} Response: * @param {Object} req -request data. - * @param {Object} req.files.entityTypes -entityTypes data. * @returns {CSV} Bulk update entity Types data. */ async bulkUpdate(req) { diff --git a/src/controllers/v1/userRoleExtension.js b/src/controllers/v1/userRoleExtension.js index c153d81..b1234ee 100644 --- a/src/controllers/v1/userRoleExtension.js +++ b/src/controllers/v1/userRoleExtension.js @@ -43,9 +43,7 @@ module.exports = class userRoleExtension extends Abstract { * @apiSampleRequest /v1/userRoleExtension/create * @apiUse successBody * @apiUse errorBody - * @param {Object} req - The request object containing the request body and user details. - * @param {Object} req.body - The data for creating a new user role extension. - * @param {Object} req.userDetails - The details of the user making the request. + * @param {Object} req - The request object. * @returns {Promise} - A promise that resolves with the result of the creation or rejects with an error. * * { @@ -100,10 +98,7 @@ module.exports = class userRoleExtension extends Abstract { * @apiSampleRequest /v1/userRoleExtension/update/663364443c990eaa179e289e * @apiUse successBody * @apiUse errorBody - * @param {Object} req - The request object containing the request parameters and body. - * @param {Object} req.params - The request parameters. - * @param {string} req.params._id - The ID of the user role extension to update. - * @param {Object} req.body - The data for updating the user role extension. + * @param {Object} req - The request object. * @returns {Promise} - A promise that resolves with the result of the update or rejects with an error. * * @@ -135,7 +130,7 @@ module.exports = class userRoleExtension extends Abstract { return new Promise(async (resolve, reject) => { try { // Call the helper function to update the user role extension document - let result = await userRoleExtensionHelper.update(req.params._id, req.body) + let result = await userRoleExtensionHelper.update(req.params._id, req.body, req.userDetails) // Resolve the promise with the result of the update operation return resolve(result) @@ -155,6 +150,7 @@ module.exports = class userRoleExtension extends Abstract { * @apiVersion 1.0.0 * @apiName find * @apiGroup userRoleExtension + * @param {Object} req - The request object containing the request body. * @apiSampleRequest { { "query": { @@ -201,8 +197,15 @@ module.exports = class userRoleExtension extends Abstract { find(req) { return new Promise(async (resolve, reject) => { try { + req.body.query = UTILS.stripOrgIds(req.body.query) // Call the helper function to find the user role extensions - let userData = await userRoleExtensionHelper.find(req.body.query, req.body.projection) + let userData = await userRoleExtensionHelper.find( + req.body.query, + req.body.projection, + req.pageSize, // response limit - specifies the limit of documents needed in the response + req.pageSize * (req.pageNo - 1), // response offset - specifies the number of documents to be skipped + true + ) // Resolve the promise with the found user role extensions return resolve(userData) } catch (error) { @@ -226,9 +229,6 @@ module.exports = class userRoleExtension extends Abstract { * @apiUse successBody * @apiUse errorBody * @param {Object} req - The request object containing the request body. - * @param {Object} req.body - The request body. - * @param {Object} req.body.query - The query object to filter user role extensions. - * @param {Array} req.body.projection - The projection array to specify which fields to include in the result. * @returns {Promise} - A promise that resolves with the user data or rejects with an error. * * @@ -241,7 +241,7 @@ module.exports = class userRoleExtension extends Abstract { return new Promise(async (resolve, reject) => { try { // Call the helper function to delete the user role extension by ID - let userData = await userRoleExtensionHelper.delete(req.params._id) + let userData = await userRoleExtensionHelper.delete(req.params._id, req.userDetails) // Resolve the promise with the result of the deletion return resolve(userData) } catch (error) { diff --git a/src/databaseQueries/entityTypes.js b/src/databaseQueries/entityTypes.js index 96ffb48..53f1584 100644 --- a/src/databaseQueries/entityTypes.js +++ b/src/databaseQueries/entityTypes.js @@ -207,4 +207,28 @@ module.exports = class EntityTypes { } }) } + + /** + * Get Aggregate of entities documents. + * @method + * @name getAggregate + * @param {Object} [aggregateData] - aggregate Data. + * @returns {Array} - entities data. + */ + + static getAggregate(aggregateData) { + return new Promise(async (resolve, reject) => { + try { + // Use database model 'entities' to perform aggregation using the provided aggregateData + let entityData = await database.models.entityTypes.aggregate(aggregateData) + return resolve(entityData) + } catch (error) { + return reject({ + status: error.status || HTTP_STATUS_CODE.bad_request.status, + message: error.message || HTTP_STATUS_CODE.bad_request.message, + errorObject: error, + }) + } + }) + } } diff --git a/src/entityTypeAndEntityOnboarding/README.md b/src/entityTypeAndEntityOnboarding/README.md new file mode 100644 index 0000000..bc4514b --- /dev/null +++ b/src/entityTypeAndEntityOnboarding/README.md @@ -0,0 +1,171 @@ +# ๐Ÿงฑ Entity Management - Creation Flow + +This guide provides comprehensive and step-by-step instructions for managing entities within the SAAS platform, tailored to different user roles and environments. It covers authentication, creation, and mapping of entities to ensure a seamless onboarding and operational experience. + +--- + +## ๐Ÿ” Auth Keys & Tokens + +Authentication headers required for API calls, based on user roles and the environment. + +### ๐Ÿ”ธ Org Admin - QA + +| Key | Value | +| ----------------------- | ------------------------- | +| `internal-access-token` | `{internal-access-token}` | +| `X-auth-token` | `{Token}` | + +### ๐Ÿ”ธ Super Admin - QA + +| Key | Value | +| ----------------------- | ------------------------- | +| `internal-access-token` | `{internal-access-token}` | +| `X-auth-token` | `{Token}` | +| `admin-auth-token` | `{admin-auth-token}` | +| `tenantId` | `shikshagraha` | +| `orgId` | `blr` | + +--- + +## ๐ŸŒ Origin URLs (Per Organization) + +Domain URLs to be passed as the `origin` header during login requests. + +| Organization | Origin URL | +| ------------ | ------------------------------- | +| Shikshalokam | `shikshalokam-qa.tekdinext.com` | +| Shikshagraha | `shikshagrah-qa.tekdinext.com` | +| Super Admin | `default-qa.tekdinext.com` | + +--- + +## ๐Ÿ”‘ Login API + +Use this API to authenticate the user and generate a session token (`X-auth-token`). The token is mandatory for all secured API requests. + +
+Login API + +```bash +curl --location '{{baseURL}}/user/v1/account/login' \ +--header 'Content-Type: application/json' \ +--header 'origin: shikshalokam-qa.tekdinext.com' \ +--data-raw '{ + "identifier": "email/phone", + "password": "password" +}' +``` + +
+ +--- + +**NOTE**: + +- If you are an **Organization Admin**, please ensure that the headers match the values listed under _Org Admin - QA_. +- If you are a **Super Admin**, use the credentials and headers mentioned under _Super Admin - QA_. + +--- + +## ๐Ÿงฑ Add Entity Type + +Use this API to create a new entity type in the system. An entity type represents a category like `school`, `cluster`, `block`, etc. + +
+Add Entity Type API + +```bash +curl --location '{{baseURL}}/entity-management/v1/entityTypes/create' \ +--header 'internal-access-token: {internal-access-token}' \ +--header 'content-type: application/json' \ +--header 'X-auth-token: {{tokenToPass}}' \ +--header 'admin-auth-token: {admin-auth-token}' \ +--header 'tenantId: shikshagraha' \ +--header 'orgid: blr' \ +--data '{ + "name": "professional_role", + "registryDetails": { + "name": "schoolRegistry" + }, + "isObservable": true, + "toBeMappedToParentEntities": true +}' +``` + +
+ +--- + +## ๐Ÿซ Bulk Upload Entities + +Use this API to bulk create entities of a specific type (e.g., schools, teachers) by uploading a formatted CSV file. + +๐Ÿ“„ **CSV File**: [Karnataka School Upload CSV](https://drive.google.com/file/d/1SwOh11gmhehhrKH7SygA40DpYRE6IIjI/view) + +
+Bulk Upload API + +```bash +curl --location '{{baseURL}}/entity-management/v1/entities/bulkCreate?type=school' \ +--header 'internal-access-token: {internal-access-token}' \ +--header 'content-type: multipart/form-data' \ +--header 'x-auth-token: {{TokenToPass}}' \ +--form 'entities=@"/home/user4/Downloads/Karnata-upload data SG prod - schoolUpload.csv"' +``` + +
+ +--- + +## ๐Ÿงพ Generate Mapping CSV + +This API helps generate a base mapping CSV from the bulk uploaded entity data. The generated CSV will be used to create mappings between parent and child entities. + +๐Ÿ“„ **CSV File**: [Download Template CSV](https://drive.google.com/file/d/1n9pFGfZKaj77OBXfsDnwL5WEOHzpq6jr/view?usp=sharing) + +
+Generate Mapping CSV API + +```bash +curl --location '{{baseURL}}/entity-management/v1/entities/createMappingCsv' \ +--header 'x-auth-token: {{TokenToPass}}' \ +--header 'content-type: multipart/form-data' \ +--header 'internal-access-token: {internal-access-token}' \ +--form 'entityCSV=@"/home/user4/Downloads/chunk_0.csv"' +``` + +
+ +--- + +## ๐Ÿ”— Upload Entity Mapping + +This API maps child entities to their respective parent entities using the result CSV generated from the previous step (`createMappingCsv`). + +๐Ÿ“„ **CSV File**: [Sample Mapping Upload CSV](https://drive.google.com/file/d/1SVvi-F0y2YcwNfBpAOYzMZVeh4TbJCxd/view?usp=sharing) + +๐Ÿ“Œ **Note**: Always use the result CSV from the `createMappingCsv` step. + +
+Mapping Upload API + +```bash +curl --location '{{baseURL}}/entity-management/v1/entities/mappingUpload' \ +--header 'internal-access-token: {internal-access-token}' \ +--header 'x-auth-token: {{TokenToPass}}' \ +--form 'entityMap=@"/home/user4/Downloads/base64-to-csv-converter (8).csv"' +``` + +
+ +--- + +## โœ… Summary of Steps + +1. **Login** โ€“ Authenticate and retrieve your `X-auth-token`. +2. **Create Entity Type** โ€“ Define the category (type) of the entities you want to manage. +3. **Bulk Upload Entities** โ€“ Upload a list of entities using a formatted CSV file. +4. **Generate Mapping CSV** โ€“ Create a base CSV that outlines relationships between entities. +5. **Upload Entity Mapping** โ€“ Finalize the entity hierarchy by uploading the mapping CSV. + +--- diff --git a/src/entityTypeAndEntityOnboarding/utilityScripts/removeDuplicate.js b/src/entityTypeAndEntityOnboarding/utilityScripts/removeDuplicate.js new file mode 100644 index 0000000..ae6069f --- /dev/null +++ b/src/entityTypeAndEntityOnboarding/utilityScripts/removeDuplicate.js @@ -0,0 +1,42 @@ +/** + * name : removeDuplicate.js + * author : Mallanagouda R Biradar + * created-date : 24-June-2025 + * Description : Remove Duplicates. + */ + +const fs = require('fs') +const csv = require('csv-parser') +const createCsvWriter = require('csv-writer').createObjectCsvWriter + +const inputFilePath = 'input.csv' // Your input CSV +const outputFilePath = 'output_unique.csv' // Output with unique rows + +const seen = new Set() +const uniqueRows = [] + +fs.createReadStream(inputFilePath) + .pipe(csv()) + .on('data', (row) => { + const parentEntityId = row['parentEntityId'].trim() + const childEntityId = row['childEntityId'].trim() + const key = `${parentEntityId}-${childEntityId}` + + if (!seen.has(key)) { + seen.add(key) + uniqueRows.push({ parentEntityId, childEntityId }) + } + }) + .on('end', () => { + const csvWriter = createCsvWriter({ + path: outputFilePath, + header: [ + { id: 'parentEntityId', title: 'parentEntityId' }, + { id: 'childEntityId', title: 'childEntityId' }, + ], + }) + + csvWriter.writeRecords(uniqueRows).then(() => { + console.log(`โœ… Deduplicated data written to: ${outputFilePath}`) + }) + }) diff --git a/src/entityTypeAndEntityOnboarding/utilityScripts/splitEntities.js b/src/entityTypeAndEntityOnboarding/utilityScripts/splitEntities.js new file mode 100644 index 0000000..7514f9c --- /dev/null +++ b/src/entityTypeAndEntityOnboarding/utilityScripts/splitEntities.js @@ -0,0 +1,49 @@ +/** + * name : splitEntities.js + * author : Mallanagouda R Biradar + * created-date : 24-June-2025 + * Description : Split the entities based on the limit pass. + */ + +const fs = require('fs') +const csv = require('csv-parser') +const { createObjectCsvWriter } = require('csv-writer') + +const inputFilePath = 'input.csv' // Input file name +const rowsPerFile = 5000 // Split size +const allRows = [] + +let headers = [] + +// Step 1: Read CSV data +fs.createReadStream(inputFilePath) + .pipe(csv()) + .on('headers', (csvHeaders) => { + // Ensure fixed header names + headers = ['parentEntiyId', 'childEntityId'].map((header) => ({ + id: header, + title: header, + })) + }) + .on('data', (row) => { + // Optional: only keep required fields to avoid extra columns + allRows.push({ + parentEntiyId: row.parentEntiyId, + childEntityId: row.childEntityId, + }) + }) + .on('end', async () => { + const totalFiles = Math.ceil(allRows.length / rowsPerFile) + + for (let i = 0; i < totalFiles; i++) { + const chunk = allRows.slice(i * rowsPerFile, (i + 1) * rowsPerFile) + + const csvWriter = createObjectCsvWriter({ + path: `data${i + 1}.csv`, + header: headers, + }) + + await csvWriter.writeRecords(chunk) + console.log(`data${i + 1}.csv written with ${chunk.length} records`) + } + }) diff --git a/src/envVariables.js b/src/envVariables.js index 7ed3868..65f7d5c 100644 --- a/src/envVariables.js +++ b/src/envVariables.js @@ -47,6 +47,24 @@ let enviromentVariables = { optional: true, default: '../keycloakPublicKeys', }, + ADMIN_TOKEN_HEADER_NAME: { + message: 'Required admin access token header name', + optional: true, + default: 'admin-auth-token', + }, + ADMIN_ACCESS_TOKEN: { + message: 'Required admin access token', + optional: false, + }, + INTERFACE_SERVICE_URL: { + message: 'Required interface service url', + optional: false, + }, + USER_SERVICE_BASE_URL: { + message: 'Required user service base url', + optional: true, + default: '/user', + }, } let success = true diff --git a/src/generics/constants/api-responses.js b/src/generics/constants/api-responses.js index 644822a..d78e7f8 100644 --- a/src/generics/constants/api-responses.js +++ b/src/generics/constants/api-responses.js @@ -60,7 +60,20 @@ module.exports = { FIELD_MISSING: 'Fields are missing', ENTITY_TYPE_CREATION_FAILED: 'ENTITY TYPE CREATION FAILED', MAPPING_CSV_GENERATED: 'MAPPING_CSV_GENERATED', + INVALID_TENANT_AND_ORG_CODE: 'ERR_TENANT_AND_ORG_INVALID', + INVALID_TENANT_AND_ORG_MESSAGE: 'Invalid tenant and org info', + ORG_DETAILS_FETCH_UNSUCCESSFUL_CODE: 'ERR_ORG_DETAILS_NOT_FETCHED', + ORG_DETAILS_FETCH_UNSUCCESSFUL_MESSAGE: 'Org details fetch unsuccessful', KEYS_INDEXED_SUCCESSFULL: 'KEYS_INDEXED_SUCCESSFULL', KEYS_ALREADY_INDEXED_SUCCESSFULL: 'KEYS_ALREADY_INDEXED_SUCCESSFULL', NOT_VALID_ID_AND_EXTERNALID: 'NOT_VALID_ID_AND_EXTERNALID', + TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_MESSAGE: 'TenantId and OrgnizationId required in the token', + TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE: 'ERR_TENANTID_AND_ORGID_REQUIRED', + KEYS_INDEXED_SUCCESSFULL: 'KEYS_INDEXED_SUCCESSFULL', + KEYS_ALREADY_INDEXED_SUCCESSFULL: 'KEYS_ALREADY_INDEXED_SUCCESSFULL', + NOT_VALID_ID_AND_EXTERNALID: 'NOT_VALID_ID_AND_EXTERNALID', + TENANT_ID_MISSING_CODE: 'ERR_TENANT_ID_MISSING', + TENANT_ID_MISSING_MESSAGE: 'Require tenantId in header', + ROLE_PERMISSION_DENIED_ERR: 'ERR_INVALID_ROLE', + ROLE_PERMISSION_DENIED_MSG: 'Invalid role provided for resource creation', } diff --git a/src/generics/constants/common.js b/src/generics/constants/common.js index d02d4fd..5011c80 100644 --- a/src/generics/constants/common.js +++ b/src/generics/constants/common.js @@ -22,6 +22,9 @@ module.exports = { '/userRoleExtension/create', '/userRoleExtension/update', '/entities/createMappingCsv', + '/userRoleExtension/create', + '/userRoleExtension/update', + '/userRoleExtension/delete', '/admin/createIndex', ], SYSTEM: 'SYSTEM', @@ -37,5 +40,10 @@ module.exports = { KEYCLOAK_PUBLIC_KEY: 'keycloak_public_key', }, ENGLISH_LANGUGE_CODE: 'en', - GUEST_URLS: ['/entities/details'], + ADMIN_ROLE: 'admin', + ORG_ADMIN: 'org_admin', + TENANT_ADMIN: 'tenant_admin', + SERVER_TIME_OUT: 5000, + GUEST_URLS: ['/entities/details', '/entities/entityListBasedOnEntityType', 'entities/subEntityList'], + ALL: 'ALL', } diff --git a/src/generics/constants/endpoints.js b/src/generics/constants/endpoints.js index 0231da6..e3053ac 100644 --- a/src/generics/constants/endpoints.js +++ b/src/generics/constants/endpoints.js @@ -7,4 +7,6 @@ module.exports = { // End points to be added here + ORGANIZATION_READ: '/v1/organization/read', + TENANT_READ: '/v1/tenant/read', } diff --git a/src/generics/helpers/utils.js b/src/generics/helpers/utils.js index 884b827..07444bb 100644 --- a/src/generics/helpers/utils.js +++ b/src/generics/helpers/utils.js @@ -238,6 +238,72 @@ function generateUniqueId() { return uuidV4() } +// Helper function to convert mongo ids to objectIds to facilitate proper query in aggregate function +function convertMongoIds(query) { + const keysToConvert = ['_id', 'entityTypeId'] // Add other fields if needed + + const convertValue = (value) => { + if (Array.isArray(value)) { + return value.map((v) => (isValidObjectId(v) ? new ObjectId(v) : v)) + } else if (isValidObjectId(value)) { + return new ObjectId(value) + } + return value + } + + const isValidObjectId = (id) => { + return typeof id === 'string' && /^[a-fA-F0-9]{24}$/.test(id) + } + + const recurse = (obj) => { + for (const key in obj) { + if (keysToConvert.includes(key)) { + if (typeof obj[key] === 'object' && obj[key] !== null && '$in' in obj[key]) { + obj[key]['$in'] = convertValue(obj[key]['$in']) + } else { + obj[key] = convertValue(obj[key]) + } + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + recurse(obj[key]) + } + } + } + + recurse(query) + return query +} + +/** + * Strip orgIds from query object and log a warning. + * @function + * @name stripOrgIds + * @param {Object} query - The query object containing orgIds. + * @returns {Object} - The query object without orgIds. + * @deprecated orgIds is deprecated and should not be used in queries. + */ + +function stripOrgIds(query) { + const { orgIds, orgId, ...rest } = query + if (orgIds || orgId) { + console.warn('orgIds/orgId deprecated.') + } + return rest +} + +/** + * Convert an array of organization objects to an array of stringified org IDs. + * @function + * @name convertOrgIdsToString + * @param {Array<{code: number|string}>} array - Array of objects each containing a `code` property. + * @returns {string[]} - Array of stringified `code` values. + */ + +function convertOrgIdsToString(array) { + return array.map((data) => { + return data.code.toString() + }) +} + module.exports = { camelCaseToTitleCase: camelCaseToTitleCase, lowerCase: lowerCase, @@ -251,4 +317,7 @@ module.exports = { noOfElementsInArray: noOfElementsInArray, operatorValidation: operatorValidation, generateUniqueId: generateUniqueId, + convertMongoIds: convertMongoIds, + stripOrgIds: stripOrgIds, + convertOrgIdsToString: convertOrgIdsToString, } diff --git a/src/generics/middleware/authenticator.js b/src/generics/middleware/authenticator.js index 71f427b..a86006a 100644 --- a/src/generics/middleware/authenticator.js +++ b/src/generics/middleware/authenticator.js @@ -10,6 +10,7 @@ const jwt = require('jsonwebtoken') const isBearerRequired = process.env.IS_AUTH_TOKEN_BEARER === 'true' const path = require('path') const fs = require('fs') +const userService = require('../services/users') var respUtil = function (resp) { return { status: resp.errCode, @@ -57,13 +58,38 @@ module.exports = async function (req, res, next, token = '') { }) ) + // if (guestAccess == true && !req.body['tenantId']) { + // rspObj.errCode = CONSTANTS.apiResponses.TENANT_ID_MISSING_CODE + // rspObj.errMsg = CONSTANTS.apiResponses.TENANT_ID_MISSING_MESSAGE + // rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status + // return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) + // } + if (guestAccess == true && !token) { + if (!req.headers['tenantid']) { + rspObj.errCode = CONSTANTS.apiResponses.TENANT_ID_MISSING_CODE + rspObj.errMsg = CONSTANTS.apiResponses.TENANT_ID_MISSING_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status + return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) + } + req.userDetails = { + userInformation: { + tenantId: req.headers.tenantid, + organizationId: req.headers['orgId'] || 'ALL', + }, + } + next() return } let internalAccessApiPaths = CONSTANTS.common.INTERNAL_ACCESS_URLS let performInternalAccessTokenCheck = false + let adminHeader = false + if (process.env.ADMIN_ACCESS_TOKEN) { + adminHeader = req.headers[process.env.ADMIN_TOKEN_HEADER_NAME] + } + await Promise.all( internalAccessApiPaths.map(async function (path) { if (req.path.includes(path)) { @@ -84,14 +110,12 @@ module.exports = async function (req, res, next, token = '') { return } } - if (!token) { rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) } - // Check if a Bearer token is required for authentication if (isBearerRequired) { const [authType, extractedToken] = token.split(' ') @@ -108,6 +132,7 @@ module.exports = async function (req, res, next, token = '') { // <---- For Elevate user service user compactibility ----> let decodedToken = null + let userInformation = {} try { if (process.env.AUTH_METHOD === CONSTANTS.common.AUTH_METHOD.NATIVE) { try { @@ -178,27 +203,388 @@ module.exports = async function (req, res, next, token = '') { decodedToken = decodedToken || {} decodedToken['data'] = data } + + if (!decodedToken) { + rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE + rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status + return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) + } + + // Path to config.json + const configFilePath = path.resolve(__dirname, '../../config.json') + // Initialize variables + let configData = {} + let defaultTokenExtraction = false + + // Check if config.json exists + if (fs.existsSync(configFilePath)) { + // Read and parse the config.json file + const rawData = fs.readFileSync(configFilePath) + try { + configData = JSON.parse(rawData) + if (!configData.authTokenUserInformation) { + defaultTokenExtraction = true + } + configData = configData.authTokenUserInformation + } catch (error) { + console.error('Error parsing config.json:', error) + } + } else { + // If file doesn't exist, set defaultTokenExtraction to true + defaultTokenExtraction = true + } + + let organizationKey = 'organization_id' + + // Create user details to request + req.userDetails = { + userToken: token, + } + + // performing default token data extraction + if (defaultTokenExtraction) { + if (!decodedToken.data.organization_ids || !decodedToken.data.tenant_id) { + rspObj.errCode = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + rspObj.errMsg = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['bad_request'].status + return res.status(HTTP_STATUS_CODE['bad_request'].status).send(respUtil(rspObj)) + } + //here assuming that req.headers['orgid'] will be a single value if multiple passed first element of the array will be taken + let fetchSingleOrgIdFunc = await fetchSingleOrgIdFromProvidedData( + decodedToken.data.tenant_id.toString(), + decodedToken.data.organization_ids, + req.headers['orgid'], + token + ) + + if (!fetchSingleOrgIdFunc.success) { + return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(fetchSingleOrgIdFunc.errorObj)) + } + userInformation = { + userId: + typeof decodedToken.data.id == 'string' ? decodedToken.data.id : decodedToken.data.id.toString(), + userName: decodedToken.data.name, + organizationId: fetchSingleOrgIdFunc.orgId, + firstName: decodedToken.data.name, + roles: decodedToken.data.roles.map((role) => role.title), + tenantId: decodedToken.data.tenant_id.toString(), + } + } else { + for (let key in configData) { + if (configData.hasOwnProperty(key)) { + let keyValue = getNestedValue(decodedToken, configData[key]) + if (key == 'userId') { + keyValue = keyValue?.toString() + } + if (key === organizationKey) { + let value = getOrgId(req.headers, decodedToken, configData[key]) + userInformation[`organizationId`] = value.toString() + decodedToken.data[key] = value + continue + } + if (key === 'roles') { + let orgId = getOrgId(req.headers, decodedToken, configData[organizationKey]) + // Now extract roles using fully dynamic path + const rolePathTemplate = configData['roles'] + decodedToken.data[organizationKey] = orgId + const resolvedRolePath = resolvePathTemplate(rolePathTemplate, decodedToken.data) + const roles = getNestedValue(decodedToken, resolvedRolePath) || [] + userInformation[`${key}`] = roles + decodedToken.data[key] = roles + continue + } + + // For each key in config, assign the corresponding value from decodedToken + decodedToken.data[key] = keyValue + if (key == 'tenant_id') { + userInformation[`tenantId`] = keyValue.toString() + } else { + userInformation[`${key}`] = keyValue + } + } + } + if (userInformation.roles && Array.isArray(userInformation.roles) && userInformation.roles.length) { + userInformation.roles = userInformation.roles.map((role) => role.title) + } + } + + // throw error if tenant_id or organization_id is not present in the decoded token + if ( + !decodedToken.data.tenant_id || + !(decodedToken.data.tenant_id.toString().length > 0) || + !decodedToken.data.organization_id || + !(decodedToken.data.organization_id.toString().length > 0) + ) { + rspObj.errCode = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + rspObj.errMsg = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['bad_request'].status + return res.status(HTTP_STATUS_CODE['bad_request'].status).send(respUtil(rspObj)) + } + + /** + * Validate if provided orgId(s) belong to the tenant by checking against related_orgs. + * + * @param {String} tenantId - ID of the tenant + * @param {String} orgId - Comma separated string of org IDs or 'ALL' + * @param {String} token - The authentication token + * @returns {Object} - Success with validOrgIds array or failure with error object + */ + async function validateIfOrgsBelongsToTenant(tenantId, orgId, token) { + let orgIdArr = Array.isArray(orgId) ? orgId : typeof orgId === 'string' ? orgId.split(',') : [] + let orgDetails = await userService.fetchTenantDetails(tenantId, token) + let validOrgIds = null + + if (orgIdArr.includes('ALL') || orgIdArr.includes('all')) { + validOrgIds = ['ALL'] + } else { + if ( + !orgDetails?.success || + !orgDetails?.data || + Object.keys(orgDetails.data).length === 0 || + !Array.isArray(orgDetails.data.organizations) || + orgDetails.data.organizations.length === 0 + ) { + let errorObj = {} + errorObj.errCode = CONSTANTS.apiResponses.ORG_DETAILS_FETCH_UNSUCCESSFUL_CODE + errorObj.errMsg = CONSTANTS.apiResponses.ORG_DETAILS_FETCH_UNSUCCESSFUL_MESSAGE + errorObj.responseCode = HTTP_STATUS_CODE['bad_request'].status + return { success: false, errorObj: errorObj } + } + + orgDetails.data.related_orgs = orgDetails.data.organizations.map((data) => { + return data.code.toString() + }) + // aggregate valid orgids + + let relatedOrgIds = orgDetails.data.related_orgs + + validOrgIds = orgIdArr.filter((id) => relatedOrgIds.includes(id)) + + if (!(validOrgIds.length > 0)) { + rspObj.errCode = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + rspObj.errMsg = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['bad_request'].status + return res.status(HTTP_STATUS_CODE['bad_request'].status).send(respUtil(rspObj)) + } + } + + return { success: true, validOrgIds: validOrgIds } + } + + /** + * Fetches a valid orgId from the provided data, checking if it's valid for the given tenant. + * + * @param {String} tenantId - ID of the tenant + * @param {String[]} orgIdArr - Array of orgIds to choose from + * @param {String} orgIdFromHeader - The orgId provided in the request headers + * @param {String} token - The authentication token + * @returns {Promise} - Returns a promise resolving to an object containing the success status, orgId, or error details + */ + async function fetchSingleOrgIdFromProvidedData(tenantId, orgIdArr, orgIdFromHeader, token) { + try { + // Check if orgIdFromHeader is provided and valid + if (orgIdFromHeader && orgIdFromHeader != '') { + if (!orgIdArr.includes(orgIdFromHeader)) { + throw CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + } + + let validateOrgsResult = await validateIfOrgsBelongsToTenant(tenantId, orgIdFromHeader, token) + + if (!validateOrgsResult.success) { + throw CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + } + + return { success: true, orgId: orgIdFromHeader } + } + + // If orgIdFromHeader is not provided, check orgIdArr + if (orgIdArr.length > 0) { + return { success: true, orgId: orgIdArr[0] } + } + + // If no orgId is found, throw error + throw CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + } catch (err) { + // Handle error when no valid orgId is found + if (orgIdArr.length > 0) { + return { success: true, orgId: orgIdArr[0] } + } + + rspObj.errCode = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_CODE + rspObj.errMsg = CONSTANTS.apiResponses.TENANTID_AND_ORGID_REQUIRED_IN_TOKEN_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['bad_request'].status + return res.status(HTTP_STATUS_CODE['bad_request'].status).send(respUtil(rspObj)) + } + } + + /** + * Extract tenantId and orgId from incoming request or decoded token. + * + * Priority order: body -> query -> headers -> decoded token data + * + * @param {Object} req - Express request object + * @param {Object} decodedTokenData - Decoded JWT token data + * @returns {Object} - Success with tenantId and orgId or failure object + */ + function getTenantIdAndOrgIdFromTheTheReqIntoHeaders(req, decodedTokenData) { + // Step 1: Check in the request body + if (req.body && req.body.tenantId && req.body.orgId) { + return { success: true, tenantId: req.body.tenantId, orgId: req.body.orgId } + } + + // Step 2: Check in query parameters if not found in body + if (req.query.tenantId && req.query.orgId) { + return { success: true, tenantId: req.query.tenantId, orgId: req.query.orgId } + } + + // Step 3: Check in headers if not found in query params + if (req.headers['tenantid'] && req.headers['orgid']) { + return { success: true, tenantId: req.headers['tenantid'], orgId: req.headers['orgid'] } + } + + // Step 4: Check in user token (already decoded) if still not found + if (decodedTokenData && decodedTokenData.tenantId && decodedTokenData.orgId) { + return { success: true, tenantId: decodedTokenData.tenantId, orgId: decodedTokenData.orgId } + } + + return { sucess: false } + } + + let userRoles = decodedToken.data.roles.map((role) => role.title) + + if (performInternalAccessTokenCheck) { + decodedToken.data['tenantAndOrgInfo'] = {} + // validate SUPER_ADMIN + if (adminHeader) { + if (adminHeader != process.env.ADMIN_ACCESS_TOKEN) { + return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) + } + decodedToken.data.roles.push({ title: CONSTANTS.common.ADMIN_ROLE }) + + let result = getTenantIdAndOrgIdFromTheTheReqIntoHeaders(req, decodedToken.data) + if (!result.success) { + rspObj.errCode = reqMsg.ADMIN_TOKEN.MISSING_CODE + rspObj.errMsg = reqMsg.ADMIN_TOKEN.MISSING_MESSAGE + rspObj.responseCode = responseCode.unauthorized.status + return res.status(responseCode.unauthorized.status).send(respUtil(rspObj)) + } + + req.headers['tenantid'] = result.tenantId + req.headers['orgid'] = result.orgId + + let validateOrgsResult = await validateIfOrgsBelongsToTenant( + req.headers['tenantid'], + req.headers['orgid'], + token + ) + + if (!validateOrgsResult.success) { + return res + .status(HTTP_STATUS_CODE['unauthorized'].status) + .send(respUtil(validateOrgsResult.errorObj)) + } + + req.headers['orgid'] = validateOrgsResult.validOrgIds + } else if (userRoles.includes(CONSTANTS.common.TENANT_ADMIN)) { + req.headers['tenantid'] = decodedToken.data.tenant_id.toString() + + let orgId = req.body.orgId || req.headers['orgid'] + + if (!orgId) { + rspObj.errCode = CONSTANTS.apiResponses.INVALID_TENANT_AND_ORG_CODE + rspObj.errMsg = CONSTANTS.apiResponses.INVALID_TENANT_AND_ORG_MESSAGE + rspObj.responseCode = HTTP_STATUS_CODE['bad_request'].status + return res.status(HTTP_STATUS_CODE['bad_request'].status).send(respUtil(rspObj)) + } + + req.headers['orgid'] = orgId + + let validateOrgsResult = await validateIfOrgsBelongsToTenant( + req.headers['tenantid'], + req.headers['orgid'], + token + ) + if (!validateOrgsResult.success) { + return res.status(responseCode['unauthorized'].status).send(respUtil(validateOrgsResult.errorObj)) + } + req.headers['orgid'] = validateOrgsResult.validOrgIds + } else if (userRoles.includes(CONSTANTS.common.ORG_ADMIN)) { + req.headers['tenantid'] = decodedToken.data.tenant_id.toString() + req.headers['orgid'] = [decodedToken.data.organization_id.toString()] + } else { + rspObj.errCode = CONSTANTS.apiResponses.ROLE_PERMISSION_DENIED_ERR + rspObj.errMsg = CONSTANTS.apiResponses.ROLE_PERMISSION_DENIED_MSG + return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) + } + + decodedToken.data.tenantAndOrgInfo['tenantId'] = req.headers['tenantid'].toString() + decodedToken.data.tenantAndOrgInfo['orgId'] = req.headers['orgid'] + } } catch (err) { rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) } - if (!decodedToken) { - rspObj.errCode = CONSTANTS.apiResponses.TOKEN_MISSING_CODE - rspObj.errMsg = CONSTANTS.apiResponses.TOKEN_MISSING_MESSAGE - rspObj.responseCode = HTTP_STATUS_CODE['unauthorized'].status - return res.status(HTTP_STATUS_CODE['unauthorized'].status).send(respUtil(rspObj)) - } req.userDetails = { userToken: token, - userInformation: { - userId: typeof decodedToken.data.id == 'string' ? decodedToken.data.id : decodedToken.data.id.toString(), - userName: decodedToken.data.name, - // email : decodedToken.data.email, //email is removed from token - firstName: decodedToken.data.name, - roles: decodedToken.data.roles.map((role) => role.title), - }, + userInformation: userInformation, + } + // add tenantAndOrgInfo to req object only for admin(s) + if (decodedToken.data.tenantAndOrgInfo) { + req.userDetails.tenantAndOrgInfo = decodedToken.data.tenantAndOrgInfo + } + + // Helper function to access nested properties + function getOrgId(headers, decodedToken, orgConfigData) { + if (headers['organization_id']) { + return (orgId = headers['organization_id'].toString()) + } else { + const orgIdPath = orgConfigData + return (orgId = getNestedValue(decodedToken, orgIdPath)?.toString()) + } + } + + function getNestedValue(obj, path) { + const parts = path.split('.') + let current = obj + + for (const part of parts) { + if (!current) return undefined + + // Conditional match: key[?field=value] + const conditionalMatch = part.match(/^(\w+)\[\?(\w+)=([^\]]+)\]$/) + if (conditionalMatch) { + const [, arrayKey, field, expected] = conditionalMatch + const array = current[arrayKey] + if (!Array.isArray(array)) return undefined + const found = array.find((item) => String(item[field]) === String(expected)) + if (!found) return undefined + current = found + continue + } + + // Index match: key[0] + const indexMatch = part.match(/^(\w+)\[(\d+)\]$/) + if (indexMatch) { + const [, key, index] = indexMatch + const array = current[key] + if (!Array.isArray(array)) return undefined + current = array[parseInt(index, 10)] + continue + } + + current = current[part] + } + return current + } + + function resolvePathTemplate(template, contextObject) { + return template.replace(/\{\{(.*?)\}\}/g, (_, path) => { + const value = getNestedValue(contextObject, path.trim()) + return value?.toString?.() ?? '' + }) } next() } diff --git a/src/generics/services/users.js b/src/generics/services/users.js new file mode 100644 index 0000000..c1d6226 --- /dev/null +++ b/src/generics/services/users.js @@ -0,0 +1,115 @@ +const request = require('request') + +const interfaceServiceUrl = process.env.INTERFACE_SERVICE_URL + +const fetchOrgDetails = function (organisationIdentifier, userToken) { + return new Promise(async (resolve, reject) => { + try { + let url + if (!isNaN(organisationIdentifier)) { + url = + interfaceServiceUrl + + process.env.USER_SERVICE_BASE_URL + + CONSTANTS.endpoints.ORGANIZATION_READ + + '?organisation_id=' + + organisationIdentifier + } else { + url = + interfaceServiceUrl + + process.env.USER_SERVICE_BASE_URL + + CONSTANTS.endpoints.ORGANIZATION_READ + + '?organisation_code=' + + organisationIdentifier + } + const options = { + headers: { + internal_access_token: process.env.INTERNAL_ACCESS_TOKEN, + }, + } + request.get(url, options, userReadCallback) + let result = { + success: true, + } + function userReadCallback(err, data) { + if (err) { + result.success = false + } else { + let response = JSON.parse(data.body) + if (response.responseCode === HTTP_STATUS_CODE['ok'].code) { + result['data'] = response.result + result.success = true + } else { + result.success = false + } + } + return resolve(result) + } + setTimeout(function () { + return resolve( + (result = { + success: false, + }) + ) + }, CONSTANTS.common.SERVER_TIME_OUT) + } catch (error) { + return reject(error) + } + }) +} +/** + * Fetches the tenant details for a given tenant ID along with org it is associated with. + * @param {string} tenantId - The code/id of the organization. + * @param {String} userToken - user token + * @returns {Promise} A promise that resolves with the organization details or rejects with an error. + */ + +const fetchTenantDetails = function (tenantId, userToken) { + return new Promise(async (resolve, reject) => { + try { + let url = + interfaceServiceUrl + + process.env.USER_SERVICE_BASE_URL + + CONSTANTS.endpoints.TENANT_READ + + '/' + + tenantId + const options = { + headers: { + 'content-type': 'application/json', + 'X-auth-token': userToken, + }, + } + + request.get(url, options, userReadCallback) + let result = { + success: true, + } + function userReadCallback(err, data) { + if (err) { + result.success = false + } else { + let response = JSON.parse(data.body) + if (response.responseCode === HTTP_STATUS_CODE['ok'].code) { + result['data'] = response.result + result.success = true + } else { + result.success = false + } + } + return resolve(result) + } + setTimeout(function () { + return resolve( + (result = { + success: false, + }) + ) + }, CONSTANTS.common.SERVER_TIME_OUT) + } catch (error) { + return reject(error) + } + }) +} +module.exports = { + fetchOrgDetails: fetchOrgDetails, + fetchTenantDetails: fetchTenantDetails, +} diff --git a/src/healthCheck/README.md b/src/healthCheck/README.md new file mode 100644 index 0000000..a76aae8 --- /dev/null +++ b/src/healthCheck/README.md @@ -0,0 +1,146 @@ +# ๐Ÿฉบ Health Check Configuration Guide + +This project uses the `elevate-project-services-health-check` package to monitor the health of various services like databases, message brokers, and internal microservices. + +To enable this, create a configuration file named `health.config.js`. This file defines **what to check**, **how to check it**, and **what constitutes a healthy response**. + +--- + +## ๐Ÿ“ File Structure + +```bash +healthCheck/ +โ”œโ”€โ”€ health.config.js # โœ… Your health check configuration +โ””โ”€โ”€ ... +``` + +--- + +## โœ… Basic Structure + +```js +module.exports = { + name: 'YourServiceName', + version: '1.0.0', + checks: { + // Define checks here + }, +} +``` + +--- + +## ๐Ÿงพ Top-Level Keys + +| Key | Type | Required | Description | +| --------- | -------- | -------- | ---------------------------------------------------------------- | +| `name` | `string` | โœ… | Name of the service. Displayed in the health check response. | +| `version` | `string` | โœ… | Current version of the service. Useful for tracking deployments. | +| `checks` | `object` | โœ… | Contains configuration for all enabled health checks. | + +--- + +## ๐Ÿ” `checks` Object + +This is the heart of your config. It allows you to define **which components to monitor** and **how**. + +### ๐Ÿงฉ Supported Built-in Checks + +Each service has the following structure: + +```js +: { + enabled: true, + url: process.env.SERVICE_URL, +} +``` + +### โœ… Common Services + +| Service | Purpose | Notes | +| ----------- | ------------------------------- | -------------------------------------------- | +| `mongodb` | Check MongoDB connection | `url` must point to a valid MongoDB URI | +| `postgres` | Check PostgreSQL database | Example: `postgres://user:pass@host:port/db` | +| `redis` | Check Redis connectivity | Can be local or remote | +| `kafka` | Check Kafka producer & consumer | Broker URL must be reachable | +| `gotenberg` | Check PDF conversion service | URL to Gotenberg's health endpoint | + +--- + +## ๐Ÿ” Microservices Health Checks + +To validate dependent microservices, use the `microservices` array. + +```js +microservices: [ + { + name: 'ServiceName', + url: 'https://host/health', + enabled: true, + request: { + method: 'GET', + header: {}, + body: {}, + }, + expectedResponse: { + status: 200, + 'result.healthy': true, + 'meta.ok': 'yes', + }, + }, +] +``` + +### ๐Ÿง  Notes on `expectedResponse` + +- Supports **deep key matching** using dot notation (e.g., `result.healthy`) +- All keys must match their expected values +- If any value does not match, the service is marked unhealthy + +--- + +## ๐Ÿ“Œ Example `.env` Usage (Recommended) + +```env +MONGODB_URL=mongodb://localhost:27017/mydb +POSTGRES_URL=postgres://user:pass@localhost:5432/mydb +GOTENBERG_URL=http://localhost:3000 +KAFKA_URL=kafka://localhost:9092 +SURVEY_SERVICE_URL=http://localhost:4001/survey/health +``` + +--- + +## ๐Ÿšจ Best Practices + +- โœ… Always keep `enabled: true` only for services currently in use. +- โœ… Use environment variables to avoid hardcoding URLs and credentials. +- โœ… Validate your config during startup using a helper like `validateHealthConfig(config)`. +- ๐Ÿ›‘ Do not include sensitive tokens or secrets directly in the config. + +--- + +## โœ… Minimal Valid Configuration + +```js +module.exports = { + name: 'MyService', + version: '1.0.0', + checks: { + mongodb: { + enabled: true, + url: process.env.MONGODB_URL, + }, + redis: { + enabled: false, + }, + microservices: [], + }, +} +``` + +--- + +## ๐Ÿ“ž Need More? + +Supports Kafka send/receive, Redis ping, MongoDB & Postgres connectivity, HTTP validation for microservices, and response structure validation. diff --git a/src/healthCheck/health-check.js b/src/healthCheck/health-check.js index e3732cb..e6eba76 100644 --- a/src/healthCheck/health-check.js +++ b/src/healthCheck/health-check.js @@ -1,76 +1,61 @@ /** - * name : health.js. - * author : Priyanka Pradeep - * created-date : 21-Mar-2024 + * name : health-check.js. + * author : Mallanagouda R Biradar + * created-date : 30-Jun-2025 * Description : Health check helper functionality. -*/ + */ // Dependencies -const mongodb = require("./mongodb") +const { healthCheckHandler } = require('elevate-services-health-check') +const healthCheckConfig = require('./health.config') const { v1: uuidv1 } = require('uuid') -const obj = { - MONGO_DB: { - NAME: 'Mongo.db', - FAILED_CODE: 'MONGODB_HEALTH_FAILED', - FAILED_MESSAGE: 'Mongo db is not connected' - }, - NAME: 'EntityServiceHealthCheck', - API_VERSION: '1.0' -} - let health_check = async function (req, res) { - - let checks = [] - let mongodbConnection = await mongodb.health_check() - checks.push(singleCheckObj("MONGO_DB", mongodbConnection)) - - - let checkServices = checks.filter(check => check.healthy === false) - - let result = { - name: obj.NAME, - version: obj.API_VERSION, - healthy: checkServices.length > 0 ? false : true, - checks: checks - } - - let responseData = response(req, result) - res.status(200).json(responseData) + try { + const response = await healthCheckHandler(healthCheckConfig, req.query.basicCheck, req.query.serviceName) + res.status(200).json(response) + } catch (err) { + console.error('Health config validation failed:', err.message || err) + res.status(400).json({ + id: 'entityService.Health.API', + ver: '1.0', + ts: new Date(), + params: { + resmsgid: uuidv1(), + msgid: req.headers['msgid'] || req.headers.msgid || uuidv1(), + status: 'failed', + err: 'CONFIG_VALIDATION_ERROR', + errMsg: err.message || 'Invalid config', + }, + status: 400, + result: {}, + }) + } } let healthCheckStatus = function (req, res) { - let responseData = response(req) - res.status(200).json(responseData) -} - -let singleCheckObj = function (serviceName, isHealthy) { - return { - name: obj[serviceName].NAME, - healthy: isHealthy, - err: !isHealthy ? obj[serviceName].FAILED_CODE : "", - errMsg: !isHealthy ? obj[serviceName].FAILED_MESSAGE : "" - } + let responseData = response(req) + res.status(200).json(responseData) } let response = function (req, result) { - return { - "id": "improvementService.Health.API", - "ver": "1.0", - "ts": new Date(), - "params": { - "resmsgid": uuidv1(), - "msgid": req.headers['msgid'] || req.headers.msgid || uuidv1(), - "status": "successful", - "err": "null", - "errMsg": "null" - }, - "status": 200, - result: result - } + return { + id: 'Entity.Management.service.Health.API', + ver: '1.0', + ts: new Date(), + params: { + resmsgid: uuidv1(), + msgid: req.headers['msgid'] || req.headers.msgid || uuidv1(), + status: 'successful', + err: 'null', + errMsg: 'null', + }, + status: 200, + result: result, + } } module.exports = { - health_check: health_check, - healthCheckStatus: healthCheckStatus -} \ No newline at end of file + health_check: health_check, + healthCheckStatus: healthCheckStatus, +} diff --git a/src/healthCheck/health.config.js b/src/healthCheck/health.config.js new file mode 100644 index 0000000..459d428 --- /dev/null +++ b/src/healthCheck/health.config.js @@ -0,0 +1,35 @@ +/** + * name : health.config.js. + * author : Mallanagouda R Biradar + * created-date : 30-Jun-2025 + * Description : Health check config file + */ + +module.exports = { + name: process.env.SERVICE_NAME, + version: '1.0.0', + checks: { + mongodb: { + enabled: true, + url: process.env.MONGODB_URL, + }, + microservices: [ + { + name: 'UserService', + url: `${process.env.USER_SERVICE_URL}/user/health?serviceName=${process.env.SERVICE_NAME}`, + enabled: true, + request: { + method: 'GET', + header: {}, + body: {}, + }, + + expectedResponse: { + status: 200, + 'params.status': 'successful', + 'result.healthy': true, + }, + }, + ], + }, +} diff --git a/src/healthCheck/index.js b/src/healthCheck/index.js index 3fcab3c..680695b 100644 --- a/src/healthCheck/index.js +++ b/src/healthCheck/index.js @@ -3,11 +3,26 @@ * author : Aman Karki. * created-date : 01-Feb-2021. * Description : Health check Root file. -*/ + */ -let healthCheckService = require("./health-check") +let healthCheckService = require('./health-check') module.exports = function (app) { - app.get("/health", healthCheckService.health_check) - app.get("/healthCheckStatus", healthCheckService.healthCheckStatus) -} \ No newline at end of file + app.get('/health', async (req, res) => { + try { + await healthCheckService.health_check(req, res) + } catch (err) { + console.error('Health check failed:', err.message || err) + res.status(500).json({ healthy: false, message: err.message || 'Internal Server Error' }) + } + }) + + app.get('/healthCheckStatus', async (req, res) => { + try { + await healthCheckService.healthCheckStatus(req, res) + } catch (err) { + console.error('HealthCheckStatus failed:', err.message || err) + res.status(500).json({ healthy: false, message: err.message || 'Internal Server Error' }) + } + }) +} diff --git a/src/healthCheck/mongodb.js b/src/healthCheck/mongodb.js deleted file mode 100644 index efcf57c..0000000 --- a/src/healthCheck/mongodb.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * name : mongodb.js. - * author : Aman Karki. - * created-date : 01-Feb-2021. - * Description : Mongodb health check functionality. -*/ - -// Dependencies - -const mongoose = require("mongoose") - -function health_check() { - return new Promise(async (resolve, reject) => { - - const db = mongoose.createConnection( - process.env.MONGODB_URL + ":" + process.env.MONGODB_PORT + "/" + process.env.MONGODB_DATABASE_NAME - ) - - db.on("error", function () { - return resolve(false) - }) - db.once("open", function () { - db.close(function () { }) - return resolve(true) - }) - }) -} - -module.exports = { - health_check: health_check -} \ No newline at end of file diff --git a/src/migrations/addTargetedEntityTypesInEntitiesCollection.js b/src/migrations/addTargetedEntityTypesInEntitiesCollection.js new file mode 100644 index 0000000..c3eb1c4 --- /dev/null +++ b/src/migrations/addTargetedEntityTypesInEntitiesCollection.js @@ -0,0 +1,143 @@ +require('dotenv').config({ path: '../.env' }) +const path = require('path') +const { MongoClient } = require('mongodb') + +const MONGODB_URL = process.env.MONGODB_URL +const dbClient = new MongoClient(MONGODB_URL, { useUnifiedTopology: true }) + +let professionalSubRolesInfo = { + 'student-preschool-class-2': ['school'], + 'student-class-1-5': ['school'], + 'student-class-3-5': ['school'], + 'student-class-6-8': ['school'], + 'student-class-6-10': ['school'], + 'student-class-9-10': ['school'], + 'student-class-11-12': ['school'], + 'student-class-8-10': ['school'], + 'student-higher-education': ['school'], + 'student-pre-service-teacher': ['school'], + 'teacher-preschool-class-2': ['school'], + 'teacher-class-1-5': ['school'], + 'teacher-class-3-5': ['school'], + 'teacher-class-6-8': ['school'], + 'teacher-class-6-10': ['school'], + 'teacher-class-9-10': ['school'], + 'teacher-class-11-12': ['school'], + 'special-educators': ['school'], + 'physical-education-teacher': ['school'], + 'art-music-performing-teacher': ['school'], + counsellor: ['school'], + 'warden-caretaker': ['school'], + 'anganwadi-worker': ['school'], + 'anganwadi-helper': ['school'], + librarian: ['school'], + 'technician-lab-it': ['school'], + 'principal-head-teacher': ['school'], + 'vice-principal-asst-head-teacher': ['school'], + 'head-teacher-incharge': ['school'], + 'teacher-educator-SCERT': ['school'], + 'teacher-educator-DIET': ['school'], + 'teacher-educator-IASE': ['school'], + 'teacher-educator-univ-deptt': ['school'], + 'teacher-educator-TEI': ['school'], + 'teacher-educator-SIET': ['school'], + 'teacher-educator-CTE': ['school'], + 'teacher-educator-BASIC': ['school'], + 'block-resource-centre-coordinator-BRCC': ['block', 'cluster', 'school'], + 'cluster-resource-centre-oordinator-CRCC': ['cluster', 'school'], + 'state-coordinator': ['state', 'district', 'block', 'cluster', 'school'], + 'district-coordinator': ['district', 'block', 'cluster', 'school'], + 'assistant-district-coordinator': ['district', 'block', 'cluster', 'school'], + coordinator: ['school'], + 'mentor-advisor': ['school'], + 'resource-person-state-district-block': ['state', 'district', 'block', 'cluster', 'school'], + 'shikshak-sankul': ['school'], + 'principal-secretary-commissioner-secretary-school-education': ['school'], + 'additional-secretary-commissioner-school-education': ['school'], + 'joint-secretary-education': ['school'], + 'assistant-commissioner': ['school'], + 'additional-director': ['school'], + 'director-public-instructions-elementary-secondary': ['school'], + 'project-director-SPD': ['state', 'district', 'block', 'cluster', 'school'], + 'joint-director': ['school'], + 'assistant-state-project-director': ['state', 'district', 'block', 'cluster', 'school'], + 'additional-state-project-director': ['state', 'district', 'block', 'cluster', 'school'], + 'director-basic': ['school'], + 'head-autonomous-organization': ['school'], + 'director-SCERT': ['state', 'district', 'block', 'cluster', 'school'], + 'principal-DIET': ['school'], + 'collector-DM-DC': ['school'], + 'head-state-training-center': ['state', 'district', 'block', 'cluster', 'school'], + 'education-officer': ['school'], + 'chief-education-officer': ['state', 'district', 'block', 'cluster', 'school'], + 'district-education-officer-DEO': ['district', 'block', 'cluster', 'school'], + 'block-ducation-fficer-BEO': ['block', 'cluster', 'school'], + 'MIS-coordinator': ['school'], + 'subject-inspector': ['school'], + 'evaluation-officer': ['school'], + 'extension-officer': ['school'], + 'CDPO-child-development-project-officer': ['school'], + supervisor: ['school'], + 'program-officer': ['school'], + 'basic-shiksha-adhikari': ['school'], + 'director-primary-education': ['school'], + 'Desk-officer-education': ['school'], + 'director-secondary-and-higher-secondary-education': ['school'], + 'director-scheme': ['school'], + 'director-balbharati': ['school'], + 'director-state-education-board': ['state', 'district', 'block', 'cluster', 'school'], +} + +async function runMigration() { + try { + await dbClient.connect() + const db = dbClient.db() + + for (const [externalId, entityTypeNames] of Object.entries(professionalSubRolesInfo)) { + if (!entityTypeNames.length) continue + + // Step 1: Fetch all matching entities (with that externalId) + const entityDocs = await db + .collection('entities') + .find({ 'metaInformation.externalId': externalId }) + .toArray() + for (const entity of entityDocs) { + const tenantId = entity.tenantId + + // Step 2: Fetch matching entityTypes for this tenant + const matchingEntityTypes = await db + .collection('entityTypes') + .find( + { + name: { $in: entityTypeNames }, + tenantId: tenantId, + }, + { projection: { _id: 1, name: 1 } } + ) + .toArray() + + const targetedEntityTypes = matchingEntityTypes.map((type) => ({ + entityType: type.name, + entityTypeId: type._id.toString(), + })) + if (targetedEntityTypes.length) { + const updateResult = await db + .collection('entities') + .updateOne( + { _id: entity._id }, + { $set: { 'metaInformation.targetedEntityTypes': targetedEntityTypes } } + ) + console.log(`Updated entity ${entity._id} for tenant ${tenantId}`) + } else { + console.warn(`No matching entityTypes found for entity ${entity._id} (tenantId: ${tenantId})`) + } + } + } + } catch (err) { + console.error('Migration error:', err) + } finally { + await dbClient.close() + } +} + +runMigration() diff --git a/src/migrations/fixOrgIdsFromCollections.js b/src/migrations/fixOrgIdsFromCollections.js new file mode 100644 index 0000000..2d8be49 --- /dev/null +++ b/src/migrations/fixOrgIdsFromCollections.js @@ -0,0 +1,92 @@ +/** + * name : fixOrgIdsFromCollections.js + * author : Saish R B + * created-date : 26-May-2025 + * Description : Migration script to update orgIds to scope + */ + +require('dotenv').config({ path: '../.env' }) +const { MongoClient } = require('mongodb') +const MONGODB_URL = process.env.MONGODB_URL + +if (!MONGODB_URL) { + throw new Error('Missing MONGODB_URL or DB in environment variables') +} + +const dbClient = new MongoClient(MONGODB_URL) + +const BATCH_SIZE = 100 + +async function modifyCollection(collectionName) { + console.log(`Starting migration for collection: ${collectionName}`) + + const db = dbClient.db() + const collection = db.collection(collectionName) + + const cursor = collection.find( + { + orgIds: { $exists: true, $type: 'array' }, + }, + { + projection: { _id: 1, orgIds: 1 }, + } + ) + + let batch = [] + + while (await cursor.hasNext()) { + try { + console.log(`processing for collection: ${collectionName}`) + const doc = await cursor.next() + console.log(`Processing document with _id: ${doc._id}`) + if (!doc.orgIds || doc.orgIds.length === 0) { + console.log(`Skipping _id: ${doc._id} - empty orgIds`) + continue + } + + batch.push({ + updateOne: { + filter: { _id: doc._id }, + update: { + $unset: { orgIds: '' }, + $set: { orgId: doc.orgIds[0] }, + }, + }, + }) + + // Process batch + if (batch.length >= BATCH_SIZE) { + await collection.bulkWrite(batch, { ordered: false }) + console.log(`Processed ${batch.length} docs in ${collectionName}`) + batch = [] + } + } catch (err) { + console.log(err, '<-- Error processing document') + } + } + + // Final remaining batch + if (batch.length > 0) { + await collection.bulkWrite(batch, { ordered: false }) + console.log(`Processed remaining ${batch.length} docs in ${collectionName}`) + } + + console.log(`Collection "${collectionName}" migration completed.`) +} + +async function runMigration() { + try { + await dbClient.connect() + const COLLECTIONS = ['entities', 'entityTypes', 'userRoleExtension'] + + for (const collection of COLLECTIONS) { + await modifyCollection(collection) + } + } catch (err) { + console.error('Migration failed:', err) + } finally { + await dbClient.close() + } +} + +runMigration() diff --git a/src/models/entities.js b/src/models/entities.js index 1fdeab8..8efbfc6 100644 --- a/src/models/entities.js +++ b/src/models/entities.js @@ -15,8 +15,9 @@ module.exports = { }, groups: Object, metaInformation: { - externalId: { type: String, index: true, unique: true }, + externalId: { type: String, index: true }, name: { type: String, index: true }, + targetedEntityTypes: { type: Array }, }, childHierarchyPath: Array, userId: { @@ -37,5 +38,21 @@ module.exports = { type: String, default: 'SYSTEM', }, + tenantId: { + type: String, + index: true, + require: true, + }, + orgId: { + type: String, + require: true, + index: true, + }, }, + compoundIndex: [ + { + name: { 'metaInformation.externalId': 1, tenantId: 1 }, + indexType: { unique: true }, + }, + ], } diff --git a/src/models/entityTypes.js b/src/models/entityTypes.js index c5641c5..6a23dd8 100644 --- a/src/models/entityTypes.js +++ b/src/models/entityTypes.js @@ -10,8 +10,6 @@ module.exports = { schema: { name: { type: String, - index: true, - unique: true, }, profileForm: Array, profileFields: Array, @@ -34,5 +32,21 @@ module.exports = { type: String, default: 'SYSTEM', }, + tenantId: { + type: String, + index: true, + require: true, + }, + orgId: { + type: String, + require: true, + index: true, + }, }, + compoundIndex: [ + { + name: { name: 1, tenantId: 1 }, + indexType: { unique: true }, + }, + ], } diff --git a/src/models/userRoleExtension.js b/src/models/userRoleExtension.js index 03dfa95..2e800d6 100644 --- a/src/models/userRoleExtension.js +++ b/src/models/userRoleExtension.js @@ -10,7 +10,6 @@ module.exports = { schema: { userRoleId: { type: String, - unique: true, }, title: { type: String, @@ -22,6 +21,7 @@ module.exports = { }, entityTypes: [ { + _id: false, entityType: { type: String }, entityTypeId: { type: String, index: true }, }, @@ -38,5 +38,21 @@ module.exports = { type: String, index: true, }, + tenantId: { + type: String, + index: true, + require: true, + }, + orgId: { + type: String, + require: true, + index: true, + }, }, + compoundIndex: [ + { + name: { userRoleId: 1, tenantId: 1 }, + indexType: { unique: true }, + }, + ], } diff --git a/src/module/entities/helper.js b/src/module/entities/helper.js index 2fc3d55..962ff24 100644 --- a/src/module/entities/helper.js +++ b/src/module/entities/helper.js @@ -93,10 +93,11 @@ module.exports = class UserProjectsHelper { * @method * @name createMappingCsv * @param {Array} entityCSVData - Array of objects parsed from the input CSV file. + * @param {String} tenantId - Tenant ID for the user. * @returns {Promise} Resolves with an object containing: */ - static async createMappingCsv(entityCSVData) { + static async createMappingCsv(entityCSVData, tenantId) { return new Promise(async (resolve, reject) => { try { const parentEntityIds = [] @@ -113,6 +114,7 @@ module.exports = class UserProjectsHelper { // Filter criteria to fetch entity documents based on entity type and external ID const filter = { 'metaInformation.externalId': value, + tenantId: tenantId, } const entityDocuments = await entitiesQueries.entityDocuments(filter, ['_id']) @@ -177,17 +179,22 @@ module.exports = class UserProjectsHelper { * List of Entities * @method * @name listByEntityIds - * @param bodyData - Body data. + * @param {Array} entityIds + * @param {Array} fields + * @param {Object} userDetails - user's loggedin info * @returns {Array} List of Entities. */ - static listByEntityIds(entityIds = [], fields = []) { + static listByEntityIds(entityIds = [], fields = [], userDetails) { return new Promise(async (resolve, reject) => { try { // Call 'entitiesQueries.entityDocuments' to retrieve entities based on provided entity IDs and fields + let tenantId = userDetails.userInformation.tenantId + const entities = await entitiesQueries.entityDocuments( { _id: { $in: entityIds }, + tenantId: tenantId, }, fields ? fields : [] ) @@ -213,12 +220,14 @@ module.exports = class UserProjectsHelper { * @param {params} limit - page limit. * @param {params} pageNo - page no. * @param {params} language - language Code + * @param {Object} userDetails - loggedin user's details * @returns {Array} - List of all sub list entities. */ - static subEntityList(entities, entityId, type, search, limit, pageNo, language) { + static subEntityList(entities, entityId, type, search, limit, pageNo, language, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.userInformation.tenantId let result = [] let obj = { entityId: entityId, @@ -229,13 +238,13 @@ module.exports = class UserProjectsHelper { } // Retrieve sub-entities using 'this.subEntities' for a single entity if (entityId !== '') { - result = await this.subEntities(obj, language) + result = await this.subEntities(obj, language, tenantId) } else { // Retrieve sub-entities using 'this.subEntities' for multiple entities await Promise.all( entities.map(async (entity) => { obj['entityId'] = entity - let entitiesDocument = await this.subEntities(obj, language) + let entitiesDocument = await this.subEntities(obj, language, tenantId) if (Array.isArray(entitiesDocument.data) && entitiesDocument.data.length > 0) { result = entitiesDocument @@ -245,58 +254,60 @@ module.exports = class UserProjectsHelper { } // Modify data properties (e.g., 'label') of retrieved entities if necessary - if (result.data && result.data.length > 0) { - // fetch the entity ids to look for parent hierarchy - const entityIds = _.map(result.data, (item) => ObjectId(item._id)) - // dynamically set the entityType to search inside the group - const key = ['groups', type] - // create filter for fetching the parent data using group - let entityFilter = {} - entityFilter[key.join('.')] = { - $in: entityIds, - } - - // Retrieve all the entity documents with the entity ids in their gropu - const entityDocuments = await entitiesQueries.entityDocuments(entityFilter, [ - 'entityType', - 'metaInformation.name', - 'childHierarchyPath', - key.join('.'), - ]) - // find out the state of the passed entityId - const stateEntity = entityDocuments.find((entity) => entity.entityType == 'state') - // fetch the child hierarchy path of the state - const stateChildHierarchy = stateEntity.childHierarchyPath - let upperLevelsOfType = type != 'state' ? ['state'] : [] // add state as default if type != state - // fetch all the upper levels of the type from state hierarchy - upperLevelsOfType = [ - ...upperLevelsOfType, - ...stateChildHierarchy.slice(0, stateChildHierarchy.indexOf(type)), - ] - result.data = result.data.map((data) => { - let cloneData = { ...data } - cloneData[cloneData.entityType] = cloneData.name - // if we have upper levels to fetch - if (upperLevelsOfType.length > 0) { - // iterate through the data fetched to fetch the parent entity names - entityDocuments.forEach((eachEntity) => { - eachEntity[key[0]][key[1]].forEach((eachEntityGroup) => { - if ( - ObjectId(eachEntityGroup).equals(cloneData._id) && - upperLevelsOfType.includes(eachEntity.entityType) - ) { - if (eachEntity?.entityType !== 'state') { - cloneData[eachEntity?.entityType] = eachEntity?.metaInformation?.name - } - } - }) - }) - } - cloneData['label'] = cloneData.name - cloneData['value'] = cloneData._id - return cloneData - }) - } + // if (result.data && result.data.length > 0) { + // // fetch the entity ids to look for parent hierarchy + // const entityIds = _.map(result.data, (item) => ObjectId(item._id)) + // // dynamically set the entityType to search inside the group + // const key = ['groups', type] + // // create filter for fetching the parent data using group + // let entityFilter = {} + // entityFilter[key.join('.')] = { + // $in: entityIds, + // } + + // entityFilter['tenantId'] = tenantId + + // // Retrieve all the entity documents with the entity ids in their gropu + // const entityDocuments = await entitiesQueries.entityDocuments(entityFilter, [ + // 'entityType', + // 'metaInformation.name', + // 'childHierarchyPath', + // key.join('.'), + // ]) + // // find out the state of the passed entityId + // const stateEntity = entityDocuments.find((entity) => entity.entityType == 'state') + // // fetch the child hierarchy path of the state + // const stateChildHierarchy = stateEntity.childHierarchyPath + // let upperLevelsOfType = type != 'state' ? ['state'] : [] // add state as default if type != state + // // fetch all the upper levels of the type from state hierarchy + // upperLevelsOfType = [ + // ...upperLevelsOfType, + // ...stateChildHierarchy.slice(0, stateChildHierarchy.indexOf(type)), + // ] + // result.data = result.data.map((data) => { + // let cloneData = { ...data } + // cloneData[cloneData.entityType] = cloneData.name + // // if we have upper levels to fetch + // if (upperLevelsOfType.length > 0) { + // // iterate through the data fetched to fetch the parent entity names + // entityDocuments.forEach((eachEntity) => { + // eachEntity[key[0]][key[1]].forEach((eachEntityGroup) => { + // if ( + // ObjectId(eachEntityGroup).equals(cloneData._id) && + // upperLevelsOfType.includes(eachEntity.entityType) + // ) { + // if (eachEntity?.entityType !== 'state') { + // cloneData[eachEntity?.entityType] = eachEntity?.metaInformation?.name + // } + // } + // }) + // }) + // } + // cloneData['label'] = cloneData.name + // cloneData['value'] = cloneData._id + // return cloneData + // }) + // } resolve({ message: CONSTANTS.apiResponses.ENTITIES_FETCHED, @@ -315,9 +326,10 @@ module.exports = class UserProjectsHelper { * @param {params} pageSize - page pageSize. * @param {params} pageNo - page no. * @param {String} type - Entity type + * @param {String} tenantId - user's tenantId * @returns {Promise} A promise that resolves to the response containing the fetched roles or an error object. */ - static targetedRoles(entityId, pageNo = '', pageSize = '', paginate, type = '') { + static targetedRoles(entityId, pageNo = '', pageSize = '', paginate, type = '', language, tenantId) { return new Promise(async (resolve, reject) => { try { // Construct the filter to retrieve entities based on provided entity IDs @@ -325,6 +337,7 @@ module.exports = class UserProjectsHelper { _id: { $in: entityId, }, + tenantId: tenantId, } const projectionFields = ['childHierarchyPath', 'entityType'] // Retrieve entityDetails based on provided entity IDs @@ -361,6 +374,7 @@ module.exports = class UserProjectsHelper { name: { $in: filteredHierarchyPaths, }, + tenantId: tenantId, isDeleted: false, } const entityTypeProjection = ['_id'] @@ -385,6 +399,7 @@ module.exports = class UserProjectsHelper { $in: userRoleFilter, }, status: CONSTANTS.common.ACTIVE_STATUS, + tenantId: tenantId, } // Specify the fields to include in the result set @@ -432,10 +447,11 @@ module.exports = class UserProjectsHelper { * @method * @name subEntities * @param {body} entitiesData + * @param {String} tenantId * @returns {Array} - List of all immediate entities or traversal data. */ - static subEntities(entitiesData, language) { + static subEntities(entitiesData, language, tenantId) { return new Promise(async (resolve, reject) => { try { let entitiesDocument @@ -448,7 +464,8 @@ module.exports = class UserProjectsHelper { entitiesData.search, entitiesData.limit, entitiesData.pageNo, - language + language, + tenantId ) } else { // Retrieve immediate entities @@ -457,7 +474,8 @@ module.exports = class UserProjectsHelper { entitiesData.search, entitiesData.limit, entitiesData.pageNo, - language + language, + tenantId ) } @@ -473,10 +491,14 @@ module.exports = class UserProjectsHelper { * @method * @name immediateEntities * @param {Object} entityId + * @param {String} searchText + * @param {String} pageSize + * @param {String} pageNo + * @param {String} tenantId - user's tenant id * @returns {Array} - List of all immediateEntities based on entityId. */ - static immediateEntities(entityId, searchText = '', pageSize = '', pageNo = '') { + static immediateEntities(entityId, searchText = '', pageSize = '', pageNo = '', tenantId) { return new Promise(async (resolve, reject) => { try { // Define projection fields for entity retrieval @@ -485,6 +507,7 @@ module.exports = class UserProjectsHelper { let entitiesDocument = await entitiesQueries.entityDocuments( { _id: entityId, + tenantId: tenantId, }, projection ) @@ -498,6 +521,7 @@ module.exports = class UserProjectsHelper { let getImmediateEntityTypes = await entityTypesHelper.entityTypesDocument( { name: entitiesDocument[0].entityType, + tenantId: tenantId, }, ['immediateChildrenEntityType'] ) @@ -537,10 +561,11 @@ module.exports = class UserProjectsHelper { * @param {Number} pageSize - total page size. * @param {Number} pageNo - Page no. * @param {String} searchText - Search Text. + * @param {String} tenantId - user's tenant id * @returns {Array} - List of all immediateEntities based on entityId. */ - static entityTraversal(entityId, entityTraversalType = '', searchText = '', pageSize, pageNo, language) { + static entityTraversal(entityId, entityTraversalType = '', searchText = '', pageSize, pageNo, language, tenantId) { return new Promise(async (resolve, reject) => { try { let entityTraversal = `groups.${entityTraversalType}` @@ -550,6 +575,7 @@ module.exports = class UserProjectsHelper { _id: entityId, groups: { $exists: true }, [entityTraversal]: { $exists: true }, + tenantId: tenantId, }, [entityTraversal] ) @@ -565,7 +591,8 @@ module.exports = class UserProjectsHelper { pageSize, pageNo, entitiesDocument[0].groups[entityTraversalType], - language + language, + tenantId ) result = entityTraversalData[0] @@ -585,15 +612,18 @@ module.exports = class UserProjectsHelper { * @param {String} language - language Code. * @param {Number} pageSize - total page size. * @param {Number} pageNo - Page no. + * @param {String} tenantId - user's tenantId * @param {Array} [entityIds = false] - Array of entity ids. */ - static search(searchText, pageSize, pageNo, entityIds = false, language) { + static search(searchText, pageSize, pageNo, entityIds = false, language, tenantId) { return new Promise(async (resolve, reject) => { try { let queryObject = {} // Configure match criteria based on search text and entity IDs (if provided) - queryObject['$match'] = {} + queryObject['$match'] = { + tenantId: tenantId, + } if (entityIds && entityIds.length > 0) { queryObject['$match']['_id'] = {} @@ -877,10 +907,11 @@ module.exports = class UserProjectsHelper { * @param {String} entityTypeId - entity type id. * @param {String} entityType - entity type. * @param {Array} [projection = "all"] - total fields to be projected. + * @param {String} tenantId - user's tenant id * @returns {Array} - returns an array of related entities data. */ - static relatedEntities(entityId, entityTypeId, entityType, projection = 'all') { + static relatedEntities(entityId, entityTypeId, entityType, projection = 'all', tenantId) { return new Promise(async (resolve, reject) => { try { // if ( @@ -892,7 +923,9 @@ module.exports = class UserProjectsHelper { // return resolve(this.entityMapProcessData.relatedEntities[entityId.toString()]) // } - let relatedEntitiesQuery = {} + let relatedEntitiesQuery = { + tenantId, + } if (entityTypeId && entityId && entityType) { relatedEntitiesQuery[`groups.${entityType}`] = entityId @@ -925,12 +958,12 @@ module.exports = class UserProjectsHelper { * Sub entity type list. * @method * @name subEntityListBasedOnRoleAndLocation - * @param role - role code + * @param userDetails - loggedin user's details * @param stateLocationId - state location id. * @returns {Array} List of sub entity type. */ - static subEntityListBasedOnRoleAndLocation(stateLocationId) { + static subEntityListBasedOnRoleAndLocation(stateLocationId, userDetails) { return new Promise(async (resolve, reject) => { try { // let rolesDocument = await userRolesHelper.roleDocuments({ @@ -943,9 +976,11 @@ module.exports = class UserProjectsHelper { // message: CONSTANTS.apiResponses.USER_ROLES_NOT_FOUND // } // } + let tenantId = userDetails.userInformation.tenantId let filterQuery = { 'registryDetails.code': stateLocationId, + tenantId: tenantId, } // Check if stateLocationId is a valid UUID and update the filterQuery accordingly @@ -1008,12 +1043,15 @@ module.exports = class UserProjectsHelper { * @method * @name listByLocationIds * @param {Object} locationIds - locationIds + * @param {Object} userDetails - loggedin user's details * @returns {Object} entity Document */ - static listByLocationIds(locationIds) { + static listByLocationIds(locationIds, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.userInformation.tenantId + // Constructing the filter query to find entities based on locationIds let filterQuery = { $or: [ @@ -1024,6 +1062,7 @@ module.exports = class UserProjectsHelper { 'registryDetails.locationId': { $in: locationIds }, }, ], + tenantId: tenantId, } // Retrieving entities that match the filter query @@ -1060,18 +1099,146 @@ module.exports = class UserProjectsHelper { * @name find * @param {Object} bodyQuery - body data * @param {Object} projection - projection to filter data + * @param {Number} pageNo - page number + * @param {Number} pageSize - page limit + * @param {String} searchText - Text string used for filtering entities using a search. + * @param {String} aggregateValue - Path to the field to aggregate (e.g., 'groups.school') used for grouping or lookups. + * @param {Boolean} aggregateStaging - Flag indicating whether aggregation stages should be used in the pipeline (true = include stages). + * @param {Boolean} aggregateSort - Flag indicating whether sorting is required within the aggregation pipeline. + * @param {Array} aggregateProjection - Array of projection fields to apply within the aggregation pipeline (used when `aggregateStaging` is true). + * @returns {Array} Entity Documents */ - static find(bodyQuery, projection) { + static find( + bodyQuery, + projection, + pageNo, + pageSize, + searchText, + aggregateValue, + aggregateStaging, + aggregateSort, + aggregateProjection = [] + ) { return new Promise(async (resolve, reject) => { try { - // Fetch entities based on the provided query and projection - const result = await entitiesQueries.entityDocuments(bodyQuery, projection) - if (result.length < 1) { - throw { - status: HTTP_STATUS_CODE.not_found.status, - message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + let aggregateData + bodyQuery = UTILS.convertMongoIds(bodyQuery) + + if (aggregateStaging == true) { + let skip = (pageNo - 1) * pageSize + let projection1 = {} + if (aggregateProjection.length > 0) { + aggregateProjection.forEach((value) => { + projection1[value] = 1 + }) + } + aggregateData = [ + { + $match: bodyQuery, + }, + { + $project: { + groupIds: aggregateValue, + }, + }, + // Unwind the array so we don't hold all in memory + { + $unwind: '$groupIds', + }, + // Replace the root so we can lookup directly + { + $replaceRoot: { newRoot: { _id: '$groupIds' } }, + }, + // Lookup actual school entity details + { + $lookup: { + from: 'entities', + localField: '_id', + foreignField: '_id', + as: 'groupEntityData', + }, + }, + { + $unwind: '$groupEntityData', + }, + ...(searchText + ? [ + { + $match: { + 'groupEntityData.metaInformation.name': { + $regex: searchText, + $options: 'i', // case-insensitive search + }, + }, + }, + ] + : []), + { + $skip: skip, + }, + { + $limit: pageSize, + }, + { + $replaceRoot: { newRoot: '$groupEntityData' }, + }, + ...(aggregateProjection.length > 0 ? [{ $project: projection1 }] : []), + ] + } else { + // Create facet object to attain pagination + let facetQuery = {} + facetQuery['$facet'] = {} + facetQuery['$facet']['totalCount'] = [{ $count: 'count' }] + if (pageSize === '' && pageNo === '') { + facetQuery['$facet']['data'] = [{ $skip: 0 }] + } else { + facetQuery['$facet']['data'] = [{ $skip: pageSize * (pageNo - 1) }, { $limit: pageSize }] + } + + // add search filter to the bodyQuery + if (searchText != '') { + let searchData = [ + { + 'metaInformation.name': new RegExp(searchText, 'i'), + }, + ] + bodyQuery['$and'] = searchData + } + + // Create projection object + let projection1 + if (Array.isArray(projection) && projection.length > 0) { + projection1 = {} + projection.forEach((projectedData) => { + projection1[projectedData] = 1 + }) + aggregateData = [{ $match: bodyQuery }, { $project: projection1 }, facetQuery] + } else { + aggregateData = [{ $match: bodyQuery }, facetQuery] + } + } + + if (aggregateSort == true) { + aggregateData.push({ $sort: { updateAt: -1 } }) + } + + let result = await entitiesQueries.getAggregate(aggregateData) + if (aggregateStaging == true) { + if (!Array.isArray(result) || !(result.length > 0)) { + throw { + status: HTTP_STATUS_CODE.not_found.status, + message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + } + } + } else { + if (!(result.length > 0) || !result[0].data || !(result[0].data.length > 0)) { + throw { + status: HTTP_STATUS_CODE.not_found.status, + message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + } } + result = result[0].data } return resolve({ success: true, @@ -1092,19 +1259,18 @@ module.exports = class UserProjectsHelper { * @param {string} pageNo - pageNo for pagination * @param {string} language - language Code * @param {string} pageSize - pageSize for pagination + * @param {Object} userDetails - user decoded token details * @returns {Promise} Promise that resolves with fetched documents or rejects with an error. */ - static entityListBasedOnEntityType(type, pageNo, pageSize, paginate, language) { + static entityListBasedOnEntityType(type, pageNo, pageSize, paginate, language, userDetails) { return new Promise(async (resolve, reject) => { try { + let query = {} + query['tenantId'] = userDetails.userInformation.tenantId + query['name'] = type // Fetch the list of entity types available - const entityList = await entityTypeQueries.entityTypesDocument( - { - name: type, - }, - ['name'] - ) + const entityList = await entityTypeQueries.entityTypesDocument(query, ['name']) // Check if entity list is empty if (!entityList.length > 0) { throw { @@ -1113,21 +1279,20 @@ module.exports = class UserProjectsHelper { } } const projection = ['_id', 'metaInformation.name', 'metaInformation.externalId', 'translations'] + delete query.name + query['entityType'] = type // Fetch documents for the matching entity type let fetchList = await entitiesQueries.entityDocuments( - { - entityType: type, - }, + query, projection, pageSize, pageSize * (pageNo - 1), '', paginate ) - const count = await entitiesQueries.countEntityDocuments({ entityType: type }) // Check if fetchList list is empty - if (count <= 0) { + if (!(fetchList.length > 0)) { throw { status: HTTP_STATUS_CODE.not_found.status, message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, @@ -1162,8 +1327,8 @@ module.exports = class UserProjectsHelper { return resolve({ success: true, message: CONSTANTS.apiResponses.ASSETS_FETCHED_SUCCESSFULLY, - result: result, - count, + result, + count: result.length, }) } catch (error) { return reject(error) @@ -1178,7 +1343,6 @@ module.exports = class UserProjectsHelper { * @param {Object} queryParams - requested query data. * @param {Object} data - requested entity data. * @param {Object} userDetails - Logged in user information. - * @param {String} userDetails.id - Logged in user id. * @returns {JSON} - Created entity information. */ @@ -1186,16 +1350,24 @@ module.exports = class UserProjectsHelper { return new Promise(async (resolve, reject) => { try { // Find the entities document based on the entityType in queryParams - let entityTypeDocument = await entityTypeQueries.findOne({ name: queryParams.type }, { _id: 1 }) + + let tenantId = userDetails.tenantAndOrgInfo.tenantId + let orgId = userDetails.tenantAndOrgInfo.orgId[0] + let entityTypeDocument = await entityTypeQueries.findOne( + { name: queryParams.type, tenantId: tenantId }, + { _id: 1 } + ) if (!entityTypeDocument) { - throw CONSTANTS.apiResponses.ENTITY_NOT_FOUND + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + } } let entityDocuments = [] let dataArray = Array.isArray(data) ? data : [data] for (let pointer = 0; pointer < dataArray.length; pointer++) { let singleEntity = dataArray[pointer] - if (singleEntity.createdByProgramId) { singleEntity.createdByProgramId = ObjectId(singleEntity.createdByProgramId) } @@ -1219,6 +1391,7 @@ module.exports = class UserProjectsHelper { name: { $in: singleEntity.childHierarchyPath, }, + tenantId: tenantId, }, // Specify to return only the 'name' field of matching documents @@ -1232,7 +1405,12 @@ module.exports = class UserProjectsHelper { // Convert the names in 'validatedChildHierarchy' to strings and assign them to 'childHierarchyPath' childHierarchyPath = validatedChildHierarchy.map(String) } - + singleEntity.targetedEntityTypes = Array.isArray(singleEntity.targetedEntityTypes) + ? await populateTargetedEntityTypesData( + singleEntity.targetedEntityTypes.map((item) => item.trim()), + tenantId + ) + : [] // Construct the entity document to be created let entityDoc = { entityTypeId: entityTypeDocument._id, @@ -1241,9 +1419,11 @@ module.exports = class UserProjectsHelper { registryDetails: registryDetails, groups: {}, metaInformation: _.omit(singleEntity, ['locationId', 'code']), - updatedBy: userDetails.userId, - createdBy: userDetails.userId, - userId: userDetails.userId, + updatedBy: userDetails.userInformation.userId, + createdBy: userDetails.userInformation.userId, + userId: userDetails.userInformation.userId, + tenantId: tenantId, + orgId: orgId, } entityDocuments.push(entityDoc) @@ -1286,14 +1466,13 @@ module.exports = class UserProjectsHelper { * @param {ObjectId} entityId - entity Id. * @param {Object} requestData - requested data. * @param {String} language - language code. + * @param {Object} userDetails - user decoded token details * @returns {JSON} - provide the details. */ - static details(entityId, requestData = {}, language) { + static details(entityId, requestData = {}, language, userDetails) { return new Promise(async (resolve, reject) => { try { - // // let entityIdNum = parseInt(entityId) - // let entityIdNum = entityId.replace(/"/, ''); let entityIds = [] let externalIds = [] @@ -1353,10 +1532,19 @@ module.exports = class UserProjectsHelper { }, }) } + // add tenantId to the query + query['tenantId'] = userDetails.userInformation.tenantId // Fetch entity documents based on constructed query let entityDocument = await entitiesQueries.entityDocuments(query, 'all') + if (!entityDocument.length) { + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, + } + } + // Initialize variables for parent entity details let entityDocumentForParent let parentInformation = {} @@ -1503,22 +1691,30 @@ module.exports = class UserProjectsHelper { // } // Find the entity type document based on the provided entityType + let tenantId = userDetails.tenantAndOrgInfo.tenantId + let orgId = userDetails.tenantAndOrgInfo.orgId[0] let entityTypeDocument = await entityTypeQueries.findOne( { name: entityType, + tenantId: tenantId, }, - { _id: 1 } + { _id: 1, tenantId: 1 } ) if (!entityTypeDocument) { - throw CONSTANTS.apiResponses.INVALID_ENTITY_TYPE + throw { + status: HTTP_STATUS_CODE.bad_request.status, + message: CONSTANTS.apiResponses.INVALID_ENTITY_TYPE, + } } - // Process each entity in the entityCSVData array to create new entities const entityUploadedData = await Promise.all( entityCSVData.map(async (singleEntity) => { singleEntity = UTILS.valueParser(singleEntity) addTagsInEntities(singleEntity) - const userId = userDetails && userDetails.id ? userDetails.id : CONSTANTS.common.SYSTEM + const userId = + userDetails && userDetails.userInformation.userId + ? userDetails.userInformation.userId + : CONSTANTS.common.SYSTEM let entityCreation = { entityTypeId: entityTypeDocument._id, entityType: entityType, @@ -1526,11 +1722,26 @@ module.exports = class UserProjectsHelper { groups: {}, updatedBy: userId, createdBy: userId, + tenantId: tenantId, + orgId: orgId, } // if (singleEntity.allowedRoles && singleEntity.allowedRoles.length > 0) { // entityCreation['allowedRoles'] = await allowedRoles(singleEntity.allowedRoles) // delete singleEntity.allowedRoles // } + let entityTypesArray = [] + if (singleEntity.targetedEntityTypes) { + entityTypesArray = singleEntity.targetedEntityTypes + .replace(/^"(.*)"$/, '$1') // remove starting and ending quotes + .split(',') + .map((type) => type.trim()) + } + + singleEntity.targetedEntityTypes = + Array.isArray(entityTypesArray) && entityTypesArray.length > 0 + ? await populateTargetedEntityTypesData(entityTypesArray, tenantId) + : [] + if (singleEntity.childHierarchyPath) { entityCreation['childHierarchyPath'] = JSON.parse(singleEntity['childHierarchyPath']) } @@ -1623,9 +1834,10 @@ module.exports = class UserProjectsHelper { * @returns {Array} - Array of updated entity data. */ - static bulkUpdate(entityCSVData, translationFile) { + static bulkUpdate(entityCSVData, translationFile, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.tenantAndOrgInfo.tenantId const entityUploadedData = await Promise.all( entityCSVData.map(async (singleEntity) => { singleEntity = UTILS.valueParser(singleEntity) @@ -1683,9 +1895,18 @@ module.exports = class UserProjectsHelper { updateData['translations'] = translationFile[updateData['metaInformation.name']] } + let targetedEntityTypes = entityCSVData[0].targetedEntityTypes + .split(',') + .map((item) => item.trim()) + + updateData['metaInformation.targetedEntityTypes'] = + Array.isArray(targetedEntityTypes) && targetedEntityTypes.length > 0 + ? await populateTargetedEntityTypesData(targetedEntityTypes, tenantId) + : [] + if (Object.keys(updateData).length > 0) { let updateEntity = await entitiesQueries.findOneAndUpdate( - { _id: singleEntity['_SYSTEM_ID'] }, + { _id: singleEntity['_SYSTEM_ID'], tenantId: tenantId }, { $set: updateData }, { _id: 1 } ) @@ -1722,15 +1943,21 @@ module.exports = class UserProjectsHelper { * @name update * @param {String} entityId - entity id. * @param {Object} data - entity information that need to be updated. + * @param {Object} userDetails - loggedin user's info * @returns {JSON} - Updated entity information. */ - static update(entityId, bodyData) { + static update(entityId, bodyData, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.tenantAndOrgInfo.tenantId + if (bodyData.translations) { // Fetch existing entity document - let entityDocuments = await entitiesQueries.entityDocuments({ _id: ObjectId(entityId) }, 'all') + let entityDocuments = await entitiesQueries.entityDocuments( + { _id: ObjectId(entityId), tenantId: tenantId }, + 'all' + ) if (entityDocuments && entityDocuments.length > 0) { const existingTranslations = entityDocuments[0].translations || {} @@ -1743,10 +1970,22 @@ module.exports = class UserProjectsHelper { } } + if (bodyData['targetedEntityTypes']) { + bodyData.targetedEntityTypes = bodyData.targetedEntityTypes.map((item) => item.trim()) + bodyData['metaInformation.targetedEntityTypes'] = await populateTargetedEntityTypesData( + bodyData.targetedEntityTypes, + tenantId + ) + delete bodyData.targetedEntityTypes + } // Update the entity using findOneAndUpdate - let entityInformation = await entitiesQueries.findOneAndUpdate({ _id: ObjectId(entityId) }, bodyData, { - new: true, - }) + let entityInformation = await entitiesQueries.findOneAndUpdate( + { _id: ObjectId(entityId), tenantId: tenantId }, + bodyData, + { + new: true, + } + ) // Check if entityInformation is null (not found) if (!entityInformation) { @@ -1796,7 +2035,7 @@ module.exports = class UserProjectsHelper { try { // Retrieve the schema meta information key let schemaMetaInformation = this.entitiesSchemaData().SCHEMA_METAINFORMATION - + let tenantId = req.userDetails.userInformation.tenantId // Define projection for entity document fields to retrieve let projection = [ schemaMetaInformation + '.externalId', @@ -1811,6 +2050,7 @@ module.exports = class UserProjectsHelper { let entityDocuments = await entitiesQueries.entityDocuments( { entityTypeId: ObjectId(req.params._id), + tenantId: tenantId, }, projection, req.pageSize, @@ -1862,21 +2102,24 @@ module.exports = class UserProjectsHelper { * @param {String} entityId - requested entity id. * @param {String} [limitingValue = ""] - Limiting value if required. * @param {String} [skippingValue = ""] - Skipping value if required. + * @param {Object} userDetails - loggedin user's details * @returns {JSON} - Details of entity. */ static list( entityType, - entityTypeId, + entityId, limitingValue = '', skippingValue = '', schoolTypes = '', - administrationTypes = '' + administrationTypes = '', + userDetails ) { return new Promise(async (resolve, reject) => { try { // Query for the specified entity type within the given entity type ID document - let queryObject = { _id: ObjectId(entityTypeId) } + let tenantId = userDetails.userInformation.tenantId + let queryObject = { _id: ObjectId(entityId), tenantId: tenantId } let projectObject = { [`groups.${entityType}`]: 1 } let result = await entitiesQueries.findOne(queryObject, projectObject) if (!result) { @@ -1885,8 +2128,7 @@ module.exports = class UserProjectsHelper { message: CONSTANTS.apiResponses.ENTITY_NOT_FOUND, }) } - - // Check if the specified entity group within the document is not found + // Check if the specified entity group within the document is present or not if (!result.groups || !result.groups[entityType]) { return resolve({ status: HTTP_STATUS_CODE.bad_request.status, @@ -1898,13 +2140,12 @@ module.exports = class UserProjectsHelper { let entityIds = result.groups[entityType] const entityTypesArray = await entityTypesHelper.list( - {}, + { tenantId: tenantId }, { name: 1, immediateChildrenEntityType: 1, } ) - let enityTypeToImmediateChildrenEntityMap = {} // Build a map of entity types to their immediate child entity types @@ -1918,7 +2159,7 @@ module.exports = class UserProjectsHelper { } let filteredQuery = { - $match: { _id: { $in: entityIds } }, + $match: { _id: { $in: entityIds }, tenantId: tenantId }, } let schoolOrAdministrationTypes = [] @@ -2080,3 +2321,25 @@ function addTagsInEntities(entityMetaInformation) { } return entityMetaInformation } + +async function populateTargetedEntityTypesData(targetedEntityTypes, tenantId) { + try { + const formattedTargetedEntityTypes = await entityTypeQueries.entityTypesDocument( + { + name: { $in: targetedEntityTypes }, + tenantId: tenantId, + }, + ['name', '_id'] + ) + + formattedTargetedEntityTypes.forEach((entityType) => { + entityType['entityTypeId'] = entityType._id.toString() + entityType['entityType'] = entityType.name + delete entityType._id + delete entityType.name + }) + return formattedTargetedEntityTypes + } catch (err) { + return [] + } +} diff --git a/src/module/entities/validator/v1.js b/src/module/entities/validator/v1.js index 38c73e2..655d9f3 100644 --- a/src/module/entities/validator/v1.js +++ b/src/module/entities/validator/v1.js @@ -61,7 +61,7 @@ module.exports = (req) => { }, find: function () { req.checkBody('query').exists().withMessage('required query') - // req.checkBody('projection').exists().withMessage('required projection') + req.checkBody('query.tenantId').exists().withMessage('required tenant id') }, listByEntityType: function () { req.checkParams('_id').exists().withMessage('required Entity type') diff --git a/src/module/entityTypes/helper.js b/src/module/entityTypes/helper.js index 09eba26..58c8775 100644 --- a/src/module/entityTypes/helper.js +++ b/src/module/entityTypes/helper.js @@ -26,12 +26,15 @@ module.exports = class UserProjectsHelper { * @returns {JSON} - uploaded entity information. */ static bulkCreate(entityTypesCSVData, userDetails) { + console.log(userDetails, '<--userDetails in bulkCreate entityTypesCSVData') return new Promise(async (resolve, reject) => { try { const entityTypesUploadedData = await Promise.all( entityTypesCSVData.map(async (entityType) => { try { entityType = UTILS.valueParser(entityType) + entityType['tenantId'] = userDetails.tenantAndOrgInfo.tenantId + entityType['orgId'] = userDetails.tenantAndOrgInfo.orgId[0] entityType.registryDetails = {} let removedKeys = [] @@ -83,8 +86,8 @@ module.exports = class UserProjectsHelper { // Set userId based on userDetails const userId = - userDetails && userDetails.userInformation.id - ? userDetails && userDetails.userInformation.id + userDetails && userDetails.userInformation.userId + ? userDetails && userDetails.userInformation.userId : CONSTANTS.common.SYSTEM if (!entityType.name) { @@ -131,7 +134,7 @@ module.exports = class UserProjectsHelper { * create single entity. * @method * @name create - * @param {Object} data - requested entity data. + * @param {Object} body - requested entity data. * @param {Object} userDetails - Logged in user information. * @returns {JSON} - create single entity. */ @@ -139,6 +142,8 @@ module.exports = class UserProjectsHelper { return new Promise(async (resolve, reject) => { try { let entityType = body + entityType['tenantId'] = userDetails.tenantAndOrgInfo.tenantId + entityType['orgId'] = userDetails.tenantAndOrgInfo.orgId[0] if (entityType.profileFields) { entityType.profileFields = entityType.profileFields.split(',') || [] @@ -170,9 +175,10 @@ module.exports = class UserProjectsHelper { // Determine userId based on userDetails or default to SYSTEM const userId = - userDetails && userDetails.userInformation.id - ? userDetails.userInformation.id + userDetails && userDetails.userInformation.userId + ? userDetails.userInformation.userId : CONSTANTS.common.SYSTEM + let newEntityType = await entityTypeQueries.create( _.merge( { @@ -187,6 +193,7 @@ module.exports = class UserProjectsHelper { if (newEntityType._id) { entityType.status = CONSTANTS.common.SUCCESS + entityType._id = newEntityType._id } else { entityType.status = CONSTANTS.common.FAILURE } @@ -200,21 +207,28 @@ module.exports = class UserProjectsHelper { }) } /** - * update single entity. + * update single entityType. * @method * @name update - * @param {Object} data - requested entity data. + * @param {Object} entityTypeId - entity type id. + * @param {Object} bodyData - requested entity data. * @param {Object} userDetails - Logged in user information. * @returns {JSON} - update single entity. * */ - static update(entityTypeId, bodyData) { + static update(entityTypeId, bodyData, userDetails) { return new Promise(async (resolve, reject) => { try { + // avoid adding manupulative data + delete bodyData.tenantId + delete bodyData.orgIds + + let tenantId = userDetails.tenantAndOrgInfo.tenantId + // Find and update the entity type by ID with the provided bodyData let entityInformation = await entityTypeQueries.findOneAndUpdate( - { _id: ObjectId(entityTypeId) }, + { _id: ObjectId(entityTypeId), tenantId: tenantId }, bodyData, { new: true } ) @@ -246,6 +260,7 @@ module.exports = class UserProjectsHelper { static bulkUpdate(entityTypesCSVData, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.tenantAndOrgInfo.tenantId // Process each entity type in the provided array asynchronously const entityTypesUploadedData = await Promise.all( entityTypesCSVData.map(async (entityType) => { @@ -313,6 +328,7 @@ module.exports = class UserProjectsHelper { let updateEntityType = await entityTypeQueries.findOneAndUpdate( { _id: ObjectId(entityType._SYSTEM_ID), + tenantId: tenantId, }, _.merge( @@ -353,25 +369,70 @@ module.exports = class UserProjectsHelper { * List enitity Type. * @method * @name list - * @param {String} entityType - entity type. - * @param {String} entityId - requested entity id. - * @param {String} [queryParameter = ""] - queryParameter value if required. + * @param {Object} [bodyQuery = {}] - query value if required. + * @param {Object} [projection = {}] - mongodb query project object + * @param {Number} [pageNo] - page no + * @param {Number} [pageSize] - page size + * @param {String} [searchText] - search text * @returns {JSON} - Details of entity. */ - static list(queryParameter = 'all', projection = {}) { + static list(bodyQuery = {}, projection = [], pageNo, pageSize, searchText = '') { return new Promise(async (resolve, reject) => { try { - // Convert 'all' to an empty object for querying all entity types - if (queryParameter === 'all') { - queryParameter = {} + // Create facet object to attain pagination + let facetQuery = {} + facetQuery['$facet'] = {} + facetQuery['$facet']['totalCount'] = [{ $count: 'count' }] + if (pageSize === '' && pageNo === '') { + facetQuery['$facet']['data'] = [{ $skip: 0 }] + } else { + facetQuery['$facet']['data'] = [{ $skip: pageSize * (pageNo - 1) }, { $limit: pageSize }] + } + + bodyQuery = UTILS.convertMongoIds(bodyQuery) + + // add search filter to the bodyQuery + if (searchText != '') { + let searchData = [ + { + name: new RegExp(searchText, 'i'), + }, + ] + bodyQuery['$and'] = searchData + } + + // Create projection object + let projection1 = {} + let aggregateData + if (Array.isArray(projection) && projection.length > 0) { + projection1 = {} + projection.forEach((projectedData) => { + projection1[projectedData] = 1 + }) + aggregateData = [ + { $match: bodyQuery }, + { + $sort: { updatedAt: -1 }, + }, + { $project: projection1 }, + facetQuery, + ] + } else { + aggregateData = [ + { $match: bodyQuery }, + { + $sort: { updatedAt: -1 }, + }, + facetQuery, + ] } - // Retrieve entity type data based on the provided queryParameter and projection - let entityTypeData = await entityTypeQueries.entityTypesDocument(queryParameter, projection) + // Retrieve entity type data based on the provided query and projection + const result = await entityTypeQueries.getAggregate(aggregateData) return resolve({ message: CONSTANTS.apiResponses.ENTITY_TYPES_FETCHED, - result: entityTypeData, + result: result[0].data, }) } catch (error) { return reject(error) diff --git a/src/module/entityTypes/validator/v1.js b/src/module/entityTypes/validator/v1.js index c142615..266203e 100644 --- a/src/module/entityTypes/validator/v1.js +++ b/src/module/entityTypes/validator/v1.js @@ -38,7 +38,8 @@ module.exports = (req, res) => { }, find: function () { - req.checkBody('query').exists().withMessage('required name') + req.checkBody('query').exists().withMessage('required query') + req.checkBody('query.tenantId').exists().withMessage('required tenantId') }, } diff --git a/src/module/userRoleExtension/helper.js b/src/module/userRoleExtension/helper.js index fc44413..f3b5a7b 100644 --- a/src/module/userRoleExtension/helper.js +++ b/src/module/userRoleExtension/helper.js @@ -5,7 +5,7 @@ * Description : user role helper functionality. */ -const { result } = require('lodash') +const { result, includes } = require('lodash') // Dependencies const userRoleExtensionQueries = require(DB_QUERY_BASE_PATH + '/userRoleExtension') @@ -15,29 +15,35 @@ module.exports = class userRoleExtensionHelper { /** * Create a new user role extension with the provided body data. * @param {Object} body - The data to create the new user role extension. + * @param {Object} userDetails - loggedin user details * @returns {Promise} - A promise that resolves with the new user role extension data or rejects with an error. */ - static create(body) { + static create(body, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.tenantAndOrgInfo.tenantId // Using map to handle validation await Promise.all( body.entityTypes.map(async (entityTypeData) => { // Validate that both entityType and entityTypeId exist in the entityType DB - let existingEntityType = await entityTypeQueries.findOne({ + let filterQuery = { name: entityTypeData.entityType, _id: ObjectId(entityTypeData.entityTypeId), - }) + tenantId: tenantId, + } + let existingEntityType = await entityTypeQueries.findOne(filterQuery) if (!existingEntityType) { // If any entityType is invalid, reject the request throw { status: HTTP_STATUS_CODE.bad_request.status, - message: `EntityType '${entityTypeData.entityType}' with ID '${entityTypeData.entityTypeId}' does not exist.`, + message: `EntityType '${entityTypeData.entityType}' with ID '${entityTypeData.entityTypeId}' & tenantId ${tenantId} does not exist.`, } } }) ) + body['tenantId'] = userDetails.tenantAndOrgInfo.tenantId + body['orgId'] = userDetails.tenantAndOrgInfo.orgId[0] // Call the queries function to create a new user role extension with the provided body data let newUserRole = await userRoleExtensionQueries.create(body) @@ -55,34 +61,41 @@ module.exports = class userRoleExtensionHelper { * Update a user role extension with the provided userRoleId and body data. * @param {ObjectId} userRoleId - The ID of the user role extension to be updated. * @param {Object} bodyData - The data to update the user role extension. + * @param {Object} userDetails - loggedin user details * @returns {Promise} - A promise that resolves with the updated user role extension data or rejects with an error. */ - static update(userRoleId, bodyData) { + static update(documentId, bodyData, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.tenantAndOrgInfo.tenantId if (bodyData.entityTypes) { await Promise.all( bodyData.entityTypes.map(async (entityTypeData) => { // Validate that both entityType and entityTypeId exist in the entityType DB - let existingEntityType = await entityTypeQueries.findOne({ + let entityTypeFilterQuery = { name: entityTypeData.entityType, _id: ObjectId(entityTypeData.entityTypeId), - }) + tenantId: tenantId, + } + let existingEntityType = await entityTypeQueries.findOne(entityTypeFilterQuery) if (!existingEntityType) { // If any entityType is invalid, reject the request throw { status: HTTP_STATUS_CODE.bad_request.status, - message: `EntityType '${entityTypeData.entityType}' with ID '${entityTypeData.entityTypeId}' does not exist.`, + message: `EntityType '${entityTypeData.entityType}' with ID '${entityTypeData.entityTypeId}' & tenantId ${tenantId} does not exist.`, } } }) ) } + delete bodyData.tenantId + delete bodyData.orgIds + // Find and update the user role extension based on the provided userRoleId and bodyData let userInformation = await userRoleExtensionQueries.findOneAndUpdate( - { _id: ObjectId(userRoleId) }, + { _id: ObjectId(documentId), tenantId: tenantId }, bodyData, { new: true } ) @@ -145,14 +158,19 @@ module.exports = class userRoleExtensionHelper { /** * Delete a user role extension by its ID. - * @param {String} userRoleId - The ID of the user role extension to delete. + * @param {String} documentId - The ID of the user role extension to delete. + * @param {Object} userDetails - loggedin user details * @returns {Promise} - A promise that resolves with a success message or rejects with an error. */ - static delete(userRoleId) { + static delete(documentId, userDetails) { return new Promise(async (resolve, reject) => { try { + let tenantId = userDetails.tenantAndOrgInfo.tenantId // Find and delete the user role extension based on the provided user role ID - let userInformation = await userRoleExtensionQueries.findOneAndDelete({ _id: ObjectId(userRoleId) }) + let userInformation = await userRoleExtensionQueries.findOneAndDelete({ + _id: ObjectId(documentId), + tenantId: tenantId, + }) // If no user role extension is found, reject the promise with a 404 status and an error message if (!userInformation) { diff --git a/src/package.json b/src/package.json index 31ef7a9..7f70679 100644 --- a/src/package.json +++ b/src/package.json @@ -41,6 +41,7 @@ "csvtojson": "^2.0.10", "dotenv": "^8.2.0", "elevate-logger": "^3.1.0", + "elevate-services-health-check": "^0.0.3", "eslint": "^8.16.0", "express": "^4.17.1", "express-fileupload": "^1.5.0", diff --git a/src/scripts/cleanUpEntityData.js b/src/scripts/cleanUpEntityData.js new file mode 100644 index 0000000..127a95f --- /dev/null +++ b/src/scripts/cleanUpEntityData.js @@ -0,0 +1,76 @@ +const MongoClient = require('mongodb').MongoClient +require('dotenv').config() + +async function deleteEntitiesInBatches(mongoUrl) { + const batchSize = 1000 // Configurable batch size + const entityTypes = ['state', 'district', 'block', 'cluster', 'school'] + + // Validate MongoDB URL + if (!mongoUrl) { + console.error('Error: MongoDB URL must be provided as a command-line argument or in .env as MONGODB_URL') + process.exit(1) + } + + let client + + try { + // Connect to MongoDB + client = await MongoClient.connect(mongoUrl, { useNewUrlParser: true, useUnifiedTopology: true }) + console.log('Connected to MongoDB') + + const db = client.db() // Use default database from URL + const collection = db.collection('entities') + + // Count total matching documents + const totalDocs = await collection.countDocuments({ entityType: { $in: entityTypes } }) + console.log(`Total documents to delete: ${totalDocs}`) + + if (totalDocs === 0) { + console.log('No documents found matching the criteria. Exiting.') + return + } + + // Delete in batches + let deletedCount = 0 + while (deletedCount < totalDocs) { + // Find a batch of document IDs to delete + const batchDocs = await collection + .find({ entityType: { $in: entityTypes } }) + .limit(batchSize) + .project({ _id: 1 }) + .toArray() + + if (batchDocs.length === 0) { + break // No more documents to delete + } + + // Extract IDs for deletion + const batchIds = batchDocs.map((doc) => doc._id) + + // Delete documents by IDs + const batchResult = await collection.deleteMany({ _id: { $in: batchIds } }) + const batchDeleted = batchResult.deletedCount + deletedCount += batchDeleted + console.log(`Deleted ${batchDeleted} documents in this batch. Total deleted: ${deletedCount}`) + } + + console.log(`Deletion complete. Total documents deleted: ${deletedCount}`) + } catch (error) { + console.error('Error during deletion:', error.message) + process.exit(1) + } finally { + if (client) { + await client.close() + console.log('MongoDB connection closed') + } + } +} + +// Get MongoDB URL from command-line argument or environment variable +const mongoUrl = process.argv[2] || process.env.MONGODB_URL + +// Run the script +deleteEntitiesInBatches(mongoUrl).catch((error) => { + console.error('Script failed:', error.message) + process.exit(1) +}) diff --git a/src/scripts/readme b/src/scripts/readme new file mode 100644 index 0000000..2b739b5 --- /dev/null +++ b/src/scripts/readme @@ -0,0 +1,33 @@ +# MongoDB Entity Data Cleanup Script + +This Node.js script deletes all documents from the MongoDB `entities` collection where `entityType` is one of `state`, `district`, `block`, `cluster`, or `school`. It processes deletions in batches for efficiency and supports MongoDB 4.x with the MongoDB Node.js driver v3.x. The script accepts a MongoDB URL as a command-line argument or falls back to an environment variable. + +## Prerequisites + +- **Node.js**: Version 14 or later. +- **MongoDB Server**: Version 4.x (e.g., 4.0 or 4.2). +- **NPM Packages**: `mongodb@3.6.12`, `dotenv`. +- **MongoDB URL**: A valid connection string (e.g., `mongodb://localhost:27017/elevate-entity`). + +## Usage + +The script can be executed **outside** or **inside** a Docker container. It deletes documents in batches of 1000 (configurable) and logs progress. + +### Option 1: Run Outside Docker + +1. Save the script as `cleanUpEntityData.js` (provided in the repository or separately). + +2. Run the script with a MongoDB URL as a command-line argument: + ```bash + node cleanUpEntityData.js mongodb://localhost:27017/prod-saas-elevate-entity + ``` + +### Option 2: Run Inside Docker + +1. Save the script as `cleanUpEntityData.js` inside entity service docker container + +2. Run the container with the MongoDB URL as an argument: + + ```bash + node cleanUpEntityData.js + ```