feat: select fields (#8550)

Adds `select` which is used to specify the field projection for local
and rest API calls. This is available as an optimization to reduce the
payload's of requests and make the database queries more efficient.

Includes:
- [x] generate types for the `select` property
- [x] infer the return type by `select` with 2 modes - include (`field:
true`) and exclude (`field: false`)
- [x] lots of integration tests, including deep fields / localization
etc
- [x] implement the property in db adapters
- [x] implement the property in the local api for most operations
- [x] implement the property in the rest api 
- [x] docs

---------

Co-authored-by: Dan Ribbens <dan.ribbens@gmail.com>
This commit is contained in:
Sasha
2024-10-29 23:47:18 +02:00
committed by GitHub
parent 6cdf141380
commit dae832c288
116 changed files with 5491 additions and 371 deletions

2
test/select/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/media
/media-gif

View File

@@ -0,0 +1,76 @@
import type { CollectionConfig } from 'payload'
export const DeepPostsCollection: CollectionConfig = {
slug: 'deep-posts',
fields: [
{
name: 'group',
type: 'group',
fields: [
{
name: 'array',
type: 'array',
fields: [
{
name: 'group',
type: 'group',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
],
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'block',
fields: [
{
type: 'text',
name: 'text',
},
{
type: 'number',
name: 'number',
},
],
},
],
},
],
},
{
name: 'arrayTop',
type: 'array',
fields: [
{
type: 'text',
name: 'text',
},
{
type: 'array',
name: 'arrayNested',
fields: [
{
type: 'text',
name: 'text',
},
{
type: 'number',
name: 'number',
},
],
},
],
},
],
}

View File

@@ -0,0 +1,147 @@
import type { CollectionConfig } from 'payload'
export const LocalizedPostsCollection: CollectionConfig = {
slug: 'localized-posts',
admin: {
useAsTitle: 'text',
},
fields: [
{
name: 'text',
localized: true,
type: 'text',
},
{
name: 'number',
localized: true,
type: 'number',
},
{
name: 'group',
localized: true,
type: 'group',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
{
name: 'groupSecond',
type: 'group',
fields: [
{
name: 'text',
type: 'text',
localized: true,
},
{
name: 'number',
type: 'number',
},
],
},
{
name: 'array',
type: 'array',
localized: true,
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
{
name: 'arraySecond',
type: 'array',
fields: [
{
name: 'text',
type: 'text',
localized: true,
},
{
name: 'number',
type: 'number',
},
],
},
{
name: 'blocks',
type: 'blocks',
localized: true,
blocks: [
{
slug: 'intro',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'introText',
type: 'text',
},
],
},
{
slug: 'cta',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'ctaText',
type: 'text',
},
],
},
],
},
{
name: 'blocksSecond',
type: 'blocks',
blocks: [
{
slug: 'first',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'firstText',
type: 'text',
localized: true,
},
],
},
{
slug: 'second',
fields: [
{
name: 'text',
type: 'text',
localized: true,
},
{
name: 'secondText',
type: 'text',
},
],
},
],
},
],
}

View File

@@ -0,0 +1,78 @@
import type { CollectionConfig } from 'payload'
export const PostsCollection: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'text',
},
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
{
name: 'group',
type: 'group',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
{
name: 'array',
type: 'array',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'intro',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'introText',
type: 'text',
},
],
},
{
slug: 'cta',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'ctaText',
type: 'text',
},
],
},
],
},
],
}

View File

@@ -0,0 +1,43 @@
import type { CollectionConfig } from 'payload'
export const VersionedPostsCollection: CollectionConfig = {
slug: 'versioned-posts',
versions: {
drafts: true,
},
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
{
name: 'array',
type: 'array',
fields: [
{
name: 'text',
type: 'text',
},
],
},
{
name: 'blocks',
type: 'blocks',
blocks: [
{
slug: 'test',
fields: [
{
name: 'text',
type: 'text',
},
],
},
],
},
],
}

73
test/select/config.ts Normal file
View File

@@ -0,0 +1,73 @@
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { fileURLToPath } from 'node:url'
import path from 'path'
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 { PostsCollection } from './collections/Posts/index.js'
import { VersionedPostsCollection } from './collections/VersionedPosts/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
// ...extend config here
collections: [
PostsCollection,
LocalizedPostsCollection,
VersionedPostsCollection,
DeepPostsCollection,
],
globals: [
{
slug: 'global-post',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'number',
type: 'number',
},
],
},
],
admin: {
importMap: {
baseDir: path.resolve(dirname),
},
},
localization: {
locales: ['en', 'de'],
defaultLocale: 'en',
},
editor: lexicalEditor({
features: ({ defaultFeatures }) => [...defaultFeatures],
}),
cors: ['http://localhost:3000', 'http://localhost:3001'],
onInit: async (payload) => {
await payload.create({
collection: 'users',
data: {
email: devUser.email,
password: devUser.password,
},
})
// // Create image
// const imageFilePath = path.resolve(dirname, '../uploads/image.png')
// const imageFile = await getFileByPath(imageFilePath)
// await payload.create({
// collection: 'media',
// data: {},
// file: imageFile,
// })
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})

View File

@@ -0,0 +1,19 @@
import { rootParserOptions } from '../../eslint.config.js'
import { testEslintConfig } from '../eslint.config.js'
/** @typedef {import('eslint').Linter.Config} Config */
/** @type {Config[]} */
export const index = [
...testEslintConfig,
{
languageOptions: {
parserOptions: {
...rootParserOptions,
tsconfigRootDir: import.meta.dirname,
},
},
},
]
export default index

1677
test/select/int.spec.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,604 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
posts: Post;
'localized-posts': LocalizedPost;
'versioned-posts': VersionedPost;
'deep-posts': DeepPost;
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsSelect?: {
posts: PostsSelect<false> | PostsSelect<true>;
'localized-posts': LocalizedPostsSelect<false> | LocalizedPostsSelect<true>;
'versioned-posts': VersionedPostsSelect<false> | VersionedPostsSelect<true>;
'deep-posts': DeepPostsSelect<false> | DeepPostsSelect<true>;
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {
'global-post': GlobalPost;
};
globalsSelect?: {
'global-post': GlobalPostSelect<false> | GlobalPostSelect<true>;
};
locale: 'en' | 'de';
user: User & {
collection: 'users';
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts".
*/
export interface Post {
id: string;
text?: string | null;
number?: number | null;
group?: {
text?: string | null;
number?: number | null;
};
array?:
| {
text?: string | null;
number?: number | null;
id?: string | null;
}[]
| null;
blocks?:
| (
| {
text?: string | null;
introText?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'intro';
}
| {
text?: string | null;
ctaText?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'cta';
}
)[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts".
*/
export interface LocalizedPost {
id: string;
text?: string | null;
number?: number | null;
group?: {
text?: string | null;
number?: number | null;
};
groupSecond?: {
text?: string | null;
number?: number | null;
};
array?:
| {
text?: string | null;
number?: number | null;
id?: string | null;
}[]
| null;
arraySecond?:
| {
text?: string | null;
number?: number | null;
id?: string | null;
}[]
| null;
blocks?:
| (
| {
text?: string | null;
introText?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'intro';
}
| {
text?: string | null;
ctaText?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'cta';
}
)[]
| null;
blocksSecond?:
| (
| {
text?: string | null;
firstText?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'first';
}
| {
text?: string | null;
secondText?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'second';
}
)[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "versioned-posts".
*/
export interface VersionedPost {
id: string;
text?: string | null;
number?: number | null;
array?:
| {
text?: string | null;
id?: string | null;
}[]
| null;
blocks?:
| {
text?: string | null;
id?: string | null;
blockName?: string | null;
blockType: 'test';
}[]
| null;
updatedAt: string;
createdAt: string;
_status?: ('draft' | 'published') | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "deep-posts".
*/
export interface DeepPost {
id: string;
group?: {
array?:
| {
group?: {
text?: string | null;
number?: number | null;
};
id?: string | null;
}[]
| null;
blocks?:
| {
text?: string | null;
number?: number | null;
id?: string | null;
blockName?: string | null;
blockType: 'block';
}[]
| null;
};
arrayTop?:
| {
text?: string | null;
arrayNested?:
| {
text?: string | null;
number?: number | null;
id?: string | null;
}[]
| null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?:
| ({
relationTo: 'posts';
value: string | Post;
} | null)
| ({
relationTo: 'localized-posts';
value: string | LocalizedPost;
} | null)
| ({
relationTo: 'versioned-posts';
value: string | VersionedPost;
} | null)
| ({
relationTo: 'deep-posts';
value: string | DeepPost;
} | null)
| ({
relationTo: 'users';
value: string | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "posts_select".
*/
export interface PostsSelect<T extends boolean = true> {
text?: T;
number?: T;
group?:
| T
| {
text?: T;
number?: T;
};
array?:
| T
| {
text?: T;
number?: T;
id?: T;
};
blocks?:
| T
| {
intro?:
| T
| {
text?: T;
introText?: T;
id?: T;
blockName?: T;
};
cta?:
| T
| {
text?: T;
ctaText?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "localized-posts_select".
*/
export interface LocalizedPostsSelect<T extends boolean = true> {
text?: T;
number?: T;
group?:
| T
| {
text?: T;
number?: T;
};
groupSecond?:
| T
| {
text?: T;
number?: T;
};
array?:
| T
| {
text?: T;
number?: T;
id?: T;
};
arraySecond?:
| T
| {
text?: T;
number?: T;
id?: T;
};
blocks?:
| T
| {
intro?:
| T
| {
text?: T;
introText?: T;
id?: T;
blockName?: T;
};
cta?:
| T
| {
text?: T;
ctaText?: T;
id?: T;
blockName?: T;
};
};
blocksSecond?:
| T
| {
first?:
| T
| {
text?: T;
firstText?: T;
id?: T;
blockName?: T;
};
second?:
| T
| {
text?: T;
secondText?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "versioned-posts_select".
*/
export interface VersionedPostsSelect<T extends boolean = true> {
text?: T;
number?: T;
array?:
| T
| {
text?: T;
id?: T;
};
blocks?:
| T
| {
test?:
| T
| {
text?: T;
id?: T;
blockName?: T;
};
};
updatedAt?: T;
createdAt?: T;
_status?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "deep-posts_select".
*/
export interface DeepPostsSelect<T extends boolean = true> {
group?:
| T
| {
array?:
| T
| {
group?:
| T
| {
text?: T;
number?: T;
};
id?: T;
};
blocks?:
| T
| {
block?:
| T
| {
text?: T;
number?: T;
id?: T;
blockName?: T;
};
};
};
arrayTop?:
| T
| {
text?: T;
arrayNested?:
| T
| {
text?: T;
number?: T;
id?: T;
};
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-post".
*/
export interface GlobalPost {
id: string;
text?: string | null;
number?: number | null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-post_select".
*/
export interface GlobalPostSelect<T extends boolean = true> {
text?: T;
number?: T;
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
// @ts-ignore
export interface GeneratedTypes extends Config {}
}

View File

@@ -0,0 +1,13 @@
{
// extend your base config to share compilerOptions, etc
//"extends": "./tsconfig.json",
"compilerOptions": {
// ensure that nobody can accidentally use this config for a build
"noEmit": true
},
"include": [
// whatever paths you intend to lint
"./**/*.ts",
"./**/*.tsx"
]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.json"
}