perf(db-mongodb): remove JSON.parse(JSON.stringify) copying of results (#11293)

Improves performance and optimizes memory usage for mongodb adapter by
cutting down copying of results via `JSON.parse(JSON.stringify())`.
Instead, `transform` does necessary transformations (`ObjectID` ->
`string,` `Date` -> `string`) without any copying
This commit is contained in:
Sasha
2025-02-21 17:31:24 +02:00
committed by GitHub
parent c7c5018675
commit 1dc748d341
23 changed files with 578 additions and 420 deletions

View File

@@ -5,7 +5,7 @@ import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js' import { handleError } from './utilities/handleError.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { transform } from './utilities/transform.js'
export const create: Create = async function create( export const create: Create = async function create(
this: MongooseAdapter, this: MongooseAdapter,
@@ -18,31 +18,31 @@ export const create: Create = async function create(
let doc let doc
const sanitizedData = sanitizeRelationshipIDs({ transform({
config: this.payload.config, adapter: this,
data, data,
fields: this.payload.collections[collection].config.fields, fields: this.payload.collections[collection].config.fields,
operation: 'write',
}) })
if (this.payload.collections[collection].customIDType) { if (this.payload.collections[collection].customIDType) {
sanitizedData._id = sanitizedData.id data._id = data.id
} }
try { try {
;[doc] = await Model.create([sanitizedData], options) ;[doc] = await Model.create([data], options)
} catch (error) { } catch (error) {
handleError({ collection, error, req }) handleError({ collection, error, req })
} }
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here doc = doc.toObject()
const result: Document = JSON.parse(JSON.stringify(doc))
const verificationToken = doc._verificationToken
// custom id type reset transform({
result.id = result._id adapter: this,
if (verificationToken) { data: doc,
result._verificationToken = verificationToken fields: this.payload.collections[collection].config.fields,
} operation: 'read',
})
return result return doc
} }

View File

@@ -4,8 +4,7 @@ import type { CreateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const createGlobal: CreateGlobal = async function createGlobal( export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter, this: MongooseAdapter,
@@ -13,26 +12,28 @@ export const createGlobal: CreateGlobal = async function createGlobal(
) { ) {
const Model = this.globals const Model = this.globals
const global = sanitizeRelationshipIDs({ transform({
config: this.payload.config, adapter: this,
data: { data,
globalType: slug,
...data,
},
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields, fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
globalSlug: slug,
operation: 'write',
}) })
const options: CreateOptions = { const options: CreateOptions = {
session: await getSession(this, req), session: await getSession(this, req),
} }
let [result] = (await Model.create([global], options)) as any let [result] = (await Model.create([data], options)) as any
result = JSON.parse(JSON.stringify(result)) result = result.toObject()
// custom id type reset transform({
result.id = result._id adapter: this,
result = sanitizeInternalFields(result) data: result,
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
operation: 'read',
})
return result return result
} }

View File

@@ -1,11 +1,11 @@
import type { CreateOptions } from 'mongoose' import type { CreateOptions } from 'mongoose'
import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload' import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { transform } from './utilities/transform.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter, this: MongooseAdapter,
@@ -26,25 +26,30 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
session: await getSession(this, req), session: await getSession(this, req),
} }
const data = sanitizeRelationshipIDs({ const data = {
config: this.payload.config, autosave,
data: { createdAt,
autosave, latest: true,
createdAt, parent,
latest: true, publishedLocale,
parent, snapshot,
publishedLocale, updatedAt,
snapshot, version: versionData,
updatedAt, }
version: versionData,
}, const fields = buildVersionGlobalFields(
fields: buildVersionGlobalFields( this.payload.config,
this.payload.config, this.payload.config.globals.find((global) => global.slug === globalSlug),
this.payload.config.globals.find((global) => global.slug === globalSlug), )
),
transform({
adapter: this,
data,
fields,
operation: 'write',
}) })
const [doc] = await VersionModel.create([data], options, req) let [doc] = await VersionModel.create([data], options, req)
await VersionModel.updateMany( await VersionModel.updateMany(
{ {
@@ -70,13 +75,14 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
options, options,
) )
const result: Document = JSON.parse(JSON.stringify(doc)) doc = doc.toObject()
const verificationToken = doc._verificationToken
// custom id type reset transform({
result.id = result._id adapter: this,
if (verificationToken) { data: doc,
result._verificationToken = verificationToken fields,
} operation: 'read',
return result })
return doc
} }

View File

@@ -1,12 +1,11 @@
import type { CreateOptions } from 'mongoose' import type { CreateOptions } from 'mongoose'
import { Types } from 'mongoose' import { buildVersionCollectionFields, type CreateVersion } from 'payload'
import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { transform } from './utilities/transform.js'
export const createVersion: CreateVersion = async function createVersion( export const createVersion: CreateVersion = async function createVersion(
this: MongooseAdapter, this: MongooseAdapter,
@@ -27,25 +26,30 @@ export const createVersion: CreateVersion = async function createVersion(
session: await getSession(this, req), session: await getSession(this, req),
} }
const data = sanitizeRelationshipIDs({ const data = {
config: this.payload.config, autosave,
data: { createdAt,
autosave, latest: true,
createdAt, parent,
latest: true, publishedLocale,
parent, snapshot,
publishedLocale, updatedAt,
snapshot, version: versionData,
updatedAt, }
version: versionData,
}, const fields = buildVersionCollectionFields(
fields: buildVersionCollectionFields( this.payload.config,
this.payload.config, this.payload.collections[collectionSlug].config,
this.payload.collections[collectionSlug].config, )
),
transform({
adapter: this,
data,
fields,
operation: 'write',
}) })
const [doc] = await VersionModel.create([data], options, req) let [doc] = await VersionModel.create([data], options, req)
const parentQuery = { const parentQuery = {
$or: [ $or: [
@@ -56,13 +60,6 @@ export const createVersion: CreateVersion = async function createVersion(
}, },
], ],
} }
if (data.parent instanceof Types.ObjectId) {
parentQuery.$or.push({
parent: {
$eq: data.parent.toString(),
},
})
}
await VersionModel.updateMany( await VersionModel.updateMany(
{ {
@@ -89,13 +86,14 @@ export const createVersion: CreateVersion = async function createVersion(
options, options,
) )
const result: Document = JSON.parse(JSON.stringify(doc)) doc = doc.toObject()
const verificationToken = doc._verificationToken
// custom id type reset transform({
result.id = result._id adapter: this,
if (verificationToken) { data: doc,
result._verificationToken = verificationToken fields,
} operation: 'read',
return result })
return doc
} }

View File

@@ -1,12 +1,12 @@
import type { QueryOptions } from 'mongoose' import type { QueryOptions } from 'mongoose'
import type { DeleteOne, Document } from 'payload' import type { DeleteOne } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js' import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const deleteOne: DeleteOne = async function deleteOne( export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter, this: MongooseAdapter,
@@ -35,11 +35,12 @@ export const deleteOne: DeleteOne = async function deleteOne(
return null return null
} }
let result: Document = JSON.parse(JSON.stringify(doc)) transform({
adapter: this,
data: doc,
fields: this.payload.collections[collection].config.fields,
operation: 'read',
})
// custom id type reset return doc
result.id = result._id
result = sanitizeInternalFields(result)
return result
} }

View File

@@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const find: Find = async function find( export const find: Find = async function find(
this: MongooseAdapter, this: MongooseAdapter,
@@ -133,13 +133,12 @@ export const find: Find = async function find(
result = await Model.paginate(query, paginationOptions) result = await Model.paginate(query, paginationOptions)
} }
const docs = JSON.parse(JSON.stringify(result.docs)) transform({
adapter: this,
data: result.docs,
fields: this.payload.collections[collection].config.fields,
operation: 'read',
})
return { return result
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
} }

View File

@@ -8,14 +8,15 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js' import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const findGlobal: FindGlobal = async function findGlobal( export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter, this: MongooseAdapter,
{ slug, locale, req, select, where }, { slug, locale, req, select, where },
) { ) {
const Model = this.globals const Model = this.globals
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields const globalConfig = this.payload.globals.config.find((each) => each.slug === slug)
const fields = globalConfig.flattenedFields
const options: QueryOptions = { const options: QueryOptions = {
lean: true, lean: true,
select: buildProjectionFromSelect({ select: buildProjectionFromSelect({
@@ -34,18 +35,18 @@ export const findGlobal: FindGlobal = async function findGlobal(
where: combineQueries({ globalType: { equals: slug } }, where), where: combineQueries({ globalType: { equals: slug } }, where),
}) })
let doc = (await Model.findOne(query, {}, options)) as any const doc = (await Model.findOne(query, {}, options)) as any
if (!doc) { if (!doc) {
return null return null
} }
if (doc._id) {
doc.id = doc._id
delete doc._id
}
doc = JSON.parse(JSON.stringify(doc)) transform({
doc = sanitizeInternalFields(doc) adapter: this,
data: doc,
fields: globalConfig.fields,
operation: 'read',
})
return doc return doc
} }

View File

@@ -9,18 +9,15 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: MongooseAdapter, this: MongooseAdapter,
{ global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where }, { global, limit, locale, page, pagination, req, select, skip, sort: sortArg, where },
) { ) {
const globalConfig = this.payload.globals.config.find(({ slug }) => slug === global)
const Model = this.versions[global] const Model = this.versions[global]
const versionFields = buildVersionGlobalFields( const versionFields = buildVersionGlobalFields(this.payload.config, globalConfig, true)
this.payload.config,
this.payload.globals.config.find(({ slug }) => slug === global),
true,
)
const session = await getSession(this, req) const session = await getSession(this, req)
const options: QueryOptions = { const options: QueryOptions = {
@@ -103,13 +100,13 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
} }
const result = await Model.paginate(query, paginationOptions) const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return { transform({
...result, adapter: this,
docs: docs.map((doc) => { data: result.docs,
doc.id = doc._id fields: buildVersionGlobalFields(this.payload.config, globalConfig),
return sanitizeInternalFields(doc) operation: 'read',
}), })
}
return result
} }

View File

@@ -1,5 +1,5 @@
import type { AggregateOptions, QueryOptions } from 'mongoose' import type { AggregateOptions, QueryOptions } from 'mongoose'
import type { Document, FindOne } from 'payload' import type { FindOne } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
@@ -7,7 +7,7 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const findOne: FindOne = async function findOne( export const findOne: FindOne = async function findOne(
this: MongooseAdapter, this: MongooseAdapter,
@@ -58,11 +58,7 @@ export const findOne: FindOne = async function findOne(
return null return null
} }
let result: Document = JSON.parse(JSON.stringify(doc)) transform({ adapter: this, data: doc, fields: collectionConfig.fields, operation: 'read' })
// custom id type reset return doc
result.id = result._id
result = sanitizeInternalFields(result)
return result
} }

View File

@@ -9,7 +9,7 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const findVersions: FindVersions = async function findVersions( export const findVersions: FindVersions = async function findVersions(
this: MongooseAdapter, this: MongooseAdapter,
@@ -104,13 +104,13 @@ export const findVersions: FindVersions = async function findVersions(
} }
const result = await Model.paginate(query, paginationOptions) const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return { transform({
...result, adapter: this,
docs: docs.map((doc) => { data: result.docs,
doc.id = doc._id fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
return sanitizeInternalFields(doc) operation: 'read',
}), })
}
return result
} }

View File

@@ -476,6 +476,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
schemaToReturn = { schemaToReturn = {
_id: false,
type: payload.config.localization.localeCodes.reduce((locales, locale) => { type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {} let localeSchema: { [key: string]: any } = {}
@@ -698,6 +699,7 @@ const fieldToSchemaMap: Record<string, FieldSchemaGenerator> = {
if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) {
schemaToReturn = { schemaToReturn = {
_id: false,
type: payload.config.localization.localeCodes.reduce((locales, locale) => { type: payload.config.localization.localeCodes.reduce((locales, locale) => {
let localeSchema: { [key: string]: any } = {} let localeSchema: { [key: string]: any } = {}

View File

@@ -6,11 +6,12 @@ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import type { MongooseAdapter } from '../index.js' import type { MongooseAdapter } from '../index.js'
import { getSession } from '../utilities/getSession.js' import { getSession } from '../utilities/getSession.js'
import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js' import { transform } from '../utilities/transform.js'
const migrateModelWithBatching = async ({ const migrateModelWithBatching = async ({
batchSize, batchSize,
config, config,
db,
fields, fields,
Model, Model,
parentIsLocalized, parentIsLocalized,
@@ -18,6 +19,7 @@ const migrateModelWithBatching = async ({
}: { }: {
batchSize: number batchSize: number
config: SanitizedConfig config: SanitizedConfig
db: MongooseAdapter
fields: Field[] fields: Field[]
Model: Model<any> Model: Model<any>
parentIsLocalized: boolean parentIsLocalized: boolean
@@ -49,7 +51,7 @@ const migrateModelWithBatching = async ({
} }
for (const doc of docs) { for (const doc of docs) {
sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized }) transform({ adapter: db, data: doc, fields, operation: 'write', parentIsLocalized })
} }
await Model.collection.bulkWrite( await Model.collection.bulkWrite(
@@ -124,6 +126,7 @@ export async function migrateRelationshipsV2_V3({
await migrateModelWithBatching({ await migrateModelWithBatching({
batchSize, batchSize,
config, config,
db,
fields: collection.fields, fields: collection.fields,
Model: db.collections[collection.slug], Model: db.collections[collection.slug],
parentIsLocalized: false, parentIsLocalized: false,
@@ -139,6 +142,7 @@ export async function migrateRelationshipsV2_V3({
await migrateModelWithBatching({ await migrateModelWithBatching({
batchSize, batchSize,
config, config,
db,
fields: buildVersionCollectionFields(config, collection), fields: buildVersionCollectionFields(config, collection),
Model: db.versions[collection.slug], Model: db.versions[collection.slug],
parentIsLocalized: false, parentIsLocalized: false,
@@ -167,10 +171,11 @@ export async function migrateRelationshipsV2_V3({
// in case if the global doesn't exist in the database yet (not saved) // in case if the global doesn't exist in the database yet (not saved)
if (doc) { if (doc) {
sanitizeRelationshipIDs({ transform({
config, adapter: db,
data: doc, data: doc,
fields: global.fields, fields: global.fields,
operation: 'write',
}) })
await GlobalsModel.collection.updateOne( await GlobalsModel.collection.updateOne(
@@ -191,6 +196,7 @@ export async function migrateRelationshipsV2_V3({
await migrateModelWithBatching({ await migrateModelWithBatching({
batchSize, batchSize,
config, config,
db,
fields: buildVersionGlobalFields(config, global), fields: buildVersionGlobalFields(config, global),
Model: db.versions[global.slug], Model: db.versions[global.slug],
parentIsLocalized: false, parentIsLocalized: false,

View File

@@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
export const queryDrafts: QueryDrafts = async function queryDrafts( export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter, this: MongooseAdapter,
@@ -124,18 +124,18 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
result = await VersionModel.paginate(versionQuery, paginationOptions) result = await VersionModel.paginate(versionQuery, paginationOptions)
} }
const docs = JSON.parse(JSON.stringify(result.docs)) transform({
adapter: this,
data: result.docs,
fields: buildVersionCollectionFields(this.payload.config, collectionConfig),
operation: 'read',
})
return { for (let i = 0; i < result.docs.length; i++) {
...result, const id = result.docs[i].parent
docs: docs.map((doc) => { result.docs[i] = result.docs[i].version
doc = { result.docs[i].id = id
_id: doc.parent,
id: doc.parent,
...doc.version,
}
return sanitizeInternalFields(doc)
}),
} }
return result
} }

View File

@@ -5,8 +5,7 @@ import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal( export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter, this: MongooseAdapter,
@@ -27,25 +26,11 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
session: await getSession(this, req), session: await getSession(this, req),
} }
let result transform({ adapter: this, data, fields, globalSlug: slug, operation: 'write' })
const sanitizedData = sanitizeRelationshipIDs({ const result: any = await Model.findOneAndUpdate({ globalType: slug }, data, options)
config: this.payload.config,
data,
fields,
})
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options) transform({ adapter: this, data: result, fields, globalSlug: slug, operation: 'read' })
if (!result) {
return null
}
result = JSON.parse(JSON.stringify(result))
// custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result return result
} }

View File

@@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js' import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { transform } from './utilities/transform.js'
export async function updateGlobalVersion<T extends TypeWithID>( export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter, this: MongooseAdapter,
@@ -47,26 +47,15 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereToUse, where: whereToUse,
}) })
const sanitizedData = sanitizeRelationshipIDs({ transform({ adapter: this, data: versionData, fields, operation: 'write' })
config: this.payload.config,
data: versionData,
fields,
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) { if (!doc) {
return null return null
} }
const result = JSON.parse(JSON.stringify(doc)) transform({ adapter: this, data: doc, fields, operation: 'read' })
const verificationToken = doc._verificationToken return doc
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
} }

View File

@@ -7,8 +7,7 @@ import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js' import { handleError } from './utilities/handleError.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' import { transform } from './utilities/transform.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const updateOne: UpdateOne = async function updateOne( export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter, this: MongooseAdapter,
@@ -39,14 +38,10 @@ export const updateOne: UpdateOne = async function updateOne(
let result let result
const sanitizedData = sanitizeRelationshipIDs({ transform({ adapter: this, data, fields, operation: 'write' })
config: this.payload.config,
data,
fields,
})
try { try {
result = await Model.findOneAndUpdate(query, sanitizedData, options) result = await Model.findOneAndUpdate(query, data, options)
} catch (error) { } catch (error) {
handleError({ collection, error, req }) handleError({ collection, error, req })
} }
@@ -55,9 +50,7 @@ export const updateOne: UpdateOne = async function updateOne(
return null return null
} }
result = JSON.parse(JSON.stringify(result)) transform({ adapter: this, data: result, fields, operation: 'read' })
result.id = result._id
result = sanitizeInternalFields(result)
return result return result
} }

View File

@@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js'
import { buildQuery } from './queries/buildQuery.js' import { buildQuery } from './queries/buildQuery.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' import { transform } from './utilities/transform.js'
export const updateVersion: UpdateVersion = async function updateVersion( export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter, this: MongooseAdapter,
@@ -45,26 +45,15 @@ export const updateVersion: UpdateVersion = async function updateVersion(
where: whereToUse, where: whereToUse,
}) })
const sanitizedData = sanitizeRelationshipIDs({ transform({ adapter: this, data: versionData, fields, operation: 'write' })
config: this.payload.config,
data: versionData,
fields,
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
if (!doc) { if (!doc) {
return null return null
} }
const result = JSON.parse(JSON.stringify(doc)) transform({ adapter: this, data: doc, fields, operation: 'write' })
const verificationToken = doc._verificationToken return doc
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
} }

View File

@@ -1,20 +0,0 @@
const internalFields = ['__v']
export const sanitizeInternalFields = <T extends Record<string, unknown>>(incomingDoc: T): T =>
Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => {
if (key === '_id') {
return {
...newDoc,
id: val,
}
}
if (internalFields.indexOf(key) > -1) {
return newDoc
}
return {
...newDoc,
[key]: val,
}
}, {} as T)

View File

@@ -1,165 +0,0 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
type Args = {
config: SanitizedConfig
data: Record<string, unknown>
fields: Field[]
parentIsLocalized?: boolean
}
interface RelationObject {
relationTo: string
value: number | string
}
function isValidRelationObject(value: unknown): value is RelationObject {
return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value
}
const convertValue = ({
relatedCollection,
value,
}: {
relatedCollection: CollectionConfig
value: number | string
}): number | string | Types.ObjectId => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (customIDField) {
return value
}
try {
return new Types.ObjectId(value)
} catch {
return value
}
}
const sanitizeRelationship = ({ config, field, locale, ref, value }) => {
let relatedCollection: CollectionConfig | undefined
let result = value
const hasManyRelations = typeof field.relationTo !== 'string'
if (!hasManyRelations) {
relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo)
}
if (Array.isArray(value)) {
result = value.map((val) => {
// Handle has many
if (relatedCollection && val && (typeof val === 'string' || typeof val === 'number')) {
return convertValue({
relatedCollection,
value: val,
})
}
// Handle has many - polymorphic
if (isValidRelationObject(val)) {
const relatedCollectionForSingleValue = config.collections?.find(
({ slug }) => slug === val.relationTo,
)
if (relatedCollectionForSingleValue) {
return {
relationTo: val.relationTo,
value: convertValue({
relatedCollection: relatedCollectionForSingleValue,
value: val.value,
}),
}
}
}
return val
})
}
// Handle has one - polymorphic
if (isValidRelationObject(value)) {
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
if (relatedCollection) {
result = {
relationTo: value.relationTo,
value: convertValue({ relatedCollection, value: value.value }),
}
}
}
// Handle has one
if (relatedCollection && value && (typeof value === 'string' || typeof value === 'number')) {
result = convertValue({
relatedCollection,
value,
})
}
if (locale) {
ref[locale] = result
} else {
ref[field.name] = result
}
}
export const sanitizeRelationshipIDs = ({
config,
data,
fields,
parentIsLocalized,
}: Args): Record<string, unknown> => {
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
return
}
if (field.type === 'relationship' || field.type === 'upload') {
if (!ref[field.name]) {
return
}
// handle localized relationships
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
if (typeof fieldRef !== 'object') {
return
}
for (const { code } of locales) {
const value = ref[field.name][code]
if (value) {
sanitizeRelationship({ config, field, locale: code, ref: fieldRef, value })
}
}
} else {
// handle non-localized relationships
sanitizeRelationship({
config,
field,
locale: undefined,
ref,
value: ref[field.name],
})
}
}
}
traverseFields({
callback: sanitize,
config,
fields,
fillEmpty: false,
parentIsLocalized,
ref: data,
})
return data
}

View File

@@ -2,7 +2,8 @@ import { flattenAllFields, type Field, type SanitizedConfig } from 'payload'
import { Types } from 'mongoose' import { Types } from 'mongoose'
import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js' import { transform } from './transform.js'
import type { MongooseAdapter } from '../index.js'
const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => { const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => {
return Object.keys(obj).reduce( return Object.keys(obj).reduce(
@@ -297,7 +298,7 @@ const relsData = {
}, },
} }
describe('sanitizeRelationshipIDs', () => { describe('transform', () => {
it('should sanitize relationships', () => { it('should sanitize relationships', () => {
const data = { const data = {
...relsData, ...relsData,
@@ -382,7 +383,18 @@ describe('sanitizeRelationshipIDs', () => {
} }
const flattenValuesBefore = Object.values(flattenRelationshipValues(data)) const flattenValuesBefore = Object.values(flattenRelationshipValues(data))
sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields }) const mockAdapter = {
payload: {
config,
},
} as MongooseAdapter
transform({
adapter: mockAdapter,
operation: 'write',
data,
fields: config.collections[0].fields,
})
const flattenValuesAfter = Object.values(flattenRelationshipValues(data)) const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
flattenValuesAfter.forEach((value, i) => { flattenValuesAfter.forEach((value, i) => {

View File

@@ -0,0 +1,347 @@
import type {
CollectionConfig,
DateField,
Field,
JoinField,
RelationshipField,
SanitizedConfig,
TraverseFieldsCallback,
UploadField,
} from 'payload'
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData, fieldShouldBeLocalized } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
interface RelationObject {
relationTo: string
value: number | string
}
function isValidRelationObject(value: unknown): value is RelationObject {
return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value
}
const convertRelationshipValue = ({
operation,
relatedCollection,
validateRelationships,
value,
}: {
operation: Args['operation']
relatedCollection: CollectionConfig
validateRelationships?: boolean
value: unknown
}) => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (operation === 'read') {
if (value instanceof Types.ObjectId) {
return value.toHexString()
}
return value
}
if (customIDField) {
return value
}
if (typeof value === 'string') {
try {
return new Types.ObjectId(value)
} catch (e) {
if (validateRelationships) {
throw e
}
return value
}
}
return value
}
const sanitizeRelationship = ({
config,
field,
locale,
operation,
ref,
validateRelationships,
value,
}: {
config: SanitizedConfig
field: JoinField | RelationshipField | UploadField
locale?: string
operation: Args['operation']
ref: Record<string, unknown>
validateRelationships?: boolean
value?: unknown
}) => {
if (field.type === 'join') {
if (
operation === 'read' &&
value &&
typeof value === 'object' &&
'docs' in value &&
Array.isArray(value.docs)
) {
for (let i = 0; i < value.docs.length; i++) {
const item = value.docs[i]
if (item instanceof Types.ObjectId) {
value.docs[i] = item.toHexString()
} else if (Array.isArray(field.collection) && item) {
// Fields here for polymorphic joins cannot be determinted, JSON.parse needed
value.docs[i] = JSON.parse(JSON.stringify(value.docs[i]))
}
}
}
return value
}
let relatedCollection: CollectionConfig | undefined
let result = value
const hasManyRelations = typeof field.relationTo !== 'string'
if (!hasManyRelations) {
relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo)
}
if (Array.isArray(value)) {
result = value.map((val) => {
// Handle has many - polymorphic
if (isValidRelationObject(val)) {
const relatedCollectionForSingleValue = config.collections?.find(
({ slug }) => slug === val.relationTo,
)
if (relatedCollectionForSingleValue) {
return {
relationTo: val.relationTo,
value: convertRelationshipValue({
operation,
relatedCollection: relatedCollectionForSingleValue,
validateRelationships,
value: val.value,
}),
}
}
}
if (relatedCollection) {
return convertRelationshipValue({
operation,
relatedCollection,
validateRelationships,
value: val,
})
}
return val
})
}
// Handle has one - polymorphic
else if (isValidRelationObject(value)) {
relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo)
if (relatedCollection) {
result = {
relationTo: value.relationTo,
value: convertRelationshipValue({
operation,
relatedCollection,
validateRelationships,
value: value.value,
}),
}
}
}
// Handle has one
else if (relatedCollection) {
result = convertRelationshipValue({
operation,
relatedCollection,
validateRelationships,
value,
})
}
if (locale) {
ref[locale] = result
} else {
ref[field.name] = result
}
}
const sanitizeDate = ({
field,
locale,
ref,
value,
}: {
field: DateField
locale?: string
ref: Record<string, unknown>
value: unknown
}) => {
if (!value) {
return
}
if (value instanceof Date) {
value = value.toISOString()
}
if (locale) {
ref[locale] = value
} else {
ref[field.name] = value
}
}
type Args = {
/** instance of the adapter */
adapter: MongooseAdapter
/** data to transform, can be an array of documents or a single document */
data: Record<string, unknown> | Record<string, unknown>[]
/** fields accossiated with the data */
fields: Field[]
/** slug of the global, pass only when the operation is `write` */
globalSlug?: string
/**
* Type of the operation
* read - sanitizes ObjectIDs, Date to strings.
* write - sanitizes string relationships to ObjectIDs.
*/
operation: 'read' | 'write'
parentIsLocalized?: boolean
/**
* Throw errors on invalid relationships
* @default true
*/
validateRelationships?: boolean
}
export const transform = ({
adapter,
data,
fields,
globalSlug,
operation,
parentIsLocalized,
validateRelationships = true,
}: Args) => {
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
transform({ adapter, data: data[i], fields, globalSlug, operation, validateRelationships })
}
return
}
const {
payload: { config },
} = adapter
if (operation === 'read') {
delete data['__v']
data.id = data._id
delete data['_id']
if (data.id instanceof Types.ObjectId) {
data.id = data.id.toHexString()
}
}
if (operation === 'write' && globalSlug) {
data.globalType = globalSlug
}
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
return
}
if (field.type === 'date' && operation === 'read' && ref[field.name]) {
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const fieldRef = ref[field.name]
if (!fieldRef || typeof fieldRef !== 'object') {
return
}
for (const locale of config.localization.localeCodes) {
sanitizeDate({
field,
ref: fieldRef,
value: fieldRef[locale],
})
}
} else {
sanitizeDate({
field,
ref: ref as Record<string, unknown>,
value: ref[field.name],
})
}
}
if (
field.type === 'relationship' ||
field.type === 'upload' ||
(operation === 'read' && field.type === 'join')
) {
if (!ref[field.name]) {
return
}
// handle localized relationships
if (config.localization && fieldShouldBeLocalized({ field, parentIsLocalized })) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
if (typeof fieldRef !== 'object') {
return
}
for (const { code } of locales) {
const value = ref[field.name][code]
if (value) {
sanitizeRelationship({
config,
field,
locale: code,
operation,
ref: fieldRef,
validateRelationships,
value,
})
}
}
} else {
// handle non-localized relationships
sanitizeRelationship({
config,
field,
locale: undefined,
operation,
ref: ref as Record<string, unknown>,
validateRelationships,
value: ref[field.name],
})
}
}
}
traverseFields({
callback: sanitize,
config,
fields,
fillEmpty: false,
parentIsLocalized,
ref: data,
})
}

View File

@@ -175,7 +175,10 @@ describe('Joins Field', () => {
collection: categoriesSlug, collection: categoriesSlug,
}) })
expect(Object.keys(categoryWithPosts)).toStrictEqual(['id', 'group']) expect(categoryWithPosts).toStrictEqual({
id: categoryWithPosts.id,
group: categoryWithPosts.group,
})
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10) expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id') expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id')

View File

@@ -1648,7 +1648,10 @@ describe('Select', () => {
}, },
}) })
expect(Object.keys(res)).toStrictEqual(['id', 'text']) expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
}) })
it('should apply select with updateByID', async () => { it('should apply select with updateByID', async () => {
@@ -1661,7 +1664,10 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(Object.keys(res)).toStrictEqual(['id', 'text']) expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
}) })
it('should apply select with updateBulk', async () => { it('should apply select with updateBulk', async () => {
@@ -1680,7 +1686,10 @@ describe('Select', () => {
assert(res.docs[0]) assert(res.docs[0])
expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) expect(res.docs[0]).toStrictEqual({
id: res.docs[0].id,
text: res.docs[0].text,
})
}) })
it('should apply select with deleteByID', async () => { it('should apply select with deleteByID', async () => {
@@ -1692,7 +1701,10 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(Object.keys(res)).toStrictEqual(['id', 'text']) expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
}) })
it('should apply select with deleteBulk', async () => { it('should apply select with deleteBulk', async () => {
@@ -1710,7 +1722,10 @@ describe('Select', () => {
assert(res.docs[0]) assert(res.docs[0])
expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) expect(res.docs[0]).toStrictEqual({
id: res.docs[0].id,
text: res.docs[0].text,
})
}) })
it('should apply select with duplicate', async () => { it('should apply select with duplicate', async () => {
@@ -1722,7 +1737,10 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(Object.keys(res)).toStrictEqual(['id', 'text']) expect(res).toStrictEqual({
id: res.id,
text: res.text,
})
}) })
}) })