Переглянути джерело

feat(core): Implement internal support for entity duplication

Relates to #627
Michael Bromley 1 рік тому
батько
коміт
477fe93e46

+ 34 - 0
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -1509,6 +1509,24 @@ export type Discount = {
   type: AdjustmentType;
 };
 
+export type DuplicateEntityError = ErrorResult & {
+  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 = {
+  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 & {
   errorCode: ErrorCode;
@@ -1526,6 +1544,14 @@ export type EntityCustomFields = {
   entityName: Scalars['String']['output'];
 };
 
+export type EntityDuplicatorDefinition = {
+  args: Array<ConfigArgDefinition>;
+  code: Scalars['String']['output'];
+  description: Scalars['String']['output'];
+  forEntities: Array<Scalars['String']['output']>;
+  requiresPermission: Array<Permission>;
+};
+
 export enum ErrorCode {
   ALREADY_REFUNDED_ERROR = 'ALREADY_REFUNDED_ERROR',
   CANCEL_ACTIVE_ORDER_ERROR = 'CANCEL_ACTIVE_ORDER_ERROR',
@@ -1535,6 +1561,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',
@@ -2720,6 +2747,7 @@ export type Mutation = {
   deleteZone: DeletionResponse;
   /** Delete a Zone */
   deleteZones: Array<DeletionResponse>;
+  duplicateEntity: DuplicateEntityResult;
   flushBufferedJobs: Success;
   importProducts?: Maybe<ImportInfo>;
   /** Authenticates the user using the native authentication strategy. This mutation is an alias for `authenticate({ native: { ... }})` */
@@ -3333,6 +3361,11 @@ export type MutationDeleteZonesArgs = {
 };
 
 
+export type MutationDuplicateEntityArgs = {
+  input: DuplicateEntityInput;
+};
+
+
 export type MutationFlushBufferedJobsArgs = {
   bufferIds?: InputMaybe<Array<Scalars['String']['input']>>;
 };
@@ -4826,6 +4859,7 @@ export type Query = {
   customers: CustomerList;
   /** Returns a list of eligible shipping methods for the draft Order */
   eligibleShippingMethodsForDraftOrder: Array<ShippingMethodQuote>;
+  entityDuplicators: Array<EntityDuplicatorDefinition>;
   facet?: Maybe<Facet>;
   facetValues: FacetValueList;
   facets: FacetList;

+ 2 - 0
packages/core/src/api/api-internal-modules.ts

@@ -20,6 +20,7 @@ import { CountryResolver } from './resolvers/admin/country.resolver';
 import { CustomerGroupResolver } from './resolvers/admin/customer-group.resolver';
 import { CustomerResolver } from './resolvers/admin/customer.resolver';
 import { DraftOrderResolver } from './resolvers/admin/draft-order.resolver';
+import { DuplicateEntityResolver } from './resolvers/admin/duplicate-entity.resolver';
 import { FacetResolver } from './resolvers/admin/facet.resolver';
 import { GlobalSettingsResolver } from './resolvers/admin/global-settings.resolver';
 import { ImportResolver } from './resolvers/admin/import.resolver';
@@ -97,6 +98,7 @@ const adminResolvers = [
     CustomerGroupResolver,
     CustomerResolver,
     DraftOrderResolver,
+    DuplicateEntityResolver,
     FacetResolver,
     GlobalSettingsResolver,
     ImportResolver,

+ 33 - 0
packages/core/src/api/resolvers/admin/duplicate-entity.resolver.ts

@@ -0,0 +1,33 @@
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
+import {
+    DuplicateEntityResult,
+    MutationDuplicateEntityArgs,
+    Permission,
+} from '@vendure/common/lib/generated-types';
+
+import { EntityDuplicatorService } from '../../../service/index';
+import { RequestContext } from '../../common/request-context';
+import { Allow } from '../../decorators/allow.decorator';
+import { Ctx } from '../../decorators/request-context.decorator';
+import { Transaction } from '../../decorators/transaction.decorator';
+
+@Resolver()
+export class DuplicateEntityResolver {
+    constructor(private entityDuplicatorService: EntityDuplicatorService) {}
+
+    @Query()
+    @Allow(Permission.Authenticated)
+    async entityDuplicators(@Ctx() ctx: RequestContext) {
+        return this.entityDuplicatorService.getEntityDuplicators(ctx);
+    }
+
+    @Mutation()
+    @Transaction()
+    @Allow(Permission.Authenticated)
+    async duplicateEntity(
+        @Ctx() ctx: RequestContext,
+        @Args() args: MutationDuplicateEntityArgs,
+    ): Promise<DuplicateEntityResult> {
+        return this.entityDuplicatorService.duplicateEntity(ctx, args.input);
+    }
+}

+ 33 - 0
packages/core/src/api/schema/admin-api/duplicate-entity.api.graphql

@@ -0,0 +1,33 @@
+extend type Query {
+    entityDuplicators: [EntityDuplicatorDefinition!]!
+}
+
+extend type Mutation {
+    duplicateEntity(input: DuplicateEntityInput!): DuplicateEntityResult!
+}
+
+type EntityDuplicatorDefinition {
+    code: String!
+    args: [ConfigArgDefinition!]!
+    description: String!
+    forEntities: [String!]!
+    requiresPermission: [Permission!]!
+}
+
+type DuplicateEntitySuccess {
+    newEntityId: ID!
+}
+
+type DuplicateEntityError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+    duplicationError: String!
+}
+
+union DuplicateEntityResult = DuplicateEntitySuccess | DuplicateEntityError
+
+input DuplicateEntityInput {
+    entityName: String!
+    entityId: ID!
+    duplicatorInput: ConfigurableOperationInput!
+}

+ 19 - 1
packages/core/src/common/error/generated-graphql-admin-errors.ts

@@ -127,6 +127,19 @@ export class CreateFulfillmentError extends ErrorResult {
   }
 }
 
+export class DuplicateEntityError extends ErrorResult {
+  readonly __typename = 'DuplicateEntityError';
+  readonly errorCode = 'DUPLICATE_ENTITY_ERROR' as any;
+  readonly message = 'DUPLICATE_ENTITY_ERROR';
+  readonly duplicationError: Scalars['String'];
+  constructor(
+    input: { duplicationError: Scalars['String'] }
+  ) {
+    super();
+    this.duplicationError = input.duplicationError
+  }
+}
+
 export class EmailAddressConflictError extends ErrorResult {
   readonly __typename = 'EmailAddressConflictError';
   readonly errorCode = 'EMAIL_ADDRESS_CONFLICT_ERROR' as any;
@@ -575,7 +588,7 @@ export class SettlePaymentError extends ErrorResult {
 }
 
 
-const errorTypeNames = new Set<string>(['AlreadyRefundedError', 'CancelActiveOrderError', 'CancelPaymentError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FacetInUseError', 'FulfillmentStateTransitionError', 'GuestCheckoutError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundAmountError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
+const errorTypeNames = new Set<string>(['AlreadyRefundedError', 'CancelActiveOrderError', 'CancelPaymentError', 'ChannelDefaultLanguageError', 'CouponCodeExpiredError', 'CouponCodeInvalidError', 'CouponCodeLimitError', 'CreateFulfillmentError', 'DuplicateEntityError', 'EmailAddressConflictError', 'EmptyOrderLineSelectionError', 'FacetInUseError', 'FulfillmentStateTransitionError', 'GuestCheckoutError', 'IneligibleShippingMethodError', 'InsufficientStockError', 'InsufficientStockOnHandError', 'InvalidCredentialsError', 'InvalidFulfillmentHandlerError', 'ItemsAlreadyFulfilledError', 'LanguageNotAvailableError', 'ManualPaymentStateError', 'MimeTypeError', 'MissingConditionsError', 'MultipleOrderError', 'NativeAuthStrategyError', 'NegativeQuantityError', 'NoActiveOrderError', 'NoChangesSpecifiedError', 'NothingToRefundError', 'OrderLimitError', 'OrderModificationError', 'OrderModificationStateError', 'OrderStateTransitionError', 'PaymentMethodMissingError', 'PaymentOrderMismatchError', 'PaymentStateTransitionError', 'ProductOptionInUseError', 'QuantityTooGreatError', 'RefundAmountError', 'RefundOrderStateError', 'RefundPaymentIdMissingError', 'RefundStateTransitionError', 'SettlePaymentError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }
@@ -636,6 +649,11 @@ export const adminErrorOperationTypeResolvers = {
       return isGraphQLError(value) ? (value as any).__typename : 'Promotion';
     },
   },
+  DuplicateEntityResult: {
+    __resolveType(value: any) {
+      return isGraphQLError(value) ? (value as any).__typename : 'DuplicateEntitySuccess';
+    },
+  },
   NativeAuthenticationResult: {
     __resolveType(value: any) {
       return isGraphQLError(value) ? (value as any).__typename : 'CurrentUser';

+ 2 - 0
packages/core/src/config/config.module.ts

@@ -151,6 +151,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
     private getConfigurableOperations(): Array<ConfigurableOperationDef<any>> {
         const { paymentMethodHandlers, paymentMethodEligibilityCheckers } = this.configService.paymentOptions;
         const { collectionFilters } = this.configService.catalogOptions;
+        const { entityDuplicators } = this.configService.entityOptions;
         const { promotionActions, promotionConditions } = this.configService.promotionOptions;
         const { shippingCalculators, shippingEligibilityCheckers, fulfillmentHandlers } =
             this.configService.shippingOptions;
@@ -163,6 +164,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             ...(shippingCalculators || []),
             ...(shippingEligibilityCheckers || []),
             ...(fulfillmentHandlers || []),
+            ...(entityDuplicators || []),
         ];
     }
 }

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -25,6 +25,7 @@ import { DefaultStockDisplayStrategy } from './catalog/default-stock-display-str
 import { DefaultStockLocationStrategy } from './catalog/default-stock-location-strategy';
 import { AutoIncrementIdStrategy } from './entity/auto-increment-id-strategy';
 import { DefaultMoneyStrategy } from './entity/default-money-strategy';
+import { defaultEntityDuplicators } from './entity/entity-duplicators/index';
 import { defaultFulfillmentProcess } from './fulfillment/default-fulfillment-process';
 import { manualFulfillmentHandler } from './fulfillment/manual-fulfillment-handler';
 import { DefaultLogger } from './logger/default-logger';
@@ -130,6 +131,7 @@ export const defaultConfig: RuntimeVendureConfig = {
     },
     entityOptions: {
         moneyStrategy: new DefaultMoneyStrategy(),
+        entityDuplicators: defaultEntityDuplicators,
         channelCacheTtl: 30000,
         zoneCacheTtl: 30000,
         taxRateCacheTtl: 30000,

+ 23 - 0
packages/core/src/config/entity/entity-duplication-strategy.ts

@@ -0,0 +1,23 @@
+import { Permission } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/index';
+import { ConfigArgs, ConfigArgValues } from '../../common/configurable-operation';
+import { InjectableStrategy } from '../../common/index';
+import { VendureEntity } from '../../entity/index';
+
+export type InputArgValues<Strategy extends EntityDuplicationStrategy> = ConfigArgValues<
+    ReturnType<Strategy['defineInputArgs']>
+>;
+
+export interface EntityDuplicationStrategy extends InjectableStrategy {
+    readonly requiresPermission: Array<Permission | string> | Permission | string;
+    readonly canDuplicateEntities: string[];
+    defineInputArgs(): ConfigArgs;
+    duplicate(input: {
+        ctx: RequestContext;
+        entityName: string;
+        id: ID;
+        args: InputArgValues<EntityDuplicationStrategy>;
+    }): Promise<VendureEntity>;
+}

+ 66 - 0
packages/core/src/config/entity/entity-duplicator.ts

@@ -0,0 +1,66 @@
+import { ConfigArg, Permission } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
+
+import { RequestContext } from '../../api/index';
+import {
+    ConfigArgs,
+    ConfigArgValues,
+    ConfigurableOperationDef,
+    ConfigurableOperationDefOptions,
+} from '../../common/configurable-operation';
+import { VendureEntity } from '../../entity/index';
+
+export type DuplicateEntityFn<T extends ConfigArgs> = (input: {
+    ctx: RequestContext;
+    entityName: string;
+    id: ID;
+    args: ConfigArgValues<T>;
+}) => Promise<VendureEntity>;
+
+/**
+ * @description
+ *
+ *
+ */
+export interface EntityDuplicatorConfig<T extends ConfigArgs> extends ConfigurableOperationDefOptions<T> {
+    requiresPermission: Array<Permission | string> | Permission | string;
+    forEntities: string[];
+    duplicate: DuplicateEntityFn<T>;
+}
+
+export class EntityDuplicator<T extends ConfigArgs = ConfigArgs> extends ConfigurableOperationDef<T> {
+    private _forEntities: string[];
+    private _requiresPermission: Array<Permission | string> | Permission | string;
+    private duplicateFn: DuplicateEntityFn<T>;
+
+    canDuplicate(entityName: string): boolean {
+        return this._forEntities.includes(entityName);
+    }
+
+    get forEntities() {
+        return this._forEntities;
+    }
+
+    get requiresPermission() {
+        return this._requiresPermission;
+    }
+
+    constructor(config: EntityDuplicatorConfig<T>) {
+        super(config);
+        this._forEntities = config.forEntities;
+        this._requiresPermission = config.requiresPermission;
+        this.duplicateFn = config.duplicate;
+    }
+
+    duplicate(input: {
+        ctx: RequestContext;
+        entityName: string;
+        id: ID;
+        args: ConfigArg[];
+    }): Promise<VendureEntity> {
+        return this.duplicateFn({
+            ...input,
+            args: this.argsArrayToHash(input.args),
+        });
+    }
+}

+ 1 - 0
packages/core/src/config/entity/entity-duplicators/index.ts

@@ -0,0 +1 @@
+export const defaultEntityDuplicators = [];

+ 2 - 0
packages/core/src/config/index.ts

@@ -27,6 +27,8 @@ export * from './custom-field/custom-field-types';
 export * from './default-config';
 export * from './entity/auto-increment-id-strategy';
 export * from './entity/default-money-strategy';
+export * from './entity/entity-duplicator';
+export * from './entity/entity-duplicators/index';
 export * from './entity/bigint-money-strategy';
 export * from './entity/entity-id-strategy';
 export * from './entity/money-strategy';

+ 3 - 0
packages/core/src/config/vendure-config.ts

@@ -24,6 +24,8 @@ import { ProductVariantPriceUpdateStrategy } from './catalog/product-variant-pri
 import { StockDisplayStrategy } from './catalog/stock-display-strategy';
 import { StockLocationStrategy } from './catalog/stock-location-strategy';
 import { CustomFields } from './custom-field/custom-field-types';
+import { EntityDuplicationStrategy } from './entity/entity-duplication-strategy';
+import { EntityDuplicator } from './entity/entity-duplicator';
 import { EntityIdStrategy } from './entity/entity-id-strategy';
 import { MoneyStrategy } from './entity/money-strategy';
 import { EntityMetadataModifier } from './entity-metadata/entity-metadata-modifier';
@@ -960,6 +962,7 @@ export interface EntityOptions {
      * @default AutoIncrementIdStrategy
      */
     entityIdStrategy?: EntityIdStrategy<any>;
+    entityDuplicators?: EntityDuplicator<any>[];
     /**
      * @description
      * Defines the strategy used to store and round monetary values.

+ 3 - 0
packages/core/src/i18n/messages/en.json

@@ -64,6 +64,7 @@
     "COUPON_CODE_INVALID_ERROR": "Coupon code \"{ couponCode }\" is not valid",
     "COUPON_CODE_LIMIT_ERROR": "Coupon code cannot be used more than {limit, plural, one {once} other {# times}} per customer",
     "CREATE_FULFILLMENT_ERROR": "An error occurred when attempting to create the Fulfillment",
+    "DUPLICATE_ENTITY_ERROR": "The entity could not be duplicated",
     "EMAIL_ADDRESS_CONFLICT_ERROR": "The email address is not available.",
     "EMPTY_ORDER_LINE_SELECTION_ERROR": "At least one OrderLine must be specified",
     "FACET_IN_USE_ERROR": "The Facet \"{ facetCode }\" includes FacetValues which are assigned to {productCount, plural, =0 {} one {1 Product} other {# Products}} {variantCount, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
@@ -123,6 +124,8 @@
     "cannot-transition-without-modification-payment": "Can only transition to the \"ArrangingAdditionalPayment\" state",
     "cannot-transition-without-settled-payments": "Cannot transition Order to the \"PaymentSettled\" state when the total is not covered by settled Payments",
     "country-used-in-addresses": "The selected Country cannot be deleted as it is used in {count, plural, one {1 Address} other {# Addresses}}",
+    "entity-duplication-no-permission": "You do not have the required permissions to duplicate this entity",
+    "entity-duplication-no-strategy-found": "No duplication strategy with code \"{ code }\" was found for the entity type \"{ entityName }\"",
     "facet-force-deleted": "The Facet was deleted and its FacetValues were removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both {, } single {} other {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-used": "The Facet \"{ facetCode }\" includes FacetValues which are assigned to {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both {, } single {} other {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}}",
     "facet-value-force-deleted": "The selected FacetValue was removed from {products, plural, =0 {} one {1 Product} other {# Products}}{both, select, both {, } single {} other {}}{variants, plural, =0 {} one {1 ProductVariant} other {# ProductVariants}} and deleted",

+ 3 - 11
packages/core/src/service/helpers/config-arg/config-arg.service.ts

@@ -5,6 +5,7 @@ import { ConfigurableOperationDef } from '../../../common/configurable-operation
 import { UserInputError } from '../../../common/error/errors';
 import { CollectionFilter } from '../../../config/catalog/collection-filter';
 import { ConfigService } from '../../../config/config.service';
+import { EntityDuplicator } from '../../../config/entity/entity-duplicator';
 import { FulfillmentHandler } from '../../../config/fulfillment/fulfillment-handler';
 import { PaymentMethodEligibilityChecker } from '../../../config/payment/payment-method-eligibility-checker';
 import { PaymentMethodHandler } from '../../../config/payment/payment-method-handler';
@@ -15,6 +16,7 @@ import { ShippingEligibilityChecker } from '../../../config/shipping-method/ship
 
 export type ConfigDefTypeMap = {
     CollectionFilter: CollectionFilter;
+    EntityDuplicator: EntityDuplicator;
     FulfillmentHandler: FulfillmentHandler;
     PaymentMethodEligibilityChecker: PaymentMethodEligibilityChecker;
     PaymentMethodHandler: PaymentMethodHandler;
@@ -36,6 +38,7 @@ export class ConfigArgService {
     constructor(private configService: ConfigService) {
         this.definitionsByType = {
             CollectionFilter: this.configService.catalogOptions.collectionFilters,
+            EntityDuplicator: this.configService.entityOptions.entityDuplicators,
             FulfillmentHandler: this.configService.shippingOptions.fulfillmentHandlers,
             PaymentMethodEligibilityChecker:
                 this.configService.paymentOptions.paymentMethodEligibilityCheckers || [],
@@ -90,17 +93,6 @@ export class ConfigArgService {
         return output;
     }
 
-    private parseOperationArgs(
-        input: ConfigurableOperationInput,
-        checkerOrCalculator: ShippingEligibilityChecker | ShippingCalculator,
-    ): ConfigurableOperation {
-        const output: ConfigurableOperation = {
-            code: input.code,
-            args: input.arguments,
-        };
-        return output;
-    }
-
     private validateRequiredFields(input: ConfigurableOperationInput, def: ConfigurableOperationDef) {
         for (const [name, argDef] of Object.entries(def.args)) {
             if (argDef.required) {

+ 66 - 0
packages/core/src/service/helpers/entity-duplicator/entity-duplicator.service.ts

@@ -0,0 +1,66 @@
+import { Injectable } from '@nestjs/common';
+import {
+    DuplicateEntityInput,
+    DuplicateEntityResult,
+    EntityDuplicatorDefinition,
+    Permission,
+} from '@vendure/common/lib/generated-types';
+
+import { RequestContext } from '../../../api/index';
+import { DuplicateEntityError } from '../../../common/index';
+import { ConfigService } from '../../../config/index';
+import { ConfigArgService } from '../config-arg/config-arg.service';
+
+@Injectable()
+export class EntityDuplicatorService {
+    constructor(private configService: ConfigService, private configArgService: ConfigArgService) {}
+
+    getEntityDuplicators(ctx: RequestContext): EntityDuplicatorDefinition[] {
+        return this.configArgService.getDefinitions('EntityDuplicator').map(x => ({
+            ...x.toGraphQlType(ctx),
+            __typename: 'EntityDuplicatorDefinition',
+            forEntities: x.forEntities,
+            requiresPermission: x.requiresPermission as Permission[],
+        }));
+    }
+
+    async duplicateEntity(ctx: RequestContext, input: DuplicateEntityInput): Promise<DuplicateEntityResult> {
+        const duplicator = this.configService.entityOptions.entityDuplicators.find(
+            s => s.forEntities.includes(input.entityName) && s.code === input.duplicatorInput.code,
+        );
+        if (!duplicator) {
+            return new DuplicateEntityError({
+                duplicationError: ctx.translate(`message.entity-duplication-no-strategy-found`, {
+                    entityName: input.entityName,
+                    code: input.duplicatorInput.code,
+                }),
+            });
+        }
+
+        // Check permissions
+        const permissionsArray = Array.isArray(duplicator.requiresPermission)
+            ? duplicator.requiresPermission
+            : [duplicator.requiresPermission];
+        if (permissionsArray.length === 0 || !ctx.userHasPermissions(permissionsArray as Permission[])) {
+            return new DuplicateEntityError({
+                duplicationError: ctx.translate(`message.entity-duplication-no-permission`),
+            });
+        }
+
+        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(),
+            });
+        }
+    }
+}

+ 1 - 0
packages/core/src/service/index.ts

@@ -1,6 +1,7 @@
 export * from './helpers/active-order/active-order.service';
 export * from './helpers/config-arg/config-arg.service';
 export * from './helpers/custom-field-relation/custom-field-relation.service';
+export * from './helpers/entity-duplicator/entity-duplicator.service';
 export * from './helpers/entity-hydrator/entity-hydrator.service';
 export * from './helpers/external-authentication/external-authentication.service';
 export * from './helpers/fulfillment-state-machine/fulfillment-state';

+ 2 - 0
packages/core/src/service/service.module.ts

@@ -9,6 +9,7 @@ import { JobQueueModule } from '../job-queue/job-queue.module';
 import { ActiveOrderService } from './helpers/active-order/active-order.service';
 import { ConfigArgService } from './helpers/config-arg/config-arg.service';
 import { CustomFieldRelationService } from './helpers/custom-field-relation/custom-field-relation.service';
+import { EntityDuplicatorService } from './helpers/entity-duplicator/entity-duplicator.service';
 import { EntityHydrator } from './helpers/entity-hydrator/entity-hydrator.service';
 import { ExternalAuthenticationService } from './helpers/external-authentication/external-authentication.service';
 import { FulfillmentStateMachine } from './helpers/fulfillment-state-machine/fulfillment-state-machine';
@@ -128,6 +129,7 @@ const helpers = [
     EntityHydrator,
     RequestContextService,
     TranslatorService,
+    EntityDuplicatorService,
 ];
 
 /**

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
schema-admin.json


Деякі файли не було показано, через те що забагато файлів було змінено