-
Notifications
You must be signed in to change notification settings - Fork 2
ReadMany
Often times in a project we need to implement filters to retrieve a list of entries. This task can be quite cumbersome even using an ORM like TypeORM. Sometimes we need to filter based on a single field, sometimes we need to filter based on multiple fields. Sometimes we need to filter based on a relationship. Sometimes we need to filter based on a relationship and a field. These various combinations often lead to mulitple methods in the DAO layer and multiple endpoints in the controller layer causing issue with maintainability.
To solve this issue we can use the ReadMany
capabilities provided by
@smoke-trees/postgres-backend
. The ReadMany
capabilities allow you to
- Filter based on a single or multiple fields
- Provide search over a single or multiple fields
- Provide a date filter over created date
- Provide inbuilt pagination support
- Provide a sort functionality
You can use all these functionalities by passing query parameters to the endpoint. The query parameters are as follows:
-
Entity Property: Use entity property like
id
,name
,userId
to filter based on a single field or multiple fields. When multiple fields are present the filter is applied as anAND
operation. -
Search: For search using field like
name
to search for a value in thelike[name]
field. When multiple fields are present the search is applied as anAND
operation. This behaviour can be configured using likeBehaviour parameter. Use%value%
to search for a value in the field. The%
is a wildcard character. -
Date Filter: For created date filer use
fromCreatedDate
andtoCreatedDate
to filter based on the created date. The date should be in ISO8601 format. -
Pagination: For pagination use
page
andcount
for pagination configuration. -
Sort: For sorting use
order
andorderBy
to sort the results. TheorderBy
field should be the entity property and theorder
field should beASC
orDESC
.
Here is a route with all the capabilities:
/address?like[name]=%John%&fromCreatedDate=2021-01-01&toCreatedDate=2021-01-31&page=1&count=10&order=ASC&orderBy=name
The ServiceController
and DAO
class provides a helper method that were used to implement the
ReadMany
capabilities.
The ServiceController
class provides a method parseReadManyQueries
that parses
the query parameters giving back data that can be used to filter the entities.
If you want to add custom filters you can use this method or override it to
add custom filters. The method is as follows:
parseReadManyQuery(query: ParsedQs) {
const {
orderBy,
order,
page,
count,
nonPaginated,
fromCreatedDate,
toCreatedDate,
like,
likeBehaviour,
...filter
} = query;
let pageNumberParsed = parseInt(page?.toString() ?? "1");
let countParsed = parseInt(count?.toString() ?? "10");
let fromCreatedDateDate;
let toCreatedDateDate;
let likeBehaviourParse: "and" | "or" | undefined = likeBehaviour
?.toString()
?.toLowerCase() as "and" | "or" | undefined;
if (likeBehaviourParse !== "or" && likeBehaviourParse !== "and") {
likeBehaviourParse = "and";
}
let orderParsed: string = order?.toString()?.toUpperCase() ?? "DESC";
if (isNaN(pageNumberParsed)) {
pageNumberParsed = 1;
}
if (isNaN(countParsed)) {
countParsed = 10;
}
if (orderParsed !== "DESC" && orderParsed !== "ASC") {
orderParsed = "DESC";
}
if (fromCreatedDate) {
fromCreatedDateDate = new Date(fromCreatedDate.toString());
if (isNaN(fromCreatedDateDate.getTime())) {
fromCreatedDateDate = undefined;
}
}
if (toCreatedDate) {
toCreatedDateDate = new Date(toCreatedDate.toString());
toCreatedDateDate.setDate(toCreatedDateDate.getDate() + 1);
if (isNaN(toCreatedDateDate.getTime())) {
toCreatedDateDate = undefined;
}
}
let nonPaginatedParsed;
if (nonPaginated === "true" && this.optionsPath.readManyWithoutPagination) {
nonPaginatedParsed = true;
} else {
nonPaginatedParsed = false;
}
return {
pageNumberParsed,
countParsed,
orderParsed: orderParsed as "ASC" | "DESC",
orderBy: orderBy?.toString() as keyof BaseEntity,
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
filter: filter as any,
likeBehaviour: likeBehaviourParse,
fromCreatedDateDate,
toCreatedDateDate,
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
like: like as any,
nonPaginated: nonPaginatedParsed,
};
}
Any property that is not used by the ReadMany
capabilities is returned as a filter.
That includes properties of entities that are later used to filter the entities.
The DAO
class provides helpers to filter the entites based on fitler, like,
fromCreatedDate, toCreatedDate and likeBehaviour.
Here are the functions from DAO:
/**
* Parses the filter values. It parses the values like true and false which
* comes from query params as string to boolean and also converts the array
* based filters to In
*/
parseFilter(
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[]
): FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[] {
if (where instanceof Array) {
where = where.map((it) =>
this.parseFilter(it)
) as FindOptionsWhere<Entity>[];
return where;
}
Object.keys(where).forEach((key) => {
if ((where as any)[key] instanceof Array) {
(where as any)[key] = In((where as any)[key]);
}
if ((where as any)[key] === "true") {
(where as any)[key] = true;
}
if ((where as any)[key] === "false") {
(where as any)[key] = false;
}
});
return where;
}
/**
* Parses sort values. Sort object to work across relations need to have
* syntax: { entity: {field: ASC }}
*
*/
parseForSort(field: keyof Entity, order: "ASC" | "DESC") {
const result: any = {};
let level = result;
const sortLevels = field.toString().split(".") || [];
for (let i = sortLevels.length - 1; i >= 0; i--) {
const prop = sortLevels[i];
if (i === sortLevels.length - 1) {
level[prop] = order || "ASC";
} else {
const newLevel = { [prop]: level };
level = newLevel;
}
}
return level;
}
/**
* Parses the like values. It parses the values to ILike for case insensitive
* search. It takes a likeBehaviour which can be 'and' or 'or'. This dictates
* how the like values are combined. If 'and' is passed, all the values are
* combined with AND. If 'or' is passed, all the values are combined with OR.
*/
parseForLike(
like?: { [key: string]: string },
likeBehaviour: "and" | "or" = "and"
) {
let parsedLike: { [key: string]: string } | { [key: string]: string }[];
if (like && likeBehaviour === "and") {
parsedLike = Object.keys(like).reduce((acc, it) => {
acc[it] = ILike((like as any)[it] as string);
return acc;
}, {} as any);
} else if (like && likeBehaviour === "or") {
parsedLike = Object.keys(like).map((it) => ({
[it]: ILike((like as any)[it]),
})) as any;
if ((parsedLike?.length ?? 0) === 0) {
parsedLike = {};
}
} else {
parsedLike = {};
}
return parsedLike;
}
/**
* Parses the date values. It parses the values to MoreThanOrEqual AND
* LessThanOrEqual for date range search. It takes a from and to 2024-03-21
* values. If both are passed, it combines them with Between. If only fromCreatedDate
* is passed, it uses MoreThanOrEqual. If only toCreatedDate is passed, it uses
* LessThanOrEqual.
*/
parseForDates(
field: keyof Entity,
from?: Date | string,
to?: Date | string,
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[]
) {
if (from && to && !(where instanceof Array)) {
if (!where) {
where = {};
}
where = { ...where, [field]: Between(from, to) };
} else if (from && !(where instanceof Array)) {
if (!where) {
where = {};
}
where = { ...where, createdAt: MoreThanOrEqual(from) };
} else if (to && !(where instanceof Array)) {
if (!where) {
where = {};
}
where = { ...where, createdAt: LessThanOrEqual(to) };
}
return where;
}
/**
* Parses the where values. It combines all the values like where, like, fromCreatedDate
* and toCreatedDate to a single where object. It also takes a likeBehaviour which can
* be 'and' or 'or'. This dictates how the like values are combined. If 'and' is passed,
* all the values are combined with AND. If 'or' is passed, all the values are combined
* with OR.
*/
parseWhere(
where?: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[],
like?: { [key: string]: string },
likeBehaviour?: "and" | "or",
fromCreatedDate?: Date,
toCreatedDate?: Date
) {
if (where) {
where = this.parseFilter(where);
}
where = this.parseForDates(
"createdAt",
fromCreatedDate,
toCreatedDate,
where
);
const parsedLike = this.parseForLike(like, likeBehaviour);
where = this.mergeParseLikeWhere(parsedLike, like, likeBehaviour, where);
return where;
}
/**
* Merges the parsed like values with the where values. It takes a parsedLike
* which is the parsed like values, like which is the original like values, likeBehaviour
* which can be 'and' or 'or'.
*/
mergeParseLikeWhere(
parsedLike: { [key: string]: string } | { [key: string]: string }[],
like?: { [key: string]: string },
likeBehaviour?: "and" | "or",
where: FindOptionsWhere<Entity> | FindOptionsWhere<Entity>[] = {}
) {
if (likeBehaviour === "and") {
where = { ...where, ...(parsedLike as any) };
} else if (likeBehaviour === "or" && parsedLike instanceof Array) {
where = parsedLike.map((a) => {
return { ...where, ...a };
}) as FindOptionsWhere<Entity>[];
} else {
where = { ...where, ...(parsedLike as any) };
}
return where;
}
The parseFilter
method is used to parse the filter values. It parses the values
like true and false which comes from query params as string to boolean and also
converts the array based filters to In.
The parseForSort
method is used to parse sort values. Sort object to work across
relations need to have syntax: { entity: {field: ASC }}.
The parseForLike
method is used to parse the like values. It parses the values
to ILike for case insensitive search. It takes a likeBehaviour which can be 'and'
or 'or'. This dictates how the like values are combined. If 'and' is passed, allow
the values are combined with AND. If 'or' is passed, all the values are combined
with OR.
The parseForDates
method is used to parse the date values. It parses the values
to MoreThanOrEqual AND LessThanOrEqual for date range search. It takes a from and
to to values. If both are passed, it combines them with Between. If only
fromCreatedDate is passed, it uses MoreThanOrEqual. If only toCreatedDate is passed,
it uses LessThanOrEqual. You can use this to filter based on date values
The parseWhere
method is used to parse the where values. It combines all the values
like where, like, fromCreatedDate and toCreatedDate to a single where object. It also
takes a likeBehaviour which can be 'and' or 'or'. This dictates how the like values
are combined. If 'and' is passed, all the values are combined with AND. If 'or' is
passed, all the values are combined with OR.
The mergeParseLikeWhere
method is used to merge the parsed like values with the parseWhere
values. It takes a parsedLike which is the parsed like values, like which is the original
like values, likeBehaviour which can be 'and' or 'or'.
You can use these methods to customize the ReadMany
capabilities to add custom filters
to the entities.
With ❤️ from Smoketrees Digital LLP