Skip to content

Commit

Permalink
feat: join field with polymorphic relationships (#9990)
Browse files Browse the repository at this point in the history
### 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 #
  • Loading branch information
DanRibbens authored Dec 19, 2024
1 parent 07be617 commit d03658d
Show file tree
Hide file tree
Showing 19 changed files with 330 additions and 40 deletions.
9 changes: 7 additions & 2 deletions packages/db-mongodb/src/utilities/buildJoinAggregation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
adapter.payload.config.localization.localeCodes.forEach((code) => {
const as = `${versions ? `version.${join.joinPath}` : join.joinPath}${code}`
Expand All @@ -89,7 +94,7 @@ export const buildJoinAggregation = async ({
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${code}`,
foreignField: `${join.field.on}${code}${polymorphicSuffix}`,
from: adapter.collections[slug].collection.name,
localField: versions ? 'parent' : '_id',
pipeline,
Expand Down Expand Up @@ -130,7 +135,7 @@ export const buildJoinAggregation = async ({
{
$lookup: {
as: `${as}.docs`,
foreignField: `${join.field.on}${localeSuffix}`,
foreignField: `${join.field.on}${localeSuffix}${polymorphicSuffix}`,
from: adapter.collections[slug].collection.name,
localField: versions ? 'parent' : '_id',
pipeline,
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/src/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const find: Find = async function find(

return findMany({
adapter: this,
collectionSlug: collectionConfig.slug,
fields: collectionConfig.flattenedFields,
joins,
limit,
Expand Down
3 changes: 3 additions & 0 deletions packages/drizzle/src/find/buildFindManyArgs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { traverseFields } from './traverseFields.js'

type BuildFindQueryArgs = {
adapter: DrizzleAdapter
collectionSlug?: string
depth: number
fields: FlattenedField[]
joinQuery?: JoinQuery
Expand All @@ -32,6 +33,7 @@ export type Result = {
// a collection field structure
export const buildFindManyArgs = ({
adapter,
collectionSlug,
depth,
fields,
joinQuery,
Expand Down Expand Up @@ -74,6 +76,7 @@ export const buildFindManyArgs = ({
traverseFields({
_locales,
adapter,
collectionSlug,
currentArgs: result,
currentTableName: tableName,
depth,
Expand Down
3 changes: 3 additions & 0 deletions packages/drizzle/src/find/findMany.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import { buildFindManyArgs } from './buildFindManyArgs.js'

type Args = {
adapter: DrizzleAdapter
collectionSlug?: string
fields: FlattenedField[]
tableName: string
versions?: boolean
} & Omit<FindArgs, 'collection'>

export const findMany = async function find({
adapter,
collectionSlug,
fields,
joins: joinQuery,
limit: limitArg,
Expand Down Expand Up @@ -70,6 +72,7 @@ export const findMany = async function find({

const findManyArgs = buildFindManyArgs({
adapter,
collectionSlug,
depth: 0,
fields,
joinQuery,
Expand Down
26 changes: 21 additions & 5 deletions packages/drizzle/src/find/traverseFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { chainMethods } from './chainMethods.js'
type TraverseFieldArgs = {
_locales: Result
adapter: DrizzleAdapter
collectionSlug?: string
currentArgs: Result
currentTableName: string
depth?: number
Expand All @@ -42,6 +43,7 @@ type TraverseFieldArgs = {
export const traverseFields = ({
_locales,
adapter,
collectionSlug,
currentArgs,
currentTableName,
depth,
Expand Down Expand Up @@ -292,6 +294,7 @@ export const traverseFields = ({
traverseFields({
_locales,
adapter,
collectionSlug,
currentArgs,
currentTableName,
depth,
Expand Down Expand Up @@ -357,13 +360,26 @@ export const traverseFields = ({
? adapter.tables[currentTableName].parent
: adapter.tables[currentTableName].id

let joinQueryWhere: Where = {
[field.on]: {
equals: rawConstraint(currentIDColumn),
},
let joinQueryWhere: Where

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 = {
and: [joinQueryWhere, where],
}
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/src/findOne.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export async function findOne<T extends TypeWithID>(

const { docs } = await findMany({
adapter: this,
collectionSlug: collection,
fields: collectionConfig.flattenedFields,
joins,
limit: 1,
Expand Down
6 changes: 6 additions & 0 deletions packages/drizzle/src/queries/sanitizeQueryValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export const sanitizeQueryValue = ({
collection: adapter.payload.collections[val.relationTo],
})

if (isRawConstraint(val.value)) {
return {
operator,
value: val.value.value,
}
}
return {
operator,
value: idType === 'number' ? Number(val.value) : String(val.value),
Expand Down
1 change: 1 addition & 0 deletions packages/drizzle/src/queryDrafts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const queryDrafts: QueryDrafts = async function queryDrafts(

const result = await findMany({
adapter: this,
collectionSlug: collection,
fields,
joins,
limit,
Expand Down
11 changes: 11 additions & 0 deletions packages/payload/src/fields/config/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ClientField,
Field,
FieldBase,
JoinFieldClient,
LabelsClient,
RadioFieldClient,
RowFieldClient,
Expand Down Expand Up @@ -229,6 +230,16 @@ export const createClientField = ({
break
}

case 'join': {
const field = clientField as JoinFieldClient

field.targetField = {
relationTo: field.targetField.relationTo,
}

break
}

case 'radio':
// falls through
case 'select': {
Expand Down
10 changes: 5 additions & 5 deletions packages/payload/src/fields/config/sanitizeJoinField.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { SanitizedJoin, SanitizedJoins } from '../../collections/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 { InvalidFieldJoin } from '../../errors/InvalidFieldJoin.js'
Expand All @@ -12,7 +12,7 @@ export const sanitizeJoinField = ({
joins,
}: {
config: Config
field: JoinField
field: FlattenedJoinField | JoinField
joinPath?: string
joins?: SanitizedJoins
}) => {
Expand Down Expand Up @@ -74,9 +74,6 @@ export const sanitizeJoinField = ({
if (!joinRelationship) {
throw new InvalidFieldJoin(join.field)
}
if (Array.isArray(joinRelationship.relationTo)) {
throw new APIError('Join fields cannot be used with polymorphic relationships.')
}

join.targetField = joinRelationship

Expand All @@ -85,6 +82,9 @@ export const sanitizeJoinField = ({
// override the join field hasMany property to use whatever the relationship field has
field.hasMany = joinRelationship.hasMany

// @ts-expect-error converting JoinField to FlattenedJoinField to track targetField
field.targetField = join.targetField

if (!joins[field.collection]) {
joins[field.collection] = [join]
} else {
Expand Down
8 changes: 6 additions & 2 deletions packages/payload/src/fields/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1425,7 +1425,7 @@ export type JoinField = {
export type JoinFieldClient = {
admin?: AdminClient &
Pick<JoinField['admin'], 'allowCreate' | 'defaultColumns' | 'disableBulkEdit' | 'readOnly'>
} & FieldBaseClient &
} & { targetField: Pick<RelationshipFieldClient, 'relationTo'> } & FieldBaseClient &
Pick<
JoinField,
'collection' | 'defaultLimit' | 'defaultSort' | 'index' | 'maxDepth' | 'on' | 'type' | 'where'
Expand All @@ -1451,6 +1451,10 @@ export type FlattenedTabAsField = {
flattenedFields: FlattenedField[]
} & MarkRequired<TabAsField, 'name'>

export type FlattenedJoinField = {
targetField: RelationshipField | UploadField
} & JoinField

export type FlattenedField =
| CheckboxField
| CodeField
Expand All @@ -1459,8 +1463,8 @@ export type FlattenedField =
| FlattenedArrayField
| FlattenedBlocksField
| FlattenedGroupField
| FlattenedJoinField
| FlattenedTabAsField
| JoinField
| JSONField
| NumberField
| PointField
Expand Down
7 changes: 6 additions & 1 deletion packages/payload/src/utilities/flattenAllFields.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -36,6 +36,11 @@ export const flattenAllFields = ({ fields }: { fields: Field[] }): FlattenedFiel
break
}

case 'join': {
result.push(field as FlattenedJoinField)
break
}

case 'tabs': {
for (const tab of field.tabs) {
if (!tabHasName(tab)) {
Expand Down
1 change: 0 additions & 1 deletion packages/ui/src/elements/RelationshipTable/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ export const RelationshipTable: React.FC<RelationshipTableComponentProps> = (pro
relationTo,
} = props
const [Table, setTable] = useState<React.ReactNode>(null)

const { getEntityConfig } = useConfig()

const { permissions } = useAuth()
Expand Down
39 changes: 32 additions & 7 deletions packages/ui/src/fields/Join/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ const ObjectId = (ObjectIdImport.default ||
* Recursively builds the default data for joined collection
*/
const getInitialDrawerData = ({
collectionSlug,
docID,
fields,
segments,
}: {
collectionSlug: string
docID: number | string
fields: ClientField[]
segments: string[]
Expand All @@ -48,22 +50,34 @@ const getInitialDrawerData = ({
}

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 {
// TODO: Handle polymorphic https://github.com/payloadcms/payload/pull/9990
[field.name]: field.hasMany ? [docID] : docID,
[field.name]: field.hasMany ? [value] : value,
}
}

const nextSegments = segments.slice(1, segments.length)

if (field.type === 'tab' || field.type === 'group') {
return {
[field.name]: getInitialDrawerData({ docID, fields: field.fields, segments: nextSegments }),
[field.name]: getInitialDrawerData({
collectionSlug,
docID,
fields: field.fields,
segments: nextSegments,
}),
}
}

if (field.type === 'array') {
const initialData = getInitialDrawerData({
collectionSlug,
docID,
fields: field.fields,
segments: nextSegments,
Expand All @@ -79,6 +93,7 @@ const getInitialDrawerData = ({
if (field.type === 'blocks') {
for (const block of field.blocks) {
const blockInitialData = getInitialDrawerData({
collectionSlug,
docID,
fields: block.fields,
segments: nextSegments,
Expand Down Expand Up @@ -110,7 +125,7 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
path,
} = props

const { id: docID } = useDocumentInfo()
const { id: docID, docConfig } = useDocumentInfo()

const {
config: { collections },
Expand All @@ -126,9 +141,18 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
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 = {
[on]: {
equals: docID,
equals: value,
},
}

Expand All @@ -139,17 +163,18 @@ const JoinFieldComponent: JoinFieldClientComponent = (props) => {
}

return where
}, [docID, on, field.where])
}, [docID, field.targetField.relationTo, field.where, on, docConfig.slug])

const initialDrawerData = useMemo(() => {
const relatedCollection = collections.find((collection) => collection.slug === field.collection)

return getInitialDrawerData({
collectionSlug: docConfig.slug,
docID,
fields: relatedCollection.fields,
segments: field.on.split('.'),
})
}, [collections, field.on, docID, field.collection])
}, [collections, field.on, field.collection, docConfig.slug, docID])

return (
<div
Expand Down
Loading

0 comments on commit d03658d

Please sign in to comment.