Procházet zdrojové kódy

Implement a method of handling localized entities

Michael Bromley před 7 roky
rodič
revize
c2fb3d2ff3

+ 18 - 0
modules/core/api/product/product.resolver.ts

@@ -0,0 +1,18 @@
+import { Query, Resolver } from '@nestjs/graphql';
+import { ProductService } from './product.service';
+import { Product } from '../../entity/product/product.interface';
+
+@Resolver('Product')
+export class ProductResolver {
+    constructor(private productService: ProductService) {}
+
+    @Query('products')
+    products(obj, args): Promise<Product[]> {
+        return this.productService.findAll(args.lang);
+    }
+
+    @Query('product')
+    product(obj, args): Promise<Product> {
+        return this.productService.findOne(args.id, args.lang);
+    }
+}

+ 23 - 0
modules/core/api/product/product.service.ts

@@ -0,0 +1,23 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { Connection, createQueryBuilder } from 'typeorm';
+import { Product } from '../../entity/product/product.interface';
+import { ProductEntity } from '../../entity/product/product.entity';
+import { Translatable, Translation } from '../../locale/locale-types';
+import { LocaleService } from '../../locale/locale.service';
+import { ProductVariant } from '../../entity/product-variant/product-variant.interface';
+import { ProductVariantEntity } from '../../entity/product-variant/product-variant.entity';
+import { ProductRepository } from "../../repository/product-repository";
+
+@Injectable()
+export class ProductService {
+    constructor(@InjectConnection() private connection: Connection, private localeService: LocaleService) {}
+
+    findAll(lang?: string): Promise<Product[]> {
+        return this.connection.getCustomRepository(ProductRepository).localeFind(lang);
+    }
+
+    findOne(productId: number, lang?: string): Promise<Product> {
+        return this.connection.getCustomRepository(ProductRepository).localeFindOne(productId, lang);
+    }
+}

+ 25 - 0
modules/core/api/product/product.types.graphql

@@ -0,0 +1,25 @@
+type Query {
+  products(lang: String): [Product]
+  product(id: Int!, lang: String): Product
+}
+
+type Product {
+    id: Int
+    name: String
+    slug: String
+    description: String
+    image: String
+    variants: [ProductVariant]
+    createdAt: String
+    updatedAt: String
+}
+
+type ProductVariant {
+    id: Int
+    sku: String
+    name: String
+    image: String
+    price: Int
+    createdAt: String
+    updatedAt: String
+}

+ 1 - 1
modules/core/users/user.controller.ts → modules/core/api/user/user.controller.ts

@@ -1,6 +1,6 @@
 import { Controller, Get, Param } from '@nestjs/common';
 import { UserService } from './user.service';
-import { User } from '../entities/User';
+import { User } from '../../entity/user/user.interface';
 
 @Controller('users')
 export class UserController {

+ 5 - 5
modules/core/users/user.resolver.ts → modules/core/api/user/user.resolver.ts

@@ -1,8 +1,8 @@
-import { Resolver, Query, ResolveProperty } from '@nestjs/graphql';
-import { Connection, EntityManager, Repository } from 'typeorm';
-import { User } from '../entities/User';
-import { Address } from '../entities/Address';
+import { Query, ResolveProperty, Resolver } from '@nestjs/graphql';
+import { UserEntity } from '../../entity/user/user.entity';
 import { UserService } from './user.service';
+import { Address } from '../../entity/address/address.interface';
+import { User } from '../../entity/user/user.interface';
 
 @Resolver('User')
 export class UserResolver {
@@ -19,7 +19,7 @@ export class UserResolver {
     }
 
     @ResolveProperty('addresses')
-    addresses(user: User): Promise<Address[]> {
+    addresses(user: UserEntity): Promise<Address[]> {
         return this.userService.findAddressesByUserId(user.id);
     }
 }

+ 7 - 5
modules/core/users/user.service.ts → modules/core/api/user/user.service.ts

@@ -1,24 +1,26 @@
 import { Injectable } from '@nestjs/common';
 import { InjectConnection } from '@nestjs/typeorm';
 import { Connection } from 'typeorm';
-import { User } from '../entities/User';
-import { Address } from '../entities/Address';
+import { UserEntity } from '../../entity/user/user.entity';
+import { AddressEntity } from '../../entity/address/address.entity';
+import { User } from '../../entity/user/user.interface';
+import { Address } from '../../entity/address/address.interface';
 
 @Injectable()
 export class UserService {
     constructor(@InjectConnection() private connection: Connection) {}
 
     findAll(): Promise<User[]> {
-        return this.connection.manager.find(User);
+        return this.connection.manager.find(UserEntity);
     }
 
     findOne(userId: number): Promise<User> {
-        return this.connection.manager.findOne(User, userId);
+        return this.connection.manager.findOne(UserEntity, userId);
     }
 
     findAddressesByUserId(userId: number): Promise<Address[]> {
         return this.connection
-            .getRepository(Address)
+            .getRepository(AddressEntity)
             .createQueryBuilder('address')
             .where('address.userId = :id', { id: userId })
             .getMany();

+ 0 - 0
modules/core/users/user.types.graphql → modules/core/api/user/user.types.graphql


+ 8 - 5
modules/core/app.module.ts

@@ -3,16 +3,19 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 import { GraphQLModule } from '@nestjs/graphql';
 import { graphqlExpress, graphiqlExpress } from 'apollo-server-express';
 import { GraphQLFactory } from '@nestjs/graphql';
-import { UserService } from './users/user.service';
-import { UserController } from './users/user.controller';
-import { UserResolver } from './users/user.resolver';
+import { UserService } from './api/user/user.service';
+import { UserController } from './api/user/user.controller';
+import { UserResolver } from './api/user/user.resolver';
+import { ProductService } from './api/product/product.service';
+import { ProductResolver } from './api/product/product.resolver';
+import { LocaleService } from './locale/locale.service';
 
 @Module({
     imports: [
         GraphQLModule,
         TypeOrmModule.forRoot({
             type: 'mysql',
-            entities: ['./**/entities/*.ts'],
+            entities: ['./**/entity/**/*.entity.ts'],
             synchronize: true,
             logging: true,
             host: '192.168.99.100',
@@ -23,7 +26,7 @@ import { UserResolver } from './users/user.resolver';
         }),
     ],
     controllers: [UserController],
-    providers: [UserService, UserResolver],
+    providers: [UserService, UserResolver, ProductService, ProductResolver, LocaleService],
 })
 export class AppModule implements NestModule {
     constructor(private readonly graphQLFactory: GraphQLFactory) {}

+ 0 - 18
modules/core/entities/Product.ts

@@ -1,18 +0,0 @@
-import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
-import { ProductVariant } from './ProductVariant';
-
-@Entity()
-export class Product {
-    @PrimaryGeneratedColumn() id: number;
-
-    @Column() name: string;
-
-    @Column() slug: string;
-
-    @Column() description: string;
-
-    @Column() image: string;
-
-    @OneToMany(type => ProductVariant, variant => variant.product)
-    variants: string;
-}

+ 0 - 18
modules/core/entities/ProductVariant.ts

@@ -1,18 +0,0 @@
-import { Column, Entity, ManyToOne, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
-import { Product } from './Product';
-
-@Entity()
-export class ProductVariant {
-    @PrimaryGeneratedColumn() id: number;
-
-    @Column() sku: string;
-
-    @Column() name: string;
-
-    @Column() image: string;
-
-    @Column() price: string;
-
-    @ManyToOne(type => Product, product => product.variants)
-    product: Product[];
-}

+ 7 - 6
modules/core/entities/Address.ts → modules/core/entity/address/address.entity.ts

@@ -1,12 +1,13 @@
-import {Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn} from 'typeorm';
-import {User} from './User';
+import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import { UserEntity } from '../user/user.entity';
+import { Address } from './address.interface';
 
-@Entity()
-export class Address {
+@Entity('address')
+export class AddressEntity implements Address {
     @PrimaryGeneratedColumn() id: number;
 
-    @ManyToOne(type => User, user => user.addresses)
-    user: User;
+    @ManyToOne(type => UserEntity, user => user.addresses)
+    user: UserEntity;
 
     @Column() fullName: string;
 

+ 16 - 0
modules/core/entity/address/address.interface.ts

@@ -0,0 +1,16 @@
+export interface Address {
+    id: number;
+    fullName: string;
+    company: string;
+    streetLine1: string;
+    streetLine2: string;
+    city: string;
+    province: string;
+    postalCode: string;
+    country: string;
+    phoneNumber: string;
+    defaultShippingAddress: boolean;
+    defaultBillingAddress: boolean;
+    createdAt: string;
+    updatedAt: string;
+}

+ 16 - 0
modules/core/entity/product-variant/product-variant-translation.entity.ts

@@ -0,0 +1,16 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Translation } from '../../locale/locale-types';
+import { ProductVariant } from './product-variant.interface';
+import { ProductVariantEntity } from './product-variant.entity';
+
+@Entity('product_variant_translation')
+export class ProductVariantTranslationEntity implements Translation<ProductVariant> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() languageCode: string;
+
+    @Column() name: string;
+
+    @ManyToOne(type => ProductVariantEntity, base => base.translations)
+    base: ProductVariantEntity;
+}

+ 34 - 0
modules/core/entity/product-variant/product-variant.entity.ts

@@ -0,0 +1,34 @@
+import { Translatable } from '../../locale/locale-types';
+import { ProductVariant } from './product-variant.interface';
+import {
+    Column,
+    CreateDateColumn,
+    Entity,
+    ManyToOne,
+    OneToMany,
+    PrimaryGeneratedColumn,
+    UpdateDateColumn,
+} from 'typeorm';
+import { ProductEntity } from '../product/product.entity';
+import { ProductVariantTranslationEntity } from './product-variant-translation.entity';
+
+@Entity('product_variant')
+export class ProductVariantEntity implements Translatable<ProductVariant> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() sku: string;
+
+    @Column() image: string;
+
+    @Column() price: string;
+
+    @CreateDateColumn() createdAt: string;
+
+    @UpdateDateColumn() updatedAt: string;
+
+    @OneToMany(type => ProductVariantTranslationEntity, translation => translation.base)
+    translations: ProductVariantTranslationEntity[];
+
+    @ManyToOne(type => ProductEntity, product => product.variants)
+    product: ProductEntity[];
+}

+ 11 - 0
modules/core/entity/product-variant/product-variant.interface.ts

@@ -0,0 +1,11 @@
+import { LocaleString } from '../../locale/locale-types';
+
+export class ProductVariant {
+    id: number;
+    sku: string;
+    name: LocaleString;
+    image: string;
+    price: string;
+    createdAt: string;
+    updatedAt: string;
+}

+ 20 - 0
modules/core/entity/product/product-translation.entity.ts

@@ -0,0 +1,20 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Translation } from '../../locale/locale-types';
+import { Product } from './product.interface';
+import { ProductEntity } from './product.entity';
+
+@Entity('product_translation')
+export class ProductTranslationEntity implements Translation<Product> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() languageCode: string;
+
+    @Column() name: string;
+
+    @Column() slug: string;
+
+    @Column() description: string;
+
+    @ManyToOne(type => ProductEntity, base => base.translations)
+    base: ProductEntity;
+}

+ 23 - 0
modules/core/entity/product/product.entity.ts

@@ -0,0 +1,23 @@
+import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import { Translatable } from '../../locale/locale-types';
+import { Product } from './product.interface';
+import { ProductTranslationEntity } from './product-translation.entity';
+import { ProductVariantEntity } from '../product-variant/product-variant.entity';
+import { ProductVariant } from '../product-variant/product-variant.interface';
+
+@Entity('product')
+export class ProductEntity implements Translatable<Product> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() image: string;
+
+    @CreateDateColumn() createdAt: string;
+
+    @UpdateDateColumn() updatedAt: string;
+
+    @OneToMany(type => ProductTranslationEntity, translation => translation.base)
+    translations: ProductTranslationEntity[];
+
+    @OneToMany(type => ProductVariantEntity, variant => variant.product)
+    variants: ProductVariant[];
+}

+ 13 - 0
modules/core/entity/product/product.interface.ts

@@ -0,0 +1,13 @@
+import { LocaleString } from '../../locale/locale-types';
+import { ProductVariant } from '../product-variant/product-variant.interface';
+
+export interface Product {
+    id: number;
+    name: LocaleString;
+    slug: LocaleString;
+    description: LocaleString;
+    image: string;
+    variants: ProductVariant[];
+    createdAt: string;
+    updatedAt: string;
+}

+ 6 - 5
modules/core/entities/User.ts → modules/core/entity/user/user.entity.ts

@@ -1,8 +1,9 @@
 import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
-import { Address } from './Address';
+import { AddressEntity } from '../address/address.entity';
+import { User } from './user.interface';
 
-@Entity()
-export class User {
+@Entity('user')
+export class UserEntity implements User {
     @PrimaryGeneratedColumn() id: number;
 
     @Column() firstName: string;
@@ -13,8 +14,8 @@ export class User {
 
     @Column() emailAddress: string;
 
-    @OneToMany(type => Address, address => address.user)
-    addresses: Address[];
+    @OneToMany(type => AddressEntity, address => address.user)
+    addresses: AddressEntity[];
 
     @CreateDateColumn() createdAt: string;
 

+ 12 - 0
modules/core/entity/user/user.interface.ts

@@ -0,0 +1,12 @@
+import { Address } from '../address/address.interface';
+
+export class User {
+    id: number;
+    firstName: string;
+    lastName: string;
+    phoneNumber: string;
+    emailAddress: string;
+    addresses: Address[];
+    createdAt: string;
+    updatedAt: string;
+}

+ 32 - 0
modules/core/locale/locale-types.ts

@@ -0,0 +1,32 @@
+/**
+ * This type should be used in any interfaces where the value is to be
+ * localized into different languages.
+ */
+export type LocaleString = string & { _opaqueType: 'LocaleString' };
+
+export type TranslatableKeys<T> = { [K in keyof T]: T[K] extends LocaleString ? K : never }[keyof T];
+
+export type NonTranslateableKeys<T> = { [K in keyof T]: T[K] extends LocaleString ? never : K }[keyof T];
+
+/**
+ * Entities which have localizable string properties should implement this type.
+ */
+export type Translatable<T> =
+    // Translatable must include all non-translatable keys of the interface
+    { [K in NonTranslateableKeys<T>]: T[K] } &
+        // Translatable must not include any translatable keys (these are instead handled by the Translation)
+        {
+            [K in TranslatableKeys<T>]?: never
+        } & // Translatable must include a reference to all translations of the translatable keys
+        { translations: Translation<T>[] };
+
+/**
+ * Translations of localizable entities should implement this type.
+ */
+export type Translation<T> =
+    // Translation must include the languageCode and a reference to the base Translatable entity it is associated with
+    {
+        languageCode: string;
+        base: Translatable<T>;
+    } & // Translation must include all translatable keys as a string type
+    { [K in TranslatableKeys<T>]: string };

+ 28 - 0
modules/core/locale/locale.service.ts

@@ -0,0 +1,28 @@
+import { Injectable } from '@nestjs/common';
+import { Translatable } from './locale-types';
+
+@Injectable()
+export class LocaleService {
+
+    translate<T>(translatable: Translatable<T>): T {
+        return translate(translatable);
+    }
+}
+
+/**
+ * Converts a Translatable entity into the public-facing entity by unwrapping
+ * the translated strings from the first of the Translation entities.
+ */
+export function translate<T>(translatable: Translatable<T>): T {
+    const translation = translatable.translations[0];
+
+    const translated = { ...(translatable as any) };
+    delete translated.translations;
+
+    for (const [key, value] of Object.entries(translation)) {
+        if (key !== 'languageCode' && key !== 'id') {
+            translated[key] = value;
+        }
+    }
+    return translated;
+}

+ 6 - 6
modules/core/main.hmr.ts

@@ -4,12 +4,12 @@ import { AppModule } from './app.module';
 declare const module: any;
 
 async function bootstrap() {
-  const app = await NestFactory.create(AppModule);
-  await app.listen(3000);
+    const app = await NestFactory.create(AppModule);
+    await app.listen(3000);
 
-  if (module.hot) {
-    module.hot.accept();
-    module.hot.dispose(() => app.close());
-  }
+    if (module.hot) {
+        module.hot.accept();
+        module.hot.dispose(() => app.close());
+    }
 }
 bootstrap();

+ 50 - 0
modules/core/repository/product-repository.ts

@@ -0,0 +1,50 @@
+import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm";
+import { ProductEntity } from "../entity/product/product.entity";
+import { Product } from "../entity/product/product.interface";
+import { translate } from "../locale/locale.service";
+import { ProductVariant } from "../entity/product-variant/product-variant.interface";
+
+@EntityRepository(ProductEntity)
+export class ProductRepository extends Repository<ProductEntity> {
+
+    /**
+     * Returns an array of Products including ProductVariants, translated into the
+     * specified language.
+     */
+    localeFind(languageCode: string): Promise<Product[]> {
+        return this.getProductQueryBuilder(languageCode)
+            .getMany()
+            .then(result => this.translateProductAndVariants(result));
+    }
+
+    /**
+     * Returns single Product including ProductVariants, translated into the
+     * specified language.
+     */
+    localeFindOne(id: number, languageCode: string): Promise<Product> {
+        return this.getProductQueryBuilder(languageCode)
+            .andWhere('ProductEntity.id = :id', { id })
+            .getMany()
+            .then(result => this.translateProductAndVariants(result))
+            .then(res => res[0]);
+    }
+
+    private getProductQueryBuilder(languageCode: string): SelectQueryBuilder<ProductEntity> {
+        const code = languageCode || 'en';
+
+        return this.manager.createQueryBuilder(ProductEntity, 'product')
+            .leftJoinAndSelect('product.variants', 'variant')
+            .leftJoinAndSelect('product.translations', 'product_translation')
+            .leftJoinAndSelect('variant.translations', 'product_variant_translation')
+            .where('product_translation.languageCode = :code', { code })
+            .andWhere('product_variant_translation.languageCode = :code', { code })
+    }
+
+    private translateProductAndVariants(result: ProductEntity[]): Product[] {
+        return result.map(productEntity => {
+            const product = translate<Product>(productEntity);
+            product.variants = product.variants.map(variant => translate<ProductVariant>(variant as any));
+            return product;
+        });
+    };
+}

+ 92 - 6
modules/testing/populate.ts

@@ -1,17 +1,102 @@
-import { Connection } from 'typeorm';
+import { Connection, createConnection } from 'typeorm';
 import * as faker from 'faker/locale/en_GB';
-import { User } from '../core/entities/User';
-import { Address } from '../core/entities/Address';
+import { UserEntity } from '../core/entity/user/user.entity';
+import { AddressEntity } from '../core/entity/address/address.entity';
+import { ProductEntity } from '../core/entity/product/product.entity';
+import { ProductTranslationEntity } from '../core/entity/product/product-translation.entity';
+import { ProductVariantTranslationEntity } from '../core/entity/product-variant/product-variant-translation.entity';
+import { ProductVariantEntity } from '../core/entity/product-variant/product-variant.entity';
 
-export async function populate(connection: Connection) {
+populate();
+
+export async function populate() {
+    const connection = await createConnection({
+        type: 'mysql',
+        entities: ['./**/entity/**/*.entity.ts'],
+        synchronize: true,
+        logging: false,
+        host: '192.168.99.100',
+        port: 3306,
+        username: 'root',
+        password: '',
+        database: 'test',
+    });
+
+    await populateUsersAndAddresses(connection);
+    await populateProducts(connection);
+}
+
+async function populateProducts(connection: Connection) {
+    for (let i = 0; i < 5; i++) {
+        const product = new ProductEntity();
+        product.image = faker.image.imageUrl();
+
+        const name = faker.commerce.productName();
+        const slug = name.toLowerCase().replace(/\s+/g, '-');
+        const description = faker.lorem.sentence();
+
+        const translation1 = makeProductTranslation('en', name, slug, description);
+        const translation2 = makeProductTranslation('de', name, slug, description);
+        await connection.manager.save(translation1);
+        await connection.manager.save(translation2);
+
+        // 1 - 4 variants
+        const variantCount = Math.floor(Math.random() * 4) + 1;
+        let variants = [];
+        for (let j = 0; j < variantCount; j++) {
+            const variant = new ProductVariantEntity();
+            const variantName = `${name} variant ${j + 1}`;
+            variant.image = faker.image.imageUrl();
+            variant.price = faker.commerce.price(100, 12000, 0);
+
+            const variantTranslation1 = makeProductVariantTranslation('en', variantName);
+            const variantTranslation2 = makeProductVariantTranslation('de', variantName);
+            await connection.manager.save(variantTranslation1);
+            await connection.manager.save(variantTranslation2);
+
+            variant.translations = [variantTranslation1, variantTranslation2];
+            await connection.manager.save(variant);
+            console.log(`${j + 1}. created product variant ${variantName}`);
+            variants.push(variant);
+        }
+
+        product.variants = variants;
+        product.translations = [translation1, translation2];
+        await connection.manager.save(product);
+        console.log(`${i + 1}. created product & translations for ${translation1.name}`);
+    }
+}
+
+function makeProductTranslation(
+    langCode: string,
+    name: string,
+    slug: string,
+    description: string,
+): ProductTranslationEntity {
+    const productTranslation = new ProductTranslationEntity();
+    productTranslation.languageCode = langCode;
+    productTranslation.name = `${langCode} ${name}`;
+    productTranslation.slug = `${langCode} ${slug}`;
+    productTranslation.description = `${langCode} ${description}`;
+    return productTranslation;
+}
+
+function makeProductVariantTranslation(langCode: string, name: string): ProductVariantTranslationEntity {
+    const productVariantTranslation = new ProductVariantTranslationEntity();
+    productVariantTranslation.languageCode = langCode;
+    productVariantTranslation.name = `${langCode} ${name}`;;
+    return productVariantTranslation;
+}
+
+async function populateUsersAndAddresses(connection: Connection) {
     for (let i = 0; i < 5; i++) {
-        const user = new User();
+        const user = new UserEntity();
         user.firstName = faker.name.firstName();
         user.lastName = faker.name.lastName();
         user.emailAddress = faker.internet.email(user.firstName, user.lastName);
         user.phoneNumber = faker.phone.phoneNumber();
 
-        const address = new Address();
+        const address = new AddressEntity();
         address.fullName = `${user.firstName} ${user.lastName}`;
         address.streetLine1 = faker.address.streetAddress();
         address.city = faker.address.city();
@@ -23,5 +108,6 @@ export async function populate(connection: Connection) {
 
         user.addresses = [address];
         await connection.manager.save(user);
+        console.log('created user and address for ' + user.firstName + ' ' + user.lastName);
     }
 }

+ 2 - 2
nodemon-debug.json

@@ -1,6 +1,6 @@
 {
-  "watch": ["src"],
+  "watch": ["modules"],
   "ext": "ts",
   "ignore": ["src/**/*.spec.ts"],
-  "exec": "node --inspect-brk -r ts-node/register src/main.ts"
+  "exec": "node --inspect=5858 -r ts-node/register modules/core/main.ts"
 }

+ 1 - 0
package.json

@@ -5,6 +5,7 @@
   "license": "MIT",
   "scripts": {
     "format": "prettier --write \"modules/**/*.ts\"",
+    "populate": "ts-node -r tsconfig-paths/register modules/testing/populate.ts",
     "start": "ts-node -r tsconfig-paths/register src/main.ts",
     "start:dev": "nodemon",
     "start:debug": "nodemon --config nodemon-debug.json",

+ 1 - 1
tsconfig.json

@@ -5,7 +5,7 @@
     "noImplicitAny": false,
     "removeComments": true,
     "noLib": false,
-    "lib": ["es2015"],
+    "lib": ["es2017"],
     "allowSyntheticDefaultImports": true,
     "emitDecoratorMetadata": true,
     "experimentalDecorators": true,