diff --git a/packages/payload/src/collections/config/types.ts b/packages/payload/src/collections/config/types.ts index 07711c56aa8..7a8931ebcaf 100644 --- a/packages/payload/src/collections/config/types.ts +++ b/packages/payload/src/collections/config/types.ts @@ -73,6 +73,18 @@ export type RequiredDataFromCollection = MarkOptional< export type RequiredDataFromCollectionSlug = RequiredDataFromCollection> +/** + * 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 = Partial< + MarkOptional +> + +export type DraftDataFromCollectionSlug = DraftDataFromCollection< + DataFromCollectionSlug +> + export type HookOperationType = | 'autosave' | 'count' diff --git a/packages/payload/src/collections/operations/local/create.ts b/packages/payload/src/collections/operations/local/create.ts index b9bed7c9fe1..a39c2a5d585 100644 --- a/packages/payload/src/collections/operations/local/create.ts +++ b/packages/payload/src/collections/operations/local/create.ts @@ -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' @@ -25,7 +26,7 @@ import { getFileByPath } from '../../../uploads/getFileByPath.js' import { createLocalReq } from '../../../utilities/createLocalReq.js' import { createOperation } from '../create.js' -export type Options = { +type BaseOptions = { /** * the Collection slug to operate against. */ @@ -37,10 +38,6 @@ export type Options = * to determine if it should run or not. */ context?: RequestContext - /** - * The data for the document to create. - */ - data: RequiredDataFromCollectionSlug /** * [Control auto-population](https://payloadcms.com/docs/queries/depth) of nested relationship and upload fields. */ @@ -55,10 +52,6 @@ export type Options = * 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 */ @@ -115,6 +108,29 @@ export type Options = user?: Document } +export type Options = + | ({ + /** + * The data for the document to create. + */ + data: RequiredDataFromCollectionSlug + /** + * Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api) + */ + draft?: false + } & BaseOptions) + | ({ + /** + * The data for the document to create. + * When creating a draft, required fields are optional as validation is skipped by default. + */ + data: DraftDataFromCollectionSlug + /** + * Create a **draft** document. [More](https://payloadcms.com/docs/versions/drafts#draft-api) + */ + draft: true + } & BaseOptions) + export async function createLocal< TSlug extends CollectionSlug, TSelect extends SelectFromCollectionSlug, diff --git a/test/versions/int.spec.ts b/test/versions/int.spec.ts index 5e2d3edeca0..34c86b67524 100644 --- a/test/versions/int.spec.ts +++ b/test/versions/int.spec.ts @@ -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