Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,8 @@ jobs:
- fields__collections__Text
- fields__collections__UI
- fields__collections__Upload
- fields__collections__UploadPoly
- fields__collections__UploadMultiPoly
- group-by
- folders
- hooks
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ test/.localstack
test/google-cloud-storage
test/azurestoragedata/
/media-without-delete-access
/media-documents


licenses.csv
39 changes: 38 additions & 1 deletion docs/fields/upload.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const MyUploadField: Field = {
| Option | Description |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
| **`relationTo`** \* | Provide a single collection `slug` to allow this field to accept a relation to. **Note: the related collection must be configured to support Uploads.** |
| **`relationTo`** \* | Provide a single collection `slug` or an array of slugs to allow this field to accept a relation to. **Note: the related collections must be configured to support Uploads.** |
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). |
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
Expand Down Expand Up @@ -140,3 +140,40 @@ The `upload` field on its own is used to reference documents in an upload collec
relationship. If you wish to allow an editor to visit the upload document and see where it is being used, you may use
the `join` field in the upload enabled collection. Read more about bi-directional relationships using
the [Join field](./join)

## Polymorphic Uploads

Upload fields can reference multiple upload collections by providing an array of collection slugs to the `relationTo` property.

```ts
import type { CollectionConfig } from 'payload'

export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'media',
type: 'upload',
relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
},
],
}
```

This can be combined with the `hasMany` property to allow multiple uploads from multiple collections.

```ts
import type { CollectionConfig } from 'payload'

export const ExampleCollection: CollectionConfig = {
slug: 'example-collection',
fields: [
{
name: 'media',
type: 'upload',
relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
hasMany: true, // allows multiple uploads
},
],
}
```
129 changes: 72 additions & 57 deletions packages/graphql/src/schema/fieldToSchemaMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,19 @@ import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
import { combineParentName } from '../utilities/combineParentName.js'
import { formatName } from '../utilities/formatName.js'
import { formatOptions } from '../utilities/formatOptions.js'
import { resolveSelect} from '../utilities/select.js'
import { resolveSelect } from '../utilities/select.js'
import { buildObjectType, type ObjectTypeConfig } from './buildObjectType.js'
import { isFieldNullable } from './isFieldNullable.js'
import { withNullableType } from './withNullableType.js'

function formattedNameResolver({
field,
...rest
}: { field: Field } & GraphQLFieldConfig<any, Context, any>): GraphQLFieldConfig<any, Context, any> {
}: { field: Field } & GraphQLFieldConfig<any, Context, any>): GraphQLFieldConfig<
any,
Context,
any
> {
if ('name' in field) {
if (formatName(field.name) !== field.name) {
return {
Expand Down Expand Up @@ -973,6 +977,10 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
let type
let relationToType = null

const graphQLCollections = config.collections.filter(
(collectionConfig) => collectionConfig.graphQL !== false,
)

if (Array.isArray(relationTo)) {
relationToType = new GraphQLEnumType({
name: `${relationshipName}_RelationTo`,
Expand Down Expand Up @@ -1073,39 +1081,44 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
const createPopulationPromise = async (relatedDoc, i) => {
let id = relatedDoc
let collectionSlug = field.relationTo
const isValidGraphQLCollection = isRelatedToManyCollections
? graphQLCollections.some((collection) => collectionSlug.includes(collection.slug))
: graphQLCollections.some((collection) => collectionSlug === collection.slug)

if (isRelatedToManyCollections) {
collectionSlug = relatedDoc.relationTo
id = relatedDoc.value
}
if (isValidGraphQLCollection) {
if (isRelatedToManyCollections) {
collectionSlug = relatedDoc.relationTo
id = relatedDoc.value
}

const result = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
select,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
const result = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: collectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
select,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)

if (result) {
if (isRelatedToManyCollections) {
results.push({
relationTo: collectionSlug,
value: {
...result,
collection: collectionSlug,
},
})
} else {
results.push(result)
if (result) {
if (isRelatedToManyCollections) {
results.push({
relationTo: collectionSlug,
value: {
...result,
collection: collectionSlug,
},
})
} else {
results.push(result)
}
}
}
}
Expand All @@ -1127,34 +1140,36 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
}

if (id) {
const relatedDocument = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: relatedCollectionSlug,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
select,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)
if (graphQLCollections.some((collection) => collection.slug === relatedCollectionSlug)) {
const relatedDocument = await context.req.payloadDataLoader.load(
createDataloaderCacheKey({
collectionSlug: relatedCollectionSlug as string,
currentDepth: 0,
depth: 0,
docID: id,
draft,
fallbackLocale,
locale,
overrideAccess: false,
select,
showHiddenFields: false,
transactionID: context.req.transactionID,
}),
)

if (relatedDocument) {
if (isRelatedToManyCollections) {
return {
relationTo: relatedCollectionSlug,
value: {
...relatedDocument,
collection: relatedCollectionSlug,
},
if (relatedDocument) {
if (isRelatedToManyCollections) {
return {
relationTo: relatedCollectionSlug,
value: {
...relatedDocument,
collection: relatedCollectionSlug,
},
}
}
}

return relatedDocument
return relatedDocument
}
}

return null
Expand Down
Loading
Loading