diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index 4dd6096c2a..7d313461bb 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -1,9 +1,10 @@ import type { CountOptions } from 'mongodb' import type { Count } from 'payload' +import { flattenWhereToOperators } from 'payload' + import type { MongooseAdapter } from './index.js' -import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { getSession } from './utilities/getSession.js' export const count: Count = async function count( @@ -11,37 +12,41 @@ export const count: Count = async function count( { collection, locale, req, where }, ) { const Model = this.collections[collection] - const session = await getSession(this, req) + const options: CountOptions = { + session: await getSession(this, req), + } - const hasNearConstraint = getHasNearConstraint(where) + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } const query = await Model.buildQuery({ locale, payload: this.payload, - session, where, }) // 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.collection.estimatedDocumentCount() + result = await Model.estimatedDocumentCount({ session: options.session }) } else { - const options: CountOptions = { session } - - 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 - // the correct indexed field - options.hint = { - _id: 1, - } - } - - result = await Model.collection.countDocuments(query, options) + result = await Model.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts index 9ff841cf7d..296f2ca077 100644 --- a/packages/db-mongodb/src/countGlobalVersions.ts +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -1,9 +1,10 @@ import type { CountOptions } from 'mongodb' import type { CountGlobalVersions } from 'payload' +import { flattenWhereToOperators } from 'payload' + import type { MongooseAdapter } from './index.js' -import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { getSession } from './utilities/getSession.js' export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions( @@ -11,37 +12,41 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob { global, locale, req, where }, ) { const Model = this.versions[global] - const session = await getSession(this, req) + const options: CountOptions = { + session: await getSession(this, req), + } - const hasNearConstraint = getHasNearConstraint(where) + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } const query = await Model.buildQuery({ locale, payload: this.payload, - session, where, }) // 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.collection.estimatedDocumentCount() + result = await Model.estimatedDocumentCount({ session: options.session }) } else { - 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) + result = await Model.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts index 864b6976ef..0b91f94ac6 100644 --- a/packages/db-mongodb/src/countVersions.ts +++ b/packages/db-mongodb/src/countVersions.ts @@ -1,9 +1,10 @@ import type { CountOptions } from 'mongodb' import type { CountVersions } from 'payload' +import { flattenWhereToOperators } from 'payload' + import type { MongooseAdapter } from './index.js' -import { getHasNearConstraint } from './utilities/getHasNearConstraint.js' import { getSession } from './utilities/getSession.js' export const countVersions: CountVersions = async function countVersions( @@ -11,37 +12,41 @@ export const countVersions: CountVersions = async function countVersions( { collection, locale, req, where }, ) { const Model = this.versions[collection] - const session = await getSession(this, req) + const options: CountOptions = { + session: await getSession(this, req), + } - const hasNearConstraint = getHasNearConstraint(where) + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } const query = await Model.buildQuery({ locale, payload: this.payload, - session, where, }) // 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.collection.estimatedDocumentCount() + result = await Model.estimatedDocumentCount({ session: options.session }) } else { - const options: CountOptions = { session } - - 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 - // the correct indexed field - options.hint = { - _id: 1, - } - } - - result = await Model.collection.countDocuments(query, options) + result = await Model.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index 785f3a01c4..49e549b7e4 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -1,44 +1,48 @@ -import type { Create } from 'payload' +import type { CreateOptions } from 'mongoose' +import type { Create, Document } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' import { handleError } from './utilities/handleError.js' -import { transform } from './utilities/transform.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const create: Create = async function create( this: MongooseAdapter, { collection, data, req }, ) { const Model = this.collections[collection] - const session = await getSession(this, req) - - const fields = this.payload.collections[collection].config.flattenedFields - - if (this.payload.collections[collection].customIDType) { - data._id = data.id + const options: CreateOptions = { + session: await getSession(this, req), } - transform({ - adapter: this, + let doc + + const sanitizedData = sanitizeRelationshipIDs({ + config: this.payload.config, data, - fields, - operation: 'create', + fields: this.payload.collections[collection].config.fields, }) + if (this.payload.collections[collection].customIDType) { + sanitizedData._id = sanitizedData.id + } + try { - const { insertedId: insertedID } = await Model.collection.insertOne(data, { session }) - data._id = insertedID - - transform({ - adapter: this, - data, - fields, - operation: 'read', - }) - - return data + ;[doc] = await Model.create([sanitizedData], options) } catch (error) { handleError({ collection, error, req }) } + + // doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here + const result: Document = JSON.parse(JSON.stringify(doc)) + const verificationToken = doc._verificationToken + + // custom id type reset + result.id = result._id + if (verificationToken) { + result._verificationToken = verificationToken + } + + return result } diff --git a/packages/db-mongodb/src/createGlobal.ts b/packages/db-mongodb/src/createGlobal.ts index 21c1c02333..969eeaed7c 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -1,9 +1,11 @@ +import type { CreateOptions } from 'mongoose' import type { CreateGlobal } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const createGlobal: CreateGlobal = async function createGlobal( this: MongooseAdapter, @@ -11,29 +13,26 @@ export const createGlobal: CreateGlobal = async function createGlobal( ) { const Model = this.globals - const fields = this.payload.config.globals.find( - (globalConfig) => globalConfig.slug === slug, - ).flattenedFields - - transform({ - adapter: this, - data, - fields, - globalSlug: slug, - operation: 'create', + const global = sanitizeRelationshipIDs({ + config: this.payload.config, + data: { + globalType: slug, + ...data, + }, + fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields, }) - const session = await getSession(this, req) + const options: CreateOptions = { + session: await getSession(this, req), + } - const { insertedId: insertedID } = await Model.collection.insertOne(data, { session }) - ;(data as any)._id = insertedID + let [result] = (await Model.create([global], options)) as any - transform({ - adapter: this, - data, - fields, - operation: 'read', - }) + result = JSON.parse(JSON.stringify(result)) - return data + // custom id type reset + result.id = result._id + result = sanitizeInternalFields(result) + + return result } diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index 00941e097f..a6fe7ccb5f 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -1,9 +1,11 @@ -import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload' +import type { CreateOptions } from 'mongoose' + +import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( this: MongooseAdapter, @@ -20,41 +22,36 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo }, ) { const VersionModel = this.versions[globalSlug] - const session = await getSession(this, req) - - const data = { - autosave, - createdAt, - latest: true, - parent, - publishedLocale, - snapshot, - updatedAt, - version: versionData, + const options: CreateOptions = { + session: await getSession(this, req), } - const fields = buildVersionGlobalFields( - this.payload.config, - this.payload.config.globals.find((global) => global.slug === globalSlug), - true, - ) - - transform({ - adapter: this, - data, - fields, - operation: 'create', + 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 { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session }) - ;(data as any)._id = insertedID + const [doc] = await VersionModel.create([data], options, req) - await VersionModel.collection.updateMany( + await VersionModel.updateMany( { $and: [ { _id: { - $ne: insertedID, + $ne: doc._id, }, }, { @@ -70,15 +67,16 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo ], }, { $unset: { latest: 1 } }, - { session }, + options, ) - transform({ - adapter: this, - data, - fields, - operation: 'read', - }) + const result: Document = JSON.parse(JSON.stringify(doc)) + const verificationToken = doc._verificationToken - return data as any + // custom id type reset + result.id = result._id + if (verificationToken) { + result._verificationToken = verificationToken + } + return result } diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index ae872bb624..3482345e24 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -1,10 +1,12 @@ +import type { CreateOptions } from 'mongoose' + import { Types } from 'mongoose' -import { buildVersionCollectionFields, type CreateVersion } from 'payload' +import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const createVersion: CreateVersion = async function createVersion( this: MongooseAdapter, @@ -21,34 +23,29 @@ export const createVersion: CreateVersion = async function createVersion( }, ) { const VersionModel = this.versions[collectionSlug] - const session = await getSession(this, req) - - const data: any = { - autosave, - createdAt, - latest: true, - parent, - publishedLocale, - snapshot, - updatedAt, - version: versionData, + const options: CreateOptions = { + session: await getSession(this, req), } - const fields = buildVersionCollectionFields( - this.payload.config, - this.payload.collections[collectionSlug].config, - true, - ) - - transform({ - adapter: this, - data, - fields, - operation: 'create', + 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 { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session }) - data._id = insertedID + const [doc] = await VersionModel.create([data], options, req) const parentQuery = { $or: [ @@ -59,7 +56,7 @@ export const createVersion: CreateVersion = async function createVersion( }, ], } - if ((data.parent as unknown) instanceof Types.ObjectId) { + if (data.parent instanceof Types.ObjectId) { parentQuery.$or.push({ parent: { $eq: data.parent.toString(), @@ -67,12 +64,12 @@ export const createVersion: CreateVersion = async function createVersion( }) } - await VersionModel.collection.updateMany( + await VersionModel.updateMany( { $and: [ { _id: { - $ne: insertedID, + $ne: doc._id, }, }, parentQuery, @@ -83,21 +80,22 @@ export const createVersion: CreateVersion = async function createVersion( }, { updatedAt: { - $lt: new Date(data.updatedAt), + $lt: new Date(doc.updatedAt), }, }, ], }, { $unset: { latest: 1 } }, - { session }, + options, ) - transform({ - adapter: this, - data, - fields, - operation: 'read', - }) + const result: Document = JSON.parse(JSON.stringify(doc)) + const verificationToken = doc._verificationToken - return data + // custom id type reset + result.id = result._id + if (verificationToken) { + result._verificationToken = verificationToken + } + return result } diff --git a/packages/db-mongodb/src/deleteMany.ts b/packages/db-mongodb/src/deleteMany.ts index 65a8eb1be2..cdc5470e9b 100644 --- a/packages/db-mongodb/src/deleteMany.ts +++ b/packages/db-mongodb/src/deleteMany.ts @@ -1,3 +1,4 @@ +import type { DeleteOptions } from 'mongodb' import type { DeleteMany } from 'payload' import type { MongooseAdapter } from './index.js' @@ -9,16 +10,14 @@ export const deleteMany: DeleteMany = async function deleteMany( { collection, req, where }, ) { const Model = this.collections[collection] - - const session = await getSession(this, req) + const options: DeleteOptions = { + session: await getSession(this, req), + } const query = await Model.buildQuery({ payload: this.payload, - session, where, }) - await Model.collection.deleteMany(query, { - session, - }) + await Model.deleteMany(query, options) } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index 9b6783513c..3d9bc07f08 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -1,41 +1,38 @@ -import type { DeleteOne } from 'payload' +import type { QueryOptions } from 'mongoose' +import type { DeleteOne, Document } from 'payload' import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const deleteOne: DeleteOne = async function deleteOne( this: MongooseAdapter, { collection, req, select, where }, ) { const Model = this.collections[collection] - const session = await getSession(this, req) + const options: QueryOptions = { + projection: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.collections[collection].config.flattenedFields, + select, + }), + session: await getSession(this, req), + } const query = await Model.buildQuery({ payload: this.payload, - session, where, }) - const fields = this.payload.collections[collection].config.flattenedFields + const doc = await Model.findOneAndDelete(query, options).lean() - const doc = await Model.collection.findOneAndDelete(query, { - projection: buildProjectionFromSelect({ - adapter: this, - fields, - select, - }), - session, - }) + let result: Document = JSON.parse(JSON.stringify(doc)) - transform({ - adapter: this, - data: doc, - fields, - operation: 'read', - }) + // custom id type reset + result.id = result._id + result = sanitizeInternalFields(result) - return doc + return result } diff --git a/packages/db-mongodb/src/deleteVersions.ts b/packages/db-mongodb/src/deleteVersions.ts index 4aff143929..5ee111f3a8 100644 --- a/packages/db-mongodb/src/deleteVersions.ts +++ b/packages/db-mongodb/src/deleteVersions.ts @@ -15,11 +15,8 @@ export const deleteVersions: DeleteVersions = async function deleteVersions( const query = await VersionsModel.buildQuery({ locale, payload: this.payload, - session, where, }) - await VersionsModel.collection.deleteMany(query, { - session, - }) + await VersionsModel.deleteMany(query, { session }) } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index 6841970928..d953c3484f 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -1,15 +1,15 @@ -import type { CollationOptions } from 'mongodb' +import type { PaginateOptions } from 'mongoose' import type { Find } from 'payload' +import { flattenWhereToOperators } from 'payload' + import type { MongooseAdapter } from './index.js' 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 { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const find: Find = async function find( this: MongooseAdapter, @@ -20,6 +20,7 @@ export const find: Find = async function find( locale, page, pagination, + projection, req, select, sort: sortArg, @@ -28,17 +29,21 @@ export const find: Find = async function find( ) { const Model = this.collections[collection] const collectionConfig = this.payload.collections[collection].config + const session = await getSession(this, req) - const hasNearConstraint = getHasNearConstraint(where) + let hasNearConstraint = false - const fields = collectionConfig.flattenedFields + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } let sort if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, - fields, + fields: collectionConfig.flattenedFields, locale, sort: sortArg || collectionConfig.defaultSort, timestamps: true, @@ -48,51 +53,90 @@ export const find: Find = async function find( const query = await Model.buildQuery({ locale, payload: this.payload, - session, where, }) // 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: { + session, + }, + page, + pagination, + projection, + sort, + useEstimatedCount, + } - const projection = buildProjectionFromSelect({ - adapter: this, - fields, - select, - }) + if (select) { + paginationOptions.projection = buildProjectionFromSelect({ + adapter: this, + fields: collectionConfig.flattenedFields, + select, + }) + } - const collation: CollationOptions | undefined = this.collation - ? { - locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', - ...this.collation, - } - : undefined + if (this.collation) { + const defaultLocale = 'en' + paginationOptions.collation = { + locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, + ...this.collation, + } + } - const joinAgreggation = await buildJoinAggregation({ + 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, { + hint: { _id: 1 }, + session, + }), + ) + } + } + + 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 + } + } + + let result + + const aggregate = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, locale, - session, - }) - - 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) + } - transform({ adapter: this, data: result.docs, fields, operation: 'read' }) + const docs = JSON.parse(JSON.stringify(result.docs)) - return result + return { + ...result, + docs: docs.map((doc) => { + doc.id = doc._id + return sanitizeInternalFields(doc) + }), + } } diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index a57d5f90a2..ec51dba5b1 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -1,3 +1,4 @@ +import type { QueryOptions } from 'mongoose' import type { FindGlobal } from 'payload' import { combineQueries } from 'payload' @@ -6,40 +7,42 @@ import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const findGlobal: FindGlobal = async function findGlobal( this: MongooseAdapter, { slug, locale, req, select, where }, ) { const Model = this.globals - - const session = await getSession(this, req) + const options: QueryOptions = { + lean: true, + select: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields, + select, + }), + session: await getSession(this, req), + } const query = await Model.buildQuery({ globalSlug: slug, locale, payload: this.payload, - session, where: combineQueries({ globalType: { equals: slug } }, where), }) - 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), - }) + let doc = (await Model.findOne(query, {}, options)) as any if (!doc) { return null } + if (doc._id) { + doc.id = doc._id + delete doc._id + } - transform({ adapter: this, data: doc, fields, operation: 'read' }) + doc = JSON.parse(JSON.stringify(doc)) + doc = sanitizeInternalFields(doc) - return doc as any + return doc } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 51c677833f..3166d89414 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -1,16 +1,14 @@ -import type { CollationOptions } from 'mongodb' +import type { PaginateOptions, QueryOptions } from 'mongoose' import type { FindGlobalVersions } from 'payload' -import { buildVersionGlobalFields } from 'payload' +import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.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 { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( this: MongooseAdapter, @@ -23,7 +21,19 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV true, ) - const hasNearConstraint = getHasNearConstraint(where) + const session = await getSession(this, req) + const options: QueryOptions = { + limit, + session, + skip, + } + + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } let sort if (!hasNearConstraint) { @@ -36,49 +46,69 @@ 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, }) // 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 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, + const paginationOptions: PaginateOptions = { + lean: true, + leanWithId: true, limit, + options, page, pagination, - projection, - query, - session, - skip, + projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }), sort, useEstimatedCount, - }) + } - transform({ - adapter: this, - data: result.docs, - fields: versionFields, - operation: 'read', - }) + if (this.collation) { + const defaultLocale = 'en' + paginationOptions.collation = { + locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, + ...this.collation, + } + } - return result + 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, { + hint: { _id: 1 }, + session, + }), + ) + } + } + + 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)) + + return { + ...result, + docs: docs.map((doc) => { + doc.id = doc._id + return sanitizeInternalFields(doc) + }), + } } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 5ab99aa3ee..10d13b6fcf 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,11 +1,12 @@ -import type { FindOne } from 'payload' +import type { AggregateOptions, QueryOptions } from 'mongoose' +import type { Document, FindOne } from 'payload' import type { MongooseAdapter } from './index.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const findOne: FindOne = async function findOne( this: MongooseAdapter, @@ -13,64 +14,52 @@ export const findOne: FindOne = async function findOne( ) { const Model = this.collections[collection] const collectionConfig = this.payload.collections[collection].config - const session = await getSession(this, req) + const options: AggregateOptions & QueryOptions = { + lean: true, + session, + } const query = await Model.buildQuery({ locale, payload: this.payload, - session, where, }) - const fields = collectionConfig.flattenedFields - const projection = buildProjectionFromSelect({ adapter: this, - fields, + fields: collectionConfig.flattenedFields, select, }) - const joinAggregation = await buildJoinAggregation({ + const aggregate = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, + limit: 1, locale, projection, - session, + query, }) let doc - if (joinAggregation) { - const aggregation = Model.collection.aggregate( - [ - { - $match: query, - }, - ], - { session }, - ) - aggregation.limit(1) - for (const stage of joinAggregation) { - aggregation.addStage(stage) - } - - ;[doc] = await aggregation.toArray() + if (aggregate) { + ;[doc] = await Model.aggregate(aggregate, { session }) } else { - doc = await Model.collection.findOne(query, { projection, session }) + ;(options as Record).projection = projection + doc = await Model.findOne(query, {}, options) } if (!doc) { return null } - transform({ - adapter: this, - data: doc, - fields, - operation: 'read', - }) + let result: Document = JSON.parse(JSON.stringify(doc)) - return doc + // custom id type reset + result.id = result._id + result = sanitizeInternalFields(result) + + return result } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 330ad814a4..19abd38d44 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -1,16 +1,14 @@ -import type { CollationOptions } from 'mongodb' +import type { PaginateOptions, QueryOptions } from 'mongoose' import type { FindVersions } from 'payload' -import { buildVersionCollectionFields } from 'payload' +import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.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 { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const findVersions: FindVersions = async function findVersions( this: MongooseAdapter, @@ -18,10 +16,19 @@ export const findVersions: FindVersions = async function findVersions( ) { const Model = this.versions[collection] const collectionConfig = this.payload.collections[collection].config - const session = await getSession(this, req) + const options: QueryOptions = { + limit, + session, + skip, + } - const hasNearConstraint = getHasNearConstraint(where) + let hasNearConstraint = false + + if (where) { + const constraints = flattenWhereToOperators(where) + hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) + } let sort if (!hasNearConstraint) { @@ -37,48 +44,69 @@ export const findVersions: FindVersions = async function findVersions( const query = await Model.buildQuery({ locale, payload: this.payload, - session, 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 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, + const paginationOptions: PaginateOptions = { + lean: true, + leanWithId: true, limit, + options, page, pagination, - projection, - query, - session, - skip, + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), + select, + }), sort, useEstimatedCount, - }) + } - transform({ - adapter: this, - data: result.docs, - fields: versionFields, - operation: 'read', - }) + if (this.collation) { + const defaultLocale = 'en' + paginationOptions.collation = { + locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, + ...this.collation, + } + } - return result + 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, { + hint: { _id: 1 }, + session, + }), + ) + } + } + + 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)) + + return { + ...result, + docs: docs.map((doc) => { + doc.id = doc._id + return sanitizeInternalFields(doc) + }), + } } diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index d4d3639b04..911677c143 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -59,8 +59,6 @@ import { upsert } from './upsert.js' export type { MigrateDownArgs, MigrateUpArgs } from './types.js' -export { transform } from './utilities/transform.js' - export interface Args { /** Set to false to disable auto-pluralization of collection names, Defaults to true */ autoPluralization?: boolean diff --git a/packages/db-mongodb/src/migrateFresh.ts b/packages/db-mongodb/src/migrateFresh.ts index cd9e538629..4db2e76c42 100644 --- a/packages/db-mongodb/src/migrateFresh.ts +++ b/packages/db-mongodb/src/migrateFresh.ts @@ -1,3 +1,5 @@ +import type { PayloadRequest } from 'payload' + import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload' import prompts from 'prompts' diff --git a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts index e363a5fe32..5d5e2e7590 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, FlattenedField, PayloadRequest } from 'payload' +import type { Field, PayloadRequest, SanitizedConfig } from 'payload' import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' import type { MongooseAdapter } from '../index.js' import { getSession } from '../utilities/getSession.js' -import { transform } from '../utilities/transform.js' +import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js' const migrateModelWithBatching = async ({ - adapter, batchSize, + config, fields, Model, session, }: { - adapter: MongooseAdapter batchSize: number - fields: FlattenedField[] + config: SanitizedConfig + fields: Field[] Model: Model session: ClientSession }): Promise => { @@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - transform({ adapter, data: doc, fields, operation: 'update', validateRelationships: false }) + sanitizeRelationshipIDs({ config, data: doc, fields }) } await Model.collection.bulkWrite( @@ -115,9 +115,9 @@ export async function migrateRelationshipsV2_V3({ payload.logger.info(`Migrating collection "${collection.slug}"`) await migrateModelWithBatching({ - adapter: db, batchSize, - fields: collection.flattenedFields, + config, + fields: collection.fields, 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, - fields: buildVersionCollectionFields(config, collection, true), + config, + fields: buildVersionCollectionFields(config, collection), Model: db.versions[collection.slug], session, }) @@ -156,13 +156,7 @@ 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', - validateRelationships: false, - }) + sanitizeRelationshipIDs({ config, data: doc, fields: global.fields }) await GlobalsModel.collection.updateOne( { @@ -179,9 +173,9 @@ export async function migrateRelationshipsV2_V3({ payload.logger.info(`Migrating global versions "${global.slug}"`) await migrateModelWithBatching({ - adapter: db, batchSize, - fields: buildVersionGlobalFields(config, global, true), + config, + fields: buildVersionGlobalFields(config, global), Model: db.versions[global.slug], session, }) diff --git a/packages/db-mongodb/src/queries/buildAndOrConditions.ts b/packages/db-mongodb/src/queries/buildAndOrConditions.ts index e513e11861..9ac0529679 100644 --- a/packages/db-mongodb/src/queries/buildAndOrConditions.ts +++ b/packages/db-mongodb/src/queries/buildAndOrConditions.ts @@ -1,4 +1,3 @@ -import type { ClientSession } from 'mongodb' import type { FlattenedField, Payload, Where } from 'payload' import { parseParams } from './parseParams.js' @@ -9,7 +8,6 @@ export async function buildAndOrConditions({ globalSlug, locale, payload, - session, where, }: { collectionSlug?: string @@ -17,7 +15,6 @@ export async function buildAndOrConditions({ globalSlug?: string locale?: string payload: Payload - session?: ClientSession where: Where[] }): Promise[]> { const completedConditions = [] @@ -33,7 +30,6 @@ 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 7dc3d5fecc..64cde6af7a 100644 --- a/packages/db-mongodb/src/queries/buildQuery.ts +++ b/packages/db-mongodb/src/queries/buildQuery.ts @@ -1,6 +1,7 @@ -import type { ClientSession } from 'mongodb' import type { FlattenedField, Payload, Where } from 'payload' +import { QueryError } from 'payload' + import { parseParams } from './parseParams.js' type GetBuildQueryPluginArgs = { @@ -12,7 +13,6 @@ export type BuildQueryArgs = { globalSlug?: string locale?: string payload: Payload - session?: ClientSession where: Where } @@ -28,7 +28,6 @@ export const getBuildQueryPlugin = ({ globalSlug, locale, payload, - session, where, }: BuildQueryArgs): Promise> { let fields = versionsFields @@ -42,17 +41,20 @@ 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 4efbd2469d..3f0f97fd67 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -1,4 +1,3 @@ -import type { ClientSession, FindOptions } from 'mongodb' import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload' import { Types } from 'mongoose' @@ -16,11 +15,9 @@ type SearchParam = { value?: unknown } -const subQueryOptions: FindOptions = { +const subQueryOptions = { + lean: true, limit: 50, - projection: { - _id: true, - }, } /** @@ -34,7 +31,6 @@ export async function buildSearchParam({ locale, operator, payload, - session, val, }: { collectionSlug?: string @@ -44,7 +40,6 @@ export async function buildSearchParam({ locale?: string operator: string payload: Payload - session?: ClientSession val: unknown }): Promise { // Replace GraphQL nested field double underscore formatting @@ -139,14 +134,17 @@ export async function buildSearchParam({ }, }) - const result = await SubModel.collection - .find(subQuery, { session, ...subQueryOptions }) - .toArray() + const result = await SubModel.find(subQuery, subQueryOptions) const $in: unknown[] = [] result.forEach((doc) => { - $in.push(doc._id) + const stringID = doc._id.toString() + $in.push(stringID) + + if (Types.ObjectId.isValid(stringID)) { + $in.push(doc._id) + } }) if (pathsToQuery.length === 1) { @@ -164,9 +162,7 @@ export async function buildSearchParam({ } const subQuery = priorQueryResult.value - const result = await SubModel.collection - .find(subQuery, { session, ...subQueryOptions }) - .toArray() + const result = await SubModel.find(subQuery, subQueryOptions) const $in = result.map((doc) => doc._id) diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index 09f7cad6df..6f76cbffef 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -11,13 +11,20 @@ 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): Record => { +}: Args): PaginateOptions['sort'] => { if (!sort) { if (timestamps) { sort = '-createdAt' @@ -30,15 +37,15 @@ export const buildSortParam = ({ sort = [sort] } - const sorting = sort.reduce>((acc, item) => { + const sorting = sort.reduce((acc, item) => { let sortProperty: string - let sortDirection: -1 | 1 + let sortDirection: SortDirection if (item.indexOf('-') === 0) { sortProperty = item.substring(1) - sortDirection = -1 + sortDirection = 'desc' } else { sortProperty = item - sortDirection = 1 + sortDirection = 'asc' } 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 59a7d3d99d..f7aec4bbd4 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 } from 'payload/shared' +import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared' type Args = { config: SanitizedConfig @@ -33,7 +33,7 @@ export const getLocalizedSortProperty = ({ (field) => fieldAffectsData(field) && field.name === firstSegment, ) - if (matchedField) { + if (matchedField && !fieldIsPresentationalOnly(matchedField)) { let nextFields: FlattenedField[] const remainingSegments = [...segments] let localizedSegment = matchedField.name diff --git a/packages/db-mongodb/src/queries/parseParams.ts b/packages/db-mongodb/src/queries/parseParams.ts index e8b4cb0197..18dae08d20 100644 --- a/packages/db-mongodb/src/queries/parseParams.ts +++ b/packages/db-mongodb/src/queries/parseParams.ts @@ -1,4 +1,3 @@ -import type { ClientSession } from 'mongodb' import type { FilterQuery } from 'mongoose' import type { FlattenedField, Operator, Payload, Where } from 'payload' @@ -14,7 +13,6 @@ export async function parseParams({ globalSlug, locale, payload, - session, where, }: { collectionSlug?: string @@ -22,7 +20,6 @@ export async function parseParams({ globalSlug?: string locale: string payload: Payload - session?: ClientSession where: Where }): Promise> { let result = {} as FilterQuery @@ -65,7 +62,6 @@ export async function parseParams({ locale, operator, payload, - session, val: pathOperators[operator], }) diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index faf1233862..ce8df39990 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -37,22 +37,6 @@ 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, @@ -375,14 +359,6 @@ 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 164a576728..3ea8cb34d7 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -1,17 +1,15 @@ -import type { CollationOptions } from 'mongodb' +import type { PaginateOptions, QueryOptions } from 'mongoose' import type { QueryDrafts } from 'payload' -import { buildVersionCollectionFields, combineQueries } from 'payload' +import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload' import type { MongooseAdapter } from './index.js' 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 { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' export const queryDrafts: QueryDrafts = async function queryDrafts( this: MongooseAdapter, @@ -19,11 +17,18 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( ) { const VersionModel = this.versions[collection] const collectionConfig = this.payload.collections[collection].config - const session = await getSession(this, req) + const options: QueryOptions = { + session: await getSession(this, req), + } - const hasNearConstraint = getHasNearConstraint(where) + let hasNearConstraint 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, @@ -39,65 +44,95 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( const versionQuery = await VersionModel.buildQuery({ locale, payload: this.payload, - session, where: combinedWhere, }) - const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true) const projection = buildProjectionFromSelect({ adapter: this, - fields: versionFields, + fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), 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, + } - const collation: CollationOptions | undefined = this.collation - ? { - locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', - ...this.collation, - } - : undefined + if (this.collation) { + const defaultLocale = 'en' + paginationOptions.collation = { + locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, + ...this.collation, + } + } - const joinAgreggation = await buildJoinAggregation({ + 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 aggregate = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, locale, projection, - session, + query: versionQuery, versions: true, }) - const result = await findMany({ - adapter: this, - collation, - collection: VersionModel.collection, - joinAgreggation, - limit, - page, - pagination, - projection, - query: versionQuery, - session, - sort, - useEstimatedCount, - }) - - transform({ - adapter: this, - data: result.docs, - fields: versionFields, - operation: 'read', - }) - - 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 + // build join aggregation + if (aggregate) { + result = await VersionModel.aggregatePaginate( + VersionModel.aggregate(aggregate), + paginationOptions, + ) + } else { + result = await VersionModel.paginate(versionQuery, paginationOptions) } - return result + const docs = JSON.parse(JSON.stringify(result.docs)) + + return { + ...result, + docs: docs.map((doc) => { + doc = { + _id: doc.parent, + id: doc.parent, + ...doc.version, + } + + return sanitizeInternalFields(doc) + }), + } } diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index ac462c85e3..239fff5fd9 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -1,45 +1,47 @@ +import type { QueryOptions } from 'mongoose' import type { UpdateGlobal } from 'payload' import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const updateGlobal: UpdateGlobal = async function updateGlobal( this: MongooseAdapter, { slug, data, options: optionsArgs = {}, req, select }, ) { const Model = this.globals - const fields = this.payload.config.globals.find((global) => global.slug === slug).flattenedFields + const fields = this.payload.config.globals.find((global) => global.slug === slug).fields - const session = await getSession(this, req) + const options: QueryOptions = { + ...optionsArgs, + lean: true, + new: true, + projection: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.config.globals.find((global) => global.slug === slug).flattenedFields, + select, + }), + session: await getSession(this, req), + } - transform({ - adapter: this, + let result + + const sanitizedData = sanitizeRelationshipIDs({ + config: this.payload.config, data, fields, - operation: 'update', - timestamps: optionsArgs.timestamps !== false, }) - const result: any = await Model.collection.findOneAndUpdate( - { globalType: slug }, - { $set: data }, - { - ...optionsArgs, - projection: buildProjectionFromSelect({ adapter: this, fields, select }), - returnDocument: 'after', - session, - }, - ) + result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options) - transform({ - adapter: this, - data: result, - fields, - operation: 'read', - }) + 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 42eaf6bf41..af1453840b 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -1,10 +1,12 @@ +import type { QueryOptions } from 'mongoose' + import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload' import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export async function updateGlobalVersion( this: MongooseAdapter, @@ -21,50 +23,44 @@ export async function updateGlobalVersion( ) { const VersionModel = this.versions[globalSlug] const whereToUse = where || { id: { equals: id } } - const fields = buildVersionGlobalFields( - this.payload.config, - this.payload.config.globals.find((global) => global.slug === globalSlug), - true, - ) - const session = await getSession(this, req) + const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug) + const fields = buildVersionGlobalFields(this.payload.config, currentGlobal) + + const options: QueryOptions = { + ...optionsArgs, + lean: true, + new: true, + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true), + select, + }), + session: await getSession(this, req), + } const query = await VersionModel.buildQuery({ locale, payload: this.payload, - session, where: whereToUse, }) - transform({ - adapter: this, + const sanitizedData = sanitizeRelationshipIDs({ + config: this.payload.config, data: versionData, fields, - operation: 'update', - timestamps: optionsArgs.timestamps !== false, }) - const doc: any = await VersionModel.collection.findOneAndUpdate( - query, - { $set: versionData }, - { - ...optionsArgs, - projection: buildProjectionFromSelect({ - adapter: this, - fields, - select, - }), - returnDocument: 'after', - session, - }, - ) + const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) - transform({ - adapter: this, - data: doc, - fields, - operation: 'read', - }) + const result = JSON.parse(JSON.stringify(doc)) - return doc + const verificationToken = doc._verificationToken + + // custom id type reset + result.id = result._id + if (verificationToken) { + result._verificationToken = verificationToken + } + return result } diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index d78c5dba15..555d46925b 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -1,3 +1,4 @@ +import type { QueryOptions } from 'mongoose' import type { UpdateOne } from 'payload' import type { MongooseAdapter } from './index.js' @@ -5,7 +6,8 @@ import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' import { handleError } from './utilities/handleError.js' -import { transform } from './utilities/transform.js' +import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const updateOne: UpdateOne = async function updateOne( this: MongooseAdapter, @@ -13,45 +15,42 @@ 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.flattenedFields - - const session = await getSession(this, req) + const fields = this.payload.collections[collection].config.fields + const options: QueryOptions = { + ...optionsArgs, + lean: true, + new: true, + projection: buildProjectionFromSelect({ + adapter: this, + fields: this.payload.collections[collection].config.flattenedFields, + select, + }), + session: await getSession(this, req), + } const query = await Model.buildQuery({ locale, payload: this.payload, - session, where, }) - transform({ - adapter: this, + let result + + const sanitizedData = sanitizeRelationshipIDs({ + config: this.payload.config, data, fields, - operation: 'update', - timestamps: optionsArgs.timestamps !== false, }) try { - const result = await Model.collection.findOneAndUpdate( - query, - { $set: data }, - { - ...optionsArgs, - projection: buildProjectionFromSelect({ adapter: this, fields, select }), - returnDocument: 'after', - session, - }, - ) - - transform({ - adapter: this, - data: result, - fields, - operation: 'read', - }) - return result + result = await Model.findOneAndUpdate(query, sanitizedData, options) } 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 4f3bb3242d..9514e7cb73 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -1,10 +1,12 @@ +import type { QueryOptions } from 'mongoose' + import { buildVersionCollectionFields, type UpdateVersion } from 'payload' import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { transform } from './utilities/transform.js' +import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' export const updateVersion: UpdateVersion = async function updateVersion( this: MongooseAdapter, @@ -15,51 +17,46 @@ export const updateVersion: UpdateVersion = async function updateVersion( const fields = buildVersionCollectionFields( this.payload.config, this.payload.collections[collection].config, - true, ) - const session = await getSession(this, req) + const options: QueryOptions = { + ...optionsArgs, + lean: true, + new: true, + projection: buildProjectionFromSelect({ + adapter: this, + fields: buildVersionCollectionFields( + this.payload.config, + this.payload.collections[collection].config, + true, + ), + select, + }), + session: await getSession(this, req), + } const query = await VersionModel.buildQuery({ locale, payload: this.payload, - session, where: whereToUse, }) - transform({ - adapter: this, + const sanitizedData = sanitizeRelationshipIDs({ + config: this.payload.config, data: versionData, fields, - operation: 'update', - timestamps: optionsArgs.timestamps !== false, }) - 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, - }, - ) + const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) - transform({ - adapter: this, - data: doc, - fields, - operation: 'read', - }) + const result = JSON.parse(JSON.stringify(doc)) - return doc as any + const verificationToken = doc._verificationToken + + // custom id type reset + result.id = result._id + if (verificationToken) { + result._verificationToken = verificationToken + } + return result } diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index 9de62af98d..b9d08a09e2 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -1,4 +1,3 @@ -import type { ClientSession } from 'mongodb' import type { PipelineStage } from 'mongoose' import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload' @@ -11,9 +10,12 @@ 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 - session?: ClientSession + // the where clause for the top collection + query?: Where /** whether the query is from drafts */ versions?: boolean } @@ -23,9 +25,10 @@ export const buildJoinAggregation = async ({ collection, collectionConfig, joins, + limit, locale, projection, - session, + query, versions, }: BuildJoinAggregationArgs): Promise => { if (Object.keys(collectionConfig.joins).length === 0 || joins === false) { @@ -33,7 +36,23 @@ export const buildJoinAggregation = async ({ } const joinConfig = adapter.payload.collections[collection].config.joins - const aggregate: PipelineStage[] = [] + const aggregate: PipelineStage[] = [ + { + $sort: { createdAt: -1 }, + }, + ] + + if (query) { + aggregate.push({ + $match: query, + }) + } + + if (limit) { + aggregate.push({ + $limit: limit, + }) + } for (const slug of Object.keys(joinConfig)) { for (const join of joinConfig[slug]) { @@ -53,25 +72,26 @@ 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, payload: adapter.payload, - session, where: whereJoin, }) const pipeline: Exclude[] = [ { $match }, { - $sort, + $sort: { [sortProperty]: sortDirection }, }, ] @@ -169,8 +189,8 @@ export const buildJoinAggregation = async ({ } } - if (!aggregate.length) { - return + if (projection) { + aggregate.push({ $project: projection }) } return aggregate diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts index e506c7da82..51215d7c6f 100644 --- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -1,4 +1,4 @@ -import type { Field, FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' +import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared' @@ -29,11 +29,6 @@ const addFieldToProjection = ({ } } -const blockTypeField: Field = { - name: 'blockType', - type: 'text', -} - const traverseFields = ({ adapter, databaseSchemaPath = '', @@ -133,14 +128,6 @@ 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, @@ -166,13 +153,7 @@ const traverseFields = ({ if (blockSelectMode === 'include') { blocksSelect[block.slug]['id'] = true - addFieldToProjection({ - adapter, - databaseSchemaPath: fieldDatabaseSchemaPath, - field: blockTypeField, - projection, - withinLocalizedField: fieldWithinLocalizedField, - }) + blocksSelect[block.slug]['blockType'] = true } traverseFields({ diff --git a/packages/db-mongodb/src/utilities/findMany.ts b/packages/db-mongodb/src/utilities/findMany.ts deleted file mode 100644 index 675cb70530..0000000000 --- a/packages/db-mongodb/src/utilities/findMany.ts +++ /dev/null @@ -1,128 +0,0 @@ -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 aggregation = collection.aggregate( - [ - { - $match: query, - }, - ], - { collation, session }, - ) - - if (sort) { - aggregation.sort(sort) - } - - if (skip) { - aggregation.skip(skip) - } - - if (limit) { - aggregation.limit(limit) - } - - for (const stage of joinAgreggation) { - aggregation.addStage(stage) - } - - if (projection) { - aggregation.project(projection) - } - - docsPromise = aggregation.toArray() - } else { - docsPromise = collection - .find(query, { - collation, - limit, - projection, - session, - skip, - sort, - }) - .toArray() - } - - if (pagination !== false && limit) { - if (useEstimatedCount) { - 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 = 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/getHasNearConstraint.ts b/packages/db-mongodb/src/utilities/getHasNearConstraint.ts deleted file mode 100644 index 861129ec31..0000000000 --- a/packages/db-mongodb/src/utilities/getHasNearConstraint.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Where } from 'payload' - -export const getHasNearConstraint = (where?: Where): boolean => { - if (!where) { - return false - } - - for (const key in where) { - const value = where[key] - - 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/sanitizeInternalFields.ts b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts new file mode 100644 index 0000000000..14ab00da67 --- /dev/null +++ b/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts @@ -0,0 +1,20 @@ +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/transform.spec.ts b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts similarity index 93% rename from packages/db-mongodb/src/utilities/transform.spec.ts rename to packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts index f68b691e2f..af95ea2a3c 100644 --- a/packages/db-mongodb/src/utilities/transform.spec.ts +++ b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts @@ -1,9 +1,8 @@ -import { flattenAllFields, type Field, type SanitizedConfig } from 'payload' +import type { Field, SanitizedConfig } from 'payload' import { Types } from 'mongoose' -import { transform } from './transform.js' -import { MongooseAdapter } from '..' +import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js' const flattenRelationshipValues = (obj: Record, prefix = ''): Record => { return Object.keys(obj).reduce( @@ -272,8 +271,8 @@ const relsData = { }, } -describe('transform', () => { - it('should sanitize relationships with transform write', () => { +describe('sanitizeRelationshipIDs', () => { + it('should sanitize relationships', () => { const data = { ...relsData, array: [ @@ -349,19 +348,12 @@ describe('transform', () => { } const flattenValuesBefore = Object.values(flattenRelationshipValues(data)) - const mockAdapter = { payload: { config } } as MongooseAdapter - - const fields = flattenAllFields({ fields: config.collections[0].fields }) - - transform({ type: 'write', adapter: mockAdapter, data, fields }) - + sanitizeRelationshipIDs({ config, data, fields: config.collections[0].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/sanitizeRelationshipIDs.ts b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts new file mode 100644 index 0000000000..bdecaa4066 --- /dev/null +++ b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts @@ -0,0 +1,156 @@ +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/transform.ts b/packages/db-mongodb/src/utilities/transform.ts deleted file mode 100644 index aa19c846db..0000000000 --- a/packages/db-mongodb/src/utilities/transform.ts +++ /dev/null @@ -1,385 +0,0 @@ -import type { - CollectionConfig, - DateField, - 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 - operation: 'create' | 'read' | 'update' - /** - * Set updatedAt and createdAt - * @default true - */ - timestamps?: boolean - /** - * Throw errors on invalid relationships - * @default true - */ - validateRelationships?: boolean -} - -interface RelationObject { - relationTo: string - value: number | string -} - -function isValidRelationObject(value: unknown): value is RelationObject { - return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value -} - -const convertRelationshipValue = ({ - operation, - relatedCollection, - validateRelationships, - value, -}: { - operation: Args['operation'] - relatedCollection: CollectionConfig - validateRelationships?: boolean - value: unknown -}) => { - const customIDField = relatedCollection.fields.find( - (field) => fieldAffectsData(field) && field.name === 'id', - ) - - if (operation === 'read') { - if (value instanceof Types.ObjectId) { - return value.toHexString() - } - - return value - } - - if (customIDField) { - return value - } - - if (typeof value === 'string') { - try { - return new Types.ObjectId(value) - } catch (e) { - if (validateRelationships) { - throw e - } - return value - } - } - - return value -} - -const sanitizeRelationship = ({ - config, - field, - locale, - operation, - ref, - validateRelationships, - value, -}: { - config: SanitizedConfig - field: JoinField | RelationshipField | UploadField - locale?: string - operation: Args['operation'] - ref: Record - validateRelationships?: boolean - value?: unknown -}) => { - if (field.type === 'join') { - if ( - operation === 'read' && - value && - typeof value === 'object' && - 'docs' in value && - Array.isArray(value.docs) - ) { - for (let i = 0; i < value.docs.length; i++) { - const item = value.docs[i] - if (item instanceof Types.ObjectId) { - value.docs[i] = item.toHexString() - } - } - } - - return value - } - let relatedCollection: CollectionConfig | undefined - let result = value - - const hasManyRelations = typeof field.relationTo !== 'string' - - if (!hasManyRelations) { - relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo) - } - - if (Array.isArray(value)) { - result = value.map((val) => { - // Handle has many - polymorphic - if (isValidRelationObject(val)) { - const relatedCollectionForSingleValue = config.collections?.find( - ({ slug }) => slug === val.relationTo, - ) - - if (relatedCollectionForSingleValue) { - return { - relationTo: val.relationTo, - value: convertRelationshipValue({ - operation, - relatedCollection: relatedCollectionForSingleValue, - validateRelationships, - value: val.value, - }), - } - } - } - - if (relatedCollection) { - return convertRelationshipValue({ - operation, - relatedCollection, - validateRelationships, - value: val, - }) - } - - return val - }) - } - // Handle has one - polymorphic - else if (isValidRelationObject(value)) { - relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo) - - if (relatedCollection) { - result = { - relationTo: value.relationTo, - value: convertRelationshipValue({ - operation, - relatedCollection, - validateRelationships, - value: value.value, - }), - } - } - } - // Handle has one - else if (relatedCollection) { - result = convertRelationshipValue({ - operation, - relatedCollection, - validateRelationships, - value, - }) - } - - if (locale) { - ref[locale] = result - } else { - ref[field.name] = result - } -} - -/** - * 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 - } -} - -/** - * @experimental This API can be changed without a major version bump. - */ -export const transform = ({ - adapter, - data, - fields, - globalSlug, - operation, - timestamps = true, - validateRelationships = true, -}: Args) => { - if (Array.isArray(data)) { - for (let i = 0; i < data.length; i++) { - transform({ adapter, data: data[i], fields, globalSlug, operation, validateRelationships }) - } - return - } - - const { - payload: { config }, - } = adapter - - if (operation === 'read') { - delete data['__v'] - data.id = data._id - delete data['_id'] - - if (data.id instanceof Types.ObjectId) { - data.id = data.id.toHexString() - } - } - - if (operation !== 'read') { - if (timestamps) { - if (operation === 'create' && !data.createdAt) { - data.createdAt = new Date() - } - - data.updatedAt = new Date() - } - - if (globalSlug) { - data.globalType = globalSlug - } - } - - const sanitize: TraverseFlattenedFieldsCallback = ({ field, ref }) => { - if (!ref || typeof ref !== 'object') { - return - } - - if (operation !== 'read') { - 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 (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], - }) - } - } else { - sanitizeDate({ - field, - operation, - ref: ref as Record, - value: ref[field.name], - }) - } - } - - if ( - field.type === 'relationship' || - field.type === 'upload' || - (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 - } - - // 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, - operation, - ref: fieldRef, - validateRelationships, - value, - }) - } - } - } else { - // handle non-localized relationships - sanitizeRelationship({ - config, - field, - locale: undefined, - operation, - ref: ref as Record, - validateRelationships, - value: ref[field.name], - }) - } - } - } - - traverseFields({ callback: sanitize, fillEmpty: false, flattenedFields: fields, ref: data }) -} diff --git a/packages/payload/src/fields/hooks/beforeValidate/promise.ts b/packages/payload/src/fields/hooks/beforeValidate/promise.ts index b873a3882c..dc094f04f1 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -126,11 +126,6 @@ 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 f5b07d6a5a..8ce6caf817 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -875,12 +875,7 @@ export type PointFieldValidation = Validate< PointField > -export const point: PointFieldValidation = (value, { req: { t }, required }) => { - // Allow to pass null to clear the field - if (!value) { - value = ['', ''] - } - +export const point: PointFieldValidation = (value = ['', ''], { req: { t }, required }) => { const lng = parseFloat(String(value[0])) const lat = parseFloat(String(value[1])) if ( diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 3515c50bbc..19e2defc64 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1362,10 +1362,7 @@ export { sanitizeJoinParams } from './utilities/sanitizeJoinParams.js' export { sanitizePopulateParam } from './utilities/sanitizePopulateParam.js' export { sanitizeSelectParam } from './utilities/sanitizeSelectParam.js' export { traverseFields } from './utilities/traverseFields.js' -export type { - TraverseFieldsCallback, - TraverseFlattenedFieldsCallback, -} from './utilities/traverseFields.js' +export type { TraverseFieldsCallback } 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 281d849f23..1bda56c274 100644 --- a/packages/payload/src/utilities/traverseFields.ts +++ b/packages/payload/src/utilities/traverseFields.ts @@ -1,12 +1,4 @@ -import type { - ArrayField, - BlocksField, - Field, - FlattenedArrayField, - FlattenedBlock, - FlattenedField, - TabAsField, -} from '../fields/config/types.js' +import type { ArrayField, BlocksField, Field, TabAsField } from '../fields/config/types.js' import { fieldHasSubFields } from '../fields/config/types.js' @@ -20,7 +12,7 @@ const traverseArrayOrBlocksField = ({ callback: TraverseFieldsCallback data: Record[] field: ArrayField | BlocksField - fillEmpty?: boolean + fillEmpty: boolean parentRef?: unknown }) => { if (fillEmpty) { @@ -36,23 +28,20 @@ const traverseArrayOrBlocksField = ({ } 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) as FlattenedBlock + const block = field.blocks.find((block) => block.slug === ref.blockType) fields = block?.fields - flattenedFields = block?.flattenedFields } else if (field.type === 'array') { fields = field.fields - flattenedFields = (field as FlattenedArrayField)?.flattenedFields } - if (flattenedFields || fields) { - traverseFields({ callback, fields, fillEmpty, flattenedFields, parentRef, ref }) + if (fields) { + traverseFields({ callback, fields, fillEmpty, parentRef, ref }) } } } -type TraverseFieldsCallbackArgs = { +export type TraverseFieldsCallback = (args: { /** * The current field */ @@ -69,45 +58,12 @@ type TraverseFieldsCallbackArgs = { * 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 | FlattenedField | TabAsField)[] - /** fill empty properties to use this without data */ + fields: (Field | TabAsField)[] fillEmpty?: boolean - flattenedFields?: FlattenedField[] parentRef?: Record | unknown ref?: Record | unknown } @@ -125,11 +81,10 @@ export const traverseFields = ({ callback, fields, fillEmpty = true, - flattenedFields, parentRef = {}, ref = {}, -}: TraverseFieldsArgs | TraverseFlattenedFieldsArgs): void => { - ;(flattenedFields ?? fields).some((field) => { +}: TraverseFieldsArgs): void => { + fields.some((field) => { let skip = false const next = () => { skip = true @@ -139,16 +94,7 @@ export const traverseFields = ({ return } - if ( - callback && - callback({ - // @ts-expect-error compatibillity Field | FlattenedField - field, - next, - parentRef, - ref, - }) - ) { + if (callback && callback({ field, next, parentRef, ref })) { return true } @@ -193,7 +139,6 @@ export const traverseFields = ({ if ( callback && callback({ - // @ts-expect-error compatibillity Field | FlattenedField field: { ...tab, type: 'tab' }, next, parentRef: currentParentRef, @@ -215,15 +160,12 @@ export const traverseFields = ({ return } - if ( - (flattenedFields || field.type !== 'tab') && - (fieldHasSubFields(field as Field) || field.type === 'tab' || field.type === 'blocks') - ) { + if (field.type !== 'tab' && (fieldHasSubFields(field) || field.type === 'blocks')) { if ('name' in field && field.name) { currentParentRef = currentRef if (!ref[field.name]) { if (fillEmpty) { - if (field.type === 'group' || field.type === 'tab') { + if (field.type === 'group') { ref[field.name] = {} } else if (field.type === 'array' || field.type === 'blocks') { if (field.localized) { @@ -240,7 +182,7 @@ export const traverseFields = ({ } if ( - (field.type === 'group' || field.type === 'tab') && + field.type === 'group' && field.localized && currentRef && typeof currentRef === 'object' @@ -251,10 +193,9 @@ export const traverseFields = ({ callback, fields: field.fields, fillEmpty, - flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined, parentRef: currentParentRef, ref: currentRef[key], - } as TraverseFieldsArgs) + }) } } return @@ -298,7 +239,6 @@ export const traverseFields = ({ callback, fields: field.fields, fillEmpty, - flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined, parentRef: currentParentRef, ref: currentRef, }) diff --git a/test/fields-relationship/int.spec.ts b/test/fields-relationship/int.spec.ts index c7d4d21935..34eaeeee96 100644 --- a/test/fields-relationship/int.spec.ts +++ b/test/fields-relationship/int.spec.ts @@ -53,12 +53,10 @@ describe('Relationship Fields', () => { collection: versionedRelationshipFieldSlug, data: { title: 'Version 1 Title', - relationshipField: [ - { - value: relatedDoc.id, - relationTo: collection1Slug, - }, - ], + relationshipField: { + value: relatedDoc.id, + relationTo: collection1Slug, + }, }, }) diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 251f8f73d6..5f0de2ee47 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -1134,30 +1134,6 @@ 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', () => { diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index 627c65b9f6..ff2cbe6e55 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -174,10 +174,7 @@ describe('Joins Field', () => { collection: categoriesSlug, }) - expect(categoryWithPosts).toStrictEqual({ - id: categoryWithPosts.id, - group: categoryWithPosts.group, - }) + expect(Object.keys(categoryWithPosts)).toStrictEqual(['id', '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 ab59faac3f..081a3f633c 100644 --- a/test/select/int.spec.ts +++ b/test/select/int.spec.ts @@ -1633,10 +1633,7 @@ describe('Select', () => { }, }) - expect(res).toStrictEqual({ - id: res.id, - text: res.text, - }) + expect(Object.keys(res)).toStrictEqual(['id', 'text']) }) it('should apply select with updateByID', async () => { @@ -1649,18 +1646,13 @@ describe('Select', () => { select: { text: true }, }) - expect(res).toStrictEqual({ - id: res.id, - text: res.text, - }) + expect(Object.keys(res)).toStrictEqual(['id', 'text']) }) it('should apply select with updateBulk', async () => { const post = await createPost() - const { - docs: [res], - } = await payload.update({ + const res = await payload.update({ collection: 'posts', where: { id: { @@ -1671,10 +1663,7 @@ describe('Select', () => { select: { text: true }, }) - expect(res).toStrictEqual({ - id: res.id, - text: res.text, - }) + expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) }) it('should apply select with deleteByID', async () => { @@ -1686,18 +1675,13 @@ describe('Select', () => { select: { text: true }, }) - expect(res).toStrictEqual({ - id: res.id, - text: res.text, - }) + expect(Object.keys(res)).toStrictEqual(['id', 'text']) }) it('should apply select with deleteBulk', async () => { const post = await createPost() - const { - docs: [res], - } = await payload.delete({ + const res = await payload.delete({ collection: 'posts', where: { id: { @@ -1707,10 +1691,7 @@ describe('Select', () => { select: { text: true }, }) - expect(res).toStrictEqual({ - id: res.id, - text: res.text, - }) + expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) }) it('should apply select with duplicate', async () => { @@ -1722,10 +1703,7 @@ describe('Select', () => { select: { text: true }, }) - expect(res).toStrictEqual({ - id: res.id, - text: res.text, - }) + expect(Object.keys(res)).toStrictEqual(['id', 'text']) }) })