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:
Dan Ribbens
2024-10-28 17:52:37 -04:00
committed by GitHub
parent 3605da1e3f
commit f0edbb79f9
7 changed files with 41 additions and 35 deletions

View File

@@ -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
---
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
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
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
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
APIs.
@@ -19,10 +21,10 @@ The Join field is useful in scenarios including:
- Displaying where a document or upload is used in other documents
<LightDarkImage
srcLight="https://payloadcms.com/images/docs/fields/join.png"
srcDark="https://payloadcms.com/images/docs/fields/join-dark.png"
alt="Shows Join field in the Payload Admin Panel"
caption="Admin Panel screenshot of Join field"
srcLight="https://payloadcms.com/images/docs/fields/join.png"
srcDark="https://payloadcms.com/images/docs/fields/join-dark.png"
alt="Shows Join field in the Payload Admin Panel"
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
@@ -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
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.
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
@@ -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. |
| **`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). |
| **`localized`** | Enable localization for this field. Requires [localization to be enabled](/docs/configuration/localization) in the Base config. |
| **`required`** | Require this field to have a value. |
| **`defaultLimit`** | The number of documents to return. Set to 0 to return all related documents. |
| **`defaultSort`** | The field name used to specify the order the joined documents are returned. |
| **`admin`** | Admin-specific configuration. |
| **`custom`** | Extension point for adding custom data (e.g. for plugins) |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema |
| **`custom`** | Extension point for adding custom data (e.g. for plugins). |
| **`typescriptSchema`** | Override field type generation with providing a JSON schema. |
_\* An asterisk denotes that a property is required._
@@ -150,12 +154,12 @@ object with:
{
"id": "66e3431a3f23e684075aaeb9",
// other fields...
"category": "66e3431a3f23e684075aae9c",
},
"category": "66e3431a3f23e684075aae9c"
}
// { ... }
],
"hasNextPage": false
},
}
// other fields...
}
```
@@ -213,7 +217,8 @@ You can specify as many `joins` parameters as needed for the same or different j
### 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:

View File

@@ -57,8 +57,8 @@ export const buildJoinAggregation = async ({
const joinModel = adapter.collections[join.field.collection]
const {
limit: limitJoin = 10,
sort: sortJoin,
limit: limitJoin = join.field.defaultLimit ?? 10,
sort: sortJoin = join.field.defaultSort || collectionConfig.defaultSort,
where: whereJoin,
} = joins?.[join.schemaPath] || {}
@@ -66,7 +66,7 @@ export const buildJoinAggregation = async ({
config: adapter.payload.config,
fields: adapter.payload.collections[slug].config.fields,
locale,
sort: sortJoin || collectionConfig.defaultSort,
sort: sortJoin,
timestamps: true,
})
const sortProperty = Object.keys(sort)[0]

View File

@@ -238,8 +238,8 @@ export const traverseFields = ({
}
const {
limit: limitArg = 10,
sort,
limit: limitArg = field.defaultLimit ?? 10,
sort = field.defaultSort,
where,
} = joinQuery[`${path.replaceAll('_', '.')}${field.name}`] || {}
let limit = limitArg
@@ -285,7 +285,9 @@ export const traverseFields = ({
let columnReferenceToCurrentID: string
if (versions) {
columnReferenceToCurrentID = `${topLevelTableName.replace('_', '').replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id`
columnReferenceToCurrentID = `${topLevelTableName
.replace('_', '')
.replace(new RegExp(`${adapter.versionsSuffix}$`), '')}_id`
} else {
columnReferenceToCurrentID = `${topLevelTableName}_id`
}

View File

@@ -420,7 +420,8 @@ export const traverseFields = <T extends Record<string, unknown>>({
}
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
if (typeof fieldData === 'string') {

View File

@@ -121,6 +121,7 @@ import type {
JSONFieldValidation,
PointFieldValidation,
RadioFieldValidation,
Sort,
TextareaFieldValidation,
} from '../../index.js'
import type { DocumentPreferences } from '../../preferences/types.js'
@@ -1452,6 +1453,8 @@ export type JoinField = {
* The slug of the collection to relate with.
*/
collection: CollectionSlug
defaultLimit?: number
defaultSort?: Sort
defaultValue?: never
/**
* This does not need to be set and will be overridden by the relationship field's hasMany property.

View File

@@ -47,7 +47,10 @@ export const Categories: CollectionConfig = {
label: 'Related Posts',
type: 'join',
collection: postsSlug,
defaultSort: '-title',
defaultLimit: 5,
on: 'category',
maxDepth: 1,
},
{
name: 'hasManyPosts',

View File

@@ -105,15 +105,6 @@ describe('Joins Field', () => {
},
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[0]).toHaveProperty('id')
@@ -125,11 +116,12 @@ describe('Joins Field', () => {
const { docs } = await payload.find({
limit: 1,
collection: 'posts',
depth: 2,
})
expect(docs[0].category.id).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 () => {