Skip to content

Commit

Permalink
feat: add query filters to query response meta data
Browse files Browse the repository at this point in the history
  • Loading branch information
pawelTshDev committed Mar 28, 2024
1 parent 83d2a2c commit 1be66ab
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 58 deletions.
1 change: 1 addition & 0 deletions functions/example-lambda/event.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const exampleLambdaSchema = z.object({
limit: z.string().optional(),
sort: z.record(z.string()).optional(),
filter: z.record(z.string()).optional(),
search: z.string().optional(),
}),
});

Expand Down
30 changes: 6 additions & 24 deletions functions/example-lambda/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,7 @@ import { queryParser } from "../../shared/middleware/query-parser";
import { zodValidator } from "../../shared/middleware/zod-validator";
import { httpCorsConfigured } from "../../shared/middleware/http-cors-configured";
import { httpErrorHandlerConfigured } from "../../shared/middleware/http-error-handler-configured";
import {
calculateSkipFindOption,
isFilterAvailable,
makePaginationResult,
} from "../../shared/pagination-utils/pagination-utils";
import { createFindManyOptions, makePaginationResult } from "../../shared/pagination-utils/pagination-utils";

const connectToDb = dataSource.initialize();
const config = createConfig(process.env);
Expand All @@ -29,27 +25,13 @@ const lambdaHandler = async (event: ExampleLambdaPayload) => {

await connectToDb;

const { page: pageString, limit: limitString, sort, filter } = event.queryStringParameters;
const page = Number(pageString);
const limit = Number(limitString);
const findOptions = {} as any;

if (limit && page) {
findOptions.take = limit;
findOptions.skip = calculateSkipFindOption(page, limit);
}

if (sort && isFilterAvailable(sort, userRepository)) {
findOptions.order = sort;
}

if (filter && isFilterAvailable(filter, userRepository)) {
findOptions.where = filter;
}

const findOptions = createFindManyOptions(userRepository, event.queryStringParameters);
const [data, total] = await userRepository.findAndCount(findOptions);

return awsLambdaResponse(StatusCodes.OK, makePaginationResult(data, total, limit, page));
return awsLambdaResponse(
StatusCodes.OK,
makePaginationResult(data, total, findOptions, event.queryStringParameters.search),
);
};

export const handle = middy()
Expand Down
4 changes: 3 additions & 1 deletion shared/middleware/query-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ export const queryParser = (): middy.MiddlewareObj<APIGatewayProxyEvent, APIGate
const nestedKey = nestedKeyMatch ? nestedKeyMatch[1] : undefined;

if (nestedKey) {
value = { [nestedKey]: queryParams[key] };
resultQuery[queryKey]
? (value = { ...(resultQuery[queryKey] as object), [nestedKey]: queryParams[key] })
: (value = { [nestedKey]: queryParams[key] });
}
}

Expand Down
47 changes: 32 additions & 15 deletions shared/pagination-utils/pagination-utils.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,40 +17,57 @@ describe("pagination-utils", () => {
},
];

const queryFilters = { order: { firstName: "ASC" }, where: { lastName: "Doe" } };

it("returns valid pagination", () => {
const result = makePaginationResult(data, 10, 4, 2);
const result = makePaginationResult(data, 10, { take: 4, skip: 2, ...queryFilters }, "john");
assert.deepEqual(result, {
meta: {
page: 2,
limit: 4,
total: 10,
totalPages: 3,
pagination: {
page: 2,
limit: 4,
total: 10,
totalPages: 3,
},
filter: queryFilters.where,
sort: queryFilters.order,
search: "john",
},
data,
});
});

it("returns valid pagination if limit is 0", () => {
const result = makePaginationResult(data, 10, 0, 2);
const result = makePaginationResult(data, 10, { take: 0, skip: 2, ...queryFilters }, "john");
assert.deepEqual(result, {
meta: {
page: 2,
limit: 0,
total: 10,
totalPages: null,
pagination: {
page: 2,
limit: 0,
total: 10,
totalPages: null,
},
filter: queryFilters.where,
sort: queryFilters.order,
search: "john",
},
data,
});
});

it("returns first page if passed 0 page", () => {
const result = makePaginationResult(data, 10, 5, 2);
const result = makePaginationResult(data, 10, { take: 5, skip: 2, ...queryFilters }, "john");
assert.deepEqual(result, {
meta: {
page: 2,
limit: 5,
total: 10,
totalPages: 2,
pagination: {
page: 2,
limit: 5,
total: 10,
totalPages: 2,
},
filter: queryFilters.where,
sort: queryFilters.order,
search: "john",
},
data,
});
Expand Down
114 changes: 96 additions & 18 deletions shared/pagination-utils/pagination-utils.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,114 @@
import { Repository } from "typeorm";
import { AppError } from "../errors/app.error";
import {
FindManyOptions,
FindOperator,
FindOptionsOrder,
FindOptionsWhere,
Like,
ObjectLiteral,
Repository,
} from "typeorm";

export interface PaginationResult {
export interface PaginationResult<T> {
meta: {
page?: number;
limit?: number;
total: number;
totalPages: number | null;
pagination: {
page?: number;
limit?: number;
total: number;
totalPages: number | null;
};
filter?: FindOptionsWhere<T> | FindOptionsWhere<T>[];
sort?: FindOptionsOrder<T> | FindOptionsOrder<T>[];
search?: string;
};
data: any;
data: T[];
}

export function calculateSkipFindOption(page: number, limit: number) {
export interface PaginationParamsDto {
page?: string;
limit?: string;
sort?: { [key: string]: string };
filter?: { [key: string]: string | string[] };
search?: string;
}

type QueryFilters = { [key: string]: "ASC" | "DESC" } | { [key: string]: string | string[] };

export function calculateSkipFindOption(page: number, limit: number): number {
return (page - 1) * limit;
}

export function isFilterAvailable(filter: any, repository: Repository<any>): boolean {
export function getAvailableFilters<T extends ObjectLiteral>(
filter: QueryFilters,
repository: Repository<T>,
): QueryFilters {
const availableFilters = repository.metadata.columns.map((column) => column.propertyName);
const currentFilters = filter;

Object.keys(currentFilters).forEach((key) => {
if (!availableFilters.includes(key)) {
delete currentFilters[key];
}
});

return currentFilters;
}

export function createFindManyOptions<T extends ObjectLiteral>(
repository: Repository<T>,
queryParams: PaginationParamsDto,
): FindManyOptions<T> {
const { page: pageString, limit: limitString, sort, filter, search } = queryParams;
const page = Number(pageString);
const limit = Number(limitString);
const findOptions: FindManyOptions = {};

if (limit && page) {
findOptions.take = limit;
findOptions.skip = calculateSkipFindOption(page, limit);
}

if (Object.keys(filter).some((key) => availableFilters.includes(key))) {
return true;
if (sort) {
findOptions.order = getAvailableFilters(sort, repository);
}
throw new AppError("Invalid query string");

if (filter) {
findOptions.where = getAvailableFilters(filter, repository);
}

if (search) {
findOptions.where = { ...findOptions.where, email: Like(`%${search}%`) };
}

return findOptions;
}

export function makePaginationResult(data: any, total: number, limit?: number, page?: number): PaginationResult {
export function makePaginationResult<T>(
data: T[],
total: number,
findOptions: FindManyOptions,
search?: string,
): PaginationResult<T> {
const { skip: page, take: limit, order: sort, where: filter } = findOptions;

if (search && filter && !Array.isArray(filter)) {
Object.keys(filter).forEach((key) => {
if (filter[key] instanceof FindOperator) {
delete filter[key];
}
});
}

return {
meta: {
page: page || 1,
limit,
total,
totalPages: limit ? Math.ceil(total / Math.max(limit, 1)) : null,
pagination: {
page: page || 1,
limit,
total,
totalPages: limit ? Math.ceil(total / Math.max(limit, 1)) : null,
},
filter,
sort,
search,
},
data,
};
Expand Down

0 comments on commit 1be66ab

Please sign in to comment.