feat!: 700% faster deepCopyObject, refactor deep merging and deep copying, type improvements (#7272)

**BREAKING:**
- The `deepMerge` exported from payload now handles more complex data and
is slower. The old, simple deepMerge is now exported as `deepMergeSimple`
- `combineMerge` is no longer exported. You can use
`deepMergeWithCombinedArrays` instead
- The behavior of the exported `deepCopyObject` and `isPlainObject` may
be different and more reliable, as the underlying algorithm has changed
This commit is contained in:
Alessio Gravili
2024-07-22 13:01:52 -04:00
committed by GitHub
parent 2c16c608ba
commit c45fbb9149
82 changed files with 592 additions and 537 deletions

View File

@@ -34,7 +34,6 @@
},
"dependencies": {
"bson-objectid": "2.0.4",
"deepmerge": "4.3.1",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-paginate-v2": "1.7.22",

View File

@@ -1,8 +1,7 @@
import type { FilterQuery } from 'mongoose'
import type { Field, Operator, Payload, Where } from 'payload'
import deepmerge from 'deepmerge'
import { combineMerge } from 'payload'
import { deepMergeWithCombinedArrays } from 'payload'
import { validOperators } from 'payload/shared'
import { buildAndOrConditions } from './buildAndOrConditions.js'
@@ -70,7 +69,7 @@ export async function parseParams({
[searchParam.path]: searchParam.value,
}
} else if (typeof searchParam?.value === 'object') {
result = deepmerge(result, searchParam.value, { arrayMerge: combineMerge })
result = deepMergeWithCombinedArrays(result, searchParam.value)
}
}
}

View File

@@ -6,7 +6,7 @@ import type { SanitizedCollectionConfig, TypeWithID } from '../collections/confi
import type { SanitizedConfig } from '../config/types.js'
import type { Field, FieldAffectingData, RichTextField, Validate } from '../fields/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../types/index.js'
import type { WithServerSidePropsComponentProps } from './elements/WithServerSideProps.js'
export type RichTextFieldProps<Value extends object, AdapterProps, ExtraFieldProperties = {}> = {
@@ -82,7 +82,7 @@ export type BeforeChangeRichTextHookArgs<
/**
* The original data with locales (not modified by any hooks). Only available in `beforeChange` and `beforeDuplicate` field hooks.
*/
docWithLocales?: Record<string, unknown>
docWithLocales?: JsonObject
duplicate?: boolean
@@ -98,7 +98,7 @@ export type BeforeChangeRichTextHookArgs<
/**
* The original siblingData with locales (not modified by any hooks).
*/
siblingDocWithLocales?: Record<string, unknown>
siblingDocWithLocales?: JsonObject
skipValidation?: boolean
}
@@ -216,7 +216,7 @@ type RichTextAdapterBase<
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
}) => void
hooks?: RichTextHooks
i18n?: Partial<GenericLanguages>

View File

@@ -59,7 +59,7 @@ export const resetPasswordOperation = async (args: Arguments): Promise<Result> =
collection: collectionConfig.slug,
req,
where: {
resetPasswordExpiration: { greater_than: new Date() },
resetPasswordExpiration: { greater_than: new Date().toISOString() },
resetPasswordToken: { equals: data.token },
},
})

View File

@@ -1,5 +1,5 @@
import type { SanitizedCollectionConfig, TypeWithID } from '../../../collections/config/types.js'
import type { Payload } from '../../../index.js'
import type { JsonObject, Payload } from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'
type Args = {
@@ -39,13 +39,13 @@ export const incrementLoginAttempts = async ({
return
}
const data: Record<string, unknown> = {
const data: JsonObject = {
loginAttempts: Number(doc.loginAttempts) + 1,
}
// Lock the account if at max attempts and not already locked
if (typeof doc.loginAttempts === 'number' && doc.loginAttempts + 1 >= maxLoginAttempts) {
const lockUntil = new Date(Date.now() + lockTime)
const lockUntil = new Date(Date.now() + lockTime).toISOString()
data.lockUntil = lockUntil
}

View File

@@ -1,5 +1,5 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { Payload } from '../../../index.js'
import type { JsonObject, Payload } from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'
import { ValidationError } from '../../../errors/index.js'
@@ -7,7 +7,7 @@ import { generatePasswordSaltHash } from './generatePasswordSaltHash.js'
type Args = {
collection: SanitizedCollectionConfig
doc: Record<string, unknown>
doc: JsonObject
password: string
payload: Payload
req: PayloadRequest

View File

@@ -1,8 +1,9 @@
import type { LoginWithUsernameOptions } from '../../auth/types.js'
import type { IncomingAuthType, LoginWithUsernameOptions } from '../../auth/types.js'
import type { CollectionConfig } from './types.js'
import defaultAccess from '../../auth/defaultAccess.js'
export const defaults = {
export const defaults: Partial<CollectionConfig> = {
access: {
create: defaultAccess,
delete: defaultAccess,
@@ -49,7 +50,7 @@ export const defaults = {
versions: false,
}
export const authDefaults = {
export const authDefaults: IncomingAuthType = {
cookies: {
sameSite: 'Lax',
secure: false,

View File

@@ -1,5 +1,3 @@
import merge from 'deepmerge'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { CollectionConfig, SanitizedCollectionConfig } from './types.js'
@@ -9,8 +7,8 @@ import { sanitizeFields } from '../../fields/config/sanitize.js'
import { fieldAffectsData } from '../../fields/config/types.js'
import mergeBaseFields from '../../fields/mergeBaseFields.js'
import { getBaseUploadFields } from '../../uploads/getBaseFields.js'
import { deepMergeWithReactComponents } from '../../utilities/deepMerge.js'
import { formatLabels } from '../../utilities/formatLabels.js'
import { isPlainObject } from '../../utilities/isPlainObject.js'
import baseVersionFields from '../../versions/baseFields.js'
import { versionDefaults } from '../../versions/defaults.js'
import { authDefaults, defaults, loginWithUsernameDefaults } from './defaults.js'
@@ -29,9 +27,7 @@ export const sanitizeCollection = async (
// Make copy of collection config
// /////////////////////////////////
const sanitized: CollectionConfig = merge(defaults, collection, {
isMergeableObject: isPlainObject,
})
const sanitized: CollectionConfig = deepMergeWithReactComponents(defaults, collection)
// /////////////////////////////////
// Sanitize fields
@@ -141,9 +137,10 @@ export const sanitizeCollection = async (
// sanitize fields for reserved names
sanitizeAuthFields(sanitized.fields, sanitized)
sanitized.auth = merge(authDefaults, typeof sanitized.auth === 'object' ? sanitized.auth : {}, {
isMergeableObject: isPlainObject,
})
sanitized.auth = deepMergeWithReactComponents(
authDefaults,
typeof sanitized.auth === 'object' ? sanitized.auth : {},
)
if (!sanitized.auth.disableLocalStrategy && sanitized.auth.verify === true) {
sanitized.auth.verify = {}
@@ -157,12 +154,12 @@ export const sanitizeCollection = async (
}
sanitized.auth.loginWithUsername = sanitized.auth.loginWithUsername
? merge(
loginWithUsernameDefaults,
typeof sanitized.auth.loginWithUsername === 'boolean'
? {
...loginWithUsernameDefaults,
...(typeof sanitized.auth.loginWithUsername === 'boolean'
? {}
: sanitized.auth.loginWithUsername,
)
: sanitized.auth.loginWithUsername),
}
: false
sanitized.fields = mergeBaseFields(sanitized.fields, getBaseAuthFields(sanitized.auth))

View File

@@ -28,7 +28,12 @@ import type {
} from '../../config/types.js'
import type { DBIdentifierName } from '../../database/types.js'
import type { Field } from '../../fields/config/types.js'
import type { CollectionSlug, TypedAuthOperations, TypedCollection } from '../../index.js'
import type {
CollectionSlug,
JsonObject,
TypedAuthOperations,
TypedCollection,
} from '../../index.js'
import type { PayloadRequest, RequestContext } from '../../types/index.js'
import type { SanitizedUploadConfig, UploadConfig } from '../../uploads/types.js'
import type {
@@ -41,7 +46,7 @@ export type DataFromCollectionSlug<TSlug extends CollectionSlug> = TypedCollecti
export type AuthOperationsFromCollectionSlug<TSlug extends CollectionSlug> =
TypedAuthOperations[TSlug]
export type RequiredDataFromCollection<TData extends Record<string, any>> = MarkOptional<
export type RequiredDataFromCollection<TData extends JsonObject> = MarkOptional<
TData,
'createdAt' | 'id' | 'sizes' | 'updatedAt'
>

View File

@@ -2,7 +2,7 @@ import type { BatchLoadFn } from 'dataloader'
import DataLoader from 'dataloader'
import type { PayloadRequest } from '../types/index.js'
import type { JsonValue, PayloadRequest } from '../types/index.js'
import type { TypeWithID } from './config/types.js'
import { isValidID } from '../utilities/isValidID.js'
@@ -119,7 +119,7 @@ const batchAndLoadDocs =
showHiddenFields: Boolean(showHiddenFields),
where: {
id: {
in: ids,
in: ids as JsonValue,
},
},
})

View File

@@ -1,6 +1,6 @@
import crypto from 'crypto'
import type { CollectionSlug } from '../../index.js'
import type { CollectionSlug, JsonObject } from '../../index.js'
import type { Document, PayloadRequest } from '../../types/index.js'
import type {
AfterChangeHook,
@@ -184,7 +184,7 @@ export const createOperation = async <TSlug extends CollectionSlug>(
// beforeChange - Fields
// /////////////////////////////////////
const resultWithLocales = await beforeChange<Record<string, unknown>>({
const resultWithLocales = await beforeChange<JsonObject>({
collection: collectionConfig,
context: req.context,
data,

View File

@@ -1,4 +1,4 @@
import type { CollectionSlug } from '../../index.js'
import type { CollectionSlug, JsonObject, TypeWithID } from '../../index.js'
import type { PayloadRequest } from '../../types/index.js'
import type { BeforeOperationHook, Collection, DataFromCollectionSlug } from '../config/types.js'

View File

@@ -194,7 +194,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
// beforeChange - Fields
// /////////////////////////////////////
result = await beforeChange<DataFromCollectionSlug<TSlug>>({
result = await beforeChange({
id,
collection: collectionConfig,
context: req.context,
@@ -280,7 +280,7 @@ export const duplicateOperation = async <TSlug extends CollectionSlug>(
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange<DataFromCollectionSlug<TSlug>>({
result = await afterChange({
collection: collectionConfig,
context: req.context,
data: versionDoc,

View File

@@ -263,7 +263,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
// beforeChange - Fields
// /////////////////////////////////////
let result = await beforeChange<DataFromCollectionSlug<TSlug>>({
let result = await beforeChange({
id,
collection: collectionConfig,
context: req.context,
@@ -349,7 +349,7 @@ export const updateOperation = async <TSlug extends CollectionSlug>(
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange<DataFromCollectionSlug<TSlug>>({
result = await afterChange({
collection: collectionConfig,
context: req.context,
data,

View File

@@ -236,7 +236,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
// beforeChange - Fields
// /////////////////////////////////////
let result = await beforeChange<DataFromCollectionSlug<TSlug>>({
let result = await beforeChange({
id,
collection: collectionConfig,
context: req.context,
@@ -341,7 +341,7 @@ export const updateByIDOperation = async <TSlug extends CollectionSlug>(
// afterChange - Fields
// /////////////////////////////////////
result = await afterChange<DataFromCollectionSlug<TSlug>>({
result = await afterChange({
collection: collectionConfig,
context: req.context,
data,

View File

@@ -1,7 +1,7 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import { en } from '@payloadcms/translations/languages/en'
import merge from 'deepmerge'
import { deepMergeSimple } from '@payloadcms/translations/utilities'
import type {
Config,
@@ -17,8 +17,7 @@ import { InvalidConfiguration } from '../errors/index.js'
import { sanitizeGlobals } from '../globals/config/sanitize.js'
import getPreferencesCollection from '../preferences/preferencesCollection.js'
import checkDuplicateCollections from '../utilities/checkDuplicateCollections.js'
import { deepMerge } from '../utilities/deepMerge.js'
import { isPlainObject } from '../utilities/isPlainObject.js'
import { deepMergeWithReactComponents } from '../utilities/deepMerge.js'
import { defaults } from './defaults.js'
const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig> => {
@@ -48,9 +47,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
}
export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedConfig> => {
const configWithDefaults: Config = merge(defaults, incomingConfig, {
isMergeableObject: isPlainObject,
}) as Config
const configWithDefaults: Config = deepMergeWithReactComponents(defaults, incomingConfig)
if (!configWithDefaults?.serverURL) {
configWithDefaults.serverURL = ''
@@ -163,7 +160,7 @@ export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedC
isRoot: true,
})
if (config.editor.i18n && Object.keys(config.editor.i18n).length >= 0) {
config.i18n.translations = deepMerge(config.i18n.translations, config.editor.i18n)
config.i18n.translations = deepMergeSimple(config.i18n.translations, config.editor.i18n)
}
}

View File

@@ -24,10 +24,18 @@ export { formatFilesize } from '../uploads/formatFilesize.js'
export { isImage } from '../uploads/isImage.js'
export { deepCopyObject } from '../utilities/deepCopyObject.js'
export { deepMerge } from '../utilities/deepMerge.js'
export {
deepCopyObject,
deepCopyObjectComplex,
deepCopyObjectSimple,
} from '../utilities/deepCopyObject.js'
export {
deepMerge,
deepMergeWithCombinedArrays,
deepMergeWithReactComponents,
deepMergeWithSourceArrays,
} from '../utilities/deepMerge.js'
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
export { getDataByPath } from '../utilities/getDataByPath.js'
@@ -55,3 +63,5 @@ export { wait } from '../utilities/wait.js'
export { default as wordBoundariesRegex } from '../utilities/wordBoundariesRegex.js'
export { versionDefaults } from '../versions/defaults.js'
export { deepMergeSimple } from '@payloadcms/translations/utilities'

View File

@@ -1,3 +1,5 @@
import { deepMergeSimple } from '@payloadcms/translations/utilities'
import type { CollectionConfig } from '../../collections/config/types.js'
import type { Config, SanitizedConfig } from '../../config/types.js'
import type { Field } from './types.js'
@@ -9,7 +11,6 @@ import {
InvalidFieldRelationship,
MissingFieldType,
} from '../../errors/index.js'
import { deepMerge } from '../../utilities/deepMerge.js'
import { formatLabels, toWords } from '../../utilities/formatLabels.js'
import { baseBlockFields } from '../baseFields/baseBlockFields.js'
import { baseIDField } from '../baseFields/baseIDField.js'
@@ -175,7 +176,7 @@ export const sanitizeFields = async ({
}
if (field.editor.i18n && Object.keys(field.editor.i18n).length >= 0) {
config.i18n.translations = deepMerge(config.i18n.translations, field.editor.i18n)
config.i18n.translations = deepMergeSimple(config.i18n.translations, field.editor.i18n)
}
}
if (richTextSanitizationPromises) {

View File

@@ -1,29 +1,31 @@
import type { PayloadRequest } from '../types/index.js'
import type { JsonValue, PayloadRequest } from '../types/index.js'
import { deepCopyObject } from '../utilities/deepCopyObject.js'
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
type Args = {
defaultValue: unknown
defaultValue: ((args: any) => JsonValue) | any
locale: string | undefined
user: PayloadRequest['user']
value?: unknown
value?: JsonValue
}
const getValueWithDefault = ({ defaultValue, locale, user, value }: Args): unknown => {
export const getDefaultValue = async ({
defaultValue,
locale,
user,
value,
}: Args): Promise<JsonValue> => {
if (typeof value !== 'undefined') {
return value
}
if (defaultValue && typeof defaultValue === 'function') {
return defaultValue({ locale, user })
return await defaultValue({ locale, user })
}
if (typeof defaultValue === 'object') {
return deepCopyObject(defaultValue)
return deepCopyObjectSimple(defaultValue)
}
return defaultValue
}
// eslint-disable-next-line no-restricted-exports
export default getValueWithDefault

View File

@@ -1,24 +1,24 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import { deepCopyObject } from '../../../utilities/deepCopyObject.js'
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
type Args<T extends JsonObject> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
/**
* The data before hooks
*/
data: Record<string, unknown> | T
data: T
/**
* The data after hooks
*/
doc: Record<string, unknown> | T
doc: T
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
previousDoc: Record<string, unknown> | T
previousDoc: T
req: PayloadRequest
}
@@ -26,7 +26,7 @@ type Args<T> = {
* This function is responsible for the following actions, in order:
* - Execute field hooks
*/
export const afterChange = async <T extends Record<string, unknown>>({
export const afterChange = async <T extends JsonObject>({
collection,
context,
data,
@@ -36,7 +36,7 @@ export const afterChange = async <T extends Record<string, unknown>>({
previousDoc,
req,
}: Args<T>): Promise<T> => {
const doc = deepCopyObject(incomingDoc)
const doc = deepCopyObjectSimple(incomingDoc)
await traverseFields({
collection,

View File

@@ -1,7 +1,7 @@
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
@@ -12,8 +12,8 @@ import { traverseFields } from './traverseFields.js'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
data: JsonObject
doc: JsonObject
field: Field | TabAsField
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
@@ -25,11 +25,11 @@ type Args = {
* The parent's schemaPath (path without indexes).
*/
parentSchemaPath: string[]
previousDoc: Record<string, unknown>
previousSiblingDoc: Record<string, unknown>
previousDoc: JsonObject
previousSiblingDoc: JsonObject
req: PayloadRequest
siblingData: Record<string, unknown>
siblingDoc: Record<string, unknown>
siblingData: JsonObject
siblingDoc: JsonObject
}
// This function is responsible for the following actions, in order:
@@ -101,11 +101,11 @@ export const promise = async ({
operation,
path: fieldPath,
previousDoc,
previousSiblingDoc: previousDoc[field.name] as Record<string, unknown>,
previousSiblingDoc: previousDoc[field.name] as JsonObject,
req,
schemaPath: fieldSchemaPath,
siblingData: (siblingData?.[field.name] as Record<string, unknown>) || {},
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
siblingData: (siblingData?.[field.name] as JsonObject) || {},
siblingDoc: siblingDoc[field.name] as JsonObject,
})
break
@@ -128,11 +128,11 @@ export const promise = async ({
operation,
path: [...fieldPath, i],
previousDoc,
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData?.[field.name]?.[i] || {},
siblingDoc: { ...row } || {},
siblingDoc: ({ ...(row as JsonObject) } as JsonObject) || {},
}),
)
})
@@ -147,7 +147,9 @@ export const promise = async ({
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
const block = field.blocks.find(
(blockType) => blockType.slug === (row as JsonObject).blockType,
)
if (block) {
promises.push(
@@ -161,12 +163,11 @@ export const promise = async ({
operation,
path: [...fieldPath, i],
previousDoc,
previousSiblingDoc:
previousDoc?.[field.name]?.[i] || ({} as Record<string, unknown>),
previousSiblingDoc: previousDoc?.[field.name]?.[i] || ({} as JsonObject),
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData?.[field.name]?.[i] || {},
siblingDoc: { ...row } || {},
siblingDoc: ({ ...(row as JsonObject) } as JsonObject) || {},
}),
)
}
@@ -205,9 +206,9 @@ export const promise = async ({
let tabPreviousSiblingDoc = siblingDoc
if (tabHasName(field)) {
tabSiblingData = siblingData[field.name] as Record<string, unknown>
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>
tabPreviousSiblingDoc = previousDoc[field.name] as Record<string, unknown>
tabSiblingData = siblingData[field.name] as JsonObject
tabSiblingDoc = siblingDoc[field.name] as JsonObject
tabPreviousSiblingDoc = previousDoc[field.name] as JsonObject
}
await traverseFields({

View File

@@ -1,6 +1,6 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
@@ -8,18 +8,18 @@ import { promise } from './promise.js'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
data: JsonObject
doc: JsonObject
fields: (Field | TabAsField)[]
global: SanitizedGlobalConfig | null
operation: 'create' | 'update'
path: (number | string)[]
previousDoc: Record<string, unknown>
previousSiblingDoc: Record<string, unknown>
previousDoc: JsonObject
previousSiblingDoc: JsonObject
req: PayloadRequest
schemaPath: string[]
siblingData: Record<string, unknown>
siblingDoc: Record<string, unknown>
siblingData: JsonObject
siblingDoc: JsonObject
}
export const traverseFields = async ({

View File

@@ -1,16 +1,16 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import { deepCopyObject } from '../../../utilities/deepCopyObject.js'
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
import { traverseFields } from './traverseFields.js'
type Args = {
type Args<T extends JsonObject> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
currentDepth?: number
depth: number
doc: Record<string, unknown>
doc: T
draft: boolean
fallbackLocale: null | string
findMany?: boolean
@@ -32,7 +32,7 @@ type Args = {
* - Populate relationships
*/
export async function afterRead<T = any>(args: Args): Promise<T> {
export async function afterRead<T extends JsonObject>(args: Args<T>): Promise<T> {
const {
collection,
context,
@@ -50,7 +50,7 @@ export async function afterRead<T = any>(args: Args): Promise<T> {
showHiddenFields,
} = args
const doc = deepCopyObject(incomingDoc)
const doc = deepCopyObjectSimple(incomingDoc)
const fieldPromises = []
const populationPromises = []

View File

@@ -1,12 +1,12 @@
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import getValueWithDefault from '../../getDefaultValue.js'
import { getDefaultValue } from '../../getDefaultValue.js'
import { getFieldPaths } from '../../getFieldPaths.js'
import { relationshipPopulationPromise } from './relationshipPopulationPromise.js'
import { traverseFields } from './traverseFields.js'
@@ -16,7 +16,7 @@ type Args = {
context: RequestContext
currentDepth: number
depth: number
doc: Record<string, unknown>
doc: JsonObject
draft: boolean
fallbackLocale: null | string
field: Field | TabAsField
@@ -40,7 +40,7 @@ type Args = {
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
triggerAccessControl?: boolean
triggerHooks?: boolean
}
@@ -276,7 +276,7 @@ export const promise = async ({
typeof siblingDoc[field.name] === 'undefined' &&
typeof field.defaultValue !== 'undefined'
) {
siblingDoc[field.name] = await getValueWithDefault({
siblingDoc[field.name] = await getDefaultValue({
defaultValue: field.defaultValue,
locale,
user: req.user,
@@ -304,7 +304,7 @@ export const promise = async ({
switch (field.type) {
case 'group': {
let groupDoc = siblingDoc[field.name] as Record<string, unknown>
let groupDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') groupDoc = {}
traverseFields({
@@ -336,7 +336,7 @@ export const promise = async ({
}
case 'array': {
const rows = siblingDoc[field.name]
const rows = siblingDoc[field.name] as JsonObject
if (Array.isArray(rows)) {
rows.forEach((row, i) => {
@@ -389,7 +389,7 @@ export const promise = async ({
req,
schemaPath: fieldSchemaPath,
showHiddenFields,
siblingDoc: row || {},
siblingDoc: (row as JsonObject) || {},
triggerAccessControl,
triggerHooks,
})
@@ -407,7 +407,9 @@ export const promise = async ({
if (Array.isArray(rows)) {
rows.forEach((row, i) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
const block = field.blocks.find(
(blockType) => blockType.slug === (row as JsonObject).blockType,
)
if (block) {
traverseFields({
@@ -430,7 +432,7 @@ export const promise = async ({
req,
schemaPath: fieldSchemaPath,
showHiddenFields,
siblingDoc: row || {},
siblingDoc: (row as JsonObject) || {},
triggerAccessControl,
triggerHooks,
})
@@ -440,7 +442,9 @@ export const promise = async ({
Object.values(rows).forEach((localeRows) => {
if (Array.isArray(localeRows)) {
localeRows.forEach((row, i) => {
const block = field.blocks.find((blockType) => blockType.slug === row.blockType)
const block = field.blocks.find(
(blockType) => blockType.slug === (row as JsonObject).blockType,
)
if (block) {
traverseFields({
@@ -463,7 +467,7 @@ export const promise = async ({
req,
schemaPath: fieldSchemaPath,
showHiddenFields,
siblingDoc: row || {},
siblingDoc: (row as JsonObject) || {},
triggerAccessControl,
triggerHooks,
})
@@ -511,7 +515,7 @@ export const promise = async ({
case 'tab': {
let tabDoc = siblingDoc
if (tabHasName(field)) {
tabDoc = siblingDoc[field.name] as Record<string, unknown>
tabDoc = siblingDoc[field.name] as JsonObject
if (typeof siblingDoc[field.name] !== 'object') tabDoc = {}
}

View File

@@ -1,6 +1,6 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
@@ -10,7 +10,7 @@ type Args = {
context: RequestContext
currentDepth: number
depth: number
doc: Record<string, unknown>
doc: JsonObject
draft: boolean
fallbackLocale: null | string
/**
@@ -28,7 +28,7 @@ type Args = {
req: PayloadRequest
schemaPath: string[]
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
triggerAccessControl?: boolean
triggerHooks?: boolean
}

View File

@@ -1,4 +1,8 @@
export const cloneDataFromOriginalDoc = (originalDocData: unknown): unknown => {
import type { JsonArray, JsonObject } from '../../../types/index.js'
export const cloneDataFromOriginalDoc = (
originalDocData: JsonArray | JsonObject,
): JsonArray | JsonObject => {
if (Array.isArray(originalDocData)) {
return originalDocData.map((row) => {
if (typeof row === 'object' && row != null) {

View File

@@ -4,11 +4,9 @@
* this is an existing row, so it should be merged.
* Otherwise, return an empty object.
*/
import type { JsonObject } from '../../../types/index.js'
export const getExistingRowDoc = (
incomingRow: Record<string, unknown>,
existingRows?: unknown,
): Record<string, unknown> => {
export const getExistingRowDoc = (incomingRow: JsonObject, existingRows?: unknown): JsonObject => {
if (incomingRow.id && Array.isArray(existingRows)) {
const matchedExistingRow = existingRows.find((existingRow) => {
if (typeof existingRow === 'object' && 'id' in existingRow) {

View File

@@ -1,17 +1,17 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import { ValidationError } from '../../../errors/index.js'
import { deepCopyObject } from '../../../utilities/deepCopyObject.js'
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
type Args<T extends JsonObject> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown> | T
doc: Record<string, unknown> | T
docWithLocales: Record<string, unknown>
data: T
doc: T
docWithLocales: JsonObject
duplicate?: boolean
global: SanitizedGlobalConfig | null
id?: number | string
@@ -29,7 +29,7 @@ type Args<T> = {
* - beforeDuplicate hooks (if duplicate)
* - Unflatten locales. The input `data` is the normal document for one locale. The output result will become the document with locales.
*/
export const beforeChange = async <T extends Record<string, unknown>>({
export const beforeChange = async <T extends JsonObject>({
id,
collection,
context,
@@ -42,7 +42,7 @@ export const beforeChange = async <T extends Record<string, unknown>>({
req,
skipValidation,
}: Args<T>): Promise<T> => {
const data = deepCopyObject(incomingData)
const data = deepCopyObjectSimple(incomingData)
const mergeLocaleActions = []
const errors: { field: string; message: string }[] = []

View File

@@ -1,12 +1,11 @@
import merge from 'deepmerge'
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, FieldHookArgs, TabAsField, ValidateOptions } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { deepMergeWithSourceArrays } from '../../../utilities/deepMerge.js'
import { fieldAffectsData, tabHasName } from '../../config/types.js'
import { getFieldPaths } from '../../getFieldPaths.js'
import { beforeDuplicate } from './beforeDuplicate.js'
@@ -16,9 +15,9 @@ import { traverseFields } from './traverseFields.js'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
doc: Record<string, unknown>
docWithLocales: Record<string, unknown>
data: JsonObject
doc: JsonObject
docWithLocales: JsonObject
duplicate: boolean
errors: { field: string; message: string }[]
field: Field | TabAsField
@@ -35,9 +34,9 @@ type Args = {
*/
parentSchemaPath: string[]
req: PayloadRequest
siblingData: Record<string, unknown>
siblingDoc: Record<string, unknown>
siblingDocWithLocales?: Record<string, unknown>
siblingData: JsonObject
siblingDoc: JsonObject
siblingDocWithLocales?: JsonObject
skipValidation: boolean
}
@@ -137,12 +136,12 @@ export const promise = async ({
const validationResult = await field.validate(valueToValidate, {
...field,
id,
data: merge(doc, data, { arrayMerge: (_, source) => source }),
data: deepMergeWithSourceArrays(doc, data),
jsonError,
operation,
preferences: { fields: {} },
req,
siblingData: merge(siblingDoc, siblingData, { arrayMerge: (_, source) => source }),
siblingData: deepMergeWithSourceArrays(siblingDoc, siblingData),
} as ValidateOptions<any, any, { jsonError: object }>)
if (typeof validationResult === 'string') {
@@ -173,7 +172,7 @@ export const promise = async ({
if (localization && field.localized) {
mergeLocaleActions.push(async () => {
const localeData = await localization.localeCodes.reduce(
async (localizedValuesPromise: Promise<Record<string, unknown>>, locale) => {
async (localizedValuesPromise: Promise<JsonObject>, locale) => {
const localizedValues = await localizedValuesPromise
let fieldValue =
locale === req.locale
@@ -253,9 +252,9 @@ export const promise = async ({
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingData: siblingData[field.name] as Record<string, unknown>,
siblingDoc: siblingDoc[field.name] as Record<string, unknown>,
siblingDocWithLocales: siblingDocWithLocales[field.name] as Record<string, unknown>,
siblingData: siblingData[field.name] as JsonObject,
siblingDoc: siblingDoc[field.name] as JsonObject,
siblingDocWithLocales: siblingDocWithLocales[field.name] as JsonObject,
skipValidation: skipValidationFromHere,
})
@@ -285,9 +284,12 @@ export const promise = async ({
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingData: row,
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
siblingDocWithLocales: getExistingRowDoc(row, siblingDocWithLocales[field.name]),
siblingData: row as JsonObject,
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
siblingDocWithLocales: getExistingRowDoc(
row as JsonObject,
siblingDocWithLocales[field.name],
),
skipValidation: skipValidationFromHere,
}),
)
@@ -305,10 +307,13 @@ export const promise = async ({
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name])
const rowSiblingDocWithLocales = getExistingRowDoc(row, siblingDocWithLocales[field.name])
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
const rowSiblingDocWithLocales = getExistingRowDoc(
row as JsonObject,
siblingDocWithLocales[field.name],
)
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
if (block) {
@@ -329,7 +334,7 @@ export const promise = async ({
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingData: row,
siblingData: row as JsonObject,
siblingDoc: rowSiblingDoc,
siblingDocWithLocales: rowSiblingDocWithLocales,
skipValidation: skipValidationFromHere,
@@ -382,9 +387,9 @@ export const promise = async ({
if (typeof siblingDocWithLocales[field.name] !== 'object')
siblingDocWithLocales[field.name] = {}
tabSiblingData = siblingData[field.name] as Record<string, unknown>
tabSiblingDoc = siblingDoc[field.name] as Record<string, unknown>
tabSiblingDocWithLocales = siblingDocWithLocales[field.name] as Record<string, unknown>
tabSiblingData = siblingData[field.name] as JsonObject
tabSiblingDoc = siblingDoc[field.name] as JsonObject
tabSiblingDocWithLocales = siblingDocWithLocales[field.name] as JsonObject
}
await traverseFields({

View File

@@ -1,6 +1,6 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, Operation, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
@@ -8,15 +8,15 @@ import { promise } from './promise.js'
type Args = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown>
data: JsonObject
/**
* The original data (not modified by any hooks)
*/
doc: Record<string, unknown>
doc: JsonObject
/**
* The original data with locales (not modified by any hooks)
*/
docWithLocales: Record<string, unknown>
docWithLocales: JsonObject
duplicate: boolean
errors: { field: string; message: string }[]
fields: (Field | TabAsField)[]
@@ -27,15 +27,15 @@ type Args = {
path: (number | string)[]
req: PayloadRequest
schemaPath: string[]
siblingData: Record<string, unknown>
siblingData: JsonObject
/**
* The original siblingData (not modified by any hooks)
*/
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
/**
* The original siblingData with locales (not modified by any hooks)
*/
siblingDocWithLocales: Record<string, unknown>
siblingDocWithLocales: JsonObject
skipValidation?: boolean
}

View File

@@ -1,15 +1,15 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import { deepCopyObject } from '../../../utilities/deepCopyObject.js'
import { deepCopyObjectSimple } from '../../../utilities/deepCopyObject.js'
import { traverseFields } from './traverseFields.js'
type Args<T> = {
type Args<T extends JsonObject> = {
collection: SanitizedCollectionConfig | null
context: RequestContext
data: Record<string, unknown> | T
doc?: Record<string, unknown> | T
data: T
doc?: T
duplicate?: boolean
global: SanitizedGlobalConfig | null
id?: number | string
@@ -26,7 +26,7 @@ type Args<T> = {
* - Merge original document data into incoming data
* - Compute default values for undefined fields
*/
export const beforeValidate = async <T extends Record<string, unknown>>({
export const beforeValidate = async <T extends JsonObject>({
id,
collection,
context,
@@ -37,7 +37,7 @@ export const beforeValidate = async <T extends Record<string, unknown>>({
overrideAccess,
req,
}: Args<T>): Promise<T> => {
const data = deepCopyObject(incomingData)
const data = deepCopyObjectSimple(incomingData)
await traverseFields({
id,

View File

@@ -1,12 +1,12 @@
import type { RichTextAdapter } from '../../../admin/RichText.js'
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, JsonValue, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { MissingEditorProp } from '../../../errors/index.js'
import { fieldAffectsData, tabHasName, valueIsValueWithRelation } from '../../config/types.js'
import getValueWithDefault from '../../getDefaultValue.js'
import { getDefaultValue } from '../../getDefaultValue.js'
import { getFieldPaths } from '../../getFieldPaths.js'
import { cloneDataFromOriginalDoc } from '../beforeChange/cloneDataFromOriginalDoc.js'
import { getExistingRowDoc } from '../beforeChange/getExistingRowDoc.js'
@@ -28,11 +28,11 @@ type Args<T> = {
parentPath: (number | string)[]
parentSchemaPath: string[]
req: PayloadRequest
siblingData: Record<string, unknown>
siblingData: JsonObject
/**
* The original siblingData (not modified by any hooks)
*/
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
}
// This function is responsible for the following actions, in order:
@@ -149,7 +149,7 @@ export const promise = async <T>({
if (Array.isArray(field.relationTo)) {
if (Array.isArray(value)) {
value.forEach((relatedDoc: { relationTo: string; value: unknown }, i) => {
value.forEach((relatedDoc: { relationTo: string; value: JsonValue }, i) => {
const relatedCollection = req.payload.config.collections.find(
(collection) => collection.slug === relatedDoc.relationTo,
)
@@ -270,11 +270,11 @@ export const promise = async <T>({
if (typeof siblingData[field.name] === 'undefined') {
// If no incoming data, but existing document data is found, merge it in
if (typeof siblingDoc[field.name] !== 'undefined') {
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name])
siblingData[field.name] = cloneDataFromOriginalDoc(siblingDoc[field.name] as any)
// Otherwise compute default value
} else if (typeof field.defaultValue !== 'undefined') {
siblingData[field.name] = await getValueWithDefault({
siblingData[field.name] = await getDefaultValue({
defaultValue: field.defaultValue,
locale: req.locale,
user: req.user,
@@ -306,8 +306,8 @@ export const promise = async <T>({
path: fieldPath,
req,
schemaPath: fieldSchemaPath,
siblingData: groupData,
siblingDoc: groupDoc,
siblingData: groupData as JsonObject,
siblingDoc: groupDoc as JsonObject,
})
break
@@ -333,8 +333,8 @@ export const promise = async <T>({
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingData: row,
siblingDoc: getExistingRowDoc(row, siblingDoc[field.name]),
siblingData: row as JsonObject,
siblingDoc: getExistingRowDoc(row as JsonObject, siblingDoc[field.name]),
}),
)
})
@@ -349,12 +349,12 @@ export const promise = async <T>({
if (Array.isArray(rows)) {
const promises = []
rows.forEach((row, i) => {
const rowSiblingDoc = getExistingRowDoc(row, siblingDoc[field.name])
const blockTypeToMatch = row.blockType || rowSiblingDoc.blockType
const rowSiblingDoc = getExistingRowDoc(row as JsonObject, siblingDoc[field.name])
const blockTypeToMatch = (row as JsonObject).blockType || rowSiblingDoc.blockType
const block = field.blocks.find((blockType) => blockType.slug === blockTypeToMatch)
if (block) {
row.blockType = blockTypeToMatch
;(row as JsonObject).blockType = blockTypeToMatch
promises.push(
traverseFields({
@@ -370,7 +370,7 @@ export const promise = async <T>({
path: [...fieldPath, i],
req,
schemaPath: fieldSchemaPath,
siblingData: row,
siblingData: row as JsonObject,
siblingDoc: rowSiblingDoc,
}),
)

View File

@@ -1,6 +1,6 @@
import type { SanitizedCollectionConfig } from '../../../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../../../globals/config/types.js'
import type { PayloadRequest, RequestContext } from '../../../types/index.js'
import type { JsonObject, PayloadRequest, RequestContext } from '../../../types/index.js'
import type { Field, TabAsField } from '../../config/types.js'
import { promise } from './promise.js'
@@ -21,11 +21,11 @@ type Args<T> = {
path: (number | string)[]
req: PayloadRequest
schemaPath: string[]
siblingData: Record<string, unknown>
siblingData: JsonObject
/**
* The original siblingData (not modified by any hooks)
*/
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
}
export const traverseFields = async <T>({

View File

@@ -1,7 +1,6 @@
import merge from 'deepmerge'
import type { Field, FieldWithSubFields } from './config/types.js'
import { deepMergeWithReactComponents } from '../utilities/deepMerge.js'
import { fieldAffectsData, fieldHasSubFields } from './config/types.js'
const mergeBaseFields = (fields: Field[], baseFields: Field[]): Field[] => {
@@ -24,7 +23,7 @@ const mergeBaseFields = (fields: Field[], baseFields: Field[]): Field[] => {
const matchCopy: Field = { ...match }
mergedFields.splice(matchedIndex, 1)
const mergedField = merge<Field>(baseField, matchCopy)
const mergedField = deepMergeWithReactComponents<Field>(baseField, matchCopy)
if (fieldHasSubFields(baseField) && fieldHasSubFields(matchCopy)) {
;(mergedField as FieldWithSubFields).fields = mergeBaseFields(

View File

@@ -1,5 +1,4 @@
import type { AccessResult } from '../../config/types.js'
import type { GeneratedTypes } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { SanitizedGlobalConfig } from '../config/types.js'

View File

@@ -8,6 +8,7 @@ import { combineQueries } from '../../database/combineQueries.js'
import { Forbidden, NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -78,7 +79,7 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
}
// Clone the result - it may have come back memoized
let result = JSON.parse(JSON.stringify(results[0]))
let result: any = deepCopyObjectSimple(results[0])
// Patch globalType onto version doc
result.version.globalType = globalConfig.slug

View File

@@ -87,7 +87,7 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
docs: await Promise.all(
paginatedDocs.docs.map(async (data) => ({
...data,
version: await afterRead({
version: await afterRead<T>({
collection: null,
context: req.context,
depth,

View File

@@ -1,6 +1,6 @@
import type { DeepPartial } from 'ts-essentials'
import type { GlobalSlug } from '../../index.js'
import type { GlobalSlug, JsonObject } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { DataFromGlobalSlug, SanitizedGlobalConfig } from '../config/types.js'
@@ -9,6 +9,7 @@ import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
import { deepCopyObjectSimple } from '../../index.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
@@ -81,10 +82,10 @@ export const updateOperation = async <TSlug extends GlobalSlug>(
where: query,
})
let globalJSON: Record<string, unknown> = {}
let globalJSON: JsonObject = {}
if (global) {
globalJSON = JSON.parse(JSON.stringify(global))
globalJSON = deepCopyObjectSimple(global)
if (globalJSON._id) {
delete globalJSON._id

View File

@@ -50,6 +50,7 @@ import type { Options as FindGlobalVersionByIDOptions } from './globals/operatio
import type { Options as FindGlobalVersionsOptions } from './globals/operations/local/findVersions.js'
import type { Options as RestoreGlobalVersionOptions } from './globals/operations/local/restoreVersion.js'
import type { Options as UpdateGlobalOptions } from './globals/operations/local/update.js'
import type { JsonObject } from './types/index.js'
import type { TypeWithVersion } from './versions/types.js'
import { decrypt, encrypt } from './auth/crypto.js'
@@ -85,10 +86,10 @@ export interface GeneratedTypes {
}
}
collectionsUntyped: {
[slug: string]: Record<string, unknown> & TypeWithID
[slug: string]: JsonObject & TypeWithID
}
globalsUntyped: {
[slug: string]: Record<string, unknown>
[slug: string]: JsonObject
}
localeUntyped: null | string
userUntyped: User
@@ -925,7 +926,7 @@ export type {
ValidateOptions,
ValueWithRelation,
} from './fields/config/types.js'
export { default as getDefaultValue } from './fields/getDefaultValue.js'
export { getDefaultValue } from './fields/getDefaultValue.js'
export { traverseFields as afterChangeTraverseFields } from './fields/hooks/afterChange/traverseFields.js'
export { promise as afterReadPromise } from './fields/hooks/afterRead/promise.js'
export { traverseFields as afterReadTraverseFields } from './fields/hooks/afterRead/traverseFields.js'
@@ -964,7 +965,6 @@ export { getLocalI18n } from './translations/getLocalI18n.js'
export * from './types/index.js'
export { getFileByPath } from './uploads/getFileByPath.js'
export type * from './uploads/types.js'
export { combineMerge } from './utilities/combineMerge.js'
export { commitTransaction } from './utilities/commitTransaction.js'
export {
configToJSONSchema,
@@ -974,8 +974,17 @@ export {
} from './utilities/configToJSONSchema.js'
export { createArrayFromCommaDelineated } from './utilities/createArrayFromCommaDelineated.js'
export { createLocalReq } from './utilities/createLocalReq.js'
export { deepCopyObject } from './utilities/deepCopyObject.js'
export { deepMerge } from './utilities/deepMerge.js'
export {
deepCopyObject,
deepCopyObjectComplex,
deepCopyObjectSimple,
} from './utilities/deepCopyObject.js'
export {
deepMerge,
deepMergeWithCombinedArrays,
deepMergeWithReactComponents,
deepMergeWithSourceArrays,
} from './utilities/deepMerge.js'
export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js'
export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js'
export { getCollectionIDFieldTypes } from './utilities/getCollectionIDFieldTypes.js'
@@ -995,6 +1004,7 @@ export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
export { getLatestCollectionVersion } from './versions/getLatestCollectionVersion.js'
export { getLatestGlobalVersion } from './versions/getLatestGlobalVersion.js'
export { getDependencies }
export { saveVersion } from './versions/saveVersion.js'
export { getDependencies }
export type { TypeWithVersion } from './versions/types.js'
export { deepMergeSimple } from '@payloadcms/translations/utilities'

View File

@@ -68,7 +68,7 @@ type PayloadRequestData = {
* 2. import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
* `await addDataAndFileToRequest(req)`
* */
data?: Record<string, unknown>
data?: JsonObject
/** The file on the request, same rules apply as the `data` property */
file?: {
data: Buffer
@@ -89,8 +89,18 @@ export interface RequestContext {
export type Operator = (typeof validOperators)[number]
// 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
export interface JsonArray extends Array<JsonValue> {}
export interface JsonObject {
[key: string]: JsonValue
}
export type WhereField = {
[key in Operator]?: unknown
// any json-serializable value
[key in Operator]?: JsonValue
}
export type Where = {

View File

@@ -1,16 +0,0 @@
import merge from 'deepmerge'
export const combineMerge = (target, source, options) => {
const destination = target.slice()
source.forEach((item, index) => {
if (typeof destination[index] === 'undefined') {
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options)
} else if (options.isMergeableObject(item)) {
destination[index] = merge(target[index], item, options)
} else if (target.indexOf(item) === -1) {
destination.push(item)
}
})
return destination
}

View File

@@ -1,20 +0,0 @@
const convertArrayToObject = (arr, key) =>
arr.reduce((obj, item) => {
if (key) {
obj[item[key]] = item
return obj
}
obj[item] = {}
return obj
}, {})
const convertObjectToArray = (arr) => Object.values(arr)
const convertArrayToHash = (arr, key) =>
arr.reduce((obj, item, i) => {
obj[item[key]] = i
return obj
}, {})
export { convertArrayToHash, convertArrayToObject, convertObjectToArray }

View File

@@ -1,23 +1,179 @@
export const deepCopyObject = (inObject) => {
if (inObject instanceof Date) return inObject
import type { JsonValue } from '../types/index.js'
if (inObject instanceof Set) return new Set(inObject)
/*
Main deepCopyObject handling - from rfdc: https://github.com/davidmarkclements/rfdc/blob/master/index.js
if (inObject instanceof Map) return new Map(inObject)
Copyright 2019 "David Mark Clements <david.mark.clements@gmail.com>"
if (typeof inObject !== 'object' || inObject === null) {
return inObject // Return the value if inObject is not an object
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
*/
function copyBuffer(cur) {
if (cur instanceof Buffer) {
return Buffer.from(cur)
}
// Create an array or object to hold the values
const outObject = Array.isArray(inObject) ? [] : {}
Object.keys(inObject).forEach((key) => {
const value = inObject[key]
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = typeof value === 'object' && value !== null ? deepCopyObject(value) : value
})
return outObject
return new cur.constructor(cur.buffer.slice(), cur.byteOffset, cur.length)
}
const constructorHandlers = new Map()
constructorHandlers.set(Date, (o) => new Date(o))
constructorHandlers.set(Map, (o, fn) => new Map(cloneArray<any>(Array.from(o), fn)))
constructorHandlers.set(Set, (o, fn) => new Set(cloneArray(Array.from(o), fn)))
let handler = null
function cloneArray<T>(a: T, fn): T {
const keys = Object.keys(a)
const a2 = new Array(keys.length)
for (let i = 0; i < keys.length; i++) {
const k = keys[i]
const cur = a[k]
if (typeof cur !== 'object' || cur === null) {
a2[k] = cur
} else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
a2[k] = handler(cur, fn)
} else if (ArrayBuffer.isView(cur)) {
a2[k] = copyBuffer(cur)
} else {
a2[k] = fn(cur)
}
}
return a2 as T
}
export const deepCopyObject = <T>(o: T): T => {
if (typeof o !== 'object' || o === null) return o
if (Array.isArray(o)) return cloneArray(o, deepCopyObject)
if (o.constructor !== Object && (handler = constructorHandlers.get(o.constructor))) {
return handler(o, deepCopyObject)
}
const o2 = {}
for (const k in o) {
if (Object.hasOwnProperty.call(o, k) === false) continue
const cur = o[k]
if (typeof cur !== 'object' || cur === null) {
o2[k as string] = cur
} else if (cur.constructor !== Object && (handler = constructorHandlers.get(cur.constructor))) {
o2[k as string] = handler(cur, deepCopyObject)
} else if (ArrayBuffer.isView(cur)) {
o2[k as string] = copyBuffer(cur)
} else {
o2[k as string] = deepCopyObject(cur)
}
}
return o2 as T
}
/*
Fast deepCopyObjectSimple handling - from fast-json-clone: https://github.com/rhysd/fast-json-clone
Benchmark: https://github.com/AlessioGr/fastest-deep-clone-json/blob/main/test/benchmark.js
*/
/**
* A deepCopyObject implementation which only works for JSON objects and arrays, and is faster than
* JSON.parse(JSON.stringify(obj))
*
* @param value The JSON value to be cloned. There are two invariants. 1) It must not contain circles
* as JSON does not allow it. This function will cause infinite loop for such values by
* design. 2) It must contain JSON values only. Other values like `Date`, `Regexp`, `Map`,
* `Set`, `Buffer`, ... are not allowed.
* @returns The cloned JSON value.
*/
export function deepCopyObjectSimple<T extends JsonValue>(value: T): T {
if (typeof value !== 'object' || value === null) {
return value
} else if (Array.isArray(value)) {
return value.map((e) =>
typeof e !== 'object' || e === null ? e : deepCopyObjectSimple(e),
) as T
} else {
if (value instanceof Date) return new Date(value) as unknown as T
const ret: { [key: string]: T } = {}
for (const k in value) {
const v = value[k]
ret[k] = typeof v !== 'object' || v === null ? v : (deepCopyObjectSimple(v as T) as any)
}
return ret as unknown as T
}
}
/**
* A deepCopyObject implementation which is slower than deepCopyObject, but more correct.
* Can be used if correctness is more important than speed. Supports circular dependencies
*/
export function deepCopyObjectComplex<T>(object: T, cache: WeakMap<any, any> = new WeakMap()): T {
if (object === null) return null
if (cache.has(object)) {
return cache.get(object)
}
// Handle Date
if (object instanceof Date) {
return new Date(object.getTime()) as unknown as T
}
// Handle RegExp
if (object instanceof RegExp) {
return new RegExp(object.source, object.flags) as unknown as T
}
// Handle Map
if (object instanceof Map) {
const clonedMap = new Map()
cache.set(object, clonedMap)
for (const [key, value] of object.entries()) {
clonedMap.set(key, deepCopyObjectComplex(value, cache))
}
return clonedMap as unknown as T
}
// Handle Set
if (object instanceof Set) {
const clonedSet = new Set()
cache.set(object, clonedSet)
for (const value of object.values()) {
clonedSet.add(deepCopyObjectComplex(value, cache))
}
return clonedSet as unknown as T
}
// Handle Array and Object
if (typeof object === 'object' && object !== null) {
if ('$$typeof' in object && typeof object.$$typeof === 'symbol') {
return object
}
const clonedObject: any = Array.isArray(object)
? []
: Object.create(Object.getPrototypeOf(object))
cache.set(object, clonedObject)
for (const key in object) {
if (
Object.prototype.hasOwnProperty.call(object, key) ||
Object.getOwnPropertySymbols(object).includes(key as any)
) {
clonedObject[key] = deepCopyObjectComplex(object[key], cache)
}
}
return clonedObject as T
}
// Handle all other cases
return object
}

View File

@@ -1,15 +1,46 @@
export function deepMerge(obj1, obj2) {
const output = { ...obj1 }
import deepMerge from 'deepmerge'
for (const key in obj2) {
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
if (typeof obj2[key] === 'object' && !Array.isArray(obj2[key]) && obj1[key]) {
output[key] = deepMerge(obj1[key], obj2[key])
} else {
output[key] = obj2[key]
}
}
}
import { isPlainObject } from './isPlainObject.js'
return output
export { deepMerge }
/**
* Fully-featured deepMerge.
*
* Array handling: Arrays in the target object are combined with the source object's arrays.
*/
export function deepMergeWithCombinedArrays<T extends object>(obj1: object, obj2: object): T {
return deepMerge<T>(obj1, obj2, {
arrayMerge: (target, source, options) => {
const destination = target.slice()
source.forEach((item, index) => {
if (typeof destination[index] === 'undefined') {
destination[index] = options.cloneUnlessOtherwiseSpecified(item, options)
} else if (options.isMergeableObject(item)) {
destination[index] = deepMerge(target[index], item, options)
} else if (target.indexOf(item) === -1) {
destination.push(item)
}
})
return destination
},
})
}
/**
* Fully-featured deepMerge.
*
* Array handling: Arrays in the target object are replaced by the source object's arrays.
*/
export function deepMergeWithSourceArrays<T extends object>(obj1: object, obj2: object): T {
return deepMerge<T>(obj1, obj2, { arrayMerge: (_, source) => source })
}
/**
* Fully-featured deepMerge. Does not clone React components by default.
*/
export function deepMergeWithReactComponents<T extends object>(obj1: object, obj2: object): T {
return deepMerge<T>(obj1, obj2, {
isMergeableObject: isPlainObject,
})
}

View File

@@ -1,2 +0,0 @@
export default (variable) =>
getComputedStyle(document.documentElement).getPropertyValue(`--${variable}`)

View File

@@ -1,29 +1,11 @@
function isObject(o: unknown): boolean {
return Object.prototype.toString.call(o) === '[object Object]'
}
import { isReactComponentOrFunction } from './isReactComponent.js'
export function isPlainObject(o: unknown): boolean {
export function isPlainObject(o: any): boolean {
// Is this a React component?
if (typeof o === 'object' && '$$typeof' in o && typeof o.$$typeof === 'symbol') {
if (isReactComponentOrFunction(o)) {
return false
}
if (isObject(o) === false) return false
// If has modified constructor
const ctor = o.constructor
if (ctor === undefined) return true
// If has modified prototype
const prot = ctor.prototype
if (isObject(prot) === false) return false
// If constructor does not have an Object-specific method
// eslint-disable-next-line no-prototype-builtins
if (prot.hasOwnProperty('isPrototypeOf') === false) {
return false
}
// Most likely a plain Object
return true
// from https://github.com/fastify/deepmerge/blob/master/index.js#L77
return typeof o === 'object' && o !== null && !(o instanceof RegExp) && !(o instanceof Date)
}

View File

@@ -1,3 +0,0 @@
const overwriteMerge = (_, sourceArray) => sourceArray
export default overwriteMerge

View File

@@ -1,4 +1,3 @@
import type { User } from '../../../auth/types.js'
import type { Payload } from '../../../index.js'
import type { PayloadRequest } from '../../../types/index.js'

View File

@@ -7,6 +7,7 @@ import type { PayloadRequest, Where } from '../../types/index.js'
import { hasWhereAccessResult } from '../../auth/index.js'
import { combineQueries } from '../../database/combineQueries.js'
import { docHasTimestamps } from '../../types/index.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { appendVersionToQueryKey } from './appendVersionToQueryKey.js'
@@ -84,7 +85,7 @@ const replaceWithDraftIfAvailable = async <T extends TypeWithID>({
return doc
}
draft = JSON.parse(JSON.stringify(draft))
draft = deepCopyObjectSimple(draft)
draft = sanitizeInternalFields(draft)
// Patch globalType onto version doc

View File

@@ -1,9 +1,9 @@
import type { SanitizedCollectionConfig, TypeWithID } from '../collections/config/types.js'
import type { SanitizedGlobalConfig } from '../globals/config/types.js'
import type { Payload } from '../index.js'
import type { Payload } from '../index.js';
import type { PayloadRequest } from '../types/index.js'
import { deepCopyObject } from '../utilities/deepCopyObject.js'
import { deepCopyObjectSimple } from '../index.js'
import sanitizeInternalFields from '../utilities/sanitizeInternalFields.js'
import { enforceMaxVersions } from './enforceMaxVersions.js'
@@ -31,7 +31,7 @@ export const saveVersion = async ({
let result
let createNewVersion = true
const now = new Date().toISOString()
const versionData = deepCopyObject(doc)
const versionData = deepCopyObjectSimple(doc)
if (draft) versionData._status = 'draft'
if (versionData._id) delete versionData._id

View File

@@ -48,7 +48,6 @@
},
"dependencies": {
"@payloadcms/ui": "workspace:*",
"deepmerge": "^4.2.2",
"escape-html": "^1.0.3"
},
"devDependencies": {

View File

@@ -1,6 +1,6 @@
import type { Block, CollectionConfig, Field } from 'payload'
import merge from 'deepmerge'
import { deepMergeWithSourceArrays } from 'payload'
import type { FieldConfig, FormBuilderPluginConfig } from '../../types.js'
@@ -84,9 +84,7 @@ export const generateFormCollection = (formConfig: FormBuilderPluginConfig): Col
}
if (typeof block === 'object' && typeof fieldConfig === 'object') {
return merge<FieldConfig>(block, fieldConfig, {
arrayMerge: (_, sourceArray) => sourceArray,
})
return deepMergeWithSourceArrays(block, fieldConfig)
}
if (typeof block === 'function') {

View File

@@ -1,4 +1,9 @@
import type { CollectionAfterChangeHook, CollectionConfig, PayloadRequest } from 'payload'
import type {
CollectionAfterChangeHook,
CollectionConfig,
JsonObject,
PayloadRequest,
} from 'payload'
import type { NestedDocsPluginConfig } from '../types.js'
@@ -6,7 +11,7 @@ import { populateBreadcrumbs } from '../utilities/populateBreadcrumbs.js'
type ResaveArgs = {
collection: CollectionConfig
doc: Record<string, unknown>
doc: JsonObject
draft: boolean
pluginConfig: NestedDocsPluginConfig
req: PayloadRequest

View File

@@ -44,8 +44,7 @@
"test": "echo \"Error: no tests specified\""
},
"dependencies": {
"@payloadcms/ui": "workspace:*",
"deepmerge": "4.3.1"
"@payloadcms/ui": "workspace:*"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",

View File

@@ -1,7 +1,5 @@
import type { CollectionConfig, Field } from 'payload'
import deepMerge from 'deepmerge'
import type { SearchPluginConfig } from '../types.js'
import { LinkToDoc } from './ui/index.js'

View File

@@ -2,7 +2,7 @@ import type { Config, Field, GroupField, TabsField, TextField } from 'payload'
import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
import { withMergedProps } from '@payloadcms/ui/shared'
import { deepMerge } from 'payload/shared'
import { deepMergeSimple } from 'payload/shared'
import type {
GenerateDescription,
@@ -298,7 +298,7 @@ export const seoPlugin =
i18n: {
...config.i18n,
translations: {
...deepMerge(translations, config.i18n?.translations),
...deepMergeSimple(translations, config.i18n?.translations),
},
},
}

View File

@@ -54,7 +54,6 @@ export {
sanitizeClientFeatures,
} from '../../lexical/config/client/sanitize.js'
export { CAN_USE_DOM } from '../../lexical/utils/canUseDOM.js'
export { cloneDeep } from '../../lexical/utils/cloneDeep.js'
export { getDOMRangeRect } from '../../lexical/utils/getDOMRangeRect.js'
export { getSelectedNode } from '../../lexical/utils/getSelectedNode.js'
export { isHTMLElement } from '../../lexical/utils/guard.js'

View File

@@ -9,12 +9,14 @@ import type {
NodeKey,
Spread,
} from 'lexical'
import type { JsonObject } from 'payload'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
import ObjectID from 'bson-objectid'
import { deepCopyObjectSimple } from 'payload/shared'
import React, { type JSX } from 'react'
export type BlockFields<TBlockFields extends object = Record<string, unknown>> = {
export type BlockFields<TBlockFields extends JsonObject = JsonObject> = {
/** Block form data */
blockName: string
blockType: string
@@ -27,7 +29,7 @@ const BlockComponent = React.lazy(() =>
})),
)
export type SerializedBlockNode<TBlockFields extends object = Record<string, unknown>> = Spread<
export type SerializedBlockNode<TBlockFields extends JsonObject = JsonObject> = Spread<
{
children?: never // required so that our typed editor state doesn't automatically add children
fields: BlockFields<TBlockFields>
@@ -117,7 +119,7 @@ export class BlockNode extends DecoratorBlockNode {
}
setFields(fields: BlockFields): void {
const fieldsCopy = JSON.parse(JSON.stringify(fields)) as BlockFields
const fieldsCopy = deepCopyObjectSimple(fields)
const writable = this.getWritable()
writable.__fields = fieldsCopy

View File

@@ -11,6 +11,7 @@ import type {
import ObjectID from 'bson-objectid'
import { DecoratorNode } from 'lexical'
import { deepCopyObjectSimple } from 'payload/shared'
import React, { type JSX } from 'react'
export type InlineBlockFields = {
@@ -112,7 +113,7 @@ export class InlineBlockNode extends DecoratorNode<React.ReactElement> {
}
setFields(fields: InlineBlockFields): void {
const fieldsCopy = JSON.parse(JSON.stringify(fields)) as InlineBlockFields
const fieldsCopy = deepCopyObjectSimple(fields) as InlineBlockFields
const writable = this.getWritable()
writable.__fields = fieldsCopy

View File

@@ -99,12 +99,14 @@ export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
if (!node) {
return false
}
node.setFields(fields as BlockFields)
setTargetNodeKey(null)
return true
}
const inlineBlockNode = $createInlineBlockNode(fields as BlockFields)
$insertNodes([inlineBlockNode])
if ($isRootOrShadowRoot(inlineBlockNode.getParentOrThrow())) {
@@ -118,9 +120,10 @@ export const BlocksPlugin: PluginComponent<BlocksFeatureClientProps> = () => {
editor.registerCommand(
OPEN_INLINE_BLOCK_DRAWER_COMMAND,
({ fields, nodeKey }) => {
setBlockFields((fields as BlockFields) ?? null)
setTargetNodeKey(nodeKey ?? null)
setBlockType((fields as BlockFields)?.blockType ?? ('' as any))
setBlockType(fields?.blockType ?? ('' as any))
if (nodeKey) {
toggleModal(drawerSlug)

View File

@@ -91,7 +91,7 @@ export const LinkFeature = createServerFeature<
// Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data.
// Otherwise, it will cause a validation error (field is required).
const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter(
(field) => field.name !== 'text',
(field) => !('name' in field) || field.name !== 'text',
)
return {

View File

@@ -1,14 +1,14 @@
import type { SerializedElementNode, SerializedLexicalNode, Spread } from 'lexical'
import type { JsonValue } from 'payload'
export type LinkFields = {
// unknown, custom fields:
[key: string]: unknown
[key: string]: JsonValue
doc: {
relationTo: string
value:
| {
// Actual doc data, populated in afterRead hook
[key: string]: unknown
[key: string]: JsonValue
id: string
}
| string

View File

@@ -7,7 +7,10 @@ import { getBaseFields } from '../../drawer/baseFields.js'
*/
export function transformExtraFields(
customFieldSchema:
| ((args: { config: SanitizedConfig; defaultFields: FieldAffectingData[] }) => Field[])
| ((args: {
config: SanitizedConfig
defaultFields: FieldAffectingData[]
}) => (Field | FieldAffectingData)[])
| Field[],
config: SanitizedConfig,
enabledCollections?: CollectionSlug[],
@@ -21,15 +24,15 @@ export function transformExtraFields(
maxDepth,
)
let fields: Field[]
let fields: (Field | FieldAffectingData)[]
if (typeof customFieldSchema === 'function') {
fields = customFieldSchema({ config, defaultFields: baseFields })
} else if (Array.isArray(customFieldSchema)) {
fields = customFieldSchema
} else {
fields = baseFields as Field[]
fields = baseFields
}
return fields
return fields as Field[]
}

View File

@@ -10,6 +10,7 @@ import type {
} from 'lexical'
import type {
Field,
JsonObject,
PayloadRequest,
ReplaceAny,
RequestContext,
@@ -61,7 +62,7 @@ export type PopulationPromise<T extends SerializedLexicalNode = SerializedLexica
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
}) => void
export type NodeValidation<T extends SerializedLexicalNode = SerializedLexicalNode> = ({
@@ -241,7 +242,7 @@ export type NodeWithHooks<T extends LexicalNode = any> = {
getSubFieldsData?: (args: {
node: ReturnType<ReplaceAny<T, LexicalNode>['exportJSON']>
req: PayloadRequest
}) => Record<string, unknown>
}) => JsonObject
/**
* Allows you to run population logic when a node's data was requested from graphQL.
* While `getSubFields` and `getSubFieldsData` automatically handle populating sub-fields (since they run hooks on them), those are only populated in the Rest API.
@@ -397,7 +398,7 @@ export type SanitizedServerFeatures = {
>
getSubFieldsData?: Map<
string,
(args: { node: SerializedLexicalNode; req: PayloadRequest }) => Record<string, unknown>
(args: { node: SerializedLexicalNode; req: PayloadRequest }) => JsonObject
>
graphQLPopulationPromises: Map<string, Array<PopulationPromise>>
hooks?: {

View File

@@ -8,7 +8,7 @@ import type {
NodeKey,
Spread,
} from 'lexical'
import type { CollectionSlug } from 'payload'
import type { CollectionSlug, JsonObject } from 'payload'
import type { JSX } from 'react'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
@@ -21,10 +21,7 @@ const RawUploadComponent = React.lazy(() =>
)
export type UploadData = {
fields: {
// unknown, custom fields:
[key: string]: unknown
}
fields: JsonObject
id: string
relationTo: CollectionSlug
value: number | string

View File

@@ -13,6 +13,7 @@ import {
afterReadTraverseFields,
beforeChangeTraverseFields,
beforeValidateTraverseFields,
deepCopyObject,
getDependencies,
withNullableJSONSchemaType,
} from 'payload'
@@ -35,7 +36,6 @@ import {
sanitizeServerEditorConfig,
sanitizeServerFeatures,
} from './lexical/config/server/sanitize.js'
import { cloneDeep } from './lexical/utils/cloneDeep.js'
import { populateLexicalPopulationPromises } from './populateGraphQL/populateLexicalPopulationPromises.js'
import { getGenerateComponentMap } from './utilities/generateComponentMap.js'
import { getGenerateSchemaMap } from './utilities/generateSchemaMap.js'
@@ -93,10 +93,10 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
defaultEditorConfig,
config,
)
features = cloneDeep(defaultEditorFeatures)
features = deepCopyObject(defaultEditorFeatures)
}
finalSanitizedEditorConfig = cloneDeep(defaultSanitizedServerEditorConfig)
finalSanitizedEditorConfig = deepCopyObject(defaultSanitizedServerEditorConfig)
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
} else {
@@ -109,12 +109,12 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
features =
props.features && typeof props.features === 'function'
? props.features({
defaultFeatures: cloneDeep(defaultEditorFeatures),
defaultFeatures: deepCopyObject(defaultEditorFeatures),
rootFeatures: rootEditorFeatures,
})
: (props.features as FeatureProviderServer<unknown, unknown, unknown>[])
if (!features) {
features = cloneDeep(defaultEditorFeatures)
features = deepCopyObject(defaultEditorFeatures)
}
const lexical: LexicalEditorConfig = props.lexical
@@ -463,8 +463,9 @@ export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapte
recurseNodeTree({
nodeIDMap: originalNodeWithLocalesIDMap,
nodes:
(siblingDocWithLocales[field.name] as SerializedEditorState)?.root?.children ??
[],
(siblingDocWithLocales[field.name] as unknown as SerializedEditorState)?.root
?.children ?? [],
})
}

View File

@@ -1,63 +0,0 @@
export function cloneDeep<T>(object: T, cache: WeakMap<any, any> = new WeakMap()): T {
if (object === null) return null
if (cache.has(object)) {
return cache.get(object)
}
// Handle Date
if (object instanceof Date) {
return new Date(object.getTime()) as unknown as T
}
// Handle RegExp
if (object instanceof RegExp) {
return new RegExp(object.source, object.flags) as unknown as T
}
// Handle Map
if (object instanceof Map) {
const clonedMap = new Map()
cache.set(object, clonedMap)
for (const [key, value] of object.entries()) {
clonedMap.set(key, cloneDeep(value, cache))
}
return clonedMap as unknown as T
}
// Handle Set
if (object instanceof Set) {
const clonedSet = new Set()
cache.set(object, clonedSet)
for (const value of object.values()) {
clonedSet.add(cloneDeep(value, cache))
}
return clonedSet as unknown as T
}
// Handle Array and Object
if (typeof object === 'object' && object !== null) {
if ('$$typeof' in object && typeof object.$$typeof === 'symbol') {
return object
}
const clonedObject: any = Array.isArray(object)
? []
: Object.create(Object.getPrototypeOf(object))
cache.set(object, clonedObject)
for (const key in object) {
if (
Object.prototype.hasOwnProperty.call(object, key) ||
Object.getOwnPropertySymbols(object).includes(key as any)
) {
clonedObject[key] = cloneDeep(object[key], cache)
}
}
return clonedObject as T
}
// Handle all other cases
return object
}

View File

@@ -59,6 +59,7 @@ export const populateLexicalPopulationPromises = ({
}
}
},
nodes: (siblingDoc[field?.name] as SerializedEditorState)?.root?.children ?? [],
nodes: (siblingDoc[field?.name] as unknown as SerializedEditorState)?.root?.children ?? [],
})
}

View File

@@ -1,4 +1,4 @@
import type { Field, PayloadRequest, RequestContext } from 'payload'
import type { Field, JsonObject, PayloadRequest, RequestContext } from 'payload'
import { afterReadTraverseFields } from 'payload'
@@ -25,7 +25,7 @@ type NestedRichTextFieldsArgs = {
populationPromises: Promise<void>[]
req: PayloadRequest
showHiddenFields: boolean
siblingDoc: Record<string, unknown>
siblingDoc: JsonObject
}
export const recursivelyPopulateFieldsForGraphQL = ({

View File

@@ -1,5 +1,5 @@
'use client'
import type { Data, FormState } from 'payload'
import type { Data, FormState, JsonObject } from 'payload'
import { Drawer } from '@payloadcms/ui'
import React from 'react'
@@ -12,7 +12,7 @@ export type FieldsDrawerProps = {
drawerSlug: string
drawerTitle?: string
featureKey: string
handleDrawerSubmit: (fields: FormState, data: Record<string, unknown>) => void
handleDrawerSubmit: (fields: FormState, data: JsonObject) => void
schemaPathSuffix?: string
}

View File

@@ -19,6 +19,11 @@
"types": "./src/exports/all.ts",
"default": "./src/exports/all.ts"
},
"./utilities": {
"import": "./src/exports/utilities.ts",
"types": "./src/exports/utilities.ts",
"default": "./src/exports/utilities.ts"
},
"./languages/*": {
"import": "./src/languages/*.ts",
"types": "./src/languages/*.ts",
@@ -61,6 +66,11 @@
"types": "./dist/exports/all.d.ts",
"default": "./dist/exports/all.js"
},
"./utilities": {
"import": "./dist/exports/utilities.js",
"types": "./dist/exports/utilities.d.ts",
"default": "./dist/exports/utilities.js"
},
"./languages/*": {
"import": "./dist/languages/*.js",
"types": "./dist/languages/*.d.ts",

View File

@@ -10,8 +10,7 @@ import type {
GenericTranslationsObject,
} from '../../src/types.js'
import { cloneDeep } from '../../src/utilities/cloneDeep.js'
import { deepMerge } from '../../src/utilities/deepMerge.js'
import { deepMergeSimple } from '../../src/utilities/deepMergeSimple.js'
import { acceptedLanguages } from '../../src/utilities/languages.js'
import { applyEslintFixes } from './applyEslintFixes.js'
import { findMissingKeys } from './findMissingKeys.js'
@@ -83,7 +82,7 @@ export async function translateObject(props: {
dateFNSKey: string
translations: GenericTranslationsObject
}
} = cloneDeep(allTranslationsObject)
} = JSON.parse(JSON.stringify(allTranslationsObject))
const allOnlyNewTranslatedTranslationsObject: GenericLanguages = {}
const translationPromises: Promise<void>[] = []
@@ -160,7 +159,7 @@ export async function translateObject(props: {
targetObj[keys[keys.length - 1]] = translated
allTranslatedTranslationsObject[targetLang].translations = sortKeys(
deepMerge(
deepMergeSimple(
allTranslatedTranslationsObject[targetLang].translations,
allOnlyNewTranslatedTranslationsObject[targetLang],
),

View File

@@ -0,0 +1 @@
export { deepMergeSimple } from '../utilities/deepMergeSimple.js'

View File

@@ -1,63 +0,0 @@
export function cloneDeep<T>(object: T, cache: WeakMap<any, any> = new WeakMap()): T {
if (object === null) return null
if (cache.has(object)) {
return cache.get(object)
}
// Handle Date
if (object instanceof Date) {
return new Date(object.getTime()) as unknown as T
}
// Handle RegExp
if (object instanceof RegExp) {
return new RegExp(object.source, object.flags) as unknown as T
}
// Handle Map
if (object instanceof Map) {
const clonedMap = new Map()
cache.set(object, clonedMap)
for (const [key, value] of object.entries()) {
clonedMap.set(key, cloneDeep(value, cache))
}
return clonedMap as unknown as T
}
// Handle Set
if (object instanceof Set) {
const clonedSet = new Set()
cache.set(object, clonedSet)
for (const value of object.values()) {
clonedSet.add(cloneDeep(value, cache))
}
return clonedSet as unknown as T
}
// Handle Array and Object
if (typeof object === 'object' && object !== null) {
if ('$$typeof' in object && typeof object.$$typeof === 'symbol') {
return object
}
const clonedObject: any = Array.isArray(object)
? []
: Object.create(Object.getPrototypeOf(object))
cache.set(object, clonedObject)
for (const key in object) {
if (
Object.prototype.hasOwnProperty.call(object, key) ||
Object.getOwnPropertySymbols(object).includes(key as any)
) {
clonedObject[key] = cloneDeep(object[key], cache)
}
}
return clonedObject as T
}
// Handle all other cases
return object
}

View File

@@ -1,32 +0,0 @@
/**
* obj2 has priority over obj1
*
* Merges obj2 into obj1. Does not handle arrays
*/
export function deepMerge(obj1: any, obj2: any, doNotMergeInNulls = true) {
const output = { ...obj1 }
for (const key in obj2) {
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
if (doNotMergeInNulls) {
if (
(obj2[key] === null || obj2[key] === undefined) &&
obj1[key] !== null &&
obj1[key] !== undefined
) {
continue
}
}
if (typeof obj2[key] === 'object' && obj1[key]) {
// Existing behavior for objects
output[key] = deepMerge(obj1[key], obj2[key], doNotMergeInNulls)
} else {
// Direct assignment for values
output[key] = obj2[key]
}
}
}
return output
}

View File

@@ -0,0 +1,25 @@
/**
* Very simple, but fast deepMerge implementation. Only deepMerges objects, not arrays and clones everything.
* Do not use this if your object contains any complex objects like React Components, or if you would like to combine Arrays.
* If you only have simple objects and need a fast deepMerge, this is the function for you.
*
* obj2 takes precedence over obj1 - thus if obj2 has a key that obj1 also has, obj2's value will be used.
*
* @param obj1 base object
* @param obj2 object to merge "into" obj1
*/
export function deepMergeSimple<T = object>(obj1: object, obj2: object): T {
const output = { ...obj1 }
for (const key in obj2) {
if (Object.prototype.hasOwnProperty.call(obj2, key)) {
if (typeof obj2[key] === 'object' && !Array.isArray(obj2[key]) && obj1[key]) {
output[key] = deepMergeSimple(obj1[key], obj2[key])
} else {
output[key] = obj2[key]
}
}
}
return output as T
}

View File

@@ -8,7 +8,7 @@ import type {
} from '../types.js'
import { importDateFNSLocale } from '../importDateFNSLocale.js'
import { deepMerge } from './deepMerge.js'
import { deepMergeSimple } from './deepMergeSimple.js'
import { getTranslationsByContext } from './getTranslationsByContext.js'
/**
@@ -143,7 +143,9 @@ export function t<
const initTFunction: InitTFunction = (args) => {
const { config, language, translations } = args
const mergedTranslations = deepMerge(translations, config?.translations?.[language] ?? {})
const mergedTranslations = config?.translations?.[language]
? deepMergeSimple<DefaultTranslationsObject>(translations, config?.translations?.[language])
: translations
return {
t: (key, vars) => {

View File

@@ -1,4 +1,4 @@
import { deepMerge, isReactServerComponentOrFunction, serverProps } from 'payload/shared'
import { isReactServerComponentOrFunction, serverProps } from 'payload/shared'
import React from 'react'
/**
@@ -37,7 +37,7 @@ export function withMergedProps<ToMergeIntoProps, CompleteReturnProps>({
}
// A wrapper around the args.Component to inject the args.toMergeArgs as props, which are merged with the passed props
const MergedPropsComponent: React.FC<CompleteReturnProps> = (passedProps) => {
const mergedProps = deepMerge(passedProps, toMergeIntoProps)
const mergedProps = simpleMergeProps(passedProps, toMergeIntoProps) as CompleteReturnProps
if (sanitizeServerOnlyProps) {
serverProps.forEach((prop) => {
@@ -50,3 +50,7 @@ export function withMergedProps<ToMergeIntoProps, CompleteReturnProps>({
return MergedPropsComponent
}
function simpleMergeProps(props, toMerge) {
return { ...props, ...toMerge }
}

View File

@@ -2,7 +2,7 @@ import type { FormField, FormState, Row } from 'payload'
import ObjectIdImport from 'bson-objectid'
import { dequal } from 'dequal/lite' // lite: no need for Map and Set support
import { deepCopyObject } from 'payload/shared'
import { deepCopyObject, deepCopyObjectSimple } from 'payload/shared'
import type { FieldAction } from './types.js'
@@ -249,11 +249,14 @@ export function fieldReducer(state: FormState, action: FieldAction): FormState {
const { remainingFields, rows } = separateRows(path, state)
const rowsMetadata = [...(state[path].rows || [])]
const duplicateRowMetadata = deepCopyObject(rowsMetadata[rowIndex])
const duplicateRowMetadata = deepCopyObjectSimple(rowsMetadata[rowIndex])
if (duplicateRowMetadata.id) duplicateRowMetadata.id = new ObjectId().toHexString()
const duplicateRowState = deepCopyObject(rows[rowIndex])
if (duplicateRowState.id) duplicateRowState.id = new ObjectId().toHexString()
if (duplicateRowState.id) {
duplicateRowState.id.value = new ObjectId().toHexString()
duplicateRowState.id.initialValue = new ObjectId().toHexString()
}
// If there are subfields
if (Object.keys(duplicateRowState).length > 0) {

9
pnpm-lock.yaml generated
View File

@@ -258,9 +258,6 @@ importers:
bson-objectid:
specifier: 2.0.4
version: 2.0.4
deepmerge:
specifier: 4.3.1
version: 4.3.1
http-status:
specifier: 1.6.2
version: 1.6.2
@@ -868,9 +865,6 @@ importers:
'@payloadcms/ui':
specifier: workspace:*
version: link:../ui
deepmerge:
specifier: ^4.2.2
version: 4.3.1
escape-html:
specifier: ^1.0.3
version: 1.0.3
@@ -951,9 +945,6 @@ importers:
'@payloadcms/ui':
specifier: workspace:*
version: link:../ui
deepmerge:
specifier: 4.3.1
version: 4.3.1
react:
specifier: ^19.0.0-rc-6230622a1a-20240610
version: 19.0.0-rc-fb9a90fa48-20240614

View File

@@ -489,7 +489,7 @@ describe('Auth', () => {
expect(lockedUser.docs[0].loginAttempts).toBe(2)
expect(lockedUser.docs[0].lockUntil).toBeDefined()
const manuallyReleaseLock = new Date(Date.now() - 605 * 1000)
const manuallyReleaseLock = new Date(Date.now() - 605 * 1000).toISOString()
const userLockElapsed = await payload.update({
collection: slug,
data: {
@@ -503,7 +503,7 @@ describe('Auth', () => {
},
})
expect(userLockElapsed.docs[0].lockUntil).toEqual(manuallyReleaseLock.toISOString())
expect(userLockElapsed.docs[0].lockUntil).toEqual(manuallyReleaseLock)
// login
await restClient.POST(`/${slug}/login`, {

View File

@@ -286,7 +286,7 @@ describe('Localization', () => {
const post = await payload.create({
collection: localizedSortSlug,
data: {
date: new Date(),
date: new Date().toISOString(),
title: `EN ${i}`,
},
locale: englishLocale,
@@ -296,7 +296,7 @@ describe('Localization', () => {
id: post.id,
collection: localizedSortSlug,
data: {
date: new Date(),
date: new Date().toISOString(),
title: `ES ${i}`,
},
locale: spanishLocale,