Skip to content

Instantly share code, notes, and snippets.

@r1tsuu
Last active September 5, 2023 06:45
Show Gist options
  • Save r1tsuu/4de3e253cde7bf9693bec0a5ca78735a to your computer and use it in GitHub Desktop.
Save r1tsuu/4de3e253cde7bf9693bec0a5ca78735a to your computer and use it in GitHub Desktop.
Find handler with sort by multiple params
// Usage example
// http://localhost:3000/api/example?sort[title]=1&sort[createdAt]=1
// or with qs.stringify({sort: {title: 1, createadAt: 1} })
const Pages: CollectionConfig = {
/// ...,
endpoints: [
{
path: '/',
handler: findHandler,
method: 'get',
},
],
}
import { Config, AccessResult } from 'payload/dist/config/types';
import { getLocalizedSortProperty } from 'payload/dist/mongoose/getLocalizedSortProperty';
import { Field } from 'payload/types';
import { Response, NextFunction } from 'express';
import httpStatus from 'http-status';
import { PayloadRequest } from 'payload/dist/express/types';
import { TypeWithID } from 'payload/dist/collections/config/types';
import { PaginatedDocs } from 'payload/dist/mongoose/types';
import { Where } from 'payload/dist/types';
import { isNumber } from 'payload/dist/utilities/isNumber';
import executeAccess from 'payload/dist/auth/executeAccess';
import sanitizeInternalFields from 'payload/dist/utilities/sanitizeInternalFields';
import { Collection } from 'payload/dist/collections/config/types';
import flattenWhereConstraints from 'payload/dist/utilities/flattenWhereConstraints';
import { buildSortParam } from 'payload/dist/mongoose/buildSortParam';
import { afterRead } from 'payload/dist/fields/hooks/afterRead';
import { queryDrafts } from 'payload/dist/versions/drafts/queryDrafts';
import { buildAfterOperation } from 'payload/dist/collections/operations/utils';
type SortObject = { [key: string]: 1 | -1 | 'asc' | 'desc' };
type Args = {
sort: SortObject;
config: Config;
fields: Field[];
locale: string;
};
const buildObjectSortParam = ({ sort: incomingSort, config, fields, locale }: Args): SortObject => {
return Object.entries(incomingSort).reduce<SortObject>((acc, [property, order]) => {
if (property === 'id') {
acc._id = order;
return acc;
}
const localizedProperty = getLocalizedSortProperty({
segments: property.split('.'),
config,
fields,
locale,
});
acc[localizedProperty] = order;
return acc;
}, {});
};
type Sort = string | { [key: string]: -1 | 1 | 'asc' | 'desc' };
export type Arguments = {
collection: Collection;
where?: Where;
page?: number;
limit?: number;
sort?: Sort;
depth?: number;
currentDepth?: number;
req?: PayloadRequest;
overrideAccess?: boolean;
disableErrors?: boolean;
pagination?: boolean;
showHiddenFields?: boolean;
draft?: boolean;
};
export async function find<T extends TypeWithID & Record<string, unknown>>(
incomingArgs: Arguments
): Promise<PaginatedDocs<T>> {
let args = incomingArgs;
// /////////////////////////////////////
// beforeOperation - Collection
// /////////////////////////////////////
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
await priorHook;
args =
(await hook({
args,
operation: 'read',
context: args.req.context,
})) || args;
}, Promise.resolve());
const {
where,
page,
limit,
depth,
currentDepth,
draft: draftsEnabled,
collection,
collection: { Model, config: collectionConfig },
req,
req: { locale, payload },
overrideAccess,
disableErrors,
showHiddenFields,
pagination = true,
} = args;
// /////////////////////////////////////
// Access
// /////////////////////////////////////
let hasNearConstraint = false;
if (where) {
const constraints = flattenWhereConstraints(where);
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'));
}
let accessResult: AccessResult;
if (!overrideAccess) {
accessResult = await executeAccess({ req, disableErrors }, collectionConfig.access.read);
// If errors are disabled, and access returns false, return empty results
if (accessResult === false) {
return {
docs: [],
totalDocs: 0,
totalPages: 1,
page: 1,
pagingCounter: 1,
hasPrevPage: false,
hasNextPage: false,
prevPage: null,
nextPage: null,
limit,
};
}
}
const query = await Model.buildQuery({
req,
where,
overrideAccess,
access: accessResult,
});
// /////////////////////////////////////
// Find
// /////////////////////////////////////
let sort;
if (!hasNearConstraint) {
if (typeof args.sort === 'object') {
sort = buildObjectSortParam({
sort: args.sort,
config: payload.config,
fields: collectionConfig.fields,
locale,
});
} else {
const [sortProperty, sortOrder] = buildSortParam({
sort: args.sort ?? collectionConfig.defaultSort,
config: payload.config,
fields: collectionConfig.fields,
timestamps: collectionConfig.timestamps,
locale,
});
sort = {
[sortProperty]: sortOrder,
};
}
}
const usePagination = pagination && limit !== 0;
const limitToUse = limit ?? (usePagination ? 10 : 0);
let result: PaginatedDocs<T>;
const paginationOptions = {
page: page || 1,
sort,
limit: limitToUse,
lean: true,
leanWithId: true,
pagination: usePagination,
useEstimatedCount: hasNearConstraint,
forceCountFn: hasNearConstraint,
options: {
// limit must also be set here, it's ignored when pagination is false
limit: limitToUse,
},
};
if (collectionConfig.versions?.drafts && draftsEnabled) {
result = await queryDrafts<T>({
accessResult,
collection,
req,
overrideAccess,
paginationOptions,
payload,
where,
});
} else {
// @ts-ignore
result = await Model.paginate(query, paginationOptions);
}
result = {
...result,
docs: result.docs.map((doc) => {
const sanitizedDoc = JSON.parse(JSON.stringify(doc));
sanitizedDoc.id = sanitizedDoc._id;
return sanitizeInternalFields(sanitizedDoc);
}),
};
// /////////////////////////////////////
// beforeRead - Collection
// /////////////////////////////////////
result = {
...result,
docs: await Promise.all(
result.docs.map(async (doc) => {
let docRef = doc;
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook;
docRef = (await hook({ req, query, doc: docRef, context: req.context })) || docRef;
}, Promise.resolve());
return docRef;
})
),
};
// /////////////////////////////////////
// afterRead - Fields
// /////////////////////////////////////
result = {
...result,
docs: await Promise.all(
result.docs.map(async (doc) =>
afterRead<T>({
depth,
currentDepth,
doc,
entityConfig: collectionConfig,
overrideAccess,
req,
showHiddenFields,
findMany: true,
context: req.context,
})
)
),
};
// /////////////////////////////////////
// afterRead - Collection
// /////////////////////////////////////
result = {
...result,
docs: await Promise.all(
result.docs.map(async (doc) => {
let docRef = doc;
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
await priorHook;
docRef =
(await hook({ req, query, doc: docRef, findMany: true, context: req.context })) || doc;
}, Promise.resolve());
return docRef;
})
),
};
// /////////////////////////////////////
// afterOperation - Collection
// /////////////////////////////////////
result = await buildAfterOperation<T>({
operation: 'find',
// @ts-ignore
args,
result,
});
// /////////////////////////////////////
// Return results
// /////////////////////////////////////
return result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default async function findHandler<T extends TypeWithID = any>(
req: PayloadRequest,
res: Response,
next: NextFunction
): Promise<Response<PaginatedDocs<T>> | void> {
try {
let page: number | undefined;
if (typeof req.query.page === 'string') {
const parsedPage = parseInt(req.query.page, 10);
if (!Number.isNaN(parsedPage)) {
page = parsedPage;
}
}
const result = await find({
req,
collection: req.collection,
where: req.query.where as Where, // This is a little shady
page,
limit: isNumber(req.query.limit) ? Number(req.query.limit) : undefined,
sort: req.query.sort as Sort,
depth: isNumber(req.query.depth) ? Number(req.query.depth) : undefined,
draft: req.query.draft === 'true',
});
return res.status(httpStatus.OK).json(result);
} catch (error) {
return next(error);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment