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 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

View File

@@ -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]

View File

@@ -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`
} }

View File

@@ -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') {

View File

@@ -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.

View File

@@ -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',

View File

@@ -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 () => {