added array filter option

This commit is contained in:
2025-05-03 18:13:31 +05:30
parent 5800069e6b
commit dbc82f53a3
9 changed files with 202 additions and 174 deletions

View File

@@ -1,13 +1,13 @@
import { orgModel } from '../organization/organization.schema';
import { getFilterObject, getSortObject, PageQueryParams } from '../pagination';
import { userModel } from '../user/user.schema';
import { generateId } from '../utils/id';
import { orgModel } from "../organization/organization.schema";
import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
import { userModel } from "../user/user.schema";
import { generateId } from "../utils/id";
import {
CreateNotificationInput,
notificationFields,
notificationModel,
UpdateNotificationInput,
} from './notification.schema';
} from "./notification.schema";
export async function createNotification(
input: CreateNotificationInput,
@@ -47,12 +47,12 @@ export async function listNotifications(
const pipeline: any = [
{
$match: { $and: [{ tenantId: tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: tenantId }, ...filterObj] },
},
];
if (params.searchToken && params.searchToken != '') {
const regex = new RegExp(params.searchToken, 'i');
if (params.searchToken && params.searchToken != "") {
const regex = new RegExp(params.searchToken, "i");
pipeline.push({
$match: {
$or: [{ permitNumber: { $regex: regex } }, { link: { $regex: regex } }],
@@ -82,7 +82,7 @@ export async function listNotifications(
},
{
$facet: {
metadata: [{ $count: 'count' }],
metadata: [{ $count: "count" }],
data: [
{ $sort: sortObj },
{ $skip: (page - 1) * pageSize },
@@ -121,12 +121,12 @@ export async function getUniqueValuesNotification(
let values = await notificationModel.distinct(field, { tenantId: tenenatId });
let matchedValues = [];
if (field === 'county.name') {
matchedValues = await orgModel.find().where('name').in(values).exec();
} else if (field === 'client') {
matchedValues = await orgModel.find().where('_id').in(values).exec();
} else if (field === 'assignedTo') {
matchedValues = await userModel.find().where('name').in(values).exec();
if (field === "county.name") {
matchedValues = await orgModel.find().where("name").in(values).exec();
} else if (field === "client") {
matchedValues = await orgModel.find().where("_id").in(values).exec();
} else if (field === "assignedTo") {
matchedValues = await userModel.find().where("name").in(values).exec();
}
if (matchedValues.length > 0) {

View File

@@ -42,7 +42,7 @@ export async function listOrgs(params: PageQueryParams, tenantId: string) {
const filterObj = getFilterObject(params);
const orgs = await orgModel.aggregate([
{ $match: { $and: [{ tenantId: tenantId }, { ...filterObj }] } },
{ $match: { $and: [{ tenantId: tenantId }, ...filterObj] } },
{
$facet: {
metadata: [{ $count: "count" }],
@@ -130,7 +130,7 @@ export async function searchOrgs(params: PageQueryParams, tenantId: string) {
const regex = new RegExp(params.searchToken, "i");
const orgs = await orgModel.aggregate([
{ $match: { $and: [{ tenantId: tenantId }, { ...filterObj }] } },
{ $match: { $and: [{ tenantId: tenantId }, ...filterObj] } },
{
$match: {
$or: [{ name: { $regex: regex } }, { domain: { $regex: regex } }],

View File

@@ -1,5 +1,5 @@
import { z } from 'zod';
import { parse } from './utils/queryParser';
import { z } from "zod";
import { parse } from "./utils/queryParser";
export const pageMetadata = z.object({
count: z.number(),
@@ -23,23 +23,26 @@ export function getSortObject(
) {
const sortObj: Record<string, 1 | -1> = {};
if (params.sort && params.sort != '') {
const sortOptions = params.sort.split(',');
if (params.sort && params.sort != "") {
const sortOptions = params.sort.split(",");
sortOptions.forEach((item) => {
const order = item.startsWith('-') ? -1 : 1;
const field = item.replace('-', '').trim();
const order = item.startsWith("-") ? -1 : 1;
const field = item.replace("-", "").trim();
if (validFields.includes(field)) sortObj[field] = order;
});
}
if (Object.keys(sortObj).length == 0) sortObj['createdAt'] = -1;
if (Object.keys(sortObj).length == 0) sortObj["createdAt"] = -1;
return sortObj;
}
export function getFilterObject(params: PageQueryParams) {
if (params.filter && params.filter != '') {
return parse(params.filter.split(','));
if (params.filter && params.filter != "") {
const parsedQuery = parse(params.filter.split("|")).filter(
(query) => Object.keys(query).length > 0
);
return parsedQuery;
}
}

View File

@@ -63,15 +63,15 @@ export async function listPermits(
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const sortObj = getSortObject(params, permitFields);
const filterObj = getFilterObject(params) || {};
const filterObj = getFilterObject(params) || [];
if (user.role == "client") {
filterObj["client"] = new mongoose.Types.ObjectId(user.orgId);
filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) });
}
const permitsList = await permitModel.aggregate([
{
$match: { $and: [{ tenantId: user.tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: user.tenantId }, ...filterObj] },
},
{
$lookup: {
@@ -216,10 +216,10 @@ export async function searchPermit(
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const sortObj = getSortObject(params, permitFields);
const filterObj = getFilterObject(params) || {};
const filterObj = getFilterObject(params) || [];
if (user.role == "client") {
filterObj["client"] = new mongoose.Types.ObjectId(user.orgId);
filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) });
}
if (!params.searchToken)
@@ -229,7 +229,7 @@ export async function searchPermit(
const permitsList = await permitModel.aggregate([
{
$match: { $and: [{ tenantId: user.tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: user.tenantId }, ...filterObj] },
},
{
$match: {

View File

@@ -41,15 +41,15 @@ export async function listProcessedPermits(
const page = params.page || 1;
const pageSize = params.pageSize || 10;
const sortObj = getSortObject(params, processedFields);
const filterObj = getFilterObject(params) || {};
const filterObj = getFilterObject(params) || [];
if (user.role == "client") {
filterObj["client"] = new mongoose.Types.ObjectId(user.orgId);
filterObj.push({ client: new mongoose.Types.ObjectId(user.orgId) });
}
const pipeline: any = [
{
$match: { $and: [{ tenantId: user.tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: user.tenantId }, ...filterObj] },
},
];

View File

@@ -4,13 +4,13 @@ import {
rtsModel,
UpdateRtsInput,
UploadRtsInput,
} from './rts.schema';
import { AuthenticatedUser } from '../auth';
import { generateId } from '../utils/id';
import { getFilterObject, getSortObject, PageQueryParams } from '../pagination';
import { getUser } from '../user/user.service';
import { orgModel } from '../organization/organization.schema';
import { userModel } from '../user/user.schema';
} from "./rts.schema";
import { AuthenticatedUser } from "../auth";
import { generateId } from "../utils/id";
import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
import { getUser } from "../user/user.service";
import { orgModel } from "../organization/organization.schema";
import { userModel } from "../user/user.schema";
export async function createRts(
input: CreateRtsInput,
@@ -53,9 +53,9 @@ export async function createRts(
export async function getRts(id: string, tenantId: string) {
return await rtsModel
.findOne({ pid: id, tenantId: tenantId })
.populate({ path: 'county', select: 'pid name avatar' })
.populate({ path: 'client', select: 'pid name avatar' })
.populate({ path: 'createdBy', select: 'pid name avatar' });
.populate({ path: "county", select: "pid name avatar" })
.populate({ path: "client", select: "pid name avatar" })
.populate({ path: "createdBy", select: "pid name avatar" });
}
export async function listRts(params: PageQueryParams, tenantId: string) {
@@ -66,30 +66,30 @@ export async function listRts(params: PageQueryParams, tenantId: string) {
const rtsList = await rtsModel.aggregate([
{
$match: { $and: [{ tenantId: tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: tenantId }, ...filterObj] },
},
{
$lookup: {
from: 'organizations',
localField: 'county',
foreignField: '_id',
as: 'countyRec',
from: "organizations",
localField: "county",
foreignField: "_id",
as: "countyRec",
},
},
{
$lookup: {
from: 'organizations',
localField: 'client',
foreignField: '_id',
as: 'clientRec',
from: "organizations",
localField: "client",
foreignField: "_id",
as: "clientRec",
},
},
{
$lookup: {
from: 'users',
localField: 'createdBy',
foreignField: '_id',
as: 'createdRec',
from: "users",
localField: "createdBy",
foreignField: "_id",
as: "createdRec",
},
},
{
@@ -101,36 +101,36 @@ export async function listRts(params: PageQueryParams, tenantId: string) {
createdAt: 1,
county: {
$let: {
vars: { county: { $arrayElemAt: ['$countyRec', 0] } },
vars: { county: { $arrayElemAt: ["$countyRec", 0] } },
in: {
_id: '$$county._id',
pid: '$$county.pid',
name: '$$county.name',
type: '$$county.type',
avatar: '$$county.avatar',
_id: "$$county._id",
pid: "$$county.pid",
name: "$$county.name",
type: "$$county.type",
avatar: "$$county.avatar",
},
},
},
client: {
$let: {
vars: { client: { $arrayElemAt: ['$clientRec', 0] } },
vars: { client: { $arrayElemAt: ["$clientRec", 0] } },
in: {
_id: '$$client._id',
pid: '$$client.pid',
name: '$$client.name',
type: '$$client.type',
avatar: '$$client.avatar',
_id: "$$client._id",
pid: "$$client.pid",
name: "$$client.name",
type: "$$client.type",
avatar: "$$client.avatar",
},
},
},
createdBy: {
$let: {
vars: { created: { $arrayElemAt: ['$createdRec', 0] } },
vars: { created: { $arrayElemAt: ["$createdRec", 0] } },
in: {
_id: '$$created._id',
pid: '$$created.pid',
name: '$$created.name',
avatar: '$$created.avatar',
_id: "$$created._id",
pid: "$$created.pid",
name: "$$created.name",
avatar: "$$created.avatar",
},
},
},
@@ -138,7 +138,7 @@ export async function listRts(params: PageQueryParams, tenantId: string) {
},
{
$facet: {
metadata: [{ $count: 'count' }],
metadata: [{ $count: "count" }],
data: [
{ $sort: sortObj },
{ $skip: (page - 1) * pageSize },
@@ -168,9 +168,9 @@ export async function updateRts(
) {
const updatedRts = await rtsModel
.findOneAndUpdate({ pid: id, tenantId: tenantId }, input, { new: true })
.populate({ path: 'createdBy', select: 'pid name avatar' })
.populate({ path: 'county', select: 'pid name avatar' })
.populate({ path: 'client', select: 'pid name avatar' });
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "county", select: "pid name avatar" })
.populate({ path: "client", select: "pid name avatar" });
return updatedRts;
}
@@ -202,12 +202,12 @@ export async function getUniqueValuesRTS(field: string, tenenatId: string) {
let values = await rtsModel.distinct(field, { tenantId: tenenatId });
let matchedValues = [];
if (field === 'county') {
matchedValues = await orgModel.find().where('_id').in(values).exec();
} else if (field === 'client') {
matchedValues = await orgModel.find().where('_id').in(values).exec();
} else if (field === 'createdBy') {
matchedValues = await userModel.find().where('_id').in(values).exec();
if (field === "county") {
matchedValues = await orgModel.find().where("_id").in(values).exec();
} else if (field === "client") {
matchedValues = await orgModel.find().where("_id").in(values).exec();
} else if (field === "createdBy") {
matchedValues = await userModel.find().where("_id").in(values).exec();
}
if (matchedValues.length > 0) {

View File

@@ -1,14 +1,14 @@
import { AuthenticatedUser } from '../auth';
import { getFilterObject, getSortObject, PageQueryParams } from '../pagination';
import { generateId } from '../utils/id';
import { taskPipeline } from '../utils/pipeline';
import { AuthenticatedUser } from "../auth";
import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
import { generateId } from "../utils/id";
import { taskPipeline } from "../utils/pipeline";
import {
CreateTaskInput,
taskFields,
taskModel,
UpdateTaskInput,
UploadTaskInput,
} from './task.schema';
} from "./task.schema";
export async function createTask(
input: CreateTaskInput,
@@ -31,8 +31,8 @@ export async function createTask(
return await taskModel
.findOne({ pid: task.pid })
.populate({ path: 'createdBy', select: 'pid name avatar' })
.populate({ path: 'assignedTo', select: 'pid name avatar' });
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" });
}
export async function updateTask(
@@ -42,8 +42,8 @@ export async function updateTask(
) {
const updatedTask = await taskModel
.findOneAndUpdate({ tenantId: tenantId, pid: taskId }, input, { new: true })
.populate({ path: 'createdBy', select: 'pid name avatar' })
.populate({ path: 'assignedTo', select: 'pid name avatar' });
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" });
return updatedTask;
}
@@ -51,8 +51,8 @@ export async function updateTask(
export async function getTask(taskId: string, tenantId: string) {
return await taskModel
.findOne({ tenantId: tenantId, pid: taskId })
.populate({ path: 'createdBy', select: 'pid name avatar' })
.populate({ path: 'assignedTo', select: 'pid name avatar' });
.populate({ path: "createdBy", select: "pid name avatar" })
.populate({ path: "assignedTo", select: "pid name avatar" });
}
export async function listTasks(params: PageQueryParams, tenantId: string) {
@@ -63,22 +63,22 @@ export async function listTasks(params: PageQueryParams, tenantId: string) {
const taskList = await taskModel.aggregate([
{
$match: { $and: [{ tenantId: tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: tenantId }, ...filterObj] },
},
{
$lookup: {
from: 'users',
localField: 'createdBy',
foreignField: '_id',
as: 'createdBy',
from: "users",
localField: "createdBy",
foreignField: "_id",
as: "createdBy",
},
},
{
$lookup: {
from: 'users',
localField: 'assignedTo',
foreignField: '_id',
as: 'assignedTo',
from: "users",
localField: "assignedTo",
foreignField: "_id",
as: "assignedTo",
},
},
{
@@ -92,21 +92,21 @@ export async function listTasks(params: PageQueryParams, tenantId: string) {
createdAt: 1,
createdBy: {
$let: {
vars: { createdBy: { $arrayElemAt: ['$createdBy', 0] } },
vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } },
in: {
_id: '$$createdBy._id',
pid: '$$createdBy.pid',
name: '$$createdBy.name',
_id: "$$createdBy._id",
pid: "$$createdBy.pid",
name: "$$createdBy.name",
},
},
},
assignedTo: {
$let: {
vars: { assignedTo: { $arrayElemAt: ['$assignedTo', 0] } },
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } },
in: {
_id: '$$assignedTo._id',
pid: '$$assignedTo.pid',
name: '$$assignedTo.name',
_id: "$$assignedTo._id",
pid: "$$assignedTo.pid",
name: "$$assignedTo.name",
},
},
},
@@ -114,7 +114,7 @@ export async function listTasks(params: PageQueryParams, tenantId: string) {
},
{
$facet: {
metadata: [{ $count: 'count' }],
metadata: [{ $count: "count" }],
data: [
{ $sort: sortObj },
{ $skip: (page - 1) * pageSize },
@@ -147,11 +147,11 @@ export async function searchTasks(params: PageQueryParams, tenantId: string) {
const sortObj = getSortObject(params, taskFields);
const filterObj = getFilterObject(params);
const regex = new RegExp(params.searchToken, 'i');
const regex = new RegExp(params.searchToken, "i");
const taskList = await taskModel.aggregate([
{
$match: { $and: [{ tenantId: tenantId }, { ...filterObj }] },
$match: { $and: [{ tenantId: tenantId }, ...filterObj] },
},
{
$match: {
@@ -160,18 +160,18 @@ export async function searchTasks(params: PageQueryParams, tenantId: string) {
},
{
$lookup: {
from: 'users',
localField: 'createdBy',
foreignField: '_id',
as: 'createdBy',
from: "users",
localField: "createdBy",
foreignField: "_id",
as: "createdBy",
},
},
{
$lookup: {
from: 'users',
localField: 'assignedTo',
foreignField: '_id',
as: 'assignedTo',
from: "users",
localField: "assignedTo",
foreignField: "_id",
as: "assignedTo",
},
},
{
@@ -185,21 +185,21 @@ export async function searchTasks(params: PageQueryParams, tenantId: string) {
createdAt: 1,
createdBy: {
$let: {
vars: { createdBy: { $arrayElemAt: ['$createdBy', 0] } },
vars: { createdBy: { $arrayElemAt: ["$createdBy", 0] } },
in: {
_id: '$$createdBy._id',
pid: '$$createdBy.pid',
name: '$$createdBy.name',
_id: "$$createdBy._id",
pid: "$$createdBy.pid",
name: "$$createdBy.name",
},
},
},
assignedTo: {
$let: {
vars: { assignedTo: { $arrayElemAt: ['$assignedTo', 0] } },
vars: { assignedTo: { $arrayElemAt: ["$assignedTo", 0] } },
in: {
_id: '$$assignedTo._id',
pid: '$$assignedTo.pid',
name: '$$assignedTo.name',
_id: "$$assignedTo._id",
pid: "$$assignedTo.pid",
name: "$$assignedTo.name",
},
},
},
@@ -207,7 +207,7 @@ export async function searchTasks(params: PageQueryParams, tenantId: string) {
},
{
$facet: {
metadata: [{ $count: 'count' }],
metadata: [{ $count: "count" }],
data: [
{ $sort: sortObj },
{ $skip: (page - 1) * pageSize },

View File

@@ -1,9 +1,55 @@
import mongoose from 'mongoose';
import mongoose from "mongoose";
type MongoFilter = Record<string, any>;
function convertValue(value: string) {
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return new Date(value);
} else if (!isNaN(Number(value))) {
return Number(value);
} else if (mongoose.Types.ObjectId.isValid(value)) {
return new mongoose.Types.ObjectId(value);
} else if (value === "true") {
return true;
} else if (value === "false") {
return false;
} else if (value === "null") {
return null;
}
return value;
}
function formulaToMongoFilter(formula: string): MongoFilter {
// Split the formula into parts
// Check for array syntax first (values in parentheses)
const arrayMatch = formula.match(/([^!<>=]+)([!<>=]*)=\(([^)]+)\)/);
if (arrayMatch) {
const [, field, operator, values] = arrayMatch;
const trimmedField = field.trim();
if (trimmedField === "tenantId") return {};
// Split values by comma and trim each value
const valueArray = values
.split(",")
.map((v) => v.trim().replace(/^['"]|['"]$/g, ""));
// Convert each value to appropriate type
const parsedValues = valueArray.map((value) => convertValue(value));
// If no operator or equals operator, use $in
if (!operator || operator === "=") {
return { [trimmedField]: { $in: parsedValues } };
}
// If not equals operator, use $nin
else if (operator === "!=") {
return { [trimmedField]: { $nin: parsedValues } };
}
// Other operators don't make sense with arrays
throw new Error(`Unsupported operator with array values: ${operator}`);
}
// Original single value processing
const operatorMatch = formula.match(/([^!<>=]+)([!<>=]+)(.+)/);
if (!operatorMatch) {
@@ -12,56 +58,35 @@ function formulaToMongoFilter(formula: string): MongoFilter {
const [, field, operator, value] = operatorMatch;
const trimmedField = field.trim();
const trimmedValue = value.trim();
const trimmedValue = value.trim().replace(/^['"]|['"]$/g, "");
if (trimmedField === 'tenantId') return {};
if (trimmedField === "tenantId") return {};
// Convert value to appropriate type (date or number if possible)
let parsedValue: any = trimmedValue;
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmedValue)) {
// Check if it's a date (ISO format)
parsedValue = new Date(trimmedValue);
} else if (!isNaN(Number(trimmedValue))) {
// Check if it's a number
parsedValue = Number(trimmedValue);
} else if (
typeof parsedValue === 'string' &&
mongoose.Types.ObjectId.isValid(parsedValue)
) {
parsedValue = new mongoose.Types.ObjectId(parsedValue);
} else if (parsedValue === 'true') {
parsedValue = true;
} else if (parsedValue === 'false') {
parsedValue = false;
} else if (parsedValue === 'null') {
parsedValue = null;
}
let parsedValue: any = convertValue(trimmedValue);
// Convert operator to MongoDB operator
switch (operator) {
case '=':
case "=":
return { [trimmedField]: { $eq: parsedValue } };
case '!=':
case "!=":
return { [trimmedField]: { $ne: parsedValue } };
case '>':
case ">":
return { [trimmedField]: { $gt: parsedValue } };
case '<':
case "<":
return { [trimmedField]: { $lt: parsedValue } };
case '>=':
case ">=":
return { [trimmedField]: { $gte: parsedValue } };
case '<=':
case "<=":
return { [trimmedField]: { $lte: parsedValue } };
default:
throw new Error(`Unsupported operator: ${operator}`);
}
}
export function parse(formulas: string[]): MongoFilter {
const parsedQuery: MongoFilter = formulas.reduce((filter, formula) => {
const newFilter = formulaToMongoFilter(formula);
return { ...filter, ...newFilter };
}, {});
export function parse(formulas: string[]): Array<MongoFilter> {
const parsedQuery: Array<MongoFilter> = formulas.map((formula) =>
formulaToMongoFilter(formula)
);
return parsedQuery;
}

View File

@@ -1,12 +1,12 @@
import { AuthenticatedUser } from '../auth';
import { getFilterObject, getSortObject, PageQueryParams } from '../pagination';
import { generateId } from '../utils/id';
import { AuthenticatedUser } from "../auth";
import { getFilterObject, getSortObject, PageQueryParams } from "../pagination";
import { generateId } from "../utils/id";
import {
CreateViewInput,
UpdateViewInput,
viewFields,
viewModel,
} from './view.schema';
} from "./view.schema";
export async function createView(
input: CreateViewInput,
@@ -39,7 +39,7 @@ export async function listViews(
$and: [
{ tenantId: user.tenantId },
{ createdBy: user.userId },
{ ...filterObj },
...filterObj,
],
});
}