Skip to content

Commit df40d0e

Browse files
authored
feat: add support for polymorphic uploads (#14363)
This PR adds support for polymorphic uploads similar to polymorphic relationships. Closes #13912 It works with bulk upload as well and in the bulk uploader you can choose which collection the files are uploaded into. You can enable this by adding an array of slugs into `relationTo`: ```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 }, ], } ``` It can also be combined with `hasMany`: ```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 }, ], } ``` **Known issue**: #12014 Filtering by polymorphic relationships is currently broken and I'll work on that in a separate PR.
1 parent f6194c6 commit df40d0e

File tree

24 files changed

+905
-375
lines changed

24 files changed

+905
-375
lines changed

.github/workflows/main.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ jobs:
296296
- fields__collections__Text
297297
- fields__collections__UI
298298
- fields__collections__Upload
299+
- fields__collections__UploadPoly
300+
- fields__collections__UploadMultiPoly
299301
- group-by
300302
- folders
301303
- hooks

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ test/.localstack
341341
test/google-cloud-storage
342342
test/azurestoragedata/
343343
/media-without-delete-access
344+
/media-documents
344345

345346

346347
licenses.csv

docs/fields/upload.mdx

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export const MyUploadField: Field = {
4747
| Option | Description |
4848
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
4949
| **`name`** \* | To be used as the property name when stored and retrieved from the database. [More details](/docs/fields/overview#field-names). |
50-
| **`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.** |
50+
| **`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.** |
5151
| **`filterOptions`** | A query to filter which options appear in the UI and validate against. [More details](#filtering-upload-options). |
5252
| **`hasMany`** | Boolean which, if set to true, allows this field to have many relations instead of only one. |
5353
| **`minRows`** | A number for the fewest allowed items during validation when a value is present. Used with hasMany. |
@@ -140,3 +140,40 @@ The `upload` field on its own is used to reference documents in an upload collec
140140
relationship. If you wish to allow an editor to visit the upload document and see where it is being used, you may use
141141
the `join` field in the upload enabled collection. Read more about bi-directional relationships using
142142
the [Join field](./join)
143+
144+
## Polymorphic Uploads
145+
146+
Upload fields can reference multiple upload collections by providing an array of collection slugs to the `relationTo` property.
147+
148+
```ts
149+
import type { CollectionConfig } from 'payload'
150+
151+
export const ExampleCollection: CollectionConfig = {
152+
slug: 'example-collection',
153+
fields: [
154+
{
155+
name: 'media',
156+
type: 'upload',
157+
relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
158+
},
159+
],
160+
}
161+
```
162+
163+
This can be combined with the `hasMany` property to allow multiple uploads from multiple collections.
164+
165+
```ts
166+
import type { CollectionConfig } from 'payload'
167+
168+
export const ExampleCollection: CollectionConfig = {
169+
slug: 'example-collection',
170+
fields: [
171+
{
172+
name: 'media',
173+
type: 'upload',
174+
relationTo: ['images', 'documents', 'videos'], // references multiple upload collections
175+
hasMany: true, // allows multiple uploads
176+
},
177+
],
178+
}
179+
```

packages/graphql/src/schema/fieldToSchemaMap.ts

Lines changed: 72 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -49,15 +49,19 @@ import { GraphQLJSON } from '../packages/graphql-type-json/index.js'
4949
import { combineParentName } from '../utilities/combineParentName.js'
5050
import { formatName } from '../utilities/formatName.js'
5151
import { formatOptions } from '../utilities/formatOptions.js'
52-
import { resolveSelect} from '../utilities/select.js'
52+
import { resolveSelect } from '../utilities/select.js'
5353
import { buildObjectType, type ObjectTypeConfig } from './buildObjectType.js'
5454
import { isFieldNullable } from './isFieldNullable.js'
5555
import { withNullableType } from './withNullableType.js'
5656

5757
function formattedNameResolver({
5858
field,
5959
...rest
60-
}: { field: Field } & GraphQLFieldConfig<any, Context, any>): GraphQLFieldConfig<any, Context, any> {
60+
}: { field: Field } & GraphQLFieldConfig<any, Context, any>): GraphQLFieldConfig<
61+
any,
62+
Context,
63+
any
64+
> {
6165
if ('name' in field) {
6266
if (formatName(field.name) !== field.name) {
6367
return {
@@ -973,6 +977,10 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
973977
let type
974978
let relationToType = null
975979

980+
const graphQLCollections = config.collections.filter(
981+
(collectionConfig) => collectionConfig.graphQL !== false,
982+
)
983+
976984
if (Array.isArray(relationTo)) {
977985
relationToType = new GraphQLEnumType({
978986
name: `${relationshipName}_RelationTo`,
@@ -1073,39 +1081,44 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
10731081
const createPopulationPromise = async (relatedDoc, i) => {
10741082
let id = relatedDoc
10751083
let collectionSlug = field.relationTo
1084+
const isValidGraphQLCollection = isRelatedToManyCollections
1085+
? graphQLCollections.some((collection) => collectionSlug.includes(collection.slug))
1086+
: graphQLCollections.some((collection) => collectionSlug === collection.slug)
10761087

1077-
if (isRelatedToManyCollections) {
1078-
collectionSlug = relatedDoc.relationTo
1079-
id = relatedDoc.value
1080-
}
1088+
if (isValidGraphQLCollection) {
1089+
if (isRelatedToManyCollections) {
1090+
collectionSlug = relatedDoc.relationTo
1091+
id = relatedDoc.value
1092+
}
10811093

1082-
const result = await context.req.payloadDataLoader.load(
1083-
createDataloaderCacheKey({
1084-
collectionSlug,
1085-
currentDepth: 0,
1086-
depth: 0,
1087-
docID: id,
1088-
draft,
1089-
fallbackLocale,
1090-
locale,
1091-
overrideAccess: false,
1092-
select,
1093-
showHiddenFields: false,
1094-
transactionID: context.req.transactionID,
1095-
}),
1096-
)
1094+
const result = await context.req.payloadDataLoader.load(
1095+
createDataloaderCacheKey({
1096+
collectionSlug: collectionSlug as string,
1097+
currentDepth: 0,
1098+
depth: 0,
1099+
docID: id,
1100+
draft,
1101+
fallbackLocale,
1102+
locale,
1103+
overrideAccess: false,
1104+
select,
1105+
showHiddenFields: false,
1106+
transactionID: context.req.transactionID,
1107+
}),
1108+
)
10971109

1098-
if (result) {
1099-
if (isRelatedToManyCollections) {
1100-
results.push({
1101-
relationTo: collectionSlug,
1102-
value: {
1103-
...result,
1104-
collection: collectionSlug,
1105-
},
1106-
})
1107-
} else {
1108-
results.push(result)
1110+
if (result) {
1111+
if (isRelatedToManyCollections) {
1112+
results.push({
1113+
relationTo: collectionSlug,
1114+
value: {
1115+
...result,
1116+
collection: collectionSlug,
1117+
},
1118+
})
1119+
} else {
1120+
results.push(result)
1121+
}
11091122
}
11101123
}
11111124
}
@@ -1127,34 +1140,36 @@ export const fieldToSchemaMap: FieldToSchemaMap = {
11271140
}
11281141

11291142
if (id) {
1130-
const relatedDocument = await context.req.payloadDataLoader.load(
1131-
createDataloaderCacheKey({
1132-
collectionSlug: relatedCollectionSlug,
1133-
currentDepth: 0,
1134-
depth: 0,
1135-
docID: id,
1136-
draft,
1137-
fallbackLocale,
1138-
locale,
1139-
overrideAccess: false,
1140-
select,
1141-
showHiddenFields: false,
1142-
transactionID: context.req.transactionID,
1143-
}),
1144-
)
1143+
if (graphQLCollections.some((collection) => collection.slug === relatedCollectionSlug)) {
1144+
const relatedDocument = await context.req.payloadDataLoader.load(
1145+
createDataloaderCacheKey({
1146+
collectionSlug: relatedCollectionSlug as string,
1147+
currentDepth: 0,
1148+
depth: 0,
1149+
docID: id,
1150+
draft,
1151+
fallbackLocale,
1152+
locale,
1153+
overrideAccess: false,
1154+
select,
1155+
showHiddenFields: false,
1156+
transactionID: context.req.transactionID,
1157+
}),
1158+
)
11451159

1146-
if (relatedDocument) {
1147-
if (isRelatedToManyCollections) {
1148-
return {
1149-
relationTo: relatedCollectionSlug,
1150-
value: {
1151-
...relatedDocument,
1152-
collection: relatedCollectionSlug,
1153-
},
1160+
if (relatedDocument) {
1161+
if (isRelatedToManyCollections) {
1162+
return {
1163+
relationTo: relatedCollectionSlug,
1164+
value: {
1165+
...relatedDocument,
1166+
collection: relatedCollectionSlug,
1167+
},
1168+
}
11541169
}
1155-
}
11561170

1157-
return relatedDocument
1171+
return relatedDocument
1172+
}
11581173
}
11591174

11601175
return null

0 commit comments

Comments
 (0)