feat: add forceSelect collection / global config property (#11627)

### What?
Adds a new property to collection / global config `forceSelect` which
can be used to ensure that some fields are always selected, regardless
of the `select` query.

### Why?
This can be beneficial for hooks and access control, for example imagine
you need the value of `data.slug` in your hook.
With the following query it would be `undefined`:
`?select[title]=true`
Now, to solve this you can specify
```
forceSelect: {
  slug: true
}
```

### How?
Every operation now merges the incoming `select` with
`collectionConfig.forceSelect`.
This commit is contained in:
Sasha
2025-03-13 22:04:53 +02:00
committed by GitHub
parent ff2df62321
commit 5e3d07bf44
25 changed files with 344 additions and 37 deletions

View File

@@ -83,7 +83,7 @@ export interface Config {
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
defaultIDType: number;
};
globals: {
menu: Menu;
@@ -123,7 +123,7 @@ export interface UserAuthOperations {
* via the `definition` "posts".
*/
export interface Post {
id: string;
id: number;
title?: string | null;
content?: {
root: {
@@ -148,7 +148,7 @@ export interface Post {
* via the `definition` "media".
*/
export interface Media {
id: string;
id: number;
updatedAt: string;
createdAt: string;
url?: string | null;
@@ -192,7 +192,7 @@ export interface Media {
* via the `definition` "users".
*/
export interface User {
id: string;
id: number;
updatedAt: string;
createdAt: string;
email: string;
@@ -209,24 +209,24 @@ export interface User {
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
id: number;
document?:
| ({
relationTo: 'posts';
value: string | Post;
value: number | Post;
} | null)
| ({
relationTo: 'media';
value: string | Media;
value: number | Media;
} | null)
| ({
relationTo: 'users';
value: string | User;
value: number | User;
} | null);
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
updatedAt: string;
createdAt: string;
@@ -236,10 +236,10 @@ export interface PayloadLockedDocument {
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
id: number;
user: {
relationTo: 'users';
value: string | User;
value: number | User;
};
key?: string | null;
value?:
@@ -259,7 +259,7 @@ export interface PayloadPreference {
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
id: number;
name?: string | null;
batch?: number | null;
updatedAt: string;
@@ -378,7 +378,7 @@ export interface PayloadMigrationsSelect<T extends boolean = true> {
* via the `definition` "menu".
*/
export interface Menu {
id: string;
id: number;
globalText?: string | null;
updatedAt?: string | null;
createdAt?: string | null;

View File

@@ -0,0 +1,26 @@
import type { CollectionConfig } from 'payload'
export const ForceSelect: CollectionConfig<'force-select'> = {
slug: 'force-select',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'forceSelected',
type: 'text',
},
{
name: 'array',
type: 'array',
fields: [
{
name: 'forceSelected',
type: 'text',
},
],
},
],
forceSelect: { array: { forceSelected: true }, forceSelected: true },
}

View File

@@ -1,11 +1,16 @@
import type { GlobalConfig } from 'payload'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import { fileURLToPath } from 'node:url'
import path from 'path'
import type { Post } from './payload-types.js'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { CustomID } from './collections/CustomID/index.js'
import { DeepPostsCollection } from './collections/DeepPosts/index.js'
import { ForceSelect } from './collections/ForceSelect/index.js'
import { LocalizedPostsCollection } from './collections/LocalizedPosts/index.js'
import { Pages } from './collections/Pages/index.js'
import { Points } from './collections/Points/index.js'
@@ -24,6 +29,7 @@ export default buildConfigWithDefaults({
DeepPostsCollection,
Pages,
Points,
ForceSelect,
{
slug: 'upload',
fields: [],
@@ -51,6 +57,30 @@ export default buildConfigWithDefaults({
},
],
},
{
slug: 'force-select-global',
fields: [
{
name: 'text',
type: 'text',
},
{
name: 'forceSelected',
type: 'text',
},
{
name: 'array',
type: 'array',
fields: [
{
name: 'forceSelected',
type: 'text',
},
],
},
],
forceSelect: { array: { forceSelected: true }, forceSelected: true },
} satisfies GlobalConfig<'force-select-global'>,
],
admin: {
importMap: {

View File

@@ -2336,6 +2336,53 @@ describe('Select', () => {
})
})
})
it('should force collection select fields with forceSelect', async () => {
const { id, text, array, forceSelected } = await payload.create({
collection: 'force-select',
data: {
array: [{ forceSelected: 'text' }],
text: 'some-text',
forceSelected: 'force-selected',
},
})
const response = await payload.findByID({
collection: 'force-select',
id,
select: { text: true },
})
expect(response).toStrictEqual({
id,
forceSelected,
text,
array,
})
})
it('should force global select fields with forceSelect', async () => {
const { forceSelected, id, array, text } = await payload.updateGlobal({
slug: 'force-select-global',
data: {
array: [{ forceSelected: 'text' }],
text: 'some-text',
forceSelected: 'force-selected',
},
})
const response = await payload.findGlobal({
slug: 'force-select-global',
select: { text: true },
})
expect(response).toStrictEqual({
id,
forceSelected,
text,
array,
})
})
})
async function createPost() {

View File

@@ -72,6 +72,7 @@ export interface Config {
'deep-posts': DeepPost;
pages: Page;
points: Point;
'force-select': ForceSelect;
upload: Upload;
rels: Rel;
'custom-ids': CustomId;
@@ -88,6 +89,7 @@ export interface Config {
'deep-posts': DeepPostsSelect<false> | DeepPostsSelect<true>;
pages: PagesSelect<false> | PagesSelect<true>;
points: PointsSelect<false> | PointsSelect<true>;
'force-select': ForceSelectSelect<false> | ForceSelectSelect<true>;
upload: UploadSelect<false> | UploadSelect<true>;
rels: RelsSelect<false> | RelsSelect<true>;
'custom-ids': CustomIdsSelect<false> | CustomIdsSelect<true>;
@@ -101,9 +103,11 @@ export interface Config {
};
globals: {
'global-post': GlobalPost;
'force-select-global': ForceSelectGlobal;
};
globalsSelect: {
'global-post': GlobalPostSelect<false> | GlobalPostSelect<true>;
'force-select-global': ForceSelectGlobalSelect<false> | ForceSelectGlobalSelect<true>;
};
locale: 'en' | 'de';
user: User & {
@@ -445,6 +449,23 @@ export interface Point {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "force-select".
*/
export interface ForceSelect {
id: string;
text?: string | null;
forceSelected?: string | null;
array?:
| {
forceSelected?: string | null;
id?: string | null;
}[]
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "custom-ids".
@@ -503,6 +524,10 @@ export interface PayloadLockedDocument {
relationTo: 'points';
value: string | Point;
} | null)
| ({
relationTo: 'force-select';
value: string | ForceSelect;
} | null)
| ({
relationTo: 'upload';
value: string | Upload;
@@ -835,6 +860,22 @@ export interface PointsSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "force-select_select".
*/
export interface ForceSelectSelect<T extends boolean = true> {
text?: T;
forceSelected?: T;
array?:
| T
| {
forceSelected?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "upload_select".
@@ -928,6 +969,23 @@ export interface GlobalPost {
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "force-select-global".
*/
export interface ForceSelectGlobal {
id: string;
text?: string | null;
forceSelected?: string | null;
array?:
| {
forceSelected?: string | null;
id?: string | null;
}[]
| null;
updatedAt?: string | null;
createdAt?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "global-post_select".
@@ -939,6 +997,23 @@ export interface GlobalPostSelect<T extends boolean = true> {
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "force-select-global_select".
*/
export interface ForceSelectGlobalSelect<T extends boolean = true> {
text?: T;
forceSelected?: T;
array?:
| T
| {
forceSelected?: T;
id?: T;
};
updatedAt?: T;
createdAt?: T;
globalType?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".