From ac8d80920120a878a22d45612b36d80a2352f486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20L=C3=B3pez=20Mendoza?= Date: Tue, 27 Feb 2018 15:24:04 +0100 Subject: [PATCH 01/39] Add config command in setup --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13250bce..af7e6ab6 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "npm start test", "build": "npm start build", "presetup": "yarn install", - "setup": "npm start setup.db" + "setup": "npm start config && npm start setup.script" }, "engines": { "node": ">=8.0.0" From 8afd098cfefdc6cf9fb09ae9a438b6a59800c50c Mon Sep 17 00:00:00 2001 From: Eric Thomas Date: Thu, 8 Mar 2018 13:10:17 -0700 Subject: [PATCH 02/39] Have repo mock return promises to match typeorm more accurately. Fixes #67 --- test/unit/lib/RepositoryMock.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/unit/lib/RepositoryMock.ts b/test/unit/lib/RepositoryMock.ts index 90f72625..5ae85a33 100644 --- a/test/unit/lib/RepositoryMock.ts +++ b/test/unit/lib/RepositoryMock.ts @@ -8,24 +8,24 @@ export class RepositoryMock { public saveMock = jest.fn(); public deleteMock = jest.fn(); - public find(...args: any[]): T[] { + public find(...args: any[]): Promise { this.findMock(args); - return this.list; + return Promise.resolve(this.list); } - public findOne(...args: any[]): T { + public findOne(...args: any[]): Promise { this.findOneMock(args); - return this.one; + return Promise.resolve(this.one); } - public save(value: T, ...args: any[]): T { + public save(value: T, ...args: any[]): Promise { this.saveMock(value, args); - return value; + return Promise.resolve(value); } - public delete(value: T, ...args: any[]): T { + public delete(value: T, ...args: any[]): Promise { this.deleteMock(value, args); - return value; + return Promise.resolve(value); } } From e5748aa6f33fe60dc2256f6a359ea078cf56043d Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 10 Mar 2018 11:41:02 +0100 Subject: [PATCH 03/39] rewrite seed functionality --- package-scripts.js | 2 +- src/database/factories/PetFactory.ts | 6 +- src/database/factories/UserFactory.ts | 7 +- src/database/seeds/CreateBruce.ts | 31 +++- src/database/seeds/CreatePets.ts | 30 +-- src/database/seeds/CreateUsers.ts | 18 +- src/lib/seed/EntityFactory.ts | 93 ++++++++++ src/lib/seed/cli.ts | 98 ++++++++++ src/lib/{seeds => seed}/connection.ts | 0 src/lib/seed/importer.ts | 48 +++++ src/lib/seed/index.ts | 68 +++++++ src/lib/seed/make.ts | 31 ++++ src/lib/seed/types.ts | 21 +++ src/lib/seed/utils.ts | 7 + src/lib/seeds/BluePrint.ts | 11 -- src/lib/seeds/EntityFactory.ts | 91 --------- src/lib/seeds/EntityFactoryInterface.ts | 23 --- src/lib/seeds/Factory.ts | 58 ------ src/lib/seeds/FactoryInterface.ts | 33 ---- src/lib/seeds/SeedsInterface.ts | 15 -- src/lib/seeds/cli.ts | 87 --------- src/lib/seeds/index.ts | 24 --- src/lib/seeds/utils.ts | 13 -- test/e2e/api/users.test.ts | 31 ++-- test/e2e/utils/server.ts | 12 ++ yarn.lock | 233 +++++++++++++----------- 26 files changed, 578 insertions(+), 513 deletions(-) create mode 100644 src/lib/seed/EntityFactory.ts create mode 100644 src/lib/seed/cli.ts rename src/lib/{seeds => seed}/connection.ts (100%) create mode 100644 src/lib/seed/importer.ts create mode 100644 src/lib/seed/index.ts create mode 100644 src/lib/seed/make.ts create mode 100644 src/lib/seed/types.ts create mode 100644 src/lib/seed/utils.ts delete mode 100644 src/lib/seeds/BluePrint.ts delete mode 100644 src/lib/seeds/EntityFactory.ts delete mode 100644 src/lib/seeds/EntityFactoryInterface.ts delete mode 100644 src/lib/seeds/Factory.ts delete mode 100644 src/lib/seeds/FactoryInterface.ts delete mode 100644 src/lib/seeds/SeedsInterface.ts delete mode 100644 src/lib/seeds/cli.ts delete mode 100644 src/lib/seeds/index.ts delete mode 100644 src/lib/seeds/utils.ts create mode 100644 test/e2e/utils/server.ts diff --git a/package-scripts.js b/package-scripts.js index 46872f86..2e36080d 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -140,7 +140,7 @@ module.exports = { script: series( 'nps banner.seed', 'nps config', - runFast('./src/lib/seeds/cli.ts') + runFast('./src/lib/seed/cli.ts') ), description: 'Seeds generated records into the database' }, diff --git a/src/database/factories/PetFactory.ts b/src/database/factories/PetFactory.ts index 7679d099..b917e050 100644 --- a/src/database/factories/PetFactory.ts +++ b/src/database/factories/PetFactory.ts @@ -1,11 +1,9 @@ import * as Faker from 'faker'; import { Pet } from '../../../src/api/models/Pet'; -import { Factory } from '../../lib/seeds'; +import { define } from '../../lib/seed'; -const factory = Factory.getInstance(); - -factory.define(Pet, (faker: typeof Faker) => { +define(Pet, (faker: typeof Faker) => { const gender = faker.random.number(1); const name = faker.name.firstName(gender); diff --git a/src/database/factories/UserFactory.ts b/src/database/factories/UserFactory.ts index bdd8aa42..e8dd0add 100644 --- a/src/database/factories/UserFactory.ts +++ b/src/database/factories/UserFactory.ts @@ -1,11 +1,10 @@ import * as Faker from 'faker'; import { User } from '../../../src/api/models/User'; -import { Factory } from '../../lib/seeds'; +import { define } from '../../lib/seed'; -const factory = Factory.getInstance(); - -factory.define(User, (faker: typeof Faker) => { +define(User, (faker: typeof Faker, settings: { role: string }) => { + console.log('UserFactory', settings.role); const gender = faker.random.number(1); const firstName = faker.name.firstName(gender); const lastName = faker.name.lastName(gender); diff --git a/src/database/seeds/CreateBruce.ts b/src/database/seeds/CreateBruce.ts index 5a42afa5..4a3439d0 100644 --- a/src/database/seeds/CreateBruce.ts +++ b/src/database/seeds/CreateBruce.ts @@ -1,10 +1,33 @@ +import { Connection } from 'typeorm'; + import { User } from '../../../src/api/models/User'; -import { FactoryInterface, SeedsInterface } from '../../lib/seeds'; +import { Factory, Seed } from '../../lib/seed/types'; + +export class CreateBruce implements Seed { + + public async seed(factory: Factory, connection: Connection): Promise { + console.log('CreateBruce'); + + // const userFactory = factory(User as any); + // const adminUserFactory = userFactory({ role: 'admin' }); + + // const bruce = await adminUserFactory.make(); + // console.log(bruce); + + // const bruce2 = await adminUserFactory.seed(); + // console.log(bruce2); + + // const bruce3 = await adminUserFactory + // .map(async (e: User) => { + // e.firstName = 'Bruce'; + // return e; + // }) + // .seed(); + // console.log(bruce3); -export class CreateBruce implements SeedsInterface { + // return bruce; - public async seed(factory: FactoryInterface): Promise { - const connection = await factory.getConnection(); + // const connection = await factory.getConnection(); const em = connection.createEntityManager(); const user = new User(); diff --git a/src/database/seeds/CreatePets.ts b/src/database/seeds/CreatePets.ts index 69071654..823b87ab 100644 --- a/src/database/seeds/CreatePets.ts +++ b/src/database/seeds/CreatePets.ts @@ -1,19 +1,23 @@ -import { Pet } from '../../../src/api/models/Pet'; -import { User } from '../../../src/api/models/User'; -import { FactoryInterface, SeedsInterface, times } from '../../lib/seeds'; +import { Connection } from 'typeorm'; -export class CreatePets implements SeedsInterface { +import { Factory, Seed } from '../../lib/seed/types'; - public async seed(factory: FactoryInterface): Promise { - const connection = await factory.getConnection(); - const em = connection.createEntityManager(); +// import { Pet } from '../../../src/api/models/Pet'; +// import { User } from '../../../src/api/models/User'; +export class CreatePets implements Seed { - await times(10, async (n) => { - const pet = await factory.get(Pet).create(); - const user = await factory.get(User).make(); - user.pets = [pet]; - await em.save(user); - }); + public async seed(factory: Factory, connection: Connection): Promise { + console.log('CreatePets'); + + // const connection = await factory.getConnection(); + // const em = connection.createEntityManager(); + + // await times(10, async (n) => { + // const pet = await factory.get(Pet).create(); + // const user = await factory.get(User).make(); + // user.pets = [pet]; + // await em.save(user); + // }); } } diff --git a/src/database/seeds/CreateUsers.ts b/src/database/seeds/CreateUsers.ts index 0d292e95..a50097b1 100644 --- a/src/database/seeds/CreateUsers.ts +++ b/src/database/seeds/CreateUsers.ts @@ -1,12 +1,16 @@ -import { User } from '../../../src/api/models/User'; -import { FactoryInterface, SeedsInterface } from '../../lib/seeds'; +import { Connection } from 'typeorm/connection/Connection'; -export class CreateUsers implements SeedsInterface { +import { Factory, Seed } from '../../lib/seed/types'; - public async seed(factory: FactoryInterface): Promise { - await factory - .get(User) - .createMany(10); +// import { User } from '../../../src/api/models/User'; +export class CreateUsers implements Seed { + + public async seed(factory: Factory, connection: Connection): Promise { + console.log('CreateUsers'); + + // await factory + // .get(User) + // .createMany(10); } } diff --git a/src/lib/seed/EntityFactory.ts b/src/lib/seed/EntityFactory.ts new file mode 100644 index 00000000..9afb234b --- /dev/null +++ b/src/lib/seed/EntityFactory.ts @@ -0,0 +1,93 @@ +import * as Faker from 'faker'; +import { Connection } from 'typeorm/connection/Connection'; + +import { FactoryFunction } from './types'; +import { isPromiseLike } from './utils'; + +export class EntityFactory { + + private mapFunction: (entity: Entity) => Promise; + + constructor( + public name: string, + public entity: Entity, + private factory: FactoryFunction, + private settings?: Settings + ) { } + + // ------------------------------------------------------------------------- + // Public API + // ------------------------------------------------------------------------- + + public map(mapFunction: (entity: Entity) => Promise): EntityFactory { + this.mapFunction = mapFunction; + return this; + } + + public async make(): Promise { + if (this.factory) { + let entity = await this.resolveEntity(this.factory(Faker, this.settings)); + if (this.mapFunction) { + entity = await this.mapFunction(entity); + } + return entity; + } + throw new Error('Could not found entity'); + } + + public async seed(): Promise { + const connection: Connection = (global as any).seeder.connection; + if (connection) { + const em = connection.createEntityManager(); + try { + const entity = await this.make(); + return await em.save(this.entity, entity); + } catch (error) { + throw new Error('Could not save entity'); + } + } else { + throw new Error('No db connection is given'); + } + } + + public async makeMany(amount: number): Promise { + const list = []; + for (let index = 0; index < amount; index++) { + list[index] = await this.make(); + } + return list; + } + + public async seedMany(amount: number): Promise { + const list = []; + for (let index = 0; index < amount; index++) { + list[index] = await this.seed(); + } + return list; + } + + // ------------------------------------------------------------------------- + // Prrivat Helpers + // ------------------------------------------------------------------------- + + private async resolveEntity(entity: Entity): Promise { + for (const attribute in entity) { + if (entity.hasOwnProperty(attribute)) { + if (isPromiseLike(entity[attribute])) { + entity[attribute] = await entity[attribute]; + } + + if (typeof entity[attribute] === 'object') { + const subEntityFactory = entity[attribute]; + try { + entity[attribute] = await (subEntityFactory as any).make(); + } catch (e) { + throw new Error(`Could not make ${(subEntityFactory as any).name}`); + } + } + } + } + return entity; + } + +} diff --git a/src/lib/seed/cli.ts b/src/lib/seed/cli.ts new file mode 100644 index 00000000..d5961de9 --- /dev/null +++ b/src/lib/seed/cli.ts @@ -0,0 +1,98 @@ +import * as Chalk from 'chalk'; +import * as commander from 'commander'; +import * as path from 'path'; + +import { loadEntityFactories } from './'; +import { getConnection } from './connection'; +import { loadSeeds } from './importer'; +import { runSeeder, setConnection } from './index'; +import { SeedConstructor } from './types'; + +// Cli helper +commander + .version('0.0.0') + .description('Run database seeds of your project') + .option('-L, --logging', 'enable sql query logging') + .option('--factories ', 'add filepath for your factories') + .option('--seeds ', 'add filepath for your seeds') + .option('--list ', 'list seeds to seed', (val) => val.split(',')) + .option('--config ', 'add filepath to your database config (must be a json)') + .parse(process.argv); + +// Get cli parameter for a different factory path +const factoryPath = (commander.factories) + ? commander.factories + : 'src/database/'; + +// Get cli parameter for a different seeds path +const seedsPath = (commander.seeds) + ? commander.seeds + : 'src/database/seeds/'; + +// Get a list of seeds +const listOfSeeds = (commander.list) + ? commander.list.map(l => l.trim()).filter(l => l.length > 0) + : []; + +// Search for seeds and factories +const run = async () => { + const log = console.log; + const chalk = Chalk.default; + + let factoryFiles; + let seedFiles; + try { + factoryFiles = await loadEntityFactories(factoryPath); + seedFiles = await loadSeeds(seedsPath); + } catch (error) { + return handleError(error); + } + + // Filter seeds + if (listOfSeeds.length > 0) { + seedFiles = seedFiles.filter(sf => listOfSeeds.indexOf(path.basename(sf).replace('.ts', '')) >= 0); + } + + // Status logging to print out the amount of factories and seeds. + log(chalk.bold('seeds')); + log('🔎 ', chalk.gray.underline(`found:`), + chalk.blue.bold(`${factoryFiles.length} factories`, chalk.gray('&'), chalk.blue.bold(`${seedFiles.length} seeds`))); + + // Get database connection and pass it to the seeder + let connection; + try { + connection = await getConnection(); + setConnection(connection); + } catch (error) { + return handleError(error); + } + + // Show seeds in the console + for (const seedFile of seedFiles) { + try { + let className = seedFile.split('/')[seedFile.split('/').length - 1]; + className = className.replace('.ts', '').replace('.js', ''); + className = className.split('-')[className.split('-').length - 1]; + log('\n' + chalk.gray.underline(`executing seed: `), chalk.green.bold(`${className}`)); + const seedFileObject: any = require(seedFile); + await runSeed(seedFileObject[className]); + } catch (error) { + console.error('Could not run seed ', error); + process.exit(1); + } + } + + log('\n👍 ', chalk.gray.underline(`finished seeding`)); + process.exit(0); +}; + +const handleError = (error) => { + console.error(error); + process.exit(1); +}; + +const runSeed = async (seedClass: SeedConstructor): Promise => { + await runSeeder(seedClass); +}; + +run(); diff --git a/src/lib/seeds/connection.ts b/src/lib/seed/connection.ts similarity index 100% rename from src/lib/seeds/connection.ts rename to src/lib/seed/connection.ts diff --git a/src/lib/seed/importer.ts b/src/lib/seed/importer.ts new file mode 100644 index 00000000..a1f186d4 --- /dev/null +++ b/src/lib/seed/importer.ts @@ -0,0 +1,48 @@ +import * as glob from 'glob'; +import * as path from 'path'; + +// ------------------------------------------------------------------------- +// Util functions +// ------------------------------------------------------------------------- + +const importFactories = (files: string[]) => files.forEach(require); + +const loadFiles = + (filePattern: string) => + (pathToFolder: string) => + (successFn: (files: string[]) => void) => + (failedFn: (error: any) => void) => { + glob(path.join(process.cwd(), pathToFolder, filePattern), (error: any, files: string[]) => { + if (error) { + return failedFn(error); + } + successFn(files); + }); + // error + // ? failedFn(error) + // : successFn(files)); + }; + +const loadFactoryFiles = loadFiles('**/*Factory{.js,.ts}'); + +// ------------------------------------------------------------------------- +// Facade functions +// ------------------------------------------------------------------------- + +export const loadEntityFactories = (pathToFolder: string): Promise => { + return new Promise((resolve, reject) => { + loadFactoryFiles(pathToFolder)(files => { + importFactories(files); + resolve(files); + })(reject); + }); +}; + +export const loadSeeds = (pathToFolder: string): Promise => { + return new Promise((resolve, reject) => { + loadFiles('**/*{.js,.ts}')(pathToFolder)(files => { + // TODO: load seeds + resolve(files); + })(reject); + }); +}; diff --git a/src/lib/seed/index.ts b/src/lib/seed/index.ts new file mode 100644 index 00000000..8aa45f88 --- /dev/null +++ b/src/lib/seed/index.ts @@ -0,0 +1,68 @@ +import 'reflect-metadata'; +import { Connection, ObjectType } from 'typeorm'; + +import { EntityFactory } from './EntityFactory'; +import { EntityFactoryDefinition, FactoryFunction, SeedConstructor } from './types'; +import { getNameOfClass } from './utils'; + +// ------------------------------------------------------------------------- +// Handy Exports +// ------------------------------------------------------------------------- + +export * from './importer'; + +// ------------------------------------------------------------------------- +// Types & Variables +// ------------------------------------------------------------------------- + +(global as any).seeder = { + connection: undefined, + entityFactories: new Map>(), +}; + +// ------------------------------------------------------------------------- +// Util functions +// ------------------------------------------------------------------------- + +// ------------------------------------------------------------------------- +// Facade functions +// ------------------------------------------------------------------------- + +export const setConnection = (connection: Connection) => (global as any).seeder.connection = connection; + +export const getConnection = () => (global as any).seeder.connection; + +export const define = (entity: ObjectType, factoryFn: FactoryFunction) => { + (global as any).seeder.entityFactories.set(getNameOfClass(entity), { entity, factory: factoryFn }); +}; + +export const factory = (entity: any) => (settings?: Settings) => { + const name = getNameOfClass(entity); + const entityFactoryObject = (global as any).seeder.entityFactories.get(name); + return new EntityFactory( + name, + entity, + entityFactoryObject.factory, + settings + ); +}; + +export const seed = async (entityFactory: EntityFactory): Promise => { + const connection: Connection = (global as any).seeder.connection; + if (connection) { + const em = connection.createEntityManager(); + try { + const entity = await entityFactory.make(); + return await em.save(entityFactory.entity, entity); + } catch (error) { + throw new Error('Could not save entity'); + } + } else { + throw new Error('No db connection is given'); + } +}; + +export const runSeeder = async (seederConstructor: SeedConstructor): Promise => { + const seeder = new seederConstructor(); + return seeder.seed(factory, getConnection()); +}; diff --git a/src/lib/seed/make.ts b/src/lib/seed/make.ts new file mode 100644 index 00000000..9f66e96f --- /dev/null +++ b/src/lib/seed/make.ts @@ -0,0 +1,31 @@ +import * as Faker from 'faker'; + +import { EntityFactoryDefinition } from './types'; +import { isPromiseLike } from './utils'; + +const resolveEntity = async (entity: E): Promise => { + for (const attribute in entity) { + if (entity.hasOwnProperty(attribute)) { + if (isPromiseLike(entity[attribute])) { + entity[attribute] = await entity[attribute]; + } + + if (typeof entity[attribute] === 'object') { + const subEntityFactory = entity[attribute]; + try { + entity[attribute] = await (subEntityFactory as any).make(); + } catch (e) { + throw new Error(`Could not make ${(subEntityFactory as any).name}`); + } + } + } + } + return entity; +}; + +export const make = (entityFactoryDefinition: EntityFactoryDefinition) => (settings?: Settings) => () => { + if (entityFactoryDefinition) { + return resolveEntity(entityFactoryDefinition.factory(Faker, settings)); + } + throw new Error('Could not found entity'); +}; diff --git a/src/lib/seed/types.ts b/src/lib/seed/types.ts new file mode 100644 index 00000000..93768e7d --- /dev/null +++ b/src/lib/seed/types.ts @@ -0,0 +1,21 @@ +import * as Faker from 'faker'; +import { Connection, ObjectType } from 'typeorm'; + +import { EntityFactory } from './EntityFactory'; + +export type FactoryFunction = (faker: typeof Faker, settings?: Settings) => Entity; + +export type Factory = (entity: Entity) => (settings?: Settings) => EntityFactory; + +export interface EntityFactoryDefinition { + entity: ObjectType; + factory: FactoryFunction; +} + +export interface Seed { + seed(factory: Factory, connection: Connection): Promise; +} + +export interface SeedConstructor { + new(): Seed; +} diff --git a/src/lib/seed/utils.ts b/src/lib/seed/utils.ts new file mode 100644 index 00000000..c5908d22 --- /dev/null +++ b/src/lib/seed/utils.ts @@ -0,0 +1,7 @@ +// ------------------------------------------------------------------------- +// Util functions +// ------------------------------------------------------------------------- + +export const getNameOfClass = (c: any): string => new c().constructor.name; + +export const isPromiseLike = (o: any): boolean => !!o && (typeof o === 'object' || typeof o === 'function') && typeof o.then === 'function'; diff --git a/src/lib/seeds/BluePrint.ts b/src/lib/seeds/BluePrint.ts deleted file mode 100644 index 4fc496b5..00000000 --- a/src/lib/seeds/BluePrint.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as Faker from 'faker'; -import { ObjectType } from 'typeorm'; - -/** - * BluePrint has the factory function for the given EntityClass - */ -export class BluePrint { - constructor( - public EntityClass: ObjectType, - public create: (faker: typeof Faker, args: any[]) => Entity) { } -} diff --git a/src/lib/seeds/EntityFactory.ts b/src/lib/seeds/EntityFactory.ts deleted file mode 100644 index 66a852b2..00000000 --- a/src/lib/seeds/EntityFactory.ts +++ /dev/null @@ -1,91 +0,0 @@ -import * as Faker from 'faker'; -import { Connection } from 'typeorm/connection/Connection'; - -import { BluePrint } from './BluePrint'; -import { EntityFactoryInterface } from './EntityFactoryInterface'; - -export class EntityFactory implements EntityFactoryInterface { - - private identifier = 'id'; - private eachFn: (obj: any, faker: typeof Faker) => Promise; - - constructor( - private faker: typeof Faker, - private connection: Connection, - private blueprint: BluePrint, - private args: any[]) { } - - public each(iterator: (entity: Entity, faker: typeof Faker) => Promise): EntityFactory { - this.eachFn = iterator; - return this; - } - - public async make(): Promise { - return await this.makeEntity(this.blueprint.create(this.faker, this.args)); - } - - public async makeMany(amount: number): Promise { - const results: Entity[] = []; - for (let i = 0; i < amount; i++) { - const entity = await this.makeEntity(this.blueprint.create(this.faker, this.args)); - if (entity) { - results.push(entity); - } - } - return results; - } - - public async create(): Promise { - const entity = await this.build(); - if (typeof this.eachFn === 'function') { - await this.eachFn(entity, this.faker); - } - return entity; - } - - public async createMany(amount: number): Promise { - const results: Entity[] = []; - for (let i = 0; i < amount; i++) { - const entity = await this.create(); - if (entity) { - results.push(entity); - } - } - return results; - } - - private async build(): Promise { - if (this.connection) { - const entity = await this.make(); - const em = this.connection.createEntityManager(); - try { - return await em.save(this.blueprint.EntityClass, entity); - } catch (error) { - console.error('saving entity failed', error); - return; - } - } - return; - } - - private async makeEntity(entity: Entity): Promise { - for (const attribute in entity) { - if (entity.hasOwnProperty(attribute)) { - if (this.isPromiseLike(entity[attribute])) { - entity[attribute] = await entity[attribute]; - } - - if (typeof entity[attribute] === 'object' && entity[attribute] instanceof EntityFactory) { - const subEntityFactory = entity[attribute]; - const subEntity = await (subEntityFactory as any).build(); - entity[attribute] = subEntity[this.identifier]; - } - } - } - return entity; - } - - private isPromiseLike(object: any): boolean { - return !!object && (typeof object === 'object' || typeof object === 'function') && typeof object.then === 'function'; - } -} diff --git a/src/lib/seeds/EntityFactoryInterface.ts b/src/lib/seeds/EntityFactoryInterface.ts deleted file mode 100644 index 61d9954c..00000000 --- a/src/lib/seeds/EntityFactoryInterface.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as Faker from 'faker'; - -/** - * EntityFactoryInterface is the one we use in our seed files. - * This will be returne of the main factory's get method. - */ -export interface EntityFactoryInterface { - /** - * Creates a entity with faked data, but not persisted to the database. - */ - make(): Promise; - makeMany(amount: number): Promise; - /** - * Creates a new faked entity in the database. - */ - create(): Promise; - createMany(amount: number): Promise; - /** - * This is called after creating a enity to the database. Use this to - * create other seeds but combined with this enitiy. - */ - each(iterator: (entity: Entity, faker: typeof Faker) => Promise): EntityFactoryInterface; -} diff --git a/src/lib/seeds/Factory.ts b/src/lib/seeds/Factory.ts deleted file mode 100644 index 2e4d8144..00000000 --- a/src/lib/seeds/Factory.ts +++ /dev/null @@ -1,58 +0,0 @@ -import * as Faker from 'faker'; -import { ObjectType } from 'typeorm'; -import { Connection } from 'typeorm/connection/Connection'; - -import { BluePrint } from './BluePrint'; -import { EntityFactory } from './EntityFactory'; -import { FactoryInterface } from './FactoryInterface'; -import { SeedsConstructorInterface } from './SeedsInterface'; - -export class Factory implements FactoryInterface { - - public static getInstance(): Factory { - if (!Factory.instance) { - Factory.instance = new Factory(Faker); - } - return Factory.instance; - } - - private static instance: Factory; - - private connection: Connection; - private blueprints: { [key: string]: BluePrint }; - - constructor(private faker: typeof Faker) { - this.blueprints = {}; - } - - public getConnection(): Connection { - return this.connection; - } - - public setConnection(connection: Connection): void { - this.connection = connection; - } - - public async runSeed(seedClass: SeedsConstructorInterface): Promise { - const seeder = new seedClass(); - return await seeder.seed(this); - } - - public define(entityClass: ObjectType, callback: (faker: typeof Faker, args: any[]) => Entity): void { - this.blueprints[this.getNameOfEntity(entityClass)] = new BluePrint(entityClass, callback); - } - - public get(entityClass: ObjectType, ...args: any[]): EntityFactory { - return new EntityFactory( - this.faker, - this.connection, - this.blueprints[this.getNameOfEntity(entityClass)], - args - ); - } - - private getNameOfEntity(EntityClass: any): string { - return new EntityClass().constructor.name; - } - -} diff --git a/src/lib/seeds/FactoryInterface.ts b/src/lib/seeds/FactoryInterface.ts deleted file mode 100644 index 435677c1..00000000 --- a/src/lib/seeds/FactoryInterface.ts +++ /dev/null @@ -1,33 +0,0 @@ -import * as Faker from 'faker'; -import { SeedsConstructorInterface } from 'src/lib/seeds'; -import { ObjectType } from 'typeorm'; -import { Connection } from 'typeorm/connection/Connection'; - -import { EntityFactoryInterface } from './EntityFactoryInterface'; - -/** - * This interface is used to define new entity faker factories or to get such a - * entity faker factory to start seeding. - */ -export interface FactoryInterface { - /** - * Returns a typeorm database connection. - */ - getConnection(): Connection; - /** - * Sets the typeorm database connection. - */ - setConnection(connection: Connection): void; - /** - * Runs the given seed class - */ - runSeed(seedClass: SeedsConstructorInterface): Promise; - /** - * Returns an EntityFactoryInterface - */ - get(entityClass: ObjectType, value?: any): EntityFactoryInterface; - /** - * Define an entity faker - */ - define(entityClass: ObjectType, fakerFunction: (faker: typeof Faker, value?: any) => Entity): void; -} diff --git a/src/lib/seeds/SeedsInterface.ts b/src/lib/seeds/SeedsInterface.ts deleted file mode 100644 index b5efa458..00000000 --- a/src/lib/seeds/SeedsInterface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Factory } from './Factory'; - -/** - * Seeds should implement this interface and all its methods. - */ -export interface SeedsInterface { - /** - * Seed data into the databas. - */ - seed(factory: Factory): Promise; -} - -export interface SeedsConstructorInterface { - new(): SeedsInterface; -} diff --git a/src/lib/seeds/cli.ts b/src/lib/seeds/cli.ts deleted file mode 100644 index 44a71015..00000000 --- a/src/lib/seeds/cli.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as Chalk from 'chalk'; -import * as commander from 'commander'; -import * as glob from 'glob'; -import * as path from 'path'; -import 'reflect-metadata'; -import { Connection } from 'typeorm'; - -import { getConnection } from './connection'; -import { Factory } from './Factory'; - -// Get executiuon path to look from there for seeds and factories -const runDir = process.cwd(); - -// Cli helper -commander - .version('0.0.0') - .description('Run database seeds of your project') - .option('-L, --logging', 'enable sql query logging') - .option('--factories ', 'add filepath for your factories') - .option('--seeds ', 'add filepath for your seeds') - .option('--config ', 'add filepath to your database config (must be a json)') - .parse(process.argv); - -// Get cli parameter for a different factory path -const factoryPath = (commander.factories) - ? commander.factories - : 'src/database/'; - -// Get cli parameter for a different seeds path -const seedsPath = (commander.seeds) - ? commander.seeds - : 'src/database/seeds/'; - -// Search for seeds and factories -glob(path.join(runDir, factoryPath, '**/*Factory{.js,.ts}'), (errFactories: any, factories: string[]) => { - glob(path.join(runDir, seedsPath, '*{.js,.ts}'), (errSeeds: any, seeds: string[]) => { - const log = console.log; - const chalk = Chalk.default; - - // Status logging to print out the amount of factories and seeds. - log(chalk.bold('seeds')); - log('🔎 ', chalk.gray.underline(`found:`), - chalk.blue.bold(`${factories.length} factories`, chalk.gray('&'), chalk.blue.bold(`${seeds.length} seeds`))); - - // Initialize all factories - for (const factory of factories) { - require(factory); - } - - // Get typeorm database connection and pass them to the factory instance - getConnection().then((connection: Connection) => { - const factory = Factory.getInstance(); - factory.setConnection(connection); - - // Initialize and seed all seeds. - const queue: Array> = []; - for (const seed of seeds) { - try { - const seedFile: any = require(seed); - let className = seed.split('/')[seed.split('/').length - 1]; - className = className.replace('.ts', '').replace('.js', ''); - className = className.split('-')[className.split('-').length - 1]; - log('\n' + chalk.gray.underline(`executing seed: `), chalk.green.bold(`${className}`)); - queue.push((new seedFile[className]()).seed(factory)); - } catch (error) { - console.error('Could not run seed ' + seed, error); - } - } - - // Promise to catch the end for termination and logging - Promise - .all(queue) - .then(() => { - log('\n👍 ', chalk.gray.underline(`finished seeding`)); - process.exit(0); - }) - .catch((error) => { - console.error('Could not run seed ' + error); - process.exit(1); - }); - - }).catch((error) => { - console.error('Could not connection to database ' + error); - process.exit(1); - }); - }); -}); diff --git a/src/lib/seeds/index.ts b/src/lib/seeds/index.ts deleted file mode 100644 index 97fa23d1..00000000 --- a/src/lib/seeds/index.ts +++ /dev/null @@ -1,24 +0,0 @@ -import 'reflect-metadata'; -import { Connection } from 'typeorm'; - -import { Factory } from './Factory'; - -// ------------------------------------------------------------------------- -// Handy Exports -// ------------------------------------------------------------------------- - -export * from './FactoryInterface'; -export * from './EntityFactoryInterface'; -export * from './SeedsInterface'; -export * from './Factory'; -export * from './utils'; - -// ------------------------------------------------------------------------- -// Facade functions -// ------------------------------------------------------------------------- - -export const getFactory = (connection: Connection) => { - const factory = Factory.getInstance(); - factory.setConnection(connection); - return factory; -}; diff --git a/src/lib/seeds/utils.ts b/src/lib/seeds/utils.ts deleted file mode 100644 index 5106607e..00000000 --- a/src/lib/seeds/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Times repeats a function n times - * @param n amount of loops - * @param iteratee this function gets n-times called - */ -export const times = async (n: number, iteratee: (index: number) => Promise): Promise => { - const rs = [] as TResult[]; - for (let i = 0; i < n; i++) { - const r = await iteratee(i); - rs.push(r); - } - return rs; -}; diff --git a/test/e2e/api/users.test.ts b/test/e2e/api/users.test.ts index 115b56a3..1290b549 100644 --- a/test/e2e/api/users.test.ts +++ b/test/e2e/api/users.test.ts @@ -3,34 +3,35 @@ import * as request from 'supertest'; import { User } from '../../../src/api/models/User'; import { CreateBruce } from '../../../src/database/seeds/CreateBruce'; -import { Factory } from '../../../src/lib/seeds/Factory'; -import { getFactory } from '../../../src/lib/seeds/index'; -import { closeDatabase, migrateDatabase } from '../../utils/database'; +import { runSeeder } from '../../../src/lib/seed'; +import { closeDatabase } from '../../utils/database'; import { fakeAuthenticationForUser } from '../utils/auth'; -import { bootstrapApp, BootstrapSettings } from '../utils/bootstrap'; +import { BootstrapSettings } from '../utils/bootstrap'; +import { prepareServer } from '../utils/server'; describe('/api/users', () => { + let bruce: User; + let settings: BootstrapSettings; + // ------------------------------------------------------------------------- // Setup up // ------------------------------------------------------------------------- - let settings: BootstrapSettings; - let factory: Factory; - let bruce: User; - let authServer: nock.Scope; - beforeAll(async () => settings = await bootstrapApp()); - beforeAll(async () => migrateDatabase(settings.connection)); - beforeAll(async () => factory = getFactory(settings.connection)); - beforeAll(async () => bruce = await factory.runSeed(CreateBruce)); - beforeAll(async () => authServer = fakeAuthenticationForUser(bruce, true)); + beforeAll(async () => { + settings = await prepareServer({ migrate: true }); + bruce = await runSeeder(CreateBruce); + fakeAuthenticationForUser(bruce, true); + }); // ------------------------------------------------------------------------- // Tear down // ------------------------------------------------------------------------- - afterAll(() => nock.cleanAll()); - afterAll(async () => closeDatabase(settings.connection)); + afterAll(async () => { + nock.cleanAll(); + await closeDatabase(settings.connection); + }); // ------------------------------------------------------------------------- // Test cases diff --git a/test/e2e/utils/server.ts b/test/e2e/utils/server.ts new file mode 100644 index 00000000..2150ebcb --- /dev/null +++ b/test/e2e/utils/server.ts @@ -0,0 +1,12 @@ +import { setConnection } from '../../../src/lib/seed'; +import { migrateDatabase } from '../../utils/database'; +import { bootstrapApp } from './bootstrap'; + +export const prepareServer = async (options?: { migrate: boolean }) => { + const settings = await bootstrapApp(); + if (options && options.migrate) { + await migrateDatabase(settings.connection); + } + setConnection(settings.connection); + return settings; +}; diff --git a/yarn.lock b/yarn.lock index 2271b4b5..d761d047 100644 --- a/yarn.lock +++ b/yarn.lock @@ -101,8 +101,8 @@ "@types/node" "*" "@types/node@*": - version "9.4.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e" + version "9.4.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" "@types/reflect-metadata@0.0.5": version "0.0.5" @@ -252,12 +252,18 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0, ansi-styles@^3.2.0: +ansi-styles@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: color-convert "^1.9.0" +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + dependencies: + color-convert "^1.9.0" + any-promise@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" @@ -302,8 +308,8 @@ are-we-there-yet@~1.1.2: readable-stream "^2.0.6" argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" dependencies: sprintf-js "~1.0.2" @@ -557,7 +563,7 @@ babel-register@^6.26.0: mkdirp "^0.5.1" source-map-support "^0.4.15" -babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: +babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" dependencies: @@ -866,12 +872,12 @@ chalk@^1.1.1, chalk@^1.1.3: supports-color "^2.0.0" chalk@^2.0.1, chalk@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" + version "2.3.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.2.tgz#250dc96b07491bfd601e648d66ddf5f60c7a5c65" dependencies: - ansi-styles "^3.2.0" + ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" - supports-color "^5.2.0" + supports-color "^5.3.0" check-error@^1.0.1: version "1.0.2" @@ -914,9 +920,9 @@ ci-info@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" -class-transformer@~0.1.6: - version "0.1.8" - resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.1.8.tgz#be04dd2afb7b301e4c8c79c5349fedaac3d5a7e1" +class-transformer@^0.1.9: + version "0.1.9" + resolved "https://registry.yarnpkg.com/class-transformer/-/class-transformer-0.1.9.tgz#29977c528233ca014e6fd9523327ebd31d11ca54" class-utils@^0.3.5: version "0.3.6" @@ -927,12 +933,18 @@ class-utils@^0.3.5: isobject "^3.0.0" static-extend "^0.1.1" -class-validator@^0.7.0, class-validator@^0.7.3: +class-validator@^0.7.3: version "0.7.3" resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.7.3.tgz#3c2821b8cf35fd8d5f4fcb8063bc57fb50049e7e" dependencies: validator "^7.0.0" +class-validator@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/class-validator/-/class-validator-0.8.1.tgz#f5efd5c613927e3c2f68692e8f14d53a2644fb2f" + dependencies: + validator "9.2.0" + cli-boxes@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" @@ -1015,18 +1027,18 @@ commander@2.6.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.6.0.tgz#9df7e52fb2a0cb0fb89058ee80c3104225f37e1d" commander@^2.11.0: - version "2.14.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" + version "2.15.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.0.tgz#ad2a23a1c3b036e392469b8012cec6b33b4c1322" commander@^2.12.1: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" common-tags@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.4.0.tgz#1187be4f3d4cf0c0427d43f74eef1f73501614c0" + version "1.7.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.7.2.tgz#24d9768c63d253a56ecff93845b44b4df1d52771" dependencies: - babel-runtime "^6.18.0" + babel-runtime "^6.26.0" component-bind@1.0.0: version "1.0.0" @@ -1141,7 +1153,11 @@ copyfiles@^1.2.0: noms "0.0.0" through2 "^2.0.1" -core-js@^2.4.0, core-js@^2.5.0: +core-js@^2.4.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" + +core-js@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" @@ -1216,8 +1232,8 @@ cross-env@^3.1.4: is-windows "^1.0.0" cross-env@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.3.tgz#f8ae18faac87692b0a8b4d2f7000d4ec3a85dfd7" + version "5.1.4" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.4.tgz#f61c14291f7cc653bb86457002ea80a04699d022" dependencies: cross-spawn "^5.1.0" is-windows "^1.0.0" @@ -2142,8 +2158,8 @@ helmet-csp@2.7.0: platform "1.3.5" helmet@^3.9.0: - version "3.11.0" - resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.11.0.tgz#5eacccc0b5b61d786e29aa3fc5650abf73e1824f" + version "3.12.0" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.12.0.tgz#2098e35cf4e51c64c2f1d38670b7d382a377d92c" dependencies: dns-prefetch-control "0.1.0" dont-sniff-mimetype "1.0.0" @@ -2156,7 +2172,7 @@ helmet@^3.9.0: ienoopen "1.0.0" nocache "2.0.0" referrer-policy "1.1.0" - x-xss-protection "1.0.0" + x-xss-protection "1.1.0" hide-powered-by@1.0.0: version "1.0.0" @@ -2188,8 +2204,8 @@ homedir-polyfill@^1.0.1: parse-passwd "^1.0.0" hosted-git-info@^2.1.4: - version "2.5.0" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.5.0.tgz#6d60e34b3abbc8313062c3b798ef8d901a07af3c" + version "2.6.0" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.6.0.tgz#23235b29ab230c576aab0d4f13fc046b0b038222" hpkp@2.0.0: version "2.0.0" @@ -2502,11 +2518,7 @@ is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" -is-windows@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.1.tgz#310db70f742d259a16a369202b51af84233310d9" - -is-windows@^1.0.2: +is-windows@^1.0.0, is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" @@ -2836,13 +2848,20 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.7.0, js-yaml@^3.8.4, js-yaml@^3.9.0: +js-yaml@^3.7.0: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^3.8.4, js-yaml@^3.9.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.11.0.tgz#597c1a8bd57152f26d622ce4117851a51f5ebaef" + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" @@ -2995,26 +3014,10 @@ locate-path@^2.0.0: p-locate "^2.0.0" path-exists "^3.0.0" -lodash.endswith@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.endswith/-/lodash.endswith-4.2.1.tgz#fed59ac1738ed3e236edd7064ec456448b37bc09" - -lodash.isfunction@^3.0.8: - version "3.0.9" - resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" - -lodash.isstring@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - lodash.reduce@4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" -lodash.startswith@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c" - lodash@^4.14.0, lodash@^4.5.1: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" @@ -3045,8 +3048,8 @@ lowercase-keys@^1.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" lru-cache@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55" + version "4.1.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.2.tgz#45234b2e6e2f2b33da125624c4664929a0224c3f" dependencies: pseudomap "^1.0.2" yallist "^2.1.2" @@ -3354,8 +3357,8 @@ nocache@2.0.0: resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980" nock@^9.1.4: - version "9.2.1" - resolved "https://registry.yarnpkg.com/nock/-/nock-9.2.1.tgz#7e73561277c3e8f2287e385d3d04c7223a54faea" + version "9.2.3" + resolved "https://registry.yarnpkg.com/nock/-/nock-9.2.3.tgz#39738087d6a0497d3a165fb352612b38a2f9b92f" dependencies: chai "^4.1.2" debug "^3.1.0" @@ -3397,17 +3400,18 @@ node-pre-gyp@^0.6.39, node-pre-gyp@~0.6.38: tar-pack "^3.4.0" nodemon@^1.12.1: - version "1.15.1" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.15.1.tgz#54daa72443d8d5a548f130866b92e65cded0ed58" + version "1.17.1" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.17.1.tgz#cdb4bc53d7a86d6162143a1a44d7adf927d8652f" dependencies: chokidar "^2.0.2" debug "^3.1.0" ignore-by-default "^1.0.1" minimatch "^3.0.4" pstree.remy "^1.1.0" - semver "^5.4.1" + semver "^5.5.0" + supports-color "^5.2.0" touch "^3.1.0" - undefsafe "^2.0.1" + undefsafe "^2.0.2" update-notifier "^2.3.0" noms@0.0.0: @@ -3476,8 +3480,8 @@ nps-utils@^1.5.0: rimraf "^2.6.1" nps@^5.7.1: - version "5.7.1" - resolved "https://registry.yarnpkg.com/nps/-/nps-5.7.1.tgz#12cbfc635a6535ccb4f6eb516e6011f3af6bef42" + version "5.8.1" + resolved "https://registry.yarnpkg.com/nps/-/nps-5.8.1.tgz#180ca887a3bf8abdb860fb972d135310260580bb" dependencies: arrify "^1.0.1" chalk "^2.0.1" @@ -3969,8 +3973,8 @@ readable-stream@2.3.3, readable-stream@^2.0.5, readable-stream@^2.1.5: util-deprecate "~1.0.1" readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4: - version "2.3.4" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" @@ -3999,8 +4003,8 @@ readdirp@^2.0.0: set-immediate-shim "^1.0.1" readline-sync@^1.4.7: - version "1.4.7" - resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.7.tgz#001bfdd4c06110c3c084c63bf7c6a56022213f30" + version "1.4.9" + resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.9.tgz#3eda8e65f23cd2a17e61301b1f0003396af5ecda" redent@^1.0.0: version "1.0.0" @@ -4013,7 +4017,7 @@ referrer-policy@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.1.0.tgz#35774eb735bf50fb6c078e83334b472350207d79" -reflect-metadata@^0.1.10: +reflect-metadata@^0.1.10, reflect-metadata@^0.1.12: version "0.1.12" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2" @@ -4158,14 +4162,14 @@ rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: glob "^7.0.5" routing-controllers@^0.7.6: - version "0.7.6" - resolved "https://registry.yarnpkg.com/routing-controllers/-/routing-controllers-0.7.6.tgz#f025b6b1f011fb5973e94db4ffde86da8d48091e" + version "0.7.7" + resolved "https://registry.yarnpkg.com/routing-controllers/-/routing-controllers-0.7.7.tgz#55b3dd3e676ade2527e522aad2834ac891cce0ee" dependencies: - class-transformer "~0.1.6" - class-validator "^0.7.0" + class-transformer "^0.1.9" + class-validator "^0.8.1" cookie "^0.3.1" glob "^7.0.5" - reflect-metadata "^0.1.10" + reflect-metadata "^0.1.12" template-url "^1.0.0" rx@2.3.24: @@ -4206,7 +4210,7 @@ semver-diff@^2.0.0: dependencies: semver "^5.0.3" -"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0: +"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" @@ -4446,22 +4450,30 @@ spawn-command-with-kill@^1.0.0: spawn-command "^0.0.2-1" spawn-command@^0.0.2-1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - spdx-license-ids "^1.0.2" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" @@ -4659,9 +4671,9 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" -supports-color@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" +supports-color@^5.2.0, supports-color@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.3.0.tgz#5b24ac15db80fa927cf5227a4a33fd3c4c7676c0" dependencies: has-flag "^3.0.0" @@ -4796,12 +4808,18 @@ touch@^3.1.0: dependencies: nopt "~1.0.10" -tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3: +tough-cookie@^2.3.2, tough-cookie@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" dependencies: punycode "^1.4.1" +tough-cookie@~2.3.0: + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" + dependencies: + punycode "^1.4.1" + tr46@~0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" @@ -4898,14 +4916,10 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@^4.0.0: +type-detect@^4.0.0, type-detect@^4.0.3: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" -type-detect@^4.0.3: - version "4.0.5" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.5.tgz#d70e5bc81db6de2a381bcaca0c6e0cbdc7635de2" - type-is@^1.6.15, type-is@~1.6.15: version "1.6.15" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" @@ -4922,8 +4936,8 @@ typeorm-typedi-extensions@^0.1.1: resolved "https://registry.yarnpkg.com/typeorm-typedi-extensions/-/typeorm-typedi-extensions-0.1.1.tgz#e99a66dcf501fbd5837cf2f7a3dc75e13de89734" typeorm@^0.1.3: - version "0.1.12" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.1.12.tgz#adcd45f302d21ab4d5b02671bfa8c07cea044af7" + version "0.1.16" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.1.16.tgz#52a9251f4394c07ce1f4e14d2c8b6cfca89535fc" dependencies: app-root-path "^2.0.1" chalk "^2.0.1" @@ -4963,7 +4977,7 @@ ultron@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" -undefsafe@^2.0.1: +undefsafe@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.2.tgz#225f6b9e0337663e0d8e7cfd686fc2836ccace76" dependencies: @@ -5004,13 +5018,8 @@ unzip-response@^2.0.1: resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" upath@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.2.tgz#80aaae5395abc5fd402933ae2f58694f0860204c" - dependencies: - lodash.endswith "^4.2.1" - lodash.isfunction "^3.0.8" - lodash.isstring "^4.0.1" - lodash.startswith "^4.2.1" + version "1.0.4" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.4.tgz#ee2321ba0a786c50973db043a50b7bcba822361d" update-notifier@^2.3.0: version "2.3.0" @@ -5077,11 +5086,15 @@ v8flags@^3.0.0: homedir-polyfill "^1.0.1" validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validator@9.2.0: + version "9.2.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-9.2.0.tgz#ad216eed5f37cac31a6fe00ceab1f6b88bded03e" validator@^7.0.0: version "7.2.0" @@ -5216,9 +5229,9 @@ ws@~3.3.1: safe-buffer "~5.1.0" ultron "~1.1.0" -x-xss-protection@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.0.0.tgz#898afb93869b24661cf9c52f9ee8db8ed0764dd9" +x-xss-protection@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.1.0.tgz#4f1898c332deb1e7f2be1280efb3e2c53d69c1a7" xdg-basedir@^3.0.0: version "3.0.0" @@ -5236,8 +5249,8 @@ xml2js@^0.4.17: xmlbuilder "~9.0.1" xmlbuilder@~9.0.1: - version "9.0.4" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.4.tgz#519cb4ca686d005a8420d3496f3f0caeecca580f" + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" xmlhttprequest-ssl@~1.5.4: version "1.5.4" From 43847b027be1e2a64a8bb9a3e2c21386bf4b9142 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 11 Mar 2018 17:50:04 +0100 Subject: [PATCH 04/39] refactor the new seed --- src/database/seeds/CreatePets.ts | 20 +++++++------- src/database/seeds/CreateUsers.ts | 7 +++-- src/lib/seed/EntityFactory.ts | 13 +++++++++ src/lib/seed/cli.ts | 9 ++----- src/lib/seed/importer.ts | 17 +++--------- src/lib/seed/index.ts | 45 ++++++++++++++++--------------- src/lib/seed/make.ts | 31 --------------------- src/lib/seed/types.ts | 25 +++++++++++++---- src/lib/seed/utils.ts | 22 ++++++++++++--- 9 files changed, 93 insertions(+), 96 deletions(-) delete mode 100644 src/lib/seed/make.ts diff --git a/src/database/seeds/CreatePets.ts b/src/database/seeds/CreatePets.ts index 823b87ab..c066a4e1 100644 --- a/src/database/seeds/CreatePets.ts +++ b/src/database/seeds/CreatePets.ts @@ -1,23 +1,23 @@ import { Connection } from 'typeorm'; -import { Factory, Seed } from '../../lib/seed/types'; +import { Pet } from '../../../src/api/models/Pet'; +import { User } from '../../../src/api/models/User'; +import { Factory, Seed, times } from '../../lib/seed'; -// import { Pet } from '../../../src/api/models/Pet'; -// import { User } from '../../../src/api/models/User'; export class CreatePets implements Seed { public async seed(factory: Factory, connection: Connection): Promise { console.log('CreatePets'); // const connection = await factory.getConnection(); - // const em = connection.createEntityManager(); + const em = connection.createEntityManager(); - // await times(10, async (n) => { - // const pet = await factory.get(Pet).create(); - // const user = await factory.get(User).make(); - // user.pets = [pet]; - // await em.save(user); - // }); + await times(10, async (n) => { + const pet = await factory(Pet as any)().seed(); + const user = await factory(User as any)().make(); + user.pets = [pet]; + await em.save(user); + }); } } diff --git a/src/database/seeds/CreateUsers.ts b/src/database/seeds/CreateUsers.ts index a50097b1..62747b1b 100644 --- a/src/database/seeds/CreateUsers.ts +++ b/src/database/seeds/CreateUsers.ts @@ -1,16 +1,15 @@ import { Connection } from 'typeorm/connection/Connection'; +import { User } from '../../../src/api/models/User'; import { Factory, Seed } from '../../lib/seed/types'; -// import { User } from '../../../src/api/models/User'; export class CreateUsers implements Seed { public async seed(factory: Factory, connection: Connection): Promise { console.log('CreateUsers'); - // await factory - // .get(User) - // .createMany(10); + await factory(User)() + .seedMany(10); } } diff --git a/src/lib/seed/EntityFactory.ts b/src/lib/seed/EntityFactory.ts index 9afb234b..facd5bc4 100644 --- a/src/lib/seed/EntityFactory.ts +++ b/src/lib/seed/EntityFactory.ts @@ -4,6 +4,9 @@ import { Connection } from 'typeorm/connection/Connection'; import { FactoryFunction } from './types'; import { isPromiseLike } from './utils'; +/** + * EntityFactory ... + */ export class EntityFactory { private mapFunction: (entity: Entity) => Promise; @@ -19,11 +22,18 @@ export class EntityFactory { // Public API // ------------------------------------------------------------------------- + /** + * This fucntion is used to alter the generated values of entity, before it + * is persist into the database + */ public map(mapFunction: (entity: Entity) => Promise): EntityFactory { this.mapFunction = mapFunction; return this; } + /** + * Make generate a new entity, but does not persist it + */ public async make(): Promise { if (this.factory) { let entity = await this.resolveEntity(this.factory(Faker, this.settings)); @@ -35,6 +45,9 @@ export class EntityFactory { throw new Error('Could not found entity'); } + /** + * Seed persist and generates a given entity + */ public async seed(): Promise { const connection: Connection = (global as any).seeder.connection; if (connection) { diff --git a/src/lib/seed/cli.ts b/src/lib/seed/cli.ts index d5961de9..2c58a812 100644 --- a/src/lib/seed/cli.ts +++ b/src/lib/seed/cli.ts @@ -5,12 +5,11 @@ import * as path from 'path'; import { loadEntityFactories } from './'; import { getConnection } from './connection'; import { loadSeeds } from './importer'; -import { runSeeder, setConnection } from './index'; -import { SeedConstructor } from './types'; +import { runSeed, setConnection } from './index'; // Cli helper commander - .version('0.0.0') + .version('1.0.0') .description('Run database seeds of your project') .option('-L, --logging', 'enable sql query logging') .option('--factories ', 'add filepath for your factories') @@ -91,8 +90,4 @@ const handleError = (error) => { process.exit(1); }; -const runSeed = async (seedClass: SeedConstructor): Promise => { - await runSeeder(seedClass); -}; - run(); diff --git a/src/lib/seed/importer.ts b/src/lib/seed/importer.ts index a1f186d4..8d188785 100644 --- a/src/lib/seed/importer.ts +++ b/src/lib/seed/importer.ts @@ -12,15 +12,9 @@ const loadFiles = (pathToFolder: string) => (successFn: (files: string[]) => void) => (failedFn: (error: any) => void) => { - glob(path.join(process.cwd(), pathToFolder, filePattern), (error: any, files: string[]) => { - if (error) { - return failedFn(error); - } - successFn(files); - }); - // error - // ? failedFn(error) - // : successFn(files)); + glob(path.join(process.cwd(), pathToFolder, filePattern), (error: any, files: string[]) => error + ? failedFn(error) + : successFn(files)); }; const loadFactoryFiles = loadFiles('**/*Factory{.js,.ts}'); @@ -40,9 +34,6 @@ export const loadEntityFactories = (pathToFolder: string): Promise => export const loadSeeds = (pathToFolder: string): Promise => { return new Promise((resolve, reject) => { - loadFiles('**/*{.js,.ts}')(pathToFolder)(files => { - // TODO: load seeds - resolve(files); - })(reject); + loadFiles('**/*{.js,.ts}')(pathToFolder)(resolve)(reject); }); }; diff --git a/src/lib/seed/index.ts b/src/lib/seed/index.ts index 8aa45f88..61f3b4e7 100644 --- a/src/lib/seed/index.ts +++ b/src/lib/seed/index.ts @@ -2,7 +2,9 @@ import 'reflect-metadata'; import { Connection, ObjectType } from 'typeorm'; import { EntityFactory } from './EntityFactory'; -import { EntityFactoryDefinition, FactoryFunction, SeedConstructor } from './types'; +import { + EntityConstructor, EntityFactoryDefinition, FactoryFunction, SeedConstructor +} from './types'; import { getNameOfClass } from './utils'; // ------------------------------------------------------------------------- @@ -10,6 +12,8 @@ import { getNameOfClass } from './utils'; // ------------------------------------------------------------------------- export * from './importer'; +export { Factory, Seed } from './types'; +export { times } from './utils'; // ------------------------------------------------------------------------- // Types & Variables @@ -20,23 +24,32 @@ export * from './importer'; entityFactories: new Map>(), }; -// ------------------------------------------------------------------------- -// Util functions -// ------------------------------------------------------------------------- - // ------------------------------------------------------------------------- // Facade functions // ------------------------------------------------------------------------- +/** + * Adds the typorm connection to the seed options + */ export const setConnection = (connection: Connection) => (global as any).seeder.connection = connection; +/** + * Returns the typorm connection from our seed options + */ export const getConnection = () => (global as any).seeder.connection; +/** + * Defines a new entity factory + */ export const define = (entity: ObjectType, factoryFn: FactoryFunction) => { (global as any).seeder.entityFactories.set(getNameOfClass(entity), { entity, factory: factoryFn }); }; -export const factory = (entity: any) => (settings?: Settings) => { + +/** + * Gets a defined entity factory and pass the settigns along to the entity factory function + */ +export const factory = (entity: EntityConstructor) => (settings?: Settings) => { const name = getNameOfClass(entity); const entityFactoryObject = (global as any).seeder.entityFactories.get(name); return new EntityFactory( @@ -47,22 +60,10 @@ export const factory = (entity: any) => (settings?: Settings) ); }; -export const seed = async (entityFactory: EntityFactory): Promise => { - const connection: Connection = (global as any).seeder.connection; - if (connection) { - const em = connection.createEntityManager(); - try { - const entity = await entityFactory.make(); - return await em.save(entityFactory.entity, entity); - } catch (error) { - throw new Error('Could not save entity'); - } - } else { - throw new Error('No db connection is given'); - } -}; - -export const runSeeder = async (seederConstructor: SeedConstructor): Promise => { +/** + * Runs a seed class + */ +export const runSeed = async (seederConstructor: SeedConstructor): Promise => { const seeder = new seederConstructor(); return seeder.seed(factory, getConnection()); }; diff --git a/src/lib/seed/make.ts b/src/lib/seed/make.ts deleted file mode 100644 index 9f66e96f..00000000 --- a/src/lib/seed/make.ts +++ /dev/null @@ -1,31 +0,0 @@ -import * as Faker from 'faker'; - -import { EntityFactoryDefinition } from './types'; -import { isPromiseLike } from './utils'; - -const resolveEntity = async (entity: E): Promise => { - for (const attribute in entity) { - if (entity.hasOwnProperty(attribute)) { - if (isPromiseLike(entity[attribute])) { - entity[attribute] = await entity[attribute]; - } - - if (typeof entity[attribute] === 'object') { - const subEntityFactory = entity[attribute]; - try { - entity[attribute] = await (subEntityFactory as any).make(); - } catch (e) { - throw new Error(`Could not make ${(subEntityFactory as any).name}`); - } - } - } - } - return entity; -}; - -export const make = (entityFactoryDefinition: EntityFactoryDefinition) => (settings?: Settings) => () => { - if (entityFactoryDefinition) { - return resolveEntity(entityFactoryDefinition.factory(Faker, settings)); - } - throw new Error('Could not found entity'); -}; diff --git a/src/lib/seed/types.ts b/src/lib/seed/types.ts index 93768e7d..c931bbe4 100644 --- a/src/lib/seed/types.ts +++ b/src/lib/seed/types.ts @@ -3,19 +3,34 @@ import { Connection, ObjectType } from 'typeorm'; import { EntityFactory } from './EntityFactory'; +/** + * FactoryFunction is the fucntion, which generate a new filled entity + */ export type FactoryFunction = (faker: typeof Faker, settings?: Settings) => Entity; +/** + * Factory gets the EntityFactory to the given Entity and pass the settings along + */ export type Factory = (entity: Entity) => (settings?: Settings) => EntityFactory; -export interface EntityFactoryDefinition { - entity: ObjectType; - factory: FactoryFunction; -} - +/** + * Seed are the class to create some data. Those seed are run by the cli. + */ export interface Seed { seed(factory: Factory, connection: Connection): Promise; } +/** + * Constructor of the seed class + */ export interface SeedConstructor { new(): Seed; } + +/** + * Value of our EntityFactory state + */ +export interface EntityFactoryDefinition { + entity: ObjectType; + factory: FactoryFunction; +} diff --git a/src/lib/seed/utils.ts b/src/lib/seed/utils.ts index c5908d22..88b05037 100644 --- a/src/lib/seed/utils.ts +++ b/src/lib/seed/utils.ts @@ -1,7 +1,21 @@ -// ------------------------------------------------------------------------- -// Util functions -// ------------------------------------------------------------------------- - +/** + * Returns the name of a class + */ export const getNameOfClass = (c: any): string => new c().constructor.name; +/** + * Checks if the given argument is a promise + */ export const isPromiseLike = (o: any): boolean => !!o && (typeof o === 'object' || typeof o === 'function') && typeof o.then === 'function'; + +/** + * Times repeats a function n times + */ +export const times = async (n: number, iteratee: (index: number) => Promise): Promise => { + const rs = [] as TResult[]; + for (let i = 0; i < n; i++) { + const r = await iteratee(i); + rs.push(r); + } + return rs; +}; From e74adc9c4a464acdfb2be8c9759257f802d3c8df Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 11 Mar 2018 18:58:43 +0100 Subject: [PATCH 05/39] house cleaning --- src/database/factories/UserFactory.ts | 1 - src/database/seeds/CreateBruce.ts | 2 -- src/database/seeds/CreatePets.ts | 3 --- src/database/seeds/CreateUsers.ts | 5 +---- src/lib/seed/index.ts | 7 ++----- 5 files changed, 3 insertions(+), 15 deletions(-) diff --git a/src/database/factories/UserFactory.ts b/src/database/factories/UserFactory.ts index e8dd0add..810ae7a5 100644 --- a/src/database/factories/UserFactory.ts +++ b/src/database/factories/UserFactory.ts @@ -4,7 +4,6 @@ import { User } from '../../../src/api/models/User'; import { define } from '../../lib/seed'; define(User, (faker: typeof Faker, settings: { role: string }) => { - console.log('UserFactory', settings.role); const gender = faker.random.number(1); const firstName = faker.name.firstName(gender); const lastName = faker.name.lastName(gender); diff --git a/src/database/seeds/CreateBruce.ts b/src/database/seeds/CreateBruce.ts index 4a3439d0..0d88108d 100644 --- a/src/database/seeds/CreateBruce.ts +++ b/src/database/seeds/CreateBruce.ts @@ -6,8 +6,6 @@ import { Factory, Seed } from '../../lib/seed/types'; export class CreateBruce implements Seed { public async seed(factory: Factory, connection: Connection): Promise { - console.log('CreateBruce'); - // const userFactory = factory(User as any); // const adminUserFactory = userFactory({ role: 'admin' }); diff --git a/src/database/seeds/CreatePets.ts b/src/database/seeds/CreatePets.ts index c066a4e1..3b68f5aa 100644 --- a/src/database/seeds/CreatePets.ts +++ b/src/database/seeds/CreatePets.ts @@ -7,9 +7,6 @@ import { Factory, Seed, times } from '../../lib/seed'; export class CreatePets implements Seed { public async seed(factory: Factory, connection: Connection): Promise { - console.log('CreatePets'); - - // const connection = await factory.getConnection(); const em = connection.createEntityManager(); await times(10, async (n) => { diff --git a/src/database/seeds/CreateUsers.ts b/src/database/seeds/CreateUsers.ts index 62747b1b..6f6f5871 100644 --- a/src/database/seeds/CreateUsers.ts +++ b/src/database/seeds/CreateUsers.ts @@ -6,10 +6,7 @@ import { Factory, Seed } from '../../lib/seed/types'; export class CreateUsers implements Seed { public async seed(factory: Factory, connection: Connection): Promise { - console.log('CreateUsers'); - - await factory(User)() - .seedMany(10); + await factory(User)().seedMany(10); } } diff --git a/src/lib/seed/index.ts b/src/lib/seed/index.ts index 61f3b4e7..ead51f6e 100644 --- a/src/lib/seed/index.ts +++ b/src/lib/seed/index.ts @@ -2,9 +2,7 @@ import 'reflect-metadata'; import { Connection, ObjectType } from 'typeorm'; import { EntityFactory } from './EntityFactory'; -import { - EntityConstructor, EntityFactoryDefinition, FactoryFunction, SeedConstructor -} from './types'; +import { EntityFactoryDefinition, FactoryFunction, SeedConstructor } from './types'; import { getNameOfClass } from './utils'; // ------------------------------------------------------------------------- @@ -45,11 +43,10 @@ export const define = (entity: ObjectType, factoryFn: (global as any).seeder.entityFactories.set(getNameOfClass(entity), { entity, factory: factoryFn }); }; - /** * Gets a defined entity factory and pass the settigns along to the entity factory function */ -export const factory = (entity: EntityConstructor) => (settings?: Settings) => { +export const factory = (entity: Entity) => (settings?: Settings) => { const name = getNameOfClass(entity); const entityFactoryObject = (global as any).seeder.entityFactories.get(name); return new EntityFactory( From 0bf3110bf90f3a67ba04fab3bb176b6a3870d0cc Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 11 Mar 2018 18:58:54 +0100 Subject: [PATCH 06/39] Adjust the documentation --- README.md | 78 ++++++++++++++++++++++--------------------------------- 1 file changed, 31 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index 3537b8c7..666db13b 100644 --- a/README.md +++ b/README.md @@ -256,14 +256,15 @@ export class UserService { ## Seeding Isn't exhausting to create some sample data into your fresh migrated database, well this time is over! -How does it work? Just, create a factory for your entities and a seeds script. +How does it work? Just, create a factory for your entities and a seed script. ### 1. Create a factory for your entity For all the entities we want to seed, we need to define a factory. To do so we give you the awesome [faker](https://github.com/marak/Faker.js/) library as a parameter into your factory. Then create your "fake" entity as you would normally do and return it. Those factory files should be in the `src/database/factories` folder and suffixed with `Factory`. Example `src/database/factories/UserFactory.ts`. +Settings can be used to pass some static value into the factory. ```typescript -factory.define(User, (faker: typeof Faker) => { +define(User, (faker: typeof Faker, settings: { roles: string[] }) => { const gender = faker.random.number(1); const firstName = faker.name.firstName(gender); const lastName = faker.name.lastName(gender); @@ -273,40 +274,25 @@ factory.define(User, (faker: typeof Faker) => { user.firstName = firstName; user.lastName = lastName; user.email = email; + user.roles = settings.roles; return user; }); ``` -This can be used to pass some dynamic value into the factory. - -```typescript -factory.define(Pet, (faker: typeof Faker, args: any[]) => { - const type = args[0]; - return { - name: faker.name.firstName(), - type: type || 'dog' - }; -}); -``` - To deal with relations you can use the entity manager like this. ```typescript -import { SeedsInterface, FactoryInterface, times } from '../../lib/seeds'; -import { Pet } from '../../../src/api/models/Pet'; -import { User } from '../../../src/api/models/User'; - export class CreatePets implements SeedsInterface { - public async seed(factory: FactoryInterface): Promise { + public async seed(factory: FactoryInterface, connection: Connection): Promise { const connection = await factory.getConnection(); const em = connection.createEntityManager(); await times(10, async (n) => { // This creates a pet in the database - const pet = await factory.get(Pet).create(); + const pet = await factory(Pet)().create(); // This only returns a entity with fake data - const user = await factory.get(User).make(); + const user = await factory(User)({ roles: ['admin'] }).make(); user.pets = [pet]; await em.save(user); }); @@ -315,51 +301,49 @@ export class CreatePets implements SeedsInterface { } ``` -### 2. Create a seed file - -The seeds files define how much and how the data are connected with each other. The files will be executed alphabetically. +Or you could do the relation in the entity factory like this. ```typescript -export class CreateUsers implements SeedsInterface { - - public async seed(factory: FactoryInterface): Promise { - await factory - .get(User) - .createMany(10); - } +define(Pet, (faker: typeof Faker, settings: undefined) => { + const gender = faker.random.number(1); + const name = faker.name.firstName(gender); -} + const pet = new Pet(); + pet.name = name; + pet.age = faker.random.number(); + pet.user = factory(User)({ roles: ['admin'] }) + return pet; +}); ``` -With the second parameter in the `.get(, )` you are able to create different variations of entities. +### 2. Create a seed file + +The seeds files define how much and how the data are connected with each other. The files will be executed alphabetically. +With the second function you are able to create different variations of entities. + ```typescript -export class CreateUsers implements SeedsInterface { +export class CreateUsers implements Seed { - public async seed(factory: FactoryInterface): Promise { - await factory - .get(User, 'admin') - .create(); + public async seed(factory: Factory, connection: Connection): Promise { + await factory(User)({ roles: [] }).createMany(10); } } ``` -Here an example with nested factories. +Here an example with nested factories. You can use the `.map()` function to alter +the generated value before they get persisted. ```typescript ... -await factory.get(User) - .each(async (user: User) => { - - const pets: Pet[] = await factory.get(Pet) - .createMany(2); - +await factory(User)() + .map(async (user: User) => { + const pets: Pet[] = await factory(Pet)().createMany(2); const petIds = pets.map((pet: Pet) => pet.Id); await user.pets().attach(petIds); - }) - .create(5); + .createMany(5); ... ``` From f5ac88da33bf700c995fc822afb0b51c7f5f980f Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 11 Mar 2018 19:12:51 +0100 Subject: [PATCH 07/39] fix types --- src/database/seeds/CreatePets.ts | 4 ++-- src/lib/seed/EntityFactory.ts | 6 +++--- src/lib/seed/index.ts | 3 ++- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/database/seeds/CreatePets.ts b/src/database/seeds/CreatePets.ts index 3b68f5aa..76ef89fc 100644 --- a/src/database/seeds/CreatePets.ts +++ b/src/database/seeds/CreatePets.ts @@ -10,8 +10,8 @@ export class CreatePets implements Seed { const em = connection.createEntityManager(); await times(10, async (n) => { - const pet = await factory(Pet as any)().seed(); - const user = await factory(User as any)().make(); + const pet = await factory(Pet)().seed(); + const user = await factory(User)().make(); user.pets = [pet]; await em.save(user); }); diff --git a/src/lib/seed/EntityFactory.ts b/src/lib/seed/EntityFactory.ts index facd5bc4..784161fe 100644 --- a/src/lib/seed/EntityFactory.ts +++ b/src/lib/seed/EntityFactory.ts @@ -1,5 +1,5 @@ import * as Faker from 'faker'; -import { Connection } from 'typeorm/connection/Connection'; +import { Connection, ObjectType } from 'typeorm'; import { FactoryFunction } from './types'; import { isPromiseLike } from './utils'; @@ -13,7 +13,7 @@ export class EntityFactory { constructor( public name: string, - public entity: Entity, + public entity: ObjectType, private factory: FactoryFunction, private settings?: Settings ) { } @@ -54,7 +54,7 @@ export class EntityFactory { const em = connection.createEntityManager(); try { const entity = await this.make(); - return await em.save(this.entity, entity); + return await em.save(entity); } catch (error) { throw new Error('Could not save entity'); } diff --git a/src/lib/seed/index.ts b/src/lib/seed/index.ts index ead51f6e..2d057a77 100644 --- a/src/lib/seed/index.ts +++ b/src/lib/seed/index.ts @@ -1,6 +1,7 @@ import 'reflect-metadata'; import { Connection, ObjectType } from 'typeorm'; +import { Factory } from '../seeds/Factory'; import { EntityFactory } from './EntityFactory'; import { EntityFactoryDefinition, FactoryFunction, SeedConstructor } from './types'; import { getNameOfClass } from './utils'; @@ -46,7 +47,7 @@ export const define = (entity: ObjectType, factoryFn: /** * Gets a defined entity factory and pass the settigns along to the entity factory function */ -export const factory = (entity: Entity) => (settings?: Settings) => { +export const factory: Factory = (entity: ObjectType) => (settings?: Settings) => { const name = getNameOfClass(entity); const entityFactoryObject = (global as any).seeder.entityFactories.get(name); return new EntityFactory( From cf7c7cbc258ebba18087cf837f74a9df056e934d Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sun, 11 Mar 2018 19:13:05 +0100 Subject: [PATCH 08/39] formatting readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 666db13b..1a2d1131 100644 --- a/README.md +++ b/README.md @@ -321,7 +321,6 @@ define(Pet, (faker: typeof Faker, settings: undefined) => { The seeds files define how much and how the data are connected with each other. The files will be executed alphabetically. With the second function you are able to create different variations of entities. - ```typescript export class CreateUsers implements Seed { From 79da010d49594974ed58141a51faaa4428e0b497 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Mon, 12 Mar 2018 08:22:59 +0100 Subject: [PATCH 09/39] Fix type issue --- src/lib/seed/EntityFactory.ts | 3 --- src/lib/seed/index.ts | 3 +-- src/lib/seed/types.ts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib/seed/EntityFactory.ts b/src/lib/seed/EntityFactory.ts index 784161fe..168dcbb6 100644 --- a/src/lib/seed/EntityFactory.ts +++ b/src/lib/seed/EntityFactory.ts @@ -4,9 +4,6 @@ import { Connection, ObjectType } from 'typeorm'; import { FactoryFunction } from './types'; import { isPromiseLike } from './utils'; -/** - * EntityFactory ... - */ export class EntityFactory { private mapFunction: (entity: Entity) => Promise; diff --git a/src/lib/seed/index.ts b/src/lib/seed/index.ts index 2d057a77..e3ef9049 100644 --- a/src/lib/seed/index.ts +++ b/src/lib/seed/index.ts @@ -1,9 +1,8 @@ import 'reflect-metadata'; import { Connection, ObjectType } from 'typeorm'; -import { Factory } from '../seeds/Factory'; import { EntityFactory } from './EntityFactory'; -import { EntityFactoryDefinition, FactoryFunction, SeedConstructor } from './types'; +import { EntityFactoryDefinition, Factory, FactoryFunction, SeedConstructor } from './types'; import { getNameOfClass } from './utils'; // ------------------------------------------------------------------------- diff --git a/src/lib/seed/types.ts b/src/lib/seed/types.ts index c931bbe4..b081ee09 100644 --- a/src/lib/seed/types.ts +++ b/src/lib/seed/types.ts @@ -11,7 +11,7 @@ export type FactoryFunction = (faker: typeof Faker, settings?: /** * Factory gets the EntityFactory to the given Entity and pass the settings along */ -export type Factory = (entity: Entity) => (settings?: Settings) => EntityFactory; +export type Factory = (entity: ObjectType) => (settings?: Settings) => EntityFactory; /** * Seed are the class to create some data. Those seed are run by the cli. From 8167b7a305292d6c130763ab418fa65fae6be6d4 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Mon, 12 Mar 2018 08:36:50 +0100 Subject: [PATCH 10/39] Describe cli commands for the seeding --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 1a2d1131..4296d628 100644 --- a/README.md +++ b/README.md @@ -354,6 +354,17 @@ The last step is the easiest, just hit the following command in your terminal, b npm start db.seed ``` +#### CLI Interface + +| Command | Description | +| --------------------------------------------------- | ----------- | +| `npm start "db.seed"` | Run all seeds | +| `npm start "db.seed --list CreateBruce,CreatePets"` | List seeds to run | +| `npm start "db.seed -L` | Log database queries to the terminal | +| `npm start "db.seed --factories` | Add a different path to your factories (Default: `src/database/`) | +| `npm start "db.seed --seeds` | Add a different path to your seeds (Default: `src/database/seeds/`) | +| `npm start "db.seed --config` | Path to your ormconfig.json file | + ## Run in Docker container ### Install Docker From 21e4c5b18dab60843522ccc6231db8d64bf22839 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Mon, 12 Mar 2018 08:49:42 +0100 Subject: [PATCH 11/39] Improve ormconfig loader --- src/lib/seed/connection.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/lib/seed/connection.ts b/src/lib/seed/connection.ts index 1d6f031e..5d7481c1 100644 --- a/src/lib/seed/connection.ts +++ b/src/lib/seed/connection.ts @@ -15,19 +15,10 @@ const indexOfConfigPath = args.indexOf(configParam) + 1; * Returns a TypeORM database connection for our entity-manager */ export const getConnection = async (): Promise => { - const ormconfig = (hasConfigPath) ? require(`${args[indexOfConfigPath]}`) : require(`${runDir}/ormconfig.json`); + ormconfig.logging = logging; - return await createConnection({ - type: (ormconfig as any).type as any, - host: (ormconfig as any).host, - port: (ormconfig as any).port, - username: (ormconfig as any).username, - password: (ormconfig as any).password, - database: (ormconfig as any).database, - entities: (ormconfig as any).entities, - logging, - }); + return createConnection(ormconfig); }; From 4fba18e06eab3a9f43a9d69fed85e5cb7dcded32 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Mon, 12 Mar 2018 08:55:28 +0100 Subject: [PATCH 12/39] Adjust tslint to frontend boilerplate --- tslint.json | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index 525f68e8..aae115af 100644 --- a/tslint.json +++ b/tslint.json @@ -20,7 +20,6 @@ "ordered-imports": false, "object-literal-sort-keys": false, "arrow-parens": false, - "no-string-literal": false, "member-ordering": [ true, { @@ -72,6 +71,58 @@ }, "singleline": "never" } + ], + "align": [ + true, + "parameters" + ], + "class-name": true, + "curly": true, + "eofline": true, + "jsdoc-format": true, + "member-access": true, + "no-arg": true, + "no-construct": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-internal-module": true, + "no-string-literal": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-var-keyword": true, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-finally", + "check-whitespace" + ], + "semicolon": true, + "switch-default": true, + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" ] } } From 50671e63ac4a09ea752f1d35f9ed1ef51dd62892 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Mon, 12 Mar 2018 08:56:10 +0100 Subject: [PATCH 13/39] Update deps --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index d761d047..3a661e7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3480,8 +3480,8 @@ nps-utils@^1.5.0: rimraf "^2.6.1" nps@^5.7.1: - version "5.8.1" - resolved "https://registry.yarnpkg.com/nps/-/nps-5.8.1.tgz#180ca887a3bf8abdb860fb972d135310260580bb" + version "5.8.2" + resolved "https://registry.yarnpkg.com/nps/-/nps-5.8.2.tgz#17fc2e86f5f5577c54c49423996a63ef9299a889" dependencies: arrify "^1.0.1" chalk "^2.0.1" @@ -5173,8 +5173,8 @@ window-size@0.1.0: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" winston@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.0.tgz#808050b93d52661ed9fb6c26b3f0c826708b0aee" + version "2.4.1" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.1.tgz#a3a9265105564263c6785b4583b8c8aca26fded6" dependencies: async "~1.0.0" colors "1.0.x" From 5f58109a461255c547153d09f21b0c5fe24171cc Mon Sep 17 00:00:00 2001 From: David Weber Date: Tue, 13 Mar 2018 17:16:52 +0100 Subject: [PATCH 14/39] Update readme --- README.md | 66 ++++++++++++++++++----------------- src/lib/seed/EntityFactory.ts | 6 ++-- src/lib/seed/cli.ts | 8 ++--- 3 files changed, 41 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 4296d628..c034a5bb 100644 --- a/README.md +++ b/README.md @@ -255,12 +255,14 @@ export class UserService { ## Seeding -Isn't exhausting to create some sample data into your fresh migrated database, well this time is over! -How does it work? Just, create a factory for your entities and a seed script. +Isn't it exhausting to create some sample data for your database, well this time is over! + +How does it work? Just create a factory for your entities (models) and a seed script. ### 1. Create a factory for your entity -For all the entities we want to seed, we need to define a factory. To do so we give you the awesome [faker](https://github.com/marak/Faker.js/) library as a parameter into your factory. Then create your "fake" entity as you would normally do and return it. Those factory files should be in the `src/database/factories` folder and suffixed with `Factory`. Example `src/database/factories/UserFactory.ts`. +For all entities we want to seed, we need to define a factory. To do so we give you the awesome [faker](https://github.com/marak/Faker.js/) library as a parameter into your factory. Then create your "fake" entity and return it. Those factory files should be in the `src/database/factories` folder and suffixed with `Factory` like `src/database/factories/UserFactory.ts`. + Settings can be used to pass some static value into the factory. ```typescript @@ -279,29 +281,7 @@ define(User, (faker: typeof Faker, settings: { roles: string[] }) => { }); ``` -To deal with relations you can use the entity manager like this. - -```typescript -export class CreatePets implements SeedsInterface { - - public async seed(factory: FactoryInterface, connection: Connection): Promise { - const connection = await factory.getConnection(); - const em = connection.createEntityManager(); - - await times(10, async (n) => { - // This creates a pet in the database - const pet = await factory(Pet)().create(); - // This only returns a entity with fake data - const user = await factory(User)({ roles: ['admin'] }).make(); - user.pets = [pet]; - await em.save(user); - }); - } - -} -``` - -Or you could do the relation in the entity factory like this. +Handle relation in the entity factory like this. ```typescript define(Pet, (faker: typeof Faker, settings: undefined) => { @@ -319,7 +299,7 @@ define(Pet, (faker: typeof Faker, settings: undefined) => { ### 2. Create a seed file The seeds files define how much and how the data are connected with each other. The files will be executed alphabetically. -With the second function you are able to create different variations of entities. +With the second function, accepting your settings defined in the factories, you are able to create different variations of entities. ```typescript export class CreateUsers implements Seed { @@ -346,6 +326,28 @@ await factory(User)() ... ``` +To deal with relations you can use the entity manager like this. + +```typescript +export class CreatePets implements SeedsInterface { + + public async seed(factory: FactoryInterface, connection: Connection): Promise { + const connection = await factory.getConnection(); + const em = connection.createEntityManager(); + + await times(10, async (n) => { + // This creates a pet in the database + const pet = await factory(Pet)().create(); + // This only returns a entity with fake data + const user = await factory(User)({ roles: ['admin'] }).make(); + user.pets = [pet]; + await em.save(user); + }); + } + +} +``` + ### 3. Run the seeder The last step is the easiest, just hit the following command in your terminal, but be sure you are in the projects root folder. @@ -359,11 +361,11 @@ npm start db.seed | Command | Description | | --------------------------------------------------- | ----------- | | `npm start "db.seed"` | Run all seeds | -| `npm start "db.seed --list CreateBruce,CreatePets"` | List seeds to run | -| `npm start "db.seed -L` | Log database queries to the terminal | -| `npm start "db.seed --factories` | Add a different path to your factories (Default: `src/database/`) | -| `npm start "db.seed --seeds` | Add a different path to your seeds (Default: `src/database/seeds/`) | -| `npm start "db.seed --config` | Path to your ormconfig.json file | +| `npm start "db.seed --run CreateBruce,CreatePets"` | Run specific seeds (file names without extension) | +| `npm start "db.seed -L"` | Log database queries to the terminal | +| `npm start "db.seed --factories "` | Add a different path to your factories (Default: `src/database/`) | +| `npm start "db.seed --seeds "` | Add a different path to your seeds (Default: `src/database/seeds/`) | +| `npm start "db.seed --config "` | Path to your ormconfig.json file | ## Run in Docker container diff --git a/src/lib/seed/EntityFactory.ts b/src/lib/seed/EntityFactory.ts index 168dcbb6..b9931878 100644 --- a/src/lib/seed/EntityFactory.ts +++ b/src/lib/seed/EntityFactory.ts @@ -20,7 +20,7 @@ export class EntityFactory { // ------------------------------------------------------------------------- /** - * This fucntion is used to alter the generated values of entity, before it + * This function is used to alter the generated values of entity, before it * is persist into the database */ public map(mapFunction: (entity: Entity) => Promise): EntityFactory { @@ -29,7 +29,7 @@ export class EntityFactory { } /** - * Make generate a new entity, but does not persist it + * Make a new entity, but does not persist it */ public async make(): Promise { if (this.factory) { @@ -43,7 +43,7 @@ export class EntityFactory { } /** - * Seed persist and generates a given entity + * Seed makes a new entity and does persist it */ public async seed(): Promise { const connection: Connection = (global as any).seeder.connection; diff --git a/src/lib/seed/cli.ts b/src/lib/seed/cli.ts index 2c58a812..20b52fae 100644 --- a/src/lib/seed/cli.ts +++ b/src/lib/seed/cli.ts @@ -14,8 +14,8 @@ commander .option('-L, --logging', 'enable sql query logging') .option('--factories ', 'add filepath for your factories') .option('--seeds ', 'add filepath for your seeds') - .option('--list ', 'list seeds to seed', (val) => val.split(',')) - .option('--config ', 'add filepath to your database config (must be a json)') + .option('--run ', 'run specific seeds (file names without extension)', (val) => val.split(',')) + .option('--config ', 'path to your ormconfig.json file (must be a json)') .parse(process.argv); // Get cli parameter for a different factory path @@ -29,8 +29,8 @@ const seedsPath = (commander.seeds) : 'src/database/seeds/'; // Get a list of seeds -const listOfSeeds = (commander.list) - ? commander.list.map(l => l.trim()).filter(l => l.length > 0) +const listOfSeeds = (commander.run) + ? commander.run.map(l => l.trim()).filter(l => l.length > 0) : []; // Search for seeds and factories From ae07db41dc103e2362c55a1fda730d91c24ce564 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 07:47:34 +0100 Subject: [PATCH 15/39] improve util times --- src/lib/seed/utils.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/lib/seed/utils.ts b/src/lib/seed/utils.ts index 88b05037..98bcb7e4 100644 --- a/src/lib/seed/utils.ts +++ b/src/lib/seed/utils.ts @@ -11,11 +11,5 @@ export const isPromiseLike = (o: any): boolean => !!o && (typeof o === 'object' /** * Times repeats a function n times */ -export const times = async (n: number, iteratee: (index: number) => Promise): Promise => { - const rs = [] as TResult[]; - for (let i = 0; i < n; i++) { - const r = await iteratee(i); - rs.push(r); - } - return rs; -}; +export const times = async (n: number, iteratee: (index: number) => Promise): Promise => + Promise.all(new Array(n).map(async (v, i) => await iteratee(i))); From cc48b89a220be418e96ecf03e8015cc1fd20e363 Mon Sep 17 00:00:00 2001 From: David Weber Date: Wed, 14 Mar 2018 07:53:22 +0100 Subject: [PATCH 16/39] Fix middleware --- src/api/middlewares/ErrorHandlerMiddleware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/middlewares/ErrorHandlerMiddleware.ts b/src/api/middlewares/ErrorHandlerMiddleware.ts index cc3181b2..a1aa3e9d 100644 --- a/src/api/middlewares/ErrorHandlerMiddleware.ts +++ b/src/api/middlewares/ErrorHandlerMiddleware.ts @@ -18,7 +18,7 @@ export class ErrorHandlerMiddleware implements ExpressErrorMiddlewareInterface { res.json({ name: error.name, message: error.message, - errors: error['errors'] || [], + errors: error[`errors`] || [], }); if (this.isProduction) { From 6247735cdf8b28bd2d931fbcd768b5df9070e901 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 18:55:34 +0100 Subject: [PATCH 17/39] face lift for our readme --- README.md | 107 +++++++++++++++++++++++++++++----------------- w3tec-divider.png | Bin 0 -> 728 bytes w3tec-logo.png | Bin 0 -> 30301 bytes 3 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 w3tec-divider.png create mode 100644 w3tec-logo.png diff --git a/README.md b/README.md index c034a5bb..506f0414 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,38 @@ -# Express Typescript Boilerplate - -[![Dependency Status](https://david-dm.org/w3tecch/express-typescript-boilerplate/status.svg?style=flat)](https://david-dm.org/w3tecch/express-typescript-boilerplate) -[![Build Status](https://travis-ci.org/w3tecch/express-typescript-boilerplate.svg?branch=master)](https://travis-ci.org/w3tecch/express-typescript-boilerplate) -[![Build status](https://ci.appveyor.com/api/projects/status/f8e7jdm8v58hcwpq/branch/master?svg=true&passingText=Windows%20passing&pendingText=Windows%20pending&failingText=Windows%20failing)](https://ci.appveyor.com/project/dweber019/express-typescript-boilerplate/branch/master) - -> A delightful way to building a RESTful API Services with beautiful code written in TypeScript. -> An Node.js Web-Service boilerplate/skeleton/starter-kit featuring -> Inspired by the awesome framework [laravel](https://laravel.com/) in PHP and of the repositories from [pleerock](https://github.com/pleerock). -[TypeScript](https://www.typescriptlang.org/), -[Express](https://expressjs.com/), -[Winston](https://github.com/winstonjs/winston), -[Microframework](https://github.com/pleerock/microframework), -[TypeDI](https://github.com/pleerock/typedi), -[TypeORM](https://github.com/typeorm/typeorm), -[TsLint](http://palantir.github.io/tslint/), -[@types](https://www.npmjs.com/~types), -[Jest](https://facebook.github.io/jest/), -[Swagger](http://swagger.io/), -[validatejs](https://validatejs.org/), -[GraphQL](http://graphql.org/), -[DataLoaders](https://github.com/facebook/dataloader), -by [w3tech](https://github.com/w3tecch) - -## Why +

+ w3tec +

+ +

❯ Express Typescript Boilerplate

+ +

+ + dependency + + + travis + + + appveyor + +

+ +

+ A delightful way to building a RESTful API Services with beautiful code written in TypeScript.
+ > Inspired by the awesome framework laravel in PHP and of the repositories from pleerock +

+ +
+ +![divider](./w3tec-divider.png) + +## ❯ Why Our main goal with this project is a feature complete server application. We like you to be focused on your business and not spending hours in project configuration. Try it!! We are happy to hear your feedback or any kind of new features. -## Features +### Features - **Beautiful Code** thanks to the awesome annotations of the libraries from [pleerock](https://github.com/pleerock). - **Easy API Testing** with included e2e testing. @@ -50,7 +53,9 @@ Try it!! We are happy to hear your feedback or any kind of new features. - **GraphQL** provides as a awesome query language for our api [GraphQL](http://graphql.org/). - **DataLoaders** helps with performance thanks to caching and batching [DataLoaders](https://github.com/facebook/dataloader). -## Table of Contents +![divider](./w3tec-divider.png) + +## ❯ Table of Contents - [Getting Started](#getting-started) - [Scripts and Tasks](#scripts-and-tasks) @@ -64,9 +69,11 @@ Try it!! We are happy to hear your feedback or any kind of new features. - [Related Projects](#related-projects) - [License](#license) -## Getting Started +![divider](./w3tec-divider.png) -### Step 1: Set up the Development Environment +## ❯ Getting Started + +### Step 1: Set up the Development Environment You need to set up your development environment before you can do anything. @@ -112,9 +119,11 @@ npm start serve > This starts a local server using `nodemon`, which will watch for any file changes and will restart the sever according to these changes. > The server address will be displayed to you as `http://0.0.0.0:3000`. -## Scripts and Tasks +![divider](./w3tec-divider.png) + +## ❯ Scripts and Tasks -All script are defined in the package.json file, but the most important ones are listed here. +All script are defined in the `package-scripts.js` file, but the most important ones are listed here. ### Install @@ -158,7 +167,9 @@ All script are defined in the package.json file, but the most important ones are To debug your code run `npm start build` or hit `cmd + b` to build your app. Then, just set a breakpoint and hit `F5` in your Visual Studio Code. -## API Routes +![divider](./w3tec-divider.png) + +## ❯ API Routes The route prefix is `/api` by default, but you can change this in the .env file. The swagger and the monitor route can be altered in the `.env` file. @@ -172,7 +183,9 @@ The swagger and the monitor route can be altered in the `.env` file. | **/api/users** | Example entity endpoint | | **/api/pets** | Example entity endpoint | -## Project Structure +![divider](./w3tec-divider.png) + +## ❯ Project Structure | Name | Description | | --------------------------------- | ----------- | @@ -212,7 +225,9 @@ The swagger and the monitor route can be altered in the `.env` file. | ormconfig.json | TypeORM configuration for the database. Used by seeds and the migration. (generated file) | | mydb.sql | SQLite database for integration tests. Ignored by git and only available after integration tests | -## Logging +![divider](./w3tec-divider.png) + +## ❯ Logging Our logger is [winston](https://github.com/winstonjs/winston). To log http request we use the express middleware [morgan](https://github.com/expressjs/morgan). We created a simple annotation to inject the logger in your service (see example below). @@ -230,7 +245,9 @@ export class UserService { ... ``` -## Event Dispatching +![divider](./w3tec-divider.png) + +## ❯ Event Dispatching Our we use this awesome repository [event-dispatch](https://github.com/pleerock/event-dispatch) for event dispatching. We created a simple annotation to inject the EventDispatcher in your service (see example below). All events are listed in the `events.ts` file. @@ -253,7 +270,9 @@ export class UserService { } ``` -## Seeding +![divider](./w3tec-divider.png) + +## ❯ Seeding Isn't it exhausting to create some sample data for your database, well this time is over! @@ -367,7 +386,9 @@ npm start db.seed | `npm start "db.seed --seeds "` | Add a different path to your seeds (Default: `src/database/seeds/`) | | `npm start "db.seed --config "` | Path to your ormconfig.json file | -## Run in Docker container +![divider](./w3tec-divider.png) + +## ❯ Run in Docker container ### Install Docker @@ -446,7 +467,9 @@ DB_HOST=localhost DB_PORT=3306 ``` -## Further Documentations +![divider](./w3tec-divider.png) + +## ❯ Further Documentations | Name & Link | Description | | --------------------------------- | --------------------------------- | @@ -468,14 +491,18 @@ DB_PORT=3306 | [GraphQL Documentation](http://graphql.org/graphql-js/) | A query language for your API. | | [DataLoader Documentation](https://github.com/facebook/dataloader) | DataLoader is a generic utility to be used as part of your application's data fetching layer to provide a consistent API over various backends and reduce requests to those backends via batching and caching. | -## Related Projects +![divider](./w3tec-divider.png) + +## ❯ Related Projects - [Microsoft/TypeScript-Node-Starter](https://github.com/Microsoft/TypeScript-Node-Starter) - A starter template for TypeScript and Node with a detailed README describing how to use the two together. - [express-graphql-typescript-boilerplate](https://github.com/w3tecch/express-graphql-typescript-boilerplate) - A starter kit for building amazing GraphQL API's with TypeScript and express by @w3tecch - [aurelia-typescript-boilerplate](https://github.com/w3tecch/aurelia-typescript-boilerplate) - An Aurelia starter kit with TypeScript - [Auth0 Mock Server](https://github.com/hirsch88/auth0-mock-server) - Useful for e2e testing or faking an oAuth server -## License +![divider](./w3tec-divider.png) + +## ❯ License [MIT](/LICENSE) diff --git a/w3tec-divider.png b/w3tec-divider.png new file mode 100644 index 0000000000000000000000000000000000000000..209b540440e78b6603bb46d249e69891da99331f GIT binary patch literal 728 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 Date: Wed, 14 Mar 2018 18:57:52 +0100 Subject: [PATCH 18/39] minimize logo --- README.md | 2 +- w3tec-logo.png | Bin 30301 -> 26656 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 506f0414..cce7c42d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- w3tec + w3tec

❯ Express Typescript Boilerplate

diff --git a/w3tec-logo.png b/w3tec-logo.png index 8c2e1df5c54fa8f43bdd9714d985d11bca55bc6b..b9d594f7681d2dc1876a24cfd3a1e2ab4774153b 100644 GIT binary patch literal 26656 zcmY(q19)UX(>5GiyKyqHZQHhOXJgy8ZJWE%#x^&$ZR<;(=l$RR`{uf4rca;luCD6p zy6>(t6D}_+1`mS;0|W#FFCi|h2m}Ni_f;N*g82G;!~2);^#$yxC?*J0GYL5Uy1+LQ zSCjz)@*o8Q@(Th2dikpII|2f7rUwE#(FX$JOalT!x65i%;QqP+Z6~hb2m}O+{QU+7 z%E-d_8cf4NS=~upMw-*e_7|;zv8|yAt=liVuhu|7+-{s-#a|{)2Ka8jtZf`Q-FOK9 z(}MG>{9R2)i2t7^PF6gG>N4{9LbeVj_$;){wDg3$F!=cR+z!U3oQlGt|I_{JiignL z$;pnBj?UH9mDZJs*4Dv{j)8-NgN~k&j**e(s|AgtyN#298;y-4(SHZ||HctEaWrzU zuyeAowZZ=$*TB%$*@=gc@LSOTzWzH;Ckxa6Uy_aE|ETq)Al>&JItE&Ly8j*ft1I_+ zEvKA=g~^xX@9}vVxc}4g|Ec>QIox#L!vC+y{8!We)PAYT3&TzKzh>iwk?tka00QC% zk`NY9b^|{D3!SAc=1r!1ultylurD>7BDN@wg+nwFY8_bDV#AOT+^ou)#NHO`0 zt7~75>u-T?hn8R^MN06+#{=;P3I`_`+-7}P0vyzJr?4<_iiXlWC;eJ(J2>*GeRzFe z@T|^BzewlRosy2ND9UkFc|TFxtay1pc}rHEWI+J=`{%jBvtE_$5lqsGDgHV}3-J#t z4qq!Zc7=*&d1MshFI=BNxAXi`G)p6|AZ*rrtbx~f`P~!usqRgs9CTcGq0f;%>_wh( zoB1B$l?eOC2TJEcnB)w=Bk|`&uuwMv{pAn8EPESSBL1Fg{FXnl=qJi(Crj5fD{84i zd3xo7e{r!)f)!1F%r?rGY9&1&%}Z_nSca?i=WDqy*t^lmleU5DQs~4Uv<1V>>2KDf zV)AxHDx_34((`%k1z-7XUq`Jc7$RFn^XkL0!fd=%w@<~fhyexD} zNx}Zkb}<;lUBq-N0)M`CplpUJABNR)0(J!+RX7b97*WJ9n9aEB+ybLjD1d52YeoPh zfgFIKfN6;-=iXWR-n2sp;0adS23>K}V?Dn6C1#`fSFShCmUEHl2gVv+X!mjeKZ)Rh zb&v7E9ESic1N|P~Q1|7*PRKU2ywOP9u5K648&c)z+0b8T}l<=7{__F>?m)sJ)qDRBWz7cq!U*2n?H5*_I zQz8V4v^H`HtAJu@n_4$5whhCE#%`K*%tyzX{=-#BZ&#vp&m3sIZNHl@Zw|FV6RJqY$KKArKs>ZFx!T8QGBS;rl`Z1}i3u;UL7FjD|< z`lZVUl1ADNPNE9h^~Vd3l{mO3P8^xC>5>=N0Zjjd@CWz7a`oB?|sfxy{=M$wO~) zD%hB@rei|Tw1pf82j|OPgJR#-9~WAQEs!eaGZ2--J=BHisJ}E#h)s_i8yanpFV@;vh+lqJ zUB>Q}a6bIDvAh4bjnkvgg7tm-w=@4;4B6aFxy$^@;`~!0{@;$Ya9g8$^l!r_XEXm^4wJV8Pl%u`Rfv^@tB3~d;h_URUHI$mF^ z#&7u5B>^Kz1qlY#lc10pGC`US?3Mm33x@P?{|(7zE7E6BHySS8{>JCvK(~eCC)b%p z2{`ygb^uB>0@opu9TYP&NX^eF)rWfU7>;-oHpI6?8{H=t@}DqImZWL+b$Kv(@Jkq5 z0=NaS^voetzjdb_Y`wxz+JFpUa#Fkl878H!I*=xN=&k=^BNIsXdQzr(b7x|=-i7nE z?~s!&`1>=pL7jaCIA&sy1*obJ>7y(oP8)x#OWWS~MBCszxgIAja1TsLo$-)5$n8_* zMo8q-oS%-D?8@|TazY&32zasinb*umy^MvJqUiXcI9KQRzA+4@m@jyz2JIE721J(`nLzo1w%JeRHZ3KuOqg{CKVflAjZ*tMKuQ%dv=s*JuTQXylIj~D*vW{r=|gpdtmBBCMhI&NSKe2hh8 zQvF#ZBNSWsa`ZAjkQ#7x-zcdeq;zgQpSc<5FKw? z=GS5R%-p>qA%Ul~yvJ+upQ{t1bd#wzgDNl97VJ>|J_33CGOU=#=N|egh?y0qHSQ~w zT8kd+I?a*E;JTrnXJ|U`ftiY4gFD%#P9@KS#tF*z==|}OU3Z|eFpqrv7KQ5raSrjx zQM?&R_pXAhs|Z_nQfM6~o1t0hPlqhekJz*`pY&+D>l`Q}5f28r!UdI3rqj#Ah$eQQ z?SXguuON*D;lI;I0^j$+tv?TQ4YaF>2R|g+1x&GXr6B8mWY0_|zMWTc*gLc3h}4?v zxhI6}vZo}y8&uyropv1-8o=m|S8RjdpRaB`*JERt6z#p}9&6HucRZ4&c7eD0E>c^J zBV<{ca1l=bPrkGsgNn_ITUpvd#`%g@KXzwz~)o0AFM;qoRE@}rNL&Mg|oF>MJK zUVq_%DJS)#QH)Us^2S=x{;oF!Eg3>b^yA;dP)!hwyNj5f=%Jmr)X-7_YFq31Nq=bL zbmQ!iPa6H2BK90@(>GOEg_^e-n6NIbtlx1Z42#6f&$u%H#4(4#)u!XAiDUXZYC)mFoH6dhJ(q3nFH;fbP zD~O16BN)%Kz=(BdgL&Y0gmy^%#(mablp~hz(8^?OGMyEXG}bWPQq9(VdTrpOE>G52 z8V<{et{n_51LA;~{Kd zA?FL6_;M)}3;Y*PgHY8oFwj$`H!eFP7&3zkozQ*AxLBREM|ZMH|A5dLD$5(Hg`lc6 z%2Q@Y={S?G50+(WBGw%|ABa{6&1iVJ!#aUd4ELAfy5XBy(*zx<)ADGHqqmg-H6#iFUud zMLPT|<}39&{`P6Q$r433*!Ko!Wd(uaDoO?-ToQ*;x?KU_cwDY1U{Y2tcBX)(ITL@! znAxvjtcaD__y`M6-@C6o4Lndk+pWy4EvSQJ37anb$q{~VZa)(E`YnO^Cy1;+v!cxe zM(v_R>onz~gZkkw0XqLzg`bM}Md_KhirujU+Mu-*N8w%&&b%Xjr}CH{Z% zAM4@GV(T6a*w|$6w67bX|2k1Y)?KV*E<5la@R_biOAT4!)$Xjacf=Gu~f$xI2=9?HNGeRU&-d#Y0? zzYx)u2R?kZw+f?Nk4&bPGT_W$`T{L!C}R8qJCD1QZj^{CtFOsjP)KbLIli|9&2-up zb$=$ZW&LKIl4%;;5o#PBEQ@8MH6Ah9{s*>p6?M}6goG8%!S-z_hKuoocD3}?CwTs| zOt$1{*FK(2|46qjZZ!V-Q(5SfX+lC$AO4Y#2W?bx^~dZxe7gGv&TWLb)ZFwO&mC(OvF~e zP=skIN1W_C1UuvsSJaj*T~#za+v|RYo%H?u;qEMTH=^_H(W2jc31Up3dX|0C)_Tb} zv_8N0%A9WoRf$xHv~p%{OaJE`3 zNYT4BZW0T4<3-}PD|3ALy>iU*S;%m-L0-6GE+>#T>__ImdPI=aM~c5O7D6hQJ$p_S zB*7%@eVq!mzE6GY?-7Spn`HxKs`pMh;-P**`m_Xbd&bAwl8fAjCIy0NOd}?EWR;mK zN|bCKZ*dB;#(RsZ8`L4njxAcrI()ZjTM)7mE^S zYxKhlYLgsaaqRXduv+V($?r+Kg1RIOEhTdh5hHNbSVjZ6AxOy1421bhym4zU^%U$x z5;Jc55T-$qL!>e~kQzuuJ+D(?oS}#*p3sIMod}~+hb3+d4;t9bwEKnkF~-14qyd5b z?U1Ge-5QP<00;~Im+QiBwdNuJh9)YtDUHW<8mVZ-#|ZzQ+oAKU&JF;?4Dvhb8S{#9 z%^dPxQtRo`-*`=0S$?A4zlC#gF}@#ikuW|oP1+wJkwbpP|51UF=W<}+sv8x~V1nwZ z#fGB+%DYbLK8>^R>E62kAV44C7NuRGiB5!=*ECEmwZ~2_g^#-EShUZZm=RIqvA*aM z93LV{!_|w)_SR5v^7zCah3$?#>$0Q$H!>s>eQCO%Hem_1L@AtejFxSwx+3?SR`wwI zo^+df6m zEWtJ~0lT(jFvqG=Jk7o{`ceLaVh*%pDG#0Z0#SN1=tbD-HOEm703F`0E|0r```|YXfb02iYrC1T9N|}}rzPuuVOya4 zo*#?2@MM5gRiv6>r3+wI)%A1dRZUrGRfKYQdw8{;GdrG^B3=Vi{c>Rttd=)H<1w1U zP18V|ElaUdZQIca8`r-jf!RxNvI0!Gp|IVzePVY*w;1B3X=TZ19!Tru4|z_#ZrExD z-})anW9lb(oRCYEiSx?(P|#eH-@MNoP?Lx=6Tdn!x!(U0;jKJ52n+L?38^#bzIiyy zO)0D|E{fr@GavnDBf*1IaZhG9L+O`M3T-bRmVXqIVo54SGQGoUCOwLgGUIiF@puqv zJczyp85HE1J~Pqxx}6)P!}C=*(goRDEX)}t8|dkEcvPD=q*yP{Zp6!G;&8dhM{<52 zlTZY@C;U->_s`r#UX^E0{_y-*PPNZ=WuEi?iSagbGv2LZ5cgUj%SOk#JX4emRbr^T z)l7Z0a1_o))QMMqzG@Y`K?zMBk&}aaM~WsS@vF4J@WqW~lrqMP+v;DH1;X6%KIgQR zAZksBy$9U!ak2-y=Y(Tl&(PMjFV#Yz$L!&x<5b~+4FCiUSe5;2adv-fMbv7&cm+F- zr}~=fv3cOTQg$5^RxRy-c_#40%fF@_A`U(jcl@IXWvmqC$1_Qw8X0w=?mJWIk0>zj zX0g(BlikYJ-Y>U9{l`)ZBeN8oY@w4Dr4b!>NHi@N3ky9h!GO_x>xIWtd-Zv?x%f$T z)LfM3_QeY#syy9UzoFX9@gh~SUFSMk>9T|GeHe1_p>`6k(}EeJ9hNMItrE$~x4yYU zc^YQZiX0I_BTkhaKe77p9-qPd<KiiV(JE=ic$EH~X7z~IPK`_H~ zhN`U3vwj;5k+{1^V3$I$g)*#>vdu!aI41`k{RUE!L7@o( z@PX;wNg+Uv=#BqANhW+Fj`4NY<9yu&t-%~FBS>&Ml9QbmiIGu+OMBLLLiK?AOWE;f z>I@^e8?X2__9Gf1*35smxSqm>YoIibbIg(Po?|` zLry*l<7p^KAwAoQbVG`(YCTwVv!OD<;T%VV8CA0z1kQyqa1^9vc|UjOemHz-MEd1n z_-W!RJZ#)w$@Xn|itN=Fi9`gUcl3B@WCT5xS+O((2mmEXDa_f=*uUYp>MS~I7wzAC z3l!vnI}WQmb5X!H5I@!fOpd4XHL>f`e`T5ym)@?)1Tt-z!JsRDk9bM62?SMIJ z!hM49jRq5`*AZ)%@)78NsX(W&_I{dNG2IqJ&F~e)I4{Bb<|r;W4Q{R=h*Kxi09d6L z3wf{EqFypQT!0tAr=THah$kl1J~^5TRlMjKvxYVXIs-aYnA0?DF{+f&FQ}_1yLGw5 zfDp@m)KKX-7|r&NkcG9_wV=j|%jxz%qN!{*F~HS|C_wJFe;~pwHs{>fP66S za2M=$fI*`%QALU-dvDzP`@4gkXR|UEJ3#V>Cw2k7u!Z&N@9O*VQdw=>mjjsL^wgbw zG;t(NGIqWqk;WqweF~of2e%LIA6s~Mj0kGxsr19}LL@w0mggZu*YAg-?tBA1@mxxj zuaWur`3fO*HRvX_FK7-zWu*KcoT@eDG&t0(JBm9wE2MoW{E`~uk z90scHwr>V?p0DQ>dP0o(RU}hXsN{}4`i2+TRmW#b-rwg?E|;_J8Wnr%ek3aFeDB30 z-}t+ZfRrXDo29ycU@y*YZh@?(Jj@Mg2noXI0~tMrV8Q)Cd<@zJCe#O4a!1GNRSM9L zfrFrteOR2AZPZO*4?wCZ*E!={Z&_6q#AFRMSo*auqO5 z(_^2_Wwtspk7|oycwX-JvwA4nV5f zz&&xnIB)?hak-)H4#YagFQ&UhvDLD(+HQDll*ex^&S<{Nx%rYj7r{l!!iO{<#s3(C z*WG->_7n`kNX3sDQo_4DW>PU$qg&@h@{T}<{Rn2Lo-N@JX zQ(ia=*AVOZ>8_#~+cp&9q#u2yv@EHdJHHou+NdB?;PSdoqU&7V_Y>KTRKH%I@=kZm zJBz&5@qBP9KTj_hoot$=u#S4nz?zkR1-&3|<1)Q|GrVnzVZFgT>2L(?ZL(%EHN9jY zSLGP|H|qgtp&s3#{}A2I;S*LDU5Dr<@hZ^e(wy$JDF}XNZS=;#%U>2MEX3K8- z@h6{I6$uQE?%KAB`6!i}^qjE6eI_hJM;UGG%3wIDi_g~Jb!diYTa>snMXrT4gUAKM zLQG7|s_lL44?oyn#0W}%HLfcK=*E&Ww%PS_FCsg>>G&g%xp;&wSX8$oLT67t)XJn_0Auv}f>xh%+CU!}* zer9xGbeJ$C=etke<@m?bG+}7jrw5L*ok5YLF-#~z>o2&7#RI)?@Q9|uM&+VQ*X~h0 z@*lUHE;EAfsK$WwBqD$EA79>2Rg<}?ayA7m^kIUulZk2PuE<<#))RI5c6TO&)*z z26bHiIxNgfg6++Aol~BJpT=#m5@+oeBWpg81fX@8vi$&O#;I6?=!iiT_m3sExdnfR ziMlIDYdS!(WfbfS>>cJU?Tw^U5>U#VAtH-lqJ{yFS29pPA5at=VCX~O_CtUBJc^Oi zNl$J?Y=5bA;QeN43|z>)DSYIy3)rmIy-Tk{3TQQ<`hongW%5tnq`}LWlT~1a^v!+j zvyAiDGcYfs6*bYTbCi#Ij~DA7O^Wh9qm*5}-HvBD>>-Vm;IWOv4&++FMl%*kcDEbp$&51u^uV62`@WX#K{)}D5fD&k6ytx-ds zt!reVs`9Z81o{cv<++i>@Myzs_qcod5V zGWC}nHTVGxUfkLACsjv=qDsQ7;VRP2^5>hlFWh*t!cJnpKXLhzvMgUCCIbzcb^_(q z{ImLxa@w`B@q*$gQ(CSVx{|Ml<{X<+@BZ|Xjug8GJ_n4iw?S-F^B^@fn(NKmO7cCq z0^sUE`6-cL8A*#AX_})ys>I>zh6J1gHa)o(t>17ucI#kFq-JlxiLLBi%Dh&@&so16-pRpOK`X+)g}oC~aDdAB0>Zm~D%WT`tMB@9^EGJR zK<`x36`NL!z0wkd)j#Jh1i~j>yGB(HoRq`mz3QLR(`5+tU+d4Mf=|J`#6F8C*Ir;y zwMUHPkZO)O1tSYJyJsHxgJDl`OUEKLdtttUEEAYaFm7gsUX{Cx5!K7Yo9G;grjwag zt+5+4D2S3u+|dN}V0yx^iYTp}_OLlC5kV}c>l8ruRNHGx3JOQbti|X$w+NLb5$QHp zA~QCx?uM?(kUA2V{0qJ@inYLqC*Y?5&h(3%nyYhH;*`0zqTf^svPMILRbMTnfg6{x zB{G&X$NaPCk#m2Vf~?WTNEVE@v))}p*_kN>)ROqopK1_^I%wE{S;1ksqF6l;G)Jv< zm2qr*+i_=BljeWGQsA&jx{Rp$Dp!C)$a5pS~0ZEJAx$DuQ-4*jj+0I*#l zIYXArNz=B!$n#ZU#4CmAVM2?ae7hWE9_hvuHMpjfD5bMER$DhzPFXuqrM_T$Aq#mw zjyk%CJ1lzKH?O#TcWY~F8>;Fj3^7)2Xw{`qB$y8}&R~N*Hu4nBRjaB5-W>gscZ`hv zf#TbaYlrG_+A>td1Su9xU$&nDNqvC63pw>6xLQHr9NG-clWrePqveykNs7LK+8Bqb zKYjwo4!uE5^qCDFkF(fV{at0OX`38n&SU(KTO7xr3HPZGsHKaGixMTG*461T)2YBw zP@Wz4U10&ike*<@GwrTUsU&ejp7&$>-Yf2;{s;Yqm-|}3t@jT29<=P?xnt;Y8a&2N zV1?cdNKYqaKN1viy7zNU zjr%(%o6uvjpO&@}t9{?6H>+@x-*K7O(uy1%@lEnMvfyp+y*~GE<=y*_9|<5GFeZ8N zB2{>Q{FSb$8K_73`|yI1GgYnAZUMS-q1W&e1-1}d*&RQ;`{y*~EvUo=iV|XyxiZ?u zWHuK=c#huWcYZ8wSecqkbS!kdA9D1n(-Ixp1Zt0pKNB00BFQp24-OIAW-Xa46R)R= z=dHw&cLW&*t$|+D<@v_x1yQ1X>lGgu$g9PO|11w0%9WC^CDX-254u3 zuc~{Mss>rHa3p9=<}v)g_NB~ER~Ng13!^_`!WmtAOH@0Ox@ovF zMuFc#X{kNjHQ|H7s?a3F7{CuYq5DlIhyOn5QDa7QFzGj{*3p+RQ!S<^F4=_-!q1Ka zb=}^fbz(O&d8WOM*OWTbw677`7e(j}+trHkGVim({n*Teto9ckcO_;q(1l}V3)h89 ze&b>?wF#E#MYnTvb%fdu7jd=N$`;A_LJmVHOG$JhEpH@1Wj^%^5Agf7%L(P$k4PH~Smzb}p4c6^X z*pD>w6@Qjog6JKDv1bsSF#=EBFsBPwplgCWCuW?7fQVZIG6OXKA$KJBU2WIlULoFX&jzh(tn+6SY&?=9uHnSx8AwD( zZR!1us%*+uurTh&yi{HORZj#ORyKyL>@{K}1mM}EBYA-nWw=Vc1`fi=* z-$KXt;v|myqhL1-nUU(1CW~;GsP)C5u&smykrd7AuI}Up;XP;u;th2?f4u8d?l`Zy zOY2ZSuNhmvOhFMn4A5Yo2v25#G1|>+!;e5<*mg~nq`IxD&cF#GIw>Sb1Av8jh(%2$ z(vYllP@Sia&h~aieJ5|+jG7SnE3_--2vsycEm&9Tjt{z9^iH2ru}%B=vVk{x63(`a z`6g)UWzI(F_zfH7oD1&gk38Z-39cB#0QYAy5a1HFeEMk>we8_>KuASrY;5k#A?@4E z_9I;&4=Q_2Lxdc=bVly%-ATO1peXRL9uLkbFszvKy#b>ci9lKz4=guu1Sf?)m%$E7{l>Q$4@F(KbPFO#ON1X=1D5 zYqLYxVXXvram_5x;N>`m3q@Z<9|^8)wb&5M;YgF2ka0ox3@5LmP_Lq>Mf%nYDIZDT zffEh=XW~x2&`bs81%?jWkS2?P_OGX>9=r=|HFm~0ENZIjimeF|3s3XHdDrAv!H;uf zYf1YKiUIk(7qsT}P$p8PC0ziuLILInEM9nU{sF}EHq6qxn8)pV?F*#FZj~b_tp6Emu&Yb?wH@`!PT0TZSI09>rl5U z!PGcVVVY_yQs;Wz>?;BedGi4)<^F=kjBndwxgKnL1FG{TzdI9EajD0rvFofL2K-<7 zY;OX5mmAHdj9TdSA+GWfyX30d4>E$cnWp}t9hC7w3ADtpf^Um6rN0PR=8+UaOa67g zFOk7p?y_1m=ztA-s{w&wru~?+q;^=ZCq) zUyH}VQR1L+6iG{=&#v1wlFjPWG}dhhj!{@C9uob_9?`V|Le>#t&<0Axdm7Ir$W@@x z2W~9HOpoR_Rh(MDG#!)zM){R$f7lDCn;-N7+Y8w59?bQnVQbnMt7kE04Fc{UsN{GD zzl`sNvNs(Ph<4=C2f3$xdcNkcT%Y`i8hp~&GgY(i*CzHhp-UPQv>jSo=L7Bt&Ignv zI5C5;>}jV7p1#@3rwS_CRWl17d)wj7jclKJ7E-7Fl&8q2cx&XfGY`Uk{Ky}%8aZ3O zja0Xnf1>YX$3Y%}qVx2563St_5~Idp}bT-MjX9TnZT--)W9YksH}62rek^l`{-E05=i&^O+}*IiJmLIMwx7ijF+Ch z1yL8%wm3qVQcBhcl%R;YSXj8!X?)+yz?J~x-$@pgT*hx$gzcwgkuq0$+O~QL9q8p3` zyJFZ?p}k$bR@;#Qo;y<1N8|HdPV7Ao$w+>5D{)$ z0@6nW$V<7AJE0!`6oM-r?=?A1VRE>6*eeT&v)Mxy$m27Hc(8=|mIGwC{h{OdtwIm; zxR>y7@I~I!q1qjG2>&6j4X`7c)cAm)e2VQ}L?G~qZC9+uh)X4WxQU(CH5(YLzysxj zGE&o!M|Z4c9NJ)VcO%dflIcN3Z&}bZqp6XN@ZQS2?z517ET(v9V~l|NnHG3EYU2yV z$~XMRJ_lmG)K1lfl14KA6aX(yZa|vLCFL*7#`GC1!9-{V%jx&LRfZy*t=Jw$zq0q} z$U$mT*4_>Lq{A0T%NsNHK?STHVrf85$IG*(8iJTH3~t4!iYI`u)BB(~!n}Aj1r@+3 z5hUMqtcE)k|Et#S^4ZT{Z$~7+uqS7e?SZQfy*J0p#cqa3l*eyM2v@})Ja~OY6j%Jc zi9xtodT3SA&|{04fAGOf3PNqWU7?bh!hiMlQ&c}a0xYQ3ucu*vF*Nc6`HkU>L0CwB zhrpjN43t?#Z&I%KgE)0UaVgPvP^a=O{@TcVo(6Cj8)B)B4o<8+=DbIAbad3+pgt{7ReE6#=5LP7*lAQ;pJT+J3b_fvTsecWa~u)gfz9K@_AYB1|&tHXJkI^dZjr<9g9YB6% z>Awh=H2G4-kb+*eJJIq-Y8RMWjP(G2yxO2YIsC(xADE(18mrPO~txhvx5qI>pmx1h76s{iukj7qcp)RnxQ;=SP7!xuqht%U&X?3qfG` zgv4g7mm3`2>JM8Cmxt~#1(tPx!H%z!wZs^MF;OqkX}w`gi%X`ZWHRF<^Qp5x4CYVs z#8n3xg=WK9FYg?>#n<{*z1rYK8H?Phq5HnZi4vAfZWn$9Q&2T7-u!rO4)3by&glOR z;TeTB3_s|ca3<+)g$u2gXHyk}MSVl-FJ6R>!1;Kw@i$Ydqu4$PXkC$5nB#o?bsD(H zmsBM-B+s3kfQ@O5BTWsItUTp- zD?G$G0y;CSh!e-(n^iNeh*Z z(NNq6hN%`;mtkqx^&B#Sm>b$gKD;+*Cs|^sCV-_FS+54DE8(7&pIqQxy`n$PH(yk9 zI0#o6;Li%pNrv1gA;*$t6*~V0?J@6)#7J(%@Eb#+K)GM|%B-VY_8 za|Ox$?!z`JY3XF#iEXJrgxsa0RiG~p_N~%|Yip%5 zPh0jK${h{nd?lLdvQ1c_heTmL32=@Ox@mE>6_ZML5bT%C>qurbQ)P)&kpu_`%JFkR z(<35#ZQ+dlu792fAENq*Z{^5Hle1K9srWQCG}Pw9+|G~st#h->t~nfqg%222LmP*L z>EOVfj1A%rD)dUkD7_Zad4p@+g=Jk*I`6e=cFqhG?c9{{mpJ}}oV}kt=2pGRPEK>b zDtv$4k1{;aXtpTF*F0UKN)>Fr=Bepn2+91Oi8a6>YK2dB88?a{q4kDT)^C91e{7YdEv z9L|V-h4uqMevPeu@IrIYn%jspFSL1W^`p;-V5prVo-FA^ePU0JCSpUZIcEd^d>H^w zwEdWnk$IMEWO~zgzJ9zf8$3#f>292nA7T3Jk+WLF4|1W$X|)6(fNHQ zPU5A4B&PFMmMjXY;c-|95l0C7$)=JoVJX=LQc>WTam7ObpkxXotKIqUV15200)Jo| z56utsX#l3KDnN;!3d>+1uj%xPeY5iztd8b5WZ-b7$E6#N9VM2MrssMC0%a#-%#iCgzZjP@Sx)y1roE{ zE?Vt}=)9+qtTJT~86h&XN#n)QZC|}km0^cCH&Vf(76_^sEc_G)$GtnfckKd+i+#6n zNQs-NhftP3kkXxiaX_&-qOdrAOtriV6wxgnoK|Ea8qD-Zx^+zhPi`Grz%6anxc_*& zvPu&EQjFo$QY#};x27n^ZZTYKJUUlxcm&3y_9!CfUpX6~eCFs_qr8qX^xpU2#KS*&RFqFR~H zga~S8Gs&+Q&#SG2daGX9q8~0Od2g6$;4LHL;D{blQyzYWzIL0%(g360Y9=(*{#`vE zx?y8XA!gm=Z6TmW8~;tNg)R9sE~_w36&F$0d*vQwA7DL(2K?JRH#SUhoH>a~3{&3U zP2J`d3?j!z-h`@?nEc2`Owhf+8;i@`&l``%y_Th`goenSu`6DAECMz$dAt<7|3V>N;2A^-bZtNGM~ty1_edO@Lw0e4MEGP z)VAT5Z_zDMPRpdre8g@ioQBYzE!G_PzL~|aF;XQC**`X`-eS}3Qr4gE&(`qjrwevJ z^j{8ONW04qDz_K8d(Eie9;f}@qw5Q08F%`F>49~*T+=Ir359)qLG}3!)euY;GHsDK zJnuiirYKw5)G;5%OMBPl62upXsg32*;T=|?WMH`1Akevj3)=$W*xrM^?{oC_PB|ZV z*Pl6uD{O^Yw2vbXQ4mMKsIupkJWsVDe;>QQUJeV({e!4Z#q0+3^!jq2wE3B0K|G8H zdVg!^RH12e0a=*vGcNhh@xHKRDPZRpVjSR$oRAHV_^-aOe2b~zZCJM*@=21(8yOA6 zR9cqvVX5^)N3MlI&#AJRN#*AhJAOVqCsfqAc}?i3Hw&b)lC0J#{QfDZsR0A;(YKCg z2rNM6I-hP75p?={C^>u6iy6&d>(MLqxFUf#p8B_5?DtZx{P&`XASMOlRuh@GTX{0q zPwifx<&1`s*|b9_lE}(>Ak$~~Gu+qiqytsJ_Dn9F^|_v`>`Pw!l3zt9FvCtv5P)~- z8H-z`7r)=8(QMfbtTUpo+K~F5e03vSFwg@jt^WW=X@;9h${m}++mh#99l74ZG4w}y zcgW@|_}`s4VL%#i1upS?M!Bf?Hrp5Q(??^_!Gk}qj8*WrJm)g_UfS3{u}a~mn(;27jSg{4DL{f-!~Ct_h<9@_}5P5W$q77ebKNPCfYb>>M+ z*|Tl#j|%PBwY+H-frg9koOc&=c+2tNEPy3sjgL&S5UUCH{_viJ z@51bITD7&3Ask>^nftFlCS?H9|7_$Z`&dT5XISw*>pJZ81h1m))F zeo8m9V?20Y-C;gHdY;l>z6RZ{%I4nhIb9>od!d(5x(qRF7JHbbSQA<(kxx5|#-V4zHWHlj8#Qy#pM1R5UQ-_L8~D2<-jAb4GWo^Qd64^Q z5(=?^qS+vbEu!WrO^#3xHvA(tKq@Pd=ehl9N@UL+_p9MfZ3OVKp^kXJJQC1Gw#ASbgcwS z$FY8<%4l2HDNwLGg)6TTFbOa!d!3xAsQ&!qs!8Ze-^IH)FEXI z7_P)~ePT!GPB3k_bp^RmT0m{l&Nxt_O)l}7b_Z~u2LXHz2Hd;7^KR-i;3c^il~5Rod&1HJz&9E!VLzd4WUr>ubdt^(_3Yh05p)DLD_*Ht zFby-NWhh`q8Fhk;LdIX&-&KwHo(&3e5+v8zrHN?jS<0J*3Zc!$Ad7*w!-a@>)#)Z- zk(uqzw(6?eFGbeL2rAz6x5axS%>bMJ4K?NiYv&oK?jsVFidO8C`AW`#c;^_u#Y$`tH5m3_seK=-wPRL@@meH zGpMmCvtugigAskLDRb%b-Lly%0{4bin`S;?2-LH(CuM0?tMN8ClY4<-3OJJ0fltxV z*n`KnAvAGvQW0h(5wOc1-81zhREq?R`6-VV8~hMsbsCM-PEy10yJ?w`X`QJMrz^#l zi)*eq?KgT{S zml{_Nsc$SKIji`N@!p?!Lb+zOn=_g4qF9@dUeKaj|BW*Hu|Vxve&5&_Q};7g_#VEh zP8pQDFUt$?Ab*sMwKemej9xB8ukDl*Pw{AH+@f@Ea2vn>EP!cWO%S={YRkU0qsmH@ zxR}QBUE#7^azM0}(`L9y&zxy_?So)D}GV7FjUK+u9_+xX239AxSlbm&T0OlH1- zc0jNqG4U*AuhI?5-+7K9>%{H2k-S(V z9E7=Oges8i31dxIwuVUvBC|Wkn>DEY4`0;K zK%2<|obmv3>=XnZr@(-PGG!KLhd3?994t2k!)7Ib_lJ!$J*bw{l*>#8U{WfxF~F1& zReOiWIMU>#3Ew760o<{p-`shH$)`N2S zadE3$^O(F%h8W?6j#o-7%r?=Cm1Fbvsr|o)Q;FlDnGRf(p^3yJX2>yo-N@ixA<*ku z-0kMsvD*7lBDl8Mgb1)e5=jM@xKPyE4D-Dde`I0#U|AwHRQH^DsHT8W@iSwR~XR@zfA9MBEA|-r!}4R5+VJ<^k=#om}MGnIP%KO3PGjSeQK5h?;*42 zep`(q!{Nr9o~N@$ZE+WEyk7(af3DDQ_EYw0oBXqW_7cCni8`T)0CAEMzmi?;?9(kC z=Ipgx(gKJw?SELhhQTudnZCJ0IA=Bs5rN+5yq9u!m`)Y?q-MI=#K><<3Zn3K&5h=h z&^icGp`7^4q;k&p1VzOw`--y?m<=a;N=2h?L*Pm{)yprv0#tSy|n%)Jq`Wv1b{sQ?q zA(6$$pW1aBmxvFTnUB(YBXR=#&T!^_jf{j%q8&7X2Vzh)vUm8F%ID@-JA&eAUlgH} zB)^)txR7_Z(?>7pVA-^A>gq1KyoFKz`^uv0)`e!5n8hj~>%G*&@bYJa_f8U2AWeGE ziGLtRds#`y63p)2jE8|7Txboh~C z(cXujO=dwHQzssQ_-oYL&1BvA))2QJC1qUnWcm)vwY-sCvMrL)tgc9N5c{FHY9GO# z0=7B!9tYnPnXj_-QdthQ3ZY!bEUt|DHJ4*VRYb$D((w^zVW<3dEw=1NWOC_{0+d8b zy^|+p?T{=+vEL=dkn0;VCQJ(2yQQYW`6sMt!h z9waflgOU4`wdvc2^x2kB-bxkElr*0t-My*x$eOq}5z zpnnv@?P9{~^5UhWuyzm4IjIiC_PIeT&uM6F85f4LOXVeqhB1qp7ZE)Pj%#{zQzG|J zkmD*?eAk!le68XDK~JdKtEs-g6Jrdt^EiTF={H}>(?mOy{@cN>EZNHH?TF3zKZ2u> z+%XoXp8y19_Hkcj?PCgWyr>S+){m zPLUUqwpXa25!&kL&=!g1XuN?xAns#OGsxZD>}F(!mypB{E3hlP3K`l)_LZY8hB*Ml zucBS54rwO*TKD(OnBo$PjVVOB(qG1WrNQlnWl_{urxpP*(;r$pui=bf42Wvk^*FuR6I7c@Yv1GjWofWWa_aUq ziO7KU3wzv5dia&rFu!E@?FJYP=Y4*59`IRC7|00QV%wo+O6v1;f(JdmV8XQP#?x<{ zkX6^HP7ELYFe)YWDzaQxVNff&GsCgiijvdy`Z6*@KLgO|O{3~vO`mHT5#NLEmIbrl zQEhT%8n{)U=n=x7lbAeyH_4`$pt%_47O36bsg7*y_d$5oZKL+rC`>+C?|3f~a-}*5 z9U$(#CTr!mr@Q@i0b+h{8@&f!_sxLsjeJiyn?<`^`t)1fEPr=&bew&iG_FzhB!aU? z99?mnUanEe-Hg%jM6QKsFebCTl{Z$|@;+D#>3EgcXl4Ozm3wU-%U@>J+a&zpkz%i_ zlT@-d&qfu`O+mr9aF6vA@m--4?NdE^{IjNkG190Mv|#@|@o!_Ld4gXTR&lzcZDZ%k zXV#{V9ZSdqpH5_iGWFri2p&T8Vjgp2)2_6-|Nfpc-w_Hw2|noYB%BBnptjN^{p#?b zJ3h^LJsqVJYi;Q#-mW;0ycKx-YFH4q1#hVX%4nLG+ANGR^Dhi6Z~@@pOdRi3cEPw*ua>N;4ElCNAstl5_sBHWDGYnbn_Y^_p-7$;DA> zU-Y?`QxYTV$8Us6>r(JcX?aRPLXU}>^ApPy9(FX|m@+Oz-PPt1BtyXmhS@{y6akNK zufqAg+C8qh?DrgGFhw9OFGp`!gu^a?sn*ep@Z6s=8VP?9=4ZG-di5-oLdjx;Kc4=! z65r)uCz^KGU>JY^Sdmr0r~*K)r<-KhS6-%e)7NCGo50RaP>ver`(M5nV0gdW-+5~4 z3mh_>$T^Q}dBI=>rz>nmY1QFv1FX`h1rD)oJMR{R=~>$sN6BS;xNu$fb>;;at|Ehz z2u!ScB<|e^6{IAqr9R(o8PfPhGuqFWsvjq~6W009m;91(iOm3*(Noc=`n(Rz9Iofg zg9vm)i^36>c#IPt3EN8@b6qk%fJtNsdW}Pt)-r)Zm}56Cg%UOC7*!lURK|$q%&y%P zjguh59?#dCE%IglhjR_f#(zL0v(T!atVh$&fK#bgd~XU^i>Ov(aWJ+_Epl|j2hQCs zCf)zQ02=k!pYb~`1Sb@7+)6xlpwT=!A&`30D?#i|)8v$0)?NTFWP>PqGNnS~x;k>S zX8IeL#2@Mre*{@tgx=gJD4%(3|62pbmhXab~DDb{UWR1K+gFun7!;5iBN^B+)rU2oD3CM1e#iHpahLqHG4y)W z?c0=dtAuX1h*1gX^pEUA zS&erowGj^I*dCqO;5&5PX=O#TYC@cgO{Ed~LG{wl8pY%HT~;cZUxm?KF!-Yu|oxKW}J18k(wB8Pz47&M+?)AMzdV z6%W(lAb&K^#R(h&J{SVci|tsrP@Dm7|^Vt!_{A zWtZMW9;W(-^k?J2B8_{ef+ebyzbvdy1Bk71)j=8>F=Ac~g1nb$ zP~kmb_(E(#@M$2@q0EiP44+9FE|G6=-dsxH^!L7a-3DT3ATwcx(yeVFZ)N^9jy@xW z@qIfki>;=dt1%s>TeuBqTFfh1gdY%Urg4};cJ+Us?tDX}+Wd>FE&Y1>3n>!u{$UP$K9&9{s3pscfPWXC+Iq4FnQDz@x_n>T9A#jkU8nQ^5t(z11h* zt}4f=&!%?u$1lA$UPuQptcpwc+b7A5-00HEpr=z%?hI<|q-s(`QLhH=z%*1MUfJF&LwbDgtu zSisWZnAxu>tN-KGuuoDnVSFe8%4=rYO)OP-i8WbodunFg%iK_UI8ZE%x=id?3Sh?S zYe&Cwtwll7>#SyJk)tHVAYR*t({TAiHk3zF%igw^z7sMhN})y(;w;=sa{??lS`>Vt zVaNQ`t70t`+ka+OlW8HxWt(JY>z373i{sc<4W4iI7QQel|Gu0Btvq^9H{xGDQtal!P<|ZFLXL ze0vdH0~LXi4muk0AKl`^^G^B^e!ny>3ehJt%2}NBbc8t%UEUH*r|$d9hcRRM$sz*| zV$wG@*rPZ|wdqo|D5CoE))6FL2F$QzFYG!@yy*UhXPc0e(KQwYYZ#paQO!l=Brxov znZzkD;3bFefp z*0C0()(_rLJn~iLJzD|JGTE|S0@qeJnTDD$m-(GMWY=qHpI{%|MG1AyLzJuJgw~Y<5jlk*HL)Fcq1BJQpIp0uGT4 zZZtbAKv~5p1ZiXw4K|Q0NWKD5Z*H>#YvxhIHno?5QHaZDd?Lb@{m$-m+OMgAK1Eaa zQ_NBp`q(!lF}!dH6EoY$wOpu@uzsH!)vK}n^{lolh7`|w@0Hj;*+V8VlUj!%Svu^Z zRJ|E3&;H9SZK^DsM<}=3B@Py!xEP_QMH`R2wFOG#FmGD?{OdX(fJSv4x$^ z4TBAae7Y4%%O=yI?8`&q%+HYmm}n3itV#p=&ufYEA<}u$Juo?8m~QM+iO*66LW3Pt zSa+?_s*j)KfWI>elBvH!)Ngj&!!1&F)Gqad6a14Nq zBqQ!B(1*iV2o~u7is8~HQxdo!hz)Onf`&jlfukHaP0*ppMJ-)$v9%>0p3#Rf%nzkh z#1+D(JPGD`BCPFn`@kFVBTxG73uQK9Q|Z0VjHgpN(ZNyyj8*+tb*3X1Cnw`c7`bt@ z%BaOf@%kPt)Yku07<0K@xnqvLV7fI?g%H!ZQ`cy-JiDV5SN;fXm-SZ+bNq}EuekaZ z^2(RWwp+SXo~%vEd!^8Xv6+CdPr1^pB4b9tNyFHMtxEoIV5glCdO)EfG0BxYl*0aJ z2h+TXo<@8#Kk>((2ZnL=_UyQe52osw2orlklecFC`o@1x_Fv|<9{1k9K0chSN1HH@ z9oO+)fYc9Q$J>Y5rP@<XJ~LOlN=&Ul`qNer(pbHAqrPnCml@iKA3ctQZH+CHz*CLG&UY^VJM@x(>~q z5|Tn?7Q7_Um#`u|2gdKHq1$bkjN}fsw$W1_SzMOL{xssaMpXL(`!FPW+EVcySfT;B zu+Ztf(Vkdexg*spEs~eAwgl?ox}xMJIe`+YH{1}9*BDr5B7dBC1uE3PBz+SZKVpbG zC@0bS=3mIY3xkOE$H0`ioZ@maX6YXDNm1z|O4sAR%w$+z6k7YUd4m&l-gSI*NBdms zHbdhgxVl?A$)&2H{+abVb>hyu8l>F^!)fSOm$!2?3vz`RC59%KM@zY$!$q-6$>MY~ zJs$pQnOe{mm*`nR5E`%WC;jE+M2EGV6iL>pKDJn^|BdhMqG+}g4Mj|gy52PwUHD6< z<7q)pD! zAd0C6NB#ZNdAn-BcW-5K%>>yHM3aqpz}+0$FKfdtUcM50yCVBbUWBN-3l8F|m&^?- z^x8WCws+F6<^DdxBAx1na-T0m9o|t2z52$R$&TKhOp=^uEYskJCCju*GXoLt+ox_$ z6NL-4EXSPd_48#MO13%GfzaZ&GcaUkILvzwHgr6Z?ZZ?Mz<_q7>u9=fXM?@gR$+xgkDl$@m7;o0}zzkF#7y266kHE@}b z458$BeM~Z!_aB$*g~1XOLs%FFVQZ$V*;2(de3st@^hjY#h1S=AX~RkULL}z-bos*l zdibkDW{sXUf5C>GN|sB!ATd5f3@G$0kCVJ&FeyHFJWka{=8I(x*x@6YQ9!h z@UGBiF9WjrUy06eR4aPXkVxj}P6UFE{y=gLX7Y+@ z0HN$!o{mQK%ID4U_tj9V2lBJ6$KSUVXSi(UWWP25LU(p{nJ?fK-wG3t1%D?D5s>6W zv)(I^g-((C$-ZL_`M(D9bzV9t&?hUM6t^UD283}}Uw}&e(Eu>2RIp60dMC7<`GnbM z30>jQILEe4Ynx)>4gEqWX1E}RPTma?SjWJ~3jH3~CL|QUQ*ZrYuNTHZ`l)e?ykDLb zE8N<$H+(p-gK!%jhgebY%AEts5<(2*PC24E3R7 z8%R*phdVfNSIvI`dt63~FJnh5y(n)Q;`GtX7+}i+0lAJkkoHUdOj24xLm>6HTxVOs z&JXcF-G<08;j5#>7oCtNSI(Da^~>1HOcVJAO?aQEt>tgjU*HQL@J$A^MRp9l&8$rH zP%BblV#v*-@ujOqZp2o~Z)>iJ|GI4w>;fd1mcsW~1?RO|!(g~3CVD=55;+{~ zR3lUNk*?ql1v{c^%EJ*w%B~)BDAkeHa#Y-ixg$YD8fQTKl|?Luznjl;-Bp?+O?}l> zA_ze9%_hf{cysuyS*8w)2Gwk1Ca!gUJ63Yz+OYMH>zCn zF(;G?tsnN>oocb0Xc+N77SvafXgqpV{v)??NM)u%i)KeG9>P^|zQ)^SrD-lA?eLu8 zO-D-B4Xw>Gi^$o}$3T}ioh-wI+25qf@i&|&b5$bscoi*X-LYtfbwXp3I~F10zIaap zon(DZ-ADeGdA(YPef>Yl|RV+XlC=@AzWUg%+i4AA5B|CE-m6ZE&5-O#@k zEHU#`RuFfv;F{ppw$$Ji&h9WyGvH!RQ1ZG;-S(TbxmF1F41^Eq71Hn~@5AY0UW8%u*Y+j{e`jAJge?Q#&$u^X zHPQ?1b<`(r;e}@y%HxEAYd0DAfOJA?}2~mM_hgc{sOg;5#$3Y z9m3uP0pS4={>Cfk2zt;0m8>q1@T6&XbV$7m(Se3sdDTh+`tyK~N5Bg7`@473n2${Z zAz?!y+=?q;Y&-jF`raDzAtIqZp|A~wGjU84I$Ky0j6Tou+e5OG0Q%qnjmeiH)fYt=o5v; zcS)%TYe-k)kt=l3COohwnUhWb-HL(_jF{)&2N4q#Q~9rHJVa1H`&!vSd}E!3Luk^d zok{>3fnCaoa|W7qBWk0J5Z%XZO5a%7cTaI}pHM}z+_|5QcyG-jDsb%TjL7TQMNH$M zTEq=2IHfgeTeoE=o2nxJ(7#T>19pYf*KbumOl2$vHi}sPz?9t-hb4+@13pRd5my}2#Hl?w5R0BbZb6O>+$OIhDInit3e5B8g9mR8kD%_uumO25P zJpX;#Nx^J)%=W1FKCyb~k*6$tW*fGvaRk?281_DQht^*fB~%ggn zTj9#$0|P8B)f$`1vxnsWz5WS!J!^GH0gjCJ8OB7v0Cbz4N}~mW;S{|ikcLrhb7dlm zkjST*jG#N5QlJy(ken4mO*dNX&215spllZn$hx;P$c7TTV=bBc$T<_-?OrXAVXlO= zJzHk+uDG+dt(qFK4|9#f6I^k17z)I15T1mB;5aW!$wZVuplK}E5SJ{pp^LPlNIX>3 z&LLGU-RBUsB~g*zLw8#7kq&RYVy1-(%$VhQVrGRHe_liV@hs=@SU_m-z9M0~pj*4< zS%FYiBF+BdI@|KeMzl~n>6u!J5Gn%X->3Zt#LkH1_kgtCp!VbmG%CZz>tUsXTO`v1 zTeael;1P`Biny@)R*|m{(?QYS1~?I3eU0x`4mpy65d3vMFq%i+__3Ln?xc(_@R}d# zpr@Y*5lp6*1SJr`v^n>vf@eea^Z%wp0;a?B@*!LMro~%WM3A*O+>u+StH6axHXl+W zAmR)2w=PdCGHv(nN2iAFWwy>+ET8~R(H)EYx^4+(!?kSpD{6{7=W>}rb#T>`&xNd8 zpFE`wzxL_R$lMS9*H@%`fT_^6^Ll4DoHQ4{^1~iHS5KW0DIKWb8mO9;!zIaHiXOUZ zonKyZ*YIlY3BCG{2|1nLLbptMn+TFh$n3LX=CiKl8h4<0dKgzGh~zgB5b`f=?)tKw zWU-nAi$Wi@*WXsi|FXiFf^Pk>g?%=fkEKtIXwKRdv*uAPGC}u0ZYL<2Je;3`nRdn1 zgwlii%a9q?#&ovAp>X4z7_%FQq7AZg53+{@!V zbt{hKS^}U3n)CsIyhMLvAK^|)+Cfz*pg-cS*Kx&4#>_`UpA>K%=P1cMAOg}|%nu}FaF%Kp-e;1FRBxgMHAw^U9uB0Oo6 z^F>E0KtOjZz~nQ(dNQ8R6TXFlC8Oi>8)N2QLG`tldpEnhv zk!!YGQS>&Hqo^v803v%tGsVGeo7FF$pYcsA*Z=@a_US1&e(}ged!RH$9$`9#l^qhU zP@7Mcw!$TOrx^afVBiB_gt(`pIU%#e4?lRE7Y`ZxA@b}D9)uhjaI_FS^1cpyKnOfo z`jlC#i*zDB1>&>@F3LI>qGWDYvJIp56FKY{ZxCHF!PYfKlg1L?`Gd!E#7Q zo&%dNyp=!d6X-%YDqGOcJr-9$%h0_Nhc*OImQFUGy$wmN$8FZ^-5r ziW>J+-aDy}Z;!?TkLou01#R-TBL&hxk<;H!=;3%n8b*rkU-ZI_1r)x`UjfXIHP0wV zg$I@-YsV?mmvmn6mrWmOgs;V837vV6R+Q41|CL=^FYMAtlga{vB91!a_K7a!p)1B^ z1f`}B&^-3fRPa8G`cINPcY#rpmiQ-V48X8T&3Asu&1Zw|Y*QFiwzwFV1sl)j0+@lq3D#p9;15h;>o@-R$J zrtJ~#2X}Gf!wb-q1~i5%JYFRUVfZZ!wTW8~)iR5lpx@9k zjIYZK0Qhy+H&V4)4mr?}yDk}D(b`^j1)ts3Ou5RkyyPgp$Z5H^%ADjt)tphq6*c;d zGldd_54FN~IZr*!td8~rD_Z>3U9~R~Y%(l;TnshPR(cLLEpNF8!;!t<8-IgG>icxm zBe&iImgB47W^Oz<@B53J7rL)<b$b>Gg zyA7ScZ7IbN@te6yF0C^Mv1jjDE*uQZYEfH+D4l}Wqg_qL8EYi<)Q3lYs>;|Q5T@Nb zjeWevhcKwkFqX(yQ2Apcc(A<*hX7B6%HR7h2;TESP2fg-QKx_MfXCqjskmOnu6uI+Xqh)-Eno0pD|g>m&>zj^!sFcAG7 zGW}6N96vImtrp5Nt4l8FBB%8A0mSzJhT2m4Sj1YNL1~J@>sV%_E_Be)Jl7$)w;Iu?RtRp^BLlsz!s1{1!sK zcf2iv$3Ioq6Cx-XgwNc5?kA7v(8&Cl;zZQHJszI}(3(!&!C0YoAuyF4&hHdVeljwD zFEH|)f?kt=phO@tGmqRqNrK_0Cv&N?2vKX43n=ahze1r62s0j`eNA7VsUynXJ?4TR=X%;Zxf`7NoMq`|Ry<(#T0PHW2co^ZC0g;U? z?pJ7EuOjm!>|@NLf7%B7E79=*kg>sur7j?iyFhZG;DGN0`mukAfJ1F}ztb&(zR<|?KaSD_fV5NjcW#<)ib?}=sp&5Cjz3V&sD z`ez{dpy@Zy8zr{%^!#;(tg6Hk2w|{ewoCO279s^Grm=}>$iJ!%rtDbd*HArWsI(!N zKsG9ksRSdge>v7S$kYGIA>dC8VNZq&BBK*VC{!e`5NmF;;0Bex`Q3r}G1WK!nZEds zbm&T`9Rv(UHYK>`mjxqFOtG9L@>y;%bS2_zzJ*EvEM8z{_iIBv6qV@ikMFy9P#Gvp z5$yx6!&@ewa4UHg|Cho_BMN;V{X7rk69HlE&XcD`5ScWg65b0cJ$z|tn+o1JZ}!$h zTKcut-z80m5$b*AN19LI99YB{|112pt|R31js^ z1iRjF?f!w9mIAuhv3(>x;oWWqW(lDnM6@)F&7zQZC80PSbEZ1_k6C^` zSS-gER_UP6^RP#Cyy+MWn8MSHfi`+@1kWf^eAFJBy7Be6h~5|9QzmLxoJqwoI}zmR zFt$H+uUz5*TSo*a2al0T3OWu2+ua*{G@fm(S(M}2B6XC|M-#r@{wzN2!SR@ls<~uq zr{3$BcFys-yomNy2&&X>_65XNBCmRsf2DF>Rg;SThEa!f*`!G8r=j}_x~ltv1Tx0d z(L3IRumtt5{;$CdM#SUKMO%%EtqAs4hOI%qn0=Jy{?3<55?a2%r*uL_T;R*B$YGGk zW?zEkj0x?k2IVQj`BBv=SM9Pg`G%($X`Fd$ZtWsXZS-4S*3IKdz}!9Zw|Z0&jB}{b zIYzkbO4d-{ht}byh7Fa?W@qxmpYCBlV0a`UT)S7*PLCUXSCbwGsCQW+sH;aQpLHNysN_vfGpfLDA7|vXfCC=5gA&jh;~CT%dNUVADh`Nlf5*IN6h8d>=XoQLl7|R)=HNeFl+&e z!_Sl{&n$x>jh%JsHSWEj06|X!f#)U!MLzNOwA~2Z?2Soo+pQ8G&|gQ85qT|(yyBo} zH@5UpU3mfX*+I##pvbvfNG5OETL0$qya}Hd0iX}8hyY|@1fFnNvHQr|>#E#}&tmv+ zQvX*~BfjV(B*0BBlnC{(sGIG6H(N*__uJ2J(fhF~Ydqxk{<^8=2uglxe zaz|DQel=eRUlB|VHHnfg7O2KB(mWNOmvKi=4;z?^OK{r|x!TTBUO5#EQG%HVuD#88^^)MU1{>i+VTkbTuSd(&5khR&{;QnjL<`?4=N@2# zw{S(5=0y%*2K8A{TbeI=mOIBY2@kK!sSbOE_a_0cFpSb@5ny59pe2_^kKhSyUmjeC zAFvB6{^v^&0Elf`fJwC8o>O)SzrFmo)Z1!jVSV%8y-6o8P>V{m@*H1l&VNLg<;fU+(-(H)7Q$`Jk){ta=!-lZL- zNQe%C@*7I_+@qsqhGDx&SHT^p8Al_o93INLNJHy*5JLJy+?VEMGU0EhH%lB|oLbEGE%1X}h~azXVu7s-Tt1MF;qZ{{&k-fUKnk6 zE$xKd7uMcEb1^Ug3*T`&iP!O6d~Ljj;11aRV1c8>jy38HTfE3fHeQtMd1M>K$^Lj z%{^2tGjUez5N_rbYGBOP2RHc+{kYoh)5~+BY>*-!DU#!u63(kR2JvE9d;qXLkY;uc z3*q-b6t-4B<=ZZ=&>;RiXDb>M>(^5)GjeS=#)6i6+K7_ma$fc{mZBKX_x(0SG#S7N zd!j-^B`J|GK04ai(oU+B@{h6YT42K7c3H*%5P*K4)e{|VV&NGbKYWr=cmLX+3-*yZ zrXCp%)tA4mJV#HTkmys)n7AVwsK2_SgvOu4wt%V8=KeH(l!K_7&5zLQ)_=5gA?4b{JYUMFKfl`CR$o{4 z{x=kngWvi5K{b{3C4mNJPgEy@l~}i*pCgP$2Bb6 z#*c|fN#VQKYx6bjrcFI!P3X&C5;bPV*%8Y8Q~hwQ=MY+gR4i&G|M5|a8`N)MX4${m z6T+2*mV45wwy1g5Jmo~*GQA_4-L5cs^zx5iix-oPl%pO0-`Ng%(UuVUPp60c499%K zuvv#{^ICo4rCmz@Qh@`a_J^+jQcoTzQxQfkD(urN9;{RKdWnN$l^s3a_3O}xvKwlq`l75|(qa5(lnPC}Z`hx3|N;fn#x z=*%9h6>6sk8;h))&!m_67JIPJ>fTCyC-Yy)3Gf;y+nn2kWZI>Mq+b>51UOG}PUDe*h3bm&jCHPKtQD5l4=t68eg)X|1z@rWRDLik5`5pPUfg-x z2F^wq*|*pIf!94)WT2tCVm+hhYPwlUUdnFG*RmV*`DJ3n~^Qqn*{18Z?UJ^a8Q{dNl;jqI%VhD=_M5k44dOIv9 zj02uYG&+zlynC=X2)dW{e)ieNF$hb z8RDcyn=@5VvEo{<9`zgrkj7f%lnaB%B*oYKSo6N2I?ZxHydm1)t$GW1>y-hrhX^Q2 za_G&i4LBlBe4o$fLntwpcW>?4jF+M2=;VSSGRe}HgKuuXT{!(Kto$1M zwi%cL0ELSyjv*qSgtu2lOF)w>9jA4zkO)&VLo7q9zGlmy=fGpkWs>C7< z;7Jo!!Afn?HghAxihJY3f4~wW>H_oXE;M`%g|H@&dc3u>sVQ?2ywawe2e!9T zb?%2$kJp_nJsz-yCU+^fgqRlI>sw>lqKR*ru-E#@K(k%wX086`8ZScpcFa4%ogaxpVCzWn=eMO% zYKzb48JDx`F%_$>9m2rjN=8;b(LYx4-~*Kq9;5;^3FtZ0fT&$c(fh@u8bq7HX?s^V zB<|<63I>zsTB`>_Ue}-cKv%gLD89+Jr&KLclF&%1qLWIy(#zyz;2&c|L_etm{wPY!o70mPSx0`{rwWUtpVTF1K5XW8Y zfkK*bzAxV6&J_ATk`WI+P1iHo!SkBpvQ&zt>joj2MUpZ3?x^ zfGz3v0nHo@Vzo!*2g|lG8HOGO)qZuxO6{qj=z$P*{P-%{9q(0hhc@~+9>+l+Det@q_zQB8PEyed(65szt8H2%oG~xX zu^o##I&&gaPqX*Ajx_~jmcq%T*gICl{JCKJO}!xq@ykzOI2p#ly^mx0+z`WD?I0*1 zmb0Um=YdZC2XBj=6Esuk2Bfg_kAel9{;FEL>AZ{iqoW0x1_=H~Xjm4avkcm})C;{sEG7W>3Lr{$6+%w#db zJDfmtcz53h-3QOd?f+_RqQ;}5_Ye54FM)HnY2B;O<#euLmBBnvqiwVz6p z3?~@0Q_EIlk&c&JCi}+-N%ivZZZno(rU*le)SQbritlE8E;Eagl1yw>p2783m%~ij z`S#)120&(mm513QC-+qM*%Ub~X9Qh!al8_Hx(z5|oXQl|%x+59=Gef`q%cmemc}=F z5xH#tMk8Cn=pVQE*0o|+JXKu#BKk&ndO;YsHq=s`|5Vu#QKyM!gL7w3cCcb|?+T}v zTI7-I@U7S$*z#-KRD{)|gv<^-^_fGpW=Mr&O3mZvb}xft6*Rv?i4Sap&B+Y%$J69Z z~a#vcw+L^LwKXKXn)4MXA;etsp7_x6?Lq|t>Y2;F3*^_b9s<8`|hYcP9 zB7w*Tdo=9qcphAZSC58uxfz>;b`uW_m(u4Zj}hqAuqjl3^2fO@%HwcFDHD2VzR2j| zJW#5;Mxw+kq31XrDES4elV6Ze@`?ARJXisnafg0b=Q~SMG+hNOlU( z@5Wx-U_nD=*rrnb(>2yY?nfgIMrSmO5DW_cMpjsqp+{6>~3s$|uSB z^lsv3o6co%lrQpK%MBiU{oWGZ!?38I%L>Z{GYq4os4+tM2i(c9uwjoWod zxb5*l$W?Tg@l+yB+3B;?!n<6_RT&x00ge+nYur+QO?jGwhT6u~QJ6r%7ucb!YEp}GZH?Z#_ERO-RdK&e~HqVe4wX|3<4^qlU!e3dP-(a z`bevPEVDc$A$n>m1wku8S!9gW7S=Aqa(2gWyKzn&%BYi) zLdm=(GrxPJG3k9%6{bcZprNo|xYr#l8K4&a6Ln;iZ9;|Stkv;d0;0AaHBOQF-i5S{ zPiZnHLB-A2(D>P7b1~|HJGvjPRCy%bLR!0HIV6)c;?H5iA?5>pZ69EZ6Z-;+hpSs{ zrE;d_CK=WDOO%7f!U>J*b6a=ET6tUEONw&67hH-TOglILOR>4C1rOefc;C9PpC`xo4Ukq$CsYS9fw&!f$f5M$=W4Y zzDk}%)`zS0cWzl+WaHZV3*7922j9qkX>#O~KpwY8)^XZ-rz(Xz|M>d&wWyg)bZ^>- zb#=wX(05g7Z4YOEb!$+JP*e7)om)<~>$rUK1edG#u34a!SZf2xMvJEFYB-a0YrvBv z9h2w{$)6)kHiiD`N=MXKS;=71@rO5MGSJy*t8NOmWz^au2lWCM8g#tek>+;-?+dX8AhM`exrZ#n(=G0dvOlax6-SQp4jPt z>ND#cmp|l=H{v|oxaR8xmiT_SG{`e0{T3-dWAgG$XHOq~Y0aN#I!}(}2(IcZuC;Br z67OZ1B*uQ-mSyN?6Pf#Ho+HV|D56$jh=paQUbnmF2UkJs_dTK=1HyRk({fJCJ*`ee zQSVKB-UCUw>B!5V~-B$}R#941HBKjlF6Ws{r6hTA?TfU8E{HgJEJ%0@` z>b4KZ{#T7qe82I3tVzrlCa8paEleNpx(2u)YFS6_HZu7wDt)EI_e~V==ZYh6`ld|& zEVud9{W9R3gBOgBU`4OOs-_+Z9>vDae1D5I9 zCJ6~`uYU#R-b`atU={@)#(h@caO(p3eyZ~xE2^98+M2$bv1)FV_eIEDDppcdJknFt z=Ex^!G2_vpfV-Jbrq-OCqWemwUqOB5O!KKw6pMxa4D1Zwq(YdItqeLUOfx>l#dW{0 zRYHF<Qw24=eHvH(J33)SPFYwIm3G1(0>(MKxe99mg!vXU{6f)X>X1|hMbKVlXJI){x2KT-7a93jNp zf1~WW&a(_;NJ9S|Q#7uy)D{h9Y`WA!vS4NFFJ3s-QquGgnP5Gw!fhL?pmI;$rKmcZ z?}QOKWEpZ{&i*t9&pqrwti|os9gHsNrfso&O#Ay>;=ZI}8H@&8;`<)= zvg*8Js`dD_4#n9HQr<1S5SYx?NABYS+j#yqd$W6y?7vQ=Lh6sPhp^BlI_-5wGn;xb zbYH&HXWf@1ZnG)Y2?}g;l%~--^My3twEX;65+r=qL&X|=W$fLjyF`--HGii33=9}TJtAb98rs*8Qk3)>ivk+1-@ejFS~s2$o@+xJ*}A+^a5<{fw@hJ6(GScQqPneoE|sM~3N z?@c*YQb!SnIm6Yp)`hZK*Vq`+bM@w(A5F+p$gwa!JVZI>Fl96vw=Xru5eF6F%4fyL z^sNvEowmrnOo3p1NW}#&-??6;32r)Bd9HHTTkAT+tRCAPQN4~r-wLLUtlpT12R=3m zpT_SrAtgns&dK6VmcWGsMx8q1Uw;&V0?UoPXbkAB#Ukyad9C(-R2so699mHM9f z#ocDBFz)KffH>5D;`5jh*)>9ad;+>10plP29>SHKpKIHkk-BUb>gr}#<^1c36?0!t zvFXn-aDrC5Z-_$v>_nhFtTbnCtp$gwLqyr%g&wU(F2iuTm5#)ag>ZCFXF;yqo%xeX ztWAVYPjE8r5w((d%^>cG7g@}HMMAs6ehwj~* z%MEX5zU4u>%!$m^`ME!PiDOm-BX6$e&s8vi;|q;Y<*ow%iQT&>x`|2*?Y7pp>EisX z1k09lNB;sPtn(nvZBN_cc1sV;BoghK5x4@@{3YfL+Xa^xQ4$Oej*JG8@1xmX=gei{ zlswJRZs;UiTH@9gDF^4B$iZFzckgmhI_nW#a$W7-diY0SvO!l21|yn}q+DHd{aXH# z`!IaOkq)vL;@{aWG(zxt)+o=IsJen0e-ujf+FYMR_R-ys_(w-8#^EVe8p;-~N)mCe zn;-{ZfgU?EuD~yZ>>C}R%l4EUlU9CsydCwuqz)#d(EV$$gduNahhj?4G%$r0>%J36 zWzqWZC2W60x2_P@9OLMef0p(~JexOrw1-BYO^240A(qUJ4g67$O8#ffeT$`57$f1s znF@(vze4)eqDLl}D47RRWtFLb=;$=f>(M#TB(sbo;s`je!THq7vpB5#E*2~-C4Dil zunBYH0ZA(MK)O3+s-JqM`2>%#h!VeHbU0;u>~>lZDk8=cO_?3ltO^5z*$_6;>bfu| zbyQ4Ld&xDHlWc|bhW%uWnaU!UX1s{nzC;Q&y8~kAQMYO%pCtB4a>5tlC*exTW=HY1 zf%h-ThEZfs6jx<4v#QhkC9!V>xmFeEDE)M7NZGX9Q98N~*M+#;iOE1~YV_k0Y|B|z zM!KGsU#pzOj7~E1yWXB4%cQvb-gXveJx9OvxLJb1>QwoK({so&WnZVC4hnyJdc>!v zmfZs2G$VN)pkViY(lfPI*!{q6d;A2rR0-;vAxdp14`3 zvfofRcSlE4>Xt%ygDO?|FpXP-8x%QTLOzLpIn<3DaB7G~$SS7B64_mk=&3VR!4L{D z0iO^uM4@sblNQ|Mpq2)sX?5sdVP_{QA+rD5$-NI+<@>4cyjF;i zRbEtt=A5f|CqJ&Hli5f#c^bq3(TX}lWYI&CrO~=W7MUJdD(4eaY>A(niD*it9a*a5 z1$=SUKHe+}uJ^bEdx_5R(r*;?f<2!xJH9;u$F66J300QL;=?j|(x{(saWg*z-5%F` zjZ$tElWf1dSEocKV7nl(+!^Pak1Q2Vs&N{RJsfs|$?7lp6s>}-I$oWP$%dYcGKtVM zPVdI#4qMC;WZpPN|MvDD-9$L^7s$dHrL+k|tH8D}w3SVoUY+IW^=O4*_l%aG%@5M z{KAE6Nn`7-j-!&Mr}vM`HJQyIgpOPUy}$VV)=Pu_NmGj73;31HI)#^@@*cHAD*TZH zH~4MzajUE^#@aV}{Mm=V!i~NHhSq@FKmiiU>Hjp}Fa?v{T!%je=2x+RqtIVcueG=> zWN^zT?O=h_)xAycb9k?_t%LJdUp(DI%0*(axR??{2~lFinS~eca96`dF=7uVxw{Q! zHUHXt>1v_Pq@+~9Ws|oSGu||38J?YME3OJ7J{2(}k4UozPi;uoS*SwYc$i$^LsrD4rz)_ocj|odwgu5&ajW^ zWAwf_lPAnhj*nI_#j)|mcH6aXbhor%(QaGHL>xkVZbC?)d*Yv>y6JChI>v=8PoWb~ zWMrD!3%SANQe)Bw?Ud|Tu7`f=p(ZT?DvulEkYVn`V%^rp)%U0FDVD7dsdX)v?krOj znyl_q=k!p!se=Zc77dVLk0$L)qHfHwWrw(oH3#)El!~nziubyV);^}gtZv$p$LBZg z59~(dX-gw5q-TOoFj%S@KuOgi*j~NiBfB?&>nipsp-0|sP$|9W2{{2wnW;DhInrrA zl#jT-vRIps4r*E5wBzf!vEY82ac{%3TjrBRK{UEZpDsO^<&I)Uh9_hP#O6xV4SPuAp@quLj$wNaG`$Cp^CwAGJ>{+C zt56(B=XQ(Y3Ixn0B_%ELz=qSaQtzmQsrziaDD%>8E-m%H`d*M_JL!9atn88>!iLxH zVig{VBSW#U`F+au3NCGA0>NNuCQ1Ea=K!K-@}}ToN}#wW&fQMg*Yp_pQjhj>q}&x+ z6>+WxWJWy{wq zLRFQmQq2};(FyEc1OOR`q)L%6xHde+K+dxi}%wb zmS;bCQ{0mjERTHKR+&-5r;?`izu4Fn&A$aNIs4HNWwQigI%|IHyAzd-m2$W; z=&ExyA@UdM8Bef>uZka`rh^%Ri~jOKI6I-tE6DIWhY+#!*l4o zAdC!FKT1bRSP_c;#fQHwZAK5?3}M$_o>m#HjaI+s;vLfb+#(?T1{f zsol4DV9CO?w7r~tWa6h!JO^ja^38Q+ccf8>Y~)yOU4JAb%WvFooLW$jpa1oHd{(o) zpibuzvvlzo6k#3v?j=!l7#R|CA?ig>C56*cc;bwYX|vmKn%C5&v+J?IsXJca` zaMNSQhNTf~*!ABY5wy==yszk=P3`s5{W<2`dhNKgg0mSf@RK9(A`5N?V!a+XVBu%_ zvUP}qW5T3Ixu1?AA;_VsMI{LK9=Tm2!=N$Yf)Y;7OfAN;WG*P-wk~mR0D*F;%Koyf z+!SA`S`YvyE8rTYn+CnANWht#?i>au{nohZc`pSPI0&x|IIpEk8~`lvN*JMn?15dm@57ws2KL?X9cj* z{qL#5)Ph%z7@wC+RN@C6nTn599&tFtZ_XXllI%4MCQQV}{-_EDXtSFk*KiT`F;A3kI|oTbDOU++^VkHiZZL( z^-CFCqeeNv%&oZsd3qaW~tW^?Ex^QlE*ivR?2PXX9uy^9eHWhnqQl2^o*? zXGu6Q*me0ZRQof7difotmP#J6d#4jBmeu%;lPG;Y*TVgv3FL62_{kfK`C!)s?;M=yB7oau{M(XP7+B}=| z*<~Q%f}}dl_x2?d%r3J^>3St^;9Dj}eMCczo><6Qjy>BFjz)Rwd@zxJxz%nR!&1t&+R|2%gFtM<{1jzWi z&f6mo-7A)=-O99HDobiH+;G5inAENl5_HT`EB)^{CHw?or~bv4PZ}LnFPlCcR9i%jx&VSf%10 zYL3gOC>xASMGx5b%`5}*GAYHT)r66({MhfYoiFy&h4wrcS}e>)XlVSG=8@nWYX}*B^iO4@@T3mzfqX=>bNO+ zcb8w+)zlmg_tTm{|M@VW$k-S?$w*B-*#}?O=lVVC>%`qWCm?@wBTli~0v3HpB zY&AFUZpcUnDh&;}KsG0zgocV5Z2lxltDNzogEC-^#wvHcz$(=poxxK`c)3IYjc+-n zY~Yk&CPB{gEnePkj667n-{T>~C>+k$z(<&VyY>elETpKNr*_u*^zQF`1fF%&Rp+Np zMNgvCAU6(2u5C-)!6=98Hpi4u^;Sdt82Vyd=6d zPQ7S8bDEgz6M%yYWVWvROek~F+1gTT)z5>%U=dVU1I488F` zad~jMT$=MrG-P1S1%IL9HhpUZV03&PC#>hz3&WdsF4x@+`#!{97{RK>REwxGvzPhW z*vuraEotz=yE(}`KshM8PzBO{^ljB&;DZkwKmsA8JLf6TTS zxaq}5&bRuG^t0$K*0TZ)DQ$o|hH`Z(z7eslCrFe9Oox`-RN>*2r>OD{wg|zgKo) zj;1f;T5IL>7aZ0>GSWosK;M8&7}opXHOh0m4w3x!4s&drr7iheQ1qCEObc2>AbU&-lRa_QorBrBOg{q%*a!fCOeZ8TX!3b=!Xuhp|yeWw{4*8z-ag^``b(km#TPhHZ zT5h(_Cbw;h@YG=3u3Eb7C(d2!)YKx3NmBEWQ^_9?OKC2=yW67?e*K5_ZGU!!{aZV` z;KIfx#Y@<=~_UIVbhdDsfw>KebPiNpQ7f`2EoR3k1CcL*PSA!MN>F*u3pb|dD2vE zm@F+b_o5)98@gUM#`U5xG;rOneGbz_e`i*G6osdUod%VeN&s7L7;YO8g<6sJ(uOvx z6Wzb{tMck*mcOL_k=Mkk0bXDBT3T3uI{SNuy1YBGQCDG^+q7^Eikbn-zn8!1%11Sfz8@=29}pQUc1{)-L8!|0R0btsPEgYm-P9L z48Pqok8HZ%MQp?Hb6-Ns#ZLTvhz-ZS)?kVL)0_37xBd^?3%k7VA!kte@q8Ym6Bs}h z3lZ0MHh(6645|D{q$oJ+nmm6sh-rtd?Ou!3Hm=wx@T=6iiy4IZJDEHqRRNY&VS3yL zbjy}Xd;(C#S{>651Dsr4Svpl5=z@f{`PP$4JyRm+_KySNP+YUbyP~zc>xSx4oBm|w6 zs34XafUv*5RNkceXrr0%qsiV@!0^tHj=Q=R+O%&z*=jJ6F$o5>b8j8-WH19bFK$J{8(_xA8SS*eL)kZL0NS^?4sM4m!(@`=Jw4EoNVQ< z9kC_#EDtY)+;`P=s*gWDQ{Id5_*Z_m*B1RNMx~j&R(0kT*a>!isfLJ!q2aOKW_7K1 zMb$FZ3lu`;r{D%{L0{BKS`n}BwfS4=%LBVBUH5UNhb5H-A!#0bY>5rFVR|tHp&ZM* zh^`>2buY#{XU^*;d8qlVcA~)9x|AL}*ukstmz+g$L6J_({FcTgMSC5$KQMG`-~N`_ zGsJdgIrpAPyZ-;$JIk&%yI_G9D_Y#WKykO=@ItWy#oZl>6-{w>f)sa$;_eOwiaS9A z6xZM`H@!dOuKVTrm}H&jteH78d!NktEydShh@_|K&aJH_IlXLo89j@1(iCp*a}}E0 zDjjhuzQV}W^XveRB)*2+ZZhS-P;8XJeC=3HU~pC4fh`QlGrc&@s=@SC1|Kot6qx+* zNn&5hr$Squxs>b?`bZ>b^R$(7cFWM~X~%x^%zj)F&i#x}cWd3U-i+81n9oKzs;TS5 z>TNCrD??#iGd31{>`xffIFp>Ow{`Gc(lN;#eE%EIX>uA_0N2<30PDckCql9h(^qq# zKsv2h>|?JVi8im*0hHoVWmG&ypD_dr+XL1VoA?!Dp!F-pmUYS2rVvO6UT!V>sC-eJPEVhsrf8jDF`bpe1VSI54es4mkR~ zUhPJQF7lwwQMax@h$*co3|RhM)MCO7Cju&#a&kc?DJQEpXBbdXQWzt2=ztM#kXxMtO5lyzpLc=@Ev(XsIir)Y?nT;>v;-aSDSmhdR`5mpEGV6 z!-B#l%2Lw-rvPSB7CKx=iiHbPu9NQbd)e*ior+pFL8)Ah4%Q>2ztgDmJp&f$ZqyO@+1-3DbU(^oO|MD+SuA&_~jbJOD`NbECoPwIh51y8ft) zzUO`*SC*1)3QYTo;0l~2UX_`Hr^zeE7xly&n=8|fhcUBlw z9c^U4NnyCHCob`y2FeNo9=KvjRJMdKce={{R};&>!=qPh zu(p^=t4gBXfvE5{+J_mrgo#a?;-tC47{L{m;#LNgQ*`WD@&ng$QZhSNoIoVTgpgJ0 zaR$SLX_lc`&6Ef(ikG_4_s*E@*`RUpHmjgNFZ9$XyTkE^AYG@E*bAJ0x;H)<92+P* ziMhGDcEsp3LWSImDam0wk>?r?bElX5@nbf{{_Q(=4ui%~=M0Rp9{`bS568C zm@Hp%9B*}CxN3Ef{EN3N_iu?p9%h%T#x6x#-ypoM6xOGD%#JcggSDWEq_0uGajO`z zLS?IW%^davXJAs zh7EXqgf2tO$gxb9$$v@poYf8AqU6WvjFY4|CpHC71he;I@buF$h$m%6(ya~6DUQ~` z3X3KgA75BVnWnJ|H>xDrqPE{^qi>yvuov$H3*kBk2b2)_V6Z_ zrLGaM%IZB<@eh5J`TV=uFAm?AK!n4K3r(yeB~M)FlpraNU36&o(dA;Ko5)t$A=YNK zg-HSTe|}*kcZ3%2gt#*KUD79}WO8yXV<;>9NWbX9c9t$qd=SsIZlGEI0E)n;zo7$P z5wC}we?B4LQbgz!3&MEmrC><};K7)}AoP5RJ1eWwGk#P~)OKm&pKkI!wmQkznnaaz2o~#0gZ_4VFHhv*d z^l7|ch!!<3vC1V)=4cW;gTv@t8pbUJWvt^7V!5)>MT1E_fo!YHfw0EQ(3dr~(2a%2?gsx6Dt&#zUfOuEPv;7BNZFxq(OvCKDz(@#u6S}`E; zkcvo8AK>jn>CBKvUMG6SKP$F@dLSLW7hb=$eX>04cxCwX4yj*R7d!91fQX`13W_6x z(K@p1I4QN4{4-J}NQ*h+I~k*9j*4#8f76lwB9KP5NURb1R6gL6(>B<4$k#2PA{ zVcn;HAD)XU_wcjjuy9Ik7{A4AFv?FrCqLvarc5H10mntyb*4a?$E9-UuP!buhlXoE zk{$4vF3F`lJ{n;}o3R+Lfc+mo2i1A1(_xCb6aZmkDEkb#maQpRo2ROO4d@0u)*FY- zn!ZskTA%a`u}5ek4<9UjyJAI+mr%u(qbr$(iTXKH8#d#T(9s@`Grl_AIi5CfRJDw; z%%UvVt5Y9yFyDA%@s^cShICt06+lz{q`B>D1cM`qK1zYMeJOW5S^tbOuUYAY8;oIr zua0Y6c3T^+|8a8(#V=WFy*=dQg^8&asJOK;0moL7ZIk+8pIF@g5v-I*r(Zs#C0nxM z!bKgYWTJ@Hn`nw0*qMX-isVD%%@92n?w`PS^+rtkK?gjMeFi|6$-vLg z(Pjk}ELn|uEUd|Y>174(^z-c$ljkKvAB)J95u2A?*e=`25BouiF*xB{#2He?IgD?b z{v~&r0akzQXLN6F_7#thybdBxGm@uk;(irAeHFO(f4<8;mYEsHMQ{s!Kv5P57wg)SuFUMS$<=VRVt_sa1D2Rc=WDVW-Xw;AaLyOJXc z!Tq@5kma`SikFhC6LZrF)J?0oY7(!HlXax6eWT%f2LI~o;lH4sj6d+s*E&zve?tr1 zr5tb6iY!O~n?R$+h7DBf=(e1qW8G>yM0df?c5l7E0t@8q7b>r?Ud;Yikh65AAO>O?vw` z)w7S3o{`oRRim&b^2H^zNy%37<`dA3pJtCloM!BKX{8|P${jzR2_4wo4=FD`n;*v- z?)Rlc`JAA3$9A&a^DjxRp=`>Il#o1@Id4;yh@!MD<6C)$HijZ%DuvEl8$O|I+HR3% zHiju{)_cG8hD6HQ*ALSfO1U54cx;FAr=_3D#J1s`;7r=;WNRNK?GTFltJHZL6Rn!B+Wl*X8L@41XmM6!zZFwP{2H$Bjl8BXQ$0qIeZS8m$+spsK z$%_XH(&!AFRX&>_K<~%2|il)U{_LDu(Xu5 z-X8_gx{utt1MlXhsMjs+ns6DiAAt{GyABr1WLWd}UjrGJL{rm)cmknddhja4c$lS5c!N^6kDa_ILg;)^JWC%NY9T`}y_tSGV7B zo0IyTKQ5*vMb_v@$Q*f+JZ)!dP|)Lh6A;fueP91Hgykd%WiWuk{{E3=6BxM-S+@~w z;wY^7GyHY2E;*oiGJm-HGqg6jxy&p4Y}%Vz@k7T2_ln8Qji7b5yB3m>F}70ah&WB0 z_eyv(EqX`ac51zE@KMmefkR+8SIuo-%Dc~;9ZK(1qGjlYT17FtA{gB`WeVXERTjsm zy_axi&jX3KN|_R|842W%$-OvEN=i&;9um`r;#{e*Bc+TpD$oBOfW|$77r;cPYfX2O z2iiFZvTspB1$o~yO{LF6K5~cq$sud^ygE79O@?f{&ewi@|KcuP*11mY`zy*4F@AKK zG@MdAYwifvg0~;M_ip<->fh;vXS9;LHZfFn!;V+H7 zLO6h`#$Q_J1FSHi=RvtyBk~vGwt1;p%~U}G;M2@vjIBClFOg3RRsCf`esMc7&|W5N z)uh>BFl?a$xMxmu93LS2PM8W>ANg4Z!(|{JUg)H}0x}B{P_%RWE25q}3&1zwuF(5T zo&05MUrWp+VN|NztbS|u-`TeL(4o9LLExGa<1j*BDI|zwS3M*pfpswe;Wf$FdEIC! zHf2&YfostWt~2LOcJUSQgL%Rx2mO5-RYGqOVpfPK&owfLi}%_=ZC(cdoSfbu7s@A@ z&YCQ-bih(hno%=aFA%-_wRcAK3^$jf>vIoF^;iq{F~wJEq44g?om7=UQ3DEE$j9Ht zQ!$cwrvlv}s~E?aQhvhELO*--a*5?T@OYz^AFg3xxB@lmoEA4N>7B<;fV_U zk;e7lAMuR7e>A0gpu;PT`?VnN`Ks_grCdE10I{`9F307azrB-_fBqkm>1TM$}8&*#fC(^WD71YH3pS1rgLrsEt0TNt8eArw~b1 z_s`PhO8y`T3(T%k1WBqGj1H1MhQ`#Tb$77v#_itli~aZA(PWPQAi;j4jfN?-yaNvQ z(9a0h&=a3$Pao?$(!4LX%oh08hYh9fpGOc9RS1h~L~P(Bo!BeN?H;dj-d}tsN7WHj zi0jw^bBa`+ZxNSCK|{rUe8djT#6d>A%t1ic_i2`v$vPdfc16Pnb66?H@ne)pAn2@< z!>7d`aLHz}^3Gf;=_Z*aprV)%ogFNGQpy8cQ1z9)NxB=3Z76BQ=IVd##eq@ilZf6nWzZaLJn*;aOY;L^o7BhIIpk{WX5I+RIsnVU2BI8iIZZ#5_? zbt9}OCJ8%XtWhNDh+o(3Do9i2Nebgbqo`TJlQBhA6p9z7IMv^n(d*QA?NRB92VaxK z^svfvH-$nKSmJz8J|9hcqtWg~n*wym4OE#s?-e)Vs;iqn+=c+!~31lHS2W3>S-NVD$$dXEj0@74ge+OYz&$w#ORFf0T zpdO;l3pLmN1G0|5hyG&-CV%i>JpV>a;V?e?a{6rlS53xWFShN`SR2gOGi<|FW&lCN zM!TQuWFb>gMZe*IIa^}p0nCeK*lkkc`0yV?YnBb|kC9kWF8B<0ZJv?|rKvm+_KyMo z`s^Dv{65O?54iD?y8F}S?<+sww7uojOjIJnqwa_EyAqQk=)J&R&SUG4<}+R|1N3Zy;t}lP_|PV{dN`blJX`7_JwD zZMi;sM@T3sDBR=&Z#$399M?nfgPN;v1Q7Kq-pb1Do;NJVQa)j2?^Hyh5N}QA{usX& zu2O&l;4B!3CA!RoRWAezy}!S>&`QTYwfdq{dU3)Ooxy6vnlu(*wp@!+X=k?@1zm78 zZQ3^%-gVU57++p~j{KWpcEkEImSX2CBV(u0n-)!f2grOBio%bQI-!3c$M+=Mp`2FaS#KsfE{xk#2 z69k8go!Oqzm%Z!bRYoUvs-((=2%iGJx0S*>P&01naU3dC=pX#vMb?@8_1llI9?-L5 zwa`z@uKvaFtsl}|nt=YyhBDIU`|X1KU$T;RH{7;FKPGir1E)etl3#Hu#nInzSDQt( zooFK;8ou^=aUGsY;e~JeEaV>cf)>N$S5{GS7o2{gLyy$nhOMmb;6H^cHO?%x)bK|)s<8e>n?~#aSXJ_aZq~9b2Ccq6> zuXof=o`6x*Jpqo6C2>3HGyS8}jRY)O*XSgL7iBkGtuv34N`Q?=fgJj5p87a@U&DV` zOh)?|IskfH8BV{rFNSilE?@FhlzTp;H(YRg)EIx3_+7X8J*1t{6 zJWE7_|4p^eUi%b^r=4V^YWm|0%g!b7JHV_cja#gtU|x2yxrrjGk@{BO!6J!K^M{ux z+y`?D(^b=4xNzQEN&b0>J)g4Qq2Jv&LUI$<{V-y$9zGbJYE%8Rn5tM=Ng0ayt}SJ5 zPVeYjbQeK82-uszrveDQyHMP>sr52qg6lUhd^I*+Fnp*jgKWDJZKT6k6Ugjpi3f%P4*tW|Hul zAW)MKZ94<2MPIm%bz&dJuF|Kkmsb!7f$FbLo0$TVuHYjKfivsB8{7xq*!cV^hD{xwcbXK2dH=D4Vqj?mp`?T5hCW4^fTCgVWEX zebbaz4*K&7m~o`hmd>Cw{fUmE^PP7{x}bl8nkdZ|Jgl03tdyJs)nW`0b8`n^eY0E< zZ>^PHKxtyS3U#{-4e2F4d1$WU!#c%>a6bE)%zwaisEfAb?nsX|;WA%J`S&wmf9~CF zMw>Fj?~TP8-3E46@hw2qO}0YLfx8$xG!QD zarBkTYBt1l3oxdr4C^gMl3pYgm9CFR8|RZz%dBm+IeHCG;RTM-Io}hRta0?FWubac zYiuDur-W~{>@G50CC;@EsZ^{(i)c7I-9YM+aBKJ?EVU7j8uV*b%d*3rcOr-DkN#01y;Br&CZU zGca|>>CR!r+&M4p+ht^AB)p$j5GAi%HznIM_SVT0Ov3r&8io8_3J|caNMMxO|KFXO zWr~?dvX}oBu_xD{jB`@tj+qk_?!aT}a42n`pH^_McQ$K7^j z9x6y>eK*)II=$(5AJ#T?BBfN=EkMcd;~8a!Ky6=gv-&-JBye$WTxbjHhfw@Yj6A;3 z;|#t1_@DZp`p@ywBy^lOf%Qe#w*nH^a?a#wH_TEIGj(an+33jKnErMbeuPHK2!Yss z>N{qMay9x5B@_+wa^@rEfUZ~dG`0!5x4U|xz64G&9pPI z;;`9aw`etW7dr5r?d48ZrT7+St37luHr~$an)a>{NUv@+m%Y_?SA94}`VlWti+C>Z ziqv@ixwcB>5e1$NvVD#NrS)f0*lmK)N$y0cz-^#z+6FCPb|HTj8nL^8dCL6*@8N?- z^+htTU_D@{?xqdJm9E!=h6Z}1NOy#D%nQBxebZZ+H47FlO7J^=N0#anRZ|Iynh)bU zS^&NU*bBwq2e-7;NOc%T7C1DMaKf=Re`pQu9mJZLMZcKnB{M^K#TJPFz+kNlvn)_` zy?9t7PuJ-v{iShX!wn&9n$3x6?+dd!kqIP>sy`kJgG;1B3m0tt)Bj)<&ec#KFPgsV?GWz}$bdfgY#V?BX6ah%hO> zPnQpPnH}tqGU{mdEjGTqOisiWYBmKd*@CwCA22Z%$1>l|3i)wBM=R4<;I_%VrUGC* zxU!I%pP;t64#WUnrzT--9jt9#9ky&2<`x0`C>em>+8|3PFE4WBaIMz5xAJ3>z{PX$u)$vvQo>Pe1bkB_3aMyxJT^`W;2CEImhhs4pT~;&a2>5Sbm@Zx)XX7I+r( zsa95m3+7|nB*%+8zX-?(T>q1{QN6qmZ_V5{QU8EiRZUr4E4>yl&9V2A`__NM&MwynYY0zvkD7>q@tM8GQ^`jHVtt23XJEV8P z?BH6=zsnPp+#n|G?@?l2nCKSY^M`YsKk|-ZAruo1}s(QAxZ$MiB7}9>H6* zV6BlWZ&Vfil8xCNje6CUq*MIaxp~QvY?ljd013@8&w5N_YiYxa)jjQ-=q0%^nT*8I z55enFcH3^ExG)dmFhPI!qSlV4riC=lR9w+P{VcLlBm*o;ovbu}qB?q1e964RLidjE zJ7BbgzV>CD=@mYH*w!kg>V<3-G2ym0OpGqgMl=g_SGyNqRmCht&T zGl^QJXKe6@p+H}R5su=RrLQ@4V%vhzC~~ST_RIA}(xl4e<@?6TLdi}8?YSQk39SOB zQ8($ijd-pp`-vEG4Qnu}%RrLokf5EMQC zHL)aXuosVw1UU-1XxHD4?|&DE`v`*_bP#3Hv82Yu{#SaP{d`EqMVZ5=ul@}HyQQ|! zj6hc0{up*daY!qEd)m5twtEl9Vff8ShJU93bb;ArjlmwWI5le?IPc_gm{vRL#nqFx z?k7-r8}yAKA7+FH1u3)%;+-#!QYZdKR^h?~j0vv3y|n;axLh>d$%mnBK&zI9Vglyr zY5Zn3OW^8kv5EteSOAyI%x7b{=mN=kliuNbd@8&Oj``1cAbQeBYMQ8g89XXAI|&~N zJ87H4v&oZjvE(SMB{dCZ2O#k50=Qyd=H4=XGC7&W^OSATzymxR5X|-7gS-jet+^i8 z1vJeBO-`Qe-YhLDw#;nwSR3O?*Jz$@Z}1orjR9oxNSW&6SX}yKo zWApsD^JEkiGCA|84o`h0tGFm{#cO_GL79^&Ulayi!Jb2lbT6ja0NuHX!-6M@@^feG z@-k+YiqKv`^nG8os`|KjGsDX5Hzc>+x#I-KugVuDrcl~QC8P2@$m&(!q3zz4d|=6m ziz_$Ki$~+ALrI%(qq^m=Y|e9oI*{2Z=0ia!M3vszO*UB0x>Hxwe(0D?89lV$T&VBa zc8J7b9Bq`XSxhMvsY$;AGVM_B1i|IH~zRMZIpp(`|{^=5F*+_^nw zKi6Tt&hyIZChOZ7_&e7d+lMaiM3-enDLv%2x$DZjyn*+PH~` z)9{quhKFh2dMa*Qz3W<^YSewF+z?6G@~mm`&CMMh+=Mb*o)+KoSr}Y_zDN2zZK((M z3&P8HidV`KsR8;_XH14C#-rbIX>Q7T^w*sr>B`F*|6wbZlFx}MSxV^Q*%ysMMpm^1>%rY_j96mAWAAi;1 z;Hj^!&V-o;y&^n>RoTxhZ>6_4Hx%s`VQe+iD73k1-wyw7KCmk|w3WdYJ=?!EsYl#g zj~#rSUFtu)|A?Z&GpnQm_hMpfY-BzQEyprUpgB(ZsX&#ms*9NRx=uC%bAO{hZ;yfg6Pb* zwF4CxnO@(cd+#KP(|Vzlxj~V6keOcuzH?h){2-K=mMz)XBN2dhtzKwdLj)uI_~#ppRhO>;kBq z`KLXT@7X_HAn%NyLBL)90{L~|=nE*a2S-v!o)%smmGS$N+E2O~t>_c+(v0v!0u&uG zd#c9~mr@BxxxJ5=9k<=W;Prmu#6V*h>iE-SvjYohvpW`l<$q$2PMuqiAo^HH+2unL zvi!F*x}}34KD$NMa=OGmq-%C{{wSvkI#*DZY~aP~mYLIc^N#T8Q<*9R0gE9gc18Loh#uf;zKqIF)Yj%fCB{K)4S zJ=J$dh7T?A08SFnUU$veV=XVF?7n7d?^g?bJF}jj%ggUnb;Rzj3C6-a6 z#RWkW1*D?)m7~;mVmKX5}Q9fHG@locdDg`nWB0XAg2+zBP5{D;twRSdAc))SFtl|He*Mqf&fjmF^6yNyT`R z*BOe2^1d6hu}Sq( zSC5)S`9mav5ru>~jJd7i{N!eiglX*d0gNONM=dz1T=<8MLi_8kt?up@2)!F2UbDyP ziYq1+%f>hRYoly*qN%?ndysNX-Pw2K;#xycBmd=M;bw=`Av?x2sBwoD@##+(LBW0?b%5o_yTjdJ!f0~L%t>gRvG4|40m;0XwYhR&aR*Em;Io| z<2ciKS(sk|dM6v;y*SJSHG2T)tF;elxKPFmB(%X{`&2CAFJkVzK5=KF!rp{{-M4ag zjA=`lMY^~q$~oOK{Duf-2xY11YHku~d}Cv#qSDx{sSwk5=&oXF!oZF)eg?5|=U3pwwU-0~#I8XqPF4Xb*1&QLa|Xo8)ReKZpsSjA4rhFDwrJ5IbFgjBQ|fUC1T6%U z-kZppZBq=IL5-B=@*}kQ;D>{sG}pC{m~RRBqS`n8Z286jhn=#?HKJ%fuh`NfzHNk0 zc0ehw#Jz=Mdv^~HVC?b*pxl7DHBPAO>&Xe-*{@hTtn$@OUuQFauCys=&Y@L5)`5E& z7sbk5>sR~zN&G^>F$U-7_CB-RV<6G9o#$}*>a|qIa&!!_Yt4{FHzc|-9tBYZ=lg%F zqndVr<$BF&E91h%?JvqdwKTccKDd!mn1_S5DXg1*V@)sQGk5Q2J^Tyx!B;hrF{ zwWjE{8F|=W@M2Kn5#%u%zwu^CRp-djNSnd(HOV{AB(aVnpq~B>PeE}!MG=>?oJY6} z`nK~Ot8yi8^OU@&-u|^r;ur@>&p*oi={tXO2O6yY@++=uQ=eO37kx{gxeh%7`xH-R zXX2w6l#OmaeB9I9Uw1|dLO7~cnNAz>Os>8NdW-JhiWxM#~=s;eyRKtj=! zTEXv;9BZhpYkspa04FrrnK&wRi+KcW(g!r2X-Ex1rvxi*W|pfJ`K2YGkCqe^abde; zxywINdibm|^%54;7RhUFj=|PgqP{zkGRTNR5_cG>XzLUCU)ZsK1e(kaxt`-r3yS0R27@rrd^#BcWV2i@0+JW2{^ z>_Vpq-ob%RhndLNn)zRJ+P}D46}PK6)NxmRTOX@jd;cr8%pE-wBg?(7;cik+B@QEE zx2MQDl^AI-mOur>WI_c}_7N-LDt*gdIv8<@7UUI=#hNjs--PFqpN(tTOC3A)7;mm< za!c1!SS8TuK-m!*^I|WV+i)3|}3i zH!N`}tUr7mSF_XLuNuwgVvg1t<}9o)<1VW&t3Mj8^mZKl{CpfnOR*5nrQ{64F=rlr zn|YEd9~UcL1!5jPmAc)?g76Rf7?ob_ao2k@#!i6Bqm}^bfOoKUfjyL1Hmfn)& Date: Wed, 14 Mar 2018 19:06:48 +0100 Subject: [PATCH 19/39] Remove tick --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cce7c42d..3a127b70 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

A delightful way to building a RESTful API Services with beautiful code written in TypeScript.
- > Inspired by the awesome framework laravel in PHP and of the repositories from pleerock + Inspired by the awesome framework laravel in PHP and of the repositories from pleerock


From 4eae03caec583425819988b056637717d5d9fd6e Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 21:09:15 +0100 Subject: [PATCH 20/39] improve doc header --- README.md | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3a127b70..3566b591 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@
+> An Node.js Web-Service boilerplate/skeleton/starter-kit with TypeScript, Express, Winston, Microframework, TypeDI, TypeORM, TsLint, @types, Jest, Swagger, validatejs, GraphQL, DataLoaders, by w3tech + ![divider](./w3tec-divider.png) ## ❯ Why @@ -57,17 +59,17 @@ Try it!! We are happy to hear your feedback or any kind of new features. ## ❯ Table of Contents -- [Getting Started](#getting-started) -- [Scripts and Tasks](#scripts-and-tasks) -- [Debugger in VSCode](#debugger-in-vscode) -- [API Routes](#api-routes) -- [Project Structure](#project-structure) -- [Logging](#logging) -- [Event Dispatching](#event-dispatching) -- [Seeding](#seeding) -- [Further Documentations](#further-documentation) -- [Related Projects](#related-projects) -- [License](#license) +- [Getting Started](#-getting-started) +- [Scripts and Tasks](#-scripts-and-tasks) +- [Debugger in VSCode](#-debugger-in-vscode) +- [API Routes](#-api-routes) +- [Project Structure](#-project-structure) +- [Logging](#-logging) +- [Event Dispatching](#-event-dispatching) +- [Seeding](#-seeding) +- [Further Documentations](#-further-documentation) +- [Related Projects](#-related-projects) +- [License](#-license) ![divider](./w3tec-divider.png) @@ -162,7 +164,9 @@ All script are defined in the `package-scripts.js` file, but the most important - Run `npm start db.seed` to seed your seeds into the database. -## Debugger in VSCode +![divider](./w3tec-divider.png) + +## ❯ Debugger in VSCode To debug your code run `npm start build` or hit `cmd + b` to build your app. Then, just set a breakpoint and hit `F5` in your Visual Studio Code. From f6ba3c850d4926966433df441f97f7bced9e6aa7 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 21:18:06 +0100 Subject: [PATCH 21/39] Add tech-stack --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3566b591..c2c7b5f5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ w3tec

-

❯ Express Typescript Boilerplate

+

Express Typescript Boilerplate

@@ -14,6 +14,9 @@ appveyor + + StackShare +

From ca672dcdc1a1298b93c0a1b792146661f4f2215a Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 21:30:00 +0100 Subject: [PATCH 22/39] fix e2e test --- test/e2e/api/users.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/e2e/api/users.test.ts b/test/e2e/api/users.test.ts index 1290b549..51e4b2e1 100644 --- a/test/e2e/api/users.test.ts +++ b/test/e2e/api/users.test.ts @@ -3,7 +3,7 @@ import * as request from 'supertest'; import { User } from '../../../src/api/models/User'; import { CreateBruce } from '../../../src/database/seeds/CreateBruce'; -import { runSeeder } from '../../../src/lib/seed'; +import { runSeed } from '../../../src/lib/seed'; import { closeDatabase } from '../../utils/database'; import { fakeAuthenticationForUser } from '../utils/auth'; import { BootstrapSettings } from '../utils/bootstrap'; @@ -20,7 +20,7 @@ describe('/api/users', () => { beforeAll(async () => { settings = await prepareServer({ migrate: true }); - bruce = await runSeeder(CreateBruce); + bruce = await runSeed(CreateBruce); fakeAuthenticationForUser(bruce, true); }); From e3a797ce697d65a472c437e50aad878022883bdb Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 21:33:20 +0100 Subject: [PATCH 23/39] add all test to travis --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c79b4af4..df3a0a2e 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,8 @@ install: - yarn install scripts: - npm test + - npm test.integration + - npm test.e2e - npm start build notifications: email: false From 9f9cd15913149e44d937856a0a1696267d6a210b Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 21:59:28 +0100 Subject: [PATCH 24/39] add all test to travis --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index df3a0a2e..0a6a1e83 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,8 +5,8 @@ install: - yarn install scripts: - npm test - - npm test.integration - - npm test.e2e + - npm start test.integration + - npm start test.e2e - npm start build notifications: email: false From 392ba21b58ce3e3838d10068d806592a96313074 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 22:06:25 +0100 Subject: [PATCH 25/39] update travis config --- .travis.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a6a1e83..05302261 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,16 @@ language: node_js node_js: - - "8.2.1" + - "8.9.4" install: - - yarn install + - yarn install +env: + - DB_TYPE="sqlite" + - DB_DATABASE="./mydb.sql" + - DB_LOGGING=false scripts: - - npm test - - npm start test.integration - - npm start test.e2e - - npm start build + - npm test + - npm start test.integration + - npm start test.e2e + - npm start build notifications: - email: false + email: false From b53bff4af70b15b7d25961bb3ea350effe68e07f Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 22:13:06 +0100 Subject: [PATCH 26/39] add 2 spaces --- .travis.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 05302261..91225b62 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,16 +1,16 @@ language: node_js node_js: - - "8.9.4" + - "8.9.4" install: - - yarn install + - yarn install env: - - DB_TYPE="sqlite" - - DB_DATABASE="./mydb.sql" - - DB_LOGGING=false + - DB_TYPE="sqlite" + - DB_DATABASE="./mydb.sql" + - DB_LOGGING=false scripts: - - npm test - - npm start test.integration - - npm start test.e2e - - npm start build + - npm test + - npm start test.integration + - npm start test.e2e + - npm start build notifications: - email: false + email: false From b0fae5664f9ab3ba5151cb4dabaf54ee536fd255 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 22:16:31 +0100 Subject: [PATCH 27/39] update travis config --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 91225b62..ac28f9ce 100755 --- a/.travis.yml +++ b/.travis.yml @@ -4,13 +4,11 @@ node_js: install: - yarn install env: - - DB_TYPE="sqlite" - - DB_DATABASE="./mydb.sql" - - DB_LOGGING=false + - DB_TYPE="sqlite" DB_DATABASE="./mydb.sql" DB_LOGGING=false scripts: - - npm test + - npm start build + - npm start test - npm start test.integration - npm start test.e2e - - npm start build notifications: email: false From cbbef9b0af151bbedd95ba610e1fa2d270a56ce9 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 22:21:39 +0100 Subject: [PATCH 28/39] update travis config --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ac28f9ce..5560e288 100755 --- a/.travis.yml +++ b/.travis.yml @@ -5,10 +5,10 @@ install: - yarn install env: - DB_TYPE="sqlite" DB_DATABASE="./mydb.sql" DB_LOGGING=false -scripts: - - npm start build +script: - npm start test - npm start test.integration - npm start test.e2e + - npm start build notifications: email: false From bb27906dbdd2bfc01c73ea7ef98932bb8472c769 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Wed, 14 Mar 2018 22:27:46 +0100 Subject: [PATCH 29/39] update config appveyor --- appveyor.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 3bc1c57b..d9d00e55 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,8 @@ environment: nodejs_version: "8" + DB_TYPE: "sqlite" + DB_DATABASE: "./mydb.sql" + DB_LOGGING: false install: - ps: Install-Product node $env:nodejs_version @@ -9,4 +12,6 @@ build_script: - npm start build test_script: - - npm test + - npm start test + - npm start test.integration + - npm start test.e2e From d8e50f58a785da37fd358494dbd56c10b32ca69b Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Thu, 15 Mar 2018 07:32:26 +0100 Subject: [PATCH 30/39] add new images --- w3tec-divider.png => w3tec-divide.png | Bin w3tec-logo.png => w3tec-log.png | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename w3tec-divider.png => w3tec-divide.png (100%) rename w3tec-logo.png => w3tec-log.png (100%) diff --git a/w3tec-divider.png b/w3tec-divide.png similarity index 100% rename from w3tec-divider.png rename to w3tec-divide.png diff --git a/w3tec-logo.png b/w3tec-log.png similarity index 100% rename from w3tec-logo.png rename to w3tec-log.png From f31e73b25d6e5fb8229255dadecbefce3fe6aa18 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Thu, 15 Mar 2018 07:32:43 +0100 Subject: [PATCH 31/39] add new images --- w3tec-divide.png => w3tec-divider.png | Bin w3tec-log.png => w3tec-logo.png | Bin 2 files changed, 0 insertions(+), 0 deletions(-) rename w3tec-divide.png => w3tec-divider.png (100%) rename w3tec-log.png => w3tec-logo.png (100%) diff --git a/w3tec-divide.png b/w3tec-divider.png similarity index 100% rename from w3tec-divide.png rename to w3tec-divider.png diff --git a/w3tec-log.png b/w3tec-logo.png similarity index 100% rename from w3tec-log.png rename to w3tec-logo.png From 8035ea233891465dc57846b3e6c075a5aecb4981 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Thu, 15 Mar 2018 07:45:50 +0100 Subject: [PATCH 32/39] Fix seeding --- src/database/seeds/CreatePets.ts | 3 +-- src/lib/seed/utils.ts | 10 ++++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/database/seeds/CreatePets.ts b/src/database/seeds/CreatePets.ts index 76ef89fc..9a0bd2c5 100644 --- a/src/database/seeds/CreatePets.ts +++ b/src/database/seeds/CreatePets.ts @@ -8,12 +8,11 @@ export class CreatePets implements Seed { public async seed(factory: Factory, connection: Connection): Promise { const em = connection.createEntityManager(); - await times(10, async (n) => { const pet = await factory(Pet)().seed(); const user = await factory(User)().make(); user.pets = [pet]; - await em.save(user); + return await em.save(user); }); } diff --git a/src/lib/seed/utils.ts b/src/lib/seed/utils.ts index 98bcb7e4..88b05037 100644 --- a/src/lib/seed/utils.ts +++ b/src/lib/seed/utils.ts @@ -11,5 +11,11 @@ export const isPromiseLike = (o: any): boolean => !!o && (typeof o === 'object' /** * Times repeats a function n times */ -export const times = async (n: number, iteratee: (index: number) => Promise): Promise => - Promise.all(new Array(n).map(async (v, i) => await iteratee(i))); +export const times = async (n: number, iteratee: (index: number) => Promise): Promise => { + const rs = [] as TResult[]; + for (let i = 0; i < n; i++) { + const r = await iteratee(i); + rs.push(r); + } + return rs; +}; From 6485ae09a4894a8f36adbad44b8fb3095db180b2 Mon Sep 17 00:00:00 2001 From: Albert G Date: Thu, 15 Mar 2018 14:51:07 +0100 Subject: [PATCH 33/39] Removing autoimport from extensions Removing autoimport from extensions since it's included in VSCode: https://code.visualstudio.com/updates/v1_18#_languages --- .vscode/extensions.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 6ffc870d..485cb437 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -2,7 +2,6 @@ "recommendations": [ "streetsidesoftware.code-spell-checker", "eg2.tslint", - "steoates.autoimport", "EditorConfig.EditorConfig", "christian-kohler.path-intellisense", "mike-co.import-sorter", From aac4a764db32d15cb016679ecf2e8252f29feb17 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 17 Mar 2018 13:08:23 +0100 Subject: [PATCH 34/39] add new style for keys in the readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c2c7b5f5..1e9af37d 100644 --- a/README.md +++ b/README.md @@ -171,8 +171,8 @@ All script are defined in the `package-scripts.js` file, but the most important ## ❯ Debugger in VSCode -To debug your code run `npm start build` or hit `cmd + b` to build your app. -Then, just set a breakpoint and hit `F5` in your Visual Studio Code. +To debug your code run `npm start build` or hit cmd + b to build your app. +Then, just set a breakpoint and hit F5 in your Visual Studio Code. ![divider](./w3tec-divider.png) From 4830cd981bb0dd0d0d6e560b3566e34f213fd9a4 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 17 Mar 2018 13:10:29 +0100 Subject: [PATCH 35/39] fix dataloaders for multiple results --- src/api/types/PetType.ts | 2 +- src/api/types/UserType.ts | 2 +- src/lib/graphql/dataloader.ts | 21 --------------------- src/lib/graphql/index.ts | 16 +++++++++++----- src/loaders/graphqlLoader.ts | 10 +++++++--- 5 files changed, 20 insertions(+), 31 deletions(-) delete mode 100644 src/lib/graphql/dataloader.ts diff --git a/src/api/types/PetType.ts b/src/api/types/PetType.ts index 3ae159ad..00be2b8a 100644 --- a/src/api/types/PetType.ts +++ b/src/api/types/PetType.ts @@ -35,7 +35,7 @@ export const PetType = new GraphQLObjectType({ type: OwnerType, description: 'The owner of the pet', resolve: (pet: Pet, args: any, context: GraphQLContext) => - context.dataLoaders.users.load(pet.userId), + context.dataLoaders.user.load(pet.userId), }, } }), }); diff --git a/src/api/types/UserType.ts b/src/api/types/UserType.ts index 1da349e4..8aab22cc 100644 --- a/src/api/types/UserType.ts +++ b/src/api/types/UserType.ts @@ -34,7 +34,7 @@ export const UserType = new GraphQLObjectType({ description: 'The pets of a user', resolve: async (user: User, args: any, context: GraphQLContext) => // We use data-loaders to save db queries - context.dataLoaders.petByUserIds.loadMany([user.id]), + context.dataLoaders.petsByUserIds.load(user.id), // This would be the case with a normal service, but not very fast // context.container.get(PetService).findByUser(user), }, diff --git a/src/lib/graphql/dataloader.ts b/src/lib/graphql/dataloader.ts deleted file mode 100644 index 7482ebd0..00000000 --- a/src/lib/graphql/dataloader.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface Identifiable { - id?: number | number; -} - -export function ensureInputOrder(ids: number[] | string[], result: T[], key: string): T[] { - // For the dataloader batching to work, the results must be in the same order and of the - // same length as the ids. See: https://github.com/facebook/dataloader#batch-function - const orderedResult: T[] = []; - for (const id of ids) { - const item = result.find(t => t[key] === id); - if (item) { - orderedResult.push(item); - } else { - /* tslint:disable */ - // @ts-ignore - orderedResult.push(null); - /* tslint:enable */ - } - } - return orderedResult; -} diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts index 82743967..7a7450d0 100644 --- a/src/lib/graphql/index.ts +++ b/src/lib/graphql/index.ts @@ -6,7 +6,6 @@ import { Container as container, ObjectType } from 'typedi'; import { getCustomRepository, getRepository, Repository } from 'typeorm'; import { getFromContainer } from './container'; -import { ensureInputOrder } from './dataloader'; import { getErrorCode, getErrorMessage, handlingErrors } from './graphql-error-handling'; import { GraphQLContext, GraphQLContextDataLoader } from './GraphQLContext'; import { importClassesFromDirectories } from './importClassesFromDirectories'; @@ -29,10 +28,16 @@ export * from './container'; // Main Functions // ------------------------------------------------------------------------- +export interface CreateDataLoaderOptions { + method?: string; + key?: string; + batch?: boolean; +} + /** * Creates a new dataloader with the typorm repository */ -export function createDataLoader(obj: ObjectType, method?: string, key?: string): DataLoader { +export function createDataLoader(obj: ObjectType, options: CreateDataLoaderOptions = {}): DataLoader { let repository; try { repository = getCustomRepository>(obj); @@ -46,13 +51,14 @@ export function createDataLoader(obj: ObjectType, method?: string, key?: s return new DataLoader(async (ids: number[]) => { let items = []; - if (method) { - items = await repository[method](ids); + if (options.method) { + items = await repository[options.method](ids); } else { items = await repository.findByIds(ids); } - return ensureInputOrder(ids, items, key || 'id'); + const handleBatch = (arr: any[]) => options.batch === true ? arr : arr[0]; + return ids.map(id => handleBatch(items.filter(item => item[options.key || 'id'] === id))); }); } diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts index 61abca0e..28f0abc6 100644 --- a/src/loaders/graphqlLoader.ts +++ b/src/loaders/graphqlLoader.ts @@ -16,9 +16,13 @@ export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSett queries: env.app.dirs.queries, mutations: env.app.dirs.mutations, dataLoaders: { - users: createDataLoader(UserRepository), - pets: createDataLoader(Pet), - petByUserIds: createDataLoader(PetRepository, 'findByUserIds', 'userId'), + user: createDataLoader(UserRepository), + pet: createDataLoader(Pet), + petsByUserIds: createDataLoader(PetRepository, { + method: 'findByUserIds', + key: 'userId', + batch: true, + }), }, }); From 9c687c892a359fbdc7361462cd993dbc1bdaebb0 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 17 Mar 2018 13:14:09 +0100 Subject: [PATCH 36/39] fix typo --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e9af37d..fc0bdc1b 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ export class UserService { ## ❯ Event Dispatching -Our we use this awesome repository [event-dispatch](https://github.com/pleerock/event-dispatch) for event dispatching. +We use this awesome repository [event-dispatch](https://github.com/pleerock/event-dispatch) for event dispatching. We created a simple annotation to inject the EventDispatcher in your service (see example below). All events are listed in the `events.ts` file. ```typescript From d7e5ff381ebe2c26ffa6cf4d5fa11a71b7605b91 Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 17 Mar 2018 14:32:20 +0100 Subject: [PATCH 37/39] update readme --- README.md | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fc0bdc1b..d65c3455 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,13 @@

- A delightful way to building a RESTful API Services with beautiful code written in TypeScript.
- Inspired by the awesome framework laravel in PHP and of the repositories from pleerock + A delightful way to building a Node.js RESTful API Services with beautiful code written in TypeScript.
+ Inspired by the awesome framework laravel in PHP and of the repositories from pleerock
+ Made with ❤️ by w3tech, Gery Hirschfeld and contributors


-> An Node.js Web-Service boilerplate/skeleton/starter-kit with TypeScript, Express, Winston, Microframework, TypeDI, TypeORM, TsLint, @types, Jest, Swagger, validatejs, GraphQL, DataLoaders, by w3tech - ![divider](./w3tec-divider.png) ## ❯ Why @@ -512,6 +511,3 @@ DB_PORT=3306 ## ❯ License [MIT](/LICENSE) - ---- -Made with ♥ by w3tech ([w3tech](https://github.com/w3tecch)), Gery Hirschfeld ([@GeryHirschfeld1](https://twitter.com/GeryHirschfeld1)) and [contributors](https://github.com/w3tecch/express-typescript-boilerplate/graphs/contributors) \ No newline at end of file From 76b63eabc2b9e6d4043b7466a05b4b16874e148f Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 17 Mar 2018 14:56:30 +0100 Subject: [PATCH 38/39] rename variable --- src/lib/graphql/index.ts | 4 ++-- src/loaders/graphqlLoader.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts index 7a7450d0..f50a9a1d 100644 --- a/src/lib/graphql/index.ts +++ b/src/lib/graphql/index.ts @@ -31,7 +31,7 @@ export * from './container'; export interface CreateDataLoaderOptions { method?: string; key?: string; - batch?: boolean; + multiple?: boolean; } /** @@ -57,7 +57,7 @@ export function createDataLoader(obj: ObjectType, options: CreateDataLoade items = await repository.findByIds(ids); } - const handleBatch = (arr: any[]) => options.batch === true ? arr : arr[0]; + const handleBatch = (arr: any[]) => options.multiple === true ? arr : arr[0]; return ids.map(id => handleBatch(items.filter(item => item[options.key || 'id'] === id))); }); } diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts index 28f0abc6..4a790e32 100644 --- a/src/loaders/graphqlLoader.ts +++ b/src/loaders/graphqlLoader.ts @@ -21,7 +21,7 @@ export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSett petsByUserIds: createDataLoader(PetRepository, { method: 'findByUserIds', key: 'userId', - batch: true, + multiple: true, }), }, }); From 009b9892d6fce07f4730999d15a47f7b934187dd Mon Sep 17 00:00:00 2001 From: hirsch88 Date: Sat, 17 Mar 2018 14:58:56 +0100 Subject: [PATCH 39/39] bump version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index af7e6ab6..b04d8e18 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "express-typescript-boilerplate", - "version": "3.0.0-rc.2", - "description": "A delightful way to building a RESTful API with NodeJs & TypeScript", + "version": "3.0.0", + "description": "A delightful way to building a Node.js RESTful API Services with beautiful code written in TypeScript", "main": "src/app.ts", "scripts": { "start": "nps",