fix(live-preview): populates rte uploads and relationships (#4379)

This commit is contained in:
Jacob Fletcher
2023-12-05 00:53:11 -05:00
committed by GitHub
parent 290e9d8238
commit 4090aebb0e
11 changed files with 677 additions and 77 deletions

View File

@@ -53,13 +53,20 @@ export const traverseRichText = ({
? Array.isArray(incomingData[key])
? []
: {}
: incomingData[key]
: undefined
}
const isRelationship = key === 'value' && 'relationTo' in incomingData
if (isRelationship) {
const needsPopulation = !result.value || typeof result.value !== 'object'
// or if there are no keys besides id
const needsPopulation =
!result.value ||
typeof result.value !== 'object' ||
(typeof result.value === 'object' &&
Object.keys(result.value).length === 1 &&
'id' in result.value)
const hasChanged =
result &&
typeof result === 'object' &&
@@ -71,7 +78,10 @@ export const traverseRichText = ({
}
populationsByCollection[incomingData.relationTo].push({
id: incomingData[key],
id:
incomingData[key] && typeof incomingData[key] === 'object'
? incomingData[key].id
: incomingData[key],
accessor: 'value',
ref: result,
})

View File

@@ -1,5 +1,6 @@
import type { CollectionConfig } from '../../../packages/payload/src/collections/config/types'
import { lexicalEditor } from '../../../packages/richtext-lexical/src'
import { Archive } from '../blocks/ArchiveBlock'
import { CallToAction } from '../blocks/CallToAction'
import { Content } from '../blocks/Content'
@@ -62,8 +63,15 @@ export const Pages: CollectionConfig = {
label: 'Test',
fields: [
{
name: 'relationshipInRichText',
label: 'Rich Text — Slate',
type: 'richText',
name: 'richTextSlate',
},
{
label: 'Rich Text — Lexical',
type: 'richText',
name: 'richTextLexical',
editor: lexicalEditor({}),
},
{
name: 'relationshipAsUpload',

View File

@@ -154,10 +154,7 @@ describe('Collections - Live Preview', () => {
expect(mergedData._numberOfRequests).toEqual(0)
})
// TODO: this test is not working in Postgres
// This is because of how relationships are handled in `mergeData`
// This test passes in MongoDB, though
it.skip('— uploads - adds and removes media', async () => {
it('— uploads - adds and removes media', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
}
@@ -198,6 +195,166 @@ describe('Collections - Live Preview', () => {
expect(mergedDataWithoutUpload.hero.media).toBeFalsy()
})
it('— uploads - populates within Slate rich text editor', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
}
// Add upload
const merge1 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...initialData,
richTextSlate: [
{
type: 'upload',
relationTo: 'media',
value: media.id,
},
],
},
initialData,
serverURL,
returnNumberOfRequests: true,
})
expect(merge1.richTextSlate).toHaveLength(1)
expect(merge1.richTextSlate[0].value).toMatchObject(media)
expect(merge1._numberOfRequests).toEqual(1)
// Remove upload
const merge2 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...merge1,
richTextSlate: [
{
type: 'paragraph',
children: [
{
text: 'Hello, world!',
},
],
},
],
},
initialData,
serverURL,
returnNumberOfRequests: true,
})
expect(merge2.richTextSlate).toHaveLength(1)
expect(merge2.richTextSlate[0].value).toBeFalsy()
expect(merge2.richTextSlate[0].type).toEqual('paragraph')
expect(merge2._numberOfRequests).toEqual(0)
})
it('— uploads - populates within Lexical rich text editor', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
}
// Add upload
const merge1 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...initialData,
richTextLexical: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Hello, world!',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'upload',
relationTo: 'media',
version: 1,
value: media.id,
},
],
direction: 'ltr',
},
},
},
initialData,
serverURL,
returnNumberOfRequests: true,
})
expect(merge1.richTextLexical.root.children).toHaveLength(2)
expect(merge1.richTextLexical.root.children[1].value).toMatchObject(media)
expect(merge1._numberOfRequests).toEqual(1)
// Remove upload
const merge2 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...merge1,
richTextLexical: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
children: [
{
detail: 0,
format: 0,
mode: 'normal',
style: '',
text: 'Hello, world!',
type: 'text',
version: 1,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
],
direction: 'ltr',
},
},
},
initialData,
serverURL,
returnNumberOfRequests: true,
})
expect(merge2.richTextLexical.root.children).toHaveLength(1)
expect(merge2.richTextLexical.root.children[0].value).toBeFalsy()
expect(merge2.richTextLexical.root.children[0].type).toEqual('paragraph')
})
it('— relationships - populates monomorphic has one relationships', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
@@ -422,7 +579,7 @@ describe('Collections - Live Preview', () => {
},
],
},
initialData,
initialData: merge1,
serverURL,
returnNumberOfRequests: true,
})
@@ -451,7 +608,130 @@ describe('Collections - Live Preview', () => {
])
})
it('— relationships - populates within rich text', async () => {
it('— relationships - populates within Slate rich text editor', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
}
// Add a relationship and an upload
const merge1 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...initialData,
richTextSlate: [
{
children: [
{
text: ' ',
},
],
relationTo: 'posts',
type: 'relationship',
value: {
id: testPost.id,
},
},
{
type: 'paragraph',
children: [
{
text: '',
},
],
},
{
children: [
{
text: '',
},
],
relationTo: 'media',
type: 'upload',
value: {
id: media.id,
},
},
],
},
initialData,
serverURL,
returnNumberOfRequests: true,
})
expect(merge1._numberOfRequests).toEqual(2)
expect(merge1.richTextSlate).toHaveLength(3)
expect(merge1.richTextSlate[0].type).toEqual('relationship')
expect(merge1.richTextSlate[0].value).toMatchObject(testPost)
expect(merge1.richTextSlate[1].type).toEqual('paragraph')
expect(merge1.richTextSlate[2].type).toEqual('upload')
expect(merge1.richTextSlate[2].value).toMatchObject(media)
// Add a new node between the relationship and the upload
const merge2 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...merge1,
richTextSlate: [
{
children: [
{
text: ' ',
},
],
relationTo: 'posts',
type: 'relationship',
value: {
id: testPost.id,
},
},
{
type: 'paragraph',
children: [
{
text: '',
},
],
},
{
type: 'paragraph',
children: [
{
text: '',
},
],
},
{
children: [
{
text: '',
},
],
relationTo: 'media',
type: 'upload',
value: {
id: media.id,
},
},
],
},
initialData: merge1,
serverURL,
returnNumberOfRequests: true,
})
expect(merge2._numberOfRequests).toEqual(1)
expect(merge2.richTextSlate).toHaveLength(4)
expect(merge2.richTextSlate[0].type).toEqual('relationship')
expect(merge2.richTextSlate[0].value).toMatchObject(testPost)
expect(merge2.richTextSlate[1].type).toEqual('paragraph')
expect(merge2.richTextSlate[2].type).toEqual('paragraph')
expect(merge2.richTextSlate[3].type).toEqual('upload')
expect(merge2.richTextSlate[3].value).toMatchObject(media)
})
it('— relationships - populates within Lexical rich text editor', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
}
@@ -462,56 +742,130 @@ describe('Collections - Live Preview', () => {
fieldSchema: schemaJSON,
incomingData: {
...initialData,
relationshipInRichText: [
{
type: 'paragraph',
text: 'Paragraph 1',
richTextLexical: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
format: '',
type: 'relationship',
version: 1,
relationTo: 'posts',
value: {
id: testPost.id,
},
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'upload',
version: 1,
fields: null,
relationTo: 'media',
value: {
id: media.id,
},
},
],
direction: null,
},
{
type: 'reference',
reference: {
relationTo: 'posts',
value: testPost.id,
},
},
],
},
},
initialData,
serverURL,
returnNumberOfRequests: true,
})
expect(merge1._numberOfRequests).toEqual(1)
expect(merge1.relationshipInRichText).toHaveLength(2)
expect(merge1.relationshipInRichText[1].reference.value).toMatchObject(testPost)
expect(merge1._numberOfRequests).toEqual(2)
expect(merge1.richTextLexical.root.children).toHaveLength(3)
expect(merge1.richTextLexical.root.children[0].type).toEqual('relationship')
expect(merge1.richTextLexical.root.children[0].value).toMatchObject(testPost)
expect(merge1.richTextLexical.root.children[1].type).toEqual('paragraph')
expect(merge1.richTextLexical.root.children[2].type).toEqual('upload')
expect(merge1.richTextLexical.root.children[2].value).toMatchObject(media)
// Remove the relationship
// Add a node before the populated one
const merge2 = await mergeData({
depth: 1,
fieldSchema: schemaJSON,
incomingData: {
...merge1,
relationshipInRichText: [
{
type: 'paragraph',
text: 'Paragraph 1',
richTextLexical: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
format: '',
type: 'relationship',
version: 1,
relationTo: 'posts',
value: {
id: testPost.id,
},
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'upload',
version: 1,
fields: null,
relationTo: 'media',
value: {
id: media.id,
},
},
],
direction: null,
},
],
},
},
initialData,
initialData: merge1,
serverURL,
returnNumberOfRequests: true,
})
expect(merge2._numberOfRequests).toEqual(0)
expect(merge2.relationshipInRichText).toHaveLength(1)
expect(merge2.relationshipInRichText[0].type).toEqual('paragraph')
expect(merge2._numberOfRequests).toEqual(1)
expect(merge2.richTextLexical.root.children).toHaveLength(4)
expect(merge2.richTextLexical.root.children[0].type).toEqual('relationship')
expect(merge2.richTextLexical.root.children[0].value).toMatchObject(testPost)
expect(merge2.richTextLexical.root.children[1].type).toEqual('paragraph')
expect(merge2.richTextLexical.root.children[2].type).toEqual('paragraph')
expect(merge2.richTextLexical.root.children[3].type).toEqual('upload')
expect(merge2.richTextLexical.root.children[3].value).toMatchObject(media)
})
it('— relationships - does not re-populate existing rich text relationships', async () => {
const initialData: Partial<Page> = {
title: 'Test Page',
relationshipInRichText: [
richTextSlate: [
{
type: 'paragraph',
text: 'Paragraph 1',
@@ -532,7 +886,7 @@ describe('Collections - Live Preview', () => {
fieldSchema: schemaJSON,
incomingData: {
...initialData,
relationshipInRichText: [
richTextSlate: [
{
type: 'paragraph',
text: 'Paragraph 1 (Updated)',
@@ -552,9 +906,9 @@ describe('Collections - Live Preview', () => {
})
expect(merge1._numberOfRequests).toEqual(0)
expect(merge1.relationshipInRichText).toHaveLength(2)
expect(merge1.relationshipInRichText[0].text).toEqual('Paragraph 1 (Updated)')
expect(merge1.relationshipInRichText[1].reference.value).toMatchObject(testPost)
expect(merge1.richTextSlate).toHaveLength(2)
expect(merge1.richTextSlate[0].text).toEqual('Paragraph 1 (Updated)')
expect(merge1.richTextSlate[1].reference.value).toMatchObject(testPost)
})
it('— relationships - populates within blocks', async () => {

View File

@@ -6,7 +6,6 @@ import React from 'react'
import { PAYLOAD_SERVER_URL } from '@/app/_api/serverURL'
import { Hero } from '@/app/_components/Hero'
import { Blocks } from '@/app/_components/Blocks'
import { RelationshipsBlock } from '@/app/_blocks/Relationships'
export const PageClient: React.FC<{
page: PageType

View File

@@ -22,9 +22,15 @@ export const RelationshipsBlock: React.FC<RelationshipsBlockProps> = (props) =>
This block is for testing purposes only. It renders every possible type of relationship.
</p>
<p>
<b>Rich Text:</b>
<b>Rich Text Slate:</b>
</p>
{data?.relationshipInRichText && <RichText content={data.relationshipInRichText} />}
{data?.richTextSlate && <RichText content={data.richTextSlate} renderUploadFilenameOnly />}
<p>
<b>Rich Text Lexical:</b>
</p>
{data?.richTextLexical && (
<RichText serializer="lexical" content={data.richTextLexical} renderUploadFilenameOnly />
)}
<p>
<b>Upload:</b>
</p>

View File

@@ -1,17 +1,25 @@
import React from 'react'
import serialize from './serialize'
import serializeSlate from './serializeSlate'
import serializeLexical from './serializeLexical'
import classes from './index.module.scss'
const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => {
const RichText: React.FC<{
className?: string
content: any
renderUploadFilenameOnly?: boolean
serializer?: 'lexical' | 'slate'
}> = ({ className, content, renderUploadFilenameOnly, serializer = 'slate' }) => {
if (!content) {
return null
}
return (
<div className={[classes.richText, className].filter(Boolean).join(' ')}>
{serialize(content)}
{serializer === 'slate'
? serializeSlate(content, renderUploadFilenameOnly)
: serializeLexical(content, renderUploadFilenameOnly)}
</div>
)
}

View File

@@ -0,0 +1,89 @@
import type { SerializedEditorState } from 'lexical'
import { CMSLink } from '../Link'
import { Media } from '../Media'
const serializer = (
content?: SerializedEditorState['root']['children'],
renderUploadFilenameOnly?: boolean,
): React.ReactNode | React.ReactNode[] =>
content?.map((node, i) => {
switch (node.type) {
case 'h1':
return <h1 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h1>
case 'h2':
return <h2 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h2>
case 'h3':
return <h3 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h3>
case 'h4':
return <h4 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h4>
case 'h5':
return <h5 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h5>
case 'h6':
return <h6 key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</h6>
case 'quote':
return (
<blockquote key={i}>
{serializeLexical(node?.children, renderUploadFilenameOnly)}
</blockquote>
)
case 'ul':
return <ul key={i}>{serializeLexical(node?.children, renderUploadFilenameOnly)}</ul>
case 'ol':
return <ol key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</ol>
case 'li':
return <li key={i}>{serializeLexical(node.children, renderUploadFilenameOnly)}</li>
case 'relationship':
return (
<span key={i}>
{node.value && typeof node.value === 'object'
? node.value.title || node.value.id
: node.value}
</span>
)
case 'link':
return (
<CMSLink
key={i}
type={node.linkType === 'internal' ? 'reference' : 'custom'}
url={node.url}
reference={node.doc as any}
newTab={Boolean(node?.newTab)}
>
{serializer(node?.children, renderUploadFilenameOnly)}
</CMSLink>
)
case 'upload':
if (renderUploadFilenameOnly) {
return <span key={i}>{node.value.filename}</span>
}
return <Media key={i} resource={node?.value} />
case 'paragraph':
return <p key={i}>{serializer(node?.children, renderUploadFilenameOnly)}</p>
case 'text':
return <span key={i}>{node.text}</span>
}
})
const serializeLexical = (
content?: SerializedEditorState,
renderUploadFilenameOnly?: boolean,
): React.ReactNode | React.ReactNode[] => {
return serializer(content?.root?.children, renderUploadFilenameOnly)
}
export default serializeLexical

View File

@@ -1,8 +1,8 @@
import React, { Fragment } from 'react'
import escapeHTML from 'escape-html'
import Link from 'next/link'
import { Text } from 'slate'
import { CMSLink } from '../Link'
import { Media } from '../Media'
// eslint-disable-next-line no-use-before-define
type Children = Leaf[]
@@ -15,7 +15,10 @@ type Leaf = {
[key: string]: unknown
}
const serialize = (children?: Children): React.ReactNode[] =>
const serializeSlate = (
children?: Children,
renderUploadFilenameOnly?: boolean,
): React.ReactNode[] =>
children?.map((node, i) => {
if (Text.isText(node)) {
let text = <span dangerouslySetInnerHTML={{ __html: escapeHTML(node.text) }} />
@@ -57,25 +60,39 @@ const serialize = (children?: Children): React.ReactNode[] =>
switch (node.type) {
case 'h1':
return <h1 key={i}>{serialize(node?.children)}</h1>
return <h1 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h1>
case 'h2':
return <h2 key={i}>{serialize(node?.children)}</h2>
return <h2 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h2>
case 'h3':
return <h3 key={i}>{serialize(node?.children)}</h3>
return <h3 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h3>
case 'h4':
return <h4 key={i}>{serialize(node?.children)}</h4>
return <h4 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h4>
case 'h5':
return <h5 key={i}>{serialize(node?.children)}</h5>
return <h5 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h5>
case 'h6':
return <h6 key={i}>{serialize(node?.children)}</h6>
return <h6 key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</h6>
case 'quote':
return <blockquote key={i}>{serialize(node?.children)}</blockquote>
return (
<blockquote key={i}>
{serializeSlate(node?.children, renderUploadFilenameOnly)}
</blockquote>
)
case 'ul':
return <ul key={i}>{serialize(node?.children)}</ul>
return <ul key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</ul>
case 'ol':
return <ol key={i}>{serialize(node.children)}</ol>
return <ol key={i}>{serializeSlate(node.children, renderUploadFilenameOnly)}</ol>
case 'li':
return <li key={i}>{serialize(node.children)}</li>
return <li key={i}>{serializeSlate(node.children, renderUploadFilenameOnly)}</li>
case 'relationship':
return (
<span key={i}>
@@ -84,6 +101,7 @@ const serialize = (children?: Children): React.ReactNode[] =>
: node.value}
</span>
)
case 'link':
return (
<CMSLink
@@ -93,13 +111,20 @@ const serialize = (children?: Children): React.ReactNode[] =>
reference={node.doc as any}
newTab={Boolean(node?.newTab)}
>
{serialize(node?.children)}
{serializeSlate(node?.children, renderUploadFilenameOnly)}
</CMSLink>
)
case 'upload':
if (renderUploadFilenameOnly) {
return <span key={i}>{node.value.filename}</span>
}
return <Media key={i} resource={node?.value} />
default:
return <p key={i}>{serialize(node?.children)}</p>
return <p key={i}>{serializeSlate(node?.children, renderUploadFilenameOnly)}</p>
}
}) || []
export default serialize
export default serializeSlate

View File

@@ -11,6 +11,7 @@ export interface Config {
users: User
pages: Page
posts: Post
tenants: Tenant
categories: Category
media: Media
'payload-preferences': PayloadPreference
@@ -37,6 +38,7 @@ export interface User {
export interface Page {
id: string
slug: string
tenant?: (string | null) | Tenant
title: string
hero: {
type: 'none' | 'highImpact' | 'lowImpact'
@@ -152,16 +154,6 @@ export interface Page {
}
)[]
| null
meta?: {
title?: string | null
description?: string | null
image?: string | Media | null
}
relationshipInRichText?:
| {
[k: string]: unknown
}[]
| null
relationshipAsUpload?: string | Media | null
relationshipMonoHasOne?: (string | null) | Post
relationshipMonoHasMany?: (string | Post)[] | null
@@ -198,6 +190,41 @@ export interface Page {
id?: string | null
}[]
| null
richTextSlate?:
| {
[k: string]: unknown
}[]
| null
richTextLexical?: {
root: {
children: {
type: string
version: number
[k: string]: unknown
}[]
direction: ('ltr' | 'rtl') | null
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''
indent: number
type: string
version: number
}
[k: string]: unknown
} | null
tab: {
relationshipInTab?: (string | null) | Post
}
meta?: {
title?: string | null
description?: string | null
image?: string | Media | null
}
updatedAt: string
createdAt: string
}
export interface Tenant {
id: string
title: string
clientURL: string
updatedAt: string
createdAt: string
}
@@ -221,6 +248,7 @@ export interface Media {
export interface Post {
id: string
slug: string
tenant?: (string | null) | Tenant
title: string
hero: {
type: 'none' | 'highImpact' | 'lowImpact'

View File

@@ -154,11 +154,6 @@ export interface Page {
}
)[]
| null
relationshipInRichText?:
| {
[k: string]: unknown
}[]
| null
relationshipAsUpload?: string | Media | null
relationshipMonoHasOne?: (string | null) | Post
relationshipMonoHasMany?: (string | Post)[] | null
@@ -195,6 +190,26 @@ export interface Page {
id?: string | null
}[]
| null
richTextSlate?:
| {
[k: string]: unknown
}[]
| null
richTextLexical?: {
root: {
children: {
type: string
version: number
[k: string]: unknown
}[]
direction: ('ltr' | 'rtl') | null
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''
indent: number
type: string
version: number
}
[k: string]: unknown
} | null
tab: {
relationshipInTab?: (string | null) | Post
}

View File

@@ -95,7 +95,7 @@ export const home: Omit<Page, 'createdAt' | 'id' | 'updatedAt'> = {
},
],
relationshipAsUpload: '{{MEDIA_ID}}',
relationshipInRichText: [
richTextSlate: [
{
children: [
{
@@ -108,7 +108,65 @@ export const home: Omit<Page, 'createdAt' | 'id' | 'updatedAt'> = {
id: '{{POST_1_ID}}',
},
},
{
type: 'paragraph',
children: [
{
text: '',
},
],
},
{
children: [
{
text: '',
},
],
relationTo: 'media',
type: 'upload',
value: {
id: '{{MEDIA_ID}}',
},
},
],
richTextLexical: {
root: {
type: 'root',
format: '',
indent: 0,
version: 1,
children: [
{
format: '',
type: 'relationship',
version: 1,
relationTo: 'posts',
value: {
id: '{{POST_1_ID}}',
},
},
{
children: [],
direction: null,
format: '',
indent: 0,
type: 'paragraph',
version: 1,
},
{
format: '',
type: 'upload',
version: 1,
fields: null,
relationTo: 'media',
value: {
id: '{{MEDIA_ID}}',
},
},
],
direction: null,
},
},
relationshipMonoHasMany: ['{{POST_1_ID}}'],
relationshipMonoHasOne: '{{POST_1_ID}}',
relationshipPolyHasMany: [{ relationTo: 'posts', value: '{{POST_1_ID}}' }],