Browse Source

refactor(admin-ui): Use decorator to encode/decode entity ids

Michael Bromley 7 years ago
parent
commit
19272c13c8

+ 4 - 0
server/src/api/administrator/administrator.resolver.ts

@@ -2,22 +2,26 @@ import { Mutation, Query, Resolver } from '@nestjs/graphql';
 
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { AdministratorService } from '../../service/administrator.service';
+import { ApplyIdCodec } from '../common/apply-id-codec-decorator';
 
 @Resolver('Administrator')
 export class AdministratorResolver {
     constructor(private administratorService: AdministratorService) {}
 
     @Query('administrators')
+    @ApplyIdCodec()
     administrators(): Promise<Administrator[]> {
         return this.administratorService.findAll();
     }
 
     @Query('administrator')
+    @ApplyIdCodec()
     administrator(obj, args): Promise<Administrator | undefined> {
         return this.administratorService.findOne(args.id);
     }
 
     @Mutation()
+    @ApplyIdCodec()
     createAdministrator(_, args): Promise<Administrator> {
         const { input } = args;
         return this.administratorService.create(input);

+ 91 - 0
server/src/api/common/apply-id-codec-decorator.spec.ts

@@ -0,0 +1,91 @@
+import { ApplyIdCodec, IdCodecType } from './apply-id-codec-decorator';
+
+describe('@ApplyIdCodec()', () => {
+    let mockResolver: any;
+    let mockIdCodec: MockIdCodec;
+    let argsReceived: any;
+    const RETURN_VAL = 'returnVal';
+    const ENCODED_VAL = 'encoded';
+    const DECODED_VAL = 'decoded';
+
+    class MockIdCodec implements IdCodecType {
+        decode = jest.fn().mockReturnValue(DECODED_VAL);
+        encode = jest.fn().mockReturnValue(ENCODED_VAL);
+    }
+
+    beforeEach(() => {
+        mockIdCodec = new MockIdCodec();
+        argsReceived = undefined;
+
+        class MockResolver {
+            @ApplyIdCodec(undefined, mockIdCodec)
+            getItem(_, args) {
+                argsReceived = args;
+                return RETURN_VAL;
+            }
+
+            @ApplyIdCodec(['foo'], mockIdCodec)
+            getItemWithKeys(_, args) {
+                argsReceived = args;
+                return RETURN_VAL;
+            }
+
+            @ApplyIdCodec(undefined, mockIdCodec)
+            getItemAsync(_, args) {
+                argsReceived = args;
+                return Promise.resolve(RETURN_VAL);
+            }
+        }
+
+        mockResolver = new MockResolver();
+    });
+
+    it('calls IdCodec.decode with args', () => {
+        const args = { foo: 1 };
+        mockResolver.getItem({}, args);
+
+        expect(mockIdCodec.decode).toHaveBeenCalledWith(args, undefined);
+    });
+
+    it('passes transformed args to resolver method', () => {
+        const args = { foo: 1 };
+        mockResolver.getItem({}, args);
+
+        expect(argsReceived).toBe(DECODED_VAL);
+    });
+
+    it('calls IdCodec.decode with args and transformKeys', () => {
+        const args = { foo: 1 };
+        mockResolver.getItemWithKeys({}, args);
+
+        expect(mockIdCodec.decode).toHaveBeenCalledWith(args, ['foo']);
+    });
+
+    it('calls IdCodec.encode on return value', () => {
+        const args = { foo: 1 };
+        mockResolver.getItem({}, args);
+
+        expect(mockIdCodec.encode).toHaveBeenCalledWith(RETURN_VAL);
+    });
+
+    it('returns the encoded value', () => {
+        const args = { foo: 1 };
+        const result = mockResolver.getItem({}, args);
+
+        expect(result).toBe(ENCODED_VAL);
+    });
+
+    it('calls IdCodec.encode on returned promise value', () => {
+        const args = { foo: 1 };
+        return mockResolver.getItemAsync({}, args).then(() => {
+            expect(mockIdCodec.encode).toHaveBeenCalledWith(RETURN_VAL);
+        });
+    });
+
+    it('returns a promise with the encoded value', () => {
+        const args = { foo: 1 };
+        return mockResolver.getItemAsync({}, args).then(result => {
+            expect(result).toBe(ENCODED_VAL);
+        });
+    });
+});

+ 41 - 0
server/src/api/common/apply-id-codec-decorator.ts

@@ -0,0 +1,41 @@
+import { EntityIdStrategy } from '../../config/entity-id-strategy';
+import { getConfig } from '../../config/vendure-config';
+
+import { IdCodec } from './id-codec';
+
+export type IdCodecType = { [K in keyof IdCodec]: IdCodec[K] };
+
+/**
+ * A decorator for use on resolver methods which automatically applies the configured
+ * EntityIdStrategy to the arguments & return values of the resolver.
+ *
+ * @param transformKeys - An optional array of keys of the arguments object to be decoded. If not set,
+ * then the arguments will be walked recursively and any `id` property will be decoded.
+ * @param customIdCodec - A custom IdCodec instance, primarily intended for testing.
+ */
+export function ApplyIdCodec(transformKeys?: string[], customIdCodec?: IdCodecType) {
+    let strategy: EntityIdStrategy;
+    let idCodec: IdCodecType;
+
+    return (target: any, name: string | symbol, descriptor: PropertyDescriptor) => {
+        if (!customIdCodec) {
+            strategy = getConfig().entityIdStrategy;
+            idCodec = new IdCodec(strategy);
+        } else {
+            idCodec = customIdCodec;
+        }
+
+        const originalFn = descriptor.value;
+        if (typeof originalFn === 'function') {
+            descriptor.value = function(rootValue?: any, args?: any, context?: any, info?: any) {
+                const encodedArgs = idCodec.decode(args, transformKeys);
+                const result = originalFn.apply(this, [rootValue, encodedArgs, context, info]);
+                if (result.then) {
+                    return result.then(data => idCodec.encode(data));
+                } else {
+                    return idCodec.encode(result);
+                }
+            };
+        }
+    };
+}

+ 40 - 34
server/src/service/id-codec.service.spec.ts → server/src/api/common/id-codec.spec.ts

@@ -1,46 +1,38 @@
-import { Test } from '@nestjs/testing';
+import { DECODED, ENCODED, MockIdStrategy } from '../../service/config.service.mock';
 
-import { ConfigService } from './config.service';
-import { DECODED, ENCODED, MockConfigService } from './config.service.mock';
-import { IdCodecService } from './id-codec.service';
+import { IdCodec } from './id-codec';
 
 describe('IdCodecService', () => {
-    let idCodecService: IdCodecService;
-    let configService: MockConfigService;
+    let idCodec: IdCodec;
 
     beforeEach(async () => {
-        const module = await Test.createTestingModule({
-            providers: [IdCodecService, { provide: ConfigService, useClass: MockConfigService }],
-        }).compile();
-
-        idCodecService = module.get(IdCodecService);
-        configService = module.get(ConfigService) as any;
+        idCodec = new IdCodec(new MockIdStrategy());
     });
 
     describe('encode()', () => {
         it('works with a string', () => {
             const input = 'id';
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual(ENCODED);
         });
 
         it('works with a number', () => {
             const input = 123;
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual(ENCODED);
         });
 
         it('passes through null or undefined without throwing', () => {
-            expect(idCodecService.encode(null as any)).toBeNull();
-            expect(idCodecService.encode(undefined as any)).toBeUndefined();
+            expect(idCodec.encode(null as any)).toBeNull();
+            expect(idCodec.encode(undefined as any)).toBeUndefined();
         });
 
         it('works with simple entity', () => {
             const input = { id: 'id', name: 'foo' };
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual({ id: ENCODED, name: 'foo' });
         });
 
@@ -50,7 +42,7 @@ describe('IdCodecService', () => {
                 friend: { id: 'id' },
             };
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual({
                 id: ENCODED,
                 friend: { id: ENCODED },
@@ -65,7 +57,7 @@ describe('IdCodecService', () => {
                 },
             };
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual({
                 id: ENCODED,
                 friend: {
@@ -77,15 +69,15 @@ describe('IdCodecService', () => {
         it('works with list of simple entities', () => {
             const input = [{ id: 'id', name: 'foo' }, { id: 'id', name: 'bar' }];
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual([{ id: ENCODED, name: 'foo' }, { id: ENCODED, name: 'bar' }]);
         });
 
         it('does not throw with an empty list', () => {
             const input = [];
 
-            const result = idCodecService.encode(input);
-            expect(() => idCodecService.encode(input)).not.toThrow();
+            const result = idCodec.encode(input);
+            expect(() => idCodec.encode(input)).not.toThrow();
         });
 
         it('works with nested list of simple entities', () => {
@@ -93,7 +85,7 @@ describe('IdCodecService', () => {
                 items: [{ id: 'id', name: 'foo' }, { id: 'id', name: 'bar' }],
             };
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual({
                 items: [{ id: ENCODED, name: 'foo' }, { id: ENCODED, name: 'bar' }],
             });
@@ -110,7 +102,7 @@ describe('IdCodecService', () => {
                 })),
             };
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result.items.length).toBe(length);
             expect(result.items[0].id).toBe(ENCODED);
             expect(result.items[0].friends[0].id).toBe(ENCODED);
@@ -131,7 +123,7 @@ describe('IdCodecService', () => {
                 ],
             };
 
-            const result = idCodecService.encode(input);
+            const result = idCodec.encode(input);
             expect(result).toEqual({
                 items: [
                     {
@@ -145,27 +137,34 @@ describe('IdCodecService', () => {
                 ],
             });
         });
+
+        it('transformKeys can be customized', () => {
+            const input = { id: 'id', name: 'foo' };
+
+            const result = idCodec.encode(input, ['name']);
+            expect(result).toEqual({ id: 'id', name: ENCODED });
+        });
     });
 
     describe('decode()', () => {
         it('works with a string', () => {
             const input = 'id';
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual(DECODED);
         });
 
         it('works with a number', () => {
             const input = 123;
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual(DECODED);
         });
 
         it('works with simple entity', () => {
             const input = { id: 'id', name: 'foo' };
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual({ id: DECODED, name: 'foo' });
         });
 
@@ -175,7 +174,7 @@ describe('IdCodecService', () => {
                 friend: { id: 'id' },
             };
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual({
                 id: DECODED,
                 friend: { id: DECODED },
@@ -190,7 +189,7 @@ describe('IdCodecService', () => {
                 },
             };
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual({
                 id: DECODED,
                 friend: {
@@ -202,7 +201,7 @@ describe('IdCodecService', () => {
         it('works with list of simple entities', () => {
             const input = [{ id: 'id', name: 'foo' }, { id: 'id', name: 'bar' }];
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual([{ id: DECODED, name: 'foo' }, { id: DECODED, name: 'bar' }]);
         });
 
@@ -211,7 +210,7 @@ describe('IdCodecService', () => {
                 items: [{ id: 'id', name: 'foo' }, { id: 'id', name: 'bar' }],
             };
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual({
                 items: [{ id: DECODED, name: 'foo' }, { id: DECODED, name: 'bar' }],
             });
@@ -228,7 +227,7 @@ describe('IdCodecService', () => {
                 })),
             };
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result.items.length).toBe(length);
             expect(result.items[0].id).toBe(DECODED);
             expect(result.items[0].friends[0].id).toBe(DECODED);
@@ -249,7 +248,7 @@ describe('IdCodecService', () => {
                 ],
             };
 
-            const result = idCodecService.decode(input);
+            const result = idCodec.decode(input);
             expect(result).toEqual({
                 items: [
                     {
@@ -263,5 +262,12 @@ describe('IdCodecService', () => {
                 ],
             });
         });
+
+        it('transformKeys can be customized', () => {
+            const input = { id: 'id', name: 'foo' };
+
+            const result = idCodec.decode(input, ['name']);
+            expect(result).toEqual({ id: 'id', name: DECODED });
+        });
     });
 });

+ 33 - 16
server/src/service/id-codec.service.ts → server/src/api/common/id-codec.ts

@@ -1,43 +1,56 @@
-import { Injectable } from '@nestjs/common';
+import { ID } from '../../../../shared/shared-types';
+import { VendureEntity } from '../../entity/base/base.entity';
 
-import { ID, PaginatedList } from '../../../shared/shared-types';
-import { VendureEntity } from '../entity/base/base.entity';
-
-import { ConfigService } from './config.service';
+import { EntityIdStrategy } from '../../config/entity-id-strategy';
 
 /**
  * This service is responsible for encoding/decoding entity IDs according to the configured EntityIdStrategy.
  * It should only need to be used in resolvers - the design is that once a request hits the business logic layer
  * (ProductService etc) all entity IDs are in the form used as the primary key in the database.
  */
-@Injectable()
-export class IdCodecService {
-    constructor(private configService: ConfigService) {}
+export class IdCodec {
+    constructor(private entityIdStrategy: EntityIdStrategy) {}
 
     /**
      * Decode an id from the client into the format used as the database primary key.
      * Acts recursively on all objects containing an "id" property.
+     *
+     * @param target - The object to be decoded
+     * @param transformKeys - An optional array of keys of the target to be decoded. If not defined,
+     * then the default recursive behaviour will be used.
      */
-    decode<T extends string | number | object | undefined>(target: T): T {
-        return this.transformRecursive(target, input => this.configService.entityIdStrategy.decodeId(input));
+    decode<T extends string | number | object | undefined>(target: T, transformKeys?: Array<keyof T>): T {
+        return this.transformRecursive(target, input => this.entityIdStrategy.decodeId(input), transformKeys);
     }
 
     /**
      * Encode any entity ids according to the encode.
      * Acts recursively on all objects containing an "id" property.
+     *
+     * @param target - The object to be encoded
+     * @param transformKeys - An optional array of keys of the target to be encoded. If not defined,
+     * then the default recursive behaviour will be used.
      */
-    encode<T extends string | number | object | undefined>(target: T): T {
-        return this.transformRecursive(target, input => this.configService.entityIdStrategy.encodeId(input));
+    encode<T extends string | number | object | undefined>(target: T, transformKeys?: Array<keyof T>): T {
+        return this.transformRecursive(target, input => this.entityIdStrategy.encodeId(input), transformKeys);
     }
 
-    private transformRecursive<T>(target: T, transformFn: (input: any) => ID): T {
+    private transformRecursive<T>(
+        target: T,
+        transformFn: (input: any) => ID,
+        transformKeys?: Array<keyof T>,
+    ): T {
         if (target == null) {
             return target;
         }
         if (typeof target === 'string' || typeof target === 'number') {
             return transformFn(target as any) as any;
         }
-        this.transform(target, transformFn);
+        this.transform(target, transformFn, transformKeys);
+
+        if (transformKeys) {
+            return target;
+        }
 
         if (Array.isArray(target)) {
             if (target.length === 0) {
@@ -69,8 +82,12 @@ export class IdCodecService {
         return target;
     }
 
-    private transform<T>(target: T, transformFn: (input: any) => ID): T {
-        if (this.isEntity(target)) {
+    private transform<T>(target: T, transformFn: (input: any) => ID, transformKeys?: Array<keyof T>): T {
+        if (transformKeys) {
+            for (const key of transformKeys) {
+                target[key] = transformFn(target[key]) as any;
+            }
+        } else if (this.isEntity(target)) {
             target.id = transformFn(target.id);
         }
         return target;

+ 12 - 20
server/src/api/customer/customer.resolver.ts

@@ -2,51 +2,43 @@ import { Mutation, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
 
 import { PaginatedList } from '../../../../shared/shared-types';
 import { Address } from '../../entity/address/address.entity';
-import { CreateCustomerDto } from '../../entity/customer/customer.dto';
 import { Customer } from '../../entity/customer/customer.entity';
 import { CustomerService } from '../../service/customer.service';
-import { IdCodecService } from '../../service/id-codec.service';
+import { ApplyIdCodec } from '../common/apply-id-codec-decorator';
 
 @Resolver('Customer')
 export class CustomerResolver {
-    constructor(private customerService: CustomerService, private idCodecService: IdCodecService) {}
+    constructor(private customerService: CustomerService) {}
 
     @Query('customers')
+    @ApplyIdCodec()
     async customers(obj, args): Promise<PaginatedList<Customer>> {
-        return this.customerService
-            .findAll(args.take, args.skip)
-            .then(list => this.idCodecService.encode(list));
+        return this.customerService.findAll(args.take, args.skip);
     }
 
     @Query('customer')
+    @ApplyIdCodec()
     async customer(obj, args): Promise<Customer | undefined> {
-        return this.customerService
-            .findOne(this.idCodecService.decode(args).id)
-            .then(c => this.idCodecService.encode(c));
+        return this.customerService.findOne(args.id);
     }
 
     @ResolveProperty('addresses')
+    @ApplyIdCodec()
     async addresses(customer: Customer): Promise<Address[]> {
-        const address = await this.customerService.findAddressesByCustomerId(
-            this.idCodecService.decode(customer).id,
-        );
-        return this.idCodecService.encode(address);
+        return this.customerService.findAddressesByCustomerId(customer.id);
     }
 
     @Mutation()
+    @ApplyIdCodec()
     async createCustomer(_, args): Promise<Customer> {
         const { input, password } = args;
-        const customer = await this.customerService.create(input, password);
-        return this.idCodecService.encode(customer);
+        return this.customerService.create(input, password);
     }
 
     @Mutation()
+    @ApplyIdCodec()
     async createCustomerAddress(_, args): Promise<Address> {
         const { customerId, input } = args;
-        const address = await this.customerService.createAddress(
-            this.idCodecService.decode(customerId),
-            input,
-        );
-        return this.idCodecService.encode(address);
+        return this.customerService.createAddress(customerId, input);
     }
 }

+ 7 - 9
server/src/api/product-option/product-option.resolver.ts

@@ -2,44 +2,42 @@ import { Mutation, Query, ResolveProperty, Resolver } from '@nestjs/graphql';
 
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
-import { Product } from '../../entity/product/product.entity';
-import { LanguageCode } from '../../locale/language-code';
 import { Translated } from '../../locale/locale-types';
-import { IdCodecService } from '../../service/id-codec.service';
 import { ProductOptionGroupService } from '../../service/product-option-group.service';
 import { ProductOptionService } from '../../service/product-option.service';
-import { ProductVariantService } from '../../service/product-variant.service';
-import { ProductService } from '../../service/product.service';
+import { ApplyIdCodec } from '../common/apply-id-codec-decorator';
 
 @Resolver('ProductOptionGroup')
 export class ProductOptionResolver {
     constructor(
         private productOptionGroupService: ProductOptionGroupService,
-        private idCodecService: IdCodecService,
         private productOptionService: ProductOptionService,
     ) {}
 
     @Query('productOptionGroups')
+    @ApplyIdCodec()
     productOptionGroups(obj, args): Promise<Array<Translated<ProductOptionGroup>>> {
         return this.productOptionGroupService.findAll(args.languageCode, args.filterTerm);
     }
 
     @Query('productOptionGroup')
+    @ApplyIdCodec()
     productOptionGroup(obj, args): Promise<Translated<ProductOptionGroup> | undefined> {
         return this.productOptionGroupService.findOne(args.id, args.languageCode);
     }
 
     @ResolveProperty('options')
+    @ApplyIdCodec()
     async options(optionGroup: Translated<ProductOptionGroup>): Promise<Array<Translated<ProductOption>>> {
         if (optionGroup.options) {
             return Promise.resolve(optionGroup.options);
         }
-
         const group = await this.productOptionGroupService.findOne(optionGroup.id, optionGroup.languageCode);
-        return group ? this.idCodecService.encode(group).options : [];
+        return group ? group.options : [];
     }
 
     @Mutation()
+    @ApplyIdCodec()
     async createProductOptionGroup(_, args): Promise<Translated<ProductOptionGroup>> {
         const { input } = args;
         const group = await this.productOptionGroupService.create(args.input);
@@ -49,11 +47,11 @@ export class ProductOptionResolver {
                 await this.productOptionService.create(group, option);
             }
         }
-
         return group;
     }
 
     @Mutation()
+    @ApplyIdCodec()
     async updateProductOptionGroup(_, args): Promise<Translated<ProductOptionGroup>> {
         const { input } = args;
         return this.productOptionGroupService.update(args.input);

+ 13 - 21
server/src/api/product/product.resolver.ts

@@ -3,33 +3,31 @@ import { Mutation, Query, Resolver } from '@nestjs/graphql';
 import { PaginatedList } from '../../../../shared/shared-types';
 import { Product } from '../../entity/product/product.entity';
 import { Translated } from '../../locale/locale-types';
-import { IdCodecService } from '../../service/id-codec.service';
 import { ProductVariantService } from '../../service/product-variant.service';
 import { ProductService } from '../../service/product.service';
+import { ApplyIdCodec } from '../common/apply-id-codec-decorator';
 
 @Resolver('Product')
 export class ProductResolver {
     constructor(
         private productService: ProductService,
-        private idCodecService: IdCodecService,
         private productVariantService: ProductVariantService,
     ) {}
 
     @Query('products')
+    @ApplyIdCodec()
     async products(obj, args): Promise<PaginatedList<Translated<Product>>> {
-        return this.productService
-            .findAll(args.languageCode, args.take, args.skip)
-            .then(list => this.idCodecService.encode(list));
+        return this.productService.findAll(args.languageCode, args.take, args.skip);
     }
 
     @Query('product')
+    @ApplyIdCodec()
     async product(obj, args): Promise<Translated<Product> | undefined> {
-        return this.productService
-            .findOne(this.idCodecService.decode(args).id, args.languageCode)
-            .then(p => this.idCodecService.encode(p));
+        return this.productService.findOne(args.id, args.languageCode);
     }
 
     @Mutation()
+    @ApplyIdCodec()
     async createProduct(_, args): Promise<Translated<Product>> {
         const { input } = args;
         const product = await this.productService.create(input);
@@ -40,33 +38,27 @@ export class ProductResolver {
             }
         }
 
-        return this.idCodecService.encode(product);
+        return product;
     }
 
     @Mutation()
+    @ApplyIdCodec()
     async updateProduct(_, args): Promise<Translated<Product>> {
         const { input } = args;
-        const product = await this.productService.update(this.idCodecService.decode(input));
-        return this.idCodecService.encode(product);
+        return this.productService.update(input);
     }
 
     @Mutation()
+    @ApplyIdCodec(['productId', 'optionGroupId'])
     async addOptionGroupToProduct(_, args): Promise<Translated<Product>> {
         const { productId, optionGroupId } = args;
-        const product = await this.productService.addOptionGroupToProduct(
-            this.idCodecService.decode(productId),
-            this.idCodecService.decode(optionGroupId),
-        );
-        return this.idCodecService.encode(product);
+        return this.productService.addOptionGroupToProduct(productId, optionGroupId);
     }
 
     @Mutation()
+    @ApplyIdCodec(['productId', 'optionGroupId'])
     async removeOptionGroupFromProduct(_, args): Promise<Translated<Product>> {
         const { productId, optionGroupId } = args;
-        const product = await this.productService.removeOptionGroupFromProduct(
-            this.idCodecService.decode(productId),
-            this.idCodecService.decode(optionGroupId),
-        );
-        return this.idCodecService.encode(product);
+        return this.productService.removeOptionGroupFromProduct(productId, optionGroupId);
     }
 }

+ 0 - 2
server/src/app.module.ts

@@ -18,7 +18,6 @@ import { TranslationUpdaterService } from './locale/translation-updater.service'
 import { AdministratorService } from './service/administrator.service';
 import { ConfigService } from './service/config.service';
 import { CustomerService } from './service/customer.service';
-import { IdCodecService } from './service/id-codec.service';
 import { ProductOptionGroupService } from './service/product-option-group.service';
 import { ProductOptionService } from './service/product-option.service';
 import { ProductVariantService } from './service/product-variant.service';
@@ -36,7 +35,6 @@ const connectionOptions = getConfig().dbConnectionOptions;
         ConfigService,
         JwtStrategy,
         I18nService,
-        IdCodecService,
         PasswordService,
         CustomerService,
         CustomerResolver,

+ 1 - 1
shared/shared-types.ts

@@ -8,7 +8,7 @@ export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
  * A type representing the type rather than instance of a class.
  */
 export type Type<T> = {
-    new (): T;
+    new (...args: any[]): T;
 } & Function;
 
 /**