feat: support hasMany virtual relationship fields (#13879)
This PR adds support for the following configuration:
```ts
const config = {
collections: [
{
slug: 'categories',
fields: [
{
name: 'title',
type: 'text',
},
],
},
{
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'categories',
type: 'relationship',
relationTo: 'categories',
hasMany: true,
},
],
},
{
slug: 'examples',
fields: [
{
name: 'postCategoriesTitles',
type: 'text',
virtual: 'post.categories.title',
// hasMany: true - added automatically during the sanitization
},
{
type: 'relationship',
relationTo: 'posts',
name: 'post',
},
{
name: 'postsTitles',
type: 'text',
virtual: 'posts.title',
// hasMany: true - added automatically during the sanitization
},
{
type: 'relationship',
relationTo: 'posts',
name: 'posts',
hasMany: true,
},
],
},
],
}
```
In the result:
`postsTitles` - will be always populated with an array of posts titles.
`postCategoriesTitles` - will be always populated with an array of the
categories titles that are related to this post
The virtual `text` field is sanitizated to `hasMany: true`
automatically, but you can specify that manually as well.
This commit is contained in:
@@ -607,6 +607,16 @@ export const getConfig: () => Partial<Config> = () => ({
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
name: 'postsTitles',
|
||||
type: 'text',
|
||||
virtual: 'posts.title',
|
||||
},
|
||||
{
|
||||
name: 'postCategoriesTitles',
|
||||
type: 'text',
|
||||
virtual: 'post.categories.title',
|
||||
},
|
||||
{
|
||||
name: 'postTitleHidden',
|
||||
type: 'text',
|
||||
@@ -643,6 +653,12 @@ export const getConfig: () => Partial<Config> = () => ({
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
{
|
||||
name: 'posts',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
hasMany: true,
|
||||
},
|
||||
{
|
||||
name: 'customID',
|
||||
type: 'relationship',
|
||||
|
||||
@@ -2988,6 +2988,58 @@ describe('database', () => {
|
||||
})
|
||||
expect(docs).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should automatically add hasMany: true to a virtual field that references a hasMany relationship', () => {
|
||||
const field = payload.collections['virtual-relations'].config.fields.find(
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
(each) => 'name' in each && each.name === 'postsTitles',
|
||||
)!
|
||||
|
||||
// eslint-disable-next-line jest/no-conditional-in-test
|
||||
expect('hasMany' in field && field.hasMany).toBe(true)
|
||||
})
|
||||
|
||||
it('should the value populate with hasMany: true relationship field', async () => {
|
||||
await payload.delete({ collection: 'categories', where: {} })
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
await payload.delete({ collection: 'virtual-relations', where: {} })
|
||||
|
||||
const post1 = await payload.create({ collection: 'posts', data: { title: 'post 1' } })
|
||||
const post2 = await payload.create({ collection: 'posts', data: { title: 'post 2' } })
|
||||
|
||||
const res = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { posts: [post1.id, post2.id] },
|
||||
})
|
||||
expect(res.postsTitles).toEqual(['post 1', 'post 2'])
|
||||
})
|
||||
|
||||
it('should the value populate with nested hasMany: true relationship field', async () => {
|
||||
await payload.delete({ collection: 'categories', where: {} })
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
await payload.delete({ collection: 'virtual-relations', where: {} })
|
||||
|
||||
const category_1 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category 1' },
|
||||
})
|
||||
const category_2 = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: 'category 2' },
|
||||
})
|
||||
const post1 = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'post 1', categories: [category_1.id, category_2.id] },
|
||||
})
|
||||
|
||||
const res = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post1.id },
|
||||
})
|
||||
expect(res.postCategoriesTitles).toEqual(['category 1', 'category 2'])
|
||||
})
|
||||
})
|
||||
|
||||
it('should convert numbers to text', async () => {
|
||||
|
||||
@@ -327,7 +327,7 @@ export interface RelationA {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
@@ -353,7 +353,7 @@ export interface RelationB {
|
||||
root: {
|
||||
type: string;
|
||||
children: {
|
||||
type: string;
|
||||
type: any;
|
||||
version: number;
|
||||
[k: string]: unknown;
|
||||
}[];
|
||||
@@ -450,6 +450,8 @@ export interface Place {
|
||||
export interface VirtualRelation {
|
||||
id: string;
|
||||
postTitle?: string | null;
|
||||
postsTitles?: string[] | null;
|
||||
postCategoriesTitles?: string[] | null;
|
||||
postTitleHidden?: string | null;
|
||||
postCategoryTitle?: string | null;
|
||||
postCategoryID?:
|
||||
@@ -473,6 +475,7 @@ export interface VirtualRelation {
|
||||
| null;
|
||||
postLocalized?: string | null;
|
||||
post?: (string | null) | Post;
|
||||
posts?: (string | Post)[] | null;
|
||||
customID?: (string | null) | CustomId;
|
||||
customIDValue?: string | null;
|
||||
updatedAt: string;
|
||||
@@ -1046,6 +1049,8 @@ export interface PlacesSelect<T extends boolean = true> {
|
||||
*/
|
||||
export interface VirtualRelationsSelect<T extends boolean = true> {
|
||||
postTitle?: T;
|
||||
postsTitles?: T;
|
||||
postCategoriesTitles?: T;
|
||||
postTitleHidden?: T;
|
||||
postCategoryTitle?: T;
|
||||
postCategoryID?: T;
|
||||
@@ -1053,6 +1058,7 @@ export interface VirtualRelationsSelect<T extends boolean = true> {
|
||||
postID?: T;
|
||||
postLocalized?: T;
|
||||
post?: T;
|
||||
posts?: T;
|
||||
customID?: T;
|
||||
customIDValue?: T;
|
||||
updatedAt?: T;
|
||||
|
||||
Reference in New Issue
Block a user