From bc42ce7421c004d219e71b794c6362d008a2f38b Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 28 Nov 2024 02:00:22 +0200 Subject: [PATCH 1/8] chore: squash changes --- packages/db-mongodb/src/count.ts | 30 +- .../db-mongodb/src/countGlobalVersions.ts | 30 +- packages/db-mongodb/src/countVersions.ts | 30 +- packages/db-mongodb/src/create.ts | 49 +-- packages/db-mongodb/src/createGlobal.ts | 41 +-- .../db-mongodb/src/createGlobalVersion.ts | 76 ++--- packages/db-mongodb/src/createVersion.ts | 78 ++--- packages/db-mongodb/src/deleteMany.ts | 10 +- packages/db-mongodb/src/deleteOne.ts | 31 +- packages/db-mongodb/src/deleteVersions.ts | 10 +- packages/db-mongodb/src/find.ts | 109 +++---- packages/db-mongodb/src/findGlobal.ts | 33 +- packages/db-mongodb/src/findGlobalVersions.ts | 88 ++---- packages/db-mongodb/src/findOne.ts | 54 ++-- packages/db-mongodb/src/findVersions.ts | 98 +++--- .../src/{withSession.ts => getSession.ts} | 6 +- .../migrateRelationshipsV2_V3.ts | 30 +- .../migrateVersionsV1_V2.ts | 4 +- .../db-mongodb/src/queries/buildSortParam.ts | 17 +- .../src/queries/getLocalizedSortProperty.ts | 4 +- .../src/queries/sanitizeQueryValue.ts | 24 ++ packages/db-mongodb/src/queryDrafts.ts | 111 +++---- packages/db-mongodb/src/updateGlobal.ts | 54 ++-- .../db-mongodb/src/updateGlobalVersion.ts | 62 ++-- packages/db-mongodb/src/updateOne.ts | 50 ++-- packages/db-mongodb/src/updateVersion.ts | 63 ++-- .../src/utilities/buildJoinAggregation.ts | 34 +-- .../utilities/buildProjectionFromSelect.ts | 23 +- packages/db-mongodb/src/utilities/findMany.ts | 131 ++++++++ .../src/utilities/sanitizeInternalFields.ts | 20 -- .../src/utilities/sanitizeRelationshipIDs.ts | 156 ---------- ...ationshipIDs.spec.ts => transform.spec.ts} | 18 +- .../db-mongodb/src/utilities/transform.ts | 282 ++++++++++++++++++ packages/payload/src/index.ts | 5 +- .../payload/src/utilities/traverseFields.ts | 93 +++++- test/joins/int.spec.ts | 5 +- test/select/int.spec.ts | 38 ++- 37 files changed, 1119 insertions(+), 878 deletions(-) rename packages/db-mongodb/src/{withSession.ts => getSession.ts} (71%) create mode 100644 packages/db-mongodb/src/utilities/findMany.ts 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} (93%) create mode 100644 packages/db-mongodb/src/utilities/transform.ts diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index 17622cf4c5a..7e83118a192 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -5,14 +5,14 @@ import { flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' export const count: Count = async function count( this: MongooseAdapter, { collection, locale, req = {} as PayloadRequest, where }, ) { const Model = this.collections[collection] - const options: CountOptions = await withSession(this, req) + const session = await getSession(this, req) let hasNearConstraint = false @@ -30,21 +30,23 @@ export const count: Count = async function count( // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 - if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - options.hint = { - _id: 1, - } - } - let result: number if (useEstimatedCount) { - result = await Model.estimatedDocumentCount({ session: options.session }) + result = await Model.collection.estimatedDocumentCount({ session }) } else { - result = await Model.countDocuments(query, options) + const options: CountOptions = { session } + + if (Object.keys(query).length === 0 && this.disableIndexHints !== true) { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + options.hint = { + _id: 1, + } + } + + result = await Model.collection.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts index 48cf67cb899..5dfb89590b6 100644 --- a/packages/db-mongodb/src/countGlobalVersions.ts +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -5,14 +5,14 @@ import { flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions( this: MongooseAdapter, { global, locale, req = {} as PayloadRequest, where }, ) { const Model = this.versions[global] - const options: CountOptions = await withSession(this, req) + const session = await getSession(this, req) let hasNearConstraint = false @@ -30,21 +30,23 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 - if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - options.hint = { - _id: 1, - } - } - let result: number if (useEstimatedCount) { - result = await Model.estimatedDocumentCount({ session: options.session }) + result = await Model.collection.estimatedDocumentCount({ session }) } else { - result = await Model.countDocuments(query, options) + const options: CountOptions = { session } + + if (Object.keys(query).length === 0 && this.disableIndexHints !== true) { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + options.hint = { + _id: 1, + } + } + + result = await Model.collection.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts index 6c7633e26a0..522a44d043a 100644 --- a/packages/db-mongodb/src/countVersions.ts +++ b/packages/db-mongodb/src/countVersions.ts @@ -5,14 +5,14 @@ import { flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' export const countVersions: CountVersions = async function countVersions( this: MongooseAdapter, { collection, locale, req = {} as PayloadRequest, where }, ) { const Model = this.versions[collection] - const options: CountOptions = await withSession(this, req) + const session = await getSession(this, req) let hasNearConstraint = false @@ -30,21 +30,23 @@ export const countVersions: CountVersions = async function countVersions( // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 - if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - options.hint = { - _id: 1, - } - } - let result: number if (useEstimatedCount) { - result = await Model.estimatedDocumentCount({ session: options.session }) + result = await Model.collection.estimatedDocumentCount({ session }) } else { - result = await Model.countDocuments(query, options) + const options: CountOptions = { session } + + if (Object.keys(query).length === 0 && this.disableIndexHints !== true) { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + options.hint = { + _id: 1, + } + } + + result = await Model.collection.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index 504cffd854a..fafbd813eba 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -1,44 +1,45 @@ -import type { Create, Document, PayloadRequest } from 'payload' +import type { Create, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { handleError } from './utilities/handleError.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const create: Create = async function create( this: MongooseAdapter, { collection, data, req = {} as PayloadRequest }, ) { const Model = this.collections[collection] - const options = await withSession(this, req) - let doc + const session = await getSession(this, req) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data, - fields: this.payload.collections[collection].config.fields, - }) + const fields = this.payload.collections[collection].config.flattenedFields if (this.payload.collections[collection].customIDType) { - sanitizedData._id = sanitizedData.id + data._id = data.id } + transform({ + type: 'write', + adapter: this, + data, + fields, + insert: true, + }) + try { - ;[doc] = await Model.create([sanitizedData], options) - } catch (error) { - handleError({ collection, error, req }) - } + const { insertedId } = await Model.collection.insertOne(data, { session }) + data._id = insertedId - // 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 + transform({ + type: 'read', + adapter: this, + data, + fields, + }) - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken + return data + } catch (error) { + handleError({ collection, error, req }) } - - return result } diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index d57c180f33d..e8f09abb0d4 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -2,9 +2,8 @@ import type { CreateGlobal, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' +import { transform } from './utilities/transform.js' export const createGlobal: CreateGlobal = async function createGlobal( this: MongooseAdapter, @@ -12,24 +11,30 @@ export const createGlobal: CreateGlobal = async function createGlobal( ) { const Model = this.globals - const global = sanitizeRelationshipIDs({ - config: this.payload.config, - data: { - globalType: slug, - ...data, - }, - fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields, + const fields = this.payload.config.globals.find( + (globalConfig) => globalConfig.slug === slug, + ).flattenedFields + + transform({ + type: 'write', + adapter: this, + data, + fields, + globalSlug: slug, + insert: true, }) - const options = await withSession(this, req) + const session = await getSession(this, req) - let [result] = (await Model.create([global], options)) as any + const { insertedId } = await Model.collection.insertOne(data, { session }) + ;(data as any)._id = insertedId - result = JSON.parse(JSON.stringify(result)) - - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + transform({ + type: 'read', + adapter: this, + data, + fields, + }) - return result + return data } diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index c149b7882f7..2f9d32eeac4 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -1,14 +1,9 @@ -import { - buildVersionGlobalFields, - type CreateGlobalVersion, - type Document, - type PayloadRequest, -} from 'payload' +import { buildVersionGlobalFields, type CreateGlobalVersion, type PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' +import { transform } from './utilities/transform.js' export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( this: MongooseAdapter, @@ -25,34 +20,42 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo }, ) { const VersionModel = this.versions[globalSlug] - const options = await withSession(this, req) + const 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), + true, + ) + + transform({ + type: 'write', + adapter: this, + data, + fields, + insert: true, }) - const [doc] = await VersionModel.create([data], options, req) + const { insertedId } = await VersionModel.collection.insertOne(data, { session }) + ;(data as any)._id = insertedId - await VersionModel.updateMany( + await VersionModel.collection.updateMany( { $and: [ { _id: { - $ne: doc._id, + $ne: insertedId, }, }, { @@ -68,16 +71,15 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo ], }, { $unset: { latest: 1 } }, - options, + { session }, ) - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + transform({ + type: 'read', + adapter: this, + data, + fields, + }) - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return data as any } diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index 75dd3eef4ae..fd3ad73a85d 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -1,15 +1,10 @@ import { Types } from 'mongoose' -import { - buildVersionCollectionFields, - type CreateVersion, - type Document, - type PayloadRequest, -} from 'payload' +import { buildVersionCollectionFields, type CreateVersion, type PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' +import { transform } from './utilities/transform.js' export const createVersion: CreateVersion = async function createVersion( this: MongooseAdapter, @@ -26,27 +21,35 @@ export const createVersion: CreateVersion = async function createVersion( }, ) { const VersionModel = this.versions[collectionSlug] - const options = await withSession(this, req) + const 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: any = { + autosave, + createdAt, + latest: true, + parent, + publishedLocale, + snapshot, + updatedAt, + version: versionData, + } + + const fields = buildVersionCollectionFields( + this.payload.config, + this.payload.collections[collectionSlug].config, + true, + ) + + transform({ + type: 'write', + adapter: this, + data, + fields, + insert: true, }) - const [doc] = await VersionModel.create([data], options, req) + const { insertedId } = await VersionModel.collection.insertOne(data, { session }) + data._id = insertedId const parentQuery = { $or: [ @@ -57,7 +60,7 @@ export const createVersion: CreateVersion = async function createVersion( }, ], } - if (data.parent instanceof Types.ObjectId) { + if ((data.parent as unknown) instanceof Types.ObjectId) { parentQuery.$or.push({ parent: { $eq: data.parent.toString(), @@ -65,12 +68,12 @@ export const createVersion: CreateVersion = async function createVersion( }) } - await VersionModel.updateMany( + await VersionModel.collection.updateMany( { $and: [ { _id: { - $ne: doc._id, + $ne: insertedId, }, }, parentQuery, @@ -82,16 +85,15 @@ export const createVersion: CreateVersion = async function createVersion( ], }, { $unset: { latest: 1 } }, - options, + { session }, ) - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + transform({ + type: 'read', + adapter: this, + data, + fields, + }) - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return data } diff --git a/packages/db-mongodb/src/deleteMany.ts b/packages/db-mongodb/src/deleteMany.ts index cc692e9e0eb..f6d60ed339c 100644 --- a/packages/db-mongodb/src/deleteMany.ts +++ b/packages/db-mongodb/src/deleteMany.ts @@ -2,22 +2,20 @@ import type { DeleteMany, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' export const deleteMany: DeleteMany = async function deleteMany( this: MongooseAdapter, { collection, req = {} as PayloadRequest, where }, ) { const Model = this.collections[collection] - const options = { - ...(await withSession(this, req)), - lean: true, - } const query = await Model.buildQuery({ payload: this.payload, where, }) - await Model.deleteMany(query, options) + await Model.collection.deleteMany(query, { + session: await getSession(this, req), + }) } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index edf9ebb6dc0..cef8daffec8 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -1,37 +1,40 @@ -import type { DeleteOne, Document, PayloadRequest } from 'payload' +import type { DeleteOne, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const deleteOne: DeleteOne = async function deleteOne( this: MongooseAdapter, { collection, req = {} as PayloadRequest, select, where }, ) { const Model = this.collections[collection] - const options = await withSession(this, req) + const session = await getSession(this, req) const query = await Model.buildQuery({ payload: this.payload, where, }) - const doc = await Model.findOneAndDelete(query, { - ...options, + const fields = this.payload.collections[collection].config.flattenedFields + + const doc = await Model.collection.findOneAndDelete(query, { projection: buildProjectionFromSelect({ adapter: this, - fields: this.payload.collections[collection].config.flattenedFields, + fields, select, }), - }).lean() - - let result: Document = JSON.parse(JSON.stringify(doc)) + session, + }) - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + transform({ + type: 'read', + adapter: this, + data: doc, + fields, + }) - return result + return doc } diff --git a/packages/db-mongodb/src/deleteVersions.ts b/packages/db-mongodb/src/deleteVersions.ts index cd36dbaeed3..90091710558 100644 --- a/packages/db-mongodb/src/deleteVersions.ts +++ b/packages/db-mongodb/src/deleteVersions.ts @@ -2,17 +2,13 @@ import type { DeleteVersions, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' -import { withSession } from './withSession.js' +import { getSession } from './getSession.js' export const deleteVersions: DeleteVersions = async function deleteVersions( this: MongooseAdapter, { collection, locale, req = {} as PayloadRequest, where }, ) { const VersionsModel = this.versions[collection] - const options = { - ...(await withSession(this, req)), - lean: true, - } const query = await VersionsModel.buildQuery({ locale, @@ -20,5 +16,7 @@ export const deleteVersions: DeleteVersions = async function deleteVersions( where, }) - await VersionsModel.deleteMany(query, options) + await VersionsModel.collection.deleteMany(query, { + session: await getSession(this, req), + }) } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 9295f8829c8..71c526db915 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -1,15 +1,16 @@ -import type { PaginateOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { Find, PayloadRequest } from 'payload' import { flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { findMany } from './utilities/findMany.js' +import { transform } from './utilities/transform.js' export const find: Find = async function find( this: MongooseAdapter, @@ -20,7 +21,6 @@ export const find: Find = async function find( locale, page, pagination, - projection, req = {} as PayloadRequest, select, sort: sortArg, @@ -29,7 +29,7 @@ export const find: Find = async function find( ) { const Model = this.collections[collection] const collectionConfig = this.payload.collections[collection].config - const options = await withSession(this, req) + const session = await getSession(this, req) let hasNearConstraint = false @@ -38,11 +38,13 @@ export const find: Find = async function find( hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) } + const fields = collectionConfig.flattenedFields + let sort if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, - fields: collectionConfig.flattenedFields, + fields, locale, sort: sortArg || collectionConfig.defaultSort, timestamps: true, @@ -57,83 +59,44 @@ export const find: Find = async function find( // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 - const paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, - options, - page, - pagination, - projection, - sort, - useEstimatedCount, - } - - if (select) { - paginationOptions.projection = buildProjectionFromSelect({ - adapter: this, - fields: collectionConfig.flattenedFields, - select, - }) - } - - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } - - if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - paginationOptions.useCustomCountFn = () => { - return Promise.resolve( - Model.countDocuments(query, { - ...options, - hint: { _id: 1 }, - }), - ) - } - } - if (limit >= 0) { - paginationOptions.limit = limit - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit - - // Disable pagination if limit is 0 - if (limit === 0) { - paginationOptions.pagination = false - } - } + const projection = buildProjectionFromSelect({ + adapter: this, + fields, + select, + }) - let result + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined - const aggregate = await buildJoinAggregation({ + const joinAgreggation = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, locale, + }) + + const result = await findMany({ + adapter: this, + collation, + collection: Model.collection, + joinAgreggation, + limit, + page, + pagination, + projection, query, + session, + sort, + useEstimatedCount, }) - // build join aggregation - if (aggregate) { - result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions) - } else { - result = await Model.paginate(query, paginationOptions) - } - const docs = JSON.parse(JSON.stringify(result.docs)) + transform({ type: 'read', adapter: this, data: result.docs, fields }) - 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 c89ef2e2960..d4220fff89e 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -4,24 +4,15 @@ import { combineQueries } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const findGlobal: FindGlobal = async function findGlobal( this: MongooseAdapter, { slug, locale, req = {} as PayloadRequest, select, where }, ) { const Model = this.globals - const options = { - ...(await withSession(this, req)), - lean: true, - select: buildProjectionFromSelect({ - adapter: this, - fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields, - select, - }), - } const query = await Model.buildQuery({ globalSlug: slug, @@ -30,18 +21,22 @@ export const findGlobal: FindGlobal = async function findGlobal( where: combineQueries({ globalType: { equals: slug } }, where), }) - let doc = (await Model.findOne(query, {}, options)) as any + const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields + + const doc = await Model.collection.findOne(query, { + projection: buildProjectionFromSelect({ + adapter: this, + fields, + select, + }), + session: await getSession(this, req), + }) if (!doc) { return null } - if (doc._id) { - doc.id = doc._id - delete doc._id - } - doc = JSON.parse(JSON.stringify(doc)) - doc = sanitizeInternalFields(doc) + transform({ type: 'read', adapter: this, data: doc, fields }) - return doc + return doc as any } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 406bac1f6d4..8ca518b713a 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -1,14 +1,15 @@ -import type { PaginateOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { FindGlobalVersions, PayloadRequest } from 'payload' import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { findMany } from './utilities/findMany.js' +import { transform } from './utilities/transform.js' export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( this: MongooseAdapter, @@ -31,11 +32,6 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV this.payload.globals.config.find(({ slug }) => slug === global), true, ) - const options = { - ...(await withSession(this, req)), - limit, - skip, - } let hasNearConstraint = false @@ -62,62 +58,40 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV where, }) + const session = await getSession(this, req) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 - const paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, + + const projection = buildProjectionFromSelect({ adapter: this, fields: versionFields, select }) + + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined + + const result = await findMany({ + adapter: this, + collation, + collection: Model.collection, limit, - options, page, pagination, - projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }), + projection, + query, + session, + skip, sort, useEstimatedCount, - } - - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } - - if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - paginationOptions.useCustomCountFn = () => { - return Promise.resolve( - Model.countDocuments(query, { - ...options, - hint: { _id: 1 }, - }), - ) - } - } - - if (limit >= 0) { - paginationOptions.limit = limit - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit - - // Disable pagination if limit is 0 - if (limit === 0) { - paginationOptions.pagination = false - } - } + }) - const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) + transform({ + type: 'read', + adapter: this, + data: result.docs, + fields: versionFields, + }) - return { - ...result, - docs: docs.map((doc) => { - doc.id = doc._id - return sanitizeInternalFields(doc) - }), - } + return result } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 5b82fd9effa..ef0b377a65f 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,12 +1,11 @@ -import type { MongooseQueryOptions, QueryOptions } from 'mongoose' -import type { Document, FindOne, PayloadRequest } from 'payload' +import type { FindOne, PayloadRequest } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const findOne: FindOne = async function findOne( this: MongooseAdapter, @@ -14,10 +13,8 @@ export const findOne: FindOne = async function findOne( ) { const Model = this.collections[collection] const collectionConfig = this.payload.collections[collection].config - const options: MongooseQueryOptions = { - ...(await withSession(this, req)), - lean: true, - } + + const session = await getSession(this, req) const query = await Model.buildQuery({ locale, @@ -25,40 +22,53 @@ export const findOne: FindOne = async function findOne( where, }) + const fields = collectionConfig.flattenedFields + const projection = buildProjectionFromSelect({ adapter: this, - fields: collectionConfig.flattenedFields, + fields, select, }) - const aggregate = await buildJoinAggregation({ + const joinAggregation = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, - limit: 1, locale, projection, - query, }) let doc - if (aggregate) { - ;[doc] = await Model.aggregate(aggregate, options) + if (joinAggregation) { + const cursor = Model.collection.aggregate( + [ + { + $match: query, + }, + ], + { session }, + ) + cursor.limit(1) + for (const stage of joinAggregation) { + cursor.addStage(stage) + } + + ;[doc] = await cursor.toArray() } else { - ;(options as Record).projection = projection - doc = await Model.findOne(query, {}, options) + doc = await Model.collection.findOne(query, { projection, session }) } if (!doc) { return null } - let result: Document = JSON.parse(JSON.stringify(doc)) - - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + transform({ + type: 'read', + adapter: this, + data: doc, + fields, + }) - return result + return doc } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index cb1810bf3bd..c65fcd71951 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -1,14 +1,15 @@ -import type { PaginateOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { FindVersions, PayloadRequest } from 'payload' import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { findMany } from './utilities/findMany.js' +import { transform } from './utilities/transform.js' export const findVersions: FindVersions = async function findVersions( this: MongooseAdapter, @@ -27,11 +28,8 @@ export const findVersions: FindVersions = async function findVersions( ) { const Model = this.versions[collection] const collectionConfig = this.payload.collections[collection].config - const options = { - ...(await withSession(this, req)), - limit, - skip, - } + + const session = await getSession(this, req) let hasNearConstraint = false @@ -57,66 +55,44 @@ export const findVersions: FindVersions = async function findVersions( where, }) + const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 - const paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, + + const projection = buildProjectionFromSelect({ + adapter: this, + fields: versionFields, + select, + }) + + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined + + const result = await findMany({ + adapter: this, + collation, + collection: Model.collection, limit, - options, page, pagination, - projection: buildProjectionFromSelect({ - adapter: this, - fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), - select, - }), + projection, + query, + session, + skip, sort, useEstimatedCount, - } - - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } - - if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - paginationOptions.useCustomCountFn = () => { - return Promise.resolve( - Model.countDocuments(query, { - ...options, - hint: { _id: 1 }, - }), - ) - } - } - - if (limit >= 0) { - paginationOptions.limit = limit - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit - - // Disable pagination if limit is 0 - if (limit === 0) { - paginationOptions.pagination = false - } - } + }) - const result = await Model.paginate(query, paginationOptions) - const docs = JSON.parse(JSON.stringify(result.docs)) + transform({ + type: 'read', + adapter: this, + data: result.docs, + fields: versionFields, + }) - return { - ...result, - docs: docs.map((doc) => { - doc.id = doc._id - return sanitizeInternalFields(doc) - }), - } + return result } diff --git a/packages/db-mongodb/src/withSession.ts b/packages/db-mongodb/src/getSession.ts similarity index 71% rename from packages/db-mongodb/src/withSession.ts rename to packages/db-mongodb/src/getSession.ts index c9f43254e79..55481d9b439 100644 --- a/packages/db-mongodb/src/withSession.ts +++ b/packages/db-mongodb/src/getSession.ts @@ -7,10 +7,10 @@ import type { MongooseAdapter } from './index.js' * returns the session belonging to the transaction of the req.session if exists * @returns ClientSession */ -export async function withSession( +export async function getSession( db: MongooseAdapter, req: PayloadRequest, -): Promise<{ session: ClientSession } | Record> { +): Promise { let transactionID = req.transactionID if (transactionID instanceof Promise) { @@ -18,6 +18,6 @@ export async function withSession( } if (req) { - return db.sessions[transactionID] ? { session: db.sessions[transactionID] } : {} + return db.sessions[transactionID] } } diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts index e5276891915..056abf42ff4 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts @@ -1,23 +1,23 @@ import type { ClientSession, Model } from 'mongoose' -import type { Field, PayloadRequest, SanitizedConfig } from 'payload' +import type { Field, FlattenedField, PayloadRequest } from 'payload' import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' import type { MongooseAdapter } from '../index.js' -import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js' -import { withSession } from '../withSession.js' +import { getSession } from '../getSession.js' +import { transform } from '../utilities/transform.js' const migrateModelWithBatching = async ({ + adapter, batchSize, - config, fields, Model, session, }: { + adapter: MongooseAdapter batchSize: number - config: SanitizedConfig - fields: Field[] + fields: FlattenedField[] Model: Model session: ClientSession }): Promise => { @@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - sanitizeRelationshipIDs({ config, data: doc, fields }) + transform({ type: 'write', adapter, data: doc, fields }) } await Model.collection.bulkWrite( @@ -109,15 +109,15 @@ export async function migrateRelationshipsV2_V3({ const db = payload.db as MongooseAdapter const config = payload.config - const { session } = await withSession(db, req) + const session = await getSession(db, req) for (const collection of payload.config.collections.filter(hasRelationshipOrUploadField)) { payload.logger.info(`Migrating collection "${collection.slug}"`) await migrateModelWithBatching({ + adapter: db, batchSize, - config, - fields: collection.fields, + fields: collection.flattenedFields, Model: db.collections[collection.slug], session, }) @@ -128,9 +128,9 @@ export async function migrateRelationshipsV2_V3({ payload.logger.info(`Migrating collection versions "${collection.slug}"`) await migrateModelWithBatching({ + adapter: db, batchSize, - config, - fields: buildVersionCollectionFields(config, collection), + fields: buildVersionCollectionFields(config, collection, true), Model: db.versions[collection.slug], session, }) @@ -156,7 +156,7 @@ export async function migrateRelationshipsV2_V3({ // in case if the global doesn't exist in the database yet (not saved) if (doc) { - sanitizeRelationshipIDs({ config, data: doc, fields: global.fields }) + transform({ type: 'write', adapter: db, data: doc, fields: global.flattenedFields }) await GlobalsModel.collection.updateOne( { @@ -173,9 +173,9 @@ export async function migrateRelationshipsV2_V3({ payload.logger.info(`Migrating global versions "${global.slug}"`) await migrateModelWithBatching({ + adapter: db, batchSize, - config, - fields: buildVersionGlobalFields(config, global), + fields: buildVersionGlobalFields(config, global, true), Model: db.versions[global.slug], session, }) diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateVersionsV1_V2.ts b/packages/db-mongodb/src/predefinedMigrations/migrateVersionsV1_V2.ts index 2177f1dee39..e035a983485 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateVersionsV1_V2.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateVersionsV1_V2.ts @@ -3,12 +3,12 @@ import type { Payload, PayloadRequest } from 'payload' import type { MongooseAdapter } from '../index.js' -import { withSession } from '../withSession.js' +import { getSession } from '../getSession.js' export async function migrateVersionsV1_V2({ req }: { req: PayloadRequest }) { const { payload } = req - const { session } = await withSession(payload.db as MongooseAdapter, req) + const session = await getSession(payload.db as MongooseAdapter, req) // For each collection diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index 6f76cbffef2..09f7cad6dfe 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -11,20 +11,13 @@ type Args = { timestamps: boolean } -export type SortArgs = { - direction: SortDirection - property: string -}[] - -export type SortDirection = 'asc' | 'desc' - export const buildSortParam = ({ config, fields, locale, sort, timestamps, -}: Args): PaginateOptions['sort'] => { +}: Args): Record => { if (!sort) { if (timestamps) { sort = '-createdAt' @@ -37,15 +30,15 @@ export const buildSortParam = ({ sort = [sort] } - const sorting = sort.reduce((acc, item) => { + const sorting = sort.reduce>((acc, item) => { let sortProperty: string - let sortDirection: SortDirection + let sortDirection: -1 | 1 if (item.indexOf('-') === 0) { sortProperty = item.substring(1) - sortDirection = 'desc' + sortDirection = -1 } else { sortProperty = item - sortDirection = 'asc' + sortDirection = 1 } if (sortProperty === 'id') { acc['_id'] = sortDirection diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts index f7aec4bbd4f..59a7d3d99d2 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts @@ -1,6 +1,6 @@ import type { FlattenedField, SanitizedConfig } from 'payload' -import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared' +import { fieldAffectsData } from 'payload/shared' type Args = { config: SanitizedConfig @@ -33,7 +33,7 @@ export const getLocalizedSortProperty = ({ (field) => fieldAffectsData(field) && field.name === firstSegment, ) - if (matchedField && !fieldIsPresentationalOnly(matchedField)) { + if (matchedField) { let nextFields: FlattenedField[] const remainingSegments = [...segments] let localizedSegment = matchedField.name diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index 34592123256..7977c2f9c1d 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -36,6 +36,22 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => { } } +const sanitizeCoordinates = (coordinates: unknown[]): unknown[] => { + const result: unknown[] = [] + + for (const value of coordinates) { + if (typeof value === 'string') { + result.push(Number(value)) + } else if (Array.isArray(value)) { + result.push(sanitizeCoordinates(value)) + } else { + result.push(value) + } + } + + return result +} + // returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships const getFieldFromSegments = ({ field, @@ -334,6 +350,14 @@ export const sanitizeQueryValue = ({ } if (operator === 'within' || operator === 'intersects') { + if ( + formattedValue && + typeof formattedValue === 'object' && + Array.isArray(formattedValue.coordinates) + ) { + formattedValue.coordinates = sanitizeCoordinates(formattedValue.coordinates) + } + formattedValue = { $geometry: formattedValue, } diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index 8b06e67ed33..f43f336a3e4 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -1,15 +1,16 @@ -import type { PaginateOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { PayloadRequest, QueryDrafts } from 'payload' import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { withSession } from './withSession.js' +import { findMany } from './utilities/findMany.js' +import { transform } from './utilities/transform.js' export const queryDrafts: QueryDrafts = async function queryDrafts( this: MongooseAdapter, @@ -28,7 +29,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( ) { const VersionModel = this.versions[collection] const collectionConfig = this.payload.collections[collection].config - const options = await withSession(this, req) + const session = await getSession(this, req) let hasNearConstraint let sort @@ -56,92 +57,60 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( where: combinedWhere, }) + const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) const projection = buildProjectionFromSelect({ adapter: this, - fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), + fields: versionFields, select, }) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0 - const paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, - options, - page, - pagination, - projection, - sort, - useEstimatedCount, - } - - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } - if ( - !useEstimatedCount && - Object.keys(versionQuery).length === 0 && - this.disableIndexHints !== true - ) { - // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding - // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, - // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses - // the correct indexed field - paginationOptions.useCustomCountFn = () => { - return Promise.resolve( - VersionModel.countDocuments(versionQuery, { - hint: { _id: 1 }, - }), - ) - } - } - - if (limit > 0) { - paginationOptions.limit = limit - // limit must also be set here, it's ignored when pagination is false - paginationOptions.options.limit = limit - } - - let result + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined - const aggregate = await buildJoinAggregation({ + const joinAgreggation = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, locale, projection, - query: versionQuery, versions: true, }) - // build join aggregation - if (aggregate) { - result = await VersionModel.aggregatePaginate( - VersionModel.aggregate(aggregate), - paginationOptions, - ) - } else { - result = await VersionModel.paginate(versionQuery, paginationOptions) - } - - const docs = JSON.parse(JSON.stringify(result.docs)) + const result = await findMany({ + adapter: this, + collation, + collection: VersionModel.collection, + joinAgreggation, + limit, + page, + pagination, + projection, + query: versionQuery, + session, + sort, + useEstimatedCount, + }) - return { - ...result, - docs: docs.map((doc) => { - doc = { - _id: doc.parent, - id: doc.parent, - ...doc.version, - } + transform({ + type: 'read', + adapter: this, + data: result.docs, + fields: versionFields, + }) - 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 ed090077cb8..5c5372f54fc 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -1,47 +1,39 @@ -import type { QueryOptions } from 'mongoose' import type { PayloadRequest, UpdateGlobal } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const updateGlobal: UpdateGlobal = async function updateGlobal( this: MongooseAdapter, { slug, data, options: optionsArgs = {}, req = {} as PayloadRequest, select }, ) { const Model = this.globals - const fields = this.payload.config.globals.find((global) => global.slug === slug).fields - - const options: QueryOptions = { - ...optionsArgs, - ...(await withSession(this, req)), - lean: true, - new: true, - projection: buildProjectionFromSelect({ - adapter: this, - fields: this.payload.config.globals.find((global) => global.slug === slug).flattenedFields, - select, - }), - } - - let result - - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data, + const fields = this.payload.config.globals.find((global) => global.slug === slug).flattenedFields + + const session = await getSession(this, req) + + transform({ type: 'write', adapter: this, data, fields }) + + const result: any = await Model.collection.findOneAndUpdate( + { globalType: slug }, + { $set: data }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), + returnDocument: 'after', + session, + }, + ) + + transform({ + type: 'read', + adapter: this, + data: result, fields, }) - result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options) - - result = JSON.parse(JSON.stringify(result)) - - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) - return result } diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index 73101f5967b..75987b1015a 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -1,5 +1,3 @@ -import type { QueryOptions } from 'mongoose' - import { buildVersionGlobalFields, type PayloadRequest, @@ -9,9 +7,9 @@ import { import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export async function updateGlobalVersion( this: MongooseAdapter, @@ -28,21 +26,11 @@ export async function updateGlobalVersion( ) { const VersionModel = this.versions[globalSlug] const whereToUse = where || { id: { equals: id } } - - const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug) - const fields = buildVersionGlobalFields(this.payload.config, currentGlobal) - - const options: QueryOptions = { - ...optionsArgs, - ...(await withSession(this, req)), - lean: true, - new: true, - projection: buildProjectionFromSelect({ - adapter: this, - fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true), - select, - }), - } + const fields = buildVersionGlobalFields( + this.payload.config, + this.payload.config.globals.find((global) => global.slug === globalSlug), + true, + ) const query = await VersionModel.buildQuery({ locale, @@ -50,22 +38,34 @@ export async function updateGlobalVersion( where: whereToUse, }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + type: 'write', + adapter: this, data: versionData, fields, }) - const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) + const doc = await VersionModel.collection.findOneAndUpdate( + query, + { $set: versionData }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ + adapter: this, + fields, + select, + }), + returnDocument: 'after', + session: await getSession(this, req), + }, + ) - const result = JSON.parse(JSON.stringify(doc)) - - const verificationToken = doc._verificationToken + transform({ + type: 'read', + adapter: this, + data: doc, + fields, + }) - // 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 f70cb149f3b..cd87d187669 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -1,13 +1,11 @@ -import type { QueryOptions } from 'mongoose' import type { PayloadRequest, UpdateOne } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { handleError } from './utilities/handleError.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const updateOne: UpdateOne = async function updateOne( this: MongooseAdapter, @@ -24,18 +22,7 @@ export const updateOne: UpdateOne = async function updateOne( ) { const where = id ? { id: { equals: id } } : whereArg const Model = this.collections[collection] - const fields = this.payload.collections[collection].config.fields - const options: QueryOptions = { - ...optionsArgs, - ...(await withSession(this, req)), - lean: true, - new: true, - projection: buildProjectionFromSelect({ - adapter: this, - fields: this.payload.collections[collection].config.flattenedFields, - select, - }), - } + const fields = this.payload.collections[collection].config.flattenedFields const query = await Model.buildQuery({ locale, @@ -43,23 +30,32 @@ export const updateOne: UpdateOne = async function updateOne( where, }) - let result - - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + type: 'write', + adapter: this, data, fields, }) try { - result = await Model.findOneAndUpdate(query, sanitizedData, options) + const result = await Model.collection.findOneAndUpdate( + query, + { $set: data }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), + returnDocument: 'after', + session: await getSession(this, req), + }, + ) + transform({ + type: 'read', + adapter: this, + data: result, + fields, + }) + return result } catch (error) { handleError({ collection, error, req }) } - - result = JSON.parse(JSON.stringify(result)) - result.id = result._id - result = sanitizeInternalFields(result) - - return result } diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 7f814b4ea5b..60bb05f4944 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -1,12 +1,10 @@ -import type { QueryOptions } from 'mongoose' - import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload' import type { MongooseAdapter } from './index.js' +import { getSession } from './getSession.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' -import { withSession } from './withSession.js' +import { transform } from './utilities/transform.js' export const updateVersion: UpdateVersion = async function updateVersion( this: MongooseAdapter, @@ -26,46 +24,47 @@ export const updateVersion: UpdateVersion = async function updateVersion( const fields = buildVersionCollectionFields( this.payload.config, this.payload.collections[collection].config, + true, ) - const options: QueryOptions = { - ...optionsArgs, - ...(await withSession(this, req)), - lean: true, - new: true, - projection: buildProjectionFromSelect({ - adapter: this, - fields: buildVersionCollectionFields( - this.payload.config, - this.payload.collections[collection].config, - true, - ), - select, - }), - } - const query = await VersionModel.buildQuery({ locale, payload: this.payload, where: whereToUse, }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + type: 'write', + adapter: this, data: versionData, fields, }) - const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) - - const result = JSON.parse(JSON.stringify(doc)) + const doc = await VersionModel.collection.findOneAndUpdate( + query, + { $set: versionData }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionCollectionFields( + this.payload.config, + this.payload.collections[collection].config, + true, + ), + select, + }), + returnDocument: 'after', + session: await getSession(this, req), + }, + ) - const verificationToken = doc._verificationToken + transform({ + type: 'read', + adapter: this, + data: doc, + fields, + }) - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return doc as any } diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index 41d5ad8f5cc..b016cac3508 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -10,12 +10,8 @@ type BuildJoinAggregationArgs = { collection: CollectionSlug collectionConfig: SanitizedCollectionConfig joins: JoinQuery - // the number of docs to get at the top collection level - limit?: number locale: string projection?: Record - // the where clause for the top collection - query?: Where /** whether the query is from drafts */ versions?: boolean } @@ -25,10 +21,8 @@ export const buildJoinAggregation = async ({ collection, collectionConfig, joins, - limit, locale, projection, - query, versions, }: BuildJoinAggregationArgs): Promise => { if (Object.keys(collectionConfig.joins).length === 0 || joins === false) { @@ -36,23 +30,7 @@ export const buildJoinAggregation = async ({ } const joinConfig = adapter.payload.collections[collection].config.joins - const aggregate: PipelineStage[] = [ - { - $sort: { createdAt: -1 }, - }, - ] - - if (query) { - aggregate.push({ - $match: query, - }) - } - - if (limit) { - aggregate.push({ - $limit: limit, - }) - } + const aggregate: PipelineStage[] = [] for (const slug of Object.keys(joinConfig)) { for (const join of joinConfig[slug]) { @@ -72,15 +50,13 @@ export const buildJoinAggregation = async ({ where: whereJoin, } = joins?.[join.joinPath] || {} - const sort = buildSortParam({ + const $sort = buildSortParam({ config: adapter.payload.config, fields: adapter.payload.collections[slug].config.flattenedFields, locale, sort: sortJoin, timestamps: true, }) - const sortProperty = Object.keys(sort)[0] - const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1 const $match = await joinModel.buildQuery({ locale, @@ -91,7 +67,7 @@ export const buildJoinAggregation = async ({ const pipeline: Exclude[] = [ { $match }, { - $sort: { [sortProperty]: sortDirection }, + $sort, }, ] @@ -184,8 +160,8 @@ export const buildJoinAggregation = async ({ } } - if (projection) { - aggregate.push({ $project: projection }) + if (!aggregate.length) { + return } return aggregate diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts index 51215d7c6fa..e506c7da829 100644 --- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -1,4 +1,4 @@ -import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' +import type { Field, FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared' @@ -29,6 +29,11 @@ const addFieldToProjection = ({ } } +const blockTypeField: Field = { + name: 'blockType', + type: 'text', +} + const traverseFields = ({ adapter, databaseSchemaPath = '', @@ -128,6 +133,14 @@ const traverseFields = ({ (selectMode === 'include' && blocksSelect[block.slug] === true) || (selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined') ) { + addFieldToProjection({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + field: blockTypeField, + projection, + withinLocalizedField: fieldWithinLocalizedField, + }) + traverseFields({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, @@ -153,7 +166,13 @@ const traverseFields = ({ if (blockSelectMode === 'include') { blocksSelect[block.slug]['id'] = true - blocksSelect[block.slug]['blockType'] = true + addFieldToProjection({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + field: blockTypeField, + projection, + withinLocalizedField: fieldWithinLocalizedField, + }) } traverseFields({ diff --git a/packages/db-mongodb/src/utilities/findMany.ts b/packages/db-mongodb/src/utilities/findMany.ts new file mode 100644 index 00000000000..5bc9cb984ac --- /dev/null +++ b/packages/db-mongodb/src/utilities/findMany.ts @@ -0,0 +1,131 @@ +import type { ClientSession, CollationOptions, Collection, Document } from 'mongodb' +import type { PipelineStage } from 'mongoose' +import type { PaginatedDocs } from 'payload' + +import type { MongooseAdapter } from '../index.js' + +export const findMany = async ({ + adapter, + collation, + collection, + joinAgreggation, + limit, + page = 1, + pagination, + projection, + query = {}, + session, + skip, + sort, + useEstimatedCount, +}: { + adapter: MongooseAdapter + collation?: CollationOptions + collection: Collection + joinAgreggation?: PipelineStage[] + limit?: number + page?: number + pagination?: boolean + projection?: Record + query?: Record + session?: ClientSession + skip?: number + sort?: Record + useEstimatedCount?: boolean +}): Promise => { + if (!skip) { + skip = (page - 1) * (limit ?? 0) + } + + let docsPromise: Promise + let countPromise: Promise = Promise.resolve(null) + + if (joinAgreggation) { + const cursor = collection.aggregate( + [ + { + $match: query, + }, + ], + { collation, session }, + ) + + if (sort) { + cursor.sort(sort) + } + + if (skip) { + cursor.skip(skip) + } + + if (limit) { + cursor.limit(limit) + } + + for (const stage of joinAgreggation) { + cursor.addStage(stage) + } + + if (projection) { + cursor.project(projection) + } + + docsPromise = cursor.toArray() + } else { + docsPromise = collection + .find(query, { + collation, + limit, + projection, + session, + skip, + sort, + }) + .toArray() + } + + if (pagination !== false && limit) { + if (useEstimatedCount) { + countPromise = collection.estimatedDocumentCount({ collation, session }) + } else { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + + const hint = + Object.keys(query).length === 0 && adapter.disableIndexHints !== true + ? { _id: 1 } + : undefined + + countPromise = collection.countDocuments(query, { collation, hint, session }) + } + } + + const [docs, countResult] = await Promise.all([docsPromise, countPromise]) + + const count = countResult === null ? docs.length : countResult + + const totalPages = + pagination !== false && typeof limit === 'number' && limit !== 0 ? Math.ceil(count / limit) : 1 + + const hasPrevPage = pagination !== false && page > 1 + const hasNextPage = pagination !== false && totalPages > page + const pagingCounter = + pagination !== false && typeof limit === 'number' ? (page - 1) * limit + 1 : 1 + + const result = { + docs, + hasNextPage, + hasPrevPage, + limit, + nextPage: hasNextPage ? page + 1 : null, + page, + pagingCounter, + prevPage: hasPrevPage ? page - 1 : null, + totalDocs: count, + totalPages, + } as PaginatedDocs> + + return result +} diff --git a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts deleted file mode 100644 index 14ab00da676..00000000000 --- 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 bdecaa4066d..00000000000 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload' - -import { Types } from 'mongoose' -import { APIError, traverseFields } from 'payload' -import { fieldAffectsData } from 'payload/shared' - -type Args = { - config: SanitizedConfig - data: Record - fields: Field[] -} - -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, -}: 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 && field.localized) { - 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, fields, fillEmpty: false, 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 93% rename from packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts rename to packages/db-mongodb/src/utilities/transform.spec.ts index af95ea2a3c1..f68b691e2f1 100644 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts +++ b/packages/db-mongodb/src/utilities/transform.spec.ts @@ -1,8 +1,9 @@ -import type { Field, SanitizedConfig } from 'payload' +import { flattenAllFields, type Field, type SanitizedConfig } from 'payload' import { Types } from 'mongoose' -import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js' +import { transform } from './transform.js' +import { MongooseAdapter } from '..' const flattenRelationshipValues = (obj: Record, prefix = ''): Record => { return Object.keys(obj).reduce( @@ -271,8 +272,8 @@ const relsData = { }, } -describe('sanitizeRelationshipIDs', () => { - it('should sanitize relationships', () => { +describe('transform', () => { + it('should sanitize relationships with transform write', () => { const data = { ...relsData, array: [ @@ -348,12 +349,19 @@ describe('sanitizeRelationshipIDs', () => { } const flattenValuesBefore = Object.values(flattenRelationshipValues(data)) - sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields }) + const mockAdapter = { payload: { config } } as MongooseAdapter + + const fields = flattenAllFields({ fields: config.collections[0].fields }) + + transform({ type: 'write', adapter: mockAdapter, data, fields }) + const flattenValuesAfter = Object.values(flattenRelationshipValues(data)) flattenValuesAfter.forEach((value, i) => { expect(value).toBeInstanceOf(Types.ObjectId) expect(flattenValuesBefore[i]).toBe(value.toHexString()) }) + + transform({ type: 'read', adapter: mockAdapter, data, fields }) }) }) diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts new file mode 100644 index 00000000000..23f3737799e --- /dev/null +++ b/packages/db-mongodb/src/utilities/transform.ts @@ -0,0 +1,282 @@ +import type { + CollectionConfig, + FlattenedField, + JoinField, + RelationshipField, + SanitizedConfig, + TraverseFlattenedFieldsCallback, + UploadField, +} from 'payload' + +import { Types } from 'mongoose' +import { traverseFields } from 'payload' +import { fieldAffectsData, fieldIsVirtual } from 'payload/shared' + +import type { MongooseAdapter } from '../index.js' + +type Args = { + adapter: MongooseAdapter + data: Record | Record[] + fields: FlattenedField[] + globalSlug?: string + insert?: boolean + type: 'read' | 'write' +} + +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 = ({ + type, + relatedCollection, + value, +}: { + relatedCollection: CollectionConfig + type: 'read' | 'write' + value: unknown +}) => { + const customIDField = relatedCollection.fields.find( + (field) => fieldAffectsData(field) && field.name === 'id', + ) + + if (type === '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 { + return value + } + } + + return value +} + +const sanitizeRelationship = ({ + type, + config, + field, + locale, + ref, + value, +}: { + config: SanitizedConfig + field: JoinField | RelationshipField | UploadField + locale?: string + ref: Record + type: 'read' | 'write' + value?: unknown +}) => { + if (field.type === 'join') { + if (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() + } + } + } + + 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: convertValue({ + type, + relatedCollection: relatedCollectionForSingleValue, + value: val.value, + }), + } + } + } + + if (relatedCollection) { + return convertValue({ + type, + relatedCollection, + 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: convertValue({ type, relatedCollection, value: value.value }), + } + } + } + // Handle has one + else if (relatedCollection) { + result = convertValue({ + type, + relatedCollection, + value, + }) + } + + if (locale) { + ref[locale] = result + } else { + ref[field.name] = result + } +} + +export const transform = ({ type, adapter, data, fields, globalSlug, insert }: Args) => { + if (Array.isArray(data)) { + for (let i = 0; i < data.length; i++) { + transform({ type, adapter, data: data[i], fields }) + } + return + } + + const { + payload: { config }, + } = adapter + + if (type === 'read') { + delete data['__v'] + data.id = data._id + delete data['_id'] + + if (data.id instanceof Types.ObjectId) { + data.id = data.id.toHexString() + } + } + + if (type === 'write') { + if (insert && !data.createdAt) { + data.createdAt = new Date() + } + + if (!data.updatedAt || insert) { + data.updatedAt = new Date() + } + + if (globalSlug) { + data.globalType = globalSlug + } + } + + const sanitize: TraverseFlattenedFieldsCallback = ({ field, ref }) => { + if (!ref || typeof ref !== 'object') { + return + } + + if (type === 'write') { + if ( + typeof ref[field.name] === 'undefined' && + typeof field.defaultValue !== 'undefined' && + typeof field.defaultValue !== 'function' + ) { + if (field.type === 'point') { + ref[field.name] = { + type: 'Point', + coordinates: field.defaultValue, + } + } else { + ref[field.name] = field.defaultValue + } + } + + if (fieldIsVirtual(field)) { + delete ref[field.name] + return + } + } + + if (field.type === 'date') { + if (type === 'read') { + const value = ref[field.name] + if (value && value instanceof Date) { + ref[field.name] = value.toISOString() + } + } + } + + if ( + field.type === 'relationship' || + field.type === 'upload' || + (type === 'read' && field.type === 'join') + ) { + if (!ref[field.name]) { + return + } + + // handle localized relationships + if (config.localization && field.localized) { + 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({ + type, + config, + field, + locale: code, + ref: fieldRef, + value, + }) + } + } + } else { + // handle non-localized relationships + sanitizeRelationship({ + type, + config, + field, + locale: undefined, + ref: ref as Record, + value: ref[field.name], + }) + } + } + } + + traverseFields({ callback: sanitize, fillEmpty: false, flattenedFields: fields, ref: data }) +} diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 543560e499b..9699b9a24f6 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1353,7 +1353,10 @@ export { killTransaction } from './utilities/killTransaction.js' export { mapAsync } from './utilities/mapAsync.js' export { sanitizeFallbackLocale } from './utilities/sanitizeFallbackLocale.js' export { traverseFields } from './utilities/traverseFields.js' -export type { TraverseFieldsCallback } from './utilities/traverseFields.js' +export type { + TraverseFieldsCallback, + TraverseFlattenedFieldsCallback, +} from './utilities/traverseFields.js' export { buildVersionCollectionFields } from './versions/buildCollectionFields.js' export { buildVersionGlobalFields } from './versions/buildGlobalFields.js' export { versionDefaults } from './versions/defaults.js' diff --git a/packages/payload/src/utilities/traverseFields.ts b/packages/payload/src/utilities/traverseFields.ts index 822a94adc3b..09775185278 100644 --- a/packages/payload/src/utilities/traverseFields.ts +++ b/packages/payload/src/utilities/traverseFields.ts @@ -1,4 +1,12 @@ -import type { ArrayField, BlocksField, Field, TabAsField } from '../fields/config/types.js' +import type { + ArrayField, + BlocksField, + Field, + FlattenedArrayField, + FlattenedBlock, + FlattenedField, + TabAsField, +} from '../fields/config/types.js' import { fieldHasSubFields } from '../fields/config/types.js' @@ -6,29 +14,34 @@ const traverseArrayOrBlocksField = ({ callback, data, field, + fillEmpty, parentRef, }: { callback: TraverseFieldsCallback data: Record[] field: ArrayField | BlocksField + fillEmpty?: boolean parentRef?: unknown }) => { for (const ref of data) { let fields: Field[] + let flattenedFields: FlattenedField[] if (field.type === 'blocks' && typeof ref?.blockType === 'string') { - const block = field.blocks.find((block) => block.slug === ref.blockType) + const block = field.blocks.find((block) => block.slug === ref.blockType) as FlattenedBlock fields = block?.fields + flattenedFields = block?.flattenedFields } else if (field.type === 'array') { fields = field.fields + flattenedFields = (field as FlattenedArrayField)?.flattenedFields } - if (fields) { - traverseFields({ callback, fields, parentRef, ref }) + if (flattenedFields || fields) { + traverseFields({ callback, fields, fillEmpty, flattenedFields, parentRef, ref }) } } } -export type TraverseFieldsCallback = (args: { +type TraverseFieldsCallbackArgs = { /** * The current field */ @@ -45,13 +58,45 @@ export type TraverseFieldsCallback = (args: { * The current reference object */ ref?: Record | unknown +} + +export type TraverseFieldsCallback = (args: TraverseFieldsCallbackArgs) => boolean | void + +export type TraverseFlattenedFieldsCallback = (args: { + /** + * The current field + */ + field: FlattenedField + /** + * Function that when called will skip the current field and continue to the next + */ + next?: () => void + /** + * The parent reference object + */ + parentRef?: Record | unknown + /** + * The current reference object + */ + ref?: Record | unknown }) => boolean | void +type TraverseFlattenedFieldsArgs = { + callback: TraverseFlattenedFieldsCallback + fields?: Field[] + /** fill empty properties to use this without data */ + fillEmpty?: boolean + flattenedFields: FlattenedField[] + parentRef?: Record | unknown + ref?: Record | unknown +} + type TraverseFieldsArgs = { callback: TraverseFieldsCallback - fields: (Field | TabAsField)[] + fields: (Field | FlattenedField | TabAsField)[] /** fill empty properties to use this without data */ fillEmpty?: boolean + flattenedFields?: FlattenedField[] parentRef?: Record | unknown ref?: Record | unknown } @@ -68,10 +113,11 @@ export const traverseFields = ({ callback, fields, fillEmpty = true, + flattenedFields, parentRef = {}, ref = {}, -}: TraverseFieldsArgs): void => { - fields.some((field) => { +}: TraverseFieldsArgs | TraverseFlattenedFieldsArgs): void => { + ;(flattenedFields ?? fields).some((field) => { let skip = false const next = () => { skip = true @@ -81,7 +127,16 @@ export const traverseFields = ({ return } - if (callback && callback({ field, next, parentRef, ref })) { + if ( + callback && + callback({ + // @ts-expect-error compatibillity Field | FlattenedField + field, + next, + parentRef, + ref, + }) + ) { return true } @@ -113,6 +168,7 @@ export const traverseFields = ({ traverseFields({ callback, fields: tab.fields, + fillEmpty, parentRef: currentParentRef, ref: tabRef[key], }) @@ -125,6 +181,7 @@ export const traverseFields = ({ if ( callback && callback({ + // @ts-expect-error compatibillity Field | FlattenedField field: { ...tab, type: 'tab' }, next, parentRef: currentParentRef, @@ -137,6 +194,7 @@ export const traverseFields = ({ traverseFields({ callback, fields: tab.fields, + fillEmpty, parentRef: currentParentRef, ref: tabRef, }) @@ -145,12 +203,15 @@ export const traverseFields = ({ return } - if (field.type !== 'tab' && (fieldHasSubFields(field) || field.type === 'blocks')) { + if ( + (flattenedFields || field.type !== 'tab') && + (fieldHasSubFields(field as Field) || field.type === 'tab' || field.type === 'blocks') + ) { if ('name' in field && field.name) { currentParentRef = currentRef if (!ref[field.name]) { if (fillEmpty) { - if (field.type === 'group') { + if (field.type === 'group' || field.type === 'tab') { ref[field.name] = {} } else if (field.type === 'array' || field.type === 'blocks') { if (field.localized) { @@ -167,7 +228,7 @@ export const traverseFields = ({ } if ( - field.type === 'group' && + (field.type === 'group' || field.type === 'tab') && field.localized && currentRef && typeof currentRef === 'object' @@ -177,9 +238,11 @@ export const traverseFields = ({ traverseFields({ callback, fields: field.fields, + fillEmpty, + flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined, parentRef: currentParentRef, ref: currentRef[key], - }) + } as TraverseFieldsArgs) } } return @@ -205,6 +268,7 @@ export const traverseFields = ({ callback, data: localeData, field, + fillEmpty, parentRef: currentParentRef, }) } @@ -213,6 +277,7 @@ export const traverseFields = ({ callback, data: currentRef as Record[], field, + fillEmpty, parentRef: currentParentRef, }) } @@ -220,6 +285,8 @@ export const traverseFields = ({ traverseFields({ callback, fields: field.fields, + fillEmpty, + flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined, parentRef: currentParentRef, ref: currentRef, }) diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index 33ea1cf7371..d5490b4bb2e 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -151,7 +151,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 558309d8f79..00dd4bc65e3 100644 --- a/test/select/int.spec.ts +++ b/test/select/int.spec.ts @@ -1413,7 +1413,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 () => { @@ -1426,13 +1429,18 @@ 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 () => { const post = await createPost() - const res = await payload.update({ + const { + docs: [res], + } = await payload.update({ collection: 'posts', where: { id: { @@ -1443,7 +1451,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with deleteByID', async () => { @@ -1455,13 +1466,18 @@ 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 () => { const post = await createPost() - const res = await payload.delete({ + const { + docs: [res], + } = await payload.delete({ collection: 'posts', where: { id: { @@ -1471,7 +1487,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with duplicate', async () => { @@ -1483,7 +1502,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res)).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) }) From 5646ff394f1fb831cedc7cf5586d8b1d83bc78ff Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 28 Nov 2024 02:20:14 +0200 Subject: [PATCH 2/8] propagate transaction to buildQuery properly --- packages/db-mongodb/src/count.ts | 1 + .../db-mongodb/src/countGlobalVersions.ts | 1 + packages/db-mongodb/src/countVersions.ts | 1 + packages/db-mongodb/src/deleteMany.ts | 5 +++- packages/db-mongodb/src/deleteOne.ts | 1 + packages/db-mongodb/src/deleteVersions.ts | 5 +++- packages/db-mongodb/src/find.ts | 2 ++ packages/db-mongodb/src/findGlobal.ts | 3 +++ packages/db-mongodb/src/findGlobalVersions.ts | 4 +++- packages/db-mongodb/src/findOne.ts | 1 + packages/db-mongodb/src/findVersions.ts | 1 + .../src/queries/buildAndOrConditions.ts | 4 ++++ packages/db-mongodb/src/queries/buildQuery.ts | 12 ++++------ .../src/queries/buildSearchParams.ts | 24 +++++++++++-------- .../db-mongodb/src/queries/parseParams.ts | 4 ++++ packages/db-mongodb/src/queryDrafts.ts | 1 + .../db-mongodb/src/updateGlobalVersion.ts | 5 +++- packages/db-mongodb/src/updateOne.ts | 6 ++++- packages/db-mongodb/src/updateVersion.ts | 5 +++- .../src/utilities/buildJoinAggregation.ts | 4 ++++ 20 files changed, 67 insertions(+), 23 deletions(-) diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index 7e83118a192..c8e9dd2df13 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -24,6 +24,7 @@ export const count: Count = async function count( const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts index 5dfb89590b6..5779c345afa 100644 --- a/packages/db-mongodb/src/countGlobalVersions.ts +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -24,6 +24,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts index 522a44d043a..2027d251ee1 100644 --- a/packages/db-mongodb/src/countVersions.ts +++ b/packages/db-mongodb/src/countVersions.ts @@ -24,6 +24,7 @@ export const countVersions: CountVersions = async function countVersions( const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) diff --git a/packages/db-mongodb/src/deleteMany.ts b/packages/db-mongodb/src/deleteMany.ts index f6d60ed339c..fc6bec79d29 100644 --- a/packages/db-mongodb/src/deleteMany.ts +++ b/packages/db-mongodb/src/deleteMany.ts @@ -10,12 +10,15 @@ export const deleteMany: DeleteMany = async function deleteMany( ) { const Model = this.collections[collection] + const session = await getSession(this, req) + const query = await Model.buildQuery({ payload: this.payload, + session, where, }) await Model.collection.deleteMany(query, { - session: await getSession(this, req), + session, }) } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index cef8daffec8..aab3cdcdf49 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -15,6 +15,7 @@ export const deleteOne: DeleteOne = async function deleteOne( const query = await Model.buildQuery({ payload: this.payload, + session, where, }) diff --git a/packages/db-mongodb/src/deleteVersions.ts b/packages/db-mongodb/src/deleteVersions.ts index 90091710558..3f851bce801 100644 --- a/packages/db-mongodb/src/deleteVersions.ts +++ b/packages/db-mongodb/src/deleteVersions.ts @@ -10,13 +10,16 @@ export const deleteVersions: DeleteVersions = async function deleteVersions( ) { const VersionsModel = this.versions[collection] + const session = await getSession(this, req) + const query = await VersionsModel.buildQuery({ locale, payload: this.payload, + session, where, }) await VersionsModel.collection.deleteMany(query, { - session: await getSession(this, req), + session, }) } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 71c526db915..7362c5a7e0c 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -54,6 +54,7 @@ export const find: Find = async function find( const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) @@ -79,6 +80,7 @@ export const find: Find = async function find( collectionConfig, joins, locale, + session, }) const result = await findMany({ diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index d4220fff89e..0be079d456b 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -14,10 +14,13 @@ export const findGlobal: FindGlobal = async function findGlobal( ) { const Model = this.globals + const session = await getSession(this, req) + const query = await Model.buildQuery({ globalSlug: slug, locale, payload: this.payload, + session, where: combineQueries({ globalType: { equals: slug } }, where), }) diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 8ca518b713a..19e4c571d8b 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -51,14 +51,16 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV }) } + const session = await getSession(this, req) + const query = await Model.buildQuery({ globalSlug: global, locale, payload: this.payload, + session, where, }) - const session = await getSession(this, req) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0 diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index ef0b377a65f..7b78edef8e4 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -19,6 +19,7 @@ export const findOne: FindOne = async function findOne( const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index c65fcd71951..bbe9205a8b9 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -52,6 +52,7 @@ export const findVersions: FindVersions = async function findVersions( const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) diff --git a/packages/db-mongodb/src/queries/buildAndOrConditions.ts b/packages/db-mongodb/src/queries/buildAndOrConditions.ts index 9ac05296796..e513e118619 100644 --- a/packages/db-mongodb/src/queries/buildAndOrConditions.ts +++ b/packages/db-mongodb/src/queries/buildAndOrConditions.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { FlattenedField, Payload, Where } from 'payload' import { parseParams } from './parseParams.js' @@ -8,6 +9,7 @@ export async function buildAndOrConditions({ globalSlug, locale, payload, + session, where, }: { collectionSlug?: string @@ -15,6 +17,7 @@ export async function buildAndOrConditions({ globalSlug?: string locale?: string payload: Payload + session?: ClientSession where: Where[] }): Promise[]> { const completedConditions = [] @@ -30,6 +33,7 @@ export async function buildAndOrConditions({ globalSlug, locale, payload, + session, where: condition, }) if (Object.keys(result).length > 0) { diff --git a/packages/db-mongodb/src/queries/buildQuery.ts b/packages/db-mongodb/src/queries/buildQuery.ts index 64cde6af7aa..7dc3d5fecca 100644 --- a/packages/db-mongodb/src/queries/buildQuery.ts +++ b/packages/db-mongodb/src/queries/buildQuery.ts @@ -1,7 +1,6 @@ +import type { ClientSession } from 'mongodb' import type { FlattenedField, Payload, Where } from 'payload' -import { QueryError } from 'payload' - import { parseParams } from './parseParams.js' type GetBuildQueryPluginArgs = { @@ -13,6 +12,7 @@ export type BuildQueryArgs = { globalSlug?: string locale?: string payload: Payload + session?: ClientSession where: Where } @@ -28,6 +28,7 @@ export const getBuildQueryPlugin = ({ globalSlug, locale, payload, + session, where, }: BuildQueryArgs): Promise> { let fields = versionsFields @@ -41,20 +42,17 @@ export const getBuildQueryPlugin = ({ fields = collectionConfig.flattenedFields } } - const errors = [] + const result = await parseParams({ collectionSlug, fields, globalSlug, locale, payload, + session, where, }) - if (errors.length > 0) { - throw new QueryError(errors) - } - return result } modifiedSchema.statics.buildQuery = buildQuery diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index c914b6d7d6c..5cca1572870 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -1,3 +1,4 @@ +import type { ClientSession, FindOptions } from 'mongodb' import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload' import { Types } from 'mongoose' @@ -15,9 +16,11 @@ type SearchParam = { value?: unknown } -const subQueryOptions = { - lean: true, +const subQueryOptions: FindOptions = { limit: 50, + projection: { + _id: true, + }, } /** @@ -31,6 +34,7 @@ export async function buildSearchParam({ locale, operator, payload, + session, val, }: { collectionSlug?: string @@ -40,6 +44,7 @@ export async function buildSearchParam({ locale?: string operator: string payload: Payload + session?: ClientSession val: unknown }): Promise { // Replace GraphQL nested field double underscore formatting @@ -133,17 +138,14 @@ export async function buildSearchParam({ }, }) - const result = await SubModel.find(subQuery, subQueryOptions) + const result = await SubModel.collection + .find(subQuery, { session, ...subQueryOptions }) + .toArray() const $in: unknown[] = [] result.forEach((doc) => { - const stringID = doc._id.toString() - $in.push(stringID) - - if (Types.ObjectId.isValid(stringID)) { - $in.push(doc._id) - } + $in.push(doc._id) }) if (pathsToQuery.length === 1) { @@ -161,7 +163,9 @@ export async function buildSearchParam({ } const subQuery = priorQueryResult.value - const result = await SubModel.find(subQuery, subQueryOptions) + const result = await SubModel.collection + .find(subQuery, { session, ...subQueryOptions }) + .toArray() const $in = result.map((doc) => doc._id) diff --git a/packages/db-mongodb/src/queries/parseParams.ts b/packages/db-mongodb/src/queries/parseParams.ts index 18dae08d209..e8b4cb0197a 100644 --- a/packages/db-mongodb/src/queries/parseParams.ts +++ b/packages/db-mongodb/src/queries/parseParams.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { FilterQuery } from 'mongoose' import type { FlattenedField, Operator, Payload, Where } from 'payload' @@ -13,6 +14,7 @@ export async function parseParams({ globalSlug, locale, payload, + session, where, }: { collectionSlug?: string @@ -20,6 +22,7 @@ export async function parseParams({ globalSlug?: string locale: string payload: Payload + session?: ClientSession where: Where }): Promise> { let result = {} as FilterQuery @@ -62,6 +65,7 @@ export async function parseParams({ locale, operator, payload, + session, val: pathOperators[operator], }) diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index f43f336a3e4..bda89e6df3b 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -54,6 +54,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( const versionQuery = await VersionModel.buildQuery({ locale, payload: this.payload, + session, where: combinedWhere, }) diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index 75987b1015a..68b7689cdea 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -32,9 +32,12 @@ export async function updateGlobalVersion( true, ) + const session = await getSession(this, req) + const query = await VersionModel.buildQuery({ locale, payload: this.payload, + session, where: whereToUse, }) @@ -56,7 +59,7 @@ export async function updateGlobalVersion( select, }), returnDocument: 'after', - session: await getSession(this, req), + session, }, ) diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index cd87d187669..aa0d0e7895d 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -24,9 +24,12 @@ export const updateOne: UpdateOne = async function updateOne( const Model = this.collections[collection] const fields = this.payload.collections[collection].config.flattenedFields + const session = await getSession(this, req) + const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) @@ -45,9 +48,10 @@ export const updateOne: UpdateOne = async function updateOne( ...optionsArgs, projection: buildProjectionFromSelect({ adapter: this, fields, select }), returnDocument: 'after', - session: await getSession(this, req), + session, }, ) + transform({ type: 'read', adapter: this, diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 60bb05f4944..33b2573e4b9 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -27,9 +27,12 @@ export const updateVersion: UpdateVersion = async function updateVersion( true, ) + const session = await getSession(this, req) + const query = await VersionModel.buildQuery({ locale, payload: this.payload, + session, where: whereToUse, }) @@ -55,7 +58,7 @@ export const updateVersion: UpdateVersion = async function updateVersion( select, }), returnDocument: 'after', - session: await getSession(this, req), + session, }, ) diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index b016cac3508..7bb5dd02d01 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { PipelineStage } from 'mongoose' import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload' @@ -12,6 +13,7 @@ type BuildJoinAggregationArgs = { joins: JoinQuery locale: string projection?: Record + session?: ClientSession /** whether the query is from drafts */ versions?: boolean } @@ -23,6 +25,7 @@ export const buildJoinAggregation = async ({ joins, locale, projection, + session, versions, }: BuildJoinAggregationArgs): Promise => { if (Object.keys(collectionConfig.joins).length === 0 || joins === false) { @@ -61,6 +64,7 @@ export const buildJoinAggregation = async ({ const $match = await joinModel.buildQuery({ locale, payload: adapter.payload, + session, where: whereJoin, }) From a3582f3992410140c230a6a274e3e659f0c0a1aa Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 28 Nov 2024 02:24:17 +0200 Subject: [PATCH 3/8] as well in buildJoinAggregation --- packages/db-mongodb/src/findOne.ts | 1 + packages/db-mongodb/src/queryDrafts.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 7b78edef8e4..8b5d1943ec6 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -38,6 +38,7 @@ export const findOne: FindOne = async function findOne( joins, locale, projection, + session, }) let doc diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index bda89e6df3b..a858ee78be0 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -82,6 +82,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( joins, locale, projection, + session, versions: true, }) From 62458949430807ce5624a7ef96732e88f84a8954 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 28 Nov 2024 03:31:09 +0200 Subject: [PATCH 4/8] transform.ts change type to operation --- packages/db-mongodb/src/create.ts | 5 +- packages/db-mongodb/src/createGlobal.ts | 5 +- .../db-mongodb/src/createGlobalVersion.ts | 5 +- packages/db-mongodb/src/createVersion.ts | 5 +- packages/db-mongodb/src/deleteOne.ts | 2 +- packages/db-mongodb/src/find.ts | 2 +- packages/db-mongodb/src/findGlobal.ts | 2 +- packages/db-mongodb/src/findGlobalVersions.ts | 2 +- packages/db-mongodb/src/findOne.ts | 2 +- packages/db-mongodb/src/findVersions.ts | 2 +- .../migrateRelationshipsV2_V3.ts | 4 +- packages/db-mongodb/src/queryDrafts.ts | 2 +- packages/db-mongodb/src/updateGlobal.ts | 4 +- .../db-mongodb/src/updateGlobalVersion.ts | 4 +- packages/db-mongodb/src/updateOne.ts | 4 +- packages/db-mongodb/src/updateVersion.ts | 4 +- .../db-mongodb/src/utilities/transform.ts | 59 +++++++++++-------- 17 files changed, 59 insertions(+), 54 deletions(-) diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index fafbd813eba..05a55771379 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -20,11 +20,10 @@ export const create: Create = async function create( } transform({ - type: 'write', adapter: this, data, fields, - insert: true, + operation: 'create', }) try { @@ -32,10 +31,10 @@ export const create: Create = async function create( data._id = insertedId transform({ - type: 'read', adapter: this, data, fields, + operation: 'read', }) return data diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index e8f09abb0d4..20f5e84cde0 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -16,12 +16,11 @@ export const createGlobal: CreateGlobal = async function createGlobal( ).flattenedFields transform({ - type: 'write', adapter: this, data, fields, globalSlug: slug, - insert: true, + operation: 'create', }) const session = await getSession(this, req) @@ -30,10 +29,10 @@ export const createGlobal: CreateGlobal = async function createGlobal( ;(data as any)._id = insertedId transform({ - type: 'read', adapter: this, data, fields, + operation: 'read', }) return data diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index 2f9d32eeac4..c160fcd0697 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -40,11 +40,10 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo ) transform({ - type: 'write', adapter: this, data, fields, - insert: true, + operation: 'create', }) const { insertedId } = await VersionModel.collection.insertOne(data, { session }) @@ -75,10 +74,10 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo ) transform({ - type: 'read', adapter: this, data, fields, + operation: 'read', }) return data as any diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index fd3ad73a85d..b4ed46f9297 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -41,11 +41,10 @@ export const createVersion: CreateVersion = async function createVersion( ) transform({ - type: 'write', adapter: this, data, fields, - insert: true, + operation: 'create', }) const { insertedId } = await VersionModel.collection.insertOne(data, { session }) @@ -89,10 +88,10 @@ export const createVersion: CreateVersion = async function createVersion( ) transform({ - type: 'read', adapter: this, data, fields, + operation: 'read', }) return data diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index aab3cdcdf49..f39de77b0a1 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -31,10 +31,10 @@ export const deleteOne: DeleteOne = async function deleteOne( }) transform({ - type: 'read', adapter: this, data: doc, fields, + operation: 'read', }) return doc diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 7362c5a7e0c..aa4e099578d 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -98,7 +98,7 @@ export const find: Find = async function find( useEstimatedCount, }) - transform({ type: 'read', adapter: this, data: result.docs, fields }) + transform({ adapter: this, data: result.docs, fields, operation: 'read' }) return result } diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index 0be079d456b..6ecdc537fa7 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -39,7 +39,7 @@ export const findGlobal: FindGlobal = async function findGlobal( return null } - transform({ type: 'read', adapter: this, data: doc, fields }) + transform({ adapter: this, data: doc, fields, operation: 'read' }) return doc as any } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 19e4c571d8b..10aafbcbc16 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -89,10 +89,10 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV }) transform({ - type: 'read', adapter: this, data: result.docs, fields: versionFields, + operation: 'read', }) return result diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 8b5d1943ec6..e54e749fc40 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -66,10 +66,10 @@ export const findOne: FindOne = async function findOne( } transform({ - type: 'read', adapter: this, data: doc, fields, + operation: 'read', }) return doc diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index bbe9205a8b9..3d66e0257bb 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -89,10 +89,10 @@ export const findVersions: FindVersions = async function findVersions( }) transform({ - type: 'read', adapter: this, data: result.docs, fields: versionFields, + operation: 'read', }) return result diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts index 056abf42ff4..16167cdfd23 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts @@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - transform({ type: 'write', adapter, data: doc, fields }) + transform({ adapter, data: doc, fields, operation: 'update' }) } await Model.collection.bulkWrite( @@ -156,7 +156,7 @@ export async function migrateRelationshipsV2_V3({ // in case if the global doesn't exist in the database yet (not saved) if (doc) { - transform({ type: 'write', adapter: db, data: doc, fields: global.flattenedFields }) + transform({ adapter: db, data: doc, fields: global.flattenedFields, operation: 'update' }) await GlobalsModel.collection.updateOne( { diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index a858ee78be0..a87bef42667 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -102,10 +102,10 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( }) transform({ - type: 'read', adapter: this, data: result.docs, fields: versionFields, + operation: 'read', }) for (let i = 0; i < result.docs.length; i++) { diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 5c5372f54fc..cd36acb1bd6 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -15,7 +15,7 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( const session = await getSession(this, req) - transform({ type: 'write', adapter: this, data, fields }) + transform({ adapter: this, data, fields, operation: 'update' }) const result: any = await Model.collection.findOneAndUpdate( { globalType: slug }, @@ -29,10 +29,10 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal( ) transform({ - type: 'read', adapter: this, data: result, fields, + operation: 'read', }) return result diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index 68b7689cdea..d407aef5d6a 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -42,10 +42,10 @@ export async function updateGlobalVersion( }) transform({ - type: 'write', adapter: this, data: versionData, fields, + operation: 'update', }) const doc = await VersionModel.collection.findOneAndUpdate( @@ -64,10 +64,10 @@ export async function updateGlobalVersion( ) transform({ - type: 'read', adapter: this, data: doc, fields, + operation: 'read', }) return doc diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index aa0d0e7895d..59bd0a8af40 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -34,10 +34,10 @@ export const updateOne: UpdateOne = async function updateOne( }) transform({ - type: 'write', adapter: this, data, fields, + operation: 'update', }) try { @@ -53,10 +53,10 @@ export const updateOne: UpdateOne = async function updateOne( ) transform({ - type: 'read', adapter: this, data: result, fields, + operation: 'read', }) return result } catch (error) { diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 33b2573e4b9..32a3e38bf3f 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -37,10 +37,10 @@ export const updateVersion: UpdateVersion = async function updateVersion( }) transform({ - type: 'write', adapter: this, data: versionData, fields, + operation: 'update', }) const doc = await VersionModel.collection.findOneAndUpdate( @@ -63,10 +63,10 @@ export const updateVersion: UpdateVersion = async function updateVersion( ) transform({ - type: 'read', adapter: this, data: doc, fields, + operation: 'read', }) return doc as any diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts index 23f3737799e..109d53b363e 100644 --- a/packages/db-mongodb/src/utilities/transform.ts +++ b/packages/db-mongodb/src/utilities/transform.ts @@ -19,8 +19,7 @@ type Args = { data: Record | Record[] fields: FlattenedField[] globalSlug?: string - insert?: boolean - type: 'read' | 'write' + operation: 'create' | 'read' | 'update' } interface RelationObject { @@ -33,19 +32,19 @@ function isValidRelationObject(value: unknown): value is RelationObject { } const convertValue = ({ - type, + operation, relatedCollection, value, }: { + operation: Args['operation'] relatedCollection: CollectionConfig - type: 'read' | 'write' value: unknown }) => { const customIDField = relatedCollection.fields.find( (field) => fieldAffectsData(field) && field.name === 'id', ) - if (type === 'read') { + if (operation === 'read') { if (value instanceof Types.ObjectId) { return value.toHexString() } @@ -69,22 +68,28 @@ const convertValue = ({ } const sanitizeRelationship = ({ - type, config, field, locale, + operation, ref, value, }: { config: SanitizedConfig field: JoinField | RelationshipField | UploadField locale?: string + operation: Args['operation'] ref: Record - type: 'read' | 'write' value?: unknown }) => { if (field.type === 'join') { - if (value && typeof value === 'object' && 'docs' in value && Array.isArray(value.docs)) { + 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) { @@ -116,7 +121,7 @@ const sanitizeRelationship = ({ return { relationTo: val.relationTo, value: convertValue({ - type, + operation, relatedCollection: relatedCollectionForSingleValue, value: val.value, }), @@ -126,7 +131,7 @@ const sanitizeRelationship = ({ if (relatedCollection) { return convertValue({ - type, + operation, relatedCollection, value: val, }) @@ -142,14 +147,14 @@ const sanitizeRelationship = ({ if (relatedCollection) { result = { relationTo: value.relationTo, - value: convertValue({ type, relatedCollection, value: value.value }), + value: convertValue({ operation, relatedCollection, value: value.value }), } } } // Handle has one else if (relatedCollection) { result = convertValue({ - type, + operation, relatedCollection, value, }) @@ -162,10 +167,10 @@ const sanitizeRelationship = ({ } } -export const transform = ({ type, adapter, data, fields, globalSlug, insert }: Args) => { +export const transform = ({ adapter, data, fields, globalSlug, operation }: Args) => { if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { - transform({ type, adapter, data: data[i], fields }) + transform({ adapter, data: data[i], fields, operation }) } return } @@ -174,7 +179,7 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A payload: { config }, } = adapter - if (type === 'read') { + if (operation === 'read') { delete data['__v'] data.id = data._id delete data['_id'] @@ -184,14 +189,12 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A } } - if (type === 'write') { - if (insert && !data.createdAt) { + if (operation !== 'read') { + if (operation === 'create' && !data.createdAt) { data.createdAt = new Date() } - if (!data.updatedAt || insert) { - data.updatedAt = new Date() - } + data.updatedAt = new Date() if (globalSlug) { data.globalType = globalSlug @@ -203,7 +206,7 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A return } - if (type === 'write') { + if (operation !== 'read') { if ( typeof ref[field.name] === 'undefined' && typeof field.defaultValue !== 'undefined' && @@ -226,7 +229,7 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A } if (field.type === 'date') { - if (type === 'read') { + if (operation === 'read') { const value = ref[field.name] if (value && value instanceof Date) { ref[field.name] = value.toISOString() @@ -237,8 +240,13 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A if ( field.type === 'relationship' || field.type === 'upload' || - (type === 'read' && field.type === 'join') + (operation === 'read' && field.type === 'join') ) { + // sanitize passed undefined in objects to null + if (operation !== 'read' && field.name in ref && ref[field.name] === undefined) { + ref[field.name] = null + } + if (!ref[field.name]) { return } @@ -255,10 +263,10 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A const value = ref[field.name][code] if (value) { sanitizeRelationship({ - type, config, field, locale: code, + operation, ref: fieldRef, value, }) @@ -266,11 +274,12 @@ export const transform = ({ type, adapter, data, fields, globalSlug, insert }: A } } else { // handle non-localized relationships + sanitizeRelationship({ - type, config, field, locale: undefined, + operation, ref: ref as Record, value: ref[field.name], }) From c4b7ac0e19432c1633702b8304f5600dbcac797e Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 28 Nov 2024 16:33:04 +0200 Subject: [PATCH 5/8] faster `hasNearConstraint` --- packages/db-mongodb/src/count.ts | 10 ++----- .../db-mongodb/src/countGlobalVersions.ts | 10 ++----- packages/db-mongodb/src/countVersions.ts | 10 ++----- packages/db-mongodb/src/find.ts | 10 ++----- packages/db-mongodb/src/findGlobalVersions.ts | 10 +++---- packages/db-mongodb/src/findVersions.ts | 10 +++---- packages/db-mongodb/src/queryDrafts.ts | 10 +++---- .../db-mongodb/src/updateGlobalVersion.ts | 2 +- .../src/utilities/getHasNearConstraint.ts | 26 +++++++++++++++++++ 9 files changed, 44 insertions(+), 54 deletions(-) create mode 100644 packages/db-mongodb/src/utilities/getHasNearConstraint.ts diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index c8e9dd2df13..ef6557e0f6c 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -1,11 +1,10 @@ import type { CountOptions } from 'mongodb' import type { Count, PayloadRequest } from 'payload' -import { flattenWhereToOperators } from 'payload' - import type { MongooseAdapter } from './index.js' import { getSession } from './getSession.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' export const count: Count = async function count( this: MongooseAdapter, @@ -14,12 +13,7 @@ export const count: Count = async function count( const Model = this.collections[collection] const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint() const query = await Model.buildQuery({ locale, diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts index 5779c345afa..0e1efb554bc 100644 --- a/packages/db-mongodb/src/countGlobalVersions.ts +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -1,11 +1,10 @@ import type { CountOptions } from 'mongodb' import type { CountGlobalVersions, PayloadRequest } from 'payload' -import { flattenWhereToOperators } from 'payload' - import type { MongooseAdapter } from './index.js' import { getSession } from './getSession.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions( this: MongooseAdapter, @@ -14,12 +13,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob const Model = this.versions[global] const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) const query = await Model.buildQuery({ locale, diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts index 2027d251ee1..c45510058a9 100644 --- a/packages/db-mongodb/src/countVersions.ts +++ b/packages/db-mongodb/src/countVersions.ts @@ -1,11 +1,10 @@ import type { CountOptions } from 'mongodb' import type { CountVersions, PayloadRequest } from 'payload' -import { flattenWhereToOperators } from 'payload' - import type { MongooseAdapter } from './index.js' import { getSession } from './getSession.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' export const countVersions: CountVersions = async function countVersions( this: MongooseAdapter, @@ -14,12 +13,7 @@ export const countVersions: CountVersions = async function countVersions( const Model = this.versions[collection] const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) const query = await Model.buildQuery({ locale, diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index aa4e099578d..800fa657e54 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -1,8 +1,6 @@ import type { CollationOptions } from 'mongodb' import type { Find, PayloadRequest } from 'payload' -import { flattenWhereToOperators } from 'payload' - import type { MongooseAdapter } from './index.js' import { getSession } from './getSession.js' @@ -10,6 +8,7 @@ import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { findMany } from './utilities/findMany.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { transform } from './utilities/transform.js' export const find: Find = async function find( @@ -31,12 +30,7 @@ export const find: Find = async function find( const collectionConfig = this.payload.collections[collection].config const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) const fields = collectionConfig.flattenedFields diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 10aafbcbc16..473ae490abf 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -1,7 +1,7 @@ import type { CollationOptions } from 'mongodb' import type { FindGlobalVersions, PayloadRequest } from 'payload' -import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload' +import { buildVersionGlobalFields } from 'payload' import type { MongooseAdapter } from './index.js' @@ -9,6 +9,7 @@ import { getSession } from './getSession.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { findMany } from './utilities/findMany.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { transform } from './utilities/transform.js' export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( @@ -33,12 +34,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV true, ) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) let sort if (!hasNearConstraint) { diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 3d66e0257bb..5ad5b0b959f 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -1,7 +1,7 @@ import type { CollationOptions } from 'mongodb' import type { FindVersions, PayloadRequest } from 'payload' -import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload' +import { buildVersionCollectionFields } from 'payload' import type { MongooseAdapter } from './index.js' @@ -9,6 +9,7 @@ import { getSession } from './getSession.js' import { buildSortParam } from './queries/buildSortParam.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { findMany } from './utilities/findMany.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { transform } from './utilities/transform.js' export const findVersions: FindVersions = async function findVersions( @@ -31,12 +32,7 @@ export const findVersions: FindVersions = async function findVersions( const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) let sort if (!hasNearConstraint) { diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index a87bef42667..e073f088a6c 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -1,7 +1,7 @@ import type { CollationOptions } from 'mongodb' import type { PayloadRequest, QueryDrafts } from 'payload' -import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload' +import { buildVersionCollectionFields, combineQueries } from 'payload' import type { MongooseAdapter } from './index.js' @@ -10,6 +10,7 @@ import { buildSortParam } from './queries/buildSortParam.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { findMany } from './utilities/findMany.js' +import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { transform } from './utilities/transform.js' export const queryDrafts: QueryDrafts = async function queryDrafts( @@ -31,14 +32,9 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( const collectionConfig = this.payload.collections[collection].config const session = await getSession(this, req) - let hasNearConstraint + const hasNearConstraint = getHasNearConstraint(where) let sort - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } - if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index d407aef5d6a..503c535ead2 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -48,7 +48,7 @@ export async function updateGlobalVersion( operation: 'update', }) - const doc = await VersionModel.collection.findOneAndUpdate( + const doc: any = await VersionModel.collection.findOneAndUpdate( query, { $set: versionData }, { diff --git a/packages/db-mongodb/src/utilities/getHasNearConstraint.ts b/packages/db-mongodb/src/utilities/getHasNearConstraint.ts new file mode 100644 index 00000000000..987ab53899e --- /dev/null +++ b/packages/db-mongodb/src/utilities/getHasNearConstraint.ts @@ -0,0 +1,26 @@ +import type { Where } from 'payload' + +export const getHasNearConstraint = (where?: Where): boolean => { + if (!where) { + return false + } + + for (const key in where) { + if (key === 'near') { + return true + } + + if (['AND', 'OR'].includes(key.toUpperCase())) { + const value = where[key] + if (Array.isArray(value)) { + for (const where of value) { + if (getHasNearConstraint(where)) { + return true + } + } + } + } + } + + return false +} From 0e67273884d8967f77f660954fe487ceda51b5bd Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Fri, 29 Nov 2024 00:42:35 +0200 Subject: [PATCH 6/8] fix tests - store Date's in mongodb properly and hasNearConstraint, flaky fields-relationship int --- packages/db-mongodb/src/count.ts | 2 +- .../src/utilities/getHasNearConstraint.ts | 21 +++--- .../db-mongodb/src/utilities/transform.ts | 73 ++++++++++++++++--- test/fields-relationship/int.spec.ts | 10 ++- 4 files changed, 82 insertions(+), 24 deletions(-) diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index ef6557e0f6c..34e1d3960bd 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -13,7 +13,7 @@ export const count: Count = async function count( const Model = this.collections[collection] const session = await getSession(this, req) - const hasNearConstraint = getHasNearConstraint() + const hasNearConstraint = getHasNearConstraint(where) const query = await Model.buildQuery({ locale, diff --git a/packages/db-mongodb/src/utilities/getHasNearConstraint.ts b/packages/db-mongodb/src/utilities/getHasNearConstraint.ts index 987ab53899e..861129ec314 100644 --- a/packages/db-mongodb/src/utilities/getHasNearConstraint.ts +++ b/packages/db-mongodb/src/utilities/getHasNearConstraint.ts @@ -6,20 +6,21 @@ export const getHasNearConstraint = (where?: Where): boolean => { } for (const key in where) { - if (key === 'near') { - return true - } + const value = where[key] - if (['AND', 'OR'].includes(key.toUpperCase())) { - const value = where[key] - if (Array.isArray(value)) { - for (const where of value) { - if (getHasNearConstraint(where)) { - return true - } + if (Array.isArray(value) && ['AND', 'OR'].includes(key.toUpperCase())) { + for (const where of value) { + if (getHasNearConstraint(where)) { + return true } } } + + for (const key in value) { + if (key === 'near') { + return true + } + } } return false diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts index 109d53b363e..04ff650459a 100644 --- a/packages/db-mongodb/src/utilities/transform.ts +++ b/packages/db-mongodb/src/utilities/transform.ts @@ -1,5 +1,6 @@ import type { CollectionConfig, + DateField, FlattenedField, JoinField, RelationshipField, @@ -31,7 +32,7 @@ function isValidRelationObject(value: unknown): value is RelationObject { return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value } -const convertValue = ({ +const convertRelationshipValue = ({ operation, relatedCollection, value, @@ -120,7 +121,7 @@ const sanitizeRelationship = ({ if (relatedCollectionForSingleValue) { return { relationTo: val.relationTo, - value: convertValue({ + value: convertRelationshipValue({ operation, relatedCollection: relatedCollectionForSingleValue, value: val.value, @@ -130,7 +131,7 @@ const sanitizeRelationship = ({ } if (relatedCollection) { - return convertValue({ + return convertRelationshipValue({ operation, relatedCollection, value: val, @@ -147,13 +148,13 @@ const sanitizeRelationship = ({ if (relatedCollection) { result = { relationTo: value.relationTo, - value: convertValue({ operation, relatedCollection, value: value.value }), + value: convertRelationshipValue({ operation, relatedCollection, value: value.value }), } } } // Handle has one else if (relatedCollection) { - result = convertValue({ + result = convertRelationshipValue({ operation, relatedCollection, value, @@ -167,6 +168,44 @@ const sanitizeRelationship = ({ } } +/** + * When sending data to Payload - convert Date to string. + * Vice versa when sending data to MongoDB so dates are stored properly. + */ +const sanitizeDate = ({ + field, + locale, + operation, + ref, + value, +}: { + field: DateField + locale?: string + operation: Args['operation'] + ref: Record + value: unknown +}) => { + if (!value) { + return + } + + if (operation === 'read') { + if (value instanceof Date) { + value = value.toISOString() + } + } else { + if (typeof value === 'string') { + value = new Date(value) + } + } + + if (locale) { + ref[locale] = value + } else { + ref[field.name] = value + } +} + export const transform = ({ adapter, data, fields, globalSlug, operation }: Args) => { if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { @@ -229,11 +268,27 @@ export const transform = ({ adapter, data, fields, globalSlug, operation }: Args } if (field.type === 'date') { - if (operation === 'read') { - const value = ref[field.name] - if (value && value instanceof Date) { - ref[field.name] = value.toISOString() + if (config.localization && field.localized) { + const fieldRef = ref[field.name] + if (!fieldRef || typeof fieldRef !== 'object') { + return + } + + for (const locale of config.localization.localeCodes) { + sanitizeDate({ + field, + operation, + ref: fieldRef, + value: fieldRef[locale].locale, + }) } + } else { + sanitizeDate({ + field, + operation, + ref: ref as Record, + value: ref[field.name], + }) } } diff --git a/test/fields-relationship/int.spec.ts b/test/fields-relationship/int.spec.ts index 34eaeeee965..c7d4d21935e 100644 --- a/test/fields-relationship/int.spec.ts +++ b/test/fields-relationship/int.spec.ts @@ -53,10 +53,12 @@ describe('Relationship Fields', () => { collection: versionedRelationshipFieldSlug, data: { title: 'Version 1 Title', - relationshipField: { - value: relatedDoc.id, - relationTo: collection1Slug, - }, + relationshipField: [ + { + value: relatedDoc.id, + relationTo: collection1Slug, + }, + ], }, }) From baeb84ab9db0052fe8c7e9088fd6cb029e8c66f3 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Fri, 29 Nov 2024 01:24:18 +0200 Subject: [PATCH 7/8] fix: do not pass session to `estimatedDocumentCount`, relationship validation --- packages/db-mongodb/src/count.ts | 4 +- .../db-mongodb/src/countGlobalVersions.ts | 2 +- packages/db-mongodb/src/countVersions.ts | 4 +- .../migrateRelationshipsV2_V3.ts | 10 ++++- packages/db-mongodb/src/utilities/findMany.ts | 7 +--- .../db-mongodb/src/utilities/transform.ts | 40 ++++++++++++++++--- 6 files changed, 49 insertions(+), 18 deletions(-) diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index 34e1d3960bd..eeed0dbc463 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -27,11 +27,11 @@ export const count: Count = async function count( let result: number if (useEstimatedCount) { - result = await Model.collection.estimatedDocumentCount({ session }) + result = await Model.collection.estimatedDocumentCount() } else { const options: CountOptions = { session } - if (Object.keys(query).length === 0 && this.disableIndexHints !== true) { + if (this.disableIndexHints !== true) { // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts index 0e1efb554bc..4666194c610 100644 --- a/packages/db-mongodb/src/countGlobalVersions.ts +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -27,7 +27,7 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob let result: number if (useEstimatedCount) { - result = await Model.collection.estimatedDocumentCount({ session }) + result = await Model.collection.estimatedDocumentCount() } else { const options: CountOptions = { session } diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts index c45510058a9..145e2ac9fcf 100644 --- a/packages/db-mongodb/src/countVersions.ts +++ b/packages/db-mongodb/src/countVersions.ts @@ -27,11 +27,11 @@ export const countVersions: CountVersions = async function countVersions( let result: number if (useEstimatedCount) { - result = await Model.collection.estimatedDocumentCount({ session }) + result = await Model.collection.estimatedDocumentCount() } else { const options: CountOptions = { session } - if (Object.keys(query).length === 0 && this.disableIndexHints !== true) { + if (this.disableIndexHints !== true) { // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts index 16167cdfd23..1b2fd1e93f6 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts @@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - transform({ adapter, data: doc, fields, operation: 'update' }) + transform({ adapter, data: doc, fields, operation: 'update', validateRelationships: false }) } await Model.collection.bulkWrite( @@ -156,7 +156,13 @@ export async function migrateRelationshipsV2_V3({ // in case if the global doesn't exist in the database yet (not saved) if (doc) { - transform({ adapter: db, data: doc, fields: global.flattenedFields, operation: 'update' }) + transform({ + adapter: db, + data: doc, + fields: global.flattenedFields, + operation: 'update', + validateRelationships: false, + }) await GlobalsModel.collection.updateOne( { diff --git a/packages/db-mongodb/src/utilities/findMany.ts b/packages/db-mongodb/src/utilities/findMany.ts index 5bc9cb984ac..7113dcc071b 100644 --- a/packages/db-mongodb/src/utilities/findMany.ts +++ b/packages/db-mongodb/src/utilities/findMany.ts @@ -86,17 +86,14 @@ export const findMany = async ({ if (pagination !== false && limit) { if (useEstimatedCount) { - countPromise = collection.estimatedDocumentCount({ collation, session }) + countPromise = collection.estimatedDocumentCount() } else { // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses // the correct indexed field - const hint = - Object.keys(query).length === 0 && adapter.disableIndexHints !== true - ? { _id: 1 } - : undefined + const hint = adapter.disableIndexHints !== true ? { _id: 1 } : undefined countPromise = collection.countDocuments(query, { collation, hint, session }) } diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts index 04ff650459a..f1d37a11ff3 100644 --- a/packages/db-mongodb/src/utilities/transform.ts +++ b/packages/db-mongodb/src/utilities/transform.ts @@ -21,6 +21,11 @@ type Args = { fields: FlattenedField[] globalSlug?: string operation: 'create' | 'read' | 'update' + /** + * Throw errors on invalid relationships + * @default true + */ + validateRelationships?: boolean } interface RelationObject { @@ -35,10 +40,12 @@ function isValidRelationObject(value: unknown): value is RelationObject { const convertRelationshipValue = ({ operation, relatedCollection, + validateRelationships, value, }: { operation: Args['operation'] relatedCollection: CollectionConfig + validateRelationships?: boolean value: unknown }) => { const customIDField = relatedCollection.fields.find( @@ -60,7 +67,10 @@ const convertRelationshipValue = ({ if (typeof value === 'string') { try { return new Types.ObjectId(value) - } catch { + } catch (e) { + if (validateRelationships) { + throw e + } return value } } @@ -74,6 +84,7 @@ const sanitizeRelationship = ({ locale, operation, ref, + validateRelationships, value, }: { config: SanitizedConfig @@ -81,6 +92,7 @@ const sanitizeRelationship = ({ locale?: string operation: Args['operation'] ref: Record + validateRelationships?: boolean value?: unknown }) => { if (field.type === 'join') { @@ -124,6 +136,7 @@ const sanitizeRelationship = ({ value: convertRelationshipValue({ operation, relatedCollection: relatedCollectionForSingleValue, + validateRelationships, value: val.value, }), } @@ -134,6 +147,7 @@ const sanitizeRelationship = ({ return convertRelationshipValue({ operation, relatedCollection, + validateRelationships, value: val, }) } @@ -148,7 +162,12 @@ const sanitizeRelationship = ({ if (relatedCollection) { result = { relationTo: value.relationTo, - value: convertRelationshipValue({ operation, relatedCollection, value: value.value }), + value: convertRelationshipValue({ + operation, + relatedCollection, + validateRelationships, + value: value.value, + }), } } } @@ -157,6 +176,7 @@ const sanitizeRelationship = ({ result = convertRelationshipValue({ operation, relatedCollection, + validateRelationships, value, }) } @@ -206,10 +226,17 @@ const sanitizeDate = ({ } } -export const transform = ({ adapter, data, fields, globalSlug, operation }: Args) => { +export const transform = ({ + adapter, + data, + fields, + globalSlug, + operation, + validateRelationships = true, +}: Args) => { if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { - transform({ adapter, data: data[i], fields, operation }) + transform({ adapter, data: data[i], fields, globalSlug, operation, validateRelationships }) } return } @@ -279,7 +306,7 @@ export const transform = ({ adapter, data, fields, globalSlug, operation }: Args field, operation, ref: fieldRef, - value: fieldRef[locale].locale, + value: fieldRef[locale], }) } } else { @@ -323,19 +350,20 @@ export const transform = ({ adapter, data, fields, globalSlug, operation }: Args 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], }) } From d2399ed1fb1fe1d64151616b2b11628ec81dae79 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Fri, 29 Nov 2024 02:03:18 +0200 Subject: [PATCH 8/8] fix: clearable point fields in both mongo and postgres --- .../fields/hooks/beforeValidate/promise.ts | 5 ++++ packages/payload/src/fields/validations.ts | 7 +++++- test/fields/int.spec.ts | 24 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index 68e19c0584d..30eda2a31b6 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -126,6 +126,11 @@ export const promise = async ({ case 'point': { if (Array.isArray(siblingData[field.name])) { + if ((siblingData[field.name] as string[]).some((val) => val === null || val === '')) { + siblingData[field.name] = null + break + } + siblingData[field.name] = (siblingData[field.name] as string[]).map((coordinate, i) => { if (typeof coordinate === 'string') { const value = siblingData[field.name][i] as string diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 49a9321bd77..751859d29ae 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -875,7 +875,12 @@ export type PointFieldValidation = Validate< PointField > -export const point: PointFieldValidation = (value = ['', ''], { req: { t }, required }) => { +export const point: PointFieldValidation = (value, { req: { t }, required }) => { + // Allow to pass null to clear the field + if (!value) { + value = ['', ''] + } + const lng = parseFloat(String(value[0])) const lat = parseFloat(String(value[1])) if ( diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 9f349a33faf..a0f675c0503 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -1089,6 +1089,30 @@ describe('Fields', () => { expect(doc.localized).toEqual(localized) expect(doc.group).toMatchObject(group) }) + + it('should clear a point field', async () => { + if (payload.db.name === 'sqlite') { + return + } + + const doc = await payload.create({ + collection: 'point-fields', + data: { + point: [7, -7], + group: { + point: [7, -7], + }, + }, + }) + + const res = await payload.update({ + collection: 'point-fields', + id: doc.id, + data: { group: { point: null } }, + }) + + expect(res.group.point).toBeFalsy() + }) }) describe('unique indexes', () => {