feat: consolidates create and duplicate operations (#9866)
This commit is contained in:
@@ -131,6 +131,9 @@ const post = await payload.create({
|
||||
// Alternatively, you can directly pass a File,
|
||||
// if file is provided, filePath will be omitted
|
||||
file: uploadedFile,
|
||||
|
||||
// If you want to create a document that is a duplicate of another document
|
||||
duplicateFromID: 'document-id-to-duplicate',
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
@@ -30,15 +30,13 @@ export function createResolver<TSlug extends CollectionSlug>(
|
||||
context.req.locale = args.locale
|
||||
}
|
||||
|
||||
const options = {
|
||||
const result = await createOperation({
|
||||
collection,
|
||||
data: args.data,
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
req: isolateObjectProperty(context.req, 'transactionID'),
|
||||
}
|
||||
|
||||
const result = await createOperation(options)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { Context } from '../types.js'
|
||||
export type Resolver<TData> = (
|
||||
_: unknown,
|
||||
args: {
|
||||
data: TData
|
||||
draft: boolean
|
||||
fallbackLocale?: string
|
||||
id: string
|
||||
@@ -28,15 +29,14 @@ export function duplicateResolver<TSlug extends CollectionSlug>(
|
||||
req.fallbackLocale = args.fallbackLocale || fallbackLocale
|
||||
context.req = req
|
||||
|
||||
const options = {
|
||||
const result = await duplicateOperation({
|
||||
id: args.id,
|
||||
collection,
|
||||
data: args.data,
|
||||
depth: 0,
|
||||
draft: args.draft,
|
||||
req: isolateObjectProperty(req, 'transactionID'),
|
||||
}
|
||||
|
||||
const result = await duplicateOperation(options)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -280,6 +280,9 @@ export function initCollections({ config, graphqlResult }: InitCollectionsGraphQ
|
||||
type: collection.graphQL.type,
|
||||
args: {
|
||||
id: { type: new GraphQLNonNull(idType) },
|
||||
...(createMutationInputType
|
||||
? { data: { type: collection.graphQL.mutationInputType } }
|
||||
: {}),
|
||||
},
|
||||
resolve: duplicateResolver(collection),
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export const duplicate: CollectionRouteHandlerWithID = async ({
|
||||
const doc = await duplicateOperation({
|
||||
id,
|
||||
collection,
|
||||
data: req.data,
|
||||
depth: isNumber(depth) ? Number(depth) : undefined,
|
||||
draft,
|
||||
populate: sanitizePopulateParam(req.query.populate),
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
BeforeOperationHook,
|
||||
BeforeValidateHook,
|
||||
Collection,
|
||||
DataFromCollectionSlug,
|
||||
RequiredDataFromCollectionSlug,
|
||||
SelectFromCollectionSlug,
|
||||
} from '../config/types.js'
|
||||
@@ -21,6 +22,7 @@ import { ensureUsernameOrEmail } from '../../auth/ensureUsernameOrEmail.js'
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { sendVerificationEmail } from '../../auth/sendVerificationEmail.js'
|
||||
import { registerLocalStrategy } from '../../auth/strategies/local/register.js'
|
||||
import { getDuplicateDocumentData } from '../../duplicateDocument/index.js'
|
||||
import { afterChange } from '../../fields/hooks/afterChange/index.js'
|
||||
import { afterRead } from '../../fields/hooks/afterRead/index.js'
|
||||
import { beforeChange } from '../../fields/hooks/beforeChange/index.js'
|
||||
@@ -43,6 +45,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
|
||||
disableTransaction?: boolean
|
||||
disableVerificationEmail?: boolean
|
||||
draft?: boolean
|
||||
duplicateFromID?: DataFromCollectionSlug<TSlug>['id']
|
||||
overrideAccess?: boolean
|
||||
overwriteExistingFiles?: boolean
|
||||
populate?: PopulateType
|
||||
@@ -97,6 +100,7 @@ export const createOperation = async <
|
||||
depth,
|
||||
disableVerificationEmail,
|
||||
draft = false,
|
||||
duplicateFromID,
|
||||
overrideAccess,
|
||||
overwriteExistingFiles = false,
|
||||
populate,
|
||||
@@ -115,6 +119,23 @@ export const createOperation = async <
|
||||
|
||||
const shouldSaveDraft = Boolean(draft && collectionConfig.versions.drafts)
|
||||
|
||||
let duplicatedFromDocWithLocales: JsonObject = {}
|
||||
let duplicatedFromDoc: JsonObject = {}
|
||||
|
||||
if (duplicateFromID) {
|
||||
const duplicateResult = await getDuplicateDocumentData({
|
||||
id: duplicateFromID,
|
||||
collectionConfig,
|
||||
draftArg: shouldSaveDraft,
|
||||
overrideAccess,
|
||||
req,
|
||||
shouldSaveDraft,
|
||||
})
|
||||
|
||||
duplicatedFromDoc = duplicateResult.duplicatedFromDoc
|
||||
duplicatedFromDocWithLocales = duplicateResult.duplicatedFromDocWithLocales
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Access
|
||||
// /////////////////////////////////////
|
||||
@@ -131,7 +152,9 @@ export const createOperation = async <
|
||||
collection,
|
||||
config,
|
||||
data,
|
||||
isDuplicating: Boolean(duplicateFromID),
|
||||
operation: 'create',
|
||||
originalDoc: duplicatedFromDoc,
|
||||
overwriteExistingFiles,
|
||||
req,
|
||||
throwOnMissingFile:
|
||||
@@ -148,7 +171,7 @@ export const createOperation = async <
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
doc: {},
|
||||
doc: duplicatedFromDoc,
|
||||
global: null,
|
||||
operation: 'create',
|
||||
overrideAccess,
|
||||
@@ -169,6 +192,7 @@ export const createOperation = async <
|
||||
context: req.context,
|
||||
data,
|
||||
operation: 'create',
|
||||
originalDoc: duplicatedFromDoc,
|
||||
req,
|
||||
})) || data
|
||||
},
|
||||
@@ -188,6 +212,7 @@ export const createOperation = async <
|
||||
context: req.context,
|
||||
data,
|
||||
operation: 'create',
|
||||
originalDoc: duplicatedFromDoc,
|
||||
req,
|
||||
})) || data
|
||||
}, Promise.resolve())
|
||||
@@ -200,8 +225,8 @@ export const createOperation = async <
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
doc: {},
|
||||
docWithLocales: {},
|
||||
doc: duplicatedFromDoc,
|
||||
docWithLocales: duplicatedFromDocWithLocales,
|
||||
global: null,
|
||||
operation: 'create',
|
||||
req,
|
||||
|
||||
@@ -1,391 +1,26 @@
|
||||
import type { DeepPartial } from 'ts-essentials'
|
||||
|
||||
import httpStatus from 'http-status'
|
||||
|
||||
import type { FindOneArgs } from '../../database/types.js'
|
||||
import type { CollectionSlug } from '../../index.js'
|
||||
import type {
|
||||
PayloadRequest,
|
||||
PopulateType,
|
||||
SelectType,
|
||||
TransformCollectionWithSelect,
|
||||
} from '../../types/index.js'
|
||||
import type {
|
||||
Collection,
|
||||
DataFromCollectionSlug,
|
||||
SelectFromCollectionSlug,
|
||||
} from '../config/types.js'
|
||||
import type { TransformCollectionWithSelect } from '../../types/index.js'
|
||||
import type { RequiredDataFromCollectionSlug, SelectFromCollectionSlug } from '../config/types.js'
|
||||
|
||||
import executeAccess from '../../auth/executeAccess.js'
|
||||
import { hasWhereAccessResult } from '../../auth/types.js'
|
||||
import { combineQueries } from '../../database/combineQueries.js'
|
||||
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 { beforeChange } from '../../fields/hooks/beforeChange/index.js'
|
||||
import { beforeDuplicate } from '../../fields/hooks/beforeDuplicate/index.js'
|
||||
import { beforeValidate } from '../../fields/hooks/beforeValidate/index.js'
|
||||
import { generateFileData } from '../../uploads/generateFileData.js'
|
||||
import { uploadFiles } from '../../uploads/uploadFiles.js'
|
||||
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 { getLatestCollectionVersion } from '../../versions/getLatestCollectionVersion.js'
|
||||
import { saveVersion } from '../../versions/saveVersion.js'
|
||||
import { buildAfterOperation } from './utils.js'
|
||||
import { type Arguments as CreateArguments, createOperation } from './create.js'
|
||||
|
||||
export type Arguments = {
|
||||
collection: Collection
|
||||
depth?: number
|
||||
disableTransaction?: boolean
|
||||
draft?: boolean
|
||||
export type Arguments<TSlug extends CollectionSlug> = {
|
||||
data?: DeepPartial<RequiredDataFromCollectionSlug<TSlug>>
|
||||
id: number | string
|
||||
overrideAccess?: boolean
|
||||
populate?: PopulateType
|
||||
req: PayloadRequest
|
||||
select?: SelectType
|
||||
showHiddenFields?: boolean
|
||||
}
|
||||
} & Omit<CreateArguments<TSlug>, 'data' | 'duplicateFromID'>
|
||||
|
||||
export const duplicateOperation = async <
|
||||
TSlug extends CollectionSlug,
|
||||
TSelect extends SelectFromCollectionSlug<TSlug>,
|
||||
>(
|
||||
incomingArgs: Arguments,
|
||||
incomingArgs: Arguments<TSlug>,
|
||||
): Promise<TransformCollectionWithSelect<TSlug, TSelect>> => {
|
||||
let args = incomingArgs
|
||||
const operation = 'create'
|
||||
|
||||
try {
|
||||
const shouldCommit = !args.disableTransaction && (await initTransaction(args.req))
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
await args.collection.config.hooks.beforeOperation.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
args =
|
||||
(await hook({
|
||||
args,
|
||||
collection: args.collection.config,
|
||||
context: args.req.context,
|
||||
operation,
|
||||
req: args.req,
|
||||
})) || args
|
||||
}, Promise.resolve())
|
||||
|
||||
const {
|
||||
id,
|
||||
collection: { config: collectionConfig },
|
||||
depth,
|
||||
draft: draftArg = true,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req: { fallbackLocale, locale: localeArg, payload },
|
||||
req,
|
||||
select,
|
||||
showHiddenFields,
|
||||
} = args
|
||||
|
||||
if (!id) {
|
||||
throw new APIError('Missing ID of document to duplicate.', httpStatus.BAD_REQUEST)
|
||||
}
|
||||
const shouldSaveDraft = Boolean(draftArg && collectionConfig.versions.drafts)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Read Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const accessResults = !overrideAccess
|
||||
? await executeAccess({ id, req }, collectionConfig.access.read)
|
||||
: true
|
||||
const hasWherePolicy = hasWhereAccessResult(accessResults)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
const findOneArgs: FindOneArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: combineQueries({ id: { equals: id } }, accessResults),
|
||||
}
|
||||
|
||||
let docWithLocales = await getLatestCollectionVersion({
|
||||
id,
|
||||
config: collectionConfig,
|
||||
payload,
|
||||
query: findOneArgs,
|
||||
req,
|
||||
})
|
||||
|
||||
if (!docWithLocales && !hasWherePolicy) {
|
||||
throw new NotFound(req.t)
|
||||
}
|
||||
if (!docWithLocales && hasWherePolicy) {
|
||||
throw new Forbidden(req.t)
|
||||
}
|
||||
|
||||
// remove the createdAt timestamp and id to rely on the db to set the default it
|
||||
delete docWithLocales.createdAt
|
||||
delete docWithLocales.id
|
||||
|
||||
docWithLocales = await beforeDuplicate({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
doc: docWithLocales,
|
||||
overrideAccess,
|
||||
req,
|
||||
})
|
||||
|
||||
// for version enabled collections, override the current status with draft, unless draft is explicitly set to false
|
||||
if (shouldSaveDraft) {
|
||||
docWithLocales._status = 'draft'
|
||||
}
|
||||
|
||||
let result
|
||||
|
||||
let originalDoc = await afterRead({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
depth: 0,
|
||||
doc: docWithLocales,
|
||||
draft: draftArg,
|
||||
fallbackLocale: null,
|
||||
global: null,
|
||||
locale: req.locale,
|
||||
overrideAccess: true,
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
const { data: newFileData, files: filesToUpload } = await generateFileData({
|
||||
collection: args.collection,
|
||||
config: req.payload.config,
|
||||
data: originalDoc,
|
||||
operation: 'create',
|
||||
overwriteExistingFiles: 'forceDisable',
|
||||
req,
|
||||
throwOnMissingFile: true,
|
||||
})
|
||||
|
||||
originalDoc = newFileData
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Create Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!overrideAccess) {
|
||||
await executeAccess({ data: originalDoc, req }, collectionConfig.access.create)
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeValidate - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
let data = await beforeValidate<DeepPartial<DataFromCollectionSlug<TSlug>>>({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data: originalDoc,
|
||||
doc: originalDoc,
|
||||
duplicate: true,
|
||||
global: null,
|
||||
operation,
|
||||
overrideAccess,
|
||||
req,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeValidate - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
data =
|
||||
(await hook({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
operation,
|
||||
originalDoc,
|
||||
req,
|
||||
})) || result
|
||||
}, Promise.resolve())
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeChange - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
data =
|
||||
(await hook({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
operation,
|
||||
originalDoc: result,
|
||||
req,
|
||||
})) || result
|
||||
}, Promise.resolve())
|
||||
|
||||
// /////////////////////////////////////
|
||||
// beforeChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await beforeChange({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data,
|
||||
doc: originalDoc,
|
||||
docWithLocales,
|
||||
global: null,
|
||||
operation,
|
||||
req,
|
||||
skipValidation:
|
||||
shouldSaveDraft &&
|
||||
collectionConfig.versions.drafts &&
|
||||
!collectionConfig.versions.drafts.validate,
|
||||
})
|
||||
|
||||
// set req.locale back to the original locale
|
||||
req.locale = localeArg
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Create / Update
|
||||
// /////////////////////////////////////
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Write files to local storage
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (!collectionConfig.upload.disableLocalStorage) {
|
||||
await uploadFiles(payload, filesToUpload, req)
|
||||
}
|
||||
|
||||
let versionDoc = await payload.db.create({
|
||||
collection: collectionConfig.slug,
|
||||
data: result,
|
||||
req,
|
||||
select,
|
||||
})
|
||||
|
||||
versionDoc = sanitizeInternalFields(versionDoc)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Create version
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (collectionConfig.versions) {
|
||||
result = await saveVersion({
|
||||
id: versionDoc.id,
|
||||
collection: collectionConfig,
|
||||
docWithLocales: versionDoc,
|
||||
draft: shouldSaveDraft,
|
||||
payload,
|
||||
req,
|
||||
})
|
||||
}
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await afterRead({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
depth,
|
||||
doc: versionDoc,
|
||||
draft: draftArg,
|
||||
fallbackLocale,
|
||||
global: null,
|
||||
locale: localeArg,
|
||||
overrideAccess,
|
||||
populate,
|
||||
req,
|
||||
select,
|
||||
showHiddenFields,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterRead - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
await collectionConfig.hooks.afterRead.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
result =
|
||||
(await hook({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
doc: result,
|
||||
req,
|
||||
})) || result
|
||||
}, Promise.resolve())
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterChange - Fields
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await afterChange({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
data: versionDoc,
|
||||
doc: result,
|
||||
global: null,
|
||||
operation,
|
||||
previousDoc: {},
|
||||
req,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterChange - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
|
||||
await priorHook
|
||||
|
||||
result =
|
||||
(await hook({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
doc: result,
|
||||
operation,
|
||||
previousDoc: {},
|
||||
req,
|
||||
})) || result
|
||||
}, Promise.resolve())
|
||||
|
||||
// /////////////////////////////////////
|
||||
// afterOperation - Collection
|
||||
// /////////////////////////////////////
|
||||
|
||||
result = await buildAfterOperation({
|
||||
args,
|
||||
collection: collectionConfig,
|
||||
operation,
|
||||
result,
|
||||
})
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Return results
|
||||
// /////////////////////////////////////
|
||||
|
||||
if (shouldCommit) {
|
||||
await commitTransaction(req)
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error: unknown) {
|
||||
await killTransaction(args.req)
|
||||
throw error
|
||||
}
|
||||
const { id, ...args } = incomingArgs
|
||||
return createOperation({
|
||||
...args,
|
||||
data: incomingArgs?.data || {},
|
||||
duplicateFromID: id,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '../../../types/index.js'
|
||||
import type { File } from '../../../uploads/types.js'
|
||||
import type {
|
||||
DataFromCollectionSlug,
|
||||
RequiredDataFromCollectionSlug,
|
||||
SelectFromCollectionSlug,
|
||||
} from '../../config/types.js'
|
||||
@@ -28,6 +29,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
|
||||
disableTransaction?: boolean
|
||||
disableVerificationEmail?: boolean
|
||||
draft?: boolean
|
||||
duplicateFromID?: DataFromCollectionSlug<TSlug>['id']
|
||||
fallbackLocale?: false | TypedLocale
|
||||
file?: File
|
||||
filePath?: string
|
||||
@@ -56,6 +58,7 @@ export default async function createLocal<
|
||||
disableTransaction,
|
||||
disableVerificationEmail,
|
||||
draft,
|
||||
duplicateFromID,
|
||||
file,
|
||||
filePath,
|
||||
overrideAccess = true,
|
||||
@@ -82,6 +85,7 @@ export default async function createLocal<
|
||||
disableTransaction,
|
||||
disableVerificationEmail,
|
||||
draft,
|
||||
duplicateFromID,
|
||||
overrideAccess,
|
||||
overwriteExistingFiles,
|
||||
populate,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { DeepPartial } from 'ts-essentials'
|
||||
|
||||
import type { CollectionSlug, TypedLocale } from '../../..//index.js'
|
||||
import type { Payload, RequestContext } from '../../../index.js'
|
||||
import type {
|
||||
@@ -7,7 +9,10 @@ import type {
|
||||
SelectType,
|
||||
TransformCollectionWithSelect,
|
||||
} from '../../../types/index.js'
|
||||
import type { SelectFromCollectionSlug } from '../../config/types.js'
|
||||
import type {
|
||||
RequiredDataFromCollectionSlug,
|
||||
SelectFromCollectionSlug,
|
||||
} from '../../config/types.js'
|
||||
|
||||
import { APIError } from '../../../errors/index.js'
|
||||
import { createLocalReq } from '../../../utilities/createLocalReq.js'
|
||||
@@ -19,6 +24,7 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
|
||||
* context, which will then be passed to req.context, which can be read by hooks
|
||||
*/
|
||||
context?: RequestContext
|
||||
data?: DeepPartial<RequiredDataFromCollectionSlug<TSlug>>
|
||||
depth?: number
|
||||
disableTransaction?: boolean
|
||||
draft?: boolean
|
||||
@@ -43,6 +49,7 @@ export async function duplicate<
|
||||
const {
|
||||
id,
|
||||
collection: collectionSlug,
|
||||
data,
|
||||
depth,
|
||||
disableTransaction,
|
||||
draft,
|
||||
@@ -71,6 +78,7 @@ export async function duplicate<
|
||||
return duplicateOperation<TSlug, TSelect>({
|
||||
id,
|
||||
collection,
|
||||
data,
|
||||
depth,
|
||||
disableTransaction,
|
||||
draft,
|
||||
|
||||
106
packages/payload/src/duplicateDocument/index.ts
Normal file
106
packages/payload/src/duplicateDocument/index.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { SanitizedCollectionConfig } from '../collections/config/types.js'
|
||||
import type { FindOneArgs } from '../database/types.js'
|
||||
import type { JsonObject, PayloadRequest } from '../types/index.js'
|
||||
|
||||
import executeAccess from '../auth/executeAccess.js'
|
||||
import { hasWhereAccessResult } from '../auth/types.js'
|
||||
import { combineQueries } from '../database/combineQueries.js'
|
||||
import { Forbidden } from '../errors/Forbidden.js'
|
||||
import { NotFound } from '../errors/NotFound.js'
|
||||
import { afterRead } from '../fields/hooks/afterRead/index.js'
|
||||
import { beforeDuplicate } from '../fields/hooks/beforeDuplicate/index.js'
|
||||
import { getLatestCollectionVersion } from '../versions/getLatestCollectionVersion.js'
|
||||
|
||||
type GetDuplicateDocumentArgs = {
|
||||
collectionConfig: SanitizedCollectionConfig
|
||||
draftArg?: boolean
|
||||
id: number | string
|
||||
overrideAccess?: boolean
|
||||
req: PayloadRequest
|
||||
shouldSaveDraft?: boolean
|
||||
}
|
||||
export const getDuplicateDocumentData = async ({
|
||||
id,
|
||||
collectionConfig,
|
||||
draftArg,
|
||||
overrideAccess,
|
||||
req,
|
||||
shouldSaveDraft,
|
||||
}: GetDuplicateDocumentArgs): Promise<{
|
||||
duplicatedFromDoc: JsonObject
|
||||
duplicatedFromDocWithLocales: JsonObject
|
||||
}> => {
|
||||
const { payload } = req
|
||||
// /////////////////////////////////////
|
||||
// Read Access
|
||||
// /////////////////////////////////////
|
||||
|
||||
const accessResults = !overrideAccess
|
||||
? await executeAccess({ id, req }, collectionConfig.access.read)
|
||||
: true
|
||||
const hasWherePolicy = hasWhereAccessResult(accessResults)
|
||||
|
||||
// /////////////////////////////////////
|
||||
// Retrieve document
|
||||
// /////////////////////////////////////
|
||||
const findOneArgs: FindOneArgs = {
|
||||
collection: collectionConfig.slug,
|
||||
locale: req.locale,
|
||||
req,
|
||||
where: combineQueries({ id: { equals: id } }, accessResults),
|
||||
}
|
||||
|
||||
let duplicatedFromDocWithLocales = await getLatestCollectionVersion({
|
||||
id,
|
||||
config: collectionConfig,
|
||||
payload,
|
||||
query: findOneArgs,
|
||||
req,
|
||||
})
|
||||
|
||||
if (!duplicatedFromDocWithLocales && !hasWherePolicy) {
|
||||
throw new NotFound(req.t)
|
||||
}
|
||||
if (!duplicatedFromDocWithLocales && hasWherePolicy) {
|
||||
throw new Forbidden(req.t)
|
||||
}
|
||||
|
||||
// remove the createdAt timestamp and rely on the db to set it
|
||||
if ('createdAt' in duplicatedFromDocWithLocales) {
|
||||
delete duplicatedFromDocWithLocales.createdAt
|
||||
}
|
||||
// remove the id and rely on the db to set it
|
||||
if ('id' in duplicatedFromDocWithLocales) {
|
||||
delete duplicatedFromDocWithLocales.id
|
||||
}
|
||||
|
||||
duplicatedFromDocWithLocales = await beforeDuplicate({
|
||||
id,
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
doc: duplicatedFromDocWithLocales,
|
||||
overrideAccess,
|
||||
req,
|
||||
})
|
||||
|
||||
// for version enabled collections, override the current status with draft, unless draft is explicitly set to false
|
||||
if (shouldSaveDraft) {
|
||||
duplicatedFromDocWithLocales._status = 'draft'
|
||||
}
|
||||
|
||||
const duplicatedFromDoc = await afterRead({
|
||||
collection: collectionConfig,
|
||||
context: req.context,
|
||||
depth: 0,
|
||||
doc: duplicatedFromDocWithLocales,
|
||||
draft: draftArg,
|
||||
fallbackLocale: null,
|
||||
global: null,
|
||||
locale: req.locale,
|
||||
overrideAccess: true,
|
||||
req,
|
||||
showHiddenFields: true,
|
||||
})
|
||||
|
||||
return { duplicatedFromDoc, duplicatedFromDocWithLocales }
|
||||
}
|
||||
@@ -286,7 +286,6 @@ export const promise = async ({
|
||||
}
|
||||
|
||||
case 'collapsible':
|
||||
|
||||
case 'row': {
|
||||
await traverseFields({
|
||||
id,
|
||||
|
||||
@@ -25,10 +25,10 @@ type Args<T> = {
|
||||
collection: Collection
|
||||
config: SanitizedConfig
|
||||
data: T
|
||||
isDuplicating?: boolean
|
||||
operation: 'create' | 'update'
|
||||
originalDoc?: T
|
||||
/** pass forceDisable to not overwrite existing files even if they already exist in `data` */
|
||||
overwriteExistingFiles?: 'forceDisable' | boolean
|
||||
overwriteExistingFiles?: boolean
|
||||
req: PayloadRequest
|
||||
throwOnMissingFile?: boolean
|
||||
}
|
||||
@@ -41,6 +41,7 @@ type Result<T> = Promise<{
|
||||
export const generateFileData = async <T>({
|
||||
collection: { config: collectionConfig },
|
||||
data,
|
||||
isDuplicating,
|
||||
operation,
|
||||
originalDoc,
|
||||
overwriteExistingFiles,
|
||||
@@ -60,6 +61,7 @@ export const generateFileData = async <T>({
|
||||
|
||||
const uploadEdits = parseUploadEditsFromReqOrIncomingData({
|
||||
data,
|
||||
isDuplicating,
|
||||
operation,
|
||||
originalDoc,
|
||||
req,
|
||||
@@ -78,33 +80,31 @@ export const generateFileData = async <T>({
|
||||
|
||||
const staticPath = staticDir
|
||||
|
||||
if (!file && uploadEdits && data) {
|
||||
const { filename, url } = data as FileData
|
||||
const incomingFileData = isDuplicating ? originalDoc : data
|
||||
|
||||
if (!file && uploadEdits && incomingFileData) {
|
||||
const { filename, url } = incomingFileData as FileData
|
||||
|
||||
try {
|
||||
if (url && url.startsWith('/') && !disableLocalStorage) {
|
||||
const filePath = `${staticPath}/${filename}`
|
||||
const response = await getFileByPath(filePath)
|
||||
file = response
|
||||
if (overwriteExistingFiles !== 'forceDisable') {
|
||||
overwriteExistingFiles = true
|
||||
}
|
||||
overwriteExistingFiles = true
|
||||
} else if (filename && url) {
|
||||
file = await getExternalFile({
|
||||
data: data as FileData,
|
||||
data: incomingFileData as FileData,
|
||||
req,
|
||||
uploadConfig: collectionConfig.upload,
|
||||
})
|
||||
if (overwriteExistingFiles !== 'forceDisable') {
|
||||
overwriteExistingFiles = true
|
||||
}
|
||||
overwriteExistingFiles = true
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
throw new FileRetrievalError(req.t, err instanceof Error ? err.message : undefined)
|
||||
}
|
||||
}
|
||||
|
||||
if (overwriteExistingFiles === 'forceDisable') {
|
||||
if (isDuplicating) {
|
||||
overwriteExistingFiles = false
|
||||
}
|
||||
|
||||
@@ -362,11 +362,12 @@ export const generateFileData = async <T>({
|
||||
*/
|
||||
function parseUploadEditsFromReqOrIncomingData(args: {
|
||||
data: unknown
|
||||
isDuplicating?: boolean
|
||||
operation: 'create' | 'update'
|
||||
originalDoc: unknown
|
||||
req: PayloadRequest
|
||||
}): UploadEdits {
|
||||
const { data, operation, originalDoc, req } = args
|
||||
const { data, isDuplicating, operation, originalDoc, req } = args
|
||||
|
||||
// Get intended focal point change from query string or incoming data
|
||||
const uploadEdits =
|
||||
@@ -381,10 +382,19 @@ function parseUploadEditsFromReqOrIncomingData(args: {
|
||||
const incomingData = data as FileData
|
||||
const origDoc = originalDoc as FileData
|
||||
|
||||
// If no change in focal point, return undefined.
|
||||
// This prevents a refocal operation triggered from admin, because it always sends the focal point.
|
||||
if (origDoc && incomingData.focalX === origDoc.focalX && incomingData.focalY === origDoc.focalY) {
|
||||
return undefined
|
||||
if (origDoc && 'focalX' in origDoc && 'focalY' in origDoc) {
|
||||
// If no change in focal point, return undefined.
|
||||
// This prevents a refocal operation triggered from admin, because it always sends the focal point.
|
||||
if (incomingData.focalX === origDoc.focalX && incomingData.focalY === origDoc.focalY) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (isDuplicating) {
|
||||
uploadEdits.focalPoint = {
|
||||
x: incomingData?.focalX || origDoc.focalX,
|
||||
y: incomingData?.focalY || origDoc.focalX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (incomingData?.focalX && incomingData?.focalY) {
|
||||
@@ -402,5 +412,6 @@ function parseUploadEditsFromReqOrIncomingData(args: {
|
||||
y: 50,
|
||||
}
|
||||
}
|
||||
|
||||
return uploadEdits
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user