feat!: join field (#7518)

## Description

- Adds a new "join" field type to Payload and is supported by all database adapters
- The UI uses a table view for the new field
- `db-mongodb` changes relationships to be stored as ObjectIDs instead of strings (for now querying works using both types internally to the DB so no data migration should be necessary unless you're querying directly, see breaking changes for details
- Adds a reusable traverseFields utility to Payload to make it easier to work with nested fields, used internally and for plugin maintainers

```ts
export const Categories: CollectionConfig = {
    slug: 'categories',
    fields: [
        {
            name: 'relatedPosts',
            type: 'join',
            collection: 'posts',
            on: 'category',
        }
    ]
}
```

BREAKING CHANGES:
All mongodb relationship and upload values will be stored as MongoDB ObjectIDs instead of strings going forward. If you have existing data and you are querying data directly, outside of Payload's APIs, you get different results. For example, a `contains` query will no longer works given a partial ID of a relationship since the ObjectID requires the whole identifier to work. 

---------

Co-authored-by: Jacob Fletcher <jacobsfletch@gmail.com>
Co-authored-by: James <james@trbl.design>
This commit is contained in:
Dan Ribbens
2024-09-20 11:10:16 -04:00
committed by GitHub
parent b51d2bcb39
commit 6ef2bdea15
189 changed files with 11076 additions and 5882 deletions

View File

@@ -38,13 +38,14 @@
"bson-objectid": "2.0.4",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "10.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
"@types/mongoose-aggregate-paginate-v2": "1.0.6",
"mongodb": "4.17.1",
"mongodb-memory-server": "^9",
"payload": "workspace:*"

View File

@@ -3,6 +3,7 @@ import type { Create, Document, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const create: Create = async function create(
@@ -12,8 +13,15 @@ export const create: Create = async function create(
const Model = this.collections[collection]
const options = await withSession(this, req)
let doc
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.collections[collection].config.fields,
})
try {
;[doc] = await Model.create([data], options)
;[doc] = await Model.create([sanitizedData], options)
} catch (error) {
handleError({ collection, error, req })
}

View File

@@ -3,6 +3,7 @@ import type { CreateGlobal, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const createGlobal: CreateGlobal = async function createGlobal(
@@ -10,10 +11,16 @@ export const createGlobal: CreateGlobal = async function createGlobal(
{ slug, data, req = {} as PayloadRequest },
) {
const Model = this.globals
const global = {
globalType: slug,
...data,
}
const global = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
globalType: slug,
...data,
},
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
})
const options = await withSession(this, req)
let [result] = (await Model.create([global], options)) as any

View File

@@ -1,7 +1,13 @@
import type { CreateGlobalVersion, Document, PayloadRequest } from 'payload'
import {
buildVersionGlobalFields,
type CreateGlobalVersion,
type Document,
type PayloadRequest,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
@@ -21,22 +27,25 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
const VersionModel = this.versions[globalSlug]
const options = await withSession(this, req)
const [doc] = await VersionModel.create(
[
{
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
],
options,
req,
)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
})
const [doc] = await VersionModel.create([data], options, req)
await VersionModel.updateMany(
{

View File

@@ -1,7 +1,13 @@
import type { CreateVersion, Document, PayloadRequest } from 'payload'
import {
buildVersionCollectionFields,
type CreateVersion,
type Document,
type PayloadRequest,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const createVersion: CreateVersion = async function createVersion(
@@ -21,22 +27,25 @@ export const createVersion: CreateVersion = async function createVersion(
const VersionModel = this.versions[collectionSlug]
const options = await withSession(this, req)
const [doc] = await VersionModel.create(
[
{
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
],
options,
req,
)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
})
const [doc] = await VersionModel.create([data], options, req)
await VersionModel.updateMany(
{
@@ -48,7 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
},
{
parent: {
$eq: parent,
$eq: data.parent,
},
},
{

View File

@@ -6,12 +6,24 @@ import { flattenWhereToOperators } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildSortParam } from './queries/buildSortParam.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
export const find: Find = async function find(
this: MongooseAdapter,
{ collection, limit, locale, page, pagination, req = {} as PayloadRequest, sort: sortArg, where },
{
collection,
joins = {},
limit,
locale,
page,
pagination,
projection,
req = {} as PayloadRequest,
sort: sortArg,
where,
},
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
@@ -50,6 +62,7 @@ export const find: Find = async function find(
options,
page,
pagination,
projection,
sort,
useEstimatedCount,
}
@@ -88,7 +101,24 @@ export const find: Find = async function find(
}
}
const result = await Model.paginate(query, paginationOptions)
let result
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
limit,
locale,
query,
})
// build join aggregation
if (aggregate) {
result = await Model.aggregatePaginate(Model.aggregate(aggregate), paginationOptions)
} else {
result = await Model.paginate(query, paginationOptions)
}
const docs = JSON.parse(JSON.stringify(result.docs))
return {

View File

@@ -3,14 +3,16 @@ import type { Document, FindOne, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'
import { buildJoinAggregation } from './utilities/buildJoinAggregation.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { withSession } from './withSession.js'
export const findOne: FindOne = async function findOne(
this: MongooseAdapter,
{ collection, locale, req = {} as PayloadRequest, where },
{ collection, joins, locale, req = {} as PayloadRequest, where },
) {
const Model = this.collections[collection]
const collectionConfig = this.payload.collections[collection].config
const options: MongooseQueryOptions = {
...(await withSession(this, req)),
lean: true,
@@ -22,7 +24,22 @@ export const findOne: FindOne = async function findOne(
where,
})
const doc = await Model.findOne(query, {}, options)
const aggregate = await buildJoinAggregation({
adapter: this,
collection,
collectionConfig,
joins,
limit: 1,
locale,
query,
})
let doc
if (aggregate) {
;[doc] = await Model.aggregate(aggregate, options)
} else {
doc = await Model.findOne(query, {}, options)
}
if (!doc) {
return null

View File

@@ -2,6 +2,7 @@ import type { PaginateOptions } from 'mongoose'
import type { Init, SanitizedCollectionConfig } from 'payload'
import mongoose from 'mongoose'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { buildVersionCollectionFields, buildVersionGlobalFields } from 'payload'
@@ -40,12 +41,16 @@ export const init: Init = function init(this: MongooseAdapter) {
}),
)
if (Object.keys(collection.joins).length > 0) {
versionSchema.plugin(mongooseAggregatePaginate)
}
const model = mongoose.model(
versionModelName,
versionSchema,
this.autoPluralization === true ? undefined : versionModelName,
) as CollectionModel
// this.payload.versions[collection.slug] = model;
this.versions[collection.slug] = model
}

View File

@@ -1,6 +1,7 @@
import type { PaginateOptions, Schema } from 'mongoose'
import type { SanitizedCollectionConfig, SanitizedConfig } from 'payload'
import mongooseAggregatePaginate from 'mongoose-aggregate-paginate-v2'
import paginate from 'mongoose-paginate-v2'
import { getBuildQueryPlugin } from '../queries/buildQuery.js'
@@ -42,5 +43,9 @@ export const buildCollectionSchema = (
.plugin<any, PaginateOptions>(paginate, { useEstimatedCount: true })
.plugin(getBuildQueryPlugin({ collectionSlug: collection.slug }))
if (Object.keys(collection.joins).length > 0) {
schema.plugin(mongooseAggregatePaginate)
}
return schema
}

View File

@@ -165,7 +165,7 @@ export async function buildSearchParam({
const subQuery = priorQueryResult.value
const result = await SubModel.find(subQuery, subQueryOptions)
const $in = result.map((doc) => doc._id.toString())
const $in = result.map((doc) => doc._id)
// If it is the last recursion
// then pass through the search param

View File

@@ -1,5 +1,6 @@
import type { Field, TabAsField } from 'payload'
import ObjectIdImport from 'bson-objectid'
import mongoose from 'mongoose'
import { createArrayFromCommaDelineated } from 'payload'
@@ -11,6 +12,8 @@ type SanitizeQueryValueArgs = {
val: any
}
const ObjectId = (ObjectIdImport.default ||
ObjectIdImport) as unknown as typeof ObjectIdImport.default
export const sanitizeQueryValue = ({
field,
hasCustomID,
@@ -26,21 +29,49 @@ export const sanitizeQueryValue = ({
let formattedOperator = operator
// Disregard invalid _ids
if (path === '_id' && typeof val === 'string' && val.split(',').length === 1) {
if (!hasCustomID) {
const isValid = mongoose.Types.ObjectId.isValid(val)
if (path === '_id') {
if (typeof val === 'string' && val.split(',').length === 1) {
if (!hasCustomID) {
const isValid = mongoose.Types.ObjectId.isValid(val)
if (!isValid) {
return { operator: formattedOperator, val: undefined }
if (!isValid) {
return { operator: formattedOperator, val: undefined }
} else {
if (['in', 'not_in'].includes(operator)) {
formattedValue = createArrayFromCommaDelineated(formattedValue).map((id) =>
ObjectId(id),
)
} else {
formattedValue = ObjectId(val)
}
}
}
}
if (field.type === 'number') {
const parsedNumber = parseFloat(val)
if (field.type === 'number') {
const parsedNumber = parseFloat(val)
if (Number.isNaN(parsedNumber)) {
return { operator: formattedOperator, val: undefined }
if (Number.isNaN(parsedNumber)) {
return { operator: formattedOperator, val: undefined }
}
}
} else if (Array.isArray(val)) {
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (!hasCustomID) {
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(ObjectId(inVal))
}
}
if (field.type === 'number') {
const parsedNumber = parseFloat(inVal)
if (!Number.isNaN(parsedNumber)) {
newValues.push(parsedNumber)
}
}
return [...formattedValues, ...newValues]
}, [])
}
}
@@ -86,6 +117,13 @@ export const sanitizeQueryValue = ({
formattedValue.value &&
formattedValue.relationTo
) {
const { value } = formattedValue
const isValid = mongoose.Types.ObjectId.isValid(value)
if (isValid) {
formattedValue.value = ObjectId(value)
}
return {
rawQuery: {
$and: [
@@ -96,11 +134,11 @@ export const sanitizeQueryValue = ({
}
}
if (operator === 'in' && Array.isArray(formattedValue)) {
if (['in', 'not_in'].includes(operator) && Array.isArray(formattedValue)) {
formattedValue = formattedValue.reduce((formattedValues, inVal) => {
const newValues = [inVal]
if (mongoose.Types.ObjectId.isValid(inVal)) {
newValues.push(new mongoose.Types.ObjectId(inVal))
newValues.push(ObjectId(inVal))
}
const parsedNumber = parseFloat(inVal)
@@ -111,6 +149,12 @@ export const sanitizeQueryValue = ({
return [...formattedValues, ...newValues]
}, [])
}
if (operator === 'contains' && typeof formattedValue === 'string') {
if (mongoose.Types.ObjectId.isValid(formattedValue)) {
formattedValue = ObjectId(formattedValue)
}
}
}
// Set up specific formatting necessary by operators
@@ -152,7 +196,7 @@ export const sanitizeQueryValue = ({
}
if (path !== '_id' || (path === '_id' && hasCustomID && field.type === 'text')) {
if (operator === 'contains') {
if (operator === 'contains' && !mongoose.Types.ObjectId.isValid(formattedValue)) {
formattedValue = {
$options: 'i',
$regex: formattedValue.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&'),

View File

@@ -1,4 +1,11 @@
import type { IndexDefinition, IndexOptions, Model, PaginateModel, SchemaOptions } from 'mongoose'
import type {
AggregatePaginateModel,
IndexDefinition,
IndexOptions,
Model,
PaginateModel,
SchemaOptions,
} from 'mongoose'
import type {
ArrayField,
BlocksField,
@@ -9,6 +16,7 @@ import type {
EmailField,
Field,
GroupField,
JoinField,
JSONField,
NumberField,
Payload,
@@ -27,7 +35,10 @@ import type {
import type { BuildQueryArgs } from './queries/buildQuery.js'
export interface CollectionModel extends Model<any>, PaginateModel<any> {
export interface CollectionModel
extends Model<any>,
PaginateModel<any>,
AggregatePaginateModel<any> {
/** buildQuery is used to transform payload's where operator into what can be used by mongoose (e.g. id => _id) */
buildQuery: (args: BuildQueryArgs) => Promise<Record<string, unknown>> // TODO: Delete this
}
@@ -83,6 +94,7 @@ export type FieldToSchemaMap<TSchema> = {
date: FieldGeneratorFunction<TSchema, DateField>
email: FieldGeneratorFunction<TSchema, EmailField>
group: FieldGeneratorFunction<TSchema, GroupField>
join: FieldGeneratorFunction<TSchema, JoinField>
json: FieldGeneratorFunction<TSchema, JSONField>
number: FieldGeneratorFunction<TSchema, NumberField>
point: FieldGeneratorFunction<TSchema, PointField>

View File

@@ -3,6 +3,7 @@ import type { PayloadRequest, UpdateGlobal } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const updateGlobal: UpdateGlobal = async function updateGlobal(
@@ -17,7 +18,14 @@ export const updateGlobal: UpdateGlobal = async function updateGlobal(
}
let result
result = await Model.findOneAndUpdate({ globalType: slug }, data, options)
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.config.globals.find((global) => global.slug === slug).fields,
})
result = await Model.findOneAndUpdate({ globalType: slug }, sanitizedData, options)
result = JSON.parse(JSON.stringify(result))

View File

@@ -1,21 +1,27 @@
import type { PayloadRequest, TypeWithID, UpdateGlobalVersionArgs } from 'payload'
import {
buildVersionGlobalFields,
type PayloadRequest,
type TypeWithID,
type UpdateGlobalVersionArgs,
} from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export async function updateGlobalVersion<T extends TypeWithID>(
this: MongooseAdapter,
{
id,
global,
global: globalSlug,
locale,
req = {} as PayloadRequest,
versionData,
where,
}: UpdateGlobalVersionArgs<T>,
) {
const VersionModel = this.versions[global]
const VersionModel = this.versions[globalSlug]
const whereToUse = where || { id: { equals: id } }
const options = {
...(await withSession(this, req)),
@@ -29,7 +35,16 @@ export async function updateGlobalVersion<T extends TypeWithID>(
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data: versionData,
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const result = JSON.parse(JSON.stringify(doc))

View File

@@ -4,6 +4,7 @@ import type { MongooseAdapter } from './index.js'
import { handleError } from './utilities/handleError.js'
import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const updateOne: UpdateOne = async function updateOne(
@@ -26,8 +27,14 @@ export const updateOne: UpdateOne = async function updateOne(
let result
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.collections[collection].config.fields,
})
try {
result = await Model.findOneAndUpdate(query, data, options)
result = await Model.findOneAndUpdate(query, sanitizedData, options)
} catch (error) {
handleError({ collection, error, req })
}

View File

@@ -1,7 +1,8 @@
import type { PayloadRequest, UpdateVersion } from 'payload'
import { buildVersionCollectionFields, type PayloadRequest, type UpdateVersion } from 'payload'
import type { MongooseAdapter } from './index.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'
export const updateVersion: UpdateVersion = async function updateVersion(
@@ -22,7 +23,16 @@ export const updateVersion: UpdateVersion = async function updateVersion(
where: whereToUse,
})
const doc = await VersionModel.findOneAndUpdate(query, versionData, options)
const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data: versionData,
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collection].config,
),
})
const doc = await VersionModel.findOneAndUpdate(query, sanitizedData, options)
const result = JSON.parse(JSON.stringify(doc))

View File

@@ -0,0 +1,174 @@
import type { PipelineStage } from 'mongoose'
import type { CollectionSlug, JoinQuery, SanitizedCollectionConfig, Where } from 'payload'
import type { MongooseAdapter } from '../index.js'
import { buildSortParam } from '../queries/buildSortParam.js'
type BuildJoinAggregationArgs = {
adapter: MongooseAdapter
collection: CollectionSlug
collectionConfig: SanitizedCollectionConfig
joins: JoinQuery
// the number of docs to get at the top collection level
limit?: number
locale: string
// the where clause for the top collection
query?: Where
}
export const buildJoinAggregation = async ({
adapter,
collection,
collectionConfig,
joins,
limit,
locale,
query,
}: BuildJoinAggregationArgs): Promise<PipelineStage[] | undefined> => {
if (Object.keys(collectionConfig.joins).length === 0 || joins === false) {
return
}
const joinConfig = adapter.payload.collections[collection].config.joins
const aggregate: PipelineStage[] = [
{
$sort: { createdAt: -1 },
},
]
if (query) {
aggregate.push({
$match: query,
})
}
if (limit) {
aggregate.push({
$limit: limit,
})
}
for (const slug of Object.keys(joinConfig)) {
for (const join of joinConfig[slug]) {
const joinModel = adapter.collections[join.field.collection]
const {
limit: limitJoin = 10,
sort: sortJoin,
where: whereJoin,
} = joins?.[join.schemaPath] || {}
const sort = buildSortParam({
config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.fields,
locale,
sort: sortJoin || collectionConfig.defaultSort,
timestamps: true,
})
const sortProperty = Object.keys(sort)[0]
const sortDirection = sort[sortProperty] === 'asc' ? 1 : -1
const $match = await joinModel.buildQuery({
locale,
payload: adapter.payload,
where: whereJoin,
})
const pipeline: Exclude<PipelineStage, PipelineStage.Merge | PipelineStage.Out>[] = [
{ $match },
{
$sort: { [sortProperty]: sortDirection },
},
]
if (limitJoin > 0) {
pipeline.push({
$limit: limitJoin + 1,
})
}
if (adapter.payload.config.localization && locale === 'all') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${join.schemaPath}${code}`
aggregate.push(
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${code}`,
from: slug,
localField: '_id',
pipeline,
},
},
{
$addFields: {
[`${as}.docs`]: {
$map: {
as: 'doc',
in: '$$doc._id',
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
}, // Boolean indicating if more docs than limit
},
},
)
if (limitJoin > 0) {
aggregate.push({
$addFields: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, limitJoin],
},
},
})
}
})
} else {
const localeSuffix =
join.field.localized && adapter.payload.config.localization && locale ? `.${locale}` : ''
const as = `${join.schemaPath}${localeSuffix}`
aggregate.push(
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${localeSuffix}`,
from: slug,
localField: '_id',
pipeline,
},
},
{
$addFields: {
[`${as}.docs`]: {
$map: {
as: 'doc',
in: '$$doc._id',
input: `$${as}.docs`,
},
}, // Slicing the docs to match the limit
[`${as}.hasNextPage`]: {
$gt: [{ $size: `$${as}.docs` }, limitJoin || Number.MAX_VALUE],
}, // Boolean indicating if more docs than limit
},
},
)
if (limitJoin > 0) {
aggregate.push({
$addFields: {
[`${as}.docs`]: {
$slice: [`$${as}.docs`, limitJoin],
},
},
})
}
}
}
}
return aggregate
}

View File

@@ -0,0 +1,140 @@
import type { CollectionConfig, Field, SanitizedConfig, TraverseFieldsCallback } from 'payload'
import mongoose from 'mongoose'
import { 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
}): mongoose.Types.ObjectId | number | string => {
const customIDField = relatedCollection.fields.find(
(field) => fieldAffectsData(field) && field.name === 'id',
)
if (!customIDField) {
return new mongoose.Types.ObjectId(value)
}
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 (field.type === 'relationship' || field.type === 'upload') {
// handle localized relationships
if (config.localization && field.localized) {
const locales = config.localization.locales
const fieldRef = ref[field.name]
for (const { code } of locales) {
if (ref[field.name]?.[code]) {
const value = ref[field.name][code]
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, ref: data })
return data
}

View File

@@ -10,7 +10,7 @@ import type { MongooseAdapter } from './index.js'
export async function withSession(
db: MongooseAdapter,
req: PayloadRequest,
): Promise<{ session: ClientSession } | object> {
): Promise<{ session: ClientSession } | Record<string, never>> {
let transactionID = req.transactionID
if (transactionID instanceof Promise) {