Skip to content
Merged
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
28 changes: 28 additions & 0 deletions src/api-doc/api-doc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1709,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
Expand Down
17 changes: 16 additions & 1 deletion src/controllers/v1/entities.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ module.exports = class Entities extends Abstract {
* @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<Object>} req.body.aggregateProjection - Optional array of projection stages for aggregation.
*
* @returns {Promise<Object>} - A Promise resolving to a list of matched entity documents with pagination.
* @apiGroup Entities
* @apiSampleRequest {
"query" : {
Expand Down Expand Up @@ -78,7 +89,11 @@ module.exports = class Entities extends Abstract {
req.body.projection,
req.pageNo,
req.pageSize,
req.searchText
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) {
Expand Down
168 changes: 123 additions & 45 deletions src/module/entities/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -1101,71 +1101,149 @@ module.exports = class UserProjectsHelper {
* @param {Object} projection - projection to filter data
* @param {Number} pageNo - page number
* @param {Number} pageSize - page limit
* @param {String} searchText - search text
* @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, pageNo, pageSize, searchText) {
static find(
bodyQuery,
projection,
pageNo,
pageSize,
searchText,
aggregateValue,
aggregateStaging,
aggregateSort,
aggregateProjection = []
) {
return new Promise(async (resolve, reject) => {
try {
// 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 }]
}

let aggregateData
bodyQuery = UTILS.convertMongoIds(bodyQuery)

// add search filter to the bodyQuery
if (searchText != '') {
let searchData = [
if (aggregateStaging == true) {
let skip = (pageNo - 1) * pageSize
let projection1 = {}
if (aggregateProjection.length > 0) {
aggregateProjection.forEach((value) => {
projection1[value] = 1
})
}
aggregateData = [
{
'metaInformation.name': new RegExp(searchText, 'i'),
$match: bodyQuery,
},
]
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: {
groupIds: aggregateValue,
},
},
{ $project: projection1 },
facetQuery,
]
} else {
aggregateData = [
{ $match: bodyQuery },
// Unwind the array so we don't hold all in memory
{
$unwind: '$groupIds',
},
// Replace the root so we can lookup directly
{
$sort: { updatedAt: -1 },
$replaceRoot: { newRoot: { _id: '$groupIds' } },
},
facetQuery,
// 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]
}
}

const result = await entitiesQueries.getAggregate(aggregateData)
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,
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,
message: CONSTANTS.apiResponses.ASSETS_FETCHED_SUCCESSFULLY,
result: result[0].data,
result: result,
})
} catch (error) {
return reject(error)
Expand Down