Skip to content

Commit 8f7ef35

Browse files
authored
fix: default values inconsistent between Local and REST APIs (#14556)
### What? Default values were incorrectly appearing (or not appearing) depending on access control configuration. When access control denied access to a global or collection document, default values would sometimes still populate, and conversely, when access was allowed, defaults would sometimes be blocked. ### Why? The previous logic checked `args.data` before querying the database, making it impossible to distinguish between: 1. A document that doesn't exist and access is allowed (should populate defaults for globals) 2. A document that was filtered by access control (should NOT populate defaults) This caused default values like `_status: 'draft'` to appear when access control should have returned an empty response, and also blocked legitimate defaults on uncreated globals without access restrictions. ### How? - **Query first**: Both globals and collections now query the database before checking `args.data` - **Detect access denial**: Only return early when `!docFromDB && !args.data && !overrideAccess && accessResult !== true` - **Correct fallback**: Use `args.data ?? docFromDB ?? {}` (globals) or `args.data ?? docFromDB` (collections) - **Simplified**: Removed `shouldSkipDefaults` logic from `promise.ts` since it's now handled at the operation level Fixes #14493 Fixes #14476
1 parent fad476d commit 8f7ef35

File tree

4 files changed

+28
-23
lines changed

4 files changed

+28
-23
lines changed

packages/payload/src/collections/operations/findByID.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,17 +170,18 @@ export const findByIDOperation = async <
170170
throw new NotFound(t)
171171
}
172172

173-
let result: DataFromCollectionSlug<TSlug> =
174-
(args.data as DataFromCollectionSlug<TSlug>) ?? (await req.payload.db.findOne(findOneArgs))!
173+
const docFromDB = await req.payload.db.findOne(findOneArgs)
175174

176-
if (!result) {
175+
if (!docFromDB && !args.data) {
177176
if (!disableErrors) {
178177
throw new NotFound(req.t)
179178
}
180-
181179
return null!
182180
}
183181

182+
let result: DataFromCollectionSlug<TSlug> =
183+
(args.data as DataFromCollectionSlug<TSlug>) ?? docFromDB!
184+
184185
// /////////////////////////////////////
185186
// Include Lock Status if required
186187
// /////////////////////////////////////

packages/payload/src/fields/hooks/afterRead/promise.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -374,18 +374,11 @@ export const promise = async ({
374374

375375
// Set defaultValue on the field for globals being returned without being first created
376376
// or collection documents created prior to having a default.
377-
378-
// Skip setting defaults when: global has no ID (never created or filtered by access)
379-
// AND access control is active (not overriding). This prevents default values like
380-
// `_status: 'draft'` from appearing when access control filters out the document.
381-
const shouldSkipDefaults = global && !doc.id && !overrideAccess
382-
383377
if (
384378
!removedFieldValue &&
385379
allowDefaultValue &&
386380
typeof siblingDoc[field.name!] === 'undefined' &&
387-
typeof field.defaultValue !== 'undefined' &&
388-
!shouldSkipDefaults
381+
typeof field.defaultValue !== 'undefined'
389382
) {
390383
siblingDoc[field.name!] = await getDefaultValue({
391384
defaultValue: field.defaultValue,

packages/payload/src/globals/operations/findOne.ts

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type {
99
import type { SanitizedGlobalConfig } from '../config/types.js'
1010

1111
import { executeAccess } from '../../auth/executeAccess.js'
12+
import { NotFound } from '../../errors/NotFound.js'
1213
import { afterRead, type AfterReadArgs } from '../../fields/hooks/afterRead/index.js'
1314
import { lockedDocumentsCollectionSlug } from '../../locked-documents/config.js'
1415
import { getSelectMode } from '../../utilities/getSelectMode.js'
@@ -80,6 +81,10 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
8081
accessResult = await executeAccess({ req }, globalConfig.access.read)
8182
}
8283

84+
if (accessResult === false) {
85+
throw new NotFound(req.t)
86+
}
87+
8388
const select = sanitizeSelect({
8489
fields: globalConfig.flattenedFields,
8590
forceSelect: globalConfig.forceSelect,
@@ -90,19 +95,23 @@ export const findOneOperation = async <T extends Record<string, unknown>>(
9095
// Perform database operation
9196
// /////////////////////////////////////
9297

93-
let doc =
94-
(args.data as any) ??
95-
(await req.payload.db.findGlobal({
96-
slug,
97-
locale: locale!,
98-
req,
99-
select,
100-
where: overrideAccess ? undefined : (accessResult as Where),
101-
}))
102-
if (!doc) {
103-
doc = {}
98+
const docFromDB = await req.payload.db.findGlobal({
99+
slug,
100+
locale: locale!,
101+
req,
102+
select,
103+
where: overrideAccess ? undefined : (accessResult as Where),
104+
})
105+
106+
// Check if no document was returned (Postgres returns {} instead of null)
107+
const hasDoc = docFromDB && Object.keys(docFromDB).length > 0
108+
109+
if (!hasDoc && !args.data && !overrideAccess && accessResult !== true) {
110+
return {} as any
104111
}
105112

113+
let doc = (args.data as any) ?? (hasDoc ? docFromDB : null) ?? {}
114+
106115
// /////////////////////////////////////
107116
// Include Lock Status if required
108117
// /////////////////////////////////////

test/live-preview/int.spec.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,8 @@ describe('Collections - Live Preview', () => {
929929
initialData,
930930
})
931931

932+
merge1.id = initialData.id
933+
932934
expect(merge1.relationshipMonoHasOne).toMatchObject(testPost)
933935
expect(merge1.relationshipMonoHasMany).toMatchObject([testPost])
934936

0 commit comments

Comments
 (0)