feat: join field defaultLimit and defaultSort (#8908)
### What?
Allow specifying the defaultSort and defaultLimit to use for populating
a join field
### Why?
It is much easier to set defaults rather than be forced to always call
the join query using the query pattern ("?joins[categories][limit]=0").
### How?
See docs and type changes
This commit is contained in:
@@ -6,8 +6,10 @@ desc: The Join field provides the ability to work on related documents. Learn ho
|
|||||||
keywords: join, relationship, junction, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
keywords: join, relationship, junction, fields, config, configuration, documentation, Content Management System, cms, headless, javascript, node, react, nextjs
|
||||||
---
|
---
|
||||||
|
|
||||||
The Join Field is used to make Relationship and Upload fields available in the opposite direction. With a Join you can edit and view collections
|
The Join Field is used to make Relationship and Upload fields available in the opposite direction. With a Join you can
|
||||||
having reference to a specific collection document. The field itself acts as a virtual field, in that no new data is stored on the collection with a Join
|
edit and view collections
|
||||||
|
having reference to a specific collection document. The field itself acts as a virtual field, in that no new data is
|
||||||
|
stored on the collection with a Join
|
||||||
field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's
|
field. Instead, the Admin UI surfaces the related documents for a better editing experience and is surfaced by Payload's
|
||||||
APIs.
|
APIs.
|
||||||
|
|
||||||
@@ -19,10 +21,10 @@ The Join field is useful in scenarios including:
|
|||||||
- Displaying where a document or upload is used in other documents
|
- Displaying where a document or upload is used in other documents
|
||||||
|
|
||||||
<LightDarkImage
|
<LightDarkImage
|
||||||
srcLight="https://payloadcms.com/images/docs/fields/join.png"
|
srcLight="https://payloadcms.com/images/docs/fields/join.png"
|
||||||
srcDark="https://payloadcms.com/images/docs/fields/join-dark.png"
|
srcDark="https://payloadcms.com/images/docs/fields/join-dark.png"
|
||||||
alt="Shows Join field in the Payload Admin Panel"
|
alt="Shows Join field in the Payload Admin Panel"
|
||||||
caption="Admin Panel screenshot of Join field"
|
caption="Admin Panel screenshot of Join field"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
For the Join field to work, you must have an existing [relationship](./relationship) or [upload](./upload) field in the
|
For the Join field to work, you must have an existing [relationship](./relationship) or [upload](./upload) field in the
|
||||||
@@ -111,9 +113,11 @@ related docs from a new pseudo-junction collection called `categories_posts`. No
|
|||||||
third junction collection, and can be surfaced on both Posts and Categories. But, importantly, you could add
|
third junction collection, and can be surfaced on both Posts and Categories. But, importantly, you could add
|
||||||
additional "context" fields to this shared junction collection.
|
additional "context" fields to this shared junction collection.
|
||||||
|
|
||||||
For example, on this `categories_posts` collection, in addition to having the `category` and `post` fields, we could add custom "context" fields like `featured` or `spotlight`,
|
For example, on this `categories_posts` collection, in addition to having the `category` and `post` fields, we could add
|
||||||
|
custom "context" fields like `featured` or `spotlight`,
|
||||||
which would allow you to store additional information directly on relationships.
|
which would allow you to store additional information directly on relationships.
|
||||||
The `join` field gives you complete control over any type of relational architecture in Payload, all wrapped up in a powerful Admin UI.
|
The `join` field gives you complete control over any type of relational architecture in Payload, all wrapped up in a
|
||||||
|
powerful Admin UI.
|
||||||
|
|
||||||
## Config Options
|
## Config Options
|
||||||
|
|
||||||
@@ -126,11 +130,11 @@ The `join` field gives you complete control over any type of relational architec
|
|||||||
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
| **`label`** | Text used as a field label in the Admin Panel or an object with keys for each language. |
|
||||||
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
| **`hooks`** | Provide Field Hooks to control logic for this field. [More details](../hooks/fields). |
|
||||||
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
| **`access`** | Provide Field Access Control to denote what users can see and do with this field's data. [More details](../access-control/fields). |
|
||||||
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
|
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
|
||||||
| **`required`** | Require this field to have a value. |
|
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
|
||||||
| **`admin`** | Admin-specific configuration. |
|
| **`admin`** | Admin-specific configuration. |
|
||||||
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
|
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
|
||||||
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
|
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
|
||||||
|
|
||||||
_\* An asterisk denotes that a property is required._
|
_\* An asterisk denotes that a property is required._
|
||||||
|
|
||||||
@@ -150,12 +154,12 @@ object with:
|
|||||||
{
|
{
|
||||||
"id": "66e3431a3f23e684075aaeb9",
|
"id": "66e3431a3f23e684075aaeb9",
|
||||||
// other fields...
|
// other fields...
|
||||||
"category": "66e3431a3f23e684075aae9c",
|
"category": "66e3431a3f23e684075aae9c"
|
||||||
},
|
}
|
||||||
// { ... }
|
// { ... }
|
||||||
],
|
],
|
||||||
"hasNextPage": false
|
"hasNextPage": false
|
||||||
},
|
}
|
||||||
// other fields...
|
// other fields...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -213,7 +217,8 @@ You can specify as many `joins` parameters as needed for the same or different j
|
|||||||
|
|
||||||
### GraphQL
|
### GraphQL
|
||||||
|
|
||||||
The GraphQL API supports the same query options as the local and REST APIs. You can specify the query options for each join field in your query.
|
The GraphQL API supports the same query options as the local and REST APIs. You can specify the query options for each
|
||||||
|
join field in your query.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
@@ -226,9 +231,9 @@ query {
|
|||||||
limit: 5
|
limit: 5
|
||||||
where: {
|
where: {
|
||||||
author: {
|
author: {
|
||||||
equals: "66e3431a3f23e684075aaeb9"
|
equals: "66e3431a3f23e684075aaeb9"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
docs {
|
docs {
|
||||||
title
|
title
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ export const buildJoinAggregation = async ({
|
|||||||
const joinModel = adapter.collections[join.field.collection]
|
const joinModel = adapter.collections[join.field.collection]
|
||||||
|
|
||||||
const {
|
const {
|
||||||
limit: limitJoin = 10,
|
limit: limitJoin = join.field.defaultLimit ?? 10,
|
||||||
sort: sortJoin,
|
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
|
||||||
where: whereJoin,
|
where: whereJoin,
|
||||||
} = joins?.[join.schemaPath] || {}
|
} = joins?.[join.schemaPath] || {}
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ export const buildJoinAggregation = async ({
|
|||||||
config: adapter.payload.config,
|
config: adapter.payload.config,
|
||||||
fields: adapter.payload.collections[slug].config.fields,
|
fields: adapter.payload.collections[slug].config.fields,
|
||||||
locale,
|
locale,
|
||||||
sort: sortJoin || collectionConfig.defaultSort,
|
sort: sortJoin,
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
})
|
})
|
||||||
const sortProperty = Object.keys(sort)[0]
|
const sortProperty = Object.keys(sort)[0]
|
||||||
|
|||||||
@@ -238,8 +238,8 @@ export const traverseFields = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
limit: limitArg = 10,
|
limit: limitArg = field.defaultLimit ?? 10,
|
||||||
sort,
|
sort = field.defaultSort,
|
||||||
where,
|
where,
|
||||||
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
|
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
|
||||||
let limit = limitArg
|
let limit = limitArg
|
||||||
@@ -285,7 +285,9 @@ export const traverseFields = ({
|
|||||||
let columnReferenceToCurrentID: string
|
let columnReferenceToCurrentID: string
|
||||||
|
|
||||||
if (versions) {
|
if (versions) {
|
||||||
columnReferenceToCurrentID = `${topLevelTableName.replace('_', '').replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id`
|
columnReferenceToCurrentID = `${topLevelTableName
|
||||||
|
.replace('_', '')
|
||||||
|
.replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id`
|
||||||
} else {
|
} else {
|
||||||
columnReferenceToCurrentID = `${topLevelTableName}_id`
|
columnReferenceToCurrentID = `${topLevelTableName}_id`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -420,7 +420,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === 'join') {
|
if (field.type === 'join') {
|
||||||
const { limit = 10 } = joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
|
const { limit = field.defaultLimit ?? 10 } =
|
||||||
|
joinQuery?.[`${fieldPrefix.replaceAll('_', '.')}${field.name}`] || {}
|
||||||
|
|
||||||
// raw hasMany results from SQLite
|
// raw hasMany results from SQLite
|
||||||
if (typeof fieldData === 'string') {
|
if (typeof fieldData === 'string') {
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ import type {
|
|||||||
JSONFieldValidation,
|
JSONFieldValidation,
|
||||||
PointFieldValidation,
|
PointFieldValidation,
|
||||||
RadioFieldValidation,
|
RadioFieldValidation,
|
||||||
|
Sort,
|
||||||
TextareaFieldValidation,
|
TextareaFieldValidation,
|
||||||
} from '../../index.js'
|
} from '../../index.js'
|
||||||
import type { DocumentPreferences } from '../../preferences/types.js'
|
import type { DocumentPreferences } from '../../preferences/types.js'
|
||||||
@@ -1452,6 +1453,8 @@ export type JoinField = {
|
|||||||
* The slug of the collection to relate with.
|
* The slug of the collection to relate with.
|
||||||
*/
|
*/
|
||||||
collection: CollectionSlug
|
collection: CollectionSlug
|
||||||
|
defaultLimit?: number
|
||||||
|
defaultSort?: Sort
|
||||||
defaultValue?: never
|
defaultValue?: never
|
||||||
/**
|
/**
|
||||||
* This does not need to be set and will be overridden by the relationship field's hasMany property.
|
* This does not need to be set and will be overridden by the relationship field's hasMany property.
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ export const Categories: CollectionConfig = {
|
|||||||
label: 'Related Posts',
|
label: 'Related Posts',
|
||||||
type: 'join',
|
type: 'join',
|
||||||
collection: postsSlug,
|
collection: postsSlug,
|
||||||
|
defaultSort: '-title',
|
||||||
|
defaultLimit: 5,
|
||||||
on: 'category',
|
on: 'category',
|
||||||
|
maxDepth: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'hasManyPosts',
|
name: 'hasManyPosts',
|
||||||
|
|||||||
@@ -105,15 +105,6 @@ describe('Joins Field', () => {
|
|||||||
},
|
},
|
||||||
collection: 'categories',
|
collection: 'categories',
|
||||||
})
|
})
|
||||||
// const sortCategoryWithPosts = await payload.findByID({
|
|
||||||
// id: category.id,
|
|
||||||
// joins: {
|
|
||||||
// 'group.relatedPosts': {
|
|
||||||
// sort: 'title',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// collection: 'categories',
|
|
||||||
// })
|
|
||||||
|
|
||||||
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
|
expect(categoryWithPosts.group.relatedPosts.docs).toHaveLength(10)
|
||||||
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id')
|
expect(categoryWithPosts.group.relatedPosts.docs[0]).toHaveProperty('id')
|
||||||
@@ -125,11 +116,12 @@ describe('Joins Field', () => {
|
|||||||
const { docs } = await payload.find({
|
const { docs } = await payload.find({
|
||||||
limit: 1,
|
limit: 1,
|
||||||
collection: 'posts',
|
collection: 'posts',
|
||||||
|
depth: 2,
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(docs[0].category.id).toBeDefined()
|
expect(docs[0].category.id).toBeDefined()
|
||||||
expect(docs[0].category.name).toBeDefined()
|
expect(docs[0].category.name).toBeDefined()
|
||||||
expect(docs[0].category.relatedPosts.docs).toHaveLength(10)
|
expect(docs[0].category.relatedPosts.docs).toHaveLength(5) // uses defaultLimit
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should populate relationships in joins with camelCase names', async () => {
|
it('should populate relationships in joins with camelCase names', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user