diff --git a/.env.example b/.env.example index a682af6b..79e94b5b 100644 --- a/.env.example +++ b/.env.example @@ -30,6 +30,13 @@ DB_DATABASE="my_database" DB_SYNCHRONIZE=false DB_LOGGING=false +# +# GraphQL +# +GRAPHQL_ENABLED=true +GRAPHQL_ROUTE="/graphql" +GRAPHQL_EDITOR=true + # # Swagger # diff --git a/.env.test b/.env.test index f08dec70..eafcd105 100644 --- a/.env.test +++ b/.env.test @@ -21,15 +21,16 @@ AUTH_ROUTE="http://localhost:3333/tokeninfo" # # DATABASE # -DB_TYPE="mysql" -DB_HOST="localhost" -DB_PORT=3306 -DB_USERNAME="root" -DB_PASSWORD="" -DB_DATABASE="my_database" -DB_SYNCHRONIZE=false +DB_TYPE="sqlite" +DB_DATABASE="./mydb.sql" DB_LOGGING=false +# +# GraphQL +# +GRAPHQL_ENABLED=true +GRAPHQL_ROUTE="/graphql" + # # Swagger # diff --git a/.gitignore b/.gitignore index 032e2e7e..5797fd36 100755 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ typings/ # Dist # dist/ ormconfig.json +tsconfig.build.json # IDE # .idea/ @@ -34,3 +35,4 @@ test/**/*.js test/**/*.js.map coverage/ !test/preprocessor.js +mydb.sql diff --git a/.vscode/settings.json b/.vscode/settings.json index 5fe3577b..91b76b5f 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,8 @@ { "typescript.tsdk": "./node_modules/typescript/lib", - "cSpell.enabled": true + "cSpell.enabled": true, + "files.exclude": { + "tsconfig.build.json": true, + "ormconfig.json": true + } } diff --git a/README.md b/README.md index ca0259f7..0afae505 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,8 @@ [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 @@ -40,15 +42,13 @@ Try it!! We are happy to hear your feedback or any kind of new features. - **API Documentation** thanks to [swagger](http://swagger.io/). - **API Monitoring** thanks to [express-status-monitor](https://github.com/RafalWilinski/express-status-monitor). - **Integrated Testing Tool** thanks to [Jest](https://facebook.github.io/jest). +- **E2E API Testing** thanks to [supertest](https://github.com/visionmedia/supertest). - **Basic Security Features** thanks to [Helmet](https://helmetjs.github.io/). - **Easy event dispatching** thanks to [event-dispatch](https://github.com/pleerock/event-dispatch). - **Fast Database Building** with simple migration from [TypeORM](https://github.com/typeorm/typeorm). - **Easy Data Seeding** with our own factories. - -### Comming soon - -- **Custom Commands** are also available in our setup and really easy to use or even extend. -- **Scaffolding Commands** will speed up your development tremendously as you should focus on business code and not scaffolding. +- **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 @@ -96,7 +96,7 @@ Create a new database with the name you have in your `.env`-file. Then setup your application environment. ```bash -npm start setup +npm run setup ``` > This installs all dependencies with yarn. After that it migrates the database and seeds some test data into it. So after that your development environment is ready to use. @@ -128,7 +128,8 @@ All script are defined in the package.json file, but the most important ones are ### Tests - Run the unit tests using `npm start test` (There is also a vscode task for this called `test`). -- Run the e2e tests using `npm start test:e2e` and don't forget to start your application and your [Auth0 Mock Server](https://github.com/hirsch88/auth0-mock-server). +- Run the integration tests using `npm start test:integration`. +- Run the e2e tests using `npm start test:e2e`. ### Running in dev mode @@ -165,9 +166,11 @@ The swagger and the monitor route can be altered in the `.env` file. | Route | Description | | -------------- | ----------- | | **/api** | Shows us the name, description and the version of the package.json | +| **/graphql** | Route to the graphql editor or your query/mutations requests | | **/swagger** | This is the Swagger UI with our API documentation | | **/monitor** | Shows a small monitor page for the server | | **/api/users** | Example entity endpoint | +| **/api/pets** | Example entity endpoint | ## Project Structure @@ -187,6 +190,9 @@ The swagger and the monitor route can be altered in the `.env` file. | **src/api/services/** | Service layer | | **src/api/subscribers/** | Event subscribers | | **src/api/validators/** | Custom validators, which can be used in the request classes | +| **src/api/queries/** | GraphQL queries | +| **src/api/mutations/** | GraphQL mutations | +| **src/api/types/** | GraphQL types | | **src/api/** swagger.json | Swagger documentation | | **src/auth/** | Authentication checkers and services | | **src/core/** | The core features like logger and env variables | @@ -199,9 +205,12 @@ The swagger and the monitor route can be altered in the `.env` file. | **src/types/** *.d.ts | Custom type definitions and files that aren't on DefinitelyTyped | | **test** | Tests | | **test/e2e/** *.test.ts | End-2-End tests (like e2e) | +| **test/integration/** *.test.ts | Integration test with SQLite3 | | **test/unit/** *.test.ts | Unit tests | | .env.example | Environment configurations | +| .env.test | Test environment configurations | | 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 @@ -316,7 +325,7 @@ export class CreateUsers implements SeedsInterface { public async seed(factory: FactoryInterface): Promise { await factory .get(User) - .create(10); + .createMany(10); } } @@ -330,7 +339,7 @@ export class CreateUsers implements SeedsInterface { public async seed(factory: FactoryInterface): Promise { await factory .get(User, 'admin') - .create(1); + .create(); } } @@ -344,7 +353,7 @@ await factory.get(User) .each(async (user: User) => { const pets: Pet[] = await factory.get(Pet) - .create(2); + .createMany(2); const petIds = pets.map((pet: Pet) => pet.Id); await user.pets().attach(petIds); @@ -377,7 +386,12 @@ npm start db.seed | [Helmet](https://helmetjs.github.io/) | Helmet helps you secure your Express apps by setting various HTTP headers. It’s not a silver bullet, but it can help! | | [Auth0 API Documentation](https://auth0.com/docs/api/management/v2) | Authentification service | | [Jest](http://facebook.github.io/jest/) | Delightful JavaScript Testing Library for unit and e2e tests | +| [supertest](https://github.com/visionmedia/supertest) | Super-agent driven library for testing node.js HTTP servers using a fluent API | +| [nock](https://github.com/node-nock/nock) | HTTP mocking and expectations library | | [swagger Documentation](http://swagger.io/) | API Tool to describe and document your api. | +| [SQLite Documentation](https://www.sitepoint.com/getting-started-sqlite3-basic-commands/) | Getting Started with SQLite3 – Basic Commands. | +| [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 diff --git a/src/lib/banner.ts b/commands/banner.ts similarity index 100% rename from src/lib/banner.ts rename to commands/banner.ts diff --git a/src/lib/ormconfig.ts b/commands/ormconfig.ts similarity index 72% rename from src/lib/ormconfig.ts rename to commands/ormconfig.ts index 667e64f4..e29d45ff 100644 --- a/src/lib/ormconfig.ts +++ b/commands/ormconfig.ts @@ -2,8 +2,9 @@ import * as dotenv from 'dotenv'; dotenv.config(); import * as path from 'path'; +import * as Chalk from 'chalk'; import * as jsonfile from 'jsonfile'; -import { env } from '../core/env'; +import { env } from '../src/core/env'; const content = { @@ -23,8 +24,13 @@ const content = { const filePath = path.join(process.cwd(), 'ormconfig.json'); jsonfile.writeFile(filePath, content, { spaces: 2 }, (err) => { if (err === null) { - console.log('Successfully generated ormconfig.json form the .env file'); + const chalk = Chalk.default; + console.log('👍 ', + chalk.gray.underline('generated:'), + chalk.blue.bold('ormconfig.json') + ); } else { console.error('Failed to generate the ormconfig.json', err); + process.exit(1); } }); diff --git a/commands/tsconfig.ts b/commands/tsconfig.ts new file mode 100644 index 00000000..d259b055 --- /dev/null +++ b/commands/tsconfig.ts @@ -0,0 +1,24 @@ +import * as path from 'path'; +import * as Chalk from 'chalk'; +import * as jsonfile from 'jsonfile'; +import * as tsconfig from '../tsconfig.json'; + + +const content: any = tsconfig; +content.include = [ + 'src/**/*', +]; + +const filePath = path.join(process.cwd(), 'tsconfig.build.json'); +jsonfile.writeFile(filePath, content, { spaces: 2 }, (err) => { + if (err === null) { + const chalk = Chalk.default; + console.log('👍 ', + chalk.gray.underline('generated:'), + chalk.blue.bold('tsconfig.build.json') + ); + } else { + console.error('Failed to generate the otsconfig.build.json', err); + process.exit(1); + } +}); diff --git a/package-scripts.js b/package-scripts.js index 749c6e23..4f30d19b 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -6,14 +6,13 @@ const { series, crossEnv, concurrent, rimraf, runInNewWindow } = require('nps-ut module.exports = { scripts: { - default: { - script: 'nps start' - }, + default: 'nps start', /** * Starts the builded app from the dist directory */ start: { - script: 'node dist/app.js' + script: 'node dist/app.js', + description: 'Starts the builded app from the dist directory' }, /** * Serves the current app and watches for changes to restart it @@ -22,17 +21,30 @@ module.exports = { script: series( 'nps banner.serve', 'nodemon --watch src --watch .env' - ) + ), + description: 'Serves the current app and watches for changes to restart it' }, /** - * Setup's the development environment and the database + * Setup's stuff */ setup: { + db: { + script: series( + 'nps db.migrate', + 'nps db.seed' + ), + description: 'Setup`s the database by migrating and seeding' + } + }, + /** + * Creates the needed configuration files + */ + config: { script: series( - 'yarn install', - 'nps db.migrate', - 'nps db.seed' - ) + runFast('./commands/tsconfig.ts'), + runFast('./commands/ormconfig.ts') + ), + hiddenFromHelp: true }, /** * Builds the app into the dist directory @@ -40,11 +52,69 @@ module.exports = { build: { script: series( 'nps banner.build', + 'nps config', 'nps lint', 'nps clean.dist', 'nps transpile', 'nps copy' - ) + ), + description: 'Builds the app into the dist directory' + }, + /** + * Runs TSLint over your project + */ + lint: { + script: tslint(`./src/**/*.ts`), + hiddenFromHelp: true + }, + /** + * Transpile your app into javascript + */ + transpile: { + script: `tsc --project ./tsconfig.build.json`, + hiddenFromHelp: true + }, + /** + * Clean files and folders + */ + clean: { + default: { + script: series( + `nps banner.clean`, + `nps clean.dist` + ), + description: 'Deletes the ./dist folder' + }, + dist: { + script: rimraf('./dist'), + hiddenFromHelp: true + } + }, + /** + * Copies static files to the build folder + */ + copy: { + default: { + script: series( + `nps copy.swagger`, + `nps copy.public` + ), + hiddenFromHelp: true + }, + swagger: { + script: copy( + './src/api/swagger.json', + './dist' + ), + hiddenFromHelp: true + }, + public: { + script: copy( + './src/public/*', + './dist' + ), + hiddenFromHelp: true + } }, /** * Database scripts @@ -53,30 +123,32 @@ module.exports = { migrate: { script: series( 'nps banner.migrate', - 'nps db.config', + 'nps config', runFast('./node_modules/typeorm/cli.js migrations:run') - ) + ), + description: 'Migrates the database to newest version available' }, revert: { script: series( 'nps banner.revert', - 'nps db.config', + 'nps config', runFast('./node_modules/typeorm/cli.js migrations:revert') - ) + ), + description: 'Downgrades the database' }, seed: { script: series( 'nps banner.seed', - 'nps db.config', - runFast('./src/lib/seeds/') - ) - }, - config: { - script: runFast('./src/lib/ormconfig.ts') + 'nps config', + runFast('./src/lib/seeds/cli.ts') + ), + description: 'Seeds generated records into the database' }, drop: { - script: runFast('./node_modules/typeorm/cli.js schema:drop') + script: runFast('./node_modules/typeorm/cli.js schema:drop'), + description: 'Drops the schema of the database' } + }, /** * These run various kinds of tests. Default is unit. @@ -86,93 +158,83 @@ module.exports = { unit: { default: { script: series( - 'nps banner.test', + 'nps banner.testUnit', 'nps test.unit.pretest', 'nps test.unit.run' - ) + ), + description: 'Runs the unit tests' + }, + pretest: { + script: tslint(`./test/unit/**.ts`), + hiddenFromHelp: true + }, + run: { + script: 'cross-env NODE_ENV=test jest --testPathPattern=unit', + hiddenFromHelp: true + }, + verbose: { + script: 'nps "test --verbose"', + hiddenFromHelp: true + }, + coverage: { + script: 'nps "test --coverage"', + hiddenFromHelp: true + } + }, + integration: { + default: { + script: series( + 'nps banner.testIntegration', + 'nps test.integration.pretest', + 'nps test.integration.run' + ), + description: 'Runs the integration tests' }, pretest: { - script: 'tslint -c ./tslint.json -t stylish ./test/unit/**/*.ts' + script: tslint(`./test/integration/**.ts`), + hiddenFromHelp: true }, run: { - script: 'cross-env NODE_ENV=test jest --testPathPattern=unit' + // -i. Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This can be useful for debugging. + script: 'cross-env NODE_ENV=test jest --testPathPattern=integration -i', + hiddenFromHelp: true }, verbose: { - script: 'nps "test --verbose"' + script: 'nps "test --verbose"', + hiddenFromHelp: true }, coverage: { - script: 'nps "test --coverage"' + script: 'nps "test --coverage"', + hiddenFromHelp: true } }, e2e: { default: { script: series( - 'nps banner.test', + 'nps banner.testE2E', 'nps test.e2e.pretest', - runInNewWindow(series('nps build', 'nps start')), 'nps test.e2e.run' - ) + ), + description: 'Runs the e2e tests' }, pretest: { - script: 'tslint -c ./tslint.json -t stylish ./test/e2e/**/*.ts' + script: tslint(`./test/e2e/**.ts`), + hiddenFromHelp: true + }, + run: { + // -i. Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This can be useful for debugging. + script: 'cross-env NODE_ENV=test jest --testPathPattern=e2e -i', + hiddenFromHelp: true }, verbose: { - script: 'nps "test.e2e --verbose"' + script: 'nps "test --verbose"', + hiddenFromHelp: true }, - run: series( - `wait-on --timeout 120000 http-get://localhost:3000/api/info`, - 'cross-env NODE_ENV=test jest --testPathPattern=e2e -i' - ) - } - }, - /** - * Runs TSLint over your project - */ - lint: { - script: `tslint -c ./tslint.json -p tsconfig.json src/**/*.ts --format stylish` - }, - /** - * Transpile your app into javascript - */ - transpile: { - script: `tsc` - }, - /** - * Clean files and folders - */ - clean: { - default: { - script: series( - `nps banner.clean`, - `nps clean.dist` - ) - }, - dist: { - script: rimraf('./dist') - } - }, - /** - * Copies static files to the build folder - */ - copy: { - default: { - script: series( - `nps copy.swagger`, - `nps copy.public` - ) - }, - swagger: { - script: copy( - './src/api/swagger.json', - './dist' - ) + coverage: { + script: 'nps "test --coverage"', + hiddenFromHelp: true + } }, - public: { - script: copy( - './src/public/*', - './dist' - ) - } }, /** * This creates pretty banner to the terminal @@ -180,7 +242,9 @@ module.exports = { banner: { build: banner('build'), serve: banner('serve'), - test: banner('test'), + testUnit: banner('test.unit'), + testIntegration: banner('test.integration'), + testE2E: banner('test.e2e'), migrate: banner('migrate'), seed: banner('seed'), revert: banner('revert'), @@ -193,9 +257,8 @@ function banner(name) { return { hiddenFromHelp: true, silent: true, - logLevel: 'error', description: `Shows ${name} banners to the console`, - script: runFast(`./src/lib/banner.ts ${name}`), + script: runFast(`./commands/banner.ts ${name}`), }; } @@ -210,3 +273,7 @@ function run(path) { function runFast(path) { return run(`-F ${path}`); } + +function tslint(path) { + return `tslint -c ./tslint.json ${path} --format stylish`; +} diff --git a/package.json b/package.json index 7d979a98..d29e0585 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,14 @@ { "name": "express-typescript-boilerplate", - "version": "3.0.0-beta.2", + "version": "3.0.0-rc.1", "description": "A delightful way to building a RESTful API with NodeJs & TypeScript", "main": "src/app.ts", "scripts": { "start": "nps", "test": "npm start test", - "build": "npm start build" + "build": "npm start build", + "presetup": "yarn install", + "setup": "npm start setup.db" }, "engines": { "node": ">=8.0.0" @@ -50,6 +52,7 @@ "@types/reflect-metadata": "0.0.5", "@types/request": "^2.0.8", "@types/serve-favicon": "^2.2.29", + "@types/supertest": "^2.0.4", "@types/uuid": "^3.4.3", "@types/winston": "^2.3.7", "body-parser": "^1.18.2", @@ -59,18 +62,21 @@ "compression": "^1.7.1", "copyfiles": "^1.2.0", "cors": "^2.8.4", + "dataloader": "^1.3.0", "dotenv": "^4.0.0", "event-dispatch": "^0.4.1", "express": "^4.16.2", "express-basic-auth": "^1.1.3", + "express-graphql": "^0.6.11", "express-status-monitor": "^1.0.1", "faker": "^4.1.0", "figlet": "^1.2.0", "glob": "^7.1.2", + "graphql": "^0.11.7", "helmet": "^3.9.0", "jsonfile": "^4.0.0", "lodash": "^4.17.4", - "microframework": "^0.6.4", + "microframework-w3tec": "^0.6.3", "morgan": "^1.9.0", "mysql": "^2.15.0", "nodemon": "^1.12.1", @@ -81,6 +87,7 @@ "request": "^2.83.0", "routing-controllers": "^0.7.6", "serve-favicon": "^2.4.5", + "supertest": "^3.0.0", "swagger-ui-express": "^2.0.10", "ts-node": "^3.3.0", "tslint": "^5.8.0", @@ -108,11 +115,13 @@ "license": "MIT", "devDependencies": { "@types/jest": "^21.1.5", + "@types/nock": "^8.2.1", "cross-env": "^5.1.1", "jest": "^21.2.1", "mock-express-request": "^0.2.0", "mock-express-response": "^0.2.1", - "nock": "^9.1.0", + "nock": "^9.1.4", + "sqlite3": "^3.1.13", "ts-jest": "^21.1.4" } } diff --git a/src/api/middlewares/CompressionMiddleware.ts b/src/api/middlewares/CompressionMiddleware.ts index c508e675..a487e207 100644 --- a/src/api/middlewares/CompressionMiddleware.ts +++ b/src/api/middlewares/CompressionMiddleware.ts @@ -5,7 +5,7 @@ import { ExpressMiddlewareInterface, Middleware } from 'routing-controllers'; @Middleware({ type: 'before' }) -export class SecurityMiddleware implements ExpressMiddlewareInterface { +export class CompressionMiddleware implements ExpressMiddlewareInterface { public use(req: express.Request, res: express.Response, next: express.NextFunction): any { return compression()(req, res, next); diff --git a/src/api/models/Pet.ts b/src/api/models/Pet.ts index 7556ff5d..948c50f6 100644 --- a/src/api/models/Pet.ts +++ b/src/api/models/Pet.ts @@ -1,4 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; +import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm'; import { IsNotEmpty } from 'class-validator'; import { User } from './User'; @@ -17,7 +17,14 @@ export class Pet { @Column() public age: number; + @Column({ + name: 'user_id', + nullable: true, + }) + public userId: number; + @ManyToOne(type => User, user => user.pets) + @JoinColumn({ name: 'user_id' }) public user: User; public toString(): string { diff --git a/src/api/mutations/CreatePetMutation.ts b/src/api/mutations/CreatePetMutation.ts new file mode 100644 index 00000000..54a5b326 --- /dev/null +++ b/src/api/mutations/CreatePetMutation.ts @@ -0,0 +1,41 @@ +import { + GraphQLFieldConfig, + GraphQLNonNull, + GraphQLString, + GraphQLInt +} from 'graphql'; +import { plainToClass } from 'class-transformer'; +import { AbstractGraphQLMutation } from '../../lib/graphql/AbstractGraphQLMutation'; +import { PetType } from '../types/PetType'; +import { PetService } from '../services/PetService'; +import { GraphQLContext, Mutation } from '../../lib/graphql'; +import { Pet } from '../models/Pet'; +import { Logger, LoggerInterface } from '../../decorators/Logger'; + + +interface CreatePetMutationArguments { + name: string; + age: number; +} + +@Mutation() +export class CreatePetMutation extends AbstractGraphQLMutation, Pet, CreatePetMutationArguments> implements GraphQLFieldConfig { + public type = PetType; + public args = { + name: { type: new GraphQLNonNull(GraphQLString) }, + age: { type: new GraphQLNonNull(GraphQLInt) }, + }; + + constructor( + private petService: PetService, + @Logger(__filename) private log: LoggerInterface + ) { + super(); + } + + public async run(root: any, args: CreatePetMutationArguments, context: GraphQLContext): Promise { + const pet = await this.petService.create(plainToClass(Pet, args)); + this.log.info('Successfully created a new pet'); + return pet; + } +} diff --git a/src/api/queries/GetPetsQuery.ts b/src/api/queries/GetPetsQuery.ts new file mode 100644 index 00000000..be013b4b --- /dev/null +++ b/src/api/queries/GetPetsQuery.ts @@ -0,0 +1,28 @@ +import { GraphQLFieldConfig, GraphQLList } from 'graphql'; +import { Query, AbstractGraphQLQuery, GraphQLContext } from './../../lib/graphql'; +import { PetService } from '../services/PetService'; +import { PetType } from './../types/PetType'; +import { Pet } from '../models/Pet'; +import { Logger, LoggerInterface } from '../../decorators/Logger'; + + +@Query() +export class GetPetsQuery extends AbstractGraphQLQuery, Pet[], any> implements GraphQLFieldConfig { + public type = new GraphQLList(PetType); + public allow = []; + public args = {}; + + constructor( + private petService: PetService, + @Logger(__filename) private log: LoggerInterface + ) { + super(); + } + + public async run(root: any, args: any, context: GraphQLContext): Promise { + const pets = await this.petService.find(); + this.log.info(`Found ${pets.length} pets`); + return pets; + } + +} diff --git a/src/api/queries/GetUsersQuery.ts b/src/api/queries/GetUsersQuery.ts new file mode 100644 index 00000000..02cde800 --- /dev/null +++ b/src/api/queries/GetUsersQuery.ts @@ -0,0 +1,28 @@ +import { GraphQLFieldConfig, GraphQLList } from 'graphql'; +import { Query, AbstractGraphQLQuery, GraphQLContext } from './../../lib/graphql'; +import { UserService } from '../services/UserService'; +import { UserType } from './../types/UserType'; +import { User } from '../models/User'; +import { Logger, LoggerInterface } from '../../decorators/Logger'; + + +@Query() +export class GetUsersQuery extends AbstractGraphQLQuery, User[], any> implements GraphQLFieldConfig { + public type = new GraphQLList(UserType); + public allow = []; + public args = {}; + + constructor( + private userService: UserService, + @Logger(__filename) private log: LoggerInterface + ) { + super(); + } + + public async run(root: any, args: any, context: GraphQLContext): Promise { + const users = await this.userService.find(); + this.log.info(`Found ${users.length} users`); + return users; + } + +} diff --git a/src/api/repositories/PetRepository.ts b/src/api/repositories/PetRepository.ts index 77e031ae..dca710f5 100644 --- a/src/api/repositories/PetRepository.ts +++ b/src/api/repositories/PetRepository.ts @@ -2,6 +2,16 @@ import { Repository, EntityRepository } from 'typeorm'; import { Pet } from '../models/Pet'; @EntityRepository(Pet) -export class PetRepository extends Repository { +export class PetRepository extends Repository { + + /** + * Find by user_id is used for our data-loader to get all needed pets in one query. + */ + public findByUserIds(ids: string[]): Promise { + return this.createQueryBuilder() + .select() + .where(`pet.user_id IN (${ids.map(id => `'${id}'`).join(', ')})`) + .getMany(); + } } diff --git a/src/api/services/PetService.ts b/src/api/services/PetService.ts index 9a063be9..c381975c 100644 --- a/src/api/services/PetService.ts +++ b/src/api/services/PetService.ts @@ -5,6 +5,7 @@ import { Pet } from '../models/Pet'; import { events } from '../subscribers/events'; import { EventDispatcher, EventDispatcherInterface } from '../../decorators/EventDispatcher'; import { Logger, LoggerInterface } from '../../decorators/Logger'; +import { User } from '../models/User'; @Service() @@ -21,6 +22,15 @@ export class PetService { return this.petRepository.find(); } + public findByUser(user: User): Promise { + this.log.info('Find all pets of the user', user.toString()); + return this.petRepository.find({ + where: { + userId: user.id, + }, + }); + } + public findOne(id: string): Promise { this.log.info('Find all pets'); return this.petRepository.findOne({ id }); diff --git a/src/api/types/PetType.ts b/src/api/types/PetType.ts new file mode 100644 index 00000000..ced8148e --- /dev/null +++ b/src/api/types/PetType.ts @@ -0,0 +1,44 @@ +import { + GraphQLID, + GraphQLString, + GraphQLInt, + GraphQLObjectType, + GraphQLFieldConfigMap, +} from 'graphql'; +import { OwnerType } from './UserType'; +import { Pet } from '../models/Pet'; +import { GraphQLContext } from '../../lib/graphql'; + +const PetFields: GraphQLFieldConfigMap = { + id: { + type: GraphQLID, + description: 'The ID', + }, + name: { + type: GraphQLString, + description: 'The name of the pet.', + }, + age: { + type: GraphQLInt, + description: 'The age of the pet in years.', + }, +}; + +export const PetOfUserType = new GraphQLObjectType({ + name: 'PetOfUser', + description: 'A users pet', + fields: () => ({ ...PetFields, ...{} }), +}); + +export const PetType = new GraphQLObjectType({ + name: 'Pet', + description: 'A single pet.', + fields: () => ({ ...PetFields, ...{ + owner: { + type: OwnerType, + description: 'The owner of the pet', + resolve: (pet: Pet, args: any, context: GraphQLContext) => + context.dataLoaders.users.load(pet.userId), + }, + } }), +}); diff --git a/src/api/types/UserType.ts b/src/api/types/UserType.ts new file mode 100644 index 00000000..45bccc4e --- /dev/null +++ b/src/api/types/UserType.ts @@ -0,0 +1,51 @@ +import { + GraphQLID, + GraphQLString, + GraphQLObjectType, + GraphQLFieldConfigMap, + GraphQLList, +} from 'graphql'; +import { GraphQLContext } from '../../lib/graphql'; +import { PetOfUserType } from './PetType'; +import { User } from '../models/User'; + +const UserFields: GraphQLFieldConfigMap = { + id: { + type: GraphQLID, + description: 'The ID', + }, + firstName: { + type: GraphQLString, + description: 'The first name of the user.', + }, + lastName: { + type: GraphQLString, + description: 'The last name of the user.', + }, + email: { + type: GraphQLString, + description: 'The email of this user.', + }, +}; + +export const UserType = new GraphQLObjectType({ + name: 'User', + description: 'A single user.', + fields: () => ({ ...UserFields, ...{ + pets: { + type: new GraphQLList(PetOfUserType), + 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]), + // This would be the case with a normal service, but not very fast + // context.container.get(PetService).findByUser(user), + }, + } }), +}); + +export const OwnerType = new GraphQLObjectType({ + name: 'Owner', + description: 'The owner of a pet', + fields: () => ({ ...UserFields, ...{} }), +}); diff --git a/src/app.ts b/src/app.ts index 86846c6d..8a1b9b79 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,7 @@ import { banner } from './core/banner'; import { Logger } from './core/Logger'; const log = new Logger(__filename); -import { bootstrapMicroframework } from 'microframework'; +import { bootstrapMicroframework } from 'microframework-w3tec'; import { expressLoader } from './loaders/expressLoader'; import { winstonLoader } from './loaders/winstonLoader'; import { typeormLoader } from './loaders/typeormLoader'; @@ -20,13 +20,14 @@ import { monitorLoader } from './loaders/monitorLoader'; import { homeLoader } from './loaders/homeLoader'; import { publicLoader } from './loaders/publicLoader'; import { iocLoader } from './loaders/iocLoader'; +import { graphqlLoader } from './loaders/graphqlLoader'; import { eventDispatchLoader } from './loaders/eventDispatchLoader'; bootstrapMicroframework({ /** * Loader is a place where you can configure all your modules during microframework - * bootstrap. All loaders are executed one by one in a sequential order. + * bootstrap process. All loaders are executed one by one in a sequential order. */ loaders: [ winstonLoader, @@ -38,6 +39,7 @@ bootstrapMicroframework({ monitorLoader, homeLoader, publicLoader, + graphqlLoader, ], }) .then(() => banner(log)) diff --git a/src/core/banner.ts b/src/core/banner.ts index ef494f10..a336cc3e 100644 --- a/src/core/banner.ts +++ b/src/core/banner.ts @@ -13,6 +13,9 @@ export function banner(log: Logger): void { log.info(`Version : ${env.app.version}`); log.info(``); log.info(`API Info : ${env.app.route}${env.app.routePrefix}`); + if (env.graphql.enabled) { + log.info(`GraphQL : ${env.app.route}${env.graphql.route}`); + } if (env.swagger.enabled) { log.info(`Swagger : ${env.app.route}${env.swagger.route}`); } diff --git a/src/core/env.ts b/src/core/env.ts index 73a6f444..3bf68160 100644 --- a/src/core/env.ts +++ b/src/core/env.ts @@ -13,6 +13,8 @@ dotenv.config({ path: path.join(process.cwd(), `.env${((process.env.NODE_ENV === export const env = { node: process.env.NODE_ENV || 'development', isProduction: process.env.NODE_ENV === 'production', + isTest: process.env.NODE_ENV === 'test', + isDevelopment: process.env.NODE_ENV === 'development', app: { name: getOsEnv('APP_NAME'), version: (pkg as any).version, @@ -29,6 +31,8 @@ export const env = { controllers: [path.join(__dirname, '..', 'api/**/*Controller{.js,.ts}')], middlewares: [path.join(__dirname, '..', 'api/**/*Middleware{.js,.ts}')], interceptors: [path.join(__dirname, '..', 'api/**/*Interceptor{.js,.ts}')], + queries: [path.join(__dirname, '..', 'api/**/*Query{.js,.ts}')], + mutations: [path.join(__dirname, '..', 'api/**/*Mutation{.js,.ts}')], }, }, log: { @@ -49,6 +53,11 @@ export const env = { synchronize: toBool(getOsEnv('DB_SYNCHRONIZE')), logging: toBool(getOsEnv('DB_LOGGING')), }, + graphql: { + enabled: toBool(getOsEnv('GRAPHQL_ENABLED')), + route: getOsEnv('GRAPHQL_ROUTE'), + editor: toBool(getOsEnv('GRAPHQL_EDITOR')), + }, swagger: { enabled: toBool(getOsEnv('SWAGGER_ENABLED')), route: getOsEnv('SWAGGER_ROUTE'), diff --git a/src/database/migrations/1511105183653-CreateUserTable.ts b/src/database/migrations/1511105183653-CreateUserTable.ts index e79b1e1d..fee0ac03 100644 --- a/src/database/migrations/1511105183653-CreateUserTable.ts +++ b/src/database/migrations/1511105183653-CreateUserTable.ts @@ -1,19 +1,40 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class CreateUserTable1511105183653 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE \`user\` ( - \`id\` varchar(255) NOT NULL PRIMARY KEY, - \`first_name\` varchar(255) NOT NULL, - \`last_name\` varchar(255) NOT NULL, - \`email\` varchar(255) NOT NULL) ENGINE=InnoDB;` - ); + const table = new Table('user', [ + { + name: 'id', + type: 'varchar', + length: 255, + isPrimary: true, + isNullable: false, + }, { + name: 'first_name', + type: 'varchar', + length: 255, + isPrimary: false, + isNullable: false, + }, { + name: 'last_name', + type: 'varchar', + length: 255, + isPrimary: false, + isNullable: false, + }, { + name: 'email', + type: 'varchar', + length: 255, + isPrimary: false, + isNullable: false, + }, + ]); + await queryRunner.createTable(table); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE \`user\`;`); + await queryRunner.dropTable('user'); } } diff --git a/src/database/migrations/1512663524808-CreatePetTable.ts b/src/database/migrations/1512663524808-CreatePetTable.ts index 479459b4..55f68e74 100644 --- a/src/database/migrations/1512663524808-CreatePetTable.ts +++ b/src/database/migrations/1512663524808-CreatePetTable.ts @@ -1,19 +1,40 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; export class CreatePetTable1512663524808 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - CREATE TABLE \`pet\` ( - \`id\` varchar(255) NOT NULL PRIMARY KEY, - \`name\` varchar(255) NOT NULL, - \`age\` int(11) NOT NULL, - \`userId\` varchar(255)) ENGINE=InnoDB;` - ); + const table = new Table('pet', [ + { + name: 'id', + type: 'varchar', + length: 255, + isPrimary: true, + isNullable: false, + }, { + name: 'name', + type: 'varchar', + length: 255, + isPrimary: false, + isNullable: false, + }, { + name: 'age', + type: 'int', + length: 11, + isPrimary: false, + isNullable: false, + }, { + name: 'user_id', + type: 'varchar', + length: 255, + isPrimary: false, + isNullable: true, + }, + ]); + await queryRunner.createTable(table); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP TABLE \`pet\`;`); + await queryRunner.dropTable('pet'); } } diff --git a/src/database/migrations/1512663990063-AddUserRelationToPetTable.ts b/src/database/migrations/1512663990063-AddUserRelationToPetTable.ts index 3ee7be34..d83da0d0 100644 --- a/src/database/migrations/1512663990063-AddUserRelationToPetTable.ts +++ b/src/database/migrations/1512663990063-AddUserRelationToPetTable.ts @@ -1,21 +1,21 @@ -import { MigrationInterface, QueryRunner } from 'typeorm'; +import { MigrationInterface, QueryRunner, TableForeignKey } from 'typeorm'; export class AddUserRelationToPetTable1512663990063 implements MigrationInterface { + private tableForeignKey = new TableForeignKey( + 'fk_user_pet', + ['user_id'], + ['id'], + 'user', + '' + ); + public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE \`pet\` - ADD CONSTRAINT \`fk_user_pet\` - FOREIGN KEY (\`userId\`) - REFERENCES \`user\`(\`id\`);` - ); + await queryRunner.createForeignKey('pet', this.tableForeignKey); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(` - ALTER TABLE \`pet\` - DROP FOREIGN KEY \`fk_user_pet\`;` - ); + await queryRunner.dropForeignKey('pet', this.tableForeignKey); } } diff --git a/src/database/seeds/CreateBruce.ts b/src/database/seeds/CreateBruce.ts new file mode 100644 index 00000000..3b9b58df --- /dev/null +++ b/src/database/seeds/CreateBruce.ts @@ -0,0 +1,18 @@ +import { SeedsInterface, FactoryInterface } from '../../lib/seeds'; +import { User } from '../../../src/api/models/User'; + + +export class CreateBruce implements SeedsInterface { + + public async seed(factory: FactoryInterface): Promise { + const connection = await factory.getConnection(); + const em = connection.createEntityManager(); + + const user = new User(); + user.firstName = 'Bruce'; + user.lastName = 'Wayne'; + user.email = 'bruce.wayne@wayne-enterprises.com'; + return await em.save(user); + } + +} diff --git a/src/database/seeds/0001-CreatePets.ts b/src/database/seeds/CreatePets.ts similarity index 100% rename from src/database/seeds/0001-CreatePets.ts rename to src/database/seeds/CreatePets.ts diff --git a/src/database/seeds/0000-CreateUsers.ts b/src/database/seeds/CreateUsers.ts similarity index 91% rename from src/database/seeds/0000-CreateUsers.ts rename to src/database/seeds/CreateUsers.ts index 4d314298..e73c637d 100644 --- a/src/database/seeds/0000-CreateUsers.ts +++ b/src/database/seeds/CreateUsers.ts @@ -7,7 +7,7 @@ export class CreateUsers implements SeedsInterface { public async seed(factory: FactoryInterface): Promise { await factory .get(User) - .create(); + .createMany(10); } } diff --git a/src/lib/graphql/AbstractGraphQLHooks.ts b/src/lib/graphql/AbstractGraphQLHooks.ts new file mode 100644 index 00000000..915cc12a --- /dev/null +++ b/src/lib/graphql/AbstractGraphQLHooks.ts @@ -0,0 +1,29 @@ +import { UserError } from './graphql-error-handling'; + +export abstract class AbstractGraphQLHooks { + + /** + * This is our before hook. Here we are able + * to alter the args object before the actual resolver(execute) + * will be called. + */ + public before(context: TContext, args: TArgs, source?: S): Promise | TArgs { + return args; + } + + /** + * This is our after hook. It will be called ater the actual resolver(execute). + * There you are able to alter the result before it is send to the client. + */ + public after(result: TResult, context: TContext, args?: TArgs, source?: S): Promise | TResult { + return result; + } + + /** + * This is our resolver, which should gather the needed data; + */ + public run(rootOrSource: S, args: TArgs, context: TContext): Promise | TResult { + throw new UserError('Query not implemented!'); + } + +} diff --git a/src/lib/graphql/AbstractGraphQLMutation.ts b/src/lib/graphql/AbstractGraphQLMutation.ts new file mode 100644 index 00000000..2f8564c1 --- /dev/null +++ b/src/lib/graphql/AbstractGraphQLMutation.ts @@ -0,0 +1,5 @@ +import { AbstractGraphQLQuery } from './AbstractGraphQLQuery'; + + +export abstract class AbstractGraphQLMutation extends AbstractGraphQLQuery { +} diff --git a/src/lib/graphql/AbstractGraphQLQuery.ts b/src/lib/graphql/AbstractGraphQLQuery.ts new file mode 100644 index 00000000..1a34e77b --- /dev/null +++ b/src/lib/graphql/AbstractGraphQLQuery.ts @@ -0,0 +1,20 @@ +import { AbstractGraphQLHooks } from './AbstractGraphQLHooks'; + + +export abstract class AbstractGraphQLQuery extends AbstractGraphQLHooks { + + /** + * This will be called by graphQL and they need it as a property function. + * We use this hook to add some more logic to it, + * like permission checking, before- and after hooks to alter some data. + */ + public resolve = async (root: S, args: TArgs, context: TContext): Promise => { + // We need to store the query arguments in the context so they can be accessed by subsequent resolvers + (context as any).resolveArgs = args; + args = await this.before(context, args); + let result = await this.run(root, args, context); + result = await this.after(result, context, args); + return result as TResult; + } + +} diff --git a/src/lib/graphql/GraphQLContext.ts b/src/lib/graphql/GraphQLContext.ts new file mode 100644 index 00000000..e8bb0250 --- /dev/null +++ b/src/lib/graphql/GraphQLContext.ts @@ -0,0 +1,16 @@ +import * as express from 'express'; +import * as DataLoader from 'dataloader'; +import { Container } from 'typedi'; + +export interface GraphQLContext { + container: typeof Container; + request: express.Request; + response: express.Response; + dataLoaders: GraphQLContextDataLoader; + resolveArgs?: TResolveArgs; + data?: TData; +} + +export interface GraphQLContextDataLoader { + [key: string]: DataLoader; +} diff --git a/src/lib/graphql/MetadataArgsStorage.ts b/src/lib/graphql/MetadataArgsStorage.ts new file mode 100644 index 00000000..e7862a7d --- /dev/null +++ b/src/lib/graphql/MetadataArgsStorage.ts @@ -0,0 +1,52 @@ +import { QueryMetadataArgs } from './QueryMetadataArgs'; +import { MutationMetadataArgs } from './MutationMetadataArgs'; + +/** + * Storage all metadatas read from decorators. + */ +export class MetadataArgsStorage { + + // ------------------------------------------------------------------------- + // Properties + // ------------------------------------------------------------------------- + + /** + * Registered controller metadata args. + */ + public queries: QueryMetadataArgs[] = []; + + /** + * Registered middleware metadata args. + */ + public mutations: MutationMetadataArgs[] = []; + + + // ------------------------------------------------------------------------- + // Public Methods + // ------------------------------------------------------------------------- + /** + * Filters registered queries by a given classes. + */ + public filterQueryMetadatasForClasses(classes: Array<() => void>): MutationMetadataArgs[] { + return this.queries.filter(ctrl => { + return classes.filter(cls => ctrl.target === cls).length > 0; + }); + } + /** + * Filters registered mutations by a given classes. + */ + public filterMutationMetadatasForClasses(classes: Array<() => void>): MutationMetadataArgs[] { + return this.mutations.filter(ctrl => { + return classes.filter(cls => ctrl.target === cls).length > 0; + }); + } + + /** + * Removes all saved metadata. + */ + public reset(): void { + this.queries = []; + this.mutations = []; + } + +} diff --git a/src/lib/graphql/Mutation.ts b/src/lib/graphql/Mutation.ts new file mode 100644 index 00000000..577231e9 --- /dev/null +++ b/src/lib/graphql/Mutation.ts @@ -0,0 +1,11 @@ +import { getMetadataArgsStorage } from './index'; + + +export function Mutation(): any { + return (object: () => void) => { + getMetadataArgsStorage().mutations.push({ + name: object.name, + target: object, + }); + }; +} diff --git a/src/lib/graphql/MutationMetadataArgs.ts b/src/lib/graphql/MutationMetadataArgs.ts new file mode 100644 index 00000000..23c1602b --- /dev/null +++ b/src/lib/graphql/MutationMetadataArgs.ts @@ -0,0 +1,7 @@ +export interface MutationMetadataArgs { + name: string; + /** + * Indicates object which is used by this controller. + */ + target: () => void; +} diff --git a/src/lib/graphql/Query.ts b/src/lib/graphql/Query.ts new file mode 100644 index 00000000..102411c2 --- /dev/null +++ b/src/lib/graphql/Query.ts @@ -0,0 +1,11 @@ +import { getMetadataArgsStorage } from './index'; + + +export function Query(): any { + return (object: () => void) => { + getMetadataArgsStorage().queries.push({ + name: object.name, + target: object, + }); + }; +} diff --git a/src/lib/graphql/QueryMetadataArgs.ts b/src/lib/graphql/QueryMetadataArgs.ts new file mode 100644 index 00000000..8e0afaf8 --- /dev/null +++ b/src/lib/graphql/QueryMetadataArgs.ts @@ -0,0 +1,7 @@ +export interface QueryMetadataArgs { + name: string; + /** + * Indicates object which is used by this controller. + */ + target: () => void; +} diff --git a/src/lib/graphql/container.ts b/src/lib/graphql/container.ts new file mode 100644 index 00000000..47b4c225 --- /dev/null +++ b/src/lib/graphql/container.ts @@ -0,0 +1,71 @@ + +/** + * Container options. + */ +export interface UseContainerOptions { + + /** + * If set to true, then default container will be used in the case if given container haven't returned anything. + */ + fallback?: boolean; + + /** + * If set to true, then default container will be used in the case if given container thrown an exception. + */ + fallbackOnErrors?: boolean; + +} + +/** + * Container to be used by this library for inversion control. If container was not implicitly set then by default + * container simply creates a new instance of the given class. + */ +const defaultContainer: { get(someClass: { new(...args: any[]): T } | (() => void)): T } = new (class { + private instances: Array<{ type: any, object: any }> = []; + public get(someClass: { new(...args: any[]): T }): T { + let instance = this.instances.find(i => i.type === someClass); + if (!instance) { + instance = { type: someClass, object: new someClass() }; + this.instances.push(instance); + } + + return instance.object; + } +})(); + +let userContainer: { get(someClass: { new(...args: any[]): T } | (() => void)): T }; +let userContainerOptions: UseContainerOptions; + +/** + * Sets container to be used by this library. + */ +export function useContainer(iocContainer: { get(someClass: any): any }, options?: UseContainerOptions): void { + userContainer = iocContainer; + if (options) { + userContainerOptions = options; + } +} + +/** + * Gets the IOC container used by this library. + */ +export function getFromContainer(someClass: { new(...args: any[]): T } | (() => void)): T { + if (userContainer) { + try { + const instance = userContainer.get(someClass); + if (instance) { + return instance; + } + + if (!userContainerOptions || !userContainerOptions.fallback) { + return instance; + } + + } catch (error) { + if (!userContainerOptions || !userContainerOptions.fallbackOnErrors) { + throw error; + } + } + } + return defaultContainer.get(someClass); +} diff --git a/src/lib/graphql/dataloader.ts b/src/lib/graphql/dataloader.ts new file mode 100644 index 00000000..7482ebd0 --- /dev/null +++ b/src/lib/graphql/dataloader.ts @@ -0,0 +1,21 @@ +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/graphql-error-handling.ts b/src/lib/graphql/graphql-error-handling.ts new file mode 100644 index 00000000..a8d2edff --- /dev/null +++ b/src/lib/graphql/graphql-error-handling.ts @@ -0,0 +1,125 @@ +// This feature is a copy from https://github.com/kadirahq/graphql-errors +import * as uuid from 'uuid'; +import { GraphQLObjectType, GraphQLSchema } from 'graphql'; + +import { env } from '../../core/env'; +import { Logger } from '../../core/Logger'; +const logger = new Logger('app:errors'); + + +// Mark field/type/schema +export const Processed = Symbol(); + +// Used to identify UserErrors +export const IsUserError = Symbol(); + +// UserErrors will be sent to the user +export class UserError extends Error { + constructor(...args: any[]) { + super(args[0]); + this.name = 'Error'; + this.message = args[0]; + this[IsUserError] = true; + Error.captureStackTrace(this); + } +} + +// Modifies errors before sending to the user +export let defaultHandler = (err?) => { + if (err[IsUserError]) { + return err; + } + const errId = uuid.v4(); + err.message = `${err.message}: ${errId}`; + if (!env.isTest) { + console.error(err && err.stack || err); + } + if (env.isProduction) { + logger.error(err); + } + err.message = `500: Internal Error: ${errId}`; + return err; +}; + +const maskField = (field, fn) => { + const resolveFn = field.resolve; + if (field[Processed] || !resolveFn) { + return; + } + + field[Processed] = true; + field.resolve = async (...args) => { + try { + const out = resolveFn.call(undefined, ...args); + return await Promise.resolve(out); + } catch (e) { + throw fn(e); + } + }; + + // save the original resolve function + field.resolve._resolveFn = resolveFn; +}; + +const maskType = (type, fn) => { + if (type[Processed] || !type.getFields) { + return; + } + + const fields = type.getFields(); + for (const fieldName in fields) { + if (!Object.hasOwnProperty.call(fields, fieldName)) { + continue; + } + maskField(fields[fieldName], fn); + } +}; + +const maskSchema = (schema, fn) => { + const types = schema.getTypeMap(); + for (const typeName in types) { + if (!Object.hasOwnProperty.call(types, typeName)) { + continue; + } + maskType(types[typeName], fn); + } +}; + +// Changes the default error handler function +export const setDefaultHandler = (handlerFn) => { + defaultHandler = handlerFn; +}; + +// Masks graphql schemas, types or individual fields +export const handlingErrors = (thing, fn = defaultHandler) => { + if (thing instanceof GraphQLSchema) { + maskSchema(thing, fn); + } else if (thing instanceof GraphQLObjectType) { + maskType(thing, fn); + } else { + maskField(thing, fn); + } +}; + +export const getErrorCode = (message: string): string => { + if (hasErrorCode(message)) { + return message.substring(0, 3); + } + return '500'; // unkown error code +}; + +export const getErrorMessage = (message: string): string => { + if (hasErrorCode(message)) { + return message.substring(5); + } + return message; +}; + +export const hasErrorCode = (error: any): boolean => { + let message = error; + if (error.message) { + message = error.message; + } + const reg = new RegExp('^[0-9]{3}: '); + return reg.test(message); +}; diff --git a/src/lib/graphql/importClassesFromDirectories.ts b/src/lib/graphql/importClassesFromDirectories.ts new file mode 100644 index 00000000..299194ef --- /dev/null +++ b/src/lib/graphql/importClassesFromDirectories.ts @@ -0,0 +1,34 @@ +import * as path from 'path'; + +/** + * Loads all exported classes from the given directory. + */ +export function importClassesFromDirectories(directories: string[], formats: string[] = ['.js', '.ts']): Array<() => void> { + + const loadFileClasses = (exported: any, allLoaded: Array<() => void>) => { + if (exported instanceof Function) { + allLoaded.push(exported); + } else if (exported instanceof Array) { + exported.forEach((i: any) => loadFileClasses(i, allLoaded)); + } else if (exported instanceof Object || typeof exported === 'object') { + Object.keys(exported).forEach(key => loadFileClasses(exported[key], allLoaded)); + } + + return allLoaded; + }; + + const allFiles = directories.reduce((allDirs, dir) => { + return allDirs.concat(require('glob').sync(path.normalize(dir))); + }, [] as string[]); + + const dirs = allFiles + .filter(file => { + const dtsExtension = file.substring(file.length - 5, file.length); + return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== '.d.ts'; + }) + .map(file => { + return require(file); + }); + + return loadFileClasses(dirs, []); +} diff --git a/src/lib/graphql/index.ts b/src/lib/graphql/index.ts new file mode 100644 index 00000000..87e2d96e --- /dev/null +++ b/src/lib/graphql/index.ts @@ -0,0 +1,212 @@ +import * as express from 'express'; +import * as GraphQLHTTP from 'express-graphql'; +import * as DataLoader from 'dataloader'; +import { GraphQLSchema, GraphQLObjectType } from 'graphql'; +import { Container as container, ObjectType } from 'typedi'; +import { Repository, getCustomRepository, getRepository } from 'typeorm'; + +import { GraphQLContext, GraphQLContextDataLoader } from './GraphQLContext'; +import { MetadataArgsStorage } from './MetadataArgsStorage'; +import { importClassesFromDirectories } from './importClassesFromDirectories'; +import { handlingErrors, getErrorCode, getErrorMessage } from './graphql-error-handling'; +import { ensureInputOrder } from './dataloader'; +import { getFromContainer } from './container'; + +// ------------------------------------------------------------------------- +// Main exports +// ------------------------------------------------------------------------- + +export * from './Query'; +export * from './Mutation'; + +export * from './AbstractGraphQLHooks'; +export * from './AbstractGraphQLQuery'; +export * from './GraphQLContext'; +export * from './graphql-error-handling'; +export * from './container'; + +// ------------------------------------------------------------------------- +// Main Functions +// ------------------------------------------------------------------------- + +/** + * Creates a new dataloader with the typorm repository + */ +export function createDataLoader(obj: ObjectType, method?: string, key?: string): DataLoader { + let repository; + try { + repository = getCustomRepository>(obj); + } catch (errorRepo) { + try { + repository = getRepository(obj); + } catch (errorModel) { + throw new Error('Could not create a dataloader, because obj is nether model or repository!'); + } + } + + return new DataLoader(async (ids: number[]) => { + let items = []; + if (method) { + items = await repository[method](ids); + } else { + items = await repository.findByIds(ids); + } + + return ensureInputOrder(ids, items, key || 'id'); + }); +} + +/** + * Defines the options to create a GraphQLServer + */ +export interface GraphQLServerOptions { + queries: string[]; + mutations: string[]; + route?: string; + dataLoaders?: GraphQLContextDataLoader; + editorEnabled?: boolean; + contextData?: TData; +} + +/** + * Create GraphQL Server and bind it to the gieven express app + */ +export function createGraphQLServer(expressApp: express.Application, options: GraphQLServerOptions): void { + // collect queries & mutaions for our graphql schema + const schema = createSchema({ + queries: options.queries, + mutations: options.mutations, + }); + + // Handles internal errors and prints the stack to the console + handlingErrors(schema); + + // Add graphql layer to the express app + expressApp.use(options.route || '/graphql', (request: express.Request, response: express.Response) => { + + // Build GraphQLContext + const context: GraphQLContext = { + container, + request, + response, + dataLoaders: options.dataLoaders || {}, + resolveArgs: {}, + data: options.contextData, + }; + + // Setup GraphQL Server + GraphQLHTTP({ + schema, + context, + graphiql: options.editorEnabled || true, + formatError: error => ({ + code: getErrorCode(error.message), + message: getErrorMessage(error.message), + path: error.path, + }), + })(request, response); + }); +} + +/** + * Gets metadata args storage. + * Metadata args storage follows the best practices and stores metadata in a global variable. + */ +export function getMetadataArgsStorage(): MetadataArgsStorage { + if (!(global as any).graphqlMetadataArgsStorage) { + (global as any).graphqlMetadataArgsStorage = new MetadataArgsStorage(); + } + + return (global as any).graphqlMetadataArgsStorage; +} + +/** + * Create query name out of the class name + */ +export function createQueryName(name: string): string { + return lowercaseFirstLetter(removeSuffix(name, 'Query')); +} + +/** + * Create mutation name out of the class name + */ +export function createMutationName(name: string): string { + return lowercaseFirstLetter(removeSuffix(name, 'Mutation')); +} + +/** + * Removes the suffix + */ +export function removeSuffix(value: string, suffix: string): string { + return value.slice(0, value.length - suffix.length); +} + +/** + * LowerCase first letter + */ +export function lowercaseFirstLetter(s: string): string { + return s.charAt(0).toLowerCase() + s.slice(1); +} + +/** + * GraphQL schema options for building it + */ +export interface GraphQLSchemaOptions { + queries: string[]; + mutations: string[]; +} + +/** + * Create schema out of the @Query and @Mutation + */ +export function createSchema(options: GraphQLSchemaOptions): GraphQLSchema { + + // import all queries + let queryClasses: Array<() => void> = []; + if (options && options.queries && options.queries.length) { + queryClasses = (options.queries as any[]).filter(query => query instanceof Function); + const queryDirs = (options.queries as any[]).filter(query => typeof query === 'string'); + queryClasses.push(...importClassesFromDirectories(queryDirs)); + } + + const queries = {}; + getMetadataArgsStorage().queries.forEach(queryMetdadata => { + queries[createQueryName(queryMetdadata.name)] = getFromContainer(queryMetdadata.target); + }); + + const RootQuery = new GraphQLObjectType({ + name: 'Query', + fields: queries, + }); + + // import all mutations + let mutationClasses: Array<() => void> = []; + if (options && options.mutations && options.mutations.length) { + mutationClasses = (options.mutations as any[]).filter(mutation => mutation instanceof Function); + const mutationDirs = (options.mutations as any[]).filter(mutation => typeof mutation === 'string'); + mutationClasses.push(...importClassesFromDirectories(mutationDirs)); + } + + const mutations = {}; + getMetadataArgsStorage().mutations.forEach(mutationMetdadata => { + mutations[createMutationName(mutationMetdadata.name)] = getFromContainer(mutationMetdadata.target); + }); + + const RootMutation: GraphQLObjectType = new GraphQLObjectType({ + name: 'Mutation', + fields: mutations, + }); + + const schemaOptions: any = {}; + + if (queryClasses && queryClasses.length) { + schemaOptions.query = RootQuery; + } + + if (mutationClasses && mutationClasses.length) { + schemaOptions.mutation = RootMutation; + } + + return new GraphQLSchema(schemaOptions); + +} diff --git a/src/lib/seeds/EntityFactory.ts b/src/lib/seeds/EntityFactory.ts index 839842bf..fc82131c 100644 --- a/src/lib/seeds/EntityFactory.ts +++ b/src/lib/seeds/EntityFactory.ts @@ -24,6 +24,17 @@ export class EntityFactory implements EntityFactoryInterface { 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') { diff --git a/src/lib/seeds/EntityFactoryInterface.ts b/src/lib/seeds/EntityFactoryInterface.ts index 10fbaf92..95547df2 100644 --- a/src/lib/seeds/EntityFactoryInterface.ts +++ b/src/lib/seeds/EntityFactoryInterface.ts @@ -10,14 +10,12 @@ 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; - /** - * Creates an amount (default 1) of the defined entity. - */ - createMany(amount: number): 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. diff --git a/src/lib/seeds/Factory.ts b/src/lib/seeds/Factory.ts index 76155ee6..23357d25 100644 --- a/src/lib/seeds/Factory.ts +++ b/src/lib/seeds/Factory.ts @@ -4,6 +4,7 @@ import { Connection } from 'typeorm/connection/Connection'; import { FactoryInterface } from './FactoryInterface'; import { EntityFactory } from './EntityFactory'; import { BluePrint } from './BluePrint'; +import { SeedsConstructorInterface } from './SeedsInterface'; export class Factory implements FactoryInterface { @@ -32,6 +33,11 @@ export class Factory implements FactoryInterface { 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); } diff --git a/src/lib/seeds/FactoryInterface.ts b/src/lib/seeds/FactoryInterface.ts index 5f25bc39..580f2804 100644 --- a/src/lib/seeds/FactoryInterface.ts +++ b/src/lib/seeds/FactoryInterface.ts @@ -2,6 +2,7 @@ import * as Faker from 'faker'; import { ObjectType } from 'typeorm'; import { EntityFactoryInterface } from './EntityFactoryInterface'; import { Connection } from 'typeorm/connection/Connection'; +import { SeedsConstructorInterface } from 'src/lib/seeds'; /** * This interface is used to define new entity faker factories or to get such a * entity faker factory to start seeding. @@ -15,6 +16,10 @@ export interface FactoryInterface { * Sets the typeorm database connection. */ setConnection(connection: Connection): void; + /** + * Runs the given seed class + */ + runSeed(seedClass: SeedsConstructorInterface): Promise; /** * Returns an EntityFactoryInterface */ diff --git a/src/lib/seeds/SeedsInterface.ts b/src/lib/seeds/SeedsInterface.ts index 81bf2397..b27667e4 100644 --- a/src/lib/seeds/SeedsInterface.ts +++ b/src/lib/seeds/SeedsInterface.ts @@ -1,4 +1,4 @@ -import { FactoryInterface } from './FactoryInterface'; +import { Factory } from './Factory'; /** * Seeds should implement this interface and all its methods. */ @@ -6,5 +6,9 @@ export interface SeedsInterface { /** * Seed data into the databas. */ - seed(factory: FactoryInterface): Promise; + seed(factory: Factory): Promise; +} + +export interface SeedsConstructorInterface { + new(): SeedsInterface; } diff --git a/src/lib/seeds/cli.ts b/src/lib/seeds/cli.ts new file mode 100644 index 00000000..a6511d96 --- /dev/null +++ b/src/lib/seeds/cli.ts @@ -0,0 +1,87 @@ +import 'reflect-metadata'; +import * as path from 'path'; +import * as glob from 'glob'; +import * as commander from 'commander'; +import * as Chalk from 'chalk'; +import { Connection } from 'typeorm'; +import { Factory } from './Factory'; +import { getConnection } from './connection'; + + +// 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 index 7336ebd3..de75f430 100644 --- a/src/lib/seeds/index.ts +++ b/src/lib/seeds/index.ts @@ -1,93 +1,23 @@ -import 'reflect-metadata'; -import * as path from 'path'; -import * as glob from 'glob'; -import * as commander from 'commander'; -import * as Chalk from 'chalk'; import { Connection } from 'typeorm'; +import 'reflect-metadata'; import { Factory } from './Factory'; -import { getConnection } from './connection'; - - -// 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); - }); - }); -}); +// ------------------------------------------------------------------------- +// 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/loaders/eventDispatchLoader.ts b/src/loaders/eventDispatchLoader.ts index 954e14a2..c1cc7756 100644 --- a/src/loaders/eventDispatchLoader.ts +++ b/src/loaders/eventDispatchLoader.ts @@ -1,5 +1,5 @@ import * as glob from 'glob'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import { env } from '../core/env'; diff --git a/src/loaders/expressLoader.ts b/src/loaders/expressLoader.ts index 9c53d3ca..309deda6 100644 --- a/src/loaders/expressLoader.ts +++ b/src/loaders/expressLoader.ts @@ -1,5 +1,6 @@ +import { Application } from 'express'; import { createExpressServer } from 'routing-controllers'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import { env } from '../core/env'; import { authorizationChecker } from '../auth/authorizationChecker'; import { currentUserChecker } from '../auth/currentUserChecker'; @@ -13,7 +14,7 @@ export const expressLoader: MicroframeworkLoader = (settings: MicroframeworkSett * We create a new express server instance. * We could have also use useExpressServer here to attach controllers to an existing express instance. */ - const expressApp = createExpressServer({ + const expressApp: Application = createExpressServer({ cors: true, classTransformer: true, routePrefix: env.app.routePrefix, @@ -34,7 +35,10 @@ export const expressLoader: MicroframeworkLoader = (settings: MicroframeworkSett }); // Run application to listen on given port - expressApp.listen(env.app.port); + if (!env.isTest) { + const server = expressApp.listen(env.app.port); + settings.setData('express_server', server); + } // Here we can set the data for other loaders settings.setData('express_app', expressApp); diff --git a/src/loaders/graphqlLoader.ts b/src/loaders/graphqlLoader.ts new file mode 100644 index 00000000..c3350d99 --- /dev/null +++ b/src/loaders/graphqlLoader.ts @@ -0,0 +1,26 @@ +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; +import { createGraphQLServer, createDataLoader } from '../lib/graphql'; +import { env } from '../core/env'; +import { PetRepository } from './../api/repositories/PetRepository'; +import { Pet } from './../api/models/Pet'; +import { UserRepository } from './../api/repositories/UserRepository'; + + +export const graphqlLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { + if (settings && env.graphql.enabled) { + const expressApp = settings.getData('express_app'); + + createGraphQLServer(expressApp, { + route: env.graphql.route, + editorEnabled: env.graphql.editor, + queries: env.app.dirs.queries, + mutations: env.app.dirs.mutations, + dataLoaders: { + users: createDataLoader(UserRepository), + pets: createDataLoader(Pet), + petByUserIds: createDataLoader(PetRepository, 'findByUserIds', 'userId'), + }, + }); + + } +}; diff --git a/src/loaders/homeLoader.ts b/src/loaders/homeLoader.ts index 0011d11d..6180ea1e 100644 --- a/src/loaders/homeLoader.ts +++ b/src/loaders/homeLoader.ts @@ -1,5 +1,5 @@ import * as express from 'express'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import { env } from '../core/env'; diff --git a/src/loaders/iocLoader.ts b/src/loaders/iocLoader.ts index c1d44ca0..99600a92 100644 --- a/src/loaders/iocLoader.ts +++ b/src/loaders/iocLoader.ts @@ -1,7 +1,8 @@ import { Container } from 'typedi'; import { useContainer as ormUseContainer } from 'typeorm'; import { useContainer as routingUseContainer } from 'routing-controllers'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { useContainer as graphqlUseContainer } from '../lib/graphql'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; export const iocLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { @@ -11,5 +12,6 @@ export const iocLoader: MicroframeworkLoader = (settings: MicroframeworkSettings */ routingUseContainer(Container); ormUseContainer(Container); + graphqlUseContainer(Container); }; diff --git a/src/loaders/monitorLoader.ts b/src/loaders/monitorLoader.ts index 0bf12aea..54ff7a70 100644 --- a/src/loaders/monitorLoader.ts +++ b/src/loaders/monitorLoader.ts @@ -1,6 +1,6 @@ import * as monitor from 'express-status-monitor'; import * as basicAuth from 'express-basic-auth'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import { env } from '../core/env'; diff --git a/src/loaders/publicLoader.ts b/src/loaders/publicLoader.ts index 491ca11e..18ea1183 100644 --- a/src/loaders/publicLoader.ts +++ b/src/loaders/publicLoader.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as express from 'express'; import * as favicon from 'serve-favicon'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; export const publicLoader: MicroframeworkLoader = (settings: MicroframeworkSettings | undefined) => { diff --git a/src/loaders/swaggerLoader.ts b/src/loaders/swaggerLoader.ts index 3fadca29..16dbda79 100644 --- a/src/loaders/swaggerLoader.ts +++ b/src/loaders/swaggerLoader.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as swaggerUi from 'swagger-ui-express'; import * as basicAuth from 'express-basic-auth'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import { env } from '../core/env'; diff --git a/src/loaders/typeormLoader.ts b/src/loaders/typeormLoader.ts index 6d6871fc..0140f844 100644 --- a/src/loaders/typeormLoader.ts +++ b/src/loaders/typeormLoader.ts @@ -1,5 +1,5 @@ import { createConnection } from 'typeorm'; -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import { env } from '../core/env'; @@ -15,6 +15,7 @@ export const typeormLoader: MicroframeworkLoader = async (settings: Microframewo synchronize: env.db.synchronize, logging: env.db.logging, entities: env.app.dirs.entities, + migrations: env.app.dirs.migrations, }); if (settings) { diff --git a/src/loaders/winstonLoader.ts b/src/loaders/winstonLoader.ts index c61f563c..4e693996 100644 --- a/src/loaders/winstonLoader.ts +++ b/src/loaders/winstonLoader.ts @@ -1,4 +1,4 @@ -import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; import * as winston from 'winston'; import { env } from '../core/env'; diff --git a/src/types/auth0.d.ts b/src/types/auth0.d.ts deleted file mode 100644 index 317ab01e..00000000 --- a/src/types/auth0.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -/** - * auth0 - * ---------------------------------------- - * - * Type definitions for the auth0 responses. - */ - -declare namespace auth0 { - - interface User { - user_id: string; - email: string; - email_verified: boolean; - picture: string; - created_at: Date; - updated_at: Date; - clientID?: string; - nickname?: string; - name?: string; - global_client_id?: string; - identities?: UserIdentities[]; - } - - interface UserIdentities { - user_id: string; - provider: string; - connection: string; - isSocial: boolean; - } - - interface Body { - client_id: string; - client_secret: string; - audience: string; - grant_type: string; - } - -} - -export as namespace auth0; -export = auth0; diff --git a/test/e2e/.gitkeep b/test/e2e/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/test/e2e/api/info.test.ts b/test/e2e/api/info.test.ts new file mode 100644 index 00000000..b1025ab5 --- /dev/null +++ b/test/e2e/api/info.test.ts @@ -0,0 +1,29 @@ +import * as request from 'supertest'; +import { bootstrapApp, BootstrapSettings } from '../utils/bootstrap'; +import { env } from '../../../src/core/env'; + + +describe('/api', () => { + + // ------------------------------------------------------------------------- + // Setup up + // ------------------------------------------------------------------------- + + let settings: BootstrapSettings; + beforeAll(async () => settings = await bootstrapApp()); + + // ------------------------------------------------------------------------- + // Test cases + // ------------------------------------------------------------------------- + + test('GET: / should return the api-version', async (done) => { + const response = await request(settings.app) + .get('/api') + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body.version).toBe(env.app.version); + done(); + }); + +}); diff --git a/test/e2e/api/users.test.ts b/test/e2e/api/users.test.ts new file mode 100644 index 00000000..8daaf4ed --- /dev/null +++ b/test/e2e/api/users.test.ts @@ -0,0 +1,64 @@ +import * as nock from 'nock'; +import * as request from 'supertest'; +import { CreateBruce } from './../../../src/database/seeds/CreateBruce'; +import { getFactory } from './../../../src/lib/seeds/index'; +import { Factory } from './../../../src/lib/seeds/Factory'; +import { User } from './../../../src/api/models/User'; +import { bootstrapApp, BootstrapSettings } from '../utils/bootstrap'; +import { migrateDatabase, closeDatabase } from '../../utils/database'; +import { fakeAuthenticationForUser } from '../utils/auth'; + + +describe('/api/users', () => { + + // ------------------------------------------------------------------------- + // 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)); + + // ------------------------------------------------------------------------- + // Tear down + // ------------------------------------------------------------------------- + + afterAll(() => nock.cleanAll()); + afterAll(async () => closeDatabase(settings.connection)); + + // ------------------------------------------------------------------------- + // Test cases + // ------------------------------------------------------------------------- + + test('GET: / should return a list of users', async (done) => { + const response = await request(settings.app) + .get('/api/users') + .set('Authorization', `Bearer 1234`) + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body.length).toBe(1); + done(); + }); + + test('GET: /:id should return bruce', async (done) => { + const response = await request(settings.app) + .get(`/api/users/${bruce.id}`) + .set('Authorization', `Bearer 1234`) + .expect('Content-Type', /json/) + .expect(200); + + expect(response.body.id).toBe(bruce.id); + expect(response.body.firstName).toBe(bruce.firstName); + expect(response.body.lastName).toBe(bruce.lastName); + expect(response.body.email).toBe(bruce.email); + done(); + }); + +}); diff --git a/test/e2e/utils/auth.ts b/test/e2e/utils/auth.ts new file mode 100644 index 00000000..eb8d832c --- /dev/null +++ b/test/e2e/utils/auth.ts @@ -0,0 +1,16 @@ +import * as nock from 'nock'; +import { User } from '../../../src/api/models/User'; +import { env } from '../../../src/core/env'; + + +export const fakeAuthenticationForUser = (user: User, persist = false): nock.Scope => { + const scope = nock(env.auth.route) + .post('') + .reply(200, { + user_id: `auth0|${user.email}`, + }); + if (persist) { + scope.persist(); + } + return scope; +}; diff --git a/test/e2e/utils/bootstrap.ts b/test/e2e/utils/bootstrap.ts new file mode 100644 index 00000000..db29ae29 --- /dev/null +++ b/test/e2e/utils/bootstrap.ts @@ -0,0 +1,35 @@ +import * as http from 'http'; +import { bootstrapMicroframework } from 'microframework-w3tec'; +import { Application } from 'express'; +import { Connection } from 'typeorm/connection/Connection'; +import { expressLoader } from './../../../src/loaders/expressLoader'; +import { winstonLoader } from './../../../src/loaders/winstonLoader'; +import { homeLoader } from './../../../src/loaders/homeLoader'; +import { typeormLoader } from '../utils/typeormLoader'; +import { iocLoader } from './../../../src/loaders/iocLoader'; +import { eventDispatchLoader } from './../../../src/loaders/eventDispatchLoader'; + + +export interface BootstrapSettings { + app: Application; + server: http.Server; + connection: Connection; +} + +export const bootstrapApp = async (): Promise => { + const framework = await bootstrapMicroframework({ + loaders: [ + winstonLoader, + iocLoader, + eventDispatchLoader, + typeormLoader, + expressLoader, + homeLoader, + ], + }); + return { + app: framework.settings.getData('express_app') as Application, + server: framework.settings.getData('express_server') as http.Server, + connection: framework.settings.getData('connection') as Connection, + } as BootstrapSettings; +}; diff --git a/test/e2e/utils/typeormLoader.ts b/test/e2e/utils/typeormLoader.ts new file mode 100644 index 00000000..31129e27 --- /dev/null +++ b/test/e2e/utils/typeormLoader.ts @@ -0,0 +1,12 @@ +import { createDatabaseConnection } from './../../utils/database'; +import { MicroframeworkSettings, MicroframeworkLoader } from 'microframework-w3tec'; + + +export const typeormLoader: MicroframeworkLoader = async (settings: MicroframeworkSettings | undefined) => { + + const connection = await createDatabaseConnection(); + if (settings) { + settings.setData('connection', connection); + settings.onShutdown(() => connection.close()); + } +}; diff --git a/test/integration/PetService.test.ts b/test/integration/PetService.test.ts new file mode 100644 index 00000000..3676049f --- /dev/null +++ b/test/integration/PetService.test.ts @@ -0,0 +1,47 @@ +import { Container } from 'typedi'; +import { Connection } from 'typeorm'; + +import { Pet } from '../../src/api/models/Pet'; +import { PetService } from './../../src/api/services/PetService'; +import { createDatabaseConnection, migrateDatabase, closeDatabase } from '../utils/database'; + +describe('PetService', () => { + + // ------------------------------------------------------------------------- + // Setup up + // ------------------------------------------------------------------------- + + let connection: Connection; + beforeAll(async () => connection = await createDatabaseConnection()); + beforeEach(() => migrateDatabase(connection)); + + // ------------------------------------------------------------------------- + // Tear down + // ------------------------------------------------------------------------- + + afterAll(() => closeDatabase(connection)); + + // ------------------------------------------------------------------------- + // Test cases + // ------------------------------------------------------------------------- + + test('should create a new pet in the database', async (done) => { + const pet = new Pet(); + pet.name = 'test'; + pet.age = 1; + const service = Container.get(PetService); + const resultCreate = await service.create(pet); + expect(resultCreate.name).toBe(pet.name); + expect(resultCreate.age).toBe(pet.age); + + const resultFind = await service.findOne(resultCreate.id); + if (resultFind) { + expect(resultFind.name).toBe(pet.name); + expect(resultFind.age).toBe(pet.age); + } else { + fail('Could not find pet'); + } + done(); + }); + +}); diff --git a/test/unit/lib/RepositoryMock.ts b/test/unit/lib/RepositoryMock.ts index 6b11d8de..90f72625 100644 --- a/test/unit/lib/RepositoryMock.ts +++ b/test/unit/lib/RepositoryMock.ts @@ -1,6 +1,3 @@ -import { validate } from 'class-validator'; - - export class RepositoryMock { public one: T; diff --git a/test/unit/services/UserService.test.ts b/test/unit/services/UserService.test.ts index ef55afc1..8a84ad2b 100644 --- a/test/unit/services/UserService.test.ts +++ b/test/unit/services/UserService.test.ts @@ -1,5 +1,4 @@ import { UserService } from '../../../src/api/services/UserService'; -import { UserRepository } from '../../../src/api/repositories/UserRepository'; import { User } from '../../../src/api/models/User'; import { events } from '../../../src/api/subscribers/events'; import { LogMock } from '../lib/LogMock'; diff --git a/test/utils/database.ts b/test/utils/database.ts new file mode 100644 index 00000000..38de7bc2 --- /dev/null +++ b/test/utils/database.ts @@ -0,0 +1,30 @@ +import { Container } from 'typedi'; +import { createConnection, useContainer, Connection } from 'typeorm'; +import { env } from '../../src/core/env'; + + +export const createDatabaseConnection = async (): Promise => { + useContainer(Container); + const connection = await createConnection({ + type: env.db.type as any, // See createConnection options for valid types + database: env.db.database, + logging: env.db.logging, + entities: env.app.dirs.entities, + migrations: env.app.dirs.migrations, + }); + return connection; +}; + +export const synchronizeDatabase = async (connection: Connection) => { + await connection.dropDatabase(); + return connection.synchronize(true); +}; + +export const migrateDatabase = async (connection: Connection) => { + await connection.dropDatabase(); + return connection.runMigrations(); +}; + +export const closeDatabase = (connection: Connection) => { + return connection.close(); +}; diff --git a/tsconfig.json b/tsconfig.json index 9f803e59..897048de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,8 +20,5 @@ "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": true - }, - "include": [ - "src/**/*" - ] + } } diff --git a/yarn.lock b/yarn.lock index 9472fbd1..707e24e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -72,8 +72,8 @@ resolved "https://registry.yarnpkg.com/@types/jest/-/jest-21.1.8.tgz#d497213725684f1e5a37900b17a47c9c018f1a97" "@types/lodash@^4.14.80": - version "4.14.88" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.88.tgz#97eaf2dc668f33ed8e511a5b627035f4ea20b0f5" + version "4.14.89" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.89.tgz#dd2114403d653a45c3d2f0cae1599949b99a5a3a" "@types/mime@*": version "2.0.0" @@ -85,17 +85,23 @@ dependencies: "@types/express" "*" +"@types/nock@^8.2.1": + version "8.2.1" + resolved "https://registry.yarnpkg.com/@types/nock/-/nock-8.2.1.tgz#1fbe5bdecb943c109a778553fa4d2401cb9394b4" + dependencies: + "@types/node" "*" + "@types/node@*": - version "8.0.57" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.57.tgz#e5d8b4dc112763e35cfc51988f4f38da3c486d99" + version "8.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.5.1.tgz#4ec3020bcdfe2abffeef9ba3fbf26fca097514b5" "@types/reflect-metadata@0.0.5": version "0.0.5" resolved "https://registry.yarnpkg.com/@types/reflect-metadata/-/reflect-metadata-0.0.5.tgz#9c042bfa9803d577aad4f57dfbca4b7cae4286fe" "@types/request@^2.0.8": - version "2.0.8" - resolved "https://registry.yarnpkg.com/@types/request/-/request-2.0.8.tgz#424d3de255868107ed4dd6695c65c5f1766aba80" + version "2.0.9" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.0.9.tgz#125b8a60d8a439e8d87e6d1335c61cccdc18343a" dependencies: "@types/form-data" "*" "@types/node" "*" @@ -113,6 +119,18 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/superagent@*": + version "3.5.6" + resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.5.6.tgz#9cf2632c075ba9e601f6a610aadc23992d02394c" + dependencies: + "@types/node" "*" + +"@types/supertest@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/supertest/-/supertest-2.0.4.tgz#28770e13293365e240a842d7d5c5a1b3d2dee593" + dependencies: + "@types/superagent" "*" + "@types/uuid@^3.4.3": version "3.4.3" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.3.tgz#121ace265f5569ce40f4f6d0ff78a338c732a754" @@ -140,7 +158,7 @@ accepts@1.3.3: mime-types "~2.1.11" negotiator "0.6.1" -accepts@^1.3.4, accepts@~1.3.4: +accepts@^1.3.0, accepts@^1.3.4, accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" dependencies: @@ -868,7 +886,7 @@ component-bind@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" -component-emitter@1.2.1: +component-emitter@1.2.1, component-emitter@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" @@ -940,7 +958,7 @@ content-type-parser@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" -content-type@^1.0.4, content-type@~1.0.4: +content-type@^1.0.2, content-type@^1.0.4, content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" @@ -960,6 +978,10 @@ cookie@0.3.1, cookie@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +cookiejar@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" + copyfiles@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-1.2.0.tgz#a8da3ac41aa2220ae29bd3c58b6984294f2c593c" @@ -1110,6 +1132,10 @@ dashify@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/dashify/-/dashify-0.2.2.tgz#6a07415a01c91faf4a32e38d9dfba71f61cb20fe" +dataloader@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.3.0.tgz#6fec5be4b30a712e4afd30b86b4334566b97673b" + date-fns@^1.23.0: version "1.29.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" @@ -1388,6 +1414,15 @@ express-basic-auth@^1.1.3: dependencies: basic-auth "^1.1.0" +express-graphql@^0.6.11: + version "0.6.11" + resolved "https://registry.yarnpkg.com/express-graphql/-/express-graphql-0.6.11.tgz#3dce78d0643e78e7e3606646ce162025ba0585ab" + dependencies: + accepts "^1.3.0" + content-type "^1.0.2" + http-errors "^1.3.0" + raw-body "^2.1.0" + express-status-monitor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/express-status-monitor/-/express-status-monitor-1.0.1.tgz#311288347b7aabfeaec0a01547e55c77652bb298" @@ -1432,7 +1467,7 @@ express@^4.16.2: utils-merge "1.0.1" vary "~1.1.2" -extend@~3.0.0, extend@~3.0.1: +extend@^3.0.0, extend@~3.0.0, extend@~3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444" @@ -1548,22 +1583,26 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" +form-data@^2.3.1, form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" dependencies: asynckit "^0.4.0" combined-stream "^1.0.5" mime-types "^2.1.12" -form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" dependencies: asynckit "^0.4.0" combined-stream "^1.0.5" mime-types "^2.1.12" +formidable@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -1736,6 +1775,12 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" +graphql@^0.11.7: + version "0.11.7" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-0.11.7.tgz#e5abaa9cb7b7cccb84e9f0836bf4370d268750c6" + dependencies: + iterall "1.1.3" + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -1898,7 +1943,7 @@ html-encoding-sniffer@^1.0.1: dependencies: whatwg-encoding "^1.0.1" -http-errors@1.6.2, http-errors@~1.6.2: +http-errors@1.6.2, http-errors@^1.3.0, http-errors@~1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" dependencies: @@ -2210,6 +2255,10 @@ istanbul-reports@^1.1.3: dependencies: handlebars "^4.0.3" +iterall@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.1.3.tgz#1cbbff96204056dde6656e2ed2e2226d0e6d72c9" + jest-changed-files@^21.2.0: version "21.2.0" resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29" @@ -2751,13 +2800,13 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" -methods@~1.1.2: +methods@^1.1.1, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" -microframework@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/microframework/-/microframework-0.6.4.tgz#668ad0a8f5d7acdfec1bbdc3c01d430ef70021fd" +microframework-w3tec@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/microframework-w3tec/-/microframework-w3tec-0.6.3.tgz#19a672d6a3b021ca3aaf30244b1b28bcea036ec0" dependencies: app-root-path "^2.0.1" @@ -2797,6 +2846,10 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -2900,6 +2953,10 @@ nan@^2.3.0: version "2.8.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" +nan@~2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.7.0.tgz#d95bf721ec877e08db276ed3fc6eb78f9083ad46" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -2922,7 +2979,7 @@ nocache@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.0.0.tgz#202b48021a0c4cbde2df80de15a17443c8b43980" -nock@^9.1.0: +nock@^9.1.4: version "9.1.4" resolved "https://registry.yarnpkg.com/nock/-/nock-9.1.4.tgz#5cdda89c5effaed0f448486f0135bf7b1e7bf1dc" dependencies: @@ -2949,7 +3006,7 @@ node-notifier@^5.0.2: shellwords "^0.1.0" which "^1.2.12" -node-pre-gyp@^0.6.39: +node-pre-gyp@^0.6.39, node-pre-gyp@~0.6.38: version "0.6.39" resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz#c00e96860b23c0e1420ac7befc5044e1d78d8649" dependencies: @@ -2966,8 +3023,8 @@ node-pre-gyp@^0.6.39: tar-pack "^3.4.0" nodemon@^1.12.1: - version "1.12.5" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.12.5.tgz#bd34afa1aa855f1996b561147848519f136de620" + version "1.12.7" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.12.7.tgz#4d0fa8386291c4f532f583cc102c05350722f647" dependencies: chokidar "^1.7.0" debug "^2.6.8" @@ -3426,7 +3483,7 @@ range-parser@^1.2.0, range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" -raw-body@2.3.2: +raw-body@2.3.2, raw-body@^2.1.0: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" dependencies: @@ -3474,7 +3531,7 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" -readable-stream@2.3.3, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5: +readable-stream@2.3.3, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4, readable-stream@^2.1.5: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -3893,6 +3950,13 @@ sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" +sqlite3@^3.1.13: + version "3.1.13" + resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-3.1.13.tgz#d990a05627392768de6278bafd1a31fdfe907dd9" + dependencies: + nan "~2.7.0" + node-pre-gyp "~0.6.38" + sqlstring@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.0.tgz#525b8a4fd26d6f71aa61e822a6caf976d31ad2a8" @@ -4017,6 +4081,28 @@ subarg@^1.0.0: dependencies: minimist "^1.1.0" +superagent@^3.0.0: + version "3.8.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.2.tgz#e4a11b9d047f7d3efeb3bbe536d9ec0021d16403" + dependencies: + component-emitter "^1.2.0" + cookiejar "^2.1.0" + debug "^3.1.0" + extend "^3.0.0" + form-data "^2.3.1" + formidable "^1.1.1" + methods "^1.1.1" + mime "^1.4.1" + qs "^6.5.1" + readable-stream "^2.0.5" + +supertest@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" + dependencies: + methods "~1.1.2" + superagent "^3.0.0" + supports-color@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-0.2.0.tgz#d92de2694eb3f67323973d7ae3d8b55b4c22190a" @@ -4038,8 +4124,8 @@ supports-color@^4.0.0: has-flag "^2.0.0" swagger-ui-express@^2.0.10: - version "2.0.11" - resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-2.0.11.tgz#db331cf888640a93b7a5b06111b02907a4be9c35" + version "2.0.12" + resolved "https://registry.yarnpkg.com/swagger-ui-express/-/swagger-ui-express-2.0.12.tgz#62d9df3b654fe3a04c0b6716f86150bc2c3f1619" symbol-tree@^3.2.1: version "3.2.2" @@ -4275,12 +4361,13 @@ 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.7" - resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.1.7.tgz#40fdb0ff1d5f69560a43a9b21297cb293152a8e2" + version "0.1.9" + resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.1.9.tgz#208a5a2c7e0139621cacecb0636753190a988d2b" dependencies: app-root-path "^2.0.1" chalk "^2.0.1" cli-highlight "^1.1.4" + debug "^3.1.0" dotenv "^4.0.0" glob "^7.1.2" js-yaml "^3.8.4"