Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions packages/payload/src/auth/operations/registerFirstUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
DataFromCollectionSlug,
RequiredDataFromCollectionSlug,
} from '../../collections/config/types.js'
import type { AuthCollectionSlug } from '../../index.js'
import type { AuthCollectionSlug, CollectionSlug } from '../../index.js'
import type { PayloadRequest, SelectType } from '../../types/index.js'

import { Forbidden } from '../../errors/index.js'
Expand Down Expand Up @@ -78,9 +78,12 @@ export const registerFirstUserOperation = async <TSlug extends AuthCollectionSlu
// Register first user
// /////////////////////////////////////

const result = await payload.create<TSlug, SelectType>({
collection: slug as TSlug,
data,
// Widened to `CollectionSlug` so the strict `Options` conditional in `create` resolves
// to its wide-slug branch. A naked TSlug generic keeps the conditional deferred and
// TypeScript can't route this call to a specific branch.
const result = await payload.create<CollectionSlug, SelectType>({
collection: slug,
data: data as RequiredDataFromCollectionSlug<CollectionSlug>,
overrideAccess: true,
req,
})
Expand Down
45 changes: 22 additions & 23 deletions packages/payload/src/collections/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,31 +93,30 @@ export type CollectionsWithoutDrafts = {
}[CollectionSlug]

/**
* Conditionally allows or forbids the `draft` property based on collection configuration.
* When `strictDraftTypes` is enabled, the `draft` property is forbidden on collections without drafts.
* Allows or forbids the `draft` property based on collection configuration.
* The `draft` property is forbidden on collections without drafts.
*
* The conditional lives inside the object so the return shape is always `{ draft?: X }`.
* That keeps `Pick<FindOptions, 'select'>` and other indexed accesses stable when
* `FindOptions` is composed via intersection with this helper.
*
* The `CollectionSlug extends TSlug` check catches the wide/generic case where TSlug is the
* full slug union (internal calls) and returns `boolean` so those calls don't get locked
* out. A narrowed TSlug distributes through the second branch and gets `never` when it
* lacks drafts.
*/
export type DraftFlagFromCollectionSlug<TSlug extends CollectionSlug> = GeneratedTypes extends {
strictDraftTypes: true
export type DraftFlagFromCollectionSlug<TSlug extends CollectionSlug> = {
/**
* Whether the document(s) should be queried from the versions table/collection or not. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*
* Typed as `never` when the collection has no drafts, so passing `draft` fails at compile time.
*/
draft?: CollectionSlug extends TSlug
? boolean
: [TSlug] extends [CollectionsWithoutDrafts]
? never
: boolean
}
? TSlug extends CollectionsWithoutDrafts
? {
/**
* The `draft` property is not allowed because this collection does not have `versions.drafts` enabled.
*/
draft?: never
}
: {
/**
* Whether the document(s) should be queried from the versions table/collection or not. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
}
: {
/**
* Whether the document(s) should be queried from the versions table/collection or not. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
}

export type AuthOperationsFromCollectionSlug<TSlug extends CollectionSlug> =
TypedAuthOperations[TSlug]
Expand Down
104 changes: 40 additions & 64 deletions packages/payload/src/collections/operations/local/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
type CollectionSlug,
deepCopyObjectSimple,
type FindOptions,
type GeneratedTypes,
type Payload,
type RequestContext,
type TypedLocale,
Expand Down Expand Up @@ -114,73 +113,50 @@ type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType> = {
export type Options<
TSlug extends CollectionSlug,
TSelect extends SelectType,
> = GeneratedTypes extends { strictDraftTypes: true }
? CollectionsWithoutDrafts extends TSlug
> = CollectionSlug extends TSlug
? {
/**
* The data for the document to create.
*/
data: RequiredDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
} & BaseOptions<TSlug, TSelect>
: TSlug extends CollectionsWithoutDrafts
? {
data: RequiredDataFromCollectionSlug<TSlug>
/**
* The data for the document to create.
* The `draft` property is not allowed because this collection does not have `versions.drafts` enabled.
*/
data: DataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
draft?: never
} & BaseOptions<TSlug, TSelect>
: TSlug extends CollectionsWithoutDrafts
? {
data: RequiredDataFromCollectionSlug<TSlug>
/**
* The `draft` property is not allowed because this collection does not have `versions.drafts` enabled.
*/
draft?: never
} & BaseOptions<TSlug, TSelect>
: (
| {
/**
* The data for the document to create.
*/
data: RequiredDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
* Omit this property or set to `false` to create a published document.
*/
draft?: false
}
| {
/**
* The data for the document to create.
* When creating a draft, required fields are optional as validation is skipped by default.
*/
data: DraftDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft: true
}
) &
BaseOptions<TSlug, TSelect>
:
| ({
/**
* The data for the document to create.
*/
data: RequiredDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: false
} & BaseOptions<TSlug, TSelect>)
| ({
/**
* The data for the document to create.
* When creating a draft, required fields are optional as validation is skipped by default.
*/
data: DraftDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft: true
} & BaseOptions<TSlug, TSelect>)
: (
| {
/**
* The data for the document to create.
*/
data: RequiredDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
* Omit this property or set to `false` to create a published document.
*/
draft?: false
}
| {
/**
* The data for the document to create.
* When creating a draft, required fields are optional as validation is skipped by default.
*/
data: DraftDataFromCollectionSlug<TSlug>
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft: true
}
) &
BaseOptions<TSlug, TSelect>

export async function createLocal<
TSlug extends CollectionSlug,
Expand Down
5 changes: 1 addition & 4 deletions packages/payload/src/collections/operations/local/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import type {
CollectionSlug,
JoinQuery,
Payload,
PayloadTypes,
RequestContext,
TypedFallbackLocale,
TypedLocale,
Expand Down Expand Up @@ -197,9 +196,7 @@ export async function findLocal<
): Promise<
PaginatedDocs<
TDraft extends true
? PayloadTypes extends { strictDraftTypes: true }
? DraftTransformCollectionWithSelect<TSlug, TSelect>
: TransformCollectionWithSelect<TSlug, TSelect>
? DraftTransformCollectionWithSelect<TSlug, TSelect>
: TransformCollectionWithSelect<TSlug, TSelect>
>
> {
Expand Down
10 changes: 0 additions & 10 deletions packages/payload/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,16 +1340,6 @@ type RootTypeScriptConfig = {
jsonSchema: JSONSchema4
}) => JSONSchema4
>

/**
* Enable strict type safety for draft operations. When enabled, the `draft` parameter is forbidden
* on collections without drafts, and query results with `draft: true` type required fields as optional.
* This prevents invalid draft usage at compile time and ensures type correctness across all Local API operations.
*
* @default false
* @todo Remove in v4. Strict draft types will become the default behavior.
*/
strictDraftTypes?: boolean
}

/**
Expand Down
40 changes: 17 additions & 23 deletions packages/payload/src/globals/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,31 +46,25 @@ export type GlobalsWithoutDrafts = {
}[GlobalSlug]

/**
* Conditionally allows or forbids the `draft` property based on global configuration.
* When `strictDraftTypes` is enabled, the `draft` property is forbidden on globals without drafts.
* Allows or forbids the `draft` property based on global configuration.
* The `draft` property is forbidden on globals without drafts.
*
* The conditional lives inside the object so the return shape is always `{ draft?: X }`.
* That keeps `Pick<FindOptions, 'select'>` and other indexed accesses stable when
* intersected with this helper.
*/
export type DraftFlagFromGlobalSlug<TSlug extends GlobalSlug> = GeneratedTypes extends {
strictDraftTypes: true
export type DraftFlagFromGlobalSlug<TSlug extends GlobalSlug> = {
/**
* Whether the global should be queried from the versions table/collection or not. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*
* Typed as `never` when the global has no drafts, so passing `draft` fails at compile time.
*/
draft?: GlobalSlug extends TSlug
? boolean
: [TSlug] extends [GlobalsWithoutDrafts]
? never
: boolean
}
? TSlug extends GlobalsWithoutDrafts
? {
/**
* The `draft` property is not allowed because this global does not have `versions.drafts` enabled.
*/
draft?: never
}
: {
/**
* Whether the global should be queried from the versions table/collection or not. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
}
: {
/**
* Whether the global should be queried from the versions table/collection or not. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
}

export type BeforeValidateHook = (args: {
context: RequestContext
Expand Down
4 changes: 1 addition & 3 deletions packages/payload/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -594,9 +594,7 @@ export class BasePayload {
): Promise<
PaginatedDocs<
TDraft extends true
? PayloadTypes extends { strictDraftTypes: true }
? DraftTransformCollectionWithSelect<TSlug, TSelect>
: TransformCollectionWithSelect<TSlug, TSelect>
? DraftTransformCollectionWithSelect<TSlug, TSelect>
: TransformCollectionWithSelect<TSlug, TSelect>
>
> => {
Expand Down
9 changes: 0 additions & 9 deletions packages/payload/src/utilities/configToJSONSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1664,14 +1664,6 @@ export function configToJSONSchema(
globalsInput: generateEntityInputSchemas(config.globals || []),
}
: {}),
...(config.typescript?.strictDraftTypes
? {
strictDraftTypes: {
type: 'boolean',
const: true,
},
}
: {}),
user: generateAuthEntitySchemas(config.collections),
},
required: [
Expand All @@ -1683,7 +1675,6 @@ export function configToJSONSchema(
'collectionsJoins',
'globalsSelect',
...(generateInputTypes ? ['collectionsInput', 'globalsInput'] : []),
...(config.typescript?.strictDraftTypes ? ['strictDraftTypes'] : []),
'globals',
'auth',
'db',
Expand Down
6 changes: 4 additions & 2 deletions test/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,10 @@ export default buildConfigWithDefaults({
RelationshipFeature(),
BlocksFeature({
blocks: [
{ slug: 'cta', fields: [{ name: 'link', type: 'relationship', relationTo: 'pages' }] },
{
slug: 'cta',
fields: [{ name: 'link', type: 'relationship', relationTo: 'pages' }],
},
],
}),
],
Expand Down Expand Up @@ -277,7 +280,6 @@ export default buildConfigWithDefaults({
typescript: {
generateInputTypes: true,
outputFile: path.resolve(dirname, 'payload-types.ts'),
strictDraftTypes: true,
postProcess: [
({ compiledTypes }) => {
const genericType = `export type TestPluginGeneric<T> = { value: T };`
Expand Down
1 change: 0 additions & 1 deletion test/types/payload-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,6 @@ export interface Config {
menu: MenuInput;
settings: SettingInput;
};
strictDraftTypes: true;
user: FallbackUser | User;
jobs: {
tasks: unknown;
Expand Down
6 changes: 3 additions & 3 deletions test/types/types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1495,17 +1495,17 @@ describe('Types testing', () => {
})
})

describe('strictDraftTypes flag', () => {
describe('strict draft types', () => {
describe('query operations', () => {
test('draft find query returns optional required fields when flag is enabled', async () => {
test('draft find query returns optional required fields', async () => {
const result = await payload.find({
collection: 'draft-posts',
draft: true,
})

const doc = result.docs[0]!

// With strictDraftTypes enabled, user-defined required fields should be optional in draft queries
// User-defined required fields should be optional in draft queries
expect(doc.description).type.toBe<string | undefined>()
expect(doc.title).type.toBe<string | undefined>()

Expand Down
Loading