feat: join field with polymorphic relationships (#9990)
### What? The join field had a limitation imposed that prevents it from targeting polymorphic relationship fields. With this change we can support any relationship fields. ### Why? Improves the functionality of join field. ### How? Extended the database adapters and removed the config sanitization that would throw an error when polymorphic relationships were used. Fixes #
This commit is contained in:
@@ -81,6 +81,11 @@ export const buildJoinAggregation = async ({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let polymorphicSuffix = ''
|
||||||
|
if (Array.isArray(join.targetField.relationTo)) {
|
||||||
|
polymorphicSuffix = '.value'
|
||||||
|
}
|
||||||
|
|
||||||
if (adapter.payload.config.localization && locale === 'all') {
|
if (adapter.payload.config.localization && locale === 'all') {
|
||||||
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
adapter.payload.config.localization.localeCodes.forEach((code) => {
|
||||||
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
|
||||||
@@ -89,7 +94,7 @@ export const buildJoinAggregation = async ({
|
|||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
as: `${as}.docs`,
|
as: `${as}.docs`,
|
||||||
foreignField: `${join.field.on}${code}`,
|
foreignField: `${join.field.on}${code}${polymorphicSuffix}`,
|
||||||
from: adapter.collections[slug].collection.name,
|
from: adapter.collections[slug].collection.name,
|
||||||
localField: versions ? 'parent' : '_id',
|
localField: versions ? 'parent' : '_id',
|
||||||
pipeline,
|
pipeline,
|
||||||
@@ -130,7 +135,7 @@ export const buildJoinAggregation = async ({
|
|||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
as: `${as}.docs`,
|
as: `${as}.docs`,
|
||||||
foreignField: `${join.field.on}${localeSuffix}`,
|
foreignField: `${join.field.on}${localeSuffix}${polymorphicSuffix}`,
|
||||||
from: adapter.collections[slug].collection.name,
|
from: adapter.collections[slug].collection.name,
|
||||||
localField: versions ? 'parent' : '_id',
|
localField: versions ? 'parent' : '_id',
|
||||||
pipeline,
|
pipeline,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const find: Find = async function find(
|
|||||||
|
|
||||||
return findMany({
|
return findMany({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
|
collectionSlug: collectionConfig.slug,
|
||||||
fields: collectionConfig.flattenedFields,
|
fields: collectionConfig.flattenedFields,
|
||||||
joins,
|
joins,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { traverseFields } from './traverseFields.js'
|
|||||||
|
|
||||||
type BuildFindQueryArgs = {
|
type BuildFindQueryArgs = {
|
||||||
adapter: DrizzleAdapter
|
adapter: DrizzleAdapter
|
||||||
|
collectionSlug?: string
|
||||||
depth: number
|
depth: number
|
||||||
fields: FlattenedField[]
|
fields: FlattenedField[]
|
||||||
joinQuery?: JoinQuery
|
joinQuery?: JoinQuery
|
||||||
@@ -32,6 +33,7 @@ export type Result = {
|
|||||||
// a collection field structure
|
// a collection field structure
|
||||||
export const buildFindManyArgs = ({
|
export const buildFindManyArgs = ({
|
||||||
adapter,
|
adapter,
|
||||||
|
collectionSlug,
|
||||||
depth,
|
depth,
|
||||||
fields,
|
fields,
|
||||||
joinQuery,
|
joinQuery,
|
||||||
@@ -74,6 +76,7 @@ export const buildFindManyArgs = ({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
_locales,
|
_locales,
|
||||||
adapter,
|
adapter,
|
||||||
|
collectionSlug,
|
||||||
currentArgs: result,
|
currentArgs: result,
|
||||||
currentTableName: tableName,
|
currentTableName: tableName,
|
||||||
depth,
|
depth,
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { buildFindManyArgs } from './buildFindManyArgs.js'
|
|||||||
|
|
||||||
type Args = {
|
type Args = {
|
||||||
adapter: DrizzleAdapter
|
adapter: DrizzleAdapter
|
||||||
|
collectionSlug?: string
|
||||||
fields: FlattenedField[]
|
fields: FlattenedField[]
|
||||||
tableName: string
|
tableName: string
|
||||||
versions?: boolean
|
versions?: boolean
|
||||||
@@ -20,6 +21,7 @@ type Args = {
|
|||||||
|
|
||||||
export const findMany = async function find({
|
export const findMany = async function find({
|
||||||
adapter,
|
adapter,
|
||||||
|
collectionSlug,
|
||||||
fields,
|
fields,
|
||||||
joins: joinQuery,
|
joins: joinQuery,
|
||||||
limit: limitArg,
|
limit: limitArg,
|
||||||
@@ -70,6 +72,7 @@ export const findMany = async function find({
|
|||||||
|
|
||||||
const findManyArgs = buildFindManyArgs({
|
const findManyArgs = buildFindManyArgs({
|
||||||
adapter,
|
adapter,
|
||||||
|
collectionSlug,
|
||||||
depth: 0,
|
depth: 0,
|
||||||
fields,
|
fields,
|
||||||
joinQuery,
|
joinQuery,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { chainMethods } from './chainMethods.js'
|
|||||||
type TraverseFieldArgs = {
|
type TraverseFieldArgs = {
|
||||||
_locales: Result
|
_locales: Result
|
||||||
adapter: DrizzleAdapter
|
adapter: DrizzleAdapter
|
||||||
|
collectionSlug?: string
|
||||||
currentArgs: Result
|
currentArgs: Result
|
||||||
currentTableName: string
|
currentTableName: string
|
||||||
depth?: number
|
depth?: number
|
||||||
@@ -42,6 +43,7 @@ type TraverseFieldArgs = {
|
|||||||
export const traverseFields = ({
|
export const traverseFields = ({
|
||||||
_locales,
|
_locales,
|
||||||
adapter,
|
adapter,
|
||||||
|
collectionSlug,
|
||||||
currentArgs,
|
currentArgs,
|
||||||
currentTableName,
|
currentTableName,
|
||||||
depth,
|
depth,
|
||||||
@@ -292,6 +294,7 @@ export const traverseFields = ({
|
|||||||
traverseFields({
|
traverseFields({
|
||||||
_locales,
|
_locales,
|
||||||
adapter,
|
adapter,
|
||||||
|
collectionSlug,
|
||||||
currentArgs,
|
currentArgs,
|
||||||
currentTableName,
|
currentTableName,
|
||||||
depth,
|
depth,
|
||||||
@@ -357,13 +360,26 @@ export const traverseFields = ({
|
|||||||
? adapter.tables[currentTableName].parent
|
? adapter.tables[currentTableName].parent
|
||||||
: adapter.tables[currentTableName].id
|
: adapter.tables[currentTableName].id
|
||||||
|
|
||||||
let joinQueryWhere: Where = {
|
let joinQueryWhere: Where
|
||||||
[field.on]: {
|
|
||||||
equals: rawConstraint(currentIDColumn),
|
if (Array.isArray(field.targetField.relationTo)) {
|
||||||
},
|
joinQueryWhere = {
|
||||||
|
[field.on]: {
|
||||||
|
equals: {
|
||||||
|
relationTo: collectionSlug,
|
||||||
|
value: rawConstraint(currentIDColumn),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
joinQueryWhere = {
|
||||||
|
[field.on]: {
|
||||||
|
equals: rawConstraint(currentIDColumn),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (where) {
|
if (where && Object.keys(where).length) {
|
||||||
joinQueryWhere = {
|
joinQueryWhere = {
|
||||||
and: [joinQueryWhere, where],
|
and: [joinQueryWhere, where],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export async function findOne<T extends TypeWithID>(
|
|||||||
|
|
||||||
const { docs } = await findMany({
|
const { docs } = await findMany({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
|
collectionSlug: collection,
|
||||||
fields: collectionConfig.flattenedFields,
|
fields: collectionConfig.flattenedFields,
|
||||||
joins,
|
joins,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|||||||
@@ -142,6 +142,12 @@ export const sanitizeQueryValue = ({
|
|||||||
collection: adapter.payload.collections[val.relationTo],
|
collection: adapter.payload.collections[val.relationTo],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isRawConstraint(val.value)) {
|
||||||
|
return {
|
||||||
|
operator,
|
||||||
|
value: val.value.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
operator,
|
operator,
|
||||||
value: idType === 'number' ? Number(val.value) : String(val.value),
|
value: idType === 'number' ? Number(val.value) : String(val.value),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(
|
|||||||
|
|
||||||
const result = await findMany({
|
const result = await findMany({
|
||||||
adapter: this,
|
adapter: this,
|
||||||
|
collectionSlug: collection,
|
||||||
fields,
|
fields,
|
||||||
joins,
|
joins,
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
ClientField,
|
ClientField,
|
||||||
Field,
|
Field,
|
||||||
FieldBase,
|
FieldBase,
|
||||||
|
JoinFieldClient,
|
||||||
LabelsClient,
|
LabelsClient,
|
||||||
RadioFieldClient,
|
RadioFieldClient,
|
||||||
RowFieldClient,
|
RowFieldClient,
|
||||||
@@ -229,6 +230,16 @@ export const createClientField = ({
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'join': {
|
||||||
|
const field = clientField as JoinFieldClient
|
||||||
|
|
||||||
|
field.targetField = {
|
||||||
|
relationTo: field.targetField.relationTo,
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'radio':
|
case 'radio':
|
||||||
// falls through
|
// falls through
|
||||||
case 'select': {
|
case 'select': {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { SanitizedJoin, SanitizedJoins } from '../../collections/config/types.js'
|
import type { SanitizedJoin, SanitizedJoins } from '../../collections/config/types.js'
|
||||||
import type { Config } from '../../config/types.js'
|
import type { Config } from '../../config/types.js'
|
||||||
import type { JoinField, RelationshipField, UploadField } from './types.js'
|
import type { FlattenedJoinField, JoinField, RelationshipField, UploadField } from './types.js'
|
||||||
|
|
||||||
import { APIError } from '../../errors/index.js'
|
import { APIError } from '../../errors/index.js'
|
||||||
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
|
import { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
|
||||||
@@ -12,7 +12,7 @@ export const sanitizeJoinField = ({
|
|||||||
joins,
|
joins,
|
||||||
}: {
|
}: {
|
||||||
config: Config
|
config: Config
|
||||||
field: JoinField
|
field: FlattenedJoinField | JoinField
|
||||||
joinPath?: string
|
joinPath?: string
|
||||||
joins?: SanitizedJoins
|
joins?: SanitizedJoins
|
||||||
}) => {
|
}) => {
|
||||||
@@ -74,9 +74,6 @@ export const sanitizeJoinField = ({
|
|||||||
if (!joinRelationship) {
|
if (!joinRelationship) {
|
||||||
throw new InvalidFieldJoin(join.field)
|
throw new InvalidFieldJoin(join.field)
|
||||||
}
|
}
|
||||||
if (Array.isArray(joinRelationship.relationTo)) {
|
|
||||||
throw new APIError('Join fields cannot be used with polymorphic relationships.')
|
|
||||||
}
|
|
||||||
|
|
||||||
join.targetField = joinRelationship
|
join.targetField = joinRelationship
|
||||||
|
|
||||||
@@ -85,6 +82,9 @@ export const sanitizeJoinField = ({
|
|||||||
// override the join field hasMany property to use whatever the relationship field has
|
// override the join field hasMany property to use whatever the relationship field has
|
||||||
field.hasMany = joinRelationship.hasMany
|
field.hasMany = joinRelationship.hasMany
|
||||||
|
|
||||||
|
// @ts-expect-error converting JoinField to FlattenedJoinField to track targetField
|
||||||
|
field.targetField = join.targetField
|
||||||
|
|
||||||
if (!joins[field.collection]) {
|
if (!joins[field.collection]) {
|
||||||
joins[field.collection] = [join]
|
joins[field.collection] = [join]
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1425,7 +1425,7 @@ export type JoinField = {
|
|||||||
export type JoinFieldClient = {
|
export type JoinFieldClient = {
|
||||||
admin?: AdminClient &
|
admin?: AdminClient &
|
||||||
Pick<JoinField['admin'], 'allowCreate' | 'defaultColumns' | 'disableBulkEdit' | 'readOnly'>
|
Pick<JoinField['admin'], 'allowCreate' | 'defaultColumns' | 'disableBulkEdit' | 'readOnly'>
|
||||||
} & FieldBaseClient &
|
} & { targetField: Pick<RelationshipFieldClient, 'relationTo'> } & FieldBaseClient &
|
||||||
Pick<
|
Pick<
|
||||||
JoinField,
|
JoinField,
|
||||||
'collection' | 'defaultLimit' | 'defaultSort' | 'index' | 'maxDepth' | 'on' | 'type' | 'where'
|
'collection' | 'defaultLimit' | 'defaultSort' | 'index' | 'maxDepth' | 'on' | 'type' | 'where'
|
||||||
@@ -1451,6 +1451,10 @@ export type FlattenedTabAsField = {
|
|||||||
flattenedFields: FlattenedField[]
|
flattenedFields: FlattenedField[]
|
||||||
} & MarkRequired<TabAsField, 'name'>
|
} & MarkRequired<TabAsField, 'name'>
|
||||||
|
|
||||||
|
export type FlattenedJoinField = {
|
||||||
|
targetField: RelationshipField | UploadField
|
||||||
|
} & JoinField
|
||||||
|
|
||||||
export type FlattenedField =
|
export type FlattenedField =
|
||||||
| CheckboxField
|
| CheckboxField
|
||||||
| CodeField
|
| CodeField
|
||||||
@@ -1459,8 +1463,8 @@ export type FlattenedField =
|
|||||||
| FlattenedArrayField
|
| FlattenedArrayField
|
||||||
| FlattenedBlocksField
|
| FlattenedBlocksField
|
||||||
| FlattenedGroupField
|
| FlattenedGroupField
|
||||||
|
| FlattenedJoinField
|
||||||
| FlattenedTabAsField
|
| FlattenedTabAsField
|
||||||
| JoinField
|
|
||||||
| JSONField
|
| JSONField
|
||||||
| NumberField
|
| NumberField
|
||||||
| PointField
|
| PointField
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Field, FlattenedField } from '../fields/config/types.js'
|
import type { Field, FlattenedField, FlattenedJoinField } from '../fields/config/types.js'
|
||||||
|
|
||||||
import { tabHasName } from '../fields/config/types.js'
|
import { tabHasName } from '../fields/config/types.js'
|
||||||
|
|
||||||
@@ -36,6 +36,11 @@ export const flattenAllFields = ({ fields }: { fields: Field[] }): FlattenedFiel
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'join': {
|
||||||
|
result.push(field as FlattenedJoinField)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
case 'tabs': {
|
case 'tabs': {
|
||||||
for (const tab of field.tabs) {
|
for (const tab of field.tabs) {
|
||||||
if (!tabHasName(tab)) {
|
if (!tabHasName(tab)) {
|
||||||
|
|||||||
@@ -61,7 +61,6 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
|
|||||||
relationTo,
|
relationTo,
|
||||||
} = props
|
} = props
|
||||||
const [Table, setTable] = useState<React.ReactNode>(null)
|
const [Table, setTable] = useState<React.ReactNode>(null)
|
||||||
|
|
||||||
const { getEntityConfig } = useConfig()
|
const { getEntityConfig } = useConfig()
|
||||||
|
|
||||||
const { permissions } = useAuth()
|
const { permissions } = useAuth()
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ const ObjectId = (ObjectIdImport.default ||
|
|||||||
* Recursively builds the default data for joined collection
|
* Recursively builds the default data for joined collection
|
||||||
*/
|
*/
|
||||||
const getInitialDrawerData = ({
|
const getInitialDrawerData = ({
|
||||||
|
collectionSlug,
|
||||||
docID,
|
docID,
|
||||||
fields,
|
fields,
|
||||||
segments,
|
segments,
|
||||||
}: {
|
}: {
|
||||||
|
collectionSlug: string
|
||||||
docID: number | string
|
docID: number | string
|
||||||
fields: ClientField[]
|
fields: ClientField[]
|
||||||
segments: string[]
|
segments: string[]
|
||||||
@@ -48,9 +50,15 @@ const getInitialDrawerData = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'relationship' || field.type === 'upload') {
|
if (field.type === 'relationship' || field.type === 'upload') {
|
||||||
|
let value: { relationTo: string; value: number | string } | number | string = docID
|
||||||
|
if (Array.isArray(field.relationTo)) {
|
||||||
|
value = {
|
||||||
|
relationTo: collectionSlug,
|
||||||
|
value: docID,
|
||||||
|
}
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
// TODO: Handle polymorphic https://github.com/payloadcms/payload/pull/9990
|
[field.name]: field.hasMany ? [value] : value,
|
||||||
[field.name]: field.hasMany ? [docID] : docID,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,12 +66,18 @@ const getInitialDrawerData = ({
|
|||||||
|
|
||||||
if (field.type === 'tab' || field.type === 'group') {
|
if (field.type === 'tab' || field.type === 'group') {
|
||||||
return {
|
return {
|
||||||
[field.name]: getInitialDrawerData({ docID, fields: field.fields, segments: nextSegments }),
|
[field.name]: getInitialDrawerData({
|
||||||
|
collectionSlug,
|
||||||
|
docID,
|
||||||
|
fields: field.fields,
|
||||||
|
segments: nextSegments,
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'array') {
|
if (field.type === 'array') {
|
||||||
const initialData = getInitialDrawerData({
|
const initialData = getInitialDrawerData({
|
||||||
|
collectionSlug,
|
||||||
docID,
|
docID,
|
||||||
fields: field.fields,
|
fields: field.fields,
|
||||||
segments: nextSegments,
|
segments: nextSegments,
|
||||||
@@ -79,6 +93,7 @@ const getInitialDrawerData = ({
|
|||||||
if (field.type === 'blocks') {
|
if (field.type === 'blocks') {
|
||||||
for (const block of field.blocks) {
|
for (const block of field.blocks) {
|
||||||
const blockInitialData = getInitialDrawerData({
|
const blockInitialData = getInitialDrawerData({
|
||||||
|
collectionSlug,
|
||||||
docID,
|
docID,
|
||||||
fields: block.fields,
|
fields: block.fields,
|
||||||
segments: nextSegments,
|
segments: nextSegments,
|
||||||
@@ -110,7 +125,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
path,
|
path,
|
||||||
} = props
|
} = props
|
||||||
|
|
||||||
const { id: docID } = useDocumentInfo()
|
const { id: docID, docConfig } = useDocumentInfo()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
config: { collections },
|
config: { collections },
|
||||||
@@ -126,9 +141,18 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let value: { relationTo: string; value: number | string } | number | string = docID
|
||||||
|
|
||||||
|
if (Array.isArray(field.targetField.relationTo)) {
|
||||||
|
value = {
|
||||||
|
relationTo: docConfig.slug,
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const where = {
|
const where = {
|
||||||
[on]: {
|
[on]: {
|
||||||
equals: docID,
|
equals: value,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,17 +163,18 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return where
|
return where
|
||||||
}, [docID, on, field.where])
|
}, [docID, field.targetField.relationTo, field.where, on, docConfig.slug])
|
||||||
|
|
||||||
const initialDrawerData = useMemo(() => {
|
const initialDrawerData = useMemo(() => {
|
||||||
const relatedCollection = collections.find((collection) => collection.slug === field.collection)
|
const relatedCollection = collections.find((collection) => collection.slug === field.collection)
|
||||||
|
|
||||||
return getInitialDrawerData({
|
return getInitialDrawerData({
|
||||||
|
collectionSlug: docConfig.slug,
|
||||||
docID,
|
docID,
|
||||||
fields: relatedCollection.fields,
|
fields: relatedCollection.fields,
|
||||||
segments: field.on.split('.'),
|
segments: field.on.split('.'),
|
||||||
})
|
})
|
||||||
}, [collections, field.on, docID, field.collection])
|
}, [collections, field.on, field.collection, docConfig.slug, docID])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -115,6 +115,30 @@ export const Categories: CollectionConfig = {
|
|||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
on: 'blocks.category',
|
on: 'blocks.category',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'polymorphic',
|
||||||
|
type: 'join',
|
||||||
|
collection: postsSlug,
|
||||||
|
on: 'polymorphic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'polymorphics',
|
||||||
|
type: 'join',
|
||||||
|
collection: postsSlug,
|
||||||
|
on: 'polymorphics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'localizedPolymorphic',
|
||||||
|
type: 'join',
|
||||||
|
collection: postsSlug,
|
||||||
|
on: 'localizedPolymorphic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'localizedPolymorphics',
|
||||||
|
type: 'join',
|
||||||
|
collection: postsSlug,
|
||||||
|
on: 'localizedPolymorphics',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'singulars',
|
name: 'singulars',
|
||||||
type: 'join',
|
type: 'join',
|
||||||
|
|||||||
@@ -53,6 +53,30 @@ export const Posts: CollectionConfig = {
|
|||||||
hasMany: true,
|
hasMany: true,
|
||||||
localized: true,
|
localized: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'polymorphic',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['categories', 'users'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'polymorphics',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['categories', 'users'],
|
||||||
|
hasMany: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'localizedPolymorphic',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['categories', 'users'],
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'localizedPolymorphics',
|
||||||
|
type: 'relationship',
|
||||||
|
relationTo: ['categories', 'users'],
|
||||||
|
hasMany: true,
|
||||||
|
localized: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'group',
|
name: 'group',
|
||||||
type: 'group',
|
type: 'group',
|
||||||
|
|||||||
@@ -291,6 +291,67 @@ test.describe('Join Field', () => {
|
|||||||
await expect(joinField.locator('tbody .row-1')).toContainText('Test Post 1 Updated')
|
await expect(joinField.locator('tbody .row-1')).toContainText('Test Post 1 Updated')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('should create join collection from polymorphic relationships', async () => {
|
||||||
|
await page.goto(categoriesURL.edit(categoryID))
|
||||||
|
const joinField = page.locator('#field-polymorphic.field-type.join')
|
||||||
|
await expect(joinField).toBeVisible()
|
||||||
|
await joinField.locator('.relationship-table__add-new').click()
|
||||||
|
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
|
||||||
|
await expect(drawer).toBeVisible()
|
||||||
|
const titleField = drawer.locator('#field-title')
|
||||||
|
await expect(titleField).toBeVisible()
|
||||||
|
await titleField.fill('Test polymorphic Post')
|
||||||
|
await expect(drawer.locator('#field-polymorphic')).toContainText('example')
|
||||||
|
await drawer.locator('button[id="action-save"]').click()
|
||||||
|
await expect(drawer).toBeHidden()
|
||||||
|
await expect(joinField.locator('tbody .row-1')).toContainText('Test polymorphic Post')
|
||||||
|
})
|
||||||
|
test('should create join collection from polymorphic, hasMany relationships', async () => {
|
||||||
|
await page.goto(categoriesURL.edit(categoryID))
|
||||||
|
const joinField = page.locator('#field-polymorphics.field-type.join')
|
||||||
|
await expect(joinField).toBeVisible()
|
||||||
|
await joinField.locator('.relationship-table__add-new').click()
|
||||||
|
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
|
||||||
|
await expect(drawer).toBeVisible()
|
||||||
|
const titleField = drawer.locator('#field-title')
|
||||||
|
await expect(titleField).toBeVisible()
|
||||||
|
await titleField.fill('Test polymorphic Post')
|
||||||
|
await expect(drawer.locator('#field-polymorphics')).toContainText('example')
|
||||||
|
await drawer.locator('button[id="action-save"]').click()
|
||||||
|
await expect(drawer).toBeHidden()
|
||||||
|
await expect(joinField.locator('tbody .row-1')).toContainText('Test polymorphic Post')
|
||||||
|
})
|
||||||
|
test('should create join collection from polymorphic localized relationships', async () => {
|
||||||
|
await page.goto(categoriesURL.edit(categoryID))
|
||||||
|
const joinField = page.locator('#field-localizedPolymorphic.field-type.join')
|
||||||
|
await expect(joinField).toBeVisible()
|
||||||
|
await joinField.locator('.relationship-table__add-new').click()
|
||||||
|
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
|
||||||
|
await expect(drawer).toBeVisible()
|
||||||
|
const titleField = drawer.locator('#field-title')
|
||||||
|
await expect(titleField).toBeVisible()
|
||||||
|
await titleField.fill('Test polymorphic Post')
|
||||||
|
await expect(drawer.locator('#field-localizedPolymorphic')).toContainText('example')
|
||||||
|
await drawer.locator('button[id="action-save"]').click()
|
||||||
|
await expect(drawer).toBeHidden()
|
||||||
|
await expect(joinField.locator('tbody .row-1')).toContainText('Test polymorphic Post')
|
||||||
|
})
|
||||||
|
test('should create join collection from polymorphic, hasMany, localized relationships', async () => {
|
||||||
|
await page.goto(categoriesURL.edit(categoryID))
|
||||||
|
const joinField = page.locator('#field-localizedPolymorphics.field-type.join')
|
||||||
|
await expect(joinField).toBeVisible()
|
||||||
|
await joinField.locator('.relationship-table__add-new').click()
|
||||||
|
const drawer = page.locator('[id^=doc-drawer_posts_1_]')
|
||||||
|
await expect(drawer).toBeVisible()
|
||||||
|
const titleField = drawer.locator('#field-title')
|
||||||
|
await expect(titleField).toBeVisible()
|
||||||
|
await titleField.fill('Test polymorphic Post')
|
||||||
|
await expect(drawer.locator('#field-localizedPolymorphics')).toContainText('example')
|
||||||
|
await drawer.locator('button[id="action-save"]').click()
|
||||||
|
await expect(drawer).toBeHidden()
|
||||||
|
await expect(joinField.locator('tbody .row-1')).toContainText('Test polymorphic Post')
|
||||||
|
})
|
||||||
|
|
||||||
test('should render empty relationship table when creating new document', async () => {
|
test('should render empty relationship table when creating new document', async () => {
|
||||||
await page.goto(categoriesURL.create)
|
await page.goto(categoriesURL.create)
|
||||||
const joinField = page.locator('#field-relatedPosts.field-type.join')
|
const joinField = page.locator('#field-relatedPosts.field-type.join')
|
||||||
|
|||||||
@@ -90,6 +90,26 @@ describe('Joins Field', () => {
|
|||||||
upload: uploadedImage,
|
upload: uploadedImage,
|
||||||
categories,
|
categories,
|
||||||
categoriesLocalized: categories,
|
categoriesLocalized: categories,
|
||||||
|
polymorphic: {
|
||||||
|
relationTo: 'categories',
|
||||||
|
value: category.id,
|
||||||
|
},
|
||||||
|
polymorphics: [
|
||||||
|
{
|
||||||
|
relationTo: 'categories',
|
||||||
|
value: category.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
localizedPolymorphic: {
|
||||||
|
relationTo: 'categories',
|
||||||
|
value: category.id,
|
||||||
|
},
|
||||||
|
localizedPolymorphics: [
|
||||||
|
{
|
||||||
|
relationTo: 'categories',
|
||||||
|
value: category.id,
|
||||||
|
},
|
||||||
|
],
|
||||||
group: {
|
group: {
|
||||||
category: category.id,
|
category: category.id,
|
||||||
camelCaseCategory: category.id,
|
camelCaseCategory: category.id,
|
||||||
@@ -216,6 +236,17 @@ describe('Joins Field', () => {
|
|||||||
expect(docs[0].upload.relatedPosts.docs).toHaveLength(10)
|
expect(docs[0].upload.relatedPosts.docs).toHaveLength(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should join on polymorphic relationships', async () => {
|
||||||
|
const categoryWithPosts = await payload.findByID({
|
||||||
|
collection: categoriesSlug,
|
||||||
|
id: category.id,
|
||||||
|
})
|
||||||
|
expect(categoryWithPosts.polymorphic.docs[0]).toHaveProperty('id')
|
||||||
|
expect(categoryWithPosts.polymorphics.docs[0]).toHaveProperty('id')
|
||||||
|
expect(categoryWithPosts.localizedPolymorphic.docs[0]).toHaveProperty('id')
|
||||||
|
expect(categoryWithPosts.localizedPolymorphics.docs[0]).toHaveProperty('id')
|
||||||
|
})
|
||||||
|
|
||||||
it('should filter joins using where query', async () => {
|
it('should filter joins using where query', async () => {
|
||||||
const categoryWithPosts = await payload.findByID({
|
const categoryWithPosts = await payload.findByID({
|
||||||
id: category.id,
|
id: category.id,
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export interface Config {
|
|||||||
'group.camelCasePosts': 'posts';
|
'group.camelCasePosts': 'posts';
|
||||||
arrayPosts: 'posts';
|
arrayPosts: 'posts';
|
||||||
blocksPosts: 'posts';
|
blocksPosts: 'posts';
|
||||||
|
polymorphic: 'posts';
|
||||||
|
polymorphics: 'posts';
|
||||||
|
localizedPolymorphic: 'posts';
|
||||||
|
localizedPolymorphics: 'posts';
|
||||||
filtered: 'posts';
|
filtered: 'posts';
|
||||||
hiddenPosts: 'hidden-posts';
|
hiddenPosts: 'hidden-posts';
|
||||||
singulars: 'singular';
|
singulars: 'singular';
|
||||||
@@ -123,6 +127,48 @@ export interface Post {
|
|||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
categories?: (string | Category)[] | null;
|
categories?: (string | Category)[] | null;
|
||||||
categoriesLocalized?: (string | Category)[] | null;
|
categoriesLocalized?: (string | Category)[] | null;
|
||||||
|
polymorphic?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'categories';
|
||||||
|
value: string | Category;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
} | null);
|
||||||
|
polymorphics?:
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
relationTo: 'categories';
|
||||||
|
value: string | Category;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
|
localizedPolymorphic?:
|
||||||
|
| ({
|
||||||
|
relationTo: 'categories';
|
||||||
|
value: string | Category;
|
||||||
|
} | null)
|
||||||
|
| ({
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
} | null);
|
||||||
|
localizedPolymorphics?:
|
||||||
|
| (
|
||||||
|
| {
|
||||||
|
relationTo: 'categories';
|
||||||
|
value: string | Category;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
relationTo: 'users';
|
||||||
|
value: string | User;
|
||||||
|
}
|
||||||
|
)[]
|
||||||
|
| null;
|
||||||
group?: {
|
group?: {
|
||||||
category?: (string | null) | Category;
|
category?: (string | null) | Category;
|
||||||
camelCaseCategory?: (string | null) | Category;
|
camelCaseCategory?: (string | null) | Category;
|
||||||
@@ -207,6 +253,22 @@ export interface Category {
|
|||||||
docs?: (string | Post)[] | null;
|
docs?: (string | Post)[] | null;
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean | null;
|
||||||
} | null;
|
} | null;
|
||||||
|
polymorphic?: {
|
||||||
|
docs?: (string | Post)[] | null;
|
||||||
|
hasNextPage?: boolean | null;
|
||||||
|
} | null;
|
||||||
|
polymorphics?: {
|
||||||
|
docs?: (string | Post)[] | null;
|
||||||
|
hasNextPage?: boolean | null;
|
||||||
|
} | null;
|
||||||
|
localizedPolymorphic?: {
|
||||||
|
docs?: (string | Post)[] | null;
|
||||||
|
hasNextPage?: boolean | null;
|
||||||
|
} | null;
|
||||||
|
localizedPolymorphics?: {
|
||||||
|
docs?: (string | Post)[] | null;
|
||||||
|
hasNextPage?: boolean | null;
|
||||||
|
} | null;
|
||||||
singulars?: {
|
singulars?: {
|
||||||
docs?: (string | Singular)[] | null;
|
docs?: (string | Singular)[] | null;
|
||||||
hasNextPage?: boolean | null;
|
hasNextPage?: boolean | null;
|
||||||
@@ -239,6 +301,23 @@ export interface Singular {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
|
* via the `definition` "users".
|
||||||
|
*/
|
||||||
|
export interface User {
|
||||||
|
id: string;
|
||||||
|
updatedAt: string;
|
||||||
|
createdAt: string;
|
||||||
|
email: string;
|
||||||
|
resetPasswordToken?: string | null;
|
||||||
|
resetPasswordExpiration?: string | null;
|
||||||
|
salt?: string | null;
|
||||||
|
hash?: string | null;
|
||||||
|
loginAttempts?: number | null;
|
||||||
|
lockUntil?: string | null;
|
||||||
|
password?: string | null;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "versions".
|
* via the `definition` "versions".
|
||||||
@@ -347,23 +426,6 @@ export interface RestrictedPost {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
|
||||||
* via the `definition` "users".
|
|
||||||
*/
|
|
||||||
export interface User {
|
|
||||||
id: string;
|
|
||||||
updatedAt: string;
|
|
||||||
createdAt: string;
|
|
||||||
email: string;
|
|
||||||
resetPasswordToken?: string | null;
|
|
||||||
resetPasswordExpiration?: string | null;
|
|
||||||
salt?: string | null;
|
|
||||||
hash?: string | null;
|
|
||||||
loginAttempts?: number | null;
|
|
||||||
lockUntil?: string | null;
|
|
||||||
password?: string | null;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* This interface was referenced by `Config`'s JSON-Schema
|
* This interface was referenced by `Config`'s JSON-Schema
|
||||||
* via the `definition` "payload-locked-documents".
|
* via the `definition` "payload-locked-documents".
|
||||||
@@ -481,6 +543,10 @@ export interface PostsSelect<T extends boolean = true> {
|
|||||||
category?: T;
|
category?: T;
|
||||||
categories?: T;
|
categories?: T;
|
||||||
categoriesLocalized?: T;
|
categoriesLocalized?: T;
|
||||||
|
polymorphic?: T;
|
||||||
|
polymorphics?: T;
|
||||||
|
localizedPolymorphic?: T;
|
||||||
|
localizedPolymorphics?: T;
|
||||||
group?:
|
group?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@@ -525,6 +591,10 @@ export interface CategoriesSelect<T extends boolean = true> {
|
|||||||
};
|
};
|
||||||
arrayPosts?: T;
|
arrayPosts?: T;
|
||||||
blocksPosts?: T;
|
blocksPosts?: T;
|
||||||
|
polymorphic?: T;
|
||||||
|
polymorphics?: T;
|
||||||
|
localizedPolymorphic?: T;
|
||||||
|
localizedPolymorphics?: T;
|
||||||
singulars?: T;
|
singulars?: T;
|
||||||
filtered?: T;
|
filtered?: T;
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
|
|||||||
Reference in New Issue
Block a user