diff --git a/packages/common/src/generated-types.ts b/packages/common/src/generated-types.ts index a157e9ece5..b77df75da4 100644 --- a/packages/common/src/generated-types.ts +++ b/packages/common/src/generated-types.ts @@ -1550,6 +1550,26 @@ export type Discount = { type: AdjustmentType; }; +export type DuplicateEntityError = ErrorResult & { + __typename?: 'DuplicateEntityError'; + duplicationError: Scalars['String']['output']; + errorCode: ErrorCode; + message: Scalars['String']['output']; +}; + +export type DuplicateEntityInput = { + duplicatorInput: ConfigurableOperationInput; + entityId: Scalars['ID']['input']; + entityName: Scalars['String']['input']; +}; + +export type DuplicateEntityResult = DuplicateEntityError | DuplicateEntitySuccess; + +export type DuplicateEntitySuccess = { + __typename?: 'DuplicateEntitySuccess'; + newEntityId: Scalars['ID']['output']; +}; + /** Returned when attempting to create a Customer with an email address already registered to an existing User. */ export type EmailAddressConflictError = ErrorResult & { __typename?: 'EmailAddressConflictError'; @@ -1570,6 +1590,15 @@ export type EntityCustomFields = { entityName: Scalars['String']['output']; }; +export type EntityDuplicatorDefinition = { + __typename?: 'EntityDuplicatorDefinition'; + args: Array; + code: Scalars['String']['output']; + description: Scalars['String']['output']; + forEntities: Array; + requiresPermission: Array; +}; + export enum ErrorCode { ALREADY_REFUNDED_ERROR = 'ALREADY_REFUNDED_ERROR', CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR', @@ -1579,6 +1608,7 @@ export enum ErrorCode { COUPON_CODE_INVALID_ERROR = 'COUPON_CODE_INVALID_ERROR', COUPON_CODE_LIMIT_ERROR = 'COUPON_CODE_LIMIT_ERROR', CREATE_FULFILLMENT_ERROR = 'CREATE_FULFILLMENT_ERROR', + DUPLICATE_ENTITY_ERROR = 'DUPLICATE_ENTITY_ERROR', EMAIL_ADDRESS_CONFLICT_ERROR = 'EMAIL_ADDRESS_CONFLICT_ERROR', EMPTY_ORDER_LINE_SELECTION_ERROR = 'EMPTY_ORDER_LINE_SELECTION_ERROR', FACET_IN_USE_ERROR = 'FACET_IN_USE_ERROR', @@ -2803,6 +2833,7 @@ export type Mutation = { deleteZone: DeletionResponse; /** Delete a Zone */ deleteZones: Array; + duplicateEntity: DuplicateEntityResult; flushBufferedJobs: Success; importProducts?: Maybe; /** Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})` */ @@ -3416,6 +3447,11 @@ export type MutationDeleteZonesArgs = { }; +export type MutationDuplicateEntityArgs = { + input: DuplicateEntityInput; +}; + + export type MutationFlushBufferedJobsArgs = { bufferIds?: InputMaybe>; }; @@ -4955,6 +4991,7 @@ export type Query = { customers: CustomerList; /** Returns a list of eligible shipping methods for the draft Order */ eligibleShippingMethodsForDraftOrder: Array; + entityDuplicators: Array; facet?: Maybe; facetValues: FacetValueList; facets: FacetList; diff --git a/packages/core/e2e/duplicate-entity.e2e-spec.ts b/packages/core/e2e/duplicate-entity.e2e-spec.ts new file mode 100644 index 0000000000..a662d7e597 --- /dev/null +++ b/packages/core/e2e/duplicate-entity.e2e-spec.ts @@ -0,0 +1,324 @@ +import { + Collection, + CollectionService, + defaultEntityDuplicators, + EntityDuplicator, + LanguageCode, + mergeConfig, + PermissionDefinition, + TransactionalConnection, +} from '@vendure/core'; +import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing'; +import gql from 'graphql-tag'; +import path from 'path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +import { initialData } from '../../../e2e-common/e2e-initial-data'; +import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config'; + +import * as Codegen from './graphql/generated-e2e-admin-types'; +import { + AdministratorFragment, + CreateAdministratorMutation, + CreateAdministratorMutationVariables, + CreateRoleMutation, + CreateRoleMutationVariables, + Permission, + RoleFragment, +} from './graphql/generated-e2e-admin-types'; +import { CREATE_ADMINISTRATOR, CREATE_ROLE, GET_COLLECTIONS } from './graphql/shared-definitions'; + +const customPermission = new PermissionDefinition({ + name: 'custom', +}); + +let collectionService: CollectionService; +let connection: TransactionalConnection; + +const customCollectionDuplicator = new EntityDuplicator({ + code: 'custom-collection-duplicator', + description: [{ languageCode: LanguageCode.en, value: 'Custom Collection Duplicator' }], + args: { + throwError: { + type: 'boolean', + defaultValue: false, + }, + }, + forEntities: ['Collection'], + requiresPermission: customPermission.Permission, + init(injector) { + collectionService = injector.get(CollectionService); + connection = injector.get(TransactionalConnection); + }, + duplicate: async input => { + const { ctx, id, args } = input; + + const original = await connection.getEntityOrThrow(ctx, Collection, id, { + relations: { + assets: true, + featuredAsset: true, + }, + }); + const newCollection = await collectionService.create(ctx, { + isPrivate: original.isPrivate, + customFields: original.customFields, + assetIds: original.assets.map(a => a.id), + featuredAssetId: original.featuredAsset?.id, + parentId: original.parentId, + filters: original.filters.map(f => ({ + code: f.code, + arguments: f.args, + })), + inheritFilters: original.inheritFilters, + translations: original.translations.map(t => ({ + languageCode: t.languageCode, + name: `${t.name} (copy)`, + slug: `${t.slug}-copy`, + description: t.description, + customFields: t.customFields, + })), + }); + + if (args.throwError) { + throw new Error('Dummy error'); + } + + return newCollection; + }, +}); + +describe('Duplicating entities', () => { + const { server, adminClient } = createTestEnvironment( + mergeConfig(testConfig(), { + authOptions: { + customPermissions: [customPermission], + }, + entityOptions: { + entityDuplicators: [/* ...defaultEntityDuplicators */ customCollectionDuplicator], + }, + }), + ); + + const duplicateEntityGuard: ErrorResultGuard<{ newEntityId: string }> = createErrorResultGuard( + result => !!result.newEntityId, + ); + + let testRole: RoleFragment; + let testAdmin: AdministratorFragment; + + beforeAll(async () => { + await server.init({ + initialData, + productsCsvPath: path.join(__dirname, 'fixtures/e2e-products-minimal.csv'), + customerCount: 1, + }); + await adminClient.asSuperAdmin(); + + // create a new role and Admin and sign in as that Admin + const { createRole } = await adminClient.query( + CREATE_ROLE, + { + input: { + channelIds: ['T_1'], + code: 'test-role', + description: 'Testing custom permissions', + permissions: [ + Permission.CreateCollection, + Permission.UpdateCollection, + Permission.ReadCollection, + ], + }, + }, + ); + testRole = createRole; + const { createAdministrator } = await adminClient.query< + CreateAdministratorMutation, + CreateAdministratorMutationVariables + >(CREATE_ADMINISTRATOR, { + input: { + firstName: 'Test', + lastName: 'Admin', + emailAddress: 'test@admin.com', + password: 'test', + roleIds: [testRole.id], + }, + }); + + testAdmin = createAdministrator; + }, TEST_SETUP_TIMEOUT_MS); + + afterAll(async () => { + await server.destroy(); + }); + + it('get entity duplicators', async () => { + const { entityDuplicators } = await adminClient.query( + GET_ENTITY_DUPLICATORS, + ); + + expect(entityDuplicators).toEqual([ + { + args: [ + { + defaultValue: false, + name: 'throwError', + type: 'boolean', + }, + ], + code: 'custom-collection-duplicator', + description: 'Custom Collection Duplicator', + forEntities: ['Collection'], + requiresPermission: ['custom'], + }, + ]); + }); + + it('cannot duplicate if lacking permissions', async () => { + await adminClient.asUserWithCredentials(testAdmin.emailAddress, 'test'); + + const { duplicateEntity } = await adminClient.query< + Codegen.DuplicateEntityMutation, + Codegen.DuplicateEntityMutationVariables + >(DUPLICATE_ENTITY, { + input: { + entityName: 'Collection', + entityId: 'T_2', + duplicatorInput: { + code: 'custom-collection-duplicator', + arguments: [ + { + name: 'throwError', + value: 'false', + }, + ], + }, + }, + }); + + duplicateEntityGuard.assertErrorResult(duplicateEntity); + + expect(duplicateEntity.message).toBe('The entity could not be duplicated'); + expect(duplicateEntity.duplicationError).toBe( + 'You do not have the required permissions to duplicate this entity', + ); + }); + + it('errors thrown in duplicator cause ErrorResult', async () => { + await adminClient.asSuperAdmin(); + + const { duplicateEntity } = await adminClient.query< + Codegen.DuplicateEntityMutation, + Codegen.DuplicateEntityMutationVariables + >(DUPLICATE_ENTITY, { + input: { + entityName: 'Collection', + entityId: 'T_2', + duplicatorInput: { + code: 'custom-collection-duplicator', + arguments: [ + { + name: 'throwError', + value: 'true', + }, + ], + }, + }, + }); + + duplicateEntityGuard.assertErrorResult(duplicateEntity); + + expect(duplicateEntity.message).toBe('The entity could not be duplicated'); + expect(duplicateEntity.duplicationError).toBe('Dummy error'); + }); + + it('errors thrown cause all DB changes to be rolled back', async () => { + await adminClient.asSuperAdmin(); + + const { collections } = await adminClient.query(GET_COLLECTIONS); + + expect(collections.items.length).toBe(1); + expect(collections.items.map(i => i.name)).toEqual(['Plants']); + }); + + it('returns ID of new entity', async () => { + await adminClient.asSuperAdmin(); + + const { duplicateEntity } = await adminClient.query< + Codegen.DuplicateEntityMutation, + Codegen.DuplicateEntityMutationVariables + >(DUPLICATE_ENTITY, { + input: { + entityName: 'Collection', + entityId: 'T_2', + duplicatorInput: { + code: 'custom-collection-duplicator', + arguments: [ + { + name: 'throwError', + value: 'false', + }, + ], + }, + }, + }); + + duplicateEntityGuard.assertSuccess(duplicateEntity); + + expect(duplicateEntity.newEntityId).toBe('T_3'); + }); + + it('duplicate gets created', async () => { + const { collection } = await adminClient.query< + Codegen.GetDuplicatedCollectionQuery, + Codegen.GetDuplicatedCollectionQueryVariables + >(GET_DUPLICATED_COLLECTION, { + id: 'T_3', + }); + + expect(collection).toEqual({ + id: 'T_3', + name: 'Plants (copy)', + slug: 'plants-copy', + }); + }); +}); + +const GET_ENTITY_DUPLICATORS = gql` + query GetEntityDuplicators { + entityDuplicators { + code + description + requiresPermission + forEntities + args { + name + type + defaultValue + } + } + } +`; + +const DUPLICATE_ENTITY = gql` + mutation DuplicateEntity($input: DuplicateEntityInput!) { + duplicateEntity(input: $input) { + ... on DuplicateEntitySuccess { + newEntityId + } + ... on DuplicateEntityError { + message + duplicationError + } + } + } +`; + +export const GET_DUPLICATED_COLLECTION = gql` + query GetDuplicatedCollection($id: ID) { + collection(id: $id) { + id + name + slug + } + } +`; diff --git a/packages/core/e2e/graphql/generated-e2e-admin-types.ts b/packages/core/e2e/graphql/generated-e2e-admin-types.ts index 1d8f9a0934..70afd2fda3 100644 --- a/packages/core/e2e/graphql/generated-e2e-admin-types.ts +++ b/packages/core/e2e/graphql/generated-e2e-admin-types.ts @@ -6808,6 +6808,25 @@ export type GetOrderPlacedAtQueryVariables = Exact<{ export type GetOrderPlacedAtQuery = { order?: { id: string, createdAt: any, updatedAt: any, state: string, orderPlacedAt?: any | null } | null }; +export type GetEntityDuplicatorsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetEntityDuplicatorsQuery = { entityDuplicators: Array<{ code: string, description: string, requiresPermission: Array, forEntities: Array, args: Array<{ name: string, type: string, defaultValue?: any | null }> }> }; + +export type DuplicateEntityMutationVariables = Exact<{ + input: DuplicateEntityInput; +}>; + + +export type DuplicateEntityMutation = { duplicateEntity: { message: string, duplicationError: string } | { newEntityId: string } }; + +export type GetDuplicatedCollectionQueryVariables = Exact<{ + id?: InputMaybe; +}>; + + +export type GetDuplicatedCollectionQuery = { collection?: { id: string, name: string, slug: string } | null }; + export type IdTest1QueryVariables = Exact<{ [key: string]: never; }>; @@ -8412,6 +8431,9 @@ export const RemoveCouponCodeFromDraftOrderDocument = {"kind":"Document","defini export const DraftOrderEligibleShippingMethodsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"DraftOrderEligibleShippingMethods"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eligibleShippingMethodsForDraftOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orderId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"priceWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}}]}}]}}]} as unknown as DocumentNode; export const SetDraftOrderShippingMethodDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetDraftOrderShippingMethod"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"shippingMethodId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setDraftOrderShippingMethod"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orderId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orderId"}}},{"kind":"Argument","name":{"kind":"Name","value":"shippingMethodId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"shippingMethodId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"OrderWithLines"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ErrorResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"errorCode"}},{"kind":"Field","name":{"kind":"Name","value":"message"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ShippingAddress"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"OrderAddress"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"company"}},{"kind":"Field","name":{"kind":"Name","value":"streetLine1"}},{"kind":"Field","name":{"kind":"Name","value":"streetLine2"}},{"kind":"Field","name":{"kind":"Name","value":"city"}},{"kind":"Field","name":{"kind":"Name","value":"province"}},{"kind":"Field","name":{"kind":"Name","value":"postalCode"}},{"kind":"Field","name":{"kind":"Name","value":"country"}},{"kind":"Field","name":{"kind":"Name","value":"phoneNumber"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"Payment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Payment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"transactionId"}},{"kind":"Field","name":{"kind":"Name","value":"amount"}},{"kind":"Field","name":{"kind":"Name","value":"method"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"nextStates"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}},{"kind":"Field","name":{"kind":"Name","value":"refunds"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"reason"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"OrderWithLines"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Order"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"active"}},{"kind":"Field","name":{"kind":"Name","value":"customer"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"firstName"}},{"kind":"Field","name":{"kind":"Name","value":"lastName"}}]}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"featuredAsset"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"preview"}}]}},{"kind":"Field","name":{"kind":"Name","value":"productVariant"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}}]}},{"kind":"Field","name":{"kind":"Name","value":"taxLines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"taxRate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"unitPrice"}},{"kind":"Field","name":{"kind":"Name","value":"unitPriceWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}},{"kind":"Field","name":{"kind":"Name","value":"unitPrice"}},{"kind":"Field","name":{"kind":"Name","value":"unitPriceWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"taxRate"}},{"kind":"Field","name":{"kind":"Name","value":"linePriceWithTax"}}]}},{"kind":"Field","name":{"kind":"Name","value":"surcharges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"sku"}},{"kind":"Field","name":{"kind":"Name","value":"price"}},{"kind":"Field","name":{"kind":"Name","value":"priceWithTax"}}]}},{"kind":"Field","name":{"kind":"Name","value":"subTotal"}},{"kind":"Field","name":{"kind":"Name","value":"subTotalWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"total"}},{"kind":"Field","name":{"kind":"Name","value":"totalWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"totalQuantity"}},{"kind":"Field","name":{"kind":"Name","value":"currencyCode"}},{"kind":"Field","name":{"kind":"Name","value":"shipping"}},{"kind":"Field","name":{"kind":"Name","value":"shippingWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"shippingLines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"priceWithTax"}},{"kind":"Field","name":{"kind":"Name","value":"shippingMethod"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"shippingAddress"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ShippingAddress"}}]}},{"kind":"Field","name":{"kind":"Name","value":"payments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"Payment"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fulfillments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"method"}},{"kind":"Field","name":{"kind":"Name","value":"trackingCode"}},{"kind":"Field","name":{"kind":"Name","value":"lines"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orderLineId"}},{"kind":"Field","name":{"kind":"Name","value":"quantity"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}}]} as unknown as DocumentNode; export const GetOrderPlacedAtDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrderPlacedAt"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"order"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"orderPlacedAt"}}]}}]}}]} as unknown as DocumentNode; +export const GetEntityDuplicatorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEntityDuplicators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entityDuplicators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"requiresPermission"}},{"kind":"Field","name":{"kind":"Name","value":"forEntities"}},{"kind":"Field","name":{"kind":"Name","value":"args"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DuplicateEntityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DuplicateEntity"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DuplicateEntityInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duplicateEntity"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DuplicateEntitySuccess"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"newEntityId"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"DuplicateEntityError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"duplicationError"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetDuplicatedCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDuplicatedCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"collection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}}]} as unknown as DocumentNode; export const IdTest1Document = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IdTest1"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"options"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"take"},"value":{"kind":"IntValue","value":"5"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const IdTest2Document = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IdTest2"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"products"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"options"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"take"},"value":{"kind":"IntValue","value":"1"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"variants"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"options"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const IdTest3Document = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"IdTest3"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"product"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"StringValue","value":"T_1","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/core/src/config/entity/entity-duplicator.ts b/packages/core/src/config/entity/entity-duplicator.ts index 0a368b49e2..3b9a4da218 100644 --- a/packages/core/src/config/entity/entity-duplicator.ts +++ b/packages/core/src/config/entity/entity-duplicator.ts @@ -41,8 +41,10 @@ export class EntityDuplicator extends Configu return this._forEntities; } - get requiresPermission() { - return this._requiresPermission; + get requiresPermission(): Permission[] { + return (Array.isArray(this._requiresPermission) + ? this._requiresPermission + : [this._requiresPermission]) as any as Permission[]; } constructor(config: EntityDuplicatorConfig) { diff --git a/packages/core/src/config/fulfillment/fulfillment-handler.ts b/packages/core/src/config/fulfillment/fulfillment-handler.ts index 82f826e189..db0d3b18ae 100644 --- a/packages/core/src/config/fulfillment/fulfillment-handler.ts +++ b/packages/core/src/config/fulfillment/fulfillment-handler.ts @@ -1,4 +1,4 @@ -import { ConfigArg, FulfillOrderInput, OrderLineInput } from '@vendure/common/lib/generated-types'; +import { ConfigArg, OrderLineInput } from '@vendure/common/lib/generated-types'; import { RequestContext } from '../../api/common/request-context'; import { @@ -14,7 +14,6 @@ import { FulfillmentState, FulfillmentTransitionData, } from '../../service/helpers/fulfillment-state-machine/fulfillment-state'; -import { CalculateShippingFnResult } from '../shipping-method/shipping-calculator'; /** * @docsCategory fulfillment diff --git a/packages/core/src/service/helpers/entity-duplicator/entity-duplicator.service.ts b/packages/core/src/service/helpers/entity-duplicator/entity-duplicator.service.ts index 7552a9ca68..281fb43bd0 100644 --- a/packages/core/src/service/helpers/entity-duplicator/entity-duplicator.service.ts +++ b/packages/core/src/service/helpers/entity-duplicator/entity-duplicator.service.ts @@ -9,11 +9,16 @@ import { import { RequestContext } from '../../../api/index'; import { DuplicateEntityError } from '../../../common/index'; import { ConfigService } from '../../../config/index'; +import { TransactionalConnection } from '../../../connection/index'; import { ConfigArgService } from '../config-arg/config-arg.service'; @Injectable() export class EntityDuplicatorService { - constructor(private configService: ConfigService, private configArgService: ConfigArgService) {} + constructor( + private configService: ConfigService, + private configArgService: ConfigArgService, + private connection: TransactionalConnection, + ) {} getEntityDuplicators(ctx: RequestContext): EntityDuplicatorDefinition[] { return this.configArgService.getDefinitions('EntityDuplicator').map(x => ({ @@ -38,10 +43,10 @@ export class EntityDuplicatorService { } // Check permissions - const permissionsArray = Array.isArray(duplicator.requiresPermission) - ? duplicator.requiresPermission - : [duplicator.requiresPermission]; - if (permissionsArray.length === 0 || !ctx.userHasPermissions(permissionsArray as Permission[])) { + if ( + duplicator.requiresPermission.length === 0 || + !ctx.userHasPermissions(duplicator.requiresPermission) + ) { return new DuplicateEntityError({ duplicationError: ctx.translate(`message.entity-duplication-no-permission`), }); @@ -49,18 +54,21 @@ export class EntityDuplicatorService { const parsedInput = this.configArgService.parseInput('EntityDuplicator', input.duplicatorInput); - try { - const newEntity = await duplicator.duplicate({ - ctx, - entityName: input.entityName, - id: input.entityId, - args: parsedInput.args, - }); - return { newEntityId: newEntity.id }; - } catch (e: any) { - return new DuplicateEntityError({ - duplicationError: e.message ?? e.toString(), - }); - } + return await this.connection.withTransaction(ctx, async innerCtx => { + try { + const newEntity = await duplicator.duplicate({ + ctx: innerCtx, + entityName: input.entityName, + id: input.entityId, + args: parsedInput.args, + }); + return { newEntityId: newEntity.id }; + } catch (e: any) { + await this.connection.rollBackTransaction(innerCtx); + return new DuplicateEntityError({ + duplicationError: e.message ?? e.toString(), + }); + } + }); } }