feat: queriable / sortable / useAsTitle virtual fields linked with a relationship field (#11805)
This PR adds an ability to specify a virtual field in this way
```js
{
slug: 'posts',
fields: [
{
name: 'title',
type: 'text',
required: true,
},
],
},
{
slug: 'virtual-relations',
fields: [
{
name: 'postTitle',
type: 'text',
virtual: 'post.title',
},
{
name: 'post',
type: 'relationship',
relationTo: 'posts',
},
],
},
```
Then, every time you query `virtual-relations`, `postTitle` will be
automatically populated (even if using `depth: 0`) on the db level. This
field also, unlike `virtual: true` is available for querying / sorting /
`useAsTitle`.
Also, the field can be deeply nested to 2 or more relationships, for
example:
```
{
name: 'postCategoryTitle',
type: 'text',
virtual: 'post.category.title',
},
```
Where the current collection has `post` - a relationship to `posts`, the
collection `posts` has `category` that's a relationship to `categories`
and finally `categories` has `title`.
This commit is contained in:
@@ -36,6 +36,15 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
},
|
||||
collections: [
|
||||
{
|
||||
slug: 'categories',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'title',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: postsSlug,
|
||||
fields: [
|
||||
@@ -43,6 +52,17 @@ export default buildConfigWithDefaults({
|
||||
name: 'title',
|
||||
type: 'text',
|
||||
required: true,
|
||||
// access: { read: () => false },
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
relationTo: 'categories',
|
||||
name: 'category',
|
||||
},
|
||||
{
|
||||
name: 'localized',
|
||||
type: 'text',
|
||||
localized: true,
|
||||
},
|
||||
{
|
||||
name: 'text',
|
||||
@@ -437,6 +457,33 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relations',
|
||||
admin: { useAsTitle: 'postTitle' },
|
||||
fields: [
|
||||
{
|
||||
name: 'postTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
name: 'postCategoryTitle',
|
||||
type: 'text',
|
||||
virtual: 'post.category.title',
|
||||
},
|
||||
{
|
||||
name: 'postLocalized',
|
||||
type: 'text',
|
||||
virtual: 'post.localized',
|
||||
},
|
||||
{
|
||||
name: 'post',
|
||||
type: 'relationship',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
versions: { drafts: true },
|
||||
},
|
||||
{
|
||||
slug: fieldsPersistanceSlug,
|
||||
fields: [
|
||||
@@ -662,6 +709,21 @@ export default buildConfigWithDefaults({
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
slug: 'virtual-relation-global',
|
||||
fields: [
|
||||
{
|
||||
type: 'text',
|
||||
name: 'postTitle',
|
||||
virtual: 'post.title',
|
||||
},
|
||||
{
|
||||
type: 'relationship',
|
||||
name: 'post',
|
||||
relationTo: 'posts',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
localization: {
|
||||
defaultLocale: 'en',
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
migrateRelationshipsV2_V3,
|
||||
migrateVersionsV1_V2,
|
||||
} from '@payloadcms/db-mongodb/migration-utils'
|
||||
import { objectToFrontmatter } from '@payloadcms/richtext-lexical'
|
||||
import { randomUUID } from 'crypto'
|
||||
import { type Table } from 'drizzle-orm'
|
||||
import * as drizzlePg from 'drizzle-orm/pg-core'
|
||||
@@ -1977,6 +1978,132 @@ describe('database', () => {
|
||||
expect(res.textWithinRow).toBeUndefined()
|
||||
expect(res.textWithinTabs).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow virtual field with reference', async () => {
|
||||
const post = await payload.create({ collection: 'posts', data: { title: 'my-title' } })
|
||||
const { id } = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post.id },
|
||||
})
|
||||
|
||||
const doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id })
|
||||
expect(doc.postTitle).toBe('my-title')
|
||||
const draft = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
where: { id: { equals: id } },
|
||||
draft: true,
|
||||
})
|
||||
expect(draft.docs[0]?.postTitle).toBe('my-title')
|
||||
})
|
||||
|
||||
it('should allow virtual field with reference localized', async () => {
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: 'my-title', localized: 'localized en' },
|
||||
})
|
||||
|
||||
await payload.update({
|
||||
collection: 'posts',
|
||||
id: post.id,
|
||||
locale: 'es',
|
||||
data: { localized: 'localized es' },
|
||||
})
|
||||
|
||||
const { id } = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post.id },
|
||||
})
|
||||
|
||||
let doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id })
|
||||
expect(doc.postLocalized).toBe('localized en')
|
||||
|
||||
doc = await payload.findByID({ collection: 'virtual-relations', depth: 0, id, locale: 'es' })
|
||||
expect(doc.postLocalized).toBe('localized es')
|
||||
})
|
||||
|
||||
it('should allow to query by a virtual field with reference', async () => {
|
||||
await payload.delete({ collection: 'posts', where: {} })
|
||||
await payload.delete({ collection: 'virtual-relations', where: {} })
|
||||
const post_1 = await payload.create({ collection: 'posts', data: { title: 'Dan' } })
|
||||
const post_2 = await payload.create({ collection: 'posts', data: { title: 'Mr.Dan' } })
|
||||
|
||||
const doc_1 = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post_1.id },
|
||||
})
|
||||
const doc_2 = await payload.create({
|
||||
collection: 'virtual-relations',
|
||||
depth: 0,
|
||||
data: { post: post_2.id },
|
||||
})
|
||||
|
||||
const { docs: ascDocs } = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
sort: 'postTitle',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(ascDocs[0]?.id).toBe(doc_1.id)
|
||||
|
||||
expect(ascDocs[1]?.id).toBe(doc_2.id)
|
||||
|
||||
const { docs: descDocs } = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
sort: '-postTitle',
|
||||
depth: 0,
|
||||
})
|
||||
|
||||
expect(descDocs[1]?.id).toBe(doc_1.id)
|
||||
|
||||
expect(descDocs[0]?.id).toBe(doc_2.id)
|
||||
})
|
||||
|
||||
it.todo('should allow to sort by a virtual field with reference')
|
||||
|
||||
it('should allow virtual field 2x deep', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: '1-category' },
|
||||
})
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: '1-post', category: category.id },
|
||||
})
|
||||
const doc = await payload.create({ collection: 'virtual-relations', data: { post: post.id } })
|
||||
expect(doc.postCategoryTitle).toBe('1-category')
|
||||
})
|
||||
|
||||
it('should allow to query by virtual field 2x deep', async () => {
|
||||
const category = await payload.create({
|
||||
collection: 'categories',
|
||||
data: { title: '2-category' },
|
||||
})
|
||||
const post = await payload.create({
|
||||
collection: 'posts',
|
||||
data: { title: '2-post', category: category.id },
|
||||
})
|
||||
const doc = await payload.create({ collection: 'virtual-relations', data: { post: post.id } })
|
||||
const found = await payload.find({
|
||||
collection: 'virtual-relations',
|
||||
where: { postCategoryTitle: { equals: '2-category' } },
|
||||
})
|
||||
expect(found.docs).toHaveLength(1)
|
||||
expect(found.docs[0].id).toBe(doc.id)
|
||||
})
|
||||
|
||||
it('should allow referenced virtual field in globals', async () => {
|
||||
const post = await payload.create({ collection: 'posts', data: { title: 'post' } })
|
||||
const globalData = await payload.updateGlobal({
|
||||
slug: 'virtual-relation-global',
|
||||
data: { post: post.id },
|
||||
depth: 0,
|
||||
})
|
||||
expect(globalData.postTitle).toBe('post')
|
||||
})
|
||||
})
|
||||
|
||||
it('should convert numbers to text', async () => {
|
||||
|
||||
@@ -67,6 +67,7 @@ export interface Config {
|
||||
};
|
||||
blocks: {};
|
||||
collections: {
|
||||
categories: Category;
|
||||
posts: Post;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedField;
|
||||
'default-values': DefaultValue;
|
||||
@@ -75,6 +76,7 @@ export interface Config {
|
||||
'pg-migrations': PgMigration;
|
||||
'custom-schema': CustomSchema;
|
||||
places: Place;
|
||||
'virtual-relations': VirtualRelation;
|
||||
'fields-persistance': FieldsPersistance;
|
||||
'custom-ids': CustomId;
|
||||
'fake-custom-ids': FakeCustomId;
|
||||
@@ -88,6 +90,7 @@ export interface Config {
|
||||
};
|
||||
collectionsJoins: {};
|
||||
collectionsSelect: {
|
||||
categories: CategoriesSelect<false> | CategoriesSelect<true>;
|
||||
posts: PostsSelect<false> | PostsSelect<true>;
|
||||
'error-on-unnamed-fields': ErrorOnUnnamedFieldsSelect<false> | ErrorOnUnnamedFieldsSelect<true>;
|
||||
'default-values': DefaultValuesSelect<false> | DefaultValuesSelect<true>;
|
||||
@@ -96,6 +99,7 @@ export interface Config {
|
||||
'pg-migrations': PgMigrationsSelect<false> | PgMigrationsSelect<true>;
|
||||
'custom-schema': CustomSchemaSelect<false> | CustomSchemaSelect<true>;
|
||||
places: PlacesSelect<false> | PlacesSelect<true>;
|
||||
'virtual-relations': VirtualRelationsSelect<false> | VirtualRelationsSelect<true>;
|
||||
'fields-persistance': FieldsPersistanceSelect<false> | FieldsPersistanceSelect<true>;
|
||||
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
|
||||
'fake-custom-ids': FakeCustomIdsSelect<false> | FakeCustomIdsSelect<true>;
|
||||
@@ -114,11 +118,13 @@ export interface Config {
|
||||
global: Global;
|
||||
'global-2': Global2;
|
||||
'global-3': Global3;
|
||||
'virtual-relation-global': VirtualRelationGlobal;
|
||||
};
|
||||
globalsSelect: {
|
||||
global: GlobalSelect<false> | GlobalSelect<true>;
|
||||
'global-2': Global2Select<false> | Global2Select<true>;
|
||||
'global-3': Global3Select<false> | Global3Select<true>;
|
||||
'virtual-relation-global': VirtualRelationGlobalSelect<false> | VirtualRelationGlobalSelect<true>;
|
||||
};
|
||||
locale: 'en' | 'es';
|
||||
user: User & {
|
||||
@@ -147,6 +153,16 @@ export interface UserAuthOperations {
|
||||
password: string;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "categories".
|
||||
*/
|
||||
export interface Category {
|
||||
id: string;
|
||||
title?: string | null;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts".
|
||||
@@ -154,6 +170,9 @@ export interface UserAuthOperations {
|
||||
export interface Post {
|
||||
id: string;
|
||||
title: string;
|
||||
category?: (string | null) | Category;
|
||||
localized?: string | null;
|
||||
text?: string | null;
|
||||
number?: number | null;
|
||||
D1?: {
|
||||
D2?: {
|
||||
@@ -346,6 +365,20 @@ export interface Place {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relations".
|
||||
*/
|
||||
export interface VirtualRelation {
|
||||
id: string;
|
||||
postTitle?: string | null;
|
||||
postCategoryTitle?: string | null;
|
||||
postLocalized?: string | null;
|
||||
post?: (string | null) | Post;
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
_status?: ('draft' | 'published') | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-persistance".
|
||||
@@ -465,6 +498,10 @@ export interface User {
|
||||
export interface PayloadLockedDocument {
|
||||
id: string;
|
||||
document?:
|
||||
| ({
|
||||
relationTo: 'categories';
|
||||
value: string | Category;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'posts';
|
||||
value: string | Post;
|
||||
@@ -497,6 +534,10 @@ export interface PayloadLockedDocument {
|
||||
relationTo: 'places';
|
||||
value: string | Place;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'virtual-relations';
|
||||
value: string | VirtualRelation;
|
||||
} | null)
|
||||
| ({
|
||||
relationTo: 'fields-persistance';
|
||||
value: string | FieldsPersistance;
|
||||
@@ -567,12 +608,24 @@ export interface PayloadMigration {
|
||||
updatedAt: string;
|
||||
createdAt: string;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "categories_select".
|
||||
*/
|
||||
export interface CategoriesSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "posts_select".
|
||||
*/
|
||||
export interface PostsSelect<T extends boolean = true> {
|
||||
title?: T;
|
||||
category?: T;
|
||||
localized?: T;
|
||||
text?: T;
|
||||
number?: T;
|
||||
D1?:
|
||||
| T
|
||||
@@ -747,6 +800,19 @@ export interface PlacesSelect<T extends boolean = true> {
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relations_select".
|
||||
*/
|
||||
export interface VirtualRelationsSelect<T extends boolean = true> {
|
||||
postTitle?: T;
|
||||
postCategoryTitle?: T;
|
||||
postLocalized?: T;
|
||||
post?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
_status?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "fields-persistance_select".
|
||||
@@ -917,6 +983,17 @@ export interface Global3 {
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relation-global".
|
||||
*/
|
||||
export interface VirtualRelationGlobal {
|
||||
id: string;
|
||||
postTitle?: string | null;
|
||||
post?: (string | null) | Post;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "global_select".
|
||||
@@ -947,6 +1024,17 @@ export interface Global3Select<T extends boolean = true> {
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "virtual-relation-global_select".
|
||||
*/
|
||||
export interface VirtualRelationGlobalSelect<T extends boolean = true> {
|
||||
postTitle?: T;
|
||||
post?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
}
|
||||
/**
|
||||
* This interface was referenced by `Config`'s JSON-Schema
|
||||
* via the `definition` "auth".
|
||||
|
||||
Reference in New Issue
Block a user