feat: add defaultPopulate property to collection config (#8934)

### What?
Adds `defaultPopulate` property to collection config that allows to
specify which fields to select when the collection is populated from
another document.
```ts
import type { CollectionConfig } from 'payload'

// The TSlug generic can be passed to have type safety for `defaultPopulate`.
// If avoided, the `defaultPopulate` type resolves to `SelectType`.
export const Pages: CollectionConfig<'pages'> = {
  slug: 'pages',
  // I need only slug, NOT the WHOLE CONTENT!
  defaultPopulate: {
    slug: true,
  },
  fields: [
    {
      name: 'slug',
      type: 'text',
      required: true,
    },
  ],
}
```

### Why?
This is essential for example in case of links. You don't need the whole
document, which can contain large data but only the `slug`.

### How?
Implements `defaultPopulate` when populating relationships, including
inside of lexical / slate rich text fields.
This commit is contained in:
Sasha
2024-10-30 19:41:34 +02:00
committed by GitHub
parent d38d7b8932
commit c41ef65a2b
15 changed files with 375 additions and 5 deletions

View File

@@ -0,0 +1,84 @@
import type { CollectionConfig } from 'payload'
import { lexicalEditor, LinkFeature } from '@payloadcms/richtext-lexical'
import { slateEditor } from '@payloadcms/richtext-slate'
// The TSlug generic can be passed to have type safety for `defaultPopulate`.
// If avoided, the `defaultPopulate` type resolves to `SelectType`.
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
// I need only slug, NOT the WHOLE CONTENT!
defaultPopulate: {
slug: true,
},
fields: [
{
name: 'content',
type: 'blocks',
blocks: [
{
slug: 'cta',
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'link',
type: 'group',
fields: [
{
name: 'docPoly',
type: 'relationship',
relationTo: ['pages'],
},
{
name: 'doc',
type: 'relationship',
relationTo: 'pages',
},
{
name: 'docMany',
hasMany: true,
type: 'relationship',
relationTo: 'pages',
},
{
name: 'docHasManyPoly',
type: 'relationship',
relationTo: ['pages'],
hasMany: true,
},
{
name: 'label',
type: 'text',
required: true,
},
],
},
{
name: 'richTextLexical',
type: 'richText',
editor: lexicalEditor({
features({ defaultFeatures }) {
return [...defaultFeatures, LinkFeature({ enabledCollections: ['pages'] })]
},
}),
},
{
name: 'richTextSlate',
type: 'richText',
editor: slateEditor({}),
},
],
},
],
},
{
name: 'slug',
type: 'text',
required: true,
},
],
}

View File

@@ -6,6 +6,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { DeepPostsCollection } from './collections/DeepPosts/index.js'
import { LocalizedPostsCollection } from './collections/LocalizedPosts/index.js'
import { Pages } from './collections/Pages/index.js'
import { PostsCollection } from './collections/Posts/index.js'
import { VersionedPostsCollection } from './collections/VersionedPosts/index.js'
@@ -19,6 +20,7 @@ export default buildConfigWithDefaults({
LocalizedPostsCollection,
VersionedPostsCollection,
DeepPostsCollection,
Pages,
],
globals: [
{

View File

@@ -8,6 +8,7 @@ import type {
DeepPost,
GlobalPost,
LocalizedPost,
Page,
Post,
VersionedPost,
} from './payload-types.js'
@@ -1561,6 +1562,141 @@ describe('Select', () => {
})
})
})
describe('defaultPopulate', () => {
let homePage: Page
let aboutPage: Page
let expectedHomePage: { id: number | string; slug: string }
beforeAll(async () => {
homePage = await payload.create({
depth: 0,
collection: 'pages',
data: { content: [], slug: 'home' },
})
expectedHomePage = { id: homePage.id, slug: homePage.slug }
aboutPage = await payload.create({
depth: 0,
collection: 'pages',
data: {
content: [
{
blockType: 'cta',
richTextSlate: [
{
type: 'relationship',
relationTo: 'pages',
value: { id: homePage.id },
},
],
richTextLexical: {
root: {
children: [
{
format: '',
type: 'relationship',
version: 2,
relationTo: 'pages',
value: homePage.id,
},
],
direction: 'ltr',
format: '',
indent: 0,
type: 'root',
version: 1,
},
},
link: {
doc: homePage.id,
docHasManyPoly: [
{
relationTo: 'pages',
value: homePage.id,
},
],
docMany: [homePage.id],
docPoly: {
relationTo: 'pages',
value: homePage.id,
},
label: 'Visit our Home Page!',
},
title: 'Contact Us',
},
],
slug: 'about',
},
})
})
it('local API - should populate with the defaultPopulate select shape', async () => {
const result = await payload.findByID({ collection: 'pages', depth: 1, id: aboutPage.id })
const {
content: [
{
link: { doc, docHasManyPoly, docMany, docPoly },
richTextSlate: [richTextSlateRel],
richTextLexical: {
root: {
children: [richTextLexicalRel],
},
},
},
],
} = result
expect(doc).toStrictEqual(expectedHomePage)
expect(docMany).toStrictEqual([expectedHomePage])
expect(docPoly).toStrictEqual({
relationTo: 'pages',
value: expectedHomePage,
})
expect(docHasManyPoly).toStrictEqual([
{
relationTo: 'pages',
value: expectedHomePage,
},
])
expect(richTextLexicalRel.value).toStrictEqual(expectedHomePage)
expect(richTextSlateRel.value).toStrictEqual(expectedHomePage)
})
it('REST API - should populate with the defaultPopulate select shape', async () => {
const restResult = await (
await restClient.GET(`/pages/${aboutPage.id}`, { query: { depth: 1 } })
).json()
const {
content: [
{
link: { doc, docHasManyPoly, docMany, docPoly },
richTextSlate: [richTextSlateRel],
richTextLexical: {
root: {
children: [richTextLexicalRel],
},
},
},
],
} = restResult
expect(doc).toMatchObject(expectedHomePage)
expect(docMany).toMatchObject([expectedHomePage])
expect(docPoly).toMatchObject({
relationTo: 'pages',
value: expectedHomePage,
})
expect(docHasManyPoly).toMatchObject([
{
relationTo: 'pages',
value: expectedHomePage,
},
])
expect(richTextLexicalRel.value).toMatchObject(expectedHomePage)
expect(richTextSlateRel.value).toMatchObject(expectedHomePage)
})
})
})
function createPost() {

View File

@@ -15,6 +15,7 @@ export interface Config {
'localized-posts': LocalizedPost;
'versioned-posts': VersionedPost;
'deep-posts': DeepPost;
pages: Page;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
@@ -25,6 +26,7 @@ export interface Config {
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
'versioned-posts': VersionedPostsSelect<false> | VersionedPostsSelect<true>;
'deep-posts': DeepPostsSelect<false> | DeepPostsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
@@ -239,6 +241,59 @@ export interface DeepPost {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages".
*/
export interface Page {
id: string;
content?:
| {
title: string;
link: {
docPoly?: {
relationTo: 'pages';
value: string | Page;
} | null;
doc?: (string | null) | Page;
docMany?: (string | Page)[] | null;
docHasManyPoly?:
| {
relationTo: 'pages';
value: string | Page;
}[]
| null;
label: string;
};
richTextLexical?: {
root: {
type: string;
children: {
type: string;
version: number;
[k: string]: unknown;
}[];
direction: ('ltr' | 'rtl') | null;
format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | '';
indent: number;
version: number;
};
[k: string]: unknown;
} | null;
richTextSlate?:
| {
[k: string]: unknown;
}[]
| null;
id?: string | null;
blockName?: string | null;
blockType: 'cta';
}[]
| null;
slug: string;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
@@ -279,6 +334,10 @@ export interface PayloadLockedDocument {
relationTo: 'deep-posts';
value: string | DeepPost;
} | null)
| ({
relationTo: 'pages';
value: string | Page;
} | null)
| ({
relationTo: 'users';
value: string | User;
@@ -520,6 +579,37 @@ export interface DeepPostsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "pages_select".
*/
export interface PagesSelect<T extends boolean = true> {
content?:
| T
| {
cta?:
| T
| {
title?: T;
link?:
| T
| {
docPoly?: T;
doc?: T;
docMany?: T;
docHasManyPoly?: T;
label?: T;
};
richTextLexical?: T;
richTextSlate?: T;
id?: T;
blockName?: T;
};
};
slug?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".