perf: operations performance optimizations (#10609)
- reduces unnecessary shallow copying within operations by removing unnecessary spreads or .map()'s - removes unnecessary `deleteMany` call in `deleteUserPreferences` for auth-enabled collections - replaces all instances of `validOperators.includes` with `validOperatorMap[]`. O(n) => O(1) - optimizes the `sanitizeInternalFields` function. Previously, it was doing a **lot** of shallow copying
This commit is contained in:
@@ -2,7 +2,7 @@ import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
|
||||
|
||||
import { Types } from 'mongoose'
|
||||
import { getLocalizedPaths } from 'payload'
|
||||
import { validOperators } from 'payload/shared'
|
||||
import { validOperatorSet } from 'payload/shared'
|
||||
|
||||
import type { MongooseAdapter } from '../index.js'
|
||||
|
||||
@@ -187,7 +187,7 @@ export async function buildSearchParam({
|
||||
return relationshipQuery
|
||||
}
|
||||
|
||||
if (formattedOperator && validOperators.includes(formattedOperator as Operator)) {
|
||||
if (formattedOperator && validOperatorSet.has(formattedOperator as Operator)) {
|
||||
const operatorKey = operatorMap[formattedOperator]
|
||||
|
||||
if (field.type === 'relationship' || field.type === 'upload') {
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { FilterQuery } from 'mongoose'
|
||||
import type { FlattenedField, Operator, Payload, Where } from 'payload'
|
||||
|
||||
import { deepMergeWithCombinedArrays } from 'payload'
|
||||
import { validOperators } from 'payload/shared'
|
||||
import { validOperatorSet } from 'payload/shared'
|
||||
|
||||
import { buildAndOrConditions } from './buildAndOrConditions.js'
|
||||
import { buildSearchParam } from './buildSearchParams.js'
|
||||
@@ -53,7 +53,7 @@ export async function parseParams({
|
||||
const pathOperators = where[relationOrPath]
|
||||
if (typeof pathOperators === 'object') {
|
||||
for (const operator of Object.keys(pathOperators)) {
|
||||
if (validOperators.includes(operator as Operator)) {
|
||||
if (validOperatorSet.has(operator as Operator)) {
|
||||
const searchParam = await buildSearchParam({
|
||||
collectionSlug,
|
||||
fields,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { FlattenedField, Operator, Where } from 'payload'
|
||||
import { and, isNotNull, isNull, ne, notInArray, or, sql } from 'drizzle-orm'
|
||||
import { PgUUID } from 'drizzle-orm/pg-core'
|
||||
import { QueryError } from 'payload'
|
||||
import { validOperators } from 'payload/shared'
|
||||
import { validOperatorSet } from 'payload/shared'
|
||||
|
||||
import type { DrizzleAdapter, GenericColumn } from '../types.js'
|
||||
import type { BuildQueryJoinAliases } from './buildQuery.js'
|
||||
@@ -73,7 +73,7 @@ export function parseParams({
|
||||
const pathOperators = where[relationOrPath]
|
||||
if (typeof pathOperators === 'object') {
|
||||
for (let operator of Object.keys(pathOperators)) {
|
||||
if (validOperators.includes(operator as Operator)) {
|
||||
if (validOperatorSet.has(operator as Operator)) {
|
||||
const val = where[relationOrPath][operator]
|
||||
|
||||
const {
|
||||
|
||||
@@ -10,7 +10,10 @@ type GetAccessResultsArgs = {
|
||||
export async function getAccessResults({
|
||||
req,
|
||||
}: GetAccessResultsArgs): Promise<SanitizedPermissions> {
|
||||
const results = {} as Permissions
|
||||
const results = {
|
||||
collections: {},
|
||||
globals: {},
|
||||
} as Permissions
|
||||
const { payload, user } = req
|
||||
|
||||
const isLoggedIn = !!user
|
||||
@@ -49,10 +52,7 @@ export async function getAccessResults({
|
||||
operations: collectionOperations,
|
||||
req,
|
||||
})
|
||||
results.collections = {
|
||||
...results.collections,
|
||||
[collection.slug]: collectionPolicy,
|
||||
}
|
||||
results.collections[collection.slug] = collectionPolicy
|
||||
}),
|
||||
)
|
||||
|
||||
@@ -70,10 +70,7 @@ export async function getAccessResults({
|
||||
operations: globalOperations,
|
||||
req,
|
||||
})
|
||||
results.globals = {
|
||||
...results.globals,
|
||||
[global.slug]: globalPolicy,
|
||||
}
|
||||
results.globals[global.slug] = globalPolicy
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@@ -47,6 +47,8 @@ export type Arguments = {
|
||||
where?: Where
|
||||
}
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
|
||||
export const findOperation = async <
|
||||
TSlug extends CollectionSlug,
|
||||
TSelect extends SelectFromCollectionSlug<TSlug>,
|
||||
@@ -189,11 +191,12 @@ export const findOperation = async <
|
||||
try {
|
||||
const lockDocumentsProp = collectionConfig?.lockDocuments
|
||||
|
||||
const lockDurationDefault = 300 // Default 5 minutes in seconds
|
||||
const lockDuration =
|
||||
typeof lockDocumentsProp === 'object' ? lockDocumentsProp.duration : lockDurationDefault
|
||||
const lockDurationInMilliseconds = lockDuration * 1000
|
||||
|
||||
const now = new Date().getTime()
|
||||
|
||||
const lockedDocuments = await payload.find({
|
||||
collection: 'payload-locked-documents',
|
||||
depth: 1,
|
||||
@@ -216,14 +219,13 @@ export const findOperation = async <
|
||||
// Query where the lock is newer than the current time minus lock time
|
||||
{
|
||||
updatedAt: {
|
||||
greater_than: new Date(new Date().getTime() - lockDurationInMilliseconds),
|
||||
greater_than: new Date(now - lockDurationInMilliseconds),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const now = new Date().getTime()
|
||||
const lockedDocs = Array.isArray(lockedDocuments?.docs) ? lockedDocuments.docs : []
|
||||
|
||||
// Filter out stale locks
|
||||
@@ -232,20 +234,16 @@ export const findOperation = async <
|
||||
return lastEditedAt + lockDurationInMilliseconds > now
|
||||
})
|
||||
|
||||
result.docs = result.docs.map((doc) => {
|
||||
for (const doc of result.docs) {
|
||||
const lockedDoc = validLockedDocs.find((lock) => lock?.document?.value === doc.id)
|
||||
return {
|
||||
...doc,
|
||||
_isLocked: !!lockedDoc,
|
||||
_userEditing: lockedDoc ? lockedDoc?.user?.value : null,
|
||||
}
|
||||
})
|
||||
doc._isLocked = !!lockedDoc
|
||||
doc._userEditing = lockedDoc ? lockedDoc?.user?.value : null
|
||||
}
|
||||
} catch (error) {
|
||||
result.docs = result.docs.map((doc) => ({
|
||||
...doc,
|
||||
_isLocked: false,
|
||||
_userEditing: null,
|
||||
}))
|
||||
for (const doc of result.docs) {
|
||||
doc._isLocked = false
|
||||
doc._userEditing = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,9 +251,8 @@ export const findOperation = async <
|
||||
// beforeRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(
|
||||
if (collectionConfig?.hooks?.beforeRead?.length) {
|
||||
result.docs = await Promise.all(
|
||||
result.docs.map(async (doc) => {
|
||||
let docRef = doc
|
||||
|
||||
@@ -274,45 +271,41 @@ export const findOperation = async <
|
||||
|
||||
return docRef
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(
|
||||
result.docs.map(async (doc) =>
|
||||
afterRead<DataFromCollectionSlug<TSlug>>({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft: draftsEnabled,
|
||||
fallbackLocale,
|
||||
findMany: true,
|
||||
global: null,
|
||||
locale,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
select,
|
||||
showHiddenFields,
|
||||
}),
|
||||
),
|
||||
result.docs = await Promise.all(
|
||||
result.docs.map(async (doc) =>
|
||||
afterRead<DataFromCollectionSlug<TSlug>>({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
currentDepth,
|
||||
depth,
|
||||
doc,
|
||||
draft: draftsEnabled,
|
||||
fallbackLocale,
|
||||
findMany: true,
|
||||
global: null,
|
||||
locale,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
select,
|
||||
showHiddenFields,
|
||||
}),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(
|
||||
if (collectionConfig?.hooks?.afterRead?.length) {
|
||||
result.docs = await Promise.all(
|
||||
result.docs.map(async (doc) => {
|
||||
let docRef = doc
|
||||
|
||||
@@ -332,7 +325,7 @@ export const findOperation = async <
|
||||
|
||||
return docRef
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -87,70 +87,62 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
|
||||
// /////////////////////////////////////
|
||||
// beforeRead - Collection
|
||||
// /////////////////////////////////////
|
||||
const result: PaginatedDocs<TData> = paginatedDocs as unknown as PaginatedDocs<TData>
|
||||
result.docs = (await Promise.all(
|
||||
paginatedDocs.docs.map(async (doc) => {
|
||||
const docRef = doc
|
||||
// Fallback if not selected
|
||||
if (!docRef.version) {
|
||||
;(docRef as any).version = {}
|
||||
}
|
||||
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
let result = {
|
||||
...paginatedDocs,
|
||||
docs: await Promise.all(
|
||||
paginatedDocs.docs.map(async (doc) => {
|
||||
const docRef = doc
|
||||
// Fallback if not selected
|
||||
if (!docRef.version) {
|
||||
;(docRef as any).version = {}
|
||||
}
|
||||
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
docRef.version =
|
||||
(await hook({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
doc: docRef.version,
|
||||
query: fullWhere,
|
||||
req,
|
||||
})) || docRef.version
|
||||
}, Promise.resolve())
|
||||
|
||||
return docRef
|
||||
}),
|
||||
),
|
||||
} as PaginatedDocs<TData>
|
||||
docRef.version =
|
||||
(await hook({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
doc: docRef.version,
|
||||
query: fullWhere,
|
||||
req,
|
||||
})) || docRef.version
|
||||
}, Promise.resolve())
|
||||
|
||||
return docRef
|
||||
}),
|
||||
)) as TData[]
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(
|
||||
result.docs.map(async (data) => ({
|
||||
...data,
|
||||
version: await afterRead({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
depth,
|
||||
doc: data.version,
|
||||
draft: undefined,
|
||||
fallbackLocale,
|
||||
findMany: true,
|
||||
global: null,
|
||||
locale,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
select: typeof select?.version === 'object' ? select.version : undefined,
|
||||
showHiddenFields,
|
||||
}),
|
||||
})),
|
||||
),
|
||||
}
|
||||
result.docs = await Promise.all(
|
||||
result.docs.map(async (data) => {
|
||||
data.version = await afterRead({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
depth,
|
||||
doc: data.version,
|
||||
draft: undefined,
|
||||
fallbackLocale,
|
||||
findMany: true,
|
||||
global: null,
|
||||
locale,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
select: typeof select?.version === 'object' ? select.version : undefined,
|
||||
showHiddenFields,
|
||||
})
|
||||
return data
|
||||
}),
|
||||
)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = {
|
||||
...result,
|
||||
docs: await Promise.all(
|
||||
if (collectionConfig.hooks.afterRead?.length) {
|
||||
result.docs = await Promise.all(
|
||||
result.docs.map(async (doc) => {
|
||||
const docRef = doc
|
||||
|
||||
@@ -170,17 +162,13 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
|
||||
|
||||
return docRef
|
||||
}),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = {
|
||||
...result,
|
||||
docs: result.docs.map((doc) => sanitizeInternalFields<TData>(doc)),
|
||||
}
|
||||
result.docs = result.docs.map((doc) => sanitizeInternalFields<TData>(doc))
|
||||
|
||||
return result
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { Operator, PayloadRequest, Where, WhereField } from '../../types/in
|
||||
import type { EntityPolicies } from './types.js'
|
||||
|
||||
import { QueryError } from '../../errors/QueryError.js'
|
||||
import { validOperators } from '../../types/constants.js'
|
||||
import { validOperatorSet } from '../../types/constants.js'
|
||||
import { validateSearchParam } from './validateSearchParams.js'
|
||||
|
||||
type Args = {
|
||||
@@ -58,10 +58,11 @@ export async function validateQueryPaths({
|
||||
const whereFields = flattenWhere(where)
|
||||
// We need to determine if the whereKey is an AND, OR, or a schema path
|
||||
const promises = []
|
||||
void whereFields.map((constraint) => {
|
||||
void Object.keys(constraint).map((path) => {
|
||||
void Object.entries(constraint[path]).map(([operator, val]) => {
|
||||
if (validOperators.includes(operator as Operator)) {
|
||||
for (const constraint of whereFields) {
|
||||
for (const path in constraint) {
|
||||
for (const operator in constraint[path]) {
|
||||
const val = constraint[path][operator]
|
||||
if (validOperatorSet.has(operator as Operator)) {
|
||||
promises.push(
|
||||
validateSearchParam({
|
||||
collectionConfig,
|
||||
@@ -78,9 +79,10 @@ export async function validateQueryPaths({
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises)
|
||||
if (errors.length > 0) {
|
||||
throw new QueryError(errors)
|
||||
|
||||
@@ -37,7 +37,7 @@ export { getFieldPaths } from '../fields/getFieldPaths.js'
|
||||
|
||||
export * from '../fields/validations.js'
|
||||
|
||||
export { validOperators } from '../types/constants.js'
|
||||
export { validOperators, validOperatorSet } from '../types/constants.js'
|
||||
|
||||
export { formatFilesize } from '../uploads/formatFilesize.js'
|
||||
|
||||
|
||||
@@ -17,22 +17,30 @@ export const deleteUserPreferences = async ({ collectionConfig, ids, payload, re
|
||||
collection: 'payload-preferences',
|
||||
req,
|
||||
where: {
|
||||
and: [
|
||||
or: [
|
||||
{
|
||||
'user.value': { in: ids },
|
||||
and: [
|
||||
{
|
||||
'user.value': { in: ids },
|
||||
},
|
||||
{
|
||||
'user.relationTo': { equals: collectionConfig.slug },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'user.relationTo': { equals: collectionConfig.slug },
|
||||
key: { in: ids.map((id) => `collection-${collectionConfig.slug}-${id}`) },
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await payload.db.deleteMany({
|
||||
collection: 'payload-preferences',
|
||||
req,
|
||||
where: {
|
||||
key: { in: ids.map((id) => `collection-${collectionConfig.slug}-${id}`) },
|
||||
},
|
||||
})
|
||||
}
|
||||
await payload.db.deleteMany({
|
||||
collection: 'payload-preferences',
|
||||
req,
|
||||
where: {
|
||||
key: { in: ids.map((id) => `collection-${collectionConfig.slug}-${id}`) },
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -15,3 +15,7 @@ export const validOperators = [
|
||||
'intersects',
|
||||
'near',
|
||||
] as const
|
||||
|
||||
export type Operator = (typeof validOperators)[number]
|
||||
|
||||
export const validOperatorSet = new Set<Operator>(validOperators)
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
TypedLocale,
|
||||
TypedUser,
|
||||
} from '../index.js'
|
||||
import type { validOperators } from './constants.js'
|
||||
import type { Operator } from './constants.js'
|
||||
export type { Payload as Payload } from '../index.js'
|
||||
|
||||
export type CustomPayloadRequestProperties = {
|
||||
@@ -101,7 +101,7 @@ export type PayloadRequest = CustomPayloadRequestProperties &
|
||||
PayloadRequestData &
|
||||
Required<Pick<Request, 'headers'>>
|
||||
|
||||
export type Operator = (typeof validOperators)[number]
|
||||
export type { Operator }
|
||||
|
||||
// Makes it so things like passing new Date() will error
|
||||
export type JsonValue = JsonArray | JsonObject | unknown //Date | JsonArray | JsonObject | boolean | null | number | string // TODO: Evaluate proper, strong type for this
|
||||
|
||||
@@ -1,22 +1,17 @@
|
||||
const internalFields = ['__v']
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T => {
|
||||
// Create a new object to hold the sanitized fields
|
||||
const newDoc: Record<string, unknown> = {}
|
||||
|
||||
const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
|
||||
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
|
||||
for (const key in incomingDoc) {
|
||||
const val = incomingDoc[key]
|
||||
if (key === '_id') {
|
||||
return {
|
||||
...newDoc,
|
||||
id: val,
|
||||
}
|
||||
newDoc['id'] = val
|
||||
} else if (key !== '__v') {
|
||||
newDoc[key] = val
|
||||
}
|
||||
}
|
||||
|
||||
if (internalFields.indexOf(key) > -1) {
|
||||
return newDoc
|
||||
}
|
||||
|
||||
return {
|
||||
...newDoc,
|
||||
[key]: val,
|
||||
}
|
||||
}, {} as T)
|
||||
return newDoc as T
|
||||
}
|
||||
|
||||
export default sanitizeInternalFields
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
import type { Operator, Where } from 'payload'
|
||||
|
||||
import { validOperators } from 'payload/shared'
|
||||
import { validOperatorSet } from 'payload/shared'
|
||||
|
||||
const validateWhereQuery = (whereQuery): whereQuery is Where => {
|
||||
if (
|
||||
@@ -27,7 +27,7 @@ const validateWhereQuery = (whereQuery): whereQuery is Where => {
|
||||
for (const key of andKeys) {
|
||||
const operator = Object.keys(andQuery[key])[0]
|
||||
// Check if the key is a valid Operator.
|
||||
if (!operator || !validOperators.includes(operator as Operator)) {
|
||||
if (!operator || !validOperatorSet.has(operator as Operator)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user