Skip to content
Merged
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
12 changes: 12 additions & 0 deletions packages/payload/src/collections/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ export type RequiredDataFromCollection<TData extends JsonObject> = MarkOptional<
export type RequiredDataFromCollectionSlug<TSlug extends CollectionSlug> =
RequiredDataFromCollection<DataFromCollectionSlug<TSlug>>

/**
* Helper type for draft data - makes all fields optional except auto-generated ones
* When creating a draft, required fields don't need to be provided as validation is skipped
*/
export type DraftDataFromCollection<TData extends JsonObject> = Partial<
MarkOptional<TData, 'createdAt' | 'deletedAt' | 'id' | 'sizes' | 'updatedAt'>
>

export type DraftDataFromCollectionSlug<TSlug extends CollectionSlug> = DraftDataFromCollection<
DataFromCollectionSlug<TSlug>
>

export type HookOperationType =
| 'autosave'
| 'count'
Expand Down
34 changes: 25 additions & 9 deletions packages/payload/src/collections/operations/local/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { File } from '../../../uploads/types.js'
import type { CreateLocalReqOptions } from '../../../utilities/createLocalReq.js'
import type {
DataFromCollectionSlug,
DraftDataFromCollectionSlug,
RequiredDataFromCollectionSlug,
SelectFromCollectionSlug,
} from '../../config/types.js'
Expand All @@ -25,7 +26,7 @@ import { getFileByPath } from '../../../uploads/getFileByPath.js'
import { createLocalReq } from '../../../utilities/createLocalReq.js'
import { createOperation } from '../create.js'

export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> = {
type BaseOptions<TSlug extends CollectionSlug, TSelect extends SelectType> = {
/**
* the Collection slug to operate against.
*/
Expand All @@ -37,10 +38,6 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
* to determine if it should run or not.
*/
context?: RequestContext
/**
* The data for the document to create.
*/
data: RequiredDataFromCollectionSlug<TSlug>
/**
* [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields.
*/
Expand All @@ -55,10 +52,6 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
* you can disable the email that is auto-sent
*/
disableVerificationEmail?: boolean
/**
* Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api)
*/
draft?: boolean
/**
* If you want to create a document that is a duplicate of another document
*/
Expand Down Expand Up @@ -115,6 +108,29 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
user?: Document
}

export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
| ({
/**
* 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>)

export async function createLocal<
TSlug extends CollectionSlug,
TSelect extends SelectFromCollectionSlug<TSlug>,
Expand Down
63 changes: 63 additions & 0 deletions test/versions/int.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,69 @@ describe('Versions', () => {
})
})

describe('Draft Types', () => {
it('should allow creating drafts without required fields', async () => {
// This test validates that when draft: true is set, required fields become optional
// TypeScript should not complain about missing 'description' field even though it's required
const draft = await payload.create({
collection: 'draft-posts',
data: {
title: 'Draft without description',
// description is required but omitted - should work with draft: true
},
draft: true,
})

expect(draft.title).toBe('Draft without description')
// Different databases return null vs undefined for missing fields
expect(draft.description).toBeFalsy()
expect(draft._status).toBe('draft')
})

it('should require all required fields when draft is false', async () => {
// This validates that required fields are still enforced when draft is false
await expect(
// @ts-expect-error - description is required when not creating a draft
payload.create({
collection: 'draft-posts',
data: {
title: 'Published without description',
},
draft: false,
}),
).rejects.toThrow(ValidationError)
})

it('should require all required fields when draft is not specified', async () => {
// This validates that required fields are still enforced when draft option is omitted
await expect(
// @ts-expect-error - description is required when draft option is not specified
payload.create({
collection: 'draft-posts',
data: {
title: 'Post without description',
},
}),
).rejects.toThrow(ValidationError)
})

it('should allow all fields to be optional with draft: true', async () => {
// Test that even fields nested in groups can be omitted
const draft = await payload.create({
collection: 'draft-posts',
data: {
// Both title and description are required but omitted
},
draft: true,
})

expect(draft._status).toBe('draft')
// Different databases return null vs undefined for missing fields
expect(draft.title).toBeFalsy()
expect(draft.description).toBeFalsy()
})
})

describe('Max Versions', () => {
// create 2 documents with 3 versions each
// expect 2 documents with 2 versions each
Expand Down