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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ typings/
# Optional npm cache directory
.npm

# yarn lock (npm used)
*yarn.lock

# Optional eslint cache
.eslintcache

Expand Down
186 changes: 162 additions & 24 deletions prisma-filter-common/src/filter.builder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { FilterOperationType, FilterOrder } from './filter.enum';
import { IFilter, ISingleFilter, ISingleOrder } from './filter.interface';
import {
IFilter,
ISingleFilter,
ISingleOrder,
} from './filter.interface';

export class FilterBuilder<T = any> {
/**
Expand All @@ -12,31 +16,65 @@ export class FilterBuilder<T = any> {
public static buildFilterQueryString(filter: IFilter): string {
const parts: string[] = [];

if(filter.offset != null) {
if (filter.offset != null) {
parts.push(`offset=${filter.offset}`);
}
if(filter.limit != null) {
if (filter.limit != null) {
parts.push(`limit=${filter.limit}`);
}
const filterQuery = filter.filter ? FilterBuilder.buildQueryString('filter', filter.filter) : null;
if(filterQuery != null) {
if (filter.page != null) {
parts.push(`page=${filter.page}`);
}
if (filter.skip != null) {
parts.push(`skip=${filter.skip}`);
}
const selectQuery = filter.select
? 'select=' + filter.select.toString()
: null;
if (selectQuery != null) {
parts.push(selectQuery);
}
const filterQuery = filter.filter
? FilterBuilder.buildQueryString('filter', filter.filter)
: null;
if (filterQuery != null) {
parts.push(filterQuery);
}
const orderQuery = filter.order ? FilterBuilder.buildQueryString('order', filter.order) : null;
if(orderQuery != null) {
const orderQuery = filter.order
? FilterBuilder.buildQueryString('order', filter.order)
: null;
if (orderQuery != null) {
parts.push(orderQuery);
}
const sortQuery = filter.sort
? FilterBuilder.buildQueryString('sort', filter.sort)
: null;
if (sortQuery != null) {
parts.push(sortQuery);
}
const cursorQuery = filter.cursor
? 'cursor[field]=' +
filter.cursor.field.toString() +
'&cursor[value]=' +
filter.cursor.value.toString()
: null;
if (cursorQuery != null) {
parts.push(cursorQuery);
}

if(parts.length === 0) return '';
if (parts.length === 0) return '';

return `?${parts.join('&')}`;
}

private static buildQueryString(paramName: string, array: Array<ISingleFilter | ISingleOrder>): string | null {
private static buildQueryString(
paramName: string,
array: Array<ISingleFilter | ISingleOrder>
): string | null {
const parts: Array<string> = [];
for(let i = 0; i < array.length; i++) {
for(const [key, value] of Object.entries(array[i])) {
if(!['field', 'dir', 'type', 'value'].includes(key)) {
for (let i = 0; i < array.length; i++) {
for (const [key, value] of Object.entries(array[i])) {
if (!['field', 'dir', 'type', 'value'].includes(key)) {
continue;
}
/**
Expand All @@ -45,21 +83,33 @@ export class FilterBuilder<T = any> {
* filter[x][field]=<fieldName>
* & filter[x][type]=in
*/
if(Array.isArray(value)) {
if(value.length === 0) {
parts.push(`${encodeURIComponent(paramName)}[${i}][${encodeURIComponent(key)}]=`);
if (Array.isArray(value)) {
if (value.length === 0) {
parts.push(
`${encodeURIComponent(paramName)}[${i}][${encodeURIComponent(
key
)}]=`
);
}
for(let y = 0; y < value.length; y++) {
for (let y = 0; y < value.length; y++) {
/**
* & filter[x][value][y]=<value>
*/
const valueY = value[y];
parts.push(
`${encodeURIComponent(paramName)}[${i}][${encodeURIComponent(key)}][${y}]=${encodeURIComponent(valueY != null ? valueY.toString() : '')}`,
`${encodeURIComponent(paramName)}[${i}][${encodeURIComponent(
key
)}][${y}]=${encodeURIComponent(
valueY != null ? valueY.toString() : ''
)}`
);
}
} else {
parts.push(`${encodeURIComponent(paramName)}[${i}][${encodeURIComponent(key)}]=${encodeURIComponent(value != null ? value.toString() : '')}`);
parts.push(
`${encodeURIComponent(paramName)}[${i}][${encodeURIComponent(
key
)}]=${encodeURIComponent(value != null ? value.toString() : '')}`
);
}
}
}
Expand All @@ -68,6 +118,7 @@ export class FilterBuilder<T = any> {

private readonly filter: IFilter<T> = Object.create(null);

// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor() {}

/**
Expand All @@ -79,15 +130,37 @@ export class FilterBuilder<T = any> {
*
* @returns FilterBuilder for chaining
*/
public addFilter(field: keyof T & string, type: FilterOperationType, value: any): this {
if(this.filter.filter == null) {
public addFilter(
field: keyof T & string,
type: FilterOperationType,
value: any
): this {
if (this.filter.filter == null) {
this.filter.filter = [];
}
this.filter.filter.push({ field, type, value });

return this;
}

/**
* Adds a single filter for one field.
*
* @param field - The name of the field to filter by
* @param type - The type of filter to apply
* @param value - The value to filter for
*
* @returns FilterBuilder for chaining
*/
public addCursor(field: keyof T & string, value: any): this {
if (this.filter.filter == null) {
this.filter.filter = [];
}
this.filter.cursor = { field, value };

return this;
}

/**
* Adds an offset to the result.
*
Expand Down Expand Up @@ -126,6 +199,32 @@ export class FilterBuilder<T = any> {
return this.limitTo(pagesize);
}

/**
* Sets the page
*
* @param page - The page that should be returned
*
* @returns FilterBuilder for chaining
*/
public setPage(page: number): this {
this.filter.page = page;

return this;
}

/**
* Sets the number of records to skip
*
* @param skip - The number of records to skip. Used for cursor based pagination
*
* @returns FilterBuilder for chaining
*/
public skip(skip: number): this {
this.filter.skip = skip;

return this;
}

/**
* Requests a specific page of the result set. Requires {@link setPageSize} to have been called before.
* Automatically calculates the required offset for the given page.
Expand All @@ -136,17 +235,36 @@ export class FilterBuilder<T = any> {
* @returns FilterBuilder for chaining
*/
public requestPage(page: number): this {
if(this.filter.limit == null) {
throw new Error('requestPage can only be called after calling setPageSize');
if (this.filter.limit == null) {
throw new Error(
'requestPage can only be called after calling setPageSize'
);
}
if(page <= 0) {
if (page <= 0) {
throw new Error('Invalid argument: page must be at least 1');
}
this.filter.offset = this.filter.limit * (page - 1);

return this;
}

/**
* Adds field selection to the query. Only selected fields will be requested. nested fields in related records can be added
* using the format 'permissions.id' on a user resource with a many-many relationship with permission
*
* @param field - The name of the field to order by
*
* @returns FilterBuilder for chaining
*/
public addSelectFields(field: string[]): this {
if (this.filter.select == null) {
this.filter.select = [];
}
this.filter.select = this.filter.select.concat(field);

return this;
}

/**
* Adds an ordering to the result.
* If there are multiple entries with the same value in the given field, then later `orderBy`s are used.
Expand All @@ -159,14 +277,34 @@ export class FilterBuilder<T = any> {
* @returns FilterBuilder for chaining
*/
public addOrderBy(field: keyof T & string, dir: FilterOrder): this {
if(this.filter.order == null) {
if (this.filter.order == null) {
this.filter.order = [];
}
this.filter.order.push({ field, dir });

return this;
}

/**
* Adds an sorting to the result.
* If there are multiple entries with the same value in the given field, then later `sortBy`s are used.
* If no additional `sortBy`s are added, then the resulting sort between them is unspecified (and may break pagination).
* Therefore, it is recommended to add a unique sort field in the end (e.g. sort by id if everything else is the same).
*
* @param field - The name of the field to sort by
* @param dir - FilterOrder direction
*
* @returns FilterBuilder for chaining
*/
public addSortBy(field: keyof T & string, dir: FilterOrder): this {
if (this.filter.sort == null) {
this.filter.sort = [];
}
this.filter.sort.push({ field, dir });

return this;
}

/**
* Returns the built filter object.
*
Expand Down
10 changes: 10 additions & 0 deletions prisma-filter-common/src/filter.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ import { FilterOperationType, FilterOrder } from './filter.enum';

export interface IFilter<T = any> {
filter?: Array<ISingleFilter<T>>;
sort?: Array<ISingleOrder<T>>;
order?: Array<ISingleOrder<T>>;
offset?: number;
limit?: number;
page?: number;
cursor?: ISingleCursor<T>;
skip?: number;
select?: string[];
}

export interface ISingleFilter<T = any> {
Expand All @@ -17,3 +22,8 @@ export interface ISingleOrder<T = any> {
field: keyof T & string;
dir: FilterOrder;
}

export interface ISingleCursor<T = any> {
field: keyof T & string;
value: any;
}
1 change: 1 addition & 0 deletions prisma-filter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export class SomeController {
@Query(new DirectFilterPipe<any, Prisma.OrderWhereInput>(
['id', 'status', 'createdAt', 'refundStatus', 'refundedPrice', 'paymentDate', 'totalPrice', 'paymentMethod'],
['event.title', 'user.email', 'user.firstname', 'user.lastname', 'contactAddress.firstName', 'contactAddress.lastName', '!paymentInAdvance'],
['user'] // a list of relations can now be passed to a new FilterPipe to define a set of default included records in the prisma query
)) filterDto: FilterDto<Prisma.OrderWhereInput>,
) {
return this.someService.getOrders(filterDto.findOptions);
Expand Down
22 changes: 17 additions & 5 deletions prisma-filter/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading