diff --git a/documentation/latest/postman-collections/mentoring/MentorED-Mentoring.postman_collection.json b/documentation/latest/postman-collections/mentoring/MentorED-Mentoring.postman_collection.json index 207008997..4cf420219 100644 --- a/documentation/latest/postman-collections/mentoring/MentorED-Mentoring.postman_collection.json +++ b/documentation/latest/postman-collections/mentoring/MentorED-Mentoring.postman_collection.json @@ -236,7 +236,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"value\": \"roless\",\n \"label\": \"RolesV\",\n \"status\": \"ACTIVE\",\n \"type\": \"SYSTEM\",\n \"data_type\": \"ARRAY[STRING]\",\n \"model_names\": [\"UserExtension\",\"Session\"],\n \"required\": true,\n \"allow_filtering\": true\n}", + "raw": "{\n \"value\": \"roless\",\n \"label\": \"RolesV\",\n \"status\": \"ACTIVE\",\n \"type\": \"SYSTEM\",\n \"data_type\": \"ARRAY[STRING]\",\n \"model_names\": [\"UserExtension\",\"Session\"],\n \"required\": true,\n \"allow_filtering\": true,\n \"meta\":{\n \"filterType\":\"OR\"\n }\n}", "options": { "raw": { "language": "json" @@ -331,7 +331,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"value\": \"roless\",\n \"label\": \"RolesV\",\n \"status\": \"ACTIVE\",\n \"type\": \"SYSTEM\",\n \"data_type\": \"ARRAY[STRING]\",\n \"model_names\": [\"UserExtension\",\"Session\"],\n \"required\": true\n}", + "raw": "{\n \"value\": \"roless\",\n \"label\": \"RolesV\",\n \"status\": \"ACTIVE\",\n \"type\": \"SYSTEM\",\n \"data_type\": \"ARRAY[STRING]\",\n \"model_names\": [\"UserExtension\",\"Session\"],\n \"required\": true,\n \"meta\":{\n \"filterType\":\"AND\"\n }\n}", "options": { "raw": { "language": "json" diff --git a/src/api-doc/api-doc.yaml b/src/api-doc/api-doc.yaml index 2116a3314..49852f238 100644 --- a/src/api-doc/api-doc.yaml +++ b/src/api-doc/api-doc.yaml @@ -5896,6 +5896,15 @@ paths: type: string type: type: string + meta: + type: object + properties: + filterType: + type: string + enum: + - OR + - AND + examples: example1: value: @@ -5908,6 +5917,8 @@ paths: required: true status: ACTIVE type: SYSTEM + meta: + filterType: OR example2: value: value: pgender @@ -5962,6 +5973,11 @@ paths: type: 'null' has_entities: type: boolean + meta: + type: object + properties: + filterType: + type: string field_0: type: string meta: @@ -5988,6 +6004,8 @@ paths: updated_at: '2023-09-22T12:40:19.817Z' created_at: '2023-09-22T12:40:19.817Z' has_entities: true + meta: + filterType: OR meta: correlation: 3babe76b-d277-4073-8a59-8dfb94face9b meeting_platform: BBB @@ -6228,6 +6246,14 @@ paths: type: boolean required: type: boolean + meta: + type: object + properties: + filterType: + type: string + enum: + - OR + - AND examples: example1: value: @@ -6240,6 +6266,8 @@ paths: - UserExtension allow_filtering: true required: true + meta: + filterType: OR example2: value: value: pgender @@ -6296,6 +6324,11 @@ paths: type: string has_entities: type: boolean + meta: + type: object + properties: + filterType: + type: string meta: type: object properties: @@ -6322,6 +6355,8 @@ paths: deleted_at: null organization_id: '1' has_entities: true + meta: + filterType: OR meta: correlation: 5f384234-cd5a-467e-a5ac-b43365d2a7a3 meeting_platform: BBB diff --git a/src/generics/utils.js b/src/generics/utils.js index d26da4d79..f8e534127 100644 --- a/src/generics/utils.js +++ b/src/generics/utils.js @@ -109,6 +109,31 @@ const getTimeZone = (date, format, tz = null) => { const utcFormat = () => { return momentTimeZone().utc().format('YYYY-MM-DDTHH:mm:ss') } +/** + * Get PostgreSQL array comparison operator based on filter type + * @param {Object} filterType - Map of entity types to their filter types (OR/AND) + * @param {String} key - Entity type key + * @returns {String} PostgreSQL operator: + * - '&&' (overlap) for OR filtering: matches if ANY value matches + * - '@>' (contains) for AND filtering: matches if ALL values match + */ +function getArrayFilterOperator(filterType, key) { + if (!filterType || typeof filterType !== 'object') { + return '@>' + } + const type = filterType[key]?.trim()?.toUpperCase() + return type === 'OR' ? '&&' : '@>' +} + +/** + * Extract and normalize filterType from entity metadata + * @param {Object} entityType - Entity type object with optional meta.filterType + * @returns {String} - Normalized filter type ('OR' or 'AND') + */ +function getFilterType(entityType) { + const type = entityType?.meta?.filterType?.trim()?.toUpperCase() + return type === 'OR' ? 'OR' : 'AND' +} /** * md5 hash @@ -515,6 +540,7 @@ const generateWhereClause = (tableName) => { */ function validateAndBuildFilters(input, validationData) { const entityTypes = {} + let filterType = {} // Ensure validationData is an array if (!Array.isArray(validationData)) { @@ -524,12 +550,23 @@ function validateAndBuildFilters(input, validationData) { // Build the entityTypes dictionary validationData.forEach((entityType) => { entityTypes[entityType.value] = entityType.data_type + filterType[entityType.value] = getFilterType(entityType) }) const queryParts = [] // Array to store parts of the query const replacements = {} // Object to store replacements for Sequelize // Function to handle string types + /** + * Handles filtering for string/scalar type fields + * Note: String types always use OR logic because a scalar field can only hold one value at a time. + * The filterType (OR/AND) configuration only applies to array-type field where records can contain + * multiple values. For example, a status field cannot be both 'Active' AND 'Inactive' simultaneously, + * so filtering for multiple statuses must use OR: (status = 'Active' OR status = 'Inactive') + * + * @param {String} key - Field name to filter + * @param {Array} values - Array of possible values to match + */ function handleStringType(key, values) { const orConditions = values .map((value, index) => { @@ -541,14 +578,14 @@ function validateAndBuildFilters(input, validationData) { } // Function to handle array types - function handleArrayType(key, values) { + function handleArrayType(key, values, filter) { const arrayValues = values .map((value, index) => { replacements[`${key}_${index}`] = value return `:${key}_${index}` }) .join(', ') - queryParts.push(`"${key}" @> ARRAY[${arrayValues}]::character varying[]`) + queryParts.push(`"${key}" ${filter} ARRAY[${arrayValues}]::character varying[]`) } // Iterate over each key in the input object @@ -560,7 +597,8 @@ function validateAndBuildFilters(input, validationData) { if (common.ENTITY_TYPE_DATA_TYPES.STRING_TYPES.includes(dataType)) { handleStringType(key, input[key]) } else if (common.ENTITY_TYPE_DATA_TYPES.ARRAY_TYPES.includes(dataType)) { - handleArrayType(key, input[key]) + let filterToBeApplied = getArrayFilterOperator(filterType, key) + handleArrayType(key, input[key], filterToBeApplied) } } else { // Remove keys that are not in the validationData @@ -726,6 +764,10 @@ function convertEntitiesForFilter(entityTypes) { }) } + const filterTypeValue = getFilterType(entityType) + /* + filterTypeValue indicates the filtering logic (OR or AND) which is used + */ const newObj = { id: entityType.id, label: entityType.label, @@ -733,6 +775,7 @@ function convertEntitiesForFilter(entityTypes) { parent_id: entityType.parent_id, organization_id: entityType.organization_id, entities: entityType.entities || [], + filterType: filterTypeValue, } result[key].push(newObj) diff --git a/src/validators/v1/entity-type.js b/src/validators/v1/entity-type.js index ef6b5af5d..9a770edc7 100644 --- a/src/validators/v1/entity-type.js +++ b/src/validators/v1/entity-type.js @@ -76,6 +76,19 @@ module.exports = { .isInt() .withMessage('parent_id is invalid,must be integer') + req.checkBody('meta') + .optional() + .custom((meta) => { + if (meta && meta.filterType) { + const filterType = meta.filterType.toUpperCase() + if (!['OR', 'AND'].includes(filterType)) { + throw new Error('filterType inside meta must be either OR or AND') + } + meta.filterType = filterType + } + return true + }) + if (req.body.has_entities == false) { req.checkBody('allow_filtering').custom((value) => { if (value) { @@ -157,6 +170,19 @@ module.exports = { .isInt() .withMessage('parent_id is invalid,must be integer') + req.checkBody('meta') + .optional() + .custom((meta) => { + if (meta && meta.filterType) { + const filterType = meta.filterType.toUpperCase() + if (!['OR', 'AND'].includes(filterType)) { + throw new Error('filterType inside meta must be either OR or AND') + } + meta.filterType = filterType + } + return true + }) + if (req.body.has_entities == false) { req.checkBody('allow_filtering').custom((value) => { if (value) {