fix(db-mongodb): mongodb optimizations (#10120)

There are few issues introduced in #9594 that we need to look into with
these changes.
This commit is contained in:
Sasha
2024-12-21 14:42:44 +02:00
committed by GitHub
parent 08eb13d189
commit b08ff88fbd
46 changed files with 939 additions and 1337 deletions

View File

@@ -1,9 +1,10 @@
import type { CountOptions } from 'mongodb' import type { CountOptions } from 'mongodb'
import type { Count } from 'payload' import type { Count } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
export const count: Count = async function count( export const count: Count = async function count(
@@ -11,37 +12,41 @@ export const count: Count = async function count(
{ collection, locale, req, where }, { collection, locale, req, where },
) { ) {
const Model = this.collections[collection] const Model = this.collections[collection]
const session = await getSession(this, req) const options: CountOptions = {
session: await getSession(this, req),
}
const hasNearConstraint = getHasNearConstraint(where) let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. // 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 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 let result: number
if (useEstimatedCount) { if (useEstimatedCount) {
result = await Model.collection.estimatedDocumentCount() result = await Model.estimatedDocumentCount({ session: options.session })
} else { } else {
const options: CountOptions = { session } result = await Model.countDocuments(query, options)
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 { return {

View File

@@ -1,9 +1,10 @@
import type { CountOptions } from 'mongodb' import type { CountOptions } from 'mongodb'
import type { CountGlobalVersions } from 'payload' import type { CountGlobalVersions } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions( export const countGlobalVersions: CountGlobalVersions = async function countGlobalVersions(
@@ -11,37 +12,41 @@ export const countGlobalVersions: CountGlobalVersions = async function countGlob
{ global, locale, req, where }, { global, locale, req, where },
) { ) {
const Model = this.versions[global] const Model = this.versions[global]
const session = await getSession(this, req) const options: CountOptions = {
session: await getSession(this, req),
}
const hasNearConstraint = getHasNearConstraint(where) let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. // 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 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 let result: number
if (useEstimatedCount) { if (useEstimatedCount) {
result = await Model.collection.estimatedDocumentCount() result = await Model.estimatedDocumentCount({ session: options.session })
} else { } else {
const options: CountOptions = { session } result = await Model.countDocuments(query, options)
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 { return {

View File

@@ -1,9 +1,10 @@
import type { CountOptions } from 'mongodb' import type { CountOptions } from 'mongodb'
import type { CountVersions } from 'payload' import type { CountVersions } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
export const countVersions: CountVersions = async function countVersions( export const countVersions: CountVersions = async function countVersions(
@@ -11,37 +12,41 @@ export const countVersions: CountVersions = async function countVersions(
{ collection, locale, req, where }, { collection, locale, req, where },
) { ) {
const Model = this.versions[collection] const Model = this.versions[collection]
const session = await getSession(this, req) const options: CountOptions = {
session: await getSession(this, req),
}
const hasNearConstraint = getHasNearConstraint(where) let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. // 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 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 let result: number
if (useEstimatedCount) { if (useEstimatedCount) {
result = await Model.collection.estimatedDocumentCount() result = await Model.estimatedDocumentCount({ session: options.session })
} else { } else {
const options: CountOptions = { session } result = await Model.countDocuments(query, options)
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 { return {

View File

@@ -1,44 +1,48 @@
import type { Create } from 'payload' import type { CreateOptions } from 'mongoose'
import type { Create, Document } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js' import { handleError } from './utilities/handleError.js'
import { transform } from './utilities/transform.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const create: Create = async function create( export const create: Create = async function create(
this: MongooseAdapter, this: MongooseAdapter,
{ collection, data, req }, { collection, data, req },
) { ) {
const Model = this.collections[collection] const Model = this.collections[collection]
const session = await getSession(this, req) const options: CreateOptions = {
session: await getSession(this, req),
const fields = this.payload.collections[collection].config.flattenedFields
if (this.payload.collections[collection].customIDType) {
data._id = data.id
} }
transform({ let doc
adapter: this,
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data, data,
fields, fields: this.payload.collections[collection].config.fields,
operation: 'create',
}) })
if (this.payload.collections[collection].customIDType) {
sanitizedData._id = sanitizedData.id
}
try { try {
const { insertedId: insertedID } = await Model.collection.insertOne(data, { session }) ;[doc] = await Model.create([sanitizedData], options)
data._id = insertedID
transform({
adapter: this,
data,
fields,
operation: 'read',
})
return data
} catch (error) { } catch (error) {
handleError({ collection, error, req }) handleError({ collection, error, req })
} }
// doc.toJSON does not do stuff like converting ObjectIds to string, or date strings to date objects. That's why we use JSON.parse/stringify here
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
} }

View File

@@ -1,9 +1,11 @@
import type { CreateOptions } from 'mongoose'
import type { CreateGlobal } from 'payload' import type { CreateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const createGlobal: CreateGlobal = async function createGlobal( export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter, this: MongooseAdapter,
@@ -11,29 +13,26 @@ export const createGlobal: CreateGlobal = async function createGlobal(
) { ) {
const Model = this.globals const Model = this.globals
const fields = this.payload.config.globals.find( const global = sanitizeRelationshipIDs({
(globalConfig) => globalConfig.slug === slug, config: this.payload.config,
).flattenedFields data: {
globalType: slug,
transform({ ...data,
adapter: this, },
data, fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
fields,
globalSlug: slug,
operation: 'create',
}) })
const session = await getSession(this, req) const options: CreateOptions = {
session: await getSession(this, req),
}
const { insertedId: insertedID } = await Model.collection.insertOne(data, { session }) let [result] = (await Model.create([global], options)) as any
;(data as any)._id = insertedID
transform({ result = JSON.parse(JSON.stringify(result))
adapter: this,
data,
fields,
operation: 'read',
})
return data // custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
} }

View File

@@ -1,9 +1,11 @@
import { buildVersionGlobalFields, type CreateGlobalVersion } from 'payload' import type { CreateOptions } from 'mongoose'
import { buildVersionGlobalFields, type CreateGlobalVersion, type Document } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion( export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
this: MongooseAdapter, this: MongooseAdapter,
@@ -20,41 +22,36 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
}, },
) { ) {
const VersionModel = this.versions[globalSlug] const VersionModel = this.versions[globalSlug]
const session = await getSession(this, req) const options: CreateOptions = {
session: await getSession(this, req),
const data = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
} }
const fields = buildVersionGlobalFields( const data = sanitizeRelationshipIDs({
this.payload.config, config: this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug), data: {
true, autosave,
) createdAt,
latest: true,
transform({ parent,
adapter: this, publishedLocale,
data, snapshot,
fields, updatedAt,
operation: 'create', version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
}) })
const { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session }) const [doc] = await VersionModel.create([data], options, req)
;(data as any)._id = insertedID
await VersionModel.collection.updateMany( await VersionModel.updateMany(
{ {
$and: [ $and: [
{ {
_id: { _id: {
$ne: insertedID, $ne: doc._id,
}, },
}, },
{ {
@@ -70,15 +67,16 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
], ],
}, },
{ $unset: { latest: 1 } }, { $unset: { latest: 1 } },
{ session }, options,
) )
transform({ const result: Document = JSON.parse(JSON.stringify(doc))
adapter: this, const verificationToken = doc._verificationToken
data,
fields,
operation: 'read',
})
return data as any // custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
} }

View File

@@ -1,10 +1,12 @@
import type { CreateOptions } from 'mongoose'
import { Types } from 'mongoose' import { Types } from 'mongoose'
import { buildVersionCollectionFields, type CreateVersion } from 'payload' import { buildVersionCollectionFields, type CreateVersion, type Document } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const createVersion: CreateVersion = async function createVersion( export const createVersion: CreateVersion = async function createVersion(
this: MongooseAdapter, this: MongooseAdapter,
@@ -21,34 +23,29 @@ export const createVersion: CreateVersion = async function createVersion(
}, },
) { ) {
const VersionModel = this.versions[collectionSlug] const VersionModel = this.versions[collectionSlug]
const session = await getSession(this, req) const options: CreateOptions = {
session: await getSession(this, req),
const data: any = {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
} }
const fields = buildVersionCollectionFields( const data = sanitizeRelationshipIDs({
this.payload.config, config: this.payload.config,
this.payload.collections[collectionSlug].config, data: {
true, autosave,
) createdAt,
latest: true,
transform({ parent,
adapter: this, publishedLocale,
data, snapshot,
fields, updatedAt,
operation: 'create', version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
}) })
const { insertedId: insertedID } = await VersionModel.collection.insertOne(data, { session }) const [doc] = await VersionModel.create([data], options, req)
data._id = insertedID
const parentQuery = { const parentQuery = {
$or: [ $or: [
@@ -59,7 +56,7 @@ export const createVersion: CreateVersion = async function createVersion(
}, },
], ],
} }
if ((data.parent as unknown) instanceof Types.ObjectId) { if (data.parent instanceof Types.ObjectId) {
parentQuery.$or.push({ parentQuery.$or.push({
parent: { parent: {
$eq: data.parent.toString(), $eq: data.parent.toString(),
@@ -67,12 +64,12 @@ export const createVersion: CreateVersion = async function createVersion(
}) })
} }
await VersionModel.collection.updateMany( await VersionModel.updateMany(
{ {
$and: [ $and: [
{ {
_id: { _id: {
$ne: insertedID, $ne: doc._id,
}, },
}, },
parentQuery, parentQuery,
@@ -83,21 +80,22 @@ export const createVersion: CreateVersion = async function createVersion(
}, },
{ {
updatedAt: { updatedAt: {
$lt: new Date(data.updatedAt), $lt: new Date(doc.updatedAt),
}, },
}, },
], ],
}, },
{ $unset: { latest: 1 } }, { $unset: { latest: 1 } },
{ session }, options,
) )
transform({ const result: Document = JSON.parse(JSON.stringify(doc))
adapter: this, const verificationToken = doc._verificationToken
data,
fields,
operation: 'read',
})
return data // custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
} }

View File

@@ -1,3 +1,4 @@
import type { DeleteOptions } from 'mongodb'
import type { DeleteMany } from 'payload' import type { DeleteMany } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
@@ -9,16 +10,14 @@ export const deleteMany: DeleteMany = async function deleteMany(
{ collection, req, where }, { collection, req, where },
) { ) {
const Model = this.collections[collection] const Model = this.collections[collection]
const options: DeleteOptions = {
const session = await getSession(this, req) session: await getSession(this, req),
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
await Model.collection.deleteMany(query, { await Model.deleteMany(query, options)
session,
})
} }

View File

@@ -1,41 +1,38 @@
import type { DeleteOne } from 'payload' import type { QueryOptions } from 'mongoose'
import type { DeleteOne, Document } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const deleteOne: DeleteOne = async function deleteOne( export const deleteOne: DeleteOne = async function deleteOne(
this: MongooseAdapter, this: MongooseAdapter,
{ collection, req, select, where }, { collection, req, select, where },
) { ) {
const Model = this.collections[collection] const Model = this.collections[collection]
const session = await getSession(this, req) const options: QueryOptions = {
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
select,
}),
session: await getSession(this, req),
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
const fields = this.payload.collections[collection].config.flattenedFields const doc = await Model.findOneAndDelete(query, options).lean()
const doc = await Model.collection.findOneAndDelete(query, { let result: Document = JSON.parse(JSON.stringify(doc))
projection: buildProjectionFromSelect({
adapter: this,
fields,
select,
}),
session,
})
transform({ // custom id type reset
adapter: this, result.id = result._id
data: doc, result = sanitizeInternalFields(result)
fields,
operation: 'read',
})
return doc return result
} }

View File

@@ -15,11 +15,8 @@ export const deleteVersions: DeleteVersions = async function deleteVersions(
const query = await VersionsModel.buildQuery({ const query = await VersionsModel.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
await VersionsModel.collection.deleteMany(query, { await VersionsModel.deleteMany(query, { session })
session,
})
} }

View File

@@ -1,15 +1,15 @@
import type { CollationOptions } from 'mongodb' import type { PaginateOptions } from 'mongoose'
import type { Find } from 'payload' import type { Find } from 'payload'
import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const find: Find = async function find( export const find: Find = async function find(
this: MongooseAdapter, this: MongooseAdapter,
@@ -20,6 +20,7 @@ export const find: Find = async function find(
locale, locale,
page, page,
pagination, pagination,
projection,
req, req,
select, select,
sort: sortArg, sort: sortArg,
@@ -28,17 +29,21 @@ export const find: Find = async function find(
) { ) {
const Model = this.collections[collection] const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config const collectionConfig = this.payload.collections[collection].config
const session = await getSession(this, req) const session = await getSession(this, req)
const hasNearConstraint = getHasNearConstraint(where) let hasNearConstraint = false
const fields = collectionConfig.flattenedFields if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
let sort let sort
if (!hasNearConstraint) { if (!hasNearConstraint) {
sort = buildSortParam({ sort = buildSortParam({
config: this.payload.config, config: this.payload.config,
fields, fields: collectionConfig.flattenedFields,
locale, locale,
sort: sortArg || collectionConfig.defaultSort, sort: sortArg || collectionConfig.defaultSort,
timestamps: true, timestamps: true,
@@ -48,51 +53,90 @@ export const find: Find = async function find(
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. // 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 useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
lean: true,
leanWithId: true,
options: {
session,
},
page,
pagination,
projection,
sort,
useEstimatedCount,
}
const projection = buildProjectionFromSelect({ if (select) {
adapter: this, paginationOptions.projection = buildProjectionFromSelect({
fields, adapter: this,
select, fields: collectionConfig.flattenedFields,
}) select,
})
}
const collation: CollationOptions | undefined = this.collation if (this.collation) {
? { const defaultLocale = 'en'
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', paginationOptions.collation = {
...this.collation, locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
} ...this.collation,
: undefined }
}
const joinAgreggation = await buildJoinAggregation({ if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
hint: { _id: 1 },
session,
}),
)
}
}
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
paginationOptions.pagination = false
}
}
let result
const aggregate = await buildJoinAggregation({
adapter: this, adapter: this,
collection, collection,
collectionConfig, collectionConfig,
joins, joins,
locale, locale,
session,
})
const result = await findMany({
adapter: this,
collation,
collection: Model.collection,
joinAgreggation,
limit,
page,
pagination,
projection,
query, query,
session,
sort,
useEstimatedCount,
}) })
// build join aggregation
if (aggregate) {
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
} else {
result = await Model.paginate(query, paginationOptions)
}
transform({ adapter: this, data: result.docs, fields, operation: 'read' }) const docs = JSON.parse(JSON.stringify(result.docs))
return result return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
} }

View File

@@ -1,3 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { FindGlobal } from 'payload' import type { FindGlobal } from 'payload'
import { combineQueries } from 'payload' import { combineQueries } from 'payload'
@@ -6,40 +7,42 @@ import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const findGlobal: FindGlobal = async function findGlobal( export const findGlobal: FindGlobal = async function findGlobal(
this: MongooseAdapter, this: MongooseAdapter,
{ slug, locale, req, select, where }, { slug, locale, req, select, where },
) { ) {
const Model = this.globals const Model = this.globals
const options: QueryOptions = {
const session = await getSession(this, req) lean: true,
select: buildProjectionFromSelect({
adapter: this,
fields: this.payload.globals.config.find((each) => each.slug === slug).flattenedFields,
select,
}),
session: await getSession(this, req),
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
globalSlug: slug, globalSlug: slug,
locale, locale,
payload: this.payload, payload: this.payload,
session,
where: combineQueries({ globalType: { equals: slug } }, where), where: combineQueries({ globalType: { equals: slug } }, where),
}) })
const fields = this.payload.globals.config.find((each) => each.slug === slug).flattenedFields let doc = (await Model.findOne(query, {}, options)) as any
const doc = await Model.collection.findOne(query, {
projection: buildProjectionFromSelect({
adapter: this,
fields,
select,
}),
session: await getSession(this, req),
})
if (!doc) { if (!doc) {
return null return null
} }
if (doc._id) {
doc.id = doc._id
delete doc._id
}
transform({ adapter: this, data: doc, fields, operation: 'read' }) doc = JSON.parse(JSON.stringify(doc))
doc = sanitizeInternalFields(doc)
return doc as any return doc
} }

View File

@@ -1,16 +1,14 @@
import type { CollationOptions } from 'mongodb' import type { PaginateOptions, QueryOptions } from 'mongoose'
import type { FindGlobalVersions } from 'payload' import type { FindGlobalVersions } from 'payload'
import { buildVersionGlobalFields } from 'payload' import { buildVersionGlobalFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions( export const findGlobalVersions: FindGlobalVersions = async function findGlobalVersions(
this: MongooseAdapter, this: MongooseAdapter,
@@ -23,7 +21,19 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
true, true,
) )
const hasNearConstraint = getHasNearConstraint(where) const session = await getSession(this, req)
const options: QueryOptions = {
limit,
session,
skip,
}
let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
let sort let sort
if (!hasNearConstraint) { if (!hasNearConstraint) {
@@ -36,49 +46,69 @@ export const findGlobalVersions: FindGlobalVersions = async function findGlobalV
}) })
} }
const session = await getSession(this, req)
const query = await Model.buildQuery({ const query = await Model.buildQuery({
globalSlug: global, globalSlug: global,
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. // 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 useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
const projection = buildProjectionFromSelect({ adapter: this, fields: versionFields, select }) lean: true,
leanWithId: true,
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, limit,
options,
page, page,
pagination, pagination,
projection, projection: buildProjectionFromSelect({ adapter: this, fields: versionFields, select }),
query,
session,
skip,
sort, sort,
useEstimatedCount, useEstimatedCount,
}) }
transform({ if (this.collation) {
adapter: this, const defaultLocale = 'en'
data: result.docs, paginationOptions.collation = {
fields: versionFields, locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
operation: 'read', ...this.collation,
}) }
}
return result if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
hint: { _id: 1 },
session,
}),
)
}
}
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
paginationOptions.pagination = false
}
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
} }

View File

@@ -1,11 +1,12 @@
import type { FindOne } from 'payload' import type { AggregateOptions, QueryOptions } from 'mongoose'
import type { Document, FindOne } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const findOne: FindOne = async function findOne( export const findOne: FindOne = async function findOne(
this: MongooseAdapter, this: MongooseAdapter,
@@ -13,64 +14,52 @@ export const findOne: FindOne = async function findOne(
) { ) {
const Model = this.collections[collection] const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config const collectionConfig = this.payload.collections[collection].config
const session = await getSession(this, req) const session = await getSession(this, req)
const options: AggregateOptions & QueryOptions = {
lean: true,
session,
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
const fields = collectionConfig.flattenedFields
const projection = buildProjectionFromSelect({ const projection = buildProjectionFromSelect({
adapter: this, adapter: this,
fields, fields: collectionConfig.flattenedFields,
select, select,
}) })
const joinAggregation = await buildJoinAggregation({ const aggregate = await buildJoinAggregation({
adapter: this, adapter: this,
collection, collection,
collectionConfig, collectionConfig,
joins, joins,
limit: 1,
locale, locale,
projection, projection,
session, query,
}) })
let doc let doc
if (joinAggregation) { if (aggregate) {
const aggregation = Model.collection.aggregate( ;[doc] = await Model.aggregate(aggregate, { session })
[
{
$match: query,
},
],
{ session },
)
aggregation.limit(1)
for (const stage of joinAggregation) {
aggregation.addStage(stage)
}
;[doc] = await aggregation.toArray()
} else { } else {
doc = await Model.collection.findOne(query, { projection, session }) ;(options as Record<string, unknown>).projection = projection
doc = await Model.findOne(query, {}, options)
} }
if (!doc) { if (!doc) {
return null return null
} }
transform({ let result: Document = JSON.parse(JSON.stringify(doc))
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc // custom id type reset
result.id = result._id
result = sanitizeInternalFields(result)
return result
} }

View File

@@ -1,16 +1,14 @@
import type { CollationOptions } from 'mongodb' import type { PaginateOptions, QueryOptions } from 'mongoose'
import type { FindVersions } from 'payload' import type { FindVersions } from 'payload'
import { buildVersionCollectionFields } from 'payload' import { buildVersionCollectionFields, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const findVersions: FindVersions = async function findVersions( export const findVersions: FindVersions = async function findVersions(
this: MongooseAdapter, this: MongooseAdapter,
@@ -18,10 +16,19 @@ export const findVersions: FindVersions = async function findVersions(
) { ) {
const Model = this.versions[collection] const Model = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config const collectionConfig = this.payload.collections[collection].config
const session = await getSession(this, req) const session = await getSession(this, req)
const options: QueryOptions = {
limit,
session,
skip,
}
const hasNearConstraint = getHasNearConstraint(where) let hasNearConstraint = false
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
let sort let sort
if (!hasNearConstraint) { if (!hasNearConstraint) {
@@ -37,48 +44,69 @@ export const findVersions: FindVersions = async function findVersions(
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, 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. // 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 useEstimatedCount = hasNearConstraint || !query || Object.keys(query).length === 0
const paginationOptions: PaginateOptions = {
const projection = buildProjectionFromSelect({ lean: true,
adapter: this, leanWithId: true,
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, limit,
options,
page, page,
pagination, pagination,
projection, projection: buildProjectionFromSelect({
query, adapter: this,
session, fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
skip, select,
}),
sort, sort,
useEstimatedCount, useEstimatedCount,
}) }
transform({ if (this.collation) {
adapter: this, const defaultLocale = 'en'
data: result.docs, paginationOptions.collation = {
fields: versionFields, locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
operation: 'read', ...this.collation,
}) }
}
return result if (!useEstimatedCount && Object.keys(query).length === 0 && this.disableIndexHints !== true) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
Model.countDocuments(query, {
hint: { _id: 1 },
session,
}),
)
}
}
if (limit >= 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
// Disable pagination if limit is 0
if (limit === 0) {
paginationOptions.pagination = false
}
}
const result = await Model.paginate(query, paginationOptions)
const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc.id = doc._id
return sanitizeInternalFields(doc)
}),
}
} }

View File

@@ -59,8 +59,6 @@ import { upsert } from './upsert.js'
export type { MigrateDownArgs, MigrateUpArgs } from './types.js' export type { MigrateDownArgs, MigrateUpArgs } from './types.js'
export { transform } from './utilities/transform.js'
export interface Args { export interface Args {
/** Set to false to disable auto-pluralization of collection names, Defaults to true */ /** Set to false to disable auto-pluralization of collection names, Defaults to true */
autoPluralization?: boolean autoPluralization?: boolean

View File

@@ -1,3 +1,5 @@
import type { PayloadRequest } from 'payload'
import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload' import { commitTransaction, initTransaction, killTransaction, readMigrationFiles } from 'payload'
import prompts from 'prompts' import prompts from 'prompts'

View File

@@ -1,23 +1,23 @@
import type { ClientSession, Model } from 'mongoose' import type { ClientSession, Model } from 'mongoose'
import type { Field, FlattenedField, PayloadRequest } from 'payload' import type { Field, PayloadRequest, SanitizedConfig } from 'payload'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload' import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
import type { MongooseAdapter } from '../index.js' import type { MongooseAdapter } from '../index.js'
import { getSession } from '../utilities/getSession.js' import { getSession } from '../utilities/getSession.js'
import { transform } from '../utilities/transform.js' import { sanitizeRelationshipIDs } from '../utilities/sanitizeRelationshipIDs.js'
const migrateModelWithBatching = async ({ const migrateModelWithBatching = async ({
adapter,
batchSize, batchSize,
config,
fields, fields,
Model, Model,
session, session,
}: { }: {
adapter: MongooseAdapter
batchSize: number batchSize: number
fields: FlattenedField[] config: SanitizedConfig
fields: Field[]
Model: Model<any> Model: Model<any>
session: ClientSession session: ClientSession
}): Promise<void> => { }): Promise<void> => {
@@ -47,7 +47,7 @@ const migrateModelWithBatching = async ({
} }
for (const doc of docs) { for (const doc of docs) {
transform({ adapter, data: doc, fields, operation: 'update', validateRelationships: false }) sanitizeRelationshipIDs({ config, data: doc, fields })
} }
await Model.collection.bulkWrite( await Model.collection.bulkWrite(
@@ -115,9 +115,9 @@ export async function migrateRelationshipsV2_V3({
payload.logger.info(`Migrating collection "${collection.slug}"`) payload.logger.info(`Migrating collection "${collection.slug}"`)
await migrateModelWithBatching({ await migrateModelWithBatching({
adapter: db,
batchSize, batchSize,
fields: collection.flattenedFields, config,
fields: collection.fields,
Model: db.collections[collection.slug], Model: db.collections[collection.slug],
session, session,
}) })
@@ -128,9 +128,9 @@ export async function migrateRelationshipsV2_V3({
payload.logger.info(`Migrating collection versions "${collection.slug}"`) payload.logger.info(`Migrating collection versions "${collection.slug}"`)
await migrateModelWithBatching({ await migrateModelWithBatching({
adapter: db,
batchSize, batchSize,
fields: buildVersionCollectionFields(config, collection, true), config,
fields: buildVersionCollectionFields(config, collection),
Model: db.versions[collection.slug], Model: db.versions[collection.slug],
session, session,
}) })
@@ -156,13 +156,7 @@ export async function migrateRelationshipsV2_V3({
// in case if the global doesn't exist in the database yet (not saved) // in case if the global doesn't exist in the database yet (not saved)
if (doc) { if (doc) {
transform({ sanitizeRelationshipIDs({ config, data: doc, fields: global.fields })
adapter: db,
data: doc,
fields: global.flattenedFields,
operation: 'update',
validateRelationships: false,
})
await GlobalsModel.collection.updateOne( await GlobalsModel.collection.updateOne(
{ {
@@ -179,9 +173,9 @@ export async function migrateRelationshipsV2_V3({
payload.logger.info(`Migrating global versions "${global.slug}"`) payload.logger.info(`Migrating global versions "${global.slug}"`)
await migrateModelWithBatching({ await migrateModelWithBatching({
adapter: db,
batchSize, batchSize,
fields: buildVersionGlobalFields(config, global, true), config,
fields: buildVersionGlobalFields(config, global),
Model: db.versions[global.slug], Model: db.versions[global.slug],
session, session,
}) })

View File

@@ -1,4 +1,3 @@
import type { ClientSession } from 'mongodb'
import type { FlattenedField, Payload, Where } from 'payload' import type { FlattenedField, Payload, Where } from 'payload'
import { parseParams } from './parseParams.js' import { parseParams } from './parseParams.js'
@@ -9,7 +8,6 @@ export async function buildAndOrConditions({
globalSlug, globalSlug,
locale, locale,
payload, payload,
session,
where, where,
}: { }: {
collectionSlug?: string collectionSlug?: string
@@ -17,7 +15,6 @@ export async function buildAndOrConditions({
globalSlug?: string globalSlug?: string
locale?: string locale?: string
payload: Payload payload: Payload
session?: ClientSession
where: Where[] where: Where[]
}): Promise<Record<string, unknown>[]> { }): Promise<Record<string, unknown>[]> {
const completedConditions = [] const completedConditions = []
@@ -33,7 +30,6 @@ export async function buildAndOrConditions({
globalSlug, globalSlug,
locale, locale,
payload, payload,
session,
where: condition, where: condition,
}) })
if (Object.keys(result).length > 0) { if (Object.keys(result).length > 0) {

View File

@@ -1,6 +1,7 @@
import type { ClientSession } from 'mongodb'
import type { FlattenedField, Payload, Where } from 'payload' import type { FlattenedField, Payload, Where } from 'payload'
import { QueryError } from 'payload'
import { parseParams } from './parseParams.js' import { parseParams } from './parseParams.js'
type GetBuildQueryPluginArgs = { type GetBuildQueryPluginArgs = {
@@ -12,7 +13,6 @@ export type BuildQueryArgs = {
globalSlug?: string globalSlug?: string
locale?: string locale?: string
payload: Payload payload: Payload
session?: ClientSession
where: Where where: Where
} }
@@ -28,7 +28,6 @@ export const getBuildQueryPlugin = ({
globalSlug, globalSlug,
locale, locale,
payload, payload,
session,
where, where,
}: BuildQueryArgs): Promise<Record<string, unknown>> { }: BuildQueryArgs): Promise<Record<string, unknown>> {
let fields = versionsFields let fields = versionsFields
@@ -42,17 +41,20 @@ export const getBuildQueryPlugin = ({
fields = collectionConfig.flattenedFields fields = collectionConfig.flattenedFields
} }
} }
const errors = []
const result = await parseParams({ const result = await parseParams({
collectionSlug, collectionSlug,
fields, fields,
globalSlug, globalSlug,
locale, locale,
payload, payload,
session,
where, where,
}) })
if (errors.length > 0) {
throw new QueryError(errors)
}
return result return result
} }
modifiedSchema.statics.buildQuery = buildQuery modifiedSchema.statics.buildQuery = buildQuery

View File

@@ -1,4 +1,3 @@
import type { ClientSession, FindOptions } from 'mongodb'
import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload' import type { FlattenedField, Operator, PathToQuery, Payload } from 'payload'
import { Types } from 'mongoose' import { Types } from 'mongoose'
@@ -16,11 +15,9 @@ type SearchParam = {
value?: unknown value?: unknown
} }
const subQueryOptions: FindOptions = { const subQueryOptions = {
lean: true,
limit: 50, limit: 50,
projection: {
_id: true,
},
} }
/** /**
@@ -34,7 +31,6 @@ export async function buildSearchParam({
locale, locale,
operator, operator,
payload, payload,
session,
val, val,
}: { }: {
collectionSlug?: string collectionSlug?: string
@@ -44,7 +40,6 @@ export async function buildSearchParam({
locale?: string locale?: string
operator: string operator: string
payload: Payload payload: Payload
session?: ClientSession
val: unknown val: unknown
}): Promise<SearchParam> { }): Promise<SearchParam> {
// Replace GraphQL nested field double underscore formatting // Replace GraphQL nested field double underscore formatting
@@ -139,14 +134,17 @@ export async function buildSearchParam({
}, },
}) })
const result = await SubModel.collection const result = await SubModel.find(subQuery, subQueryOptions)
.find(subQuery, { session, ...subQueryOptions })
.toArray()
const $in: unknown[] = [] const $in: unknown[] = []
result.forEach((doc) => { result.forEach((doc) => {
$in.push(doc._id) const stringID = doc._id.toString()
$in.push(stringID)
if (Types.ObjectId.isValid(stringID)) {
$in.push(doc._id)
}
}) })
if (pathsToQuery.length === 1) { if (pathsToQuery.length === 1) {
@@ -164,9 +162,7 @@ export async function buildSearchParam({
} }
const subQuery = priorQueryResult.value const subQuery = priorQueryResult.value
const result = await SubModel.collection const result = await SubModel.find(subQuery, subQueryOptions)
.find(subQuery, { session, ...subQueryOptions })
.toArray()
const $in = result.map((doc) => doc._id) const $in = result.map((doc) => doc._id)

View File

@@ -11,13 +11,20 @@ type Args = {
timestamps: boolean timestamps: boolean
} }
export type SortArgs = {
direction: SortDirection
property: string
}[]
export type SortDirection = 'asc' | 'desc'
export const buildSortParam = ({ export const buildSortParam = ({
config, config,
fields, fields,
locale, locale,
sort, sort,
timestamps, timestamps,
}: Args): Record<string, -1 | 1> => { }: Args): PaginateOptions['sort'] => {
if (!sort) { if (!sort) {
if (timestamps) { if (timestamps) {
sort = '-createdAt' sort = '-createdAt'
@@ -30,15 +37,15 @@ export const buildSortParam = ({
sort = [sort] sort = [sort]
} }
const sorting = sort.reduce<Record<string, -1 | 1>>((acc, item) => { const sorting = sort.reduce<PaginateOptions['sort']>((acc, item) => {
let sortProperty: string let sortProperty: string
let sortDirection: -1 | 1 let sortDirection: SortDirection
if (item.indexOf('-') === 0) { if (item.indexOf('-') === 0) {
sortProperty = item.substring(1) sortProperty = item.substring(1)
sortDirection = -1 sortDirection = 'desc'
} else { } else {
sortProperty = item sortProperty = item
sortDirection = 1 sortDirection = 'asc'
} }
if (sortProperty === 'id') { if (sortProperty === 'id') {
acc['_id'] = sortDirection acc['_id'] = sortDirection

View File

@@ -1,6 +1,6 @@
import type { FlattenedField, SanitizedConfig } from 'payload' import type { FlattenedField, SanitizedConfig } from 'payload'
import { fieldAffectsData } from 'payload/shared' import { fieldAffectsData, fieldIsPresentationalOnly } from 'payload/shared'
type Args = { type Args = {
config: SanitizedConfig config: SanitizedConfig
@@ -33,7 +33,7 @@ export const getLocalizedSortProperty = ({
(field) => fieldAffectsData(field) && field.name === firstSegment, (field) => fieldAffectsData(field) && field.name === firstSegment,
) )
if (matchedField) { if (matchedField && !fieldIsPresentationalOnly(matchedField)) {
let nextFields: FlattenedField[] let nextFields: FlattenedField[]
const remainingSegments = [...segments] const remainingSegments = [...segments]
let localizedSegment = matchedField.name let localizedSegment = matchedField.name

View File

@@ -1,4 +1,3 @@
import type { ClientSession } from 'mongodb'
import type { FilterQuery } from 'mongoose' import type { FilterQuery } from 'mongoose'
import type { FlattenedField, Operator, Payload, Where } from 'payload' import type { FlattenedField, Operator, Payload, Where } from 'payload'
@@ -14,7 +13,6 @@ export async function parseParams({
globalSlug, globalSlug,
locale, locale,
payload, payload,
session,
where, where,
}: { }: {
collectionSlug?: string collectionSlug?: string
@@ -22,7 +20,6 @@ export async function parseParams({
globalSlug?: string globalSlug?: string
locale: string locale: string
payload: Payload payload: Payload
session?: ClientSession
where: Where where: Where
}): Promise<Record<string, unknown>> { }): Promise<Record<string, unknown>> {
let result = {} as FilterQuery<any> let result = {} as FilterQuery<any>
@@ -65,7 +62,6 @@ export async function parseParams({
locale, locale,
operator, operator,
payload, payload,
session,
val: pathOperators[operator], val: pathOperators[operator],
}) })

View File

@@ -37,22 +37,6 @@ const buildExistsQuery = (formattedValue, path, treatEmptyString = true) => {
} }
} }
const sanitizeCoordinates = (coordinates: unknown[]): unknown[] => {
const result: unknown[] = []
for (const value of coordinates) {
if (typeof value === 'string') {
result.push(Number(value))
} else if (Array.isArray(value)) {
result.push(sanitizeCoordinates(value))
} else {
result.push(value)
}
}
return result
}
// returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships // returns nestedField Field object from blocks.nestedField path because getLocalizedPaths splits them only for relationships
const getFieldFromSegments = ({ const getFieldFromSegments = ({
field, field,
@@ -375,14 +359,6 @@ export const sanitizeQueryValue = ({
} }
if (operator === 'within' || operator === 'intersects') { if (operator === 'within' || operator === 'intersects') {
if (
formattedValue &&
typeof formattedValue === 'object' &&
Array.isArray(formattedValue.coordinates)
) {
formattedValue.coordinates = sanitizeCoordinates(formattedValue.coordinates)
}
formattedValue = { formattedValue = {
$geometry: formattedValue, $geometry: formattedValue,
} }

View File

@@ -1,17 +1,15 @@
import type { CollationOptions } from 'mongodb' import type { PaginateOptions, QueryOptions } from 'mongoose'
import type { QueryDrafts } from 'payload' import type { QueryDrafts } from 'payload'
import { buildVersionCollectionFields, combineQueries } from 'payload' import { buildVersionCollectionFields, combineQueries, flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js' import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js' import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { findMany } from './utilities/findMany.js'
import { getHasNearConstraint } from './utilities/getHasNearConstraint.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
export const queryDrafts: QueryDrafts = async function queryDrafts( export const queryDrafts: QueryDrafts = async function queryDrafts(
this: MongooseAdapter, this: MongooseAdapter,
@@ -19,11 +17,18 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
) { ) {
const VersionModel = this.versions[collection] const VersionModel = this.versions[collection]
const collectionConfig = this.payload.collections[collection].config const collectionConfig = this.payload.collections[collection].config
const session = await getSession(this, req) const options: QueryOptions = {
session: await getSession(this, req),
}
const hasNearConstraint = getHasNearConstraint(where) let hasNearConstraint
let sort let sort
if (where) {
const constraints = flattenWhereToOperators(where)
hasNearConstraint = constraints.some((prop) => Object.keys(prop).some((key) => key === 'near'))
}
if (!hasNearConstraint) { if (!hasNearConstraint) {
sort = buildSortParam({ sort = buildSortParam({
config: this.payload.config, config: this.payload.config,
@@ -39,65 +44,95 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
const versionQuery = await VersionModel.buildQuery({ const versionQuery = await VersionModel.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where: combinedWhere, where: combinedWhere,
}) })
const versionFields = buildVersionCollectionFields(this.payload.config, collectionConfig, true)
const projection = buildProjectionFromSelect({ const projection = buildProjectionFromSelect({
adapter: this, adapter: this,
fields: versionFields, fields: buildVersionCollectionFields(this.payload.config, collectionConfig, true),
select, select,
}) })
// useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters. // useEstimatedCount is faster, but not accurate, as it ignores any filters. It is thus set to true if there are no filters.
const useEstimatedCount = const useEstimatedCount =
hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0 hasNearConstraint || !versionQuery || Object.keys(versionQuery).length === 0
const paginationOptions: PaginateOptions = {
lean: true,
leanWithId: true,
options,
page,
pagination,
projection,
sort,
useEstimatedCount,
}
const collation: CollationOptions | undefined = this.collation if (this.collation) {
? { const defaultLocale = 'en'
locale: locale && locale !== 'all' && locale !== '*' ? locale : 'en', paginationOptions.collation = {
...this.collation, locale: locale && locale !== 'all' && locale !== '*' ? locale : defaultLocale,
} ...this.collation,
: undefined }
}
const joinAgreggation = await buildJoinAggregation({ if (
!useEstimatedCount &&
Object.keys(versionQuery).length === 0 &&
this.disableIndexHints !== true
) {
// Improve the performance of the countDocuments query which is used if useEstimatedCount is set to false by adding
// a hint. By default, if no hint is provided, MongoDB does not use an indexed field to count the returned documents,
// which makes queries very slow. This only happens when no query (filter) is provided. If one is provided, it uses
// the correct indexed field
paginationOptions.useCustomCountFn = () => {
return Promise.resolve(
VersionModel.countDocuments(versionQuery, {
hint: { _id: 1 },
}),
)
}
}
if (limit > 0) {
paginationOptions.limit = limit
// limit must also be set here, it's ignored when pagination is false
paginationOptions.options.limit = limit
}
let result
const aggregate = await buildJoinAggregation({
adapter: this, adapter: this,
collection, collection,
collectionConfig, collectionConfig,
joins, joins,
locale, locale,
projection, projection,
session, query: versionQuery,
versions: true, versions: true,
}) })
const result = await findMany({ // build join aggregation
adapter: this, if (aggregate) {
collation, result = await VersionModel.aggregatePaginate(
collection: VersionModel.collection, VersionModel.aggregate(aggregate),
joinAgreggation, paginationOptions,
limit, )
page, } else {
pagination, result = await VersionModel.paginate(versionQuery, paginationOptions)
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
} }
return result const docs = JSON.parse(JSON.stringify(result.docs))
return {
...result,
docs: docs.map((doc) => {
doc = {
_id: doc.parent,
id: doc.parent,
...doc.version,
}
return sanitizeInternalFields(doc)
}),
}
} }

View File

@@ -1,45 +1,47 @@
import type { QueryOptions } from 'mongoose'
import type { UpdateGlobal } from 'payload' import type { UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal( export const updateGlobal: UpdateGlobal = async function updateGlobal(
this: MongooseAdapter, this: MongooseAdapter,
{ slug, data, options: optionsArgs = {}, req, select }, { slug, data, options: optionsArgs = {}, req, select },
) { ) {
const Model = this.globals const Model = this.globals
const fields = this.payload.config.globals.find((global) => global.slug === slug).flattenedFields const fields = this.payload.config.globals.find((global) => global.slug === slug).fields
const session = await getSession(this, req) const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.config.globals.find((global) => global.slug === slug).flattenedFields,
select,
}),
session: await getSession(this, req),
}
transform({ let result
adapter: this,
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data, data,
fields, fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
}) })
const result: any = await Model.collection.findOneAndUpdate( result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
{ globalType: slug },
{ $set: data },
{
...optionsArgs,
projection: buildProjectionFromSelect({ adapter: this, fields, select }),
returnDocument: 'after',
session,
},
)
transform({ result = JSON.parse(JSON.stringify(result))
adapter: this,
data: result, // custom id type reset
fields, result.id = result._id
operation: 'read', result = sanitizeInternalFields(result)
})
return result return result
} }

View File

@@ -1,10 +1,12 @@
import type { QueryOptions } from 'mongoose'
import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload' import { buildVersionGlobalFields, type TypeWithID, type UpdateGlobalVersionArgs } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export async function updateGlobalVersion<T extends TypeWithID>( export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter, this: MongooseAdapter,
@@ -21,50 +23,44 @@ export async function updateGlobalVersion<T extends TypeWithID>(
) { ) {
const VersionModel = this.versions[globalSlug] const VersionModel = this.versions[globalSlug]
const whereToUse = where || { id: { equals: id } } const whereToUse = where || { id: { equals: id } }
const fields = buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
true,
)
const session = await getSession(this, req) const currentGlobal = this.payload.config.globals.find((global) => global.slug === globalSlug)
const fields = buildVersionGlobalFields(this.payload.config, currentGlobal)
const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionGlobalFields(this.payload.config, currentGlobal, true),
select,
}),
session: await getSession(this, req),
}
const query = await VersionModel.buildQuery({ const query = await VersionModel.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where: whereToUse, where: whereToUse,
}) })
transform({ const sanitizedData = sanitizeRelationshipIDs({
adapter: this, config: this.payload.config,
data: versionData, data: versionData,
fields, fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
}) })
const doc: any = await VersionModel.collection.findOneAndUpdate( const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
query,
{ $set: versionData },
{
...optionsArgs,
projection: buildProjectionFromSelect({
adapter: this,
fields,
select,
}),
returnDocument: 'after',
session,
},
)
transform({ const result = JSON.parse(JSON.stringify(doc))
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
} }

View File

@@ -1,3 +1,4 @@
import type { QueryOptions } from 'mongoose'
import type { UpdateOne } from 'payload' import type { UpdateOne } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
@@ -5,7 +6,8 @@ import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { handleError } from './utilities/handleError.js' import { handleError } from './utilities/handleError.js'
import { transform } from './utilities/transform.js' import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const updateOne: UpdateOne = async function updateOne( export const updateOne: UpdateOne = async function updateOne(
this: MongooseAdapter, this: MongooseAdapter,
@@ -13,45 +15,42 @@ export const updateOne: UpdateOne = async function updateOne(
) { ) {
const where = id ? { id: { equals: id } } : whereArg const where = id ? { id: { equals: id } } : whereArg
const Model = this.collections[collection] const Model = this.collections[collection]
const fields = this.payload.collections[collection].config.flattenedFields const fields = this.payload.collections[collection].config.fields
const options: QueryOptions = {
const session = await getSession(this, req) ...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: this.payload.collections[collection].config.flattenedFields,
select,
}),
session: await getSession(this, req),
}
const query = await Model.buildQuery({ const query = await Model.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where, where,
}) })
transform({ let result
adapter: this,
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data, data,
fields, fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
}) })
try { try {
const result = await Model.collection.findOneAndUpdate( result = await Model.findOneAndUpdate(query, sanitizedData, options)
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) { } catch (error) {
handleError({ collection, error, req }) handleError({ collection, error, req })
} }
result = JSON.parse(JSON.stringify(result))
result.id = result._id
result = sanitizeInternalFields(result)
return result
} }

View File

@@ -1,10 +1,12 @@
import type { QueryOptions } from 'mongoose'
import { buildVersionCollectionFields, type UpdateVersion } from 'payload' import { buildVersionCollectionFields, type UpdateVersion } from 'payload'
import type { MongooseAdapter } from './index.js' import type { MongooseAdapter } from './index.js'
import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js' import { buildProjectionFromSelect } from './utilities/buildProjectionFromSelect.js'
import { getSession } from './utilities/getSession.js' import { getSession } from './utilities/getSession.js'
import { transform } from './utilities/transform.js' import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
export const updateVersion: UpdateVersion = async function updateVersion( export const updateVersion: UpdateVersion = async function updateVersion(
this: MongooseAdapter, this: MongooseAdapter,
@@ -15,51 +17,46 @@ export const updateVersion: UpdateVersion = async function updateVersion(
const fields = buildVersionCollectionFields( const fields = buildVersionCollectionFields(
this.payload.config, this.payload.config,
this.payload.collections[collection].config, this.payload.collections[collection].config,
true,
) )
const session = await getSession(this, req) const options: QueryOptions = {
...optionsArgs,
lean: true,
new: true,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
select,
}),
session: await getSession(this, req),
}
const query = await VersionModel.buildQuery({ const query = await VersionModel.buildQuery({
locale, locale,
payload: this.payload, payload: this.payload,
session,
where: whereToUse, where: whereToUse,
}) })
transform({ const sanitizedData = sanitizeRelationshipIDs({
adapter: this, config: this.payload.config,
data: versionData, data: versionData,
fields, fields,
operation: 'update',
timestamps: optionsArgs.timestamps !== false,
}) })
const doc = await VersionModel.collection.findOneAndUpdate( const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
query,
{ $set: versionData },
{
...optionsArgs,
projection: buildProjectionFromSelect({
adapter: this,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
true,
),
select,
}),
returnDocument: 'after',
session,
},
)
transform({ const result = JSON.parse(JSON.stringify(doc))
adapter: this,
data: doc,
fields,
operation: 'read',
})
return doc as any const verificationToken = doc._verificationToken
// custom id type reset
result.id = result._id
if (verificationToken) {
result._verificationToken = verificationToken
}
return result
} }

View File

@@ -1,4 +1,3 @@
import type { ClientSession } from 'mongodb'
import type { PipelineStage } from 'mongoose' import type { PipelineStage } from 'mongoose'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload' import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
@@ -11,9 +10,12 @@ type BuildJoinAggregationArgs = {
collection: CollectionSlug collection: CollectionSlug
collectionConfig: SanitizedCollectionConfig collectionConfig: SanitizedCollectionConfig
joins: JoinQuery joins: JoinQuery
// the number of docs to get at the top collection level
limit?: number
locale: string locale: string
projection?: Record<string, true> projection?: Record<string, true>
session?: ClientSession // the where clause for the top collection
query?: Where
/** whether the query is from drafts */ /** whether the query is from drafts */
versions?: boolean versions?: boolean
} }
@@ -23,9 +25,10 @@ export const buildJoinAggregation = async ({
collection, collection,
collectionConfig, collectionConfig,
joins, joins,
limit,
locale, locale,
projection, projection,
session, query,
versions, versions,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => { }: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) { if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
@@ -33,7 +36,23 @@ export const buildJoinAggregation = async ({
} }
const joinConfig = adapter.payload.collections[collection].config.joins const joinConfig = adapter.payload.collections[collection].config.joins
const aggregate: PipelineStage[] = [] const aggregate: PipelineStage[] = [
{
$sort: { createdAt: -1 },
},
]
if (query) {
aggregate.push({
$match: query,
})
}
if (limit) {
aggregate.push({
$limit: limit,
})
}
for (const slug of Object.keys(joinConfig)) { for (const slug of Object.keys(joinConfig)) {
for (const join of joinConfig[slug]) { for (const join of joinConfig[slug]) {
@@ -53,25 +72,26 @@ export const buildJoinAggregation = async ({
where: whereJoin, where: whereJoin,
} = joins?.[join.joinPath] || {} } = joins?.[join.joinPath] || {}
const $sort = buildSortParam({ const sort = buildSortParam({
config: adapter.payload.config, config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.flattenedFields, fields: adapter.payload.collections[slug].config.flattenedFields,
locale, locale,
sort: sortJoin, sort: sortJoin,
timestamps: true, timestamps: true,
}) })
const sortProperty = Object.keys(sort)[0]
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
const $match = await joinModel.buildQuery({ const $match = await joinModel.buildQuery({
locale, locale,
payload: adapter.payload, payload: adapter.payload,
session,
where: whereJoin, where: whereJoin,
}) })
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [ const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
{ $match }, { $match },
{ {
$sort, $sort: { [sortProperty]: sortDirection },
}, },
] ]
@@ -169,8 +189,8 @@ export const buildJoinAggregation = async ({
} }
} }
if (!aggregate.length) { if (projection) {
return aggregate.push({ $project: projection })
} }
return aggregate return aggregate

View File

@@ -1,4 +1,4 @@
import type { Field, FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload' import type { FieldAffectingData, FlattenedField, SelectMode, SelectType } from 'payload'
import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared' import { deepCopyObjectSimple, fieldAffectsData, getSelectMode } from 'payload/shared'
@@ -29,11 +29,6 @@ const addFieldToProjection = ({
} }
} }
const blockTypeField: Field = {
name: 'blockType',
type: 'text',
}
const traverseFields = ({ const traverseFields = ({
adapter, adapter,
databaseSchemaPath = '', databaseSchemaPath = '',
@@ -133,14 +128,6 @@ const traverseFields = ({
(selectMode === 'include' && blocksSelect[block.slug] === true) || (selectMode === 'include' && blocksSelect[block.slug] === true) ||
(selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined') (selectMode === 'exclude' && typeof blocksSelect[block.slug] === 'undefined')
) { ) {
addFieldToProjection({
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
field: blockTypeField,
projection,
withinLocalizedField: fieldWithinLocalizedField,
})
traverseFields({ traverseFields({
adapter, adapter,
databaseSchemaPath: fieldDatabaseSchemaPath, databaseSchemaPath: fieldDatabaseSchemaPath,
@@ -166,13 +153,7 @@ const traverseFields = ({
if (blockSelectMode === 'include') { if (blockSelectMode === 'include') {
blocksSelect[block.slug]['id'] = true blocksSelect[block.slug]['id'] = true
addFieldToProjection({ blocksSelect[block.slug]['blockType'] = true
adapter,
databaseSchemaPath: fieldDatabaseSchemaPath,
field: blockTypeField,
projection,
withinLocalizedField: fieldWithinLocalizedField,
})
} }
traverseFields({ traverseFields({

View File

@@ -1,128 +0,0 @@
import type { ClientSession, CollationOptions, Collection, Document } from 'mongodb'
import type { PipelineStage } from 'mongoose'
import type { PaginatedDocs } from 'payload'
import type { MongooseAdapter } from '../index.js'
export const findMany = async ({
adapter,
collation,
collection,
joinAgreggation,
limit,
page = 1,
pagination,
projection,
query = {},
session,
skip,
sort,
useEstimatedCount,
}: {
adapter: MongooseAdapter
collation?: CollationOptions
collection: Collection
joinAgreggation?: PipelineStage[]
limit?: number
page?: number
pagination?: boolean
projection?: Record<string, unknown>
query?: Record<string, unknown>
session?: ClientSession
skip?: number
sort?: Record<string, -1 | 1>
useEstimatedCount?: boolean
}): Promise<PaginatedDocs> => {
if (!skip) {
skip = (page - 1) * (limit ?? 0)
}
let docsPromise: Promise<Document[]>
let countPromise: Promise<null | number> = 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<Record<string, unknown>>
return result
}

View File

@@ -1,27 +0,0 @@
import type { Where } from 'payload'
export const getHasNearConstraint = (where?: Where): boolean => {
if (!where) {
return false
}
for (const key in where) {
const value = where[key]
if (Array.isArray(value) && ['AND', 'OR'].includes(key.toUpperCase())) {
for (const where of value) {
if (getHasNearConstraint(where)) {
return true
}
}
}
for (const key in value) {
if (key === 'near') {
return true
}
}
}
return false
}

View File

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

View File

@@ -1,9 +1,8 @@
import { flattenAllFields, type Field, type SanitizedConfig } from 'payload' import type { Field, SanitizedConfig } from 'payload'
import { Types } from 'mongoose' import { Types } from 'mongoose'
import { transform } from './transform.js' import { sanitizeRelationshipIDs } from './sanitizeRelationshipIDs.js'
import { MongooseAdapter } from '..'
const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => { const flattenRelationshipValues = (obj: Record<string, any>, prefix = ''): Record<string, any> => {
return Object.keys(obj).reduce( return Object.keys(obj).reduce(
@@ -272,8 +271,8 @@ const relsData = {
}, },
} }
describe('transform', () => { describe('sanitizeRelationshipIDs', () => {
it('should sanitize relationships with transform write', () => { it('should sanitize relationships', () => {
const data = { const data = {
...relsData, ...relsData,
array: [ array: [
@@ -349,19 +348,12 @@ describe('transform', () => {
} }
const flattenValuesBefore = Object.values(flattenRelationshipValues(data)) const flattenValuesBefore = Object.values(flattenRelationshipValues(data))
const mockAdapter = { payload: { config } } as MongooseAdapter sanitizeRelationshipIDs({ config, data, fields: config.collections[0].fields })
const fields = flattenAllFields({ fields: config.collections[0].fields })
transform({ type: 'write', adapter: mockAdapter, data, fields })
const flattenValuesAfter = Object.values(flattenRelationshipValues(data)) const flattenValuesAfter = Object.values(flattenRelationshipValues(data))
flattenValuesAfter.forEach((value, i) => { flattenValuesAfter.forEach((value, i) => {
expect(value).toBeInstanceOf(Types.ObjectId) expect(value).toBeInstanceOf(Types.ObjectId)
expect(flattenValuesBefore[i]).toBe(value.toHexString()) expect(flattenValuesBefore[i]).toBe(value.toHexString())
}) })
transform({ type: 'read', adapter: mockAdapter, data, fields })
}) })
}) })

View File

@@ -0,0 +1,156 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import { Types } from 'mongoose'
import { APIError, traverseFields } from 'payload'
import { fieldAffectsData } from 'payload/shared'
type Args = {
config: SanitizedConfig
data: Record<string, unknown>
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<string, unknown> => {
const sanitize: TraverseFieldsCallback = ({ field, ref }) => {
if (!ref || typeof ref !== 'object') {
return
}
if (field.type === 'relationship' || field.type === 'upload') {
if (!ref[field.name]) {
return
}
// handle localized relationships
if (config.localization && 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
}

View File

@@ -1,385 +0,0 @@
import type {
CollectionConfig,
DateField,
FlattenedField,
JoinField,
RelationshipField,
SanitizedConfig,
TraverseFlattenedFieldsCallback,
UploadField,
} from 'payload'
import { Types } from 'mongoose'
import { traverseFields } from 'payload'
import { fieldAffectsData, fieldIsVirtual } from 'payload/shared'
import type { MongooseAdapter } from '../index.js'
type Args = {
adapter: MongooseAdapter
data: Record<string, unknown> | Record<string, unknown>[]
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<string, unknown>
validateRelationships?: boolean
value?: unknown
}) => {
if (field.type === 'join') {
if (
operation === 'read' &&
value &&
typeof value === 'object' &&
'docs' in value &&
Array.isArray(value.docs)
) {
for (let i = 0; i < value.docs.length; i++) {
const item = value.docs[i]
if (item instanceof Types.ObjectId) {
value.docs[i] = item.toHexString()
}
}
}
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<string, unknown>
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<string, unknown>,
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<string, unknown>,
validateRelationships,
value: ref[field.name],
})
}
}
}
traverseFields({ callback: sanitize, fillEmpty: false, flattenedFields: fields, ref: data })
}

View File

@@ -126,11 +126,6 @@ export const promise = async <T>({
case 'point': { case 'point': {
if (Array.isArray(siblingData[field.name])) { 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) => { siblingData[field.name] = (siblingData[field.name] as string[]).map((coordinate, i) => {
if (typeof coordinate === 'string') { if (typeof coordinate === 'string') {
const value = siblingData[field.name][i] as string const value = siblingData[field.name][i] as string

View File

@@ -875,12 +875,7 @@ export type PointFieldValidation = Validate<
PointField 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 lng = parseFloat(String(value[0]))
const lat = parseFloat(String(value[1])) const lat = parseFloat(String(value[1]))
if ( if (

View File

@@ -1362,10 +1362,7 @@ export { sanitizeJoinParams } from './utilities/sanitizeJoinParams.js'
export { sanitizePopulateParam } from './utilities/sanitizePopulateParam.js' export { sanitizePopulateParam } from './utilities/sanitizePopulateParam.js'
export { sanitizeSelectParam } from './utilities/sanitizeSelectParam.js' export { sanitizeSelectParam } from './utilities/sanitizeSelectParam.js'
export { traverseFields } from './utilities/traverseFields.js' export { traverseFields } from './utilities/traverseFields.js'
export type { export type { TraverseFieldsCallback } from './utilities/traverseFields.js'
TraverseFieldsCallback,
TraverseFlattenedFieldsCallback,
} from './utilities/traverseFields.js'
export { buildVersionCollectionFields } from './versions/buildCollectionFields.js' export { buildVersionCollectionFields } from './versions/buildCollectionFields.js'
export { buildVersionGlobalFields } from './versions/buildGlobalFields.js' export { buildVersionGlobalFields } from './versions/buildGlobalFields.js'
export { versionDefaults } from './versions/defaults.js' export { versionDefaults } from './versions/defaults.js'

View File

@@ -1,12 +1,4 @@
import type { import type { ArrayField, BlocksField, Field, TabAsField } from '../fields/config/types.js'
ArrayField,
BlocksField,
Field,
FlattenedArrayField,
FlattenedBlock,
FlattenedField,
TabAsField,
} from '../fields/config/types.js'
import { fieldHasSubFields } from '../fields/config/types.js' import { fieldHasSubFields } from '../fields/config/types.js'
@@ -20,7 +12,7 @@ const traverseArrayOrBlocksField = ({
callback: TraverseFieldsCallback callback: TraverseFieldsCallback
data: Record<string, unknown>[] data: Record<string, unknown>[]
field: ArrayField | BlocksField field: ArrayField | BlocksField
fillEmpty?: boolean fillEmpty: boolean
parentRef?: unknown parentRef?: unknown
}) => { }) => {
if (fillEmpty) { if (fillEmpty) {
@@ -36,23 +28,20 @@ const traverseArrayOrBlocksField = ({
} }
for (const ref of data) { for (const ref of data) {
let fields: Field[] let fields: Field[]
let flattenedFields: FlattenedField[]
if (field.type === 'blocks' && typeof ref?.blockType === 'string') { if (field.type === 'blocks' && typeof ref?.blockType === 'string') {
const block = field.blocks.find((block) => block.slug === ref.blockType) as FlattenedBlock const block = field.blocks.find((block) => block.slug === ref.blockType)
fields = block?.fields fields = block?.fields
flattenedFields = block?.flattenedFields
} else if (field.type === 'array') { } else if (field.type === 'array') {
fields = field.fields fields = field.fields
flattenedFields = (field as FlattenedArrayField)?.flattenedFields
} }
if (flattenedFields || fields) { if (fields) {
traverseFields({ callback, fields, fillEmpty, flattenedFields, parentRef, ref }) traverseFields({ callback, fields, fillEmpty, parentRef, ref })
} }
} }
} }
type TraverseFieldsCallbackArgs = { export type TraverseFieldsCallback = (args: {
/** /**
* The current field * The current field
*/ */
@@ -69,45 +58,12 @@ type TraverseFieldsCallbackArgs = {
* The current reference object * The current reference object
*/ */
ref?: Record<string, unknown> | unknown ref?: Record<string, unknown> | 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<string, unknown> | unknown
/**
* The current reference object
*/
ref?: Record<string, unknown> | unknown
}) => boolean | void }) => boolean | void
type TraverseFlattenedFieldsArgs = {
callback: TraverseFlattenedFieldsCallback
fields?: Field[]
/** fill empty properties to use this without data */
fillEmpty?: boolean
flattenedFields: FlattenedField[]
parentRef?: Record<string, unknown> | unknown
ref?: Record<string, unknown> | unknown
}
type TraverseFieldsArgs = { type TraverseFieldsArgs = {
callback: TraverseFieldsCallback callback: TraverseFieldsCallback
fields: (Field | FlattenedField | TabAsField)[] fields: (Field | TabAsField)[]
/** fill empty properties to use this without data */
fillEmpty?: boolean fillEmpty?: boolean
flattenedFields?: FlattenedField[]
parentRef?: Record<string, unknown> | unknown parentRef?: Record<string, unknown> | unknown
ref?: Record<string, unknown> | unknown ref?: Record<string, unknown> | unknown
} }
@@ -125,11 +81,10 @@ export const traverseFields = ({
callback, callback,
fields, fields,
fillEmpty = true, fillEmpty = true,
flattenedFields,
parentRef = {}, parentRef = {},
ref = {}, ref = {},
}: TraverseFieldsArgs | TraverseFlattenedFieldsArgs): void => { }: TraverseFieldsArgs): void => {
;(flattenedFields ?? fields).some((field) => { fields.some((field) => {
let skip = false let skip = false
const next = () => { const next = () => {
skip = true skip = true
@@ -139,16 +94,7 @@ export const traverseFields = ({
return return
} }
if ( if (callback && callback({ field, next, parentRef, ref })) {
callback &&
callback({
// @ts-expect-error compatibillity Field | FlattenedField
field,
next,
parentRef,
ref,
})
) {
return true return true
} }
@@ -193,7 +139,6 @@ export const traverseFields = ({
if ( if (
callback && callback &&
callback({ callback({
// @ts-expect-error compatibillity Field | FlattenedField
field: { ...tab, type: 'tab' }, field: { ...tab, type: 'tab' },
next, next,
parentRef: currentParentRef, parentRef: currentParentRef,
@@ -215,15 +160,12 @@ export const traverseFields = ({
return return
} }
if ( if (field.type !== 'tab' && (fieldHasSubFields(field) || field.type === 'blocks')) {
(flattenedFields || field.type !== 'tab') &&
(fieldHasSubFields(field as Field) || field.type === 'tab' || field.type === 'blocks')
) {
if ('name' in field && field.name) { if ('name' in field && field.name) {
currentParentRef = currentRef currentParentRef = currentRef
if (!ref[field.name]) { if (!ref[field.name]) {
if (fillEmpty) { if (fillEmpty) {
if (field.type === 'group' || field.type === 'tab') { if (field.type === 'group') {
ref[field.name] = {} ref[field.name] = {}
} else if (field.type === 'array' || field.type === 'blocks') { } else if (field.type === 'array' || field.type === 'blocks') {
if (field.localized) { if (field.localized) {
@@ -240,7 +182,7 @@ export const traverseFields = ({
} }
if ( if (
(field.type === 'group' || field.type === 'tab') && field.type === 'group' &&
field.localized && field.localized &&
currentRef && currentRef &&
typeof currentRef === 'object' typeof currentRef === 'object'
@@ -251,10 +193,9 @@ export const traverseFields = ({
callback, callback,
fields: field.fields, fields: field.fields,
fillEmpty, fillEmpty,
flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined,
parentRef: currentParentRef, parentRef: currentParentRef,
ref: currentRef[key], ref: currentRef[key],
} as TraverseFieldsArgs) })
} }
} }
return return
@@ -298,7 +239,6 @@ export const traverseFields = ({
callback, callback,
fields: field.fields, fields: field.fields,
fillEmpty, fillEmpty,
flattenedFields: 'flattenedFields' in field ? field.flattenedFields : undefined,
parentRef: currentParentRef, parentRef: currentParentRef,
ref: currentRef, ref: currentRef,
}) })

View File

@@ -53,12 +53,10 @@ describe('Relationship Fields', () => {
collection: versionedRelationshipFieldSlug, collection: versionedRelationshipFieldSlug,
data: { data: {
title: 'Version 1 Title', title: 'Version 1 Title',
relationshipField: [ relationshipField: {
{ value: relatedDoc.id,
value: relatedDoc.id, relationTo: collection1Slug,
relationTo: collection1Slug, },
},
],
}, },
}) })

View File

@@ -1134,30 +1134,6 @@ describe('Fields', () => {
expect(doc.localized).toEqual(localized) expect(doc.localized).toEqual(localized)
expect(doc.group).toMatchObject(group) 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', () => { describe('unique indexes', () => {

View File

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

View File

@@ -1633,10 +1633,7 @@ describe('Select', () => {
}, },
}) })
expect(res).toStrictEqual({ expect(Object.keys(res)).toStrictEqual(['id', 'text'])
id: res.id,
text: res.text,
})
}) })
it('should apply select with updateByID', async () => { it('should apply select with updateByID', async () => {
@@ -1649,18 +1646,13 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(res).toStrictEqual({ expect(Object.keys(res)).toStrictEqual(['id', 'text'])
id: res.id,
text: res.text,
})
}) })
it('should apply select with updateBulk', async () => { it('should apply select with updateBulk', async () => {
const post = await createPost() const post = await createPost()
const { const res = await payload.update({
docs: [res],
} = await payload.update({
collection: 'posts', collection: 'posts',
where: { where: {
id: { id: {
@@ -1671,10 +1663,7 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(res).toStrictEqual({ expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text'])
id: res.id,
text: res.text,
})
}) })
it('should apply select with deleteByID', async () => { it('should apply select with deleteByID', async () => {
@@ -1686,18 +1675,13 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(res).toStrictEqual({ expect(Object.keys(res)).toStrictEqual(['id', 'text'])
id: res.id,
text: res.text,
})
}) })
it('should apply select with deleteBulk', async () => { it('should apply select with deleteBulk', async () => {
const post = await createPost() const post = await createPost()
const { const res = await payload.delete({
docs: [res],
} = await payload.delete({
collection: 'posts', collection: 'posts',
where: { where: {
id: { id: {
@@ -1707,10 +1691,7 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(res).toStrictEqual({ expect(Object.keys(res.docs[0])).toStrictEqual(['id', 'text'])
id: res.id,
text: res.text,
})
}) })
it('should apply select with duplicate', async () => { it('should apply select with duplicate', async () => {
@@ -1722,10 +1703,7 @@ describe('Select', () => {
select: { text: true }, select: { text: true },
}) })
expect(res).toStrictEqual({ expect(Object.keys(res)).toStrictEqual(['id', 'text'])
id: res.id,
text: res.text,
})
}) })
}) })