Last active
September 5, 2023 06:45
-
-
Save r1tsuu/4de3e253cde7bf9693bec0a5ca78735a to your computer and use it in GitHub Desktop.
Find handler with sort by multiple params
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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', | |
}, | |
], | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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