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

@@ -79,6 +79,7 @@ The following options are available:
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#collection-config). |
| `defaultPopulate` | Specify which fields to select when this Collection is populated from another document. [More Details](../queries/select#defaultpopulate-collection-config-property). |
| `indexes` | Define compound indexes for this collection. This can be used to either speed up querying/sorting by 2 or more fields at the same time or to ensure uniqueness between several fields. |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
_* An asterisk denotes that a property is required._

View File

@@ -66,7 +66,7 @@ export const Nav: GlobalConfig = {
The following options are available:
| Option | Description |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `access` | Provide Access Control functions to define exactly who should be able to do what with this Global. [More details](../access-control/globals). |
| `admin` | The configuration options for the Admin Panel. [More details](#admin-options). |
| `custom` | Extension point for adding custom data (e.g. for plugins) |
@@ -81,6 +81,7 @@ The following options are available:
| `slug` * | Unique, URL-friendly string that will act as an identifier for this Global. |
| `typescript` | An object with property `interface` as the text used in schema generation. Auto-generated from slug if not defined. |
| `versions` | Set to true to enable default options, or configure with object properties. [More details](../versions/overview#global-config). |
| `forceSelect` | Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks |
_* An asterisk denotes that a property is required._

View File

@@ -56,6 +56,7 @@ const getPosts = async (payload: Payload) => {
<Banner type="warning">
**Important:**
To perform querying with `select` efficiently, Payload implements your `select` query on the database level. Because of that, your `beforeRead` and `afterRead` hooks may not receive the full `doc`.
To ensure that some fields are always selected for your hooks / access control, regardless of the `select` query you can use `forceSelect` collection config property.
</Banner>

View File

@@ -43,6 +43,7 @@ import type {
} from '../../index.js'
import type {
PayloadRequest,
SelectIncludeType,
SelectType,
Sort,
TransformCollectionWithSelect,
@@ -424,6 +425,12 @@ export type CollectionConfig<TSlug extends CollectionSlug = any> = {
*/
endpoints?: false | Omit<Endpoint, 'root'>[]
fields: Field[]
/**
* Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks
*/
forceSelect?: IsAny<SelectFromCollectionSlug<TSlug>> extends true
? SelectIncludeType
: SelectFromCollectionSlug<TSlug>
/**
* GraphQL configuration
*/

View File

@@ -35,6 +35,7 @@ import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { saveVersion } from '../../versions/saveVersion.js'
import { buildAfterOperation } from './utils.js'
@@ -109,7 +110,7 @@ export const createOperation = async <
payload: { config },
},
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -245,6 +246,11 @@ export const createOperation = async <
let doc
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
if (collectionConfig.auth && !collectionConfig.auth.disableLocalStrategy) {
if (collectionConfig.auth.verify) {
resultWithLocales._verified = Boolean(resultWithLocales._verified) || false

View File

@@ -23,6 +23,7 @@ import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions.js'
import { deleteScheduledPublishJobs } from '../../versions/deleteScheduledPublishJobs.js'
import { buildAfterOperation } from './utils.js'
@@ -80,7 +81,7 @@ export const deleteOperation = async <
payload,
},
req,
select,
select: incomingSelect,
showHiddenFields,
where,
} = args
@@ -108,6 +109,11 @@ export const deleteOperation = async <
const fullWhere = combineQueries(where, accessResult)
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
// /////////////////////////////////////
// Retrieve documents
// /////////////////////////////////////

View File

@@ -19,6 +19,7 @@ import { checkDocumentLockStatus } from '../../utilities/checkDocumentLockStatus
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { deleteCollectionVersions } from '../../versions/deleteCollectionVersions.js'
import { deleteScheduledPublishJobs } from '../../versions/deleteScheduledPublishJobs.js'
import { buildAfterOperation } from './utils.js'
@@ -75,7 +76,7 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
payload,
},
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -166,6 +167,11 @@ export const deleteByIDOperation = async <TSlug extends CollectionSlug, TSelect
})
}
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
// /////////////////////////////////////
// Delete document
// /////////////////////////////////////

View File

@@ -23,6 +23,7 @@ import { sanitizeJoinQuery } from '../../database/sanitizeJoinQuery.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
@@ -93,12 +94,17 @@ export const findOperation = async <
populate,
req: { fallbackLocale, locale, payload },
req,
select,
select: incomingSelect,
showHiddenFields,
sort,
where,
} = args
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
// /////////////////////////////////////
// Access
// /////////////////////////////////////

View File

@@ -22,6 +22,7 @@ import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { validateQueryPaths } from '../../index.js'
import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable.js'
import { buildAfterOperation } from './utils.js'
@@ -81,10 +82,15 @@ export const findByIDOperation = async <
populate,
req: { fallbackLocale, locale, t },
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
// /////////////////////////////////////
// Access
// /////////////////////////////////////

View File

@@ -10,6 +10,8 @@ import { combineQueries } from '../../database/combineQueries.js'
import { APIError, Forbidden, NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
export type Arguments = {
collection: Collection
@@ -37,7 +39,7 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
populate,
req: { fallbackLocale, locale, payload },
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -67,6 +69,11 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
// Find by ID
// /////////////////////////////////////
const select = sanitizeSelect({
forceSelect: getQueryDraftsSelect({ select: collectionConfig.forceSelect }),
select: incomingSelect,
})
const versionsQuery = await payload.db.findVersions<TData>({
collection: collectionConfig.slug,
limit: 1,

View File

@@ -10,7 +10,9 @@ import { validateQueryPaths } from '../../database/queryValidation/validateQuery
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
export type Arguments = {
collection: Collection
@@ -40,7 +42,7 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
populate,
req: { fallbackLocale, locale, payload },
req,
select,
select: incomingSelect,
showHiddenFields,
sort,
where,
@@ -69,6 +71,11 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
const fullWhere = combineQueries(where, accessResults)
const select = sanitizeSelect({
forceSelect: getQueryDraftsSelect({ select: collectionConfig.forceSelect }),
select: incomingSelect,
})
// /////////////////////////////////////
// Find
// /////////////////////////////////////

View File

@@ -12,6 +12,7 @@ import { APIError, Forbidden, NotFound } from '../../errors/index.js'
import { afterChange } from '../../fields/hooks/afterChange/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js'
export type Arguments = {
@@ -40,7 +41,7 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
populate,
req,
req: { fallbackLocale, locale, payload },
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -115,6 +116,11 @@ export const restoreVersionOperation = async <TData extends TypeWithID = any>(
// Update
// /////////////////////////////////////
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
let result = await req.payload.db.updateOne({
id: parentDocID,
collection: collectionConfig.slug,

View File

@@ -23,6 +23,7 @@ import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionCollectionFields } from '../../versions/buildCollectionFields.js'
import { appendVersionToQueryKey } from '../../versions/drafts/appendVersionToQueryKey.js'
import { updateDocument } from './utilities/update.js'
@@ -93,7 +94,7 @@ export const updateOperation = async <
payload,
},
req,
select,
select: incomingSelect,
showHiddenFields,
where,
} = args
@@ -183,6 +184,11 @@ export const updateOperation = async <
const { id } = docWithLocales
try {
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
// ///////////////////////////////////////////////
// Update document, runs all document level hooks
// ///////////////////////////////////////////////

View File

@@ -26,6 +26,7 @@ import { unlinkTempFiles } from '../../uploads/unlinkTempFiles.js'
import { commitTransaction } from '../../utilities/commitTransaction.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js'
import { updateDocument } from './utilities/update.js'
import { buildAfterOperation } from './utils.js'
@@ -100,7 +101,7 @@ export const updateByIDOperation = async <
payload,
},
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -159,6 +160,11 @@ export const updateByIDOperation = async <
throwOnMissingFile: false,
})
const select = sanitizeSelect({
forceSelect: collectionConfig.forceSelect,
select: incomingSelect,
})
// ///////////////////////////////////////////////
// Update document, runs all document level hooks
// ///////////////////////////////////////////////

View File

@@ -1,5 +1,5 @@
import type { GraphQLNonNull, GraphQLObjectType } from 'graphql'
import type { DeepRequired } from 'ts-essentials'
import type { DeepRequired, IsAny } from 'ts-essentials'
import type {
CustomPreviewButton,
@@ -22,7 +22,7 @@ import type {
import type { DBIdentifierName } from '../../database/types.js'
import type { Field, FlattenedField } from '../../fields/config/types.js'
import type { GlobalSlug, RequestContext, TypedGlobal, TypedGlobalSelect } from '../../index.js'
import type { PayloadRequest, Where } from '../../types/index.js'
import type { PayloadRequest, SelectIncludeType, Where } from '../../types/index.js'
import type { IncomingGlobalVersions, SanitizedGlobalVersions } from '../../versions/types.js'
export type DataFromGlobalSlug<TSlug extends GlobalSlug> = TypedGlobal[TSlug]
@@ -142,7 +142,7 @@ export type GlobalAdminOptions = {
preview?: GeneratePreviewURL
}
export type GlobalConfig = {
export type GlobalConfig<TSlug extends GlobalSlug = any> = {
/**
* Do not set this property manually. This is set to true during sanitization, to avoid
* sanitizing the same global multiple times.
@@ -163,6 +163,12 @@ export type GlobalConfig = {
dbName?: DBIdentifierName
endpoints?: false | Omit<Endpoint, 'root'>[]
fields: Field[]
/**
* Specify which fields should be selected always, regardless of the `select` query which can be useful that the field exists for access control / hooks
*/
forceSelect?: IsAny<SelectFromGlobalSlug<TSlug>> extends true
? SelectIncludeType
: SelectFromGlobalSlug<TSlug>
graphQL?:
| {
disableMutations?: true

View File

@@ -8,6 +8,7 @@ import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js'
import { getSelectMode } from '../../utilities/getSelectMode.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import replaceWithDraftIfAvailable from '../../versions/drafts/replaceWithDraftIfAvailable.js'
type Args = {
@@ -36,7 +37,7 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
populate,
req: { fallbackLocale, locale },
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -51,6 +52,11 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
accessResult = await executeAccess({ req }, globalConfig.access.read)
}
const select = sanitizeSelect({
forceSelect: globalConfig.forceSelect,
select: incomingSelect,
})
// /////////////////////////////////////
// Perform database operation
// /////////////////////////////////////

View File

@@ -10,6 +10,8 @@ import { Forbidden, NotFound } from '../../errors/index.js'
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
export type Arguments = {
currentDepth?: number
@@ -37,7 +39,7 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
populate,
req: { fallbackLocale, locale, payload },
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -57,6 +59,11 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
const hasWhereAccess = typeof accessResults === 'object'
const select = sanitizeSelect({
forceSelect: getQueryDraftsSelect({ select: globalConfig.forceSelect }),
select: incomingSelect,
})
const findGlobalVersionsArgs: FindGlobalVersionsArgs = {
global: globalConfig.slug,
limit: 1,

View File

@@ -10,7 +10,9 @@ import { validateQueryPaths } from '../../database/queryValidation/validateQuery
import { afterRead } from '../../fields/hooks/afterRead/index.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import sanitizeInternalFields from '../../utilities/sanitizeInternalFields.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { buildVersionGlobalFields } from '../../versions/buildGlobalFields.js'
import { getQueryDraftsSelect } from '../../versions/drafts/getQueryDraftsSelect.js'
export type Arguments = {
depth?: number
@@ -40,7 +42,7 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
populate,
req: { fallbackLocale, locale, payload },
req,
select,
select: incomingSelect,
showHiddenFields,
sort,
where,
@@ -67,6 +69,11 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
const fullWhere = combineQueries(where, accessResults)
const select = sanitizeSelect({
forceSelect: getQueryDraftsSelect({ select: globalConfig.forceSelect }),
select: incomingSelect,
})
// /////////////////////////////////////
// Find
// /////////////////////////////////////

View File

@@ -27,6 +27,7 @@ import { commitTransaction } from '../../utilities/commitTransaction.js'
import { getSelectMode } from '../../utilities/getSelectMode.js'
import { initTransaction } from '../../utilities/initTransaction.js'
import { killTransaction } from '../../utilities/killTransaction.js'
import { sanitizeSelect } from '../../utilities/sanitizeSelect.js'
import { getLatestGlobalVersion } from '../../versions/getLatestGlobalVersion.js'
import { saveVersion } from '../../versions/saveVersion.js'
@@ -70,7 +71,7 @@ export const updateOperation = async <
publishSpecificLocale,
req: { fallbackLocale, locale, payload },
req,
select,
select: incomingSelect,
showHiddenFields,
} = args
@@ -244,6 +245,11 @@ export const updateOperation = async <
// Update
// /////////////////////////////////////
const select = sanitizeSelect({
forceSelect: globalConfig.forceSelect,
select: incomingSelect,
})
if (!shouldSaveDraft) {
// Ensure global has createdAt
if (!result.createdAt) {

View File

@@ -0,0 +1,25 @@
import { deepMergeSimple } from '@payloadcms/translations/utilities'
import type { SelectType } from '../types/index.js'
import { getSelectMode } from './getSelectMode.js'
export const sanitizeSelect = ({
forceSelect,
select,
}: {
forceSelect?: SelectType
select?: SelectType
}): SelectType | undefined => {
if (!forceSelect || !select) {
return select
}
const selectMode = getSelectMode(select)
if (selectMode === 'exclude') {
return select
}
return deepMergeSimple(select, forceSelect)
}

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".