Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
35 changes: 35 additions & 0 deletions src/api-doc/api-doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5896,6 +5896,15 @@ paths:
type: string
type:
type: string
meta:
type: object
properties:
filterType:
type: string
enum:
- OR
- AND

examples:
example1:
value:
Expand All @@ -5908,6 +5917,8 @@ paths:
required: true
status: ACTIVE
type: SYSTEM
meta:
filterType: OR
example2:
value:
value: pgender
Expand Down Expand Up @@ -5962,6 +5973,11 @@ paths:
type: 'null'
has_entities:
type: boolean
meta:
type: object
properties:
filterType:
type: string
field_0:
type: string
meta:
Expand All @@ -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
Expand Down Expand Up @@ -6228,6 +6246,14 @@ paths:
type: boolean
required:
type: boolean
meta:
type: object
properties:
filterType:
type: string
enum:
- OR
- AND
examples:
example1:
value:
Expand All @@ -6240,6 +6266,8 @@ paths:
- UserExtension
allow_filtering: true
required: true
meta:
filterType: OR
example2:
value:
value: pgender
Expand Down Expand Up @@ -6296,6 +6324,11 @@ paths:
type: string
has_entities:
type: boolean
meta:
type: object
properties:
filterType:
type: string
meta:
type: object
properties:
Expand All @@ -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
Expand Down
49 changes: 46 additions & 3 deletions src/generics/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -515,6 +540,7 @@ const generateWhereClause = (tableName) => {
*/
function validateAndBuildFilters(input, validationData) {
const entityTypes = {}
let filterType = {}

// Ensure validationData is an array
if (!Array.isArray(validationData)) {
Expand All @@ -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<String>} values - Array of possible values to match
*/
function handleStringType(key, values) {
const orConditions = values
.map((value, index) => {
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -726,13 +764,18 @@ 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,
value: entityType.value,
parent_id: entityType.parent_id,
organization_id: entityType.organization_id,
entities: entityType.entities || [],
filterType: filterTypeValue,
}

result[key].push(newObj)
Expand Down
26 changes: 26 additions & 0 deletions src/validators/v1/entity-type.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Comment on lines +79 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate meta shape and filterType type before normalization.

If meta is not an object or filterType isn’t a string, .toUpperCase() can throw and the payload isn’t cleanly rejected. Add explicit guards so invalid shapes produce a validation error instead of a runtime TypeError.

🔧 Suggested fix (apply to both create & update blocks)
 req.checkBody('meta')
 	.optional()
 	.custom((meta) => {
-		if (meta && meta.filterType) {
-			const filterType = meta.filterType.toUpperCase()
+		if (meta && (typeof meta !== 'object' || Array.isArray(meta))) {
+			throw new Error('meta must be an object')
+		}
+		if (meta && meta.filterType !== undefined) {
+			if (typeof meta.filterType !== 'string') {
+				throw new Error('filterType inside meta must be a string')
+			}
+			const filterType = meta.filterType.trim().toUpperCase()
 			if (!['OR', 'AND'].includes(filterType)) {
 				throw new Error('filterType inside meta must be either OR or AND')
 			}
 			meta.filterType = filterType
 		}
 		return true
 	})

As per coding guidelines, validate all incoming data thoroughly.

Also applies to: 173-184

🤖 Prompt for AI Agents
In `@src/validators/v1/entity-type.js` around lines 79 - 90, The current custom
validator on req.checkBody('meta') calls meta.filterType.toUpperCase() without
guarding types, causing runtime TypeError for non-object meta or non-string
filterType; update the custom validator used in both the create and update
blocks to first ensure meta is an object (e.g., typeof meta === 'object' && meta
!== null) and that meta.filterType, if present, is a string before calling
toUpperCase(); if those checks fail, throw a validation Error (e.g., "meta must
be an object" or "meta.filterType must be a string") so invalid payloads are
rejected rather than causing runtime exceptions, and then normalize
meta.filterType to uppercase and validate it against ['OR','AND'] as before.


if (req.body.has_entities == false) {
req.checkBody('allow_filtering').custom((value) => {
if (value) {
Expand Down Expand Up @@ -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) {
Expand Down