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:
@@ -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._
|
||||
|
||||
|
||||
@@ -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._
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// ///////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
// ///////////////////////////////////////////////
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
// /////////////////////////////////////
|
||||
|
||||
@@ -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) {
|
||||
|
||||
25
packages/payload/src/utilities/sanitizeSelect.ts
Normal file
25
packages/payload/src/utilities/sanitizeSelect.ts
Normal 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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
26
test/select/collections/ForceSelect/index.tsx
Normal file
26
test/select/collections/ForceSelect/index.tsx
Normal 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 },
|
||||
}
|
||||
@@ -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: {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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".
|
||||
|
||||
Reference in New Issue
Block a user