From e4682920394dabdfaa57bf6cecf556fa67bb22c9 Mon Sep 17 00:00:00 2001 From: Sasha <64744993+r1tsuu@users.noreply.github.com> Date: Thu, 19 Dec 2024 20:20:39 +0200 Subject: [PATCH] perf(db-mongodb): improve performance of all operations, up to 50% faster (#9594) This PR improves speed and memory efficiency across all operations with the Mongoose adapter. ### How? - Removes Mongoose layer from all database calls, instead uses MongoDB directly. (this doesn't remove building mongoose schema since it's still needed for indexes + users in theory can use it) - Replaces deep copying of read results using `JSON.parse(JSON.stringify(data))` with the `transform` `operation: 'read'` function which converts Date's, ObjectID's in relationships / joins to strings. As before, it also handles transformations for write operations. - Faster `hasNearConstraint` for potentially large `where`'s - `traverseFields` now can accept `flattenedFields` which we use in `transform`. Less recursive calls with tabs/rows/collapsible Additional fixes - Uses current transaction for querying nested relationships properties in `buildQuery`, previously it wasn't used which could've led to wrong results - Allows to clear not required point fields with passing `null` from the Local API. Previously it didn't work in both, MongoDB and Postgres Benchmarks using this file https://github.com/payloadcms/payload/blob/chore/db-benchmark/test/_community/int.spec.ts ### Small Dataset Performance | Metric | Before Optimization | After Optimization | Improvement (%) | |---------------------------|---------------------|--------------------|-----------------| | Average FULL (ms) | 1170 | 844 | 27.86% | | `payload.db.create` (ms) | 1413 | 691 | 51.12% | | `payload.db.find` (ms) | 2856 | 2204 | 22.83% | | `payload.db.deleteMany` (ms) | 15206 | 8439 | 44.53% | | `payload.db.updateOne` (ms) | 21444 | 12162 | 43.30% | | `payload.db.findOne` (ms) | 159 | 112 | 29.56% | | `payload.db.deleteOne` (ms) | 3729 | 2578 | 30.89% | | DB small FULL (ms) | 64473 | 46451 | 27.93% | --- ### Medium Dataset Performance | Metric | Before Optimization | After Optimization | Improvement (%) | |---------------------------|---------------------|--------------------|-----------------| | Average FULL (ms) | 9407 | 6210 | 33.99% | | `payload.db.create` (ms) | 10270 | 4321 | 57.93% | | `payload.db.find` (ms) | 20814 | 16036 | 22.93% | | `payload.db.deleteMany` (ms) | 126351 | 61789 | 51.11% | | `payload.db.updateOne` (ms) | 201782 | 99943 | 50.49% | | `payload.db.findOne` (ms) | 1081 | 817 | 24.43% | | `payload.db.deleteOne` (ms) | 28534 | 23363 | 18.12% | | DB medium FULL (ms) | 519518 | 342194 | 34.13% | --- ### Large Dataset Performance | Metric | Before Optimization | After Optimization | Improvement (%) | |---------------------------|---------------------|--------------------|-----------------| | Average FULL (ms) | 26575 | 17509 | 34.14% | | `payload.db.create` (ms) | 29085 | 12196 | 58.08% | | `payload.db.find` (ms) | 58497 | 43838 | 25.04% | | `payload.db.deleteMany` (ms) | 372195 | 173218 | 53.47% | | `payload.db.updateOne` (ms) | 544089 | 288350 | 47.00% | | `payload.db.findOne` (ms) | 3058 | 2197 | 28.14% | | `payload.db.deleteOne` (ms) | 82444 | 64730 | 21.49% | | DB large FULL (ms) | 1461097 | 969714 | 33.62% | --- packages/db-mongodb/src/count.ts | 41 +- .../db-mongodb/src/countGlobalVersions.ts | 41 +- packages/db-mongodb/src/countVersions.ts | 41 +- packages/db-mongodb/src/create.ts | 50 ++- packages/db-mongodb/src/createGlobal.ts | 41 +- .../db-mongodb/src/createGlobalVersion.ts | 70 ++-- packages/db-mongodb/src/createVersion.ts | 74 ++-- packages/db-mongodb/src/deleteMany.ts | 11 +- packages/db-mongodb/src/deleteOne.ts | 37 +- packages/db-mongodb/src/deleteVersions.ts | 5 +- packages/db-mongodb/src/find.ts | 120 ++---- packages/db-mongodb/src/findGlobal.ts | 35 +- packages/db-mongodb/src/findGlobalVersions.ts | 100 ++--- packages/db-mongodb/src/findOne.ts | 53 ++- packages/db-mongodb/src/findVersions.ts | 106 ++--- packages/db-mongodb/src/index.ts | 2 + packages/db-mongodb/src/migrateFresh.ts | 2 - .../migrateRelationshipsV2_V3.ts | 32 +- .../src/queries/buildAndOrConditions.ts | 4 + packages/db-mongodb/src/queries/buildQuery.ts | 12 +- .../src/queries/buildSearchParams.ts | 24 +- .../db-mongodb/src/queries/buildSortParam.ts | 17 +- .../src/queries/getLocalizedSortProperty.ts | 4 +- .../db-mongodb/src/queries/parseParams.ts | 4 + .../src/queries/sanitizeQueryValue.ts | 24 ++ packages/db-mongodb/src/queryDrafts.ts | 125 ++---- packages/db-mongodb/src/updateGlobal.ts | 48 ++- .../db-mongodb/src/updateGlobalVersion.ts | 62 +-- packages/db-mongodb/src/updateOne.ts | 53 +-- packages/db-mongodb/src/updateVersion.ts | 63 +-- .../src/utilities/buildJoinAggregation.ts | 38 +- .../utilities/buildProjectionFromSelect.ts | 23 +- packages/db-mongodb/src/utilities/findMany.ts | 128 ++++++ .../src/utilities/getHasNearConstraint.ts | 27 ++ .../src/utilities/sanitizeInternalFields.ts | 20 - .../src/utilities/sanitizeRelationshipIDs.ts | 156 ------- ...ationshipIDs.spec.ts => transform.spec.ts} | 18 +- .../db-mongodb/src/utilities/transform.ts | 385 ++++++++++++++++++ .../fields/hooks/beforeValidate/promise.ts | 5 + packages/payload/src/fields/validations.ts | 7 +- packages/payload/src/index.ts | 5 +- .../payload/src/utilities/traverseFields.ts | 88 +++- test/fields-relationship/int.spec.ts | 10 +- test/fields/int.spec.ts | 24 ++ test/joins/int.spec.ts | 5 +- test/select/int.spec.ts | 38 +- 46 files changed, 1338 insertions(+), 940 deletions(-) create mode 100644 packages/db-mongodb/src/utilities/findMany.ts create mode 100644 packages/db-mongodb/src/utilities/getHasNearConstraint.ts delete mode 100644 packages/db-mongodb/src/utilities/sanitizeInternalFields.ts delete mode 100644 packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts rename packages/db-mongodb/src/utilities/{sanitizeRelationshipIDs.spec.ts => transform.spec.ts} (93%) create mode 100644 packages/db-mongodb/src/utilities/transform.ts diff --git a/packages/db-mongodb/src/count.ts b/packages/db-mongodb/src/count.ts index 7d313461b..4dd6096c2 100644 --- a/packages/db-mongodb/src/count.ts +++ b/packages/db-mongodb/src/count.ts @@ -1,10 +1,9 @@ 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( @@ -12,41 +11,37 @@ export const count: Count = async function count( { collection, locale, req, where }, ) { const Model = this.collections[collection] - const options: CountOptions = { - session: await getSession(this, req), - } + const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) const query = await Model.buildQuery({ locale, 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.estimatedDocumentCount({ session: options.session }) + result = await Model.collection.estimatedDocumentCount() } else { - result = await Model.countDocuments(query, options) + 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) } return { diff --git a/packages/db-mongodb/src/countGlobalVersions.ts b/packages/db-mongodb/src/countGlobalVersions.ts index 296f2ca07..9ff841cf7 100644 --- a/packages/db-mongodb/src/countGlobalVersions.ts +++ b/packages/db-mongodb/src/countGlobalVersions.ts @@ -1,10 +1,9 @@ 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( @@ -12,41 +11,37 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob { global, locale, req, where }, ) { const Model = this.versions[global] - const options: CountOptions = { - session: await getSession(this, req), - } + const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) const query = await Model.buildQuery({ locale, 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.estimatedDocumentCount({ session: options.session }) + result = await Model.collection.estimatedDocumentCount() } else { - result = await Model.countDocuments(query, options) + const options: CountOptions = { session } + + if (Object.keys(query).length === 0 && this.disableIndexHints !== true) { + // Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding + // a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents, + // which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses + // the correct indexed field + options.hint = { + _id: 1, + } + } + + result = await Model.collection.countDocuments(query, options) } return { diff --git a/packages/db-mongodb/src/countVersions.ts b/packages/db-mongodb/src/countVersions.ts index 0b91f94ac..864b6976e 100644 --- a/packages/db-mongodb/src/countVersions.ts +++ b/packages/db-mongodb/src/countVersions.ts @@ -1,10 +1,9 @@ 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( @@ -12,41 +11,37 @@ export const countVersions: CountVersions = async function countVersions( { collection, locale, req, where }, ) { const Model = this.versions[collection] - const options: CountOptions = { - session: await getSession(this, req), - } + const session = await getSession(this, req) - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) const query = await Model.buildQuery({ locale, 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.estimatedDocumentCount({ session: options.session }) + result = await Model.collection.estimatedDocumentCount() } else { - result = await Model.countDocuments(query, options) + 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) } return { diff --git a/packages/db-mongodb/src/create.ts b/packages/db-mongodb/src/create.ts index 49e549b7e..663b27fb7 100644 --- a/packages/db-mongodb/src/create.ts +++ b/packages/db-mongodb/src/create.ts @@ -1,48 +1,44 @@ -import type { CreateOptions } from 'mongoose' -import type { Create, Document } from 'payload' +import type { Create } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' import { handleError } from './utilities/handleError.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const create: Create = async function create( this: MongooseAdapter, { collection, data, req }, ) { const Model = this.collections[collection] - const options: CreateOptions = { - session: await getSession(this, req), - } + const session = await getSession(this, req) - let doc - - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, - data, - fields: this.payload.collections[collection].config.fields, - }) + const fields = this.payload.collections[collection].config.flattenedFields if (this.payload.collections[collection].customIDType) { - sanitizedData._id = sanitizedData.id + data._id = data.id } + transform({ + adapter: this, + data, + fields, + operation: 'create', + }) + try { - ;[doc] = await Model.create([sanitizedData], options) + const { insertedId } = await Model.collection.insertOne(data, { session }) + data._id = insertedId + + transform({ + adapter: this, + data, + fields, + operation: 'read', + }) + + return data } 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 969eeaed7..973504686 100644 --- a/packages/db-mongodb/src/createGlobal.ts +++ b/packages/db-mongodb/src/createGlobal.ts @@ -1,11 +1,9 @@ -import type { CreateOptions } from 'mongoose' import type { CreateGlobal } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const createGlobal: CreateGlobal = async function createGlobal( this: MongooseAdapter, @@ -13,26 +11,29 @@ export const createGlobal: CreateGlobal = async function createGlobal( ) { const Model = this.globals - const global = sanitizeRelationshipIDs({ - config: this.payload.config, - data: { - globalType: slug, - ...data, - }, - fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields, + const fields = this.payload.config.globals.find( + (globalConfig) => globalConfig.slug === slug, + ).flattenedFields + + transform({ + adapter: this, + data, + fields, + globalSlug: slug, + operation: 'create', }) - const options: CreateOptions = { - session: await getSession(this, req), - } + const session = await getSession(this, req) - let [result] = (await Model.create([global], options)) as any + const { insertedId } = await Model.collection.insertOne(data, { session }) + ;(data as any)._id = insertedId - result = JSON.parse(JSON.stringify(result)) + transform({ + adapter: this, + data, + fields, + operation: 'read', + }) - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) - - return result + return data } diff --git a/packages/db-mongodb/src/createGlobalVersion.ts b/packages/db-mongodb/src/createGlobalVersion.ts index a6fe7ccb5..3d9ea3a55 100644 --- a/packages/db-mongodb/src/createGlobalVersion.ts +++ b/packages/db-mongodb/src/createGlobalVersion.ts @@ -1,11 +1,9 @@ -import type { CreateOptions } from 'mongoose' - -import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload' +import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( this: MongooseAdapter, @@ -22,36 +20,41 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo }, ) { const VersionModel = this.versions[globalSlug] - const options: CreateOptions = { - session: await getSession(this, req), + const session = await getSession(this, req) + + const data = { + autosave, + createdAt, + latest: true, + parent, + publishedLocale, + snapshot, + updatedAt, + version: versionData, } - 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 fields = buildVersionGlobalFields( + this.payload.config, + this.payload.config.globals.find((global) => global.slug === globalSlug), + true, + ) + + transform({ + adapter: this, + data, + fields, + operation: 'create', }) - const [doc] = await VersionModel.create([data], options, req) + const { insertedId } = await VersionModel.collection.insertOne(data, { session }) + ;(data as any)._id = insertedId - await VersionModel.updateMany( + await VersionModel.collection.updateMany( { $and: [ { _id: { - $ne: doc._id, + $ne: insertedId, }, }, { @@ -67,16 +70,15 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo ], }, { $unset: { latest: 1 } }, - options, + { session }, ) - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + transform({ + adapter: this, + data, + fields, + operation: 'read', + }) - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return data as any } diff --git a/packages/db-mongodb/src/createVersion.ts b/packages/db-mongodb/src/createVersion.ts index 3482345e2..b26396b1f 100644 --- a/packages/db-mongodb/src/createVersion.ts +++ b/packages/db-mongodb/src/createVersion.ts @@ -1,12 +1,10 @@ -import type { CreateOptions } from 'mongoose' - import { Types } from 'mongoose' -import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload' +import { buildVersionCollectionFields, type CreateVersion } from 'payload' import type { MongooseAdapter } from './index.js' import { getSession } from './utilities/getSession.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const createVersion: CreateVersion = async function createVersion( this: MongooseAdapter, @@ -23,29 +21,34 @@ export const createVersion: CreateVersion = async function createVersion( }, ) { const VersionModel = this.versions[collectionSlug] - const options: CreateOptions = { - session: await getSession(this, req), + const session = await getSession(this, req) + + const data: any = { + autosave, + createdAt, + latest: true, + parent, + publishedLocale, + snapshot, + updatedAt, + version: versionData, } - 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 fields = buildVersionCollectionFields( + this.payload.config, + this.payload.collections[collectionSlug].config, + true, + ) + + transform({ + adapter: this, + data, + fields, + operation: 'create', }) - const [doc] = await VersionModel.create([data], options, req) + const { insertedId } = await VersionModel.collection.insertOne(data, { session }) + data._id = insertedId const parentQuery = { $or: [ @@ -56,7 +59,7 @@ export const createVersion: CreateVersion = async function createVersion( }, ], } - if (data.parent instanceof Types.ObjectId) { + if ((data.parent as unknown) instanceof Types.ObjectId) { parentQuery.$or.push({ parent: { $eq: data.parent.toString(), @@ -64,12 +67,12 @@ export const createVersion: CreateVersion = async function createVersion( }) } - await VersionModel.updateMany( + await VersionModel.collection.updateMany( { $and: [ { _id: { - $ne: doc._id, + $ne: insertedId, }, }, parentQuery, @@ -80,22 +83,21 @@ export const createVersion: CreateVersion = async function createVersion( }, { updatedAt: { - $lt: new Date(doc.updatedAt), + $lt: new Date(data.updatedAt), }, }, ], }, { $unset: { latest: 1 } }, - options, + { session }, ) - const result: Document = JSON.parse(JSON.stringify(doc)) - const verificationToken = doc._verificationToken + transform({ + adapter: this, + data, + fields, + operation: 'read', + }) - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return data } diff --git a/packages/db-mongodb/src/deleteMany.ts b/packages/db-mongodb/src/deleteMany.ts index cdc5470e9..65a8eb1be 100644 --- a/packages/db-mongodb/src/deleteMany.ts +++ b/packages/db-mongodb/src/deleteMany.ts @@ -1,4 +1,3 @@ -import type { DeleteOptions } from 'mongodb' import type { DeleteMany } from 'payload' import type { MongooseAdapter } from './index.js' @@ -10,14 +9,16 @@ export const deleteMany: DeleteMany = async function deleteMany( { collection, req, where }, ) { const Model = this.collections[collection] - const options: DeleteOptions = { - session: await getSession(this, req), - } + + const session = await getSession(this, req) const query = await Model.buildQuery({ payload: this.payload, + session, where, }) - await Model.deleteMany(query, options) + await Model.collection.deleteMany(query, { + session, + }) } diff --git a/packages/db-mongodb/src/deleteOne.ts b/packages/db-mongodb/src/deleteOne.ts index 3d9bc07f0..9b6783513 100644 --- a/packages/db-mongodb/src/deleteOne.ts +++ b/packages/db-mongodb/src/deleteOne.ts @@ -1,38 +1,41 @@ -import type { QueryOptions } from 'mongoose' -import type { DeleteOne, Document } from 'payload' +import type { DeleteOne } from 'payload' import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const deleteOne: DeleteOne = async function deleteOne( this: MongooseAdapter, { collection, req, select, where }, ) { const Model = this.collections[collection] - const options: QueryOptions = { - projection: buildProjectionFromSelect({ - adapter: this, - fields: this.payload.collections[collection].config.flattenedFields, - select, - }), - session: await getSession(this, req), - } + const session = await getSession(this, req) const query = await Model.buildQuery({ payload: this.payload, + session, where, }) - const doc = await Model.findOneAndDelete(query, options).lean() + const fields = this.payload.collections[collection].config.flattenedFields - let result: Document = JSON.parse(JSON.stringify(doc)) + const doc = await Model.collection.findOneAndDelete(query, { + projection: buildProjectionFromSelect({ + adapter: this, + fields, + select, + }), + session, + }) - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + transform({ + adapter: this, + data: doc, + fields, + operation: 'read', + }) - return result + return doc } diff --git a/packages/db-mongodb/src/deleteVersions.ts b/packages/db-mongodb/src/deleteVersions.ts index 5ee111f3a..4aff14392 100644 --- a/packages/db-mongodb/src/deleteVersions.ts +++ b/packages/db-mongodb/src/deleteVersions.ts @@ -15,8 +15,11 @@ export const deleteVersions: DeleteVersions = async function deleteVersions( const query = await VersionsModel.buildQuery({ locale, payload: this.payload, + session, where, }) - await VersionsModel.deleteMany(query, { session }) + await VersionsModel.collection.deleteMany(query, { + session, + }) } diff --git a/packages/db-mongodb/src/find.ts b/packages/db-mongodb/src/find.ts index d953c3484..684197092 100644 --- a/packages/db-mongodb/src/find.ts +++ b/packages/db-mongodb/src/find.ts @@ -1,15 +1,15 @@ -import type { PaginateOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' 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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const find: Find = async function find( this: MongooseAdapter, @@ -20,7 +20,6 @@ export const find: Find = async function find( locale, page, pagination, - projection, req, select, sort: sortArg, @@ -29,21 +28,17 @@ 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) - let hasNearConstraint = false + const hasNearConstraint = getHasNearConstraint(where) - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const fields = collectionConfig.flattenedFields let sort if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, - fields: collectionConfig.flattenedFields, + fields, locale, sort: sortArg || collectionConfig.defaultSort, timestamps: true, @@ -53,90 +48,51 @@ 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, - } - if (select) { - paginationOptions.projection = buildProjectionFromSelect({ - adapter: this, - fields: collectionConfig.flattenedFields, - select, - }) - } + const projection = buildProjectionFromSelect({ + adapter: this, + fields, + select, + }) - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined - 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({ + const joinAgreggation = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, locale, - query, + session, }) - // build join aggregation - if (aggregate) { - result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions) - } else { - result = await Model.paginate(query, paginationOptions) - } - const docs = JSON.parse(JSON.stringify(result.docs)) + const result = await findMany({ + adapter: this, + collation, + collection: Model.collection, + joinAgreggation, + limit, + page, + pagination, + projection, + query, + session, + sort, + useEstimatedCount, + }) - return { - ...result, - docs: docs.map((doc) => { - doc.id = doc._id - return sanitizeInternalFields(doc) - }), - } + transform({ adapter: this, data: result.docs, fields, operation: 'read' }) + + return result } diff --git a/packages/db-mongodb/src/findGlobal.ts b/packages/db-mongodb/src/findGlobal.ts index ec51dba5b..a57d5f90a 100644 --- a/packages/db-mongodb/src/findGlobal.ts +++ b/packages/db-mongodb/src/findGlobal.ts @@ -1,4 +1,3 @@ -import type { QueryOptions } from 'mongoose' import type { FindGlobal } from 'payload' import { combineQueries } from 'payload' @@ -7,42 +6,40 @@ import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const findGlobal: FindGlobal = async function findGlobal( this: MongooseAdapter, { slug, locale, req, select, where }, ) { const Model = this.globals - 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 session = await getSession(this, req) const query = await Model.buildQuery({ globalSlug: slug, locale, payload: this.payload, + session, where: combineQueries({ globalType: { equals: slug } }, where), }) - let doc = (await Model.findOne(query, {}, options)) as any + const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields + + const doc = await Model.collection.findOne(query, { + projection: buildProjectionFromSelect({ + adapter: this, + fields, + select, + }), + session: await getSession(this, req), + }) if (!doc) { return null } - if (doc._id) { - doc.id = doc._id - delete doc._id - } - doc = JSON.parse(JSON.stringify(doc)) - doc = sanitizeInternalFields(doc) + transform({ adapter: this, data: doc, fields, operation: 'read' }) - return doc + return doc as any } diff --git a/packages/db-mongodb/src/findGlobalVersions.ts b/packages/db-mongodb/src/findGlobalVersions.ts index 3166d8941..51c677833 100644 --- a/packages/db-mongodb/src/findGlobalVersions.ts +++ b/packages/db-mongodb/src/findGlobalVersions.ts @@ -1,14 +1,16 @@ -import type { PaginateOptions, QueryOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { FindGlobalVersions } from 'payload' -import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload' +import { buildVersionGlobalFields } 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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( this: MongooseAdapter, @@ -21,19 +23,7 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV true, ) - 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')) - } + const hasNearConstraint = getHasNearConstraint(where) let sort if (!hasNearConstraint) { @@ -46,69 +36,49 @@ 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 paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, + + const projection = buildProjectionFromSelect({ adapter: this, fields: versionFields, select }) + + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined + + const result = await findMany({ + adapter: this, + collation, + collection: Model.collection, limit, - options, page, pagination, - projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }), + projection, + query, + session, + skip, sort, useEstimatedCount, - } + }) - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } + transform({ + adapter: this, + data: result.docs, + fields: versionFields, + operation: 'read', + }) - 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) - }), - } + return result } diff --git a/packages/db-mongodb/src/findOne.ts b/packages/db-mongodb/src/findOne.ts index 10d13b6fc..5ab99aa3e 100644 --- a/packages/db-mongodb/src/findOne.ts +++ b/packages/db-mongodb/src/findOne.ts @@ -1,12 +1,11 @@ -import type { AggregateOptions, QueryOptions } from 'mongoose' -import type { Document, FindOne } from 'payload' +import type { 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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const findOne: FindOne = async function findOne( this: MongooseAdapter, @@ -14,52 +13,64 @@ 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: collectionConfig.flattenedFields, + fields, select, }) - const aggregate = await buildJoinAggregation({ + const joinAggregation = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, - limit: 1, locale, projection, - query, + session, }) let doc - if (aggregate) { - ;[doc] = await Model.aggregate(aggregate, { session }) + 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() } else { - ;(options as Record).projection = projection - doc = await Model.findOne(query, {}, options) + doc = await Model.collection.findOne(query, { projection, session }) } if (!doc) { return null } - let result: Document = JSON.parse(JSON.stringify(doc)) + transform({ + adapter: this, + data: doc, + fields, + operation: 'read', + }) - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) - - return result + return doc } diff --git a/packages/db-mongodb/src/findVersions.ts b/packages/db-mongodb/src/findVersions.ts index 19abd38d4..330ad814a 100644 --- a/packages/db-mongodb/src/findVersions.ts +++ b/packages/db-mongodb/src/findVersions.ts @@ -1,14 +1,16 @@ -import type { PaginateOptions, QueryOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { FindVersions } from 'payload' -import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload' +import { buildVersionCollectionFields } 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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const findVersions: FindVersions = async function findVersions( this: MongooseAdapter, @@ -16,19 +18,10 @@ 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, - } - let hasNearConstraint = false - - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } + const hasNearConstraint = getHasNearConstraint(where) let sort if (!hasNearConstraint) { @@ -44,69 +37,48 @@ 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 paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, + + const projection = buildProjectionFromSelect({ + adapter: this, + fields: versionFields, + select, + }) + + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined + + const result = await findMany({ + adapter: this, + collation, + collection: Model.collection, limit, - options, page, pagination, - projection: buildProjectionFromSelect({ - adapter: this, - fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true), - select, - }), + projection, + query, + session, + skip, sort, useEstimatedCount, - } + }) - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } + transform({ + adapter: this, + data: result.docs, + fields: versionFields, + operation: 'read', + }) - 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) - }), - } + return result } diff --git a/packages/db-mongodb/src/index.ts b/packages/db-mongodb/src/index.ts index 911677c14..d4d3639b0 100644 --- a/packages/db-mongodb/src/index.ts +++ b/packages/db-mongodb/src/index.ts @@ -59,6 +59,8 @@ 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 4db2e76c4..cd9e53862 100644 --- a/packages/db-mongodb/src/migrateFresh.ts +++ b/packages/db-mongodb/src/migrateFresh.ts @@ -1,5 +1,3 @@ -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 5d5e2e759..e363a5fe3 100644 --- a/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts +++ b/packages/db-mongodb/src/predefinedMigrations/migrateRelationshipsV2_V3.ts @@ -1,23 +1,23 @@ import type { ClientSession, Model } from 'mongoose' -import type { Field, PayloadRequest, SanitizedConfig } from 'payload' +import type { Field, FlattenedField, PayloadRequest } from 'payload' import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' import type { MongooseAdapter } from '../index.js' import { getSession } from '../utilities/getSession.js' -import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js' +import { transform } from '../utilities/transform.js' const migrateModelWithBatching = async ({ + adapter, batchSize, - config, fields, Model, session, }: { + adapter: MongooseAdapter batchSize: number - config: SanitizedConfig - fields: Field[] + fields: FlattenedField[] Model: Model session: ClientSession }): Promise => { @@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({ } for (const doc of docs) { - sanitizeRelationshipIDs({ config, data: doc, fields }) + transform({ adapter, data: doc, fields, operation: 'update', validateRelationships: false }) } 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, - config, - fields: collection.fields, + fields: collection.flattenedFields, Model: db.collections[collection.slug], session, }) @@ -128,9 +128,9 @@ export async function migrateRelationshipsV2_V3({ payload.logger.info(`Migrating collection versions "${collection.slug}"`) await migrateModelWithBatching({ + adapter: db, batchSize, - config, - fields: buildVersionCollectionFields(config, collection), + fields: buildVersionCollectionFields(config, collection, true), Model: db.versions[collection.slug], session, }) @@ -156,7 +156,13 @@ export async function migrateRelationshipsV2_V3({ // in case if the global doesn't exist in the database yet (not saved) if (doc) { - sanitizeRelationshipIDs({ config, data: doc, fields: global.fields }) + transform({ + adapter: db, + data: doc, + fields: global.flattenedFields, + operation: 'update', + validateRelationships: false, + }) await GlobalsModel.collection.updateOne( { @@ -173,9 +179,9 @@ export async function migrateRelationshipsV2_V3({ payload.logger.info(`Migrating global versions "${global.slug}"`) await migrateModelWithBatching({ + adapter: db, batchSize, - config, - fields: buildVersionGlobalFields(config, global), + fields: buildVersionGlobalFields(config, global, true), Model: db.versions[global.slug], session, }) diff --git a/packages/db-mongodb/src/queries/buildAndOrConditions.ts b/packages/db-mongodb/src/queries/buildAndOrConditions.ts index 9ac052967..e513e1186 100644 --- a/packages/db-mongodb/src/queries/buildAndOrConditions.ts +++ b/packages/db-mongodb/src/queries/buildAndOrConditions.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { FlattenedField, Payload, Where } from 'payload' import { parseParams } from './parseParams.js' @@ -8,6 +9,7 @@ export async function buildAndOrConditions({ globalSlug, locale, payload, + session, where, }: { collectionSlug?: string @@ -15,6 +17,7 @@ export async function buildAndOrConditions({ globalSlug?: string locale?: string payload: Payload + session?: ClientSession where: Where[] }): Promise[]> { const completedConditions = [] @@ -30,6 +33,7 @@ export async function buildAndOrConditions({ globalSlug, locale, payload, + session, where: condition, }) if (Object.keys(result).length > 0) { diff --git a/packages/db-mongodb/src/queries/buildQuery.ts b/packages/db-mongodb/src/queries/buildQuery.ts index 64cde6af7..7dc3d5fec 100644 --- a/packages/db-mongodb/src/queries/buildQuery.ts +++ b/packages/db-mongodb/src/queries/buildQuery.ts @@ -1,7 +1,6 @@ +import type { ClientSession } from 'mongodb' import type { FlattenedField, Payload, Where } from 'payload' -import { QueryError } from 'payload' - import { parseParams } from './parseParams.js' type GetBuildQueryPluginArgs = { @@ -13,6 +12,7 @@ export type BuildQueryArgs = { globalSlug?: string locale?: string payload: Payload + session?: ClientSession where: Where } @@ -28,6 +28,7 @@ export const getBuildQueryPlugin = ({ globalSlug, locale, payload, + session, where, }: BuildQueryArgs): Promise> { let fields = versionsFields @@ -41,20 +42,17 @@ export const getBuildQueryPlugin = ({ fields = collectionConfig.flattenedFields } } - const errors = [] + const result = await parseParams({ collectionSlug, fields, globalSlug, locale, payload, + session, where, }) - if (errors.length > 0) { - throw new QueryError(errors) - } - return result } modifiedSchema.statics.buildQuery = buildQuery diff --git a/packages/db-mongodb/src/queries/buildSearchParams.ts b/packages/db-mongodb/src/queries/buildSearchParams.ts index 3f0f97fd6..4efbd2469 100644 --- a/packages/db-mongodb/src/queries/buildSearchParams.ts +++ b/packages/db-mongodb/src/queries/buildSearchParams.ts @@ -1,3 +1,4 @@ +import type { ClientSession, FindOptions } from 'mongodb' import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload' import { Types } from 'mongoose' @@ -15,9 +16,11 @@ type SearchParam = { value?: unknown } -const subQueryOptions = { - lean: true, +const subQueryOptions: FindOptions = { limit: 50, + projection: { + _id: true, + }, } /** @@ -31,6 +34,7 @@ export async function buildSearchParam({ locale, operator, payload, + session, val, }: { collectionSlug?: string @@ -40,6 +44,7 @@ export async function buildSearchParam({ locale?: string operator: string payload: Payload + session?: ClientSession val: unknown }): Promise { // Replace GraphQL nested field double underscore formatting @@ -134,17 +139,14 @@ export async function buildSearchParam({ }, }) - const result = await SubModel.find(subQuery, subQueryOptions) + const result = await SubModel.collection + .find(subQuery, { session, ...subQueryOptions }) + .toArray() const $in: unknown[] = [] result.forEach((doc) => { - const stringID = doc._id.toString() - $in.push(stringID) - - if (Types.ObjectId.isValid(stringID)) { - $in.push(doc._id) - } + $in.push(doc._id) }) if (pathsToQuery.length === 1) { @@ -162,7 +164,9 @@ export async function buildSearchParam({ } const subQuery = priorQueryResult.value - const result = await SubModel.find(subQuery, subQueryOptions) + const result = await SubModel.collection + .find(subQuery, { session, ...subQueryOptions }) + .toArray() const $in = result.map((doc) => doc._id) diff --git a/packages/db-mongodb/src/queries/buildSortParam.ts b/packages/db-mongodb/src/queries/buildSortParam.ts index 6f76cbffe..09f7cad6d 100644 --- a/packages/db-mongodb/src/queries/buildSortParam.ts +++ b/packages/db-mongodb/src/queries/buildSortParam.ts @@ -11,20 +11,13 @@ type Args = { timestamps: boolean } -export type SortArgs = { - direction: SortDirection - property: string -}[] - -export type SortDirection = 'asc' | 'desc' - export const buildSortParam = ({ config, fields, locale, sort, timestamps, -}: Args): PaginateOptions['sort'] => { +}: Args): Record => { if (!sort) { if (timestamps) { sort = '-createdAt' @@ -37,15 +30,15 @@ export const buildSortParam = ({ sort = [sort] } - const sorting = sort.reduce((acc, item) => { + const sorting = sort.reduce>((acc, item) => { let sortProperty: string - let sortDirection: SortDirection + let sortDirection: -1 | 1 if (item.indexOf('-') === 0) { sortProperty = item.substring(1) - sortDirection = 'desc' + sortDirection = -1 } else { sortProperty = item - sortDirection = 'asc' + sortDirection = 1 } if (sortProperty === 'id') { acc['_id'] = sortDirection diff --git a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts index f7aec4bbd..59a7d3d99 100644 --- a/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts +++ b/packages/db-mongodb/src/queries/getLocalizedSortProperty.ts @@ -1,6 +1,6 @@ import type { FlattenedField, SanitizedConfig } from 'payload' -import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared' +import { fieldAffectsData } from 'payload/shared' type Args = { config: SanitizedConfig @@ -33,7 +33,7 @@ export const getLocalizedSortProperty = ({ (field) => fieldAffectsData(field) && field.name === firstSegment, ) - if (matchedField && !fieldIsPresentationalOnly(matchedField)) { + if (matchedField) { let nextFields: FlattenedField[] const remainingSegments = [...segments] let localizedSegment = matchedField.name diff --git a/packages/db-mongodb/src/queries/parseParams.ts b/packages/db-mongodb/src/queries/parseParams.ts index 18dae08d2..e8b4cb019 100644 --- a/packages/db-mongodb/src/queries/parseParams.ts +++ b/packages/db-mongodb/src/queries/parseParams.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { FilterQuery } from 'mongoose' import type { FlattenedField, Operator, Payload, Where } from 'payload' @@ -13,6 +14,7 @@ export async function parseParams({ globalSlug, locale, payload, + session, where, }: { collectionSlug?: string @@ -20,6 +22,7 @@ export async function parseParams({ globalSlug?: string locale: string payload: Payload + session?: ClientSession where: Where }): Promise> { let result = {} as FilterQuery @@ -62,6 +65,7 @@ export async function parseParams({ locale, operator, payload, + session, val: pathOperators[operator], }) diff --git a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts index ce8df3999..faf123386 100644 --- a/packages/db-mongodb/src/queries/sanitizeQueryValue.ts +++ b/packages/db-mongodb/src/queries/sanitizeQueryValue.ts @@ -37,6 +37,22 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => { } } +const sanitizeCoordinates = (coordinates: unknown[]): unknown[] => { + const result: unknown[] = [] + + for (const value of coordinates) { + if (typeof value === 'string') { + result.push(Number(value)) + } else if (Array.isArray(value)) { + result.push(sanitizeCoordinates(value)) + } else { + result.push(value) + } + } + + return result +} + // returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships const getFieldFromSegments = ({ field, @@ -359,6 +375,14 @@ export const sanitizeQueryValue = ({ } if (operator === 'within' || operator === 'intersects') { + if ( + formattedValue && + typeof formattedValue === 'object' && + Array.isArray(formattedValue.coordinates) + ) { + formattedValue.coordinates = sanitizeCoordinates(formattedValue.coordinates) + } + formattedValue = { $geometry: formattedValue, } diff --git a/packages/db-mongodb/src/queryDrafts.ts b/packages/db-mongodb/src/queryDrafts.ts index 3ea8cb34d..164a57672 100644 --- a/packages/db-mongodb/src/queryDrafts.ts +++ b/packages/db-mongodb/src/queryDrafts.ts @@ -1,15 +1,17 @@ -import type { PaginateOptions, QueryOptions } from 'mongoose' +import type { CollationOptions } from 'mongodb' import type { QueryDrafts } from 'payload' -import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload' +import { buildVersionCollectionFields, combineQueries } 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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' +import { transform } from './utilities/transform.js' export const queryDrafts: QueryDrafts = async function queryDrafts( this: MongooseAdapter, @@ -17,18 +19,11 @@ export const queryDrafts: QueryDrafts = async function queryDrafts( ) { const VersionModel = this.versions[collection] const collectionConfig = this.payload.collections[collection].config - const options: QueryOptions = { - session: await getSession(this, req), - } + const session = await getSession(this, req) - let hasNearConstraint + const hasNearConstraint = getHasNearConstraint(where) let sort - if (where) { - const constraints = flattenWhereToOperators(where) - hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near')) - } - if (!hasNearConstraint) { sort = buildSortParam({ config: this.payload.config, @@ -44,95 +39,65 @@ 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: buildVersionCollectionFields(this.payload.config, collectionConfig, true), + fields: versionFields, select, }) // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. const useEstimatedCount = hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0 - const paginationOptions: PaginateOptions = { - lean: true, - leanWithId: true, - options, - page, - pagination, - projection, - sort, - useEstimatedCount, - } - if (this.collation) { - const defaultLocale = 'en' - paginationOptions.collation = { - locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale, - ...this.collation, - } - } + const collation: CollationOptions | undefined = this.collation + ? { + locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', + ...this.collation, + } + : undefined - 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({ + const joinAgreggation = await buildJoinAggregation({ adapter: this, collection, collectionConfig, joins, locale, projection, - query: versionQuery, + session, versions: true, }) - // build join aggregation - if (aggregate) { - result = await VersionModel.aggregatePaginate( - VersionModel.aggregate(aggregate), - paginationOptions, - ) - } else { - result = await VersionModel.paginate(versionQuery, paginationOptions) + 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 } - 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) - }), - } + return result } diff --git a/packages/db-mongodb/src/updateGlobal.ts b/packages/db-mongodb/src/updateGlobal.ts index 239fff5fd..ac462c85e 100644 --- a/packages/db-mongodb/src/updateGlobal.ts +++ b/packages/db-mongodb/src/updateGlobal.ts @@ -1,47 +1,45 @@ -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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const updateGlobal: UpdateGlobal = async function updateGlobal( this: MongooseAdapter, { slug, data, options: optionsArgs = {}, req, select }, ) { const Model = this.globals - const fields = this.payload.config.globals.find((global) => global.slug === slug).fields + const fields = this.payload.config.globals.find((global) => global.slug === slug).flattenedFields - 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), - } + const session = await getSession(this, req) - let result - - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + adapter: this, data, fields, + operation: 'update', + timestamps: optionsArgs.timestamps !== false, }) - result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options) + const result: any = await Model.collection.findOneAndUpdate( + { globalType: slug }, + { $set: data }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), + returnDocument: 'after', + session, + }, + ) - result = JSON.parse(JSON.stringify(result)) - - // custom id type reset - result.id = result._id - result = sanitizeInternalFields(result) + transform({ + adapter: this, + data: result, + fields, + operation: 'read', + }) return result } diff --git a/packages/db-mongodb/src/updateGlobalVersion.ts b/packages/db-mongodb/src/updateGlobalVersion.ts index af1453840..42eaf6bf4 100644 --- a/packages/db-mongodb/src/updateGlobalVersion.ts +++ b/packages/db-mongodb/src/updateGlobalVersion.ts @@ -1,12 +1,10 @@ -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 { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export async function updateGlobalVersion( this: MongooseAdapter, @@ -23,44 +21,50 @@ 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 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 session = await getSession(this, req) const query = await VersionModel.buildQuery({ locale, payload: this.payload, + session, where: whereToUse, }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + adapter: this, data: versionData, fields, + operation: 'update', + timestamps: optionsArgs.timestamps !== false, }) - const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) + const doc: any = await VersionModel.collection.findOneAndUpdate( + query, + { $set: versionData }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ + adapter: this, + fields, + select, + }), + returnDocument: 'after', + session, + }, + ) - const result = JSON.parse(JSON.stringify(doc)) + transform({ + adapter: this, + data: doc, + fields, + operation: 'read', + }) - const verificationToken = doc._verificationToken - - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return doc } diff --git a/packages/db-mongodb/src/updateOne.ts b/packages/db-mongodb/src/updateOne.ts index 555d46925..d78c5dba1 100644 --- a/packages/db-mongodb/src/updateOne.ts +++ b/packages/db-mongodb/src/updateOne.ts @@ -1,4 +1,3 @@ -import type { QueryOptions } from 'mongoose' import type { UpdateOne } from 'payload' import type { MongooseAdapter } from './index.js' @@ -6,8 +5,7 @@ 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 { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const updateOne: UpdateOne = async function updateOne( this: MongooseAdapter, @@ -15,42 +13,45 @@ export const updateOne: UpdateOne = async function updateOne( ) { const where = id ? { id: { equals: id } } : whereArg const Model = this.collections[collection] - const fields = this.payload.collections[collection].config.fields - const options: QueryOptions = { - ...optionsArgs, - lean: true, - new: true, - projection: buildProjectionFromSelect({ - adapter: this, - fields: this.payload.collections[collection].config.flattenedFields, - select, - }), - session: await getSession(this, req), - } + const fields = this.payload.collections[collection].config.flattenedFields + + const session = await getSession(this, req) const query = await Model.buildQuery({ locale, payload: this.payload, + session, where, }) - let result - - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + adapter: this, data, fields, + operation: 'update', + timestamps: optionsArgs.timestamps !== false, }) try { - result = await Model.findOneAndUpdate(query, sanitizedData, options) + const result = await Model.collection.findOneAndUpdate( + query, + { $set: data }, + { + ...optionsArgs, + projection: buildProjectionFromSelect({ adapter: this, fields, select }), + returnDocument: 'after', + session, + }, + ) + + transform({ + adapter: this, + data: result, + fields, + operation: 'read', + }) + return result } catch (error) { handleError({ collection, error, req }) } - - result = JSON.parse(JSON.stringify(result)) - result.id = result._id - result = sanitizeInternalFields(result) - - return result } diff --git a/packages/db-mongodb/src/updateVersion.ts b/packages/db-mongodb/src/updateVersion.ts index 9514e7cb7..4f3bb3242 100644 --- a/packages/db-mongodb/src/updateVersion.ts +++ b/packages/db-mongodb/src/updateVersion.ts @@ -1,12 +1,10 @@ -import type { QueryOptions } from 'mongoose' - import { buildVersionCollectionFields, type UpdateVersion } from 'payload' import type { MongooseAdapter } from './index.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { getSession } from './utilities/getSession.js' -import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js' +import { transform } from './utilities/transform.js' export const updateVersion: UpdateVersion = async function updateVersion( this: MongooseAdapter, @@ -17,46 +15,51 @@ export const updateVersion: UpdateVersion = async function updateVersion( const fields = buildVersionCollectionFields( this.payload.config, this.payload.collections[collection].config, + true, ) - 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 session = await getSession(this, req) const query = await VersionModel.buildQuery({ locale, payload: this.payload, + session, where: whereToUse, }) - const sanitizedData = sanitizeRelationshipIDs({ - config: this.payload.config, + transform({ + adapter: this, data: versionData, fields, + operation: 'update', + timestamps: optionsArgs.timestamps !== false, }) - const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options) + 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 result = JSON.parse(JSON.stringify(doc)) + transform({ + adapter: this, + data: doc, + fields, + operation: 'read', + }) - const verificationToken = doc._verificationToken - - // custom id type reset - result.id = result._id - if (verificationToken) { - result._verificationToken = verificationToken - } - return result + return doc as any } diff --git a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts index 41d5ad8f5..7bb5dd02d 100644 --- a/packages/db-mongodb/src/utilities/buildJoinAggregation.ts +++ b/packages/db-mongodb/src/utilities/buildJoinAggregation.ts @@ -1,3 +1,4 @@ +import type { ClientSession } from 'mongodb' import type { PipelineStage } from 'mongoose' import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload' @@ -10,12 +11,9 @@ type BuildJoinAggregationArgs = { collection: CollectionSlug collectionConfig: SanitizedCollectionConfig joins: JoinQuery - // the number of docs to get at the top collection level - limit?: number locale: string projection?: Record - // the where clause for the top collection - query?: Where + session?: ClientSession /** whether the query is from drafts */ versions?: boolean } @@ -25,10 +23,9 @@ export const buildJoinAggregation = async ({ collection, collectionConfig, joins, - limit, locale, projection, - query, + session, versions, }: BuildJoinAggregationArgs): Promise => { if (Object.keys(collectionConfig.joins).length === 0 || joins === false) { @@ -36,23 +33,7 @@ export const buildJoinAggregation = async ({ } const joinConfig = adapter.payload.collections[collection].config.joins - const aggregate: PipelineStage[] = [ - { - $sort: { createdAt: -1 }, - }, - ] - - if (query) { - aggregate.push({ - $match: query, - }) - } - - if (limit) { - aggregate.push({ - $limit: limit, - }) - } + const aggregate: PipelineStage[] = [] for (const slug of Object.keys(joinConfig)) { for (const join of joinConfig[slug]) { @@ -72,26 +53,25 @@ 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: { [sortProperty]: sortDirection }, + $sort, }, ] @@ -184,8 +164,8 @@ export const buildJoinAggregation = async ({ } } - if (projection) { - aggregate.push({ $project: projection }) + if (!aggregate.length) { + return } return aggregate diff --git a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts index 51215d7c6..e506c7da8 100644 --- a/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts +++ b/packages/db-mongodb/src/utilities/buildProjectionFromSelect.ts @@ -1,4 +1,4 @@ -import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' +import type { Field, FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared' @@ -29,6 +29,11 @@ const addFieldToProjection = ({ } } +const blockTypeField: Field = { + name: 'blockType', + type: 'text', +} + const traverseFields = ({ adapter, databaseSchemaPath = '', @@ -128,6 +133,14 @@ const traverseFields = ({ (selectMode === 'include' && blocksSelect[block.slug] === true) || (selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined') ) { + addFieldToProjection({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + field: blockTypeField, + projection, + withinLocalizedField: fieldWithinLocalizedField, + }) + traverseFields({ adapter, databaseSchemaPath: fieldDatabaseSchemaPath, @@ -153,7 +166,13 @@ const traverseFields = ({ if (blockSelectMode === 'include') { blocksSelect[block.slug]['id'] = true - blocksSelect[block.slug]['blockType'] = true + addFieldToProjection({ + adapter, + databaseSchemaPath: fieldDatabaseSchemaPath, + field: blockTypeField, + projection, + withinLocalizedField: fieldWithinLocalizedField, + }) } traverseFields({ diff --git a/packages/db-mongodb/src/utilities/findMany.ts b/packages/db-mongodb/src/utilities/findMany.ts new file mode 100644 index 000000000..675cb7053 --- /dev/null +++ b/packages/db-mongodb/src/utilities/findMany.ts @@ -0,0 +1,128 @@ +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 new file mode 100644 index 000000000..861129ec3 --- /dev/null +++ b/packages/db-mongodb/src/utilities/getHasNearConstraint.ts @@ -0,0 +1,27 @@ +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 deleted file mode 100644 index 14ab00da6..000000000 --- a/packages/db-mongodb/src/utilities/sanitizeInternalFields.ts +++ /dev/null @@ -1,20 +0,0 @@ -const internalFields = ['__v'] - -export const sanitizeInternalFields = >(incomingDoc: T): T => - Object.entries(incomingDoc).reduce((newDoc, [key, val]): T => { - if (key === '_id') { - return { - ...newDoc, - id: val, - } - } - - if (internalFields.indexOf(key) > -1) { - return newDoc - } - - return { - ...newDoc, - [key]: val, - } - }, {} as T) diff --git a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts b/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts deleted file mode 100644 index bdecaa406..000000000 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.ts +++ /dev/null @@ -1,156 +0,0 @@ -import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload' - -import { Types } from 'mongoose' -import { APIError, traverseFields } from 'payload' -import { fieldAffectsData } from 'payload/shared' - -type Args = { - config: SanitizedConfig - data: Record - fields: Field[] -} - -interface RelationObject { - relationTo: string - value: number | string -} - -function isValidRelationObject(value: unknown): value is RelationObject { - return typeof value === 'object' && value !== null && 'relationTo' in value && 'value' in value -} - -const convertValue = ({ - relatedCollection, - value, -}: { - relatedCollection: CollectionConfig - value: number | string -}): number | string | Types.ObjectId => { - const customIDField = relatedCollection.fields.find( - (field) => fieldAffectsData(field) && field.name === 'id', - ) - - if (customIDField) { - return value - } - - try { - return new Types.ObjectId(value) - } catch { - return value - } -} - -const sanitizeRelationship = ({ config, field, locale, ref, value }) => { - let relatedCollection: CollectionConfig | undefined - let result = value - - const hasManyRelations = typeof field.relationTo !== 'string' - - if (!hasManyRelations) { - relatedCollection = config.collections?.find(({ slug }) => slug === field.relationTo) - } - - if (Array.isArray(value)) { - result = value.map((val) => { - // Handle has many - if (relatedCollection && val && (typeof val === 'string' || typeof val === 'number')) { - return convertValue({ - relatedCollection, - value: val, - }) - } - - // Handle has many - polymorphic - if (isValidRelationObject(val)) { - const relatedCollectionForSingleValue = config.collections?.find( - ({ slug }) => slug === val.relationTo, - ) - - if (relatedCollectionForSingleValue) { - return { - relationTo: val.relationTo, - value: convertValue({ - relatedCollection: relatedCollectionForSingleValue, - value: val.value, - }), - } - } - } - - return val - }) - } - - // Handle has one - polymorphic - if (isValidRelationObject(value)) { - relatedCollection = config.collections?.find(({ slug }) => slug === value.relationTo) - - if (relatedCollection) { - result = { - relationTo: value.relationTo, - value: convertValue({ relatedCollection, value: value.value }), - } - } - } - - // Handle has one - if (relatedCollection && value && (typeof value === 'string' || typeof value === 'number')) { - result = convertValue({ - relatedCollection, - value, - }) - } - if (locale) { - ref[locale] = result - } else { - ref[field.name] = result - } -} - -export const sanitizeRelationshipIDs = ({ - config, - data, - fields, -}: Args): Record => { - const sanitize: TraverseFieldsCallback = ({ field, ref }) => { - if (!ref || typeof ref !== 'object') { - return - } - - if (field.type === 'relationship' || field.type === 'upload') { - if (!ref[field.name]) { - return - } - - // handle localized relationships - if (config.localization && field.localized) { - const locales = config.localization.locales - const fieldRef = ref[field.name] - if (typeof fieldRef !== 'object') { - return - } - - for (const { code } of locales) { - const value = ref[field.name][code] - if (value) { - sanitizeRelationship({ config, field, locale: code, ref: fieldRef, value }) - } - } - } else { - // handle non-localized relationships - sanitizeRelationship({ - config, - field, - locale: undefined, - ref, - value: ref[field.name], - }) - } - } - } - - traverseFields({ callback: sanitize, fields, fillEmpty: false, ref: data }) - - return data -} diff --git a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts b/packages/db-mongodb/src/utilities/transform.spec.ts similarity index 93% rename from packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts rename to packages/db-mongodb/src/utilities/transform.spec.ts index af95ea2a3..f68b691e2 100644 --- a/packages/db-mongodb/src/utilities/sanitizeRelationshipIDs.spec.ts +++ b/packages/db-mongodb/src/utilities/transform.spec.ts @@ -1,8 +1,9 @@ -import type { Field, SanitizedConfig } from 'payload' +import { flattenAllFields, type Field, type SanitizedConfig } from 'payload' import { Types } from 'mongoose' -import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js' +import { transform } from './transform.js' +import { MongooseAdapter } from '..' const flattenRelationshipValues = (obj: Record, prefix = ''): Record => { return Object.keys(obj).reduce( @@ -271,8 +272,8 @@ const relsData = { }, } -describe('sanitizeRelationshipIDs', () => { - it('should sanitize relationships', () => { +describe('transform', () => { + it('should sanitize relationships with transform write', () => { const data = { ...relsData, array: [ @@ -348,12 +349,19 @@ describe('sanitizeRelationshipIDs', () => { } const flattenValuesBefore = Object.values(flattenRelationshipValues(data)) - sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields }) + const mockAdapter = { payload: { config } } as MongooseAdapter + + const fields = flattenAllFields({ fields: config.collections[0].fields }) + + transform({ type: 'write', adapter: mockAdapter, data, fields }) + const flattenValuesAfter = Object.values(flattenRelationshipValues(data)) flattenValuesAfter.forEach((value, i) => { expect(value).toBeInstanceOf(Types.ObjectId) expect(flattenValuesBefore[i]).toBe(value.toHexString()) }) + + transform({ type: 'read', adapter: mockAdapter, data, fields }) }) }) diff --git a/packages/db-mongodb/src/utilities/transform.ts b/packages/db-mongodb/src/utilities/transform.ts new file mode 100644 index 000000000..aa19c846d --- /dev/null +++ b/packages/db-mongodb/src/utilities/transform.ts @@ -0,0 +1,385 @@ +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 dc094f04f..b873a3882 100644 --- a/packages/payload/src/fields/hooks/beforeValidate/promise.ts +++ b/packages/payload/src/fields/hooks/beforeValidate/promise.ts @@ -126,6 +126,11 @@ export const promise = async ({ case 'point': { if (Array.isArray(siblingData[field.name])) { + if ((siblingData[field.name] as string[]).some((val) => val === null || val === '')) { + siblingData[field.name] = null + break + } + siblingData[field.name] = (siblingData[field.name] as string[]).map((coordinate, i) => { if (typeof coordinate === 'string') { const value = siblingData[field.name][i] as string diff --git a/packages/payload/src/fields/validations.ts b/packages/payload/src/fields/validations.ts index 8ce6caf81..f5b07d6a5 100644 --- a/packages/payload/src/fields/validations.ts +++ b/packages/payload/src/fields/validations.ts @@ -875,7 +875,12 @@ export type PointFieldValidation = Validate< PointField > -export const point: PointFieldValidation = (value = ['', ''], { req: { t }, required }) => { +export const point: PointFieldValidation = (value, { req: { t }, required }) => { + // Allow to pass null to clear the field + if (!value) { + value = ['', ''] + } + const lng = parseFloat(String(value[0])) const lat = parseFloat(String(value[1])) if ( diff --git a/packages/payload/src/index.ts b/packages/payload/src/index.ts index 19e2defc6..3515c50bb 100644 --- a/packages/payload/src/index.ts +++ b/packages/payload/src/index.ts @@ -1362,7 +1362,10 @@ 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 } from './utilities/traverseFields.js' +export type { + TraverseFieldsCallback, + TraverseFlattenedFieldsCallback, +} from './utilities/traverseFields.js' export { buildVersionCollectionFields } from './versions/buildCollectionFields.js' export { buildVersionGlobalFields } from './versions/buildGlobalFields.js' export { versionDefaults } from './versions/defaults.js' diff --git a/packages/payload/src/utilities/traverseFields.ts b/packages/payload/src/utilities/traverseFields.ts index 1bda56c27..281d849f2 100644 --- a/packages/payload/src/utilities/traverseFields.ts +++ b/packages/payload/src/utilities/traverseFields.ts @@ -1,4 +1,12 @@ -import type { ArrayField, BlocksField, Field, TabAsField } from '../fields/config/types.js' +import type { + ArrayField, + BlocksField, + Field, + FlattenedArrayField, + FlattenedBlock, + FlattenedField, + TabAsField, +} from '../fields/config/types.js' import { fieldHasSubFields } from '../fields/config/types.js' @@ -12,7 +20,7 @@ const traverseArrayOrBlocksField = ({ callback: TraverseFieldsCallback data: Record[] field: ArrayField | BlocksField - fillEmpty: boolean + fillEmpty?: boolean parentRef?: unknown }) => { if (fillEmpty) { @@ -28,20 +36,23 @@ 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) + const block = field.blocks.find((block) => block.slug === ref.blockType) as FlattenedBlock fields = block?.fields + flattenedFields = block?.flattenedFields } else if (field.type === 'array') { fields = field.fields + flattenedFields = (field as FlattenedArrayField)?.flattenedFields } - if (fields) { - traverseFields({ callback, fields, fillEmpty, parentRef, ref }) + if (flattenedFields || fields) { + traverseFields({ callback, fields, fillEmpty, flattenedFields, parentRef, ref }) } } } -export type TraverseFieldsCallback = (args: { +type TraverseFieldsCallbackArgs = { /** * The current field */ @@ -58,12 +69,45 @@ export type TraverseFieldsCallback = (args: { * The current reference object */ ref?: Record | unknown +} + +export type TraverseFieldsCallback = (args: TraverseFieldsCallbackArgs) => boolean | void + +export type TraverseFlattenedFieldsCallback = (args: { + /** + * The current field + */ + field: FlattenedField + /** + * Function that when called will skip the current field and continue to the next + */ + next?: () => void + /** + * The parent reference object + */ + parentRef?: Record | unknown + /** + * The current reference object + */ + ref?: Record | unknown }) => boolean | void +type TraverseFlattenedFieldsArgs = { + callback: TraverseFlattenedFieldsCallback + fields?: Field[] + /** fill empty properties to use this without data */ + fillEmpty?: boolean + flattenedFields: FlattenedField[] + parentRef?: Record | unknown + ref?: Record | unknown +} + type TraverseFieldsArgs = { callback: TraverseFieldsCallback - fields: (Field | TabAsField)[] + fields: (Field | FlattenedField | TabAsField)[] + /** fill empty properties to use this without data */ fillEmpty?: boolean + flattenedFields?: FlattenedField[] parentRef?: Record | unknown ref?: Record | unknown } @@ -81,10 +125,11 @@ export const traverseFields = ({ callback, fields, fillEmpty = true, + flattenedFields, parentRef = {}, ref = {}, -}: TraverseFieldsArgs): void => { - fields.some((field) => { +}: TraverseFieldsArgs | TraverseFlattenedFieldsArgs): void => { + ;(flattenedFields ?? fields).some((field) => { let skip = false const next = () => { skip = true @@ -94,7 +139,16 @@ export const traverseFields = ({ return } - if (callback && callback({ field, next, parentRef, ref })) { + if ( + callback && + callback({ + // @ts-expect-error compatibillity Field | FlattenedField + field, + next, + parentRef, + ref, + }) + ) { return true } @@ -139,6 +193,7 @@ export const traverseFields = ({ if ( callback && callback({ + // @ts-expect-error compatibillity Field | FlattenedField field: { ...tab, type: 'tab' }, next, parentRef: currentParentRef, @@ -160,12 +215,15 @@ export const traverseFields = ({ return } - if (field.type !== 'tab' && (fieldHasSubFields(field) || field.type === 'blocks')) { + if ( + (flattenedFields || field.type !== 'tab') && + (fieldHasSubFields(field as Field) || field.type === 'tab' || field.type === 'blocks') + ) { if ('name' in field && field.name) { currentParentRef = currentRef if (!ref[field.name]) { if (fillEmpty) { - if (field.type === 'group') { + if (field.type === 'group' || field.type === 'tab') { ref[field.name] = {} } else if (field.type === 'array' || field.type === 'blocks') { if (field.localized) { @@ -182,7 +240,7 @@ export const traverseFields = ({ } if ( - field.type === 'group' && + (field.type === 'group' || field.type === 'tab') && field.localized && currentRef && typeof currentRef === 'object' @@ -193,9 +251,10 @@ export const traverseFields = ({ callback, fields: field.fields, fillEmpty, + flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined, parentRef: currentParentRef, ref: currentRef[key], - }) + } as TraverseFieldsArgs) } } return @@ -239,6 +298,7 @@ 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 34eaeeee9..c7d4d2193 100644 --- a/test/fields-relationship/int.spec.ts +++ b/test/fields-relationship/int.spec.ts @@ -53,10 +53,12 @@ describe('Relationship Fields', () => { collection: versionedRelationshipFieldSlug, data: { title: 'Version 1 Title', - relationshipField: { - value: relatedDoc.id, - relationTo: collection1Slug, - }, + relationshipField: [ + { + value: relatedDoc.id, + relationTo: collection1Slug, + }, + ], }, }) diff --git a/test/fields/int.spec.ts b/test/fields/int.spec.ts index 8aaa086bf..16f5b5338 100644 --- a/test/fields/int.spec.ts +++ b/test/fields/int.spec.ts @@ -1134,6 +1134,30 @@ describe('Fields', () => { expect(doc.localized).toEqual(localized) expect(doc.group).toMatchObject(group) }) + + it('should clear a point field', async () => { + if (payload.db.name === 'sqlite') { + return + } + + const doc = await payload.create({ + collection: 'point-fields', + data: { + point: [7, -7], + group: { + point: [7, -7], + }, + }, + }) + + const res = await payload.update({ + collection: 'point-fields', + id: doc.id, + data: { group: { point: null } }, + }) + + expect(res.group.point).toBeFalsy() + }) }) describe('unique indexes', () => { diff --git a/test/joins/int.spec.ts b/test/joins/int.spec.ts index 8f90d68d3..da47c7bb5 100644 --- a/test/joins/int.spec.ts +++ b/test/joins/int.spec.ts @@ -154,7 +154,10 @@ describe('Joins Field', () => { collection: categoriesSlug, }) - expect(Object.keys(categoryWithPosts)).toStrictEqual(['id', 'group']) + expect(categoryWithPosts).toStrictEqual({ + id: categoryWithPosts.id, + group: categoryWithPosts.group, + }) expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10) expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id') diff --git a/test/select/int.spec.ts b/test/select/int.spec.ts index 081a3f633..ab59faac3 100644 --- a/test/select/int.spec.ts +++ b/test/select/int.spec.ts @@ -1633,7 +1633,10 @@ describe('Select', () => { }, }) - expect(Object.keys(res)).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with updateByID', async () => { @@ -1646,13 +1649,18 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res)).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with updateBulk', async () => { const post = await createPost() - const res = await payload.update({ + const { + docs: [res], + } = await payload.update({ collection: 'posts', where: { id: { @@ -1663,7 +1671,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with deleteByID', async () => { @@ -1675,13 +1686,18 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res)).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with deleteBulk', async () => { const post = await createPost() - const res = await payload.delete({ + const { + docs: [res], + } = await payload.delete({ collection: 'posts', where: { id: { @@ -1691,7 +1707,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) it('should apply select with duplicate', async () => { @@ -1703,7 +1722,10 @@ describe('Select', () => { select: { text: true }, }) - expect(Object.keys(res)).toStrictEqual(['id', 'text']) + expect(res).toStrictEqual({ + id: res.id, + text: res.text, + }) }) })