Skip to content

Commit

Permalink
chore: merge feat/join-field
Browse files Browse the repository at this point in the history
  • Loading branch information
DanRibbens committed Sep 13, 2024
2 parents a5d05ad + 20439ba commit 0374de4
Show file tree
Hide file tree
Showing 144 changed files with 4,872 additions and 1,292 deletions.
1 change: 0 additions & 1 deletion .github/workflows/pr-title.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ jobs:
plugin-form-builder
plugin-nested-docs
plugin-redirects
plugin-relationship-object-ids
plugin-search
plugin-sentry
plugin-seo
Expand Down
140 changes: 140 additions & 0 deletions docs/fields/join.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
---
title: Join Field
label: Join
order: 140
desc: The Join field provides the ability to work on related documents. Learn how to use Join field, see examples and options.
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 fields in the opposite direction. It is used to show the relationship from
the other side. 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.

The Join field is useful in scenarios including:

- To see related `Products` on an `Order`
- To view and edit `Posts` belonging to a `Category`
- To work with any bi-directional relationship data

For the Join field to work, you must have an existing [relationship](./relationship) field in the collection you are
joining. This will reference the collection and path of the field of the related documents.
To add a Relationship Field, set the `type` to `join` in your [Field Config](./overview):

```ts
import type { Field } from 'payload/types'

export const MyJoinField: Field = {
// highlight-start
name: 'relatedPosts',
type: 'join',
collection: 'posts',
on: 'category',
// highlight-end
}

// relationship field in another collection:
export const MyRelationshipField: Field = {
name: 'category',
type: 'relationship',
relationTo: 'categories',
}
```

In this example, the field is defined to show the related `posts` when added to a `category` collection. The `on` property is used to
specify the relationship field name of the field that relates to the collection document.

## Config Options

| Option | Description |
|------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **`name`** \* | To be used as the property name when retrieved from the database. [More](/docs/fields/overview#field-names) |
| **`collection`** \* | The `slug`s having the relationship field. |
| **`on`** \* | The relationship field name of the field that relates to collection document. Use dot notation for nested paths, like 'myGroup.relationName'. |
| **`maxDepth`** | Default is 1, Sets a maximum population depth for this field, regardless of the remaining depth when this field is reached. [Max Depth](/docs/getting-started/concepts#field-level-max-depth) |
| **`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. |
| **`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 |

_\* An asterisk denotes that a property is required._

## Join Field Data

When a document is returned that for a Join field is populated with related documents. The structure returned is an object with:

- `docs` an array of related documents or only IDs if the depth is reached
- `hasNextPage` a boolean indicating if there are additional documents

```json
{
"id": "66e3431a3f23e684075aae9c",
"relatedPosts": {
"docs": [
{
"id": "66e3431a3f23e684075aaeb9",
// other fields...
"category": "66e3431a3f23e684075aae9c",
},
// { ... }
],
"hasNextPage": false
},
// other fields...
}
```

## Query Options

The Join Field supports custom queries to filter, sort, and limit the related documents that will be returned. In addition to the specific query options for each Join Field, you can pass `joins: false` to disable all Join Field from returning. This is useful for performance reasons when you don't need the related documents.

The following query options are supported:

| Property | Description |
|-------------|--------------------------------------------------------------|
| **`limit`** | The maximum related documents to be returned, default is 10. |
| **`where`** | An optional `Where` query to filter joined documents. |
| **`sort`** | A string used to order related results |

These can be applied to the local API, GraphQL, and REST API.

### Local API

By adding `joins` to the local API you can customize the request for each join field by the `name` of the field.

```js
const result = await db.findOne('categories', {
where: {
title: {
equals: 'My Category'
}
},
joins: {
relatedPosts: {
limit: 5,
where: {
title: {
equals: 'My Post'
}
},
sort: 'title'
}
}
})
```

### Rest API

The rest API supports the same query options as the local API. You can use the `joins` query parameter to customize the request for each join field by the `name` of the field. For example, an API call to get a document with the related posts limited to 5 and sorted by title:

`/api/categories/${id}?joins[relatedPosts][limit]=5&joins[relatedPosts][sort]=title`

You can specify as many `joins` parameters as needed for the same or different join fields for a single request.

### GraphQL

Coming soon.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
"build:plugin-form-builder": "turbo build --filter \"@payloadcms/plugin-form-builder\"",
"build:plugin-nested-docs": "turbo build --filter \"@payloadcms/plugin-nested-docs\"",
"build:plugin-redirects": "turbo build --filter \"@payloadcms/plugin-redirects\"",
"build:plugin-relationship-object-ids": "turbo build --filter \"@payloadcms/plugin-relationship-object-ids\"",
"build:plugin-search": "turbo build --filter \"@payloadcms/plugin-search\"",
"build:plugin-sentry": "turbo build --filter \"@payloadcms/plugin-sentry\"",
"build:plugin-seo": "turbo build --filter \"@payloadcms/plugin-seo\"",
Expand Down
3 changes: 2 additions & 1 deletion packages/db-mongodb/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,14 @@
"bson-objectid": "2.0.4",
"http-status": "1.6.2",
"mongoose": "6.12.3",
"mongoose-aggregate-paginate-v2": "1.0.6",
"mongoose-paginate-v2": "1.7.22",
"prompts": "2.4.2",
"uuid": "10.0.0"
},
"devDependencies": {
"@payloadcms/eslint-config": "workspace:*",
"@types/mongoose-aggregate-paginate-v2": "1.0.9",
"@types/mongoose-aggregate-paginate-v2": "1.0.6",
"mongodb": "4.17.1",
"mongodb-memory-server": "^9",
"payload": "workspace:*"
Expand Down
10 changes: 9 additions & 1 deletion packages/db-mongodb/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Create, Document, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'

import { handleError } from './utilities/handleError.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'

export const create: Create = async function create(
Expand All @@ -12,8 +13,15 @@ export const create: Create = async function create(
const Model = this.collections[collection]
const options = await withSession(this, req)
let doc

const sanitizedData = sanitizeRelationshipIDs({
config: this.payload.config,
data,
fields: this.payload.collections[collection].config.fields,
})

try {
;[doc] = await Model.create([data], options)
;[doc] = await Model.create([sanitizedData], options)
} catch (error) {
handleError({ collection, error, req })
}
Expand Down
15 changes: 11 additions & 4 deletions packages/db-mongodb/src/createGlobal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@ import type { CreateGlobal, PayloadRequest } from 'payload'
import type { MongooseAdapter } from './index.js'

import { sanitizeInternalFields } from './utilities/sanitizeInternalFields.js'
import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'

export const createGlobal: CreateGlobal = async function createGlobal(
this: MongooseAdapter,
{ slug, data, req = {} as PayloadRequest },
) {
const Model = this.globals
const global = {
globalType: slug,
...data,
}

const global = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
globalType: slug,
...data,
},
fields: this.payload.config.globals.find((globalConfig) => globalConfig.slug === slug).fields,
})

const options = await withSession(this, req)

let [result] = (await Model.create([global], options)) as any
Expand Down
43 changes: 26 additions & 17 deletions packages/db-mongodb/src/createGlobalVersion.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { CreateGlobalVersion, Document, PayloadRequest } from 'payload'
import {
buildVersionGlobalFields,
type CreateGlobalVersion,
type Document,
type PayloadRequest,
} from 'payload'

import type { MongooseAdapter } from './index.js'

import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'

export const createGlobalVersion: CreateGlobalVersion = async function createGlobalVersion(
Expand All @@ -21,22 +27,25 @@ export const createGlobalVersion: CreateGlobalVersion = async function createGlo
const VersionModel = this.versions[globalSlug]
const options = await withSession(this, req)

const [doc] = await VersionModel.create(
[
{
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
],
options,
req,
)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionGlobalFields(
this.payload.config,
this.payload.config.globals.find((global) => global.slug === globalSlug),
),
})

const [doc] = await VersionModel.create([data], options, req)

await VersionModel.updateMany(
{
Expand Down
45 changes: 27 additions & 18 deletions packages/db-mongodb/src/createVersion.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { CreateVersion, Document, PayloadRequest } from 'payload'
import {
buildVersionCollectionFields,
type CreateVersion,
type Document,
type PayloadRequest,
} from 'payload'

import type { MongooseAdapter } from './index.js'

import { sanitizeRelationshipIDs } from './utilities/sanitizeRelationshipIDs.js'
import { withSession } from './withSession.js'

export const createVersion: CreateVersion = async function createVersion(
Expand All @@ -21,22 +27,25 @@ export const createVersion: CreateVersion = async function createVersion(
const VersionModel = this.versions[collectionSlug]
const options = await withSession(this, req)

const [doc] = await VersionModel.create(
[
{
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
],
options,
req,
)
const data = sanitizeRelationshipIDs({
config: this.payload.config,
data: {
autosave,
createdAt,
latest: true,
parent,
publishedLocale,
snapshot,
updatedAt,
version: versionData,
},
fields: buildVersionCollectionFields(
this.payload.config,
this.payload.collections[collectionSlug].config,
),
})

const [doc] = await VersionModel.create([data], options, req)

await VersionModel.updateMany(
{
Expand All @@ -48,7 +57,7 @@ export const createVersion: CreateVersion = async function createVersion(
},
{
parent: {
$eq: parent,
$eq: data.parent,
},
},
{
Expand Down
Loading

0 comments on commit 0374de4

Please sign in to comment.