From 1dc748d3415e2f5ca4e3412ceffba77d1ee97007 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Fri, 21 Feb 2025 17:31:24 +0200 Subject: [PATCH] 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 --- packages/db-mongodb/src/create.ts | 28 +- packages/db-mongodb/src/createGlobal.ts | 27 +- .../db-mongodb/src/createGlobalVersion.ts | 60 +-- packages/db-mongodb/src/createVersion.ts | 68 ++-- packages/db-mongodb/src/deleteOne.ts | 17 +- packages/db-mongodb/src/find.ts | 17 +- packages/db-mongodb/src/findGlobal.ts | 19 +- packages/db-mongodb/src/findGlobalVersions.ts | 25 +- packages/db-mongodb/src/findOne.ts | 12 +- packages/db-mongodb/src/findVersions.ts | 18 +- packages/db-mongodb/src/models/buildSchema.ts | 2 + .../migrateRelationshipsV2_V3.ts | 14 +- packages/db-mongodb/src/queryDrafts.ts | 26 +- packages/db-mongodb/src/updateGlobal.ts | 23 +- .../db-mongodb/src/updateGlobalVersion.ts | 21 +- packages/db-mongodb/src/updateOne.ts | 15 +- packages/db-mongodb/src/updateVersion.ts | 21 +- .../src/utilities/sanitizeInternalFields.ts | 20 - .../src/utilities/sanitizeRelationshipIDs.ts | 165 --------- ...ationshipIDs.spec.ts => transform.spec.ts} | 18 +- .../db-mongodb/src/utilities/transform.ts | 347 ++++++++++++++++++ test/joins/int.spec.ts | 5 +- test/select/int.spec.ts | 30 +- 23 files changed, 578 insertions(+), 420 deletions(-) delete mode 100644 packages/db-mongodb/src/utilities/sanitizeInternalFields.ts delete mode 100644 packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts rename packages/db-mongodb/src/utilities/{sanitizeRelationshipIDs.spec.ts => transform.spec.ts} (95%) create mode 100644 packages/db-mongodb/src/utilities/transform.ts diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index 49e549b7e4..3c99e79324 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -5,7 +5,7 @@ import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.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( this: MongooseAdapter, @@ -18,31 +18,31 @@ export const create: Create = async function create( let doc - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + adapter: this, data, fields: this.payload.collections[collection].config.fields, + operation: 'write', }) if (this.payload.collections[collection].customIDType) { - sanitizedData._id = sanitizedData.id + data._id = data.id } try { - ;[doc] = await Model.create([sanitizedData], options) + ;[doc] = await Model.create([data], options) } catch (error) { 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 - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + doc = doc.toObject() - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } + transform({ + adapter: this, + data: doc, + fields: this.payload.collections[collection].config.fields, + operation: 'read', + }) - return result + return doc } diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index 969eeaed7c..28c10b39ca 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -4,8 +4,7 @@ import type { CreateGlobal } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const createGlobal: CreateGlobal = async function createGlobal( this: MongooseAdapter, @@ -13,26 +12,28 @@ export const createGlobal: CreateGlobal = async function createGlobal( ) { const Model = this.globals - const global = sanitizeRelationshipIDs({ - config: this.payload.config, - data: { - globalType: slug, - ...data, - }, + transform({ + adapter: this, + data, fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields, + globalSlug: slug, + operation: 'write', }) const options: CreateOptions = { 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 - result.id = result._id - result = sanitizeInternalFields(result) + transform({ + adapter: this, + data: result, + fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields, + operation: 'read', + }) return result } diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index a6fe7ccb5f..2137ca84aa 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -1,11 +1,11 @@ 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 { getSession } from './utilities/getSession.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( this: MongooseAdapter, @@ -26,25 +26,30 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo session: await getSession(this, req), } - const data = sanitizeRelationshipIDs({ - config: this.payload.config, - data: { - autosave, - createdAt, - latest: true, - parent, - publishedLocale, - snapshot, - updatedAt, - version: versionData, - }, - fields: buildVersionGlobalFields( - this.payload.config, - this.payload.config.globals.find((global) => global.slug === globalSlug), - ), + const data = { + autosave, + createdAt, + latest: true, + parent, + publishedLocale, + snapshot, + updatedAt, + version: versionData, + } + + const fields = buildVersionGlobalFields( + this.payload.config, + 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( { @@ -70,13 +75,14 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo options, ) - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + doc = doc.toObject() - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + transform({ + adapter: this, + data: doc, + fields, + operation: 'read', + }) + + return doc } diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index 3482345e24..a0894adc88 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -1,12 +1,11 @@ import type { CreateOptions } from 'mongoose' -import { Types } from 'mongoose' -import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload' +import { buildVersionCollectionFields, type CreateVersion } from 'payload' import type { MongooseAdapter } from './index.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( this: MongooseAdapter, @@ -27,25 +26,30 @@ export const createVersion: CreateVersion = async function createVersion( session: await getSession(this, req), } - const data = sanitizeRelationshipIDs({ - config: this.payload.config, - data: { - autosave, - createdAt, - latest: true, - parent, - publishedLocale, - snapshot, - updatedAt, - version: versionData, - }, - fields: buildVersionCollectionFields( - this.payload.config, - this.payload.collections[collectionSlug].config, - ), + const data = { + autosave, + createdAt, + latest: true, + parent, + publishedLocale, + snapshot, + updatedAt, + version: versionData, + } + + const fields = buildVersionCollectionFields( + this.payload.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 = { $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( { @@ -89,13 +86,14 @@ export const createVersion: CreateVersion = async function createVersion( options, ) - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + doc = doc.toObject() - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + transform({ + adapter: this, + data: doc, + fields, + operation: 'read', + }) + + return doc } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index 1ba5c2b7fd..0ae3231531 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -1,12 +1,12 @@ import type { QueryOptions } from 'mongoose' -import type { DeleteOne, Document } from 'payload' +import type { DeleteOne } from 'payload' import type { MongooseAdapter } from './index.js' import { buildQuery } from './queries/buildQuery.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, @@ -35,11 +35,12 @@ export const deleteOne: DeleteOne = async function deleteOne( 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 - result.id = result._id - result = sanitizeInternalFields(result) - - return result + return doc } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index ab8733f87c..65f556ec33 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, @@ -133,13 +133,12 @@ export const find: Find = async function find( 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 { - ...result, - docs: docs.map((doc) => { - doc.id = doc._id - return sanitizeInternalFields(doc) - }), - } + return result } diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index 61b61abdf7..dbefa258c8 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -8,14 +8,15 @@ import type { MongooseAdapter } from './index.js' import { buildQuery } from './queries/buildQuery.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, { slug, locale, req, select, where }, ) { 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 = { lean: true, select: buildProjectionFromSelect({ @@ -34,18 +35,18 @@ export const findGlobal: FindGlobal = async function findGlobal( 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) { return null } - if (doc._id) { - doc.id = doc._id - delete doc._id - } - doc = JSON.parse(JSON.stringify(doc)) - doc = sanitizeInternalFields(doc) + transform({ + adapter: this, + data: doc, + fields: globalConfig.fields, + operation: 'read', + }) return doc } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index b028a8d224..2ecd21edde 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -9,18 +9,15 @@ import { buildQuery } from './queries/buildQuery.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, { 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 versionFields = buildVersionGlobalFields( - this.payload.config, - this.payload.globals.config.find(({ slug }) => slug === global), - true, - ) + const versionFields = buildVersionGlobalFields(this.payload.config, globalConfig, true) const session = await getSession(this, req) const options: QueryOptions = { @@ -103,13 +100,13 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV } const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) - return { - ...result, - docs: docs.map((doc) => { - doc.id = doc._id - return sanitizeInternalFields(doc) - }), - } + transform({ + adapter: this, + data: result.docs, + fields: buildVersionGlobalFields(this.payload.config, globalConfig), + operation: 'read', + }) + + return result } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 8db28b3d59..7fec58d949 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,5 +1,5 @@ import type { AggregateOptions, QueryOptions } from 'mongoose' -import type { Document, FindOne } from 'payload' +import type { FindOne } from 'payload' import type { MongooseAdapter } from './index.js' @@ -7,7 +7,7 @@ import { buildQuery } from './queries/buildQuery.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, @@ -58,11 +58,7 @@ export const findOne: FindOne = async function findOne( return null } - let result: Document = JSON.parse(JSON.stringify(doc)) + transform({ adapter: this, data: doc, fields: collectionConfig.fields, operation: 'read' }) - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) - - return result + return doc } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 0f123b1f23..0b5eb37459 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -9,7 +9,7 @@ import { buildQuery } from './queries/buildQuery.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, @@ -104,13 +104,13 @@ export const findVersions: FindVersions = async function findVersions( } const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) - return { - ...result, - docs: docs.map((doc) => { - doc.id = doc._id - return sanitizeInternalFields(doc) - }), - } + transform({ + adapter: this, + data: result.docs, + fields: buildVersionCollectionFields(this.payload.config, collectionConfig), + operation: 'read', + }) + + return result } diff --git a/packages/db-mongodb/src/models/buildSchema.ts b/packages/db-mongodb/src/models/buildSchema.ts index af7f501585..2c6d5b69ed 100644 --- a/packages/db-mongodb/src/models/buildSchema.ts +++ b/packages/db-mongodb/src/models/buildSchema.ts @@ -476,6 +476,7 @@ const fieldToSchemaMap: Record = { if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { schemaToReturn = { + _id: false, type: payload.config.localization.localeCodes.reduce((locales, locale) => { let localeSchema: { [key: string]: any } = {} @@ -698,6 +699,7 @@ const fieldToSchemaMap: Record = { if (fieldShouldBeLocalized({ field, parentIsLocalized }) && payload.config.localization) { schemaToReturn = { + _id: false, type: payload.config.localization.localeCodes.reduce((locales, locale) => { let localeSchema: { [key: string]: any } = {} diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts index 11d5f4b4fc..7eabd67769 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts @@ -6,11 +6,12 @@ import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' import type { MongooseAdapter } from '../index.js' import { getSession } from '../utilities/getSession.js' -import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js' +import { transform } from '../utilities/transform.js' const migrateModelWithBatching = async ({ batchSize, config, + db, fields, Model, parentIsLocalized, @@ -18,6 +19,7 @@ const migrateModelWithBatching = async ({ }: { batchSize: number config: SanitizedConfig + db: MongooseAdapter fields: Field[] Model: Model parentIsLocalized: boolean @@ -49,7 +51,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - sanitizeRelationshipIDs({ config, data: doc, fields, parentIsLocalized }) + transform({ adapter: db, data: doc, fields, operation: 'write', parentIsLocalized }) } await Model.collection.bulkWrite( @@ -124,6 +126,7 @@ export async function migrateRelationshipsV2_V3({ await migrateModelWithBatching({ batchSize, config, + db, fields: collection.fields, Model: db.collections[collection.slug], parentIsLocalized: false, @@ -139,6 +142,7 @@ export async function migrateRelationshipsV2_V3({ await migrateModelWithBatching({ batchSize, config, + db, fields: buildVersionCollectionFields(config, collection), Model: db.versions[collection.slug], 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) if (doc) { - sanitizeRelationshipIDs({ - config, + transform({ + adapter: db, data: doc, fields: global.fields, + operation: 'write', }) await GlobalsModel.collection.updateOne( @@ -191,6 +196,7 @@ export async function migrateRelationshipsV2_V3({ await migrateModelWithBatching({ batchSize, config, + db, fields: buildVersionGlobalFields(config, global), Model: db.versions[global.slug], parentIsLocalized: false, diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index 0216d24fd0..1a8ba6269b 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -10,7 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, @@ -124,18 +124,18 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( 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 { - ...result, - docs: docs.map((doc) => { - doc = { - _id: doc.parent, - id: doc.parent, - ...doc.version, - } - - return sanitizeInternalFields(doc) - }), + for (let i = 0; i < result.docs.length; i++) { + const id = result.docs[i].parent + result.docs[i] = result.docs[i].version + result.docs[i].id = id } + + return result } diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 86686064db..3ed8a04b3c 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -5,8 +5,7 @@ import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const updateGlobal: UpdateGlobal = async function updateGlobal( this: MongooseAdapter, @@ -27,25 +26,11 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( session: await getSession(this, req), } - let result + transform({ adapter: this, data, fields, globalSlug: slug, operation: 'write' }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data, - fields, - }) + const result: any = await Model.findOneAndUpdate({ globalType: slug }, data, options) - result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options) - - if (!result) { - return null - } - - result = JSON.parse(JSON.stringify(result)) - - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + transform({ adapter: this, data: result, fields, globalSlug: slug, operation: 'read' }) return result } diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index 3337d63f5b..9bc4b592ea 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js' import { buildQuery } from './queries/buildQuery.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export async function updateGlobalVersion( this: MongooseAdapter, @@ -47,26 +47,15 @@ export async function updateGlobalVersion( where: whereToUse, }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data: versionData, - fields, - }) + transform({ adapter: this, data: versionData, fields, operation: 'write' }) - const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) + const doc = await VersionModel.findOneAndUpdate(query, versionData, options) if (!doc) { return null } - const result = JSON.parse(JSON.stringify(doc)) + transform({ adapter: this, data: doc, fields, operation: 'read' }) - const verificationToken = doc._verificationToken - - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return doc } diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index 569a36a83b..25dd47ba37 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -7,8 +7,7 @@ import { buildQuery } from './queries/buildQuery.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' import { handleError } from './utilities/handleError.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const updateOne: UpdateOne = async function updateOne( this: MongooseAdapter, @@ -39,14 +38,10 @@ export const updateOne: UpdateOne = async function updateOne( let result - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data, - fields, - }) + transform({ adapter: this, data, fields, operation: 'write' }) try { - result = await Model.findOneAndUpdate(query, sanitizedData, options) + result = await Model.findOneAndUpdate(query, data, options) } catch (error) { handleError({ collection, error, req }) } @@ -55,9 +50,7 @@ export const updateOne: UpdateOne = async function updateOne( return null } - result = JSON.parse(JSON.stringify(result)) - result.id = result._id - result = sanitizeInternalFields(result) + transform({ adapter: this, data: result, fields, operation: 'read' }) return result } diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 4f9bd5d4c5..a2431388a4 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -7,7 +7,7 @@ import type { MongooseAdapter } from './index.js' import { buildQuery } from './queries/buildQuery.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.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( this: MongooseAdapter, @@ -45,26 +45,15 @@ export const updateVersion: UpdateVersion = async function updateVersion( where: whereToUse, }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data: versionData, - fields, - }) + transform({ adapter: this, data: versionData, fields, operation: 'write' }) - const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) + const doc = await VersionModel.findOneAndUpdate(query, versionData, options) if (!doc) { return null } - const result = JSON.parse(JSON.stringify(doc)) + transform({ adapter: this, data: doc, fields, operation: 'write' }) - const verificationToken = doc._verificationToken - - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return doc } diff --git a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts deleted file mode 100644 index 14ab00da67..0000000000 --- a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts +++ /dev/null @@ -1,20 +0,0 @@ -const internalFields = ['__v'] - -export const sanitizeInternalFields = >(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) diff --git a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts deleted file mode 100644 index cb699cc294..0000000000 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts +++ /dev/null @@ -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 - 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 => { - 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 -} diff --git a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts b/packages/db-mongodb/src/utilities/transform.spec.ts similarity index 95% rename from packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts rename to packages/db-mongodb/src/utilities/transform.spec.ts index 4bc8c3c64f..85b5bdb49e 100644 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts +++ b/packages/db-mongodb/src/utilities/transform.spec.ts @@ -2,7 +2,8 @@ import { flattenAllFields, type Field, type SanitizedConfig } from 'payload' import { Types } from 'mongoose' -import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js' +import { transform } from './transform.js' +import type { MongooseAdapter } from '../index.js' const flattenRelationshipValues = (obj: Record, prefix = ''): Record => { return Object.keys(obj).reduce( @@ -297,7 +298,7 @@ const relsData = { }, } -describe('sanitizeRelationshipIDs', () => { +describe('transform', () => { it('should sanitize relationships', () => { const data = { ...relsData, @@ -382,7 +383,18 @@ describe('sanitizeRelationshipIDs', () => { } 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)) flattenValuesAfter.forEach((value, i) => { diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts new file mode 100644 index 0000000000..02c7b63cdf --- /dev/null +++ b/packages/db-mongodb/src/utilities/transform.ts @@ -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 + 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 + 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 | Record[] + /** 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, + 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, + validateRelationships, + value: ref[field.name], + }) + } + } + } + + traverseFields({ + callback: sanitize, + config, + fields, + fillEmpty: false, + parentIsLocalized, + ref: data, + }) +} diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index 12b535e01c..a83c3b3f92 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -175,7 +175,10 @@ describe('Joins Field', () => { 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[0]).toHaveProperty('id') diff --git a/test/select/int.spec.ts b/test/select/int.spec.ts index a106a8d2cb..dd8cf4f2c1 100644 --- a/test/select/int.spec.ts +++ b/test/select/int.spec.ts @@ -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 () => { @@ -1661,7 +1664,10 @@ describe('Select', () => { 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 () => { @@ -1680,7 +1686,10 @@ describe('Select', () => { 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 () => { @@ -1692,7 +1701,10 @@ describe('Select', () => { 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 () => { @@ -1710,7 +1722,10 @@ describe('Select', () => { 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 () => { @@ -1722,7 +1737,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res)).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) })