Skip to content

Commit

Permalink
PIMS-1868 More Quickfilter Fixes (#2520)
Browse files Browse the repository at this point in the history
Co-authored-by: GrahamS-Quartech <[email protected]>
  • Loading branch information
dbarkowsky and GrahamS-Quartech authored Jul 15, 2024
1 parent 9c9c584 commit c5ceb41
Show file tree
Hide file tree
Showing 29 changed files with 634 additions and 289 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,11 @@ export const getAdministrativeAreas = async (req: Request, res: Response) => {
if (filter.success) {
const adminAreas = await administrativeAreasServices.getAdministrativeAreas(filter.data);
if (!ssoUser.hasRoles([Roles.ADMIN])) {
const trimmed = AdministrativeAreaPublicResponseSchema.array().parse(adminAreas);
return res.status(200).send(trimmed);
const trimmed = AdministrativeAreaPublicResponseSchema.array().parse(adminAreas.data);
return res.status(200).send({
...adminAreas,
data: trimmed,
});
}
return res.status(200).send(adminAreas);
} else {
Expand Down
8 changes: 5 additions & 3 deletions express-api/src/controllers/agencies/agenciesController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ export const getAgencies = async (req: Request, res: Response) => {
const ssoUser = req.user;
const filter = AgencyFilterSchema.safeParse(req.query);
if (filter.success) {
const includeRelations = req.query.includeRelations === 'true';
const agencies = await agencyService.getAgencies(filter.data, includeRelations);
const agencies = await agencyService.getAgencies(filter.data);
if (!ssoUser.client_roles || !ssoUser.client_roles.includes(Roles.ADMIN)) {
const trimmed = AgencyPublicResponseSchema.array().parse(agencies);
return res.status(200).send(trimmed);
return res.status(200).send({
...agencies,
data: trimmed,
});
}
return res.status(200).send(agencies);
} else {
Expand Down
3 changes: 1 addition & 2 deletions express-api/src/controllers/projects/projectsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,6 @@ export const searchProjects = async (req: Request, res: Response) => {
*/
export const filterProjects = async (req: Request, res: Response) => {
const filter = ProjectFilterSchema.safeParse(req.query);
const includeRelations = req.query.includeRelations === 'true';
const forExcelExport = req.query.excelExport === 'true';
if (!filter.success) {
return res.status(400).send('Could not parse filter.');
Expand All @@ -320,7 +319,7 @@ export const filterProjects = async (req: Request, res: Response) => {
}
// Get projects associated with agencies of the requesting user
const projects = forExcelExport
? await projectServices.getProjectsForExport(filterResult as ProjectFilter, includeRelations)
? await projectServices.getProjectsForExport(filterResult as ProjectFilter)
: await projectServices.getProjects(filterResult as ProjectFilter);
return res.status(200).send(projects);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ export const AdministrativeAreaFilterSchema = z.object({
name: z.string().optional(),
provinceId: z.string().optional(),
regionalDistrictId: z.number().int().optional(),
regionalDistrict: z.string().optional(),
regionalDistrictName: z.string().optional(),
isDisabled: z.string().optional(),
updatedOn: z.string().optional(),
createdOn: z.string().optional(),
quickFilter: z.string().optional(),
});

export const AdministrativeAreaPublicResponseSchema = z.object({
Id: z.number(),
Name: z.string(),
RegionalDistrictName: z.string(),
ProvinceId: z.string(),
CreatedOn: z.date(),
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,44 +1,76 @@
import { AppDataSource } from '@/appDataSource';
import { AdministrativeArea } from '@/typeorm/Entities/AdministrativeArea';
import { AdministrativeAreaFilter } from './administrativeAreaSchema';
import { DeepPartial, FindOptionsOrder, FindOptionsOrderValue } from 'typeorm';
import { Brackets, DeepPartial, FindOptionsWhere } from 'typeorm';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { constructFindOptionFromQuery } from '@/utilities/helperFunctions';
import {
constructFindOptionFromQuery,
constructFindOptionFromQueryBoolean,
} from '@/utilities/helperFunctions';
import { AdministrativeAreaJoinView } from '@/typeorm/Entities/views/AdministrativeAreaJoinView';
import { SortOrders } from '@/constants/types';
import logger from '@/utilities/winstonLogger';

const sortKeyMapping = (
sortKey: string,
sortDirection: FindOptionsOrderValue,
): FindOptionsOrder<AdministrativeArea> => {
switch (sortKey) {
case 'RegionalDistrict':
return { RegionalDistrict: { Name: sortDirection } };
default:
return { [sortKey]: sortDirection };
}
const sortKeyTranslator: Record<string, string> = {
Name: 'name',
IsDisabled: 'is_disabled',
RegionalDistrictName: 'regional_district_name',
CreatedOn: 'created_on',
UpdatedOn: 'updated_on',
};

const collectFindOptions = (filter: AdministrativeAreaFilter) => {
const options = [];
if (filter.name) options.push(constructFindOptionFromQuery('Name', filter.name));
if (filter.regionalDistrict)
options.push({
RegionalDistrict: constructFindOptionFromQuery('Name', filter.regionalDistrict),
});
if (filter.regionalDistrictName)
options.push(constructFindOptionFromQuery('RegionalDistrictName', filter.regionalDistrictName));
if (filter.isDisabled)
options.push(constructFindOptionFromQuery('IsDisabled', filter.isDisabled));
options.push(constructFindOptionFromQueryBoolean('IsDisabled', filter.isDisabled));
if (filter.createdOn) options.push(constructFindOptionFromQuery('CreatedOn', filter.createdOn));
if (filter.updatedOn) options.push(constructFindOptionFromQuery('UpdatedOn', filter.updatedOn));
return options;
};

const getAdministrativeAreas = (filter: AdministrativeAreaFilter) => {
return AppDataSource.getRepository(AdministrativeArea).find({
relations: {
RegionalDistrict: true,
},
where: collectFindOptions(filter),
take: filter.quantity,
skip: (filter.quantity ?? 0) * (filter.page ?? 0),
order: sortKeyMapping(filter.sortKey, filter.sortOrder as FindOptionsOrderValue),
});
const getAdministrativeAreas = async (filter: AdministrativeAreaFilter) => {
const options = collectFindOptions(filter);
const query = AppDataSource.getRepository(AdministrativeAreaJoinView)
.createQueryBuilder()
.where(
new Brackets((qb) => {
options.forEach((option) => qb.orWhere(option));
}),
);

// Add quickfilter part
if (filter.quickFilter) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const quickFilterOptions: FindOptionsWhere<any>[] = [];
const quickfilterFields = ['Name', 'RegionalDistrictName', 'CreatedOn'];
quickfilterFields.forEach((field) =>
quickFilterOptions.push(constructFindOptionFromQuery(field, filter.quickFilter)),
);
query.andWhere(
new Brackets((qb) => {
quickFilterOptions.forEach((option) => qb.orWhere(option));
}),
);
}

if (filter.quantity) query.take(filter.quantity);
if (filter.page && filter.quantity) query.skip((filter.page ?? 0) * (filter.quantity ?? 0));
if (filter.sortKey && filter.sortOrder) {
if (sortKeyTranslator[filter.sortKey]) {
query.orderBy(
sortKeyTranslator[filter.sortKey],
filter.sortOrder.toUpperCase() as SortOrders,
'NULLS LAST',
);
} else {
logger.error('getAdministrativeAreas Service - Invalid Sort Key');
}
}
const [data, totalCount] = await query.getManyAndCount();
return { data, totalCount };
};

const addAdministrativeArea = async (adminArea: AdministrativeArea) => {
Expand Down
5 changes: 4 additions & 1 deletion express-api/src/services/agencies/agencySchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const AgencyCreationSchema = z.object({
export const AgencyFilterSchema = z.object({
name: z.string().optional(),
parentId: z.coerce.number().int().optional(),
parent: z.string().optional(),
parentName: z.string().optional(),
isDisabled: z.string().optional(),
sortOrder: z.string().optional(),
page: z.coerce.number().optional(),
Expand All @@ -32,9 +32,11 @@ export const AgencyFilterSchema = z.object({
id: z.coerce.number().optional(),
status: z.string().optional(),
email: z.string().optional(),
sendEmail: z.string().optional(),
updatedOn: z.string().optional(),
createdOn: z.string().optional(),
code: z.string().optional(),
quickFilter: z.string().optional(),
});

export const AgencyPublicResponseSchema = z.object({
Expand All @@ -45,6 +47,7 @@ export const AgencyPublicResponseSchema = z.object({
Description: z.string().nullable(),
IsDisabled: z.boolean(),
ParentId: z.number().int().nullable(),
ParentName: z.string(),
});

export type Agency = z.infer<typeof AgencyCreationSchema>;
Expand Down
91 changes: 64 additions & 27 deletions express-api/src/services/agencies/agencyServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,88 @@ import { AppDataSource } from '@/appDataSource';
import { Agency } from '@/typeorm/Entities/Agency';
import { ErrorWithCode } from '@/utilities/customErrors/ErrorWithCode';
import { AgencyFilter } from './agencySchema';
import { constructFindOptionFromQuery } from '@/utilities/helperFunctions';
import { FindOptionsOrderValue, FindOptionsOrder } from 'typeorm';
import {
constructFindOptionFromQuery,
constructFindOptionFromQueryBoolean,
} from '@/utilities/helperFunctions';
import { Brackets, FindOptionsWhere } from 'typeorm';
import logger from '@/utilities/winstonLogger';
import { SortOrders } from '@/constants/types';
import { AgencyJoinView } from '@/typeorm/Entities/views/AgencyJoinView';

const agencyRepo = AppDataSource.getRepository(Agency);

const collectFindOptions = (filter: AgencyFilter) => {
const options = [];
if (filter.name) options.push(constructFindOptionFromQuery('Name', filter.name));
if (filter.parent) options.push({ Parent: constructFindOptionFromQuery('Name', filter.parent) });
if (filter.isDisabled)
options.push(constructFindOptionFromQuery('IsDisabled', filter.isDisabled));
if (filter.code) options.push(constructFindOptionFromQuery('Code', filter.code));
if (filter.isDisabled)
options.push(constructFindOptionFromQueryBoolean('IsDisabled', filter.isDisabled));
if (filter.parentName)
options.push(constructFindOptionFromQuery('ParentName', filter.parentName));
if (filter.sendEmail)
options.push(constructFindOptionFromQueryBoolean('SendEmail', filter.sendEmail));
if (filter.email) options.push(constructFindOptionFromQuery('Email', filter.email));
if (filter.createdOn) options.push(constructFindOptionFromQuery('CreatedOn', filter.createdOn));
if (filter.updatedOn) options.push(constructFindOptionFromQuery('UpdatedOn', filter.updatedOn));
return options;
};

const sortKeyMapping = (
sortKey: string,
sortDirection: FindOptionsOrderValue,
): FindOptionsOrder<Agency> => {
switch (sortKey) {
case 'Parent':
return { Parent: { Name: sortDirection } };
default:
return { [sortKey]: sortDirection };
}
const sortKeyTranslator: Record<string, string> = {
Name: 'name',
Code: 'code',
IsDisabled: 'is_disabled',
ParentName: 'parent_name',
SendEmail: 'send_email',
Email: 'email',
CreatedOn: 'created_on',
UpdatedOn: 'updated_on',
};

/**
* @description Gets and returns a list of all agencies.
* @returns { Agency[] } A list of all agencies in the database
*/
export const getAgencies = async (filter: AgencyFilter, includeRelations: boolean = false) => {
const allAgencies = await agencyRepo.find({
where: collectFindOptions(filter),
relations: {
Parent: includeRelations,
},
take: filter.quantity,
skip: (filter.quantity ?? 0) * (filter.page ?? 0),
order: sortKeyMapping(filter.sortKey, filter.sortOrder as FindOptionsOrderValue),
});
return allAgencies;
export const getAgencies = async (filter: AgencyFilter) => {
const options = collectFindOptions(filter);
const query = AppDataSource.getRepository(AgencyJoinView)
.createQueryBuilder()
.where(
new Brackets((qb) => {
options.forEach((option) => qb.orWhere(option));
}),
);

// Add quickfilter part
if (filter.quickFilter) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const quickFilterOptions: FindOptionsWhere<any>[] = [];
const quickfilterFields = ['Name', 'Code', 'ParentName', 'Email', 'UpdatedOn', 'CreatedOn'];
quickfilterFields.forEach((field) =>
quickFilterOptions.push(constructFindOptionFromQuery(field, filter.quickFilter)),
);
query.andWhere(
new Brackets((qb) => {
quickFilterOptions.forEach((option) => qb.orWhere(option));
}),
);
}

if (filter.quantity) query.take(filter.quantity);
if (filter.page && filter.quantity) query.skip((filter.page ?? 0) * (filter.quantity ?? 0));
if (filter.sortKey && filter.sortOrder) {
if (sortKeyTranslator[filter.sortKey]) {
query.orderBy(
sortKeyTranslator[filter.sortKey],
filter.sortOrder.toUpperCase() as SortOrders,
'NULLS LAST',
);
} else {
logger.error('getAgencies Service - Invalid Sort Key');
}
}
const [data, totalCount] = await query.getManyAndCount();
return { data, totalCount };
};

/**
Expand Down Expand Up @@ -84,7 +121,7 @@ export const getAgencyById = async (agencyId: number) => {
* @returns Status and information on updated agency.
*/
export const updateAgencyById = async (agencyIn: Agency) => {
const agencies = await getAgencies({});
const { data: agencies } = await getAgencies({});
const findAgency = agencies.find((agency) => agency.Id === agencyIn.Id);
if (findAgency == null) {
throw new ErrorWithCode('Agency not found.', 404);
Expand Down
4 changes: 4 additions & 0 deletions express-api/src/services/projects/projectSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ export const ProjectFilterSchema = z.object({
agency: z.string().optional(),
page: z.coerce.number().optional(),
updatedOn: z.string().optional(),
updatedBy: z.string().optional(),
market: z.string().optional(),
netBook: z.string().optional(),
quantity: z.coerce.number().optional(),
sortKey: z.string().optional(),
sortOrder: z.string().optional(),
quickFilter: z.string().optional(),
});

export type ProjectFilter = z.infer<typeof ProjectFilterSchema>;
Loading

0 comments on commit c5ceb41

Please sign in to comment.