├── .eslintrc.yaml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── codegen.yml ├── nodemon.json ├── package-lock.json ├── package.json ├── readme.md ├── renovate.json ├── src ├── context.ts ├── core │ ├── config │ │ ├── AbstractSetting.ts │ │ ├── IConfig.ts │ │ └── Setting.ts │ ├── injector.ts │ └── logger │ │ ├── AbstractLogger.ts │ │ └── Logger.ts ├── graphql │ ├── resolvers │ │ ├── car.ts │ │ └── train.ts │ ├── schema │ │ ├── genereator-schema.ts │ │ └── schema.ts │ ├── subscriptions │ │ ├── Pubsub │ │ │ ├── AbstractPubsubManager.ts │ │ │ └── PubsubManager.ts │ │ └── Topics │ │ │ ├── IPubsubTopics.ts │ │ │ └── PubsubTopicsImpl.ts │ └── types │ │ ├── car.ts │ │ └── train.ts ├── interfaces │ └── IAppContext.ts ├── main.ts ├── server.ts └── services │ ├── cars │ └── CarsService.ts │ └── trains │ └── TrainsService.ts ├── tsconfig.json └── tslint.json /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: airbnb-base 3 | env: 4 | node: true 5 | mocha: true 6 | es6: true 7 | parser: typescript-eslint-parser 8 | parserOptions: 9 | sourceType: module 10 | ecmaFeatures: 11 | modules: true 12 | rules: 13 | generator-star-spacing: 14 | - 2 15 | - before: true 16 | after: true 17 | no-shadow: 0 18 | import/no-unresolved: 0 19 | import/extensions: 0 20 | require-yield: 0 21 | no-param-reassign: 0 22 | comma-dangle: 0 23 | no-underscore-dangle: 0 24 | no-control-regex: 0 25 | import/no-extraneous-dependencies: 26 | - 2 27 | - devDependencies: true 28 | func-names: 0 29 | no-unused-expressions: 0 30 | prefer-arrow-callback: 1 31 | no-use-before-define: 32 | - 2 33 | - functions: false 34 | space-before-function-paren: 35 | - 2 36 | - always 37 | max-len: 38 | - 2 39 | - 120 40 | - 2 41 | semi: 42 | - 2 43 | - never 44 | strict: 45 | - 2 46 | - global 47 | arrow-parens: 48 | - 2 49 | - always 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | .idea 5 | types.d.ts 6 | src/log 7 | ts-files.txt -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 115, 6 | "useTabs": false, 7 | "jsxBracketSameLine": true, 8 | "arrowParens": "always", 9 | "bracketSpacing": false 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Tom 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: 2 | - "./dist/graphql/schema/genereator-schema.js" 3 | documents: [] 4 | overwrite: true 5 | generates: 6 | ./src/interfaces/types.d.ts: 7 | plugins: 8 | - "typescript" 9 | require: [] 10 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "ignore": ["src/**/*.spec.ts"], 5 | "exec": "ts-node ./src/main.ts" 6 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tomyitav/graphql-server-typed", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-schema:win": "rimraf ./dist/graphql/schema/ && dir /s /b \"src/graphql/types\" | findstr /e .ts > ts-files.txt && tsc ./src/graphql/schema/genereator-schema.ts --outDir ./dist/graphql/schema/ --lib esnext && tsc @ts-files.txt --outDir ./dist/graphql/types --lib esnext && del ts-files.txt", 8 | "generate-ts": "gql-gen", 9 | "pregenerate:win": "npm run build-schema:win", 10 | "generate:win": "npm run generate-ts", 11 | "clean": "rimraf dist", 12 | "prebuild:win": "npm run clean && npm run generate:win", 13 | "build:win": "npm run tsc:win", 14 | "start": "node dist/main.js", 15 | "start:dev": "ts-node src/main.ts", 16 | "start:watch": "nodemon", 17 | "test": "tsc && mocha dist/**/*.spec.js", 18 | "lint": "eslint src --ext ts", 19 | "tsc:win": "tsc", 20 | "build-schema": "rimraf ./dist/graphql/schema/ && find src/graphql/types -iname \"*.ts\" > ts-files.txt && tsc ./src/graphql/schema/genereator-schema.ts --outDir ./dist/graphql/schema/ --lib esnext && tsc @ts-files.txt --outDir ./dist/graphql/types --lib esnext && rimraf ts-files.txt", 21 | "pregenerate": "npm run build-schema", 22 | "generate": "npm run generate-ts", 23 | "prebuild": "npm run clean && npm run generate", 24 | "build": "npm run tsc", 25 | "tsc": "tsc", 26 | "tslint": "tslint --fix -c tslint.json 'src/**/*.ts'", 27 | "prettier": "prettier --write \"src/**/*.ts\"", 28 | "precommit-tslint": "tslint --fix -c tslint.json", 29 | "precommit-prettier": "prettier --write", 30 | "tslint-check": "tslint-config-prettier-check ./tslint.json", 31 | "deploy": "now" 32 | }, 33 | "husky": { 34 | "hooks": { 35 | "pre-commit": "lint-staged" 36 | } 37 | }, 38 | "lint-staged": { 39 | "linters": { 40 | "*.ts": [ 41 | "npm run precommit-tslint", 42 | "npm run precommit-prettier", 43 | "git add" 44 | ] 45 | } 46 | }, 47 | "_moduleAliases": { 48 | "@src": "dist" 49 | }, 50 | "keywords": [], 51 | "author": "", 52 | "license": "ISC", 53 | "dependencies": { 54 | "apollo-server-express": "2.6.3", 55 | "core-js": "3.1.3", 56 | "cors": "2.8.5", 57 | "express": "4.17.1", 58 | "graphql": "14.3.1", 59 | "graphql-subscriptions": "1.1.0", 60 | "graphql-tools": "4.0.5", 61 | "injection-js": "2.2.2", 62 | "merge-graphql-schemas": "1.5.8", 63 | "module-alias": "2.2.0", 64 | "reflect-metadata": "0.1.13", 65 | "subscriptions-transport-ws": "0.9.16", 66 | "winston": "3.2.1", 67 | "winston-daily-rotate-file": "3.9.0", 68 | "zone.js": "0.9.1" 69 | }, 70 | "devDependencies": { 71 | "@types/express": "4.17.0", 72 | "@types/graphql": "14.2.0", 73 | "@types/node": "12.0.12", 74 | "@graphql-codegen/cli": "1.2.1", 75 | "@graphql-codegen/typescript": "1.2.1", 76 | "@graphql-codegen/typescript-operations": "1.2.1", 77 | "husky": "2.4.1", 78 | "lint-staged": "8.2.1", 79 | "nodemon": "1.19.2", 80 | "prettier": "1.18.2", 81 | "rimraf": "2.6.3", 82 | "ts-node": "8.2.0", 83 | "tslint": "5.18.0", 84 | "tslint-config-prettier": "1.18.0", 85 | "tslint-eslint-rules": "5.4.0", 86 | "tslint-react": "4.0.0", 87 | "typescript": "3.5.3", 88 | "typescript-eslint-parser": "21.0.2" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # graphql-server-typed 2 | 3 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 4 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 5 | [![renovate-app badge][renovate-badge]][renovate-app] 6 | 7 | [renovate-badge]: https://img.shields.io/badge/renovate-app-blue.svg 8 | [renovate-app]: https://renovateapp.com/ 9 | 10 | Boilerplate project for [create-graphql-app](https://github.com/tomyitav/create-graphql-app) cli. 11 | 12 | Create a fully configured, production ready graphql server, using 13 | 14 | + typescript 15 | + [graphql-code-generator](https://github.com/dotansimha/graphql-code-generator) 16 | + graphql-subscriptions 17 | + merge-graphql-schemas 18 | + Dependency injection with `injection-js` 19 | 20 | This project demonstrates how to generate typescript types from graphql schema, using Graphql code generetor library. 21 | 22 | ## Installation 23 | 24 | Clone the repository and run `npm install` 25 | 26 | ``` 27 | git clone https://github.com/tomyitav/graphql-server-typed.git 28 | npm install 29 | ``` 30 | 31 | ## Build and start server instance 32 | 33 | #### Build command 34 | 35 | Running the command 36 | ``` 37 | npm run build 38 | ``` 39 | 40 | Or on windows machine, run 41 | ``` 42 | npm run build:win 43 | ``` 44 | 45 | will generate typescript types, and transpile code to *dist* folder 46 | 47 | #### How to start server instance after building 48 | 49 | ``` 50 | npm start 51 | ``` 52 | 53 | ## Run server directly with ts-node 54 | 55 | ``` 56 | npm start:dev 57 | ``` 58 | 59 | #### Watch for code changes 60 | 61 | ``` 62 | npm run start:watch 63 | ``` 64 | 65 | This will monitor your changes and will automatically restart the server. 66 | 67 | The server will run on port 8080. 68 | You can change this by editing the config file. 69 | 70 | ## Code Formatting 71 | 72 | We use Prettier and Tslint to format and enforce standards on our code.
73 | Both will run on the project automatically before each commit.
74 | 75 | Prettier rewrites code according to the .prettierrc.json configuration file.
76 | If you want to activate prettier manually (on all .ts files inside src folder) without committing, run:
77 | 78 | ``` 79 | npm run prettier 80 | ``` 81 | 82 | Tslint will check rules found in the tslint.json configuration file.
83 | If you want to check tslint manually (on all .ts files inside src folder) without committing, run:
84 | 85 | ``` 86 | npm run tslint 87 | ``` 88 | 89 | 90 | ## Type generation using gql codegen 91 | 92 | ``` 93 | npm run generate 94 | ``` 95 | 96 | This will automatically generate types in types.d.ts file! 97 | generate command is executed in every build 98 | 99 | ## Project structure 100 | 101 | We use the function `makeExecutableSchema()` from graphql-tools to to combine our 102 | types and resolvers. Instead of passing one large string for our schema, we 103 | split our types and resolvers to multiple files, located in graphql directory in 104 | types and resolvers directories. This way, we avoid schema complexity by using 105 | merge-graphql-schemas: 106 | 107 | ```js 108 | import * as path from "path"; 109 | import {makeExecutableSchema} from "graphql-tools"; 110 | import {fileLoader, mergeTypes, mergeResolvers} from "merge-graphql-schemas"; 111 | import {GraphQLSchema} from "graphql"; 112 | 113 | const typesArray = fileLoader(path.join(__dirname, '../types'), { recursive: true }); 114 | const resolversArray = fileLoader(path.join(__dirname, '../resolvers')); 115 | const allTypes = mergeTypes(typesArray); 116 | const allResolvers = mergeResolvers(resolversArray); 117 | let schema: GraphQLSchema; 118 | schema= makeExecutableSchema({ 119 | typeDefs: allTypes, 120 | resolvers: allResolvers 121 | }); 122 | 123 | export default schema; 124 | ``` 125 | 126 | So as your project grows - you can extend the schema by adding new type in types 127 | directory, and adding matching resolver file in resolvers directory. The schema 128 | is updated automatically. 129 | 130 | ## Deploy server to production :rocket: 131 | First, make sure you have [now-cli](https://zeit.co/now) installed. 132 | Then, execute the following command: 133 | 134 | ``` 135 | npm run deploy 136 | ``` 137 | 138 | That's it! The server will be deployed on *now* 139 | 140 | ## Run server as AWS lambda 141 | 142 | See the following [project](https://github.com/tomyitav/apollo-typed-lambda) for setting up aws lambda integration 143 | 144 | ## Connect to the server from client app 145 | 146 | See the following [example](https://github.com/tomyitav/apollo-angular-client-starter) on how to connect to the server using apollo-angular. 147 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import {Injector} from 'injection-js' 2 | import {IAppContext} from './interfaces/IAppContext' 3 | import {CarsService} from '@src/services/cars/CarsService' 4 | import {TrainsService} from '@src/services/trains/TrainsService' 5 | import {AbstractPubsubManager} from './graphql/subscriptions/Pubsub/AbstractPubsubManager' 6 | 7 | export function getContext(injector: Injector): IAppContext { 8 | return { 9 | pubsubManager: injector.get(AbstractPubsubManager), 10 | carsService: injector.get(CarsService), 11 | trainsService: injector.get(TrainsService) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/core/config/AbstractSetting.ts: -------------------------------------------------------------------------------- 1 | import {IConfig} from './IConfig' 2 | 3 | export abstract class AbstractSetting { 4 | public abstract get config(): IConfig 5 | } 6 | -------------------------------------------------------------------------------- /src/core/config/IConfig.ts: -------------------------------------------------------------------------------- 1 | export interface IConfig { 2 | server: IServerConfig 3 | log: ILoggerConfig 4 | } 5 | 6 | interface IServerConfig { 7 | port: string 8 | } 9 | 10 | interface ILoggerConfig { 11 | filename: string 12 | filedir: string 13 | } 14 | -------------------------------------------------------------------------------- /src/core/config/Setting.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'injection-js' 2 | import * as path from 'path' 3 | import {AbstractSetting} from './AbstractSetting' 4 | import {IConfig} from './IConfig' 5 | 6 | @Injectable() 7 | export class Setting extends AbstractSetting { 8 | private readonly settings: IConfig 9 | 10 | constructor() { 11 | super() 12 | this.settings = { 13 | log: { 14 | filedir: path.join(__dirname, '../../log'), 15 | filename: 'log.txt' 16 | }, 17 | server: { 18 | port: process.env.serverPort || '8080' 19 | } 20 | } 21 | } 22 | 23 | public get config(): IConfig { 24 | return this.settings 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/core/injector.ts: -------------------------------------------------------------------------------- 1 | import {Injector, ReflectiveInjector} from 'injection-js' 2 | import 'reflect-metadata' 3 | import 'zone.js' 4 | import {AbstractPubsubManager} from '../graphql/subscriptions/Pubsub/AbstractPubsubManager' 5 | import {PubsubManager} from '../graphql/subscriptions/Pubsub/PubsubManager' 6 | import {CarsService} from '@src/services/cars/CarsService' 7 | import {TrainsService} from '@src/services/trains/TrainsService' 8 | import {Server} from '../server' 9 | import {AbstractSetting} from './config/AbstractSetting' 10 | import {Setting} from './config/Setting' 11 | import {AbstractLogger} from './logger/AbstractLogger' 12 | import {Logger} from './logger/Logger' 13 | 14 | const injector: Injector = ReflectiveInjector.resolveAndCreate([ 15 | {provide: AbstractLogger, useClass: Logger}, 16 | {provide: AbstractSetting, useClass: Setting}, 17 | {provide: AbstractPubsubManager, useClass: PubsubManager}, 18 | Server, 19 | CarsService, 20 | TrainsService 21 | ]) 22 | 23 | export default injector 24 | -------------------------------------------------------------------------------- /src/core/logger/AbstractLogger.ts: -------------------------------------------------------------------------------- 1 | export abstract class AbstractLogger { 2 | public abstract log(level: string, message: string) 3 | 4 | public error(message: string) { 5 | this.log('ERROR', message) 6 | } 7 | 8 | public warn(message: string) { 9 | this.log('WARN', message) 10 | } 11 | 12 | public info(message: string) { 13 | this.log('INFO', message) 14 | } 15 | 16 | public debug(message: string) { 17 | this.log('DEBUG', message) 18 | } 19 | 20 | public verbose(message: string) { 21 | this.log('VERBOSE', message) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/core/logger/Logger.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import {Injectable} from 'injection-js' 3 | import * as winston from 'winston' 4 | import * as DailyRotate from 'winston-daily-rotate-file' 5 | import {AbstractSetting} from '../config/AbstractSetting' 6 | import {AbstractLogger} from './AbstractLogger' 7 | const format = winston.format 8 | 9 | @Injectable() 10 | export class Logger extends AbstractLogger { 11 | private logger: winston.Logger 12 | 13 | constructor(private settings: AbstractSetting) { 14 | super() 15 | this.checkForLogFileDir() 16 | this.initializeLogger() 17 | } 18 | 19 | public log(level: string, message: string) { 20 | this.logger.log(level.toLowerCase(), message) 21 | } 22 | 23 | private checkForLogFileDir() { 24 | const dir = this.settings.config.log.filedir 25 | 26 | if (!fs.existsSync(dir)) { 27 | fs.mkdirSync(dir) 28 | } 29 | } 30 | 31 | private initializeLogger() { 32 | this.logger = winston.createLogger({ 33 | level: 'info', 34 | format: winston.format.json(), 35 | transports: [ 36 | new winston.transports.Console({ 37 | format: format.combine(format.colorize(), format.simple()) 38 | }), 39 | new DailyRotate({ 40 | filename: this.settings.config.log.filename, 41 | dirname: this.settings.config.log.filedir, 42 | maxSize: 20971520, // 20MB 43 | maxFiles: 25, 44 | datePattern: 'DD-MM-YYYY' 45 | }) 46 | ] 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/graphql/resolvers/car.ts: -------------------------------------------------------------------------------- 1 | import {IAppContext} from '../../interfaces/IAppContext' 2 | import {Car, QueryCarArgs, MutationUpdateCarNameArgs} from '../../interfaces/types' 3 | import {CarsService} from '../../services/cars/CarsService' 4 | import TOPICS from '../subscriptions/Topics/PubsubTopicsImpl' 5 | 6 | const CAR_CHANGED_TOPIC = TOPICS.CAR_CHANGED_TOPIC 7 | 8 | const resolveFunctions = { 9 | Query: { 10 | car(_, args: QueryCarArgs, context: IAppContext): Promise { 11 | const carsService: CarsService = context.carsService 12 | 13 | return carsService.getCars(args.name) 14 | } 15 | }, 16 | 17 | Mutation: { 18 | updateCarName(_, args: MutationUpdateCarNameArgs, context: IAppContext): Promise { 19 | const carsService: CarsService = context.carsService 20 | 21 | return carsService.updateCarName(args._id, args.newName) 22 | } 23 | }, 24 | 25 | Subscription: { 26 | carChanged: { 27 | subscribe: (_, args, context: IAppContext) => 28 | context.pubsubManager.getPubSub().asyncIterator(CAR_CHANGED_TOPIC) 29 | } 30 | } 31 | } 32 | 33 | export default resolveFunctions 34 | -------------------------------------------------------------------------------- /src/graphql/resolvers/train.ts: -------------------------------------------------------------------------------- 1 | import {IAppContext} from '../../interfaces/IAppContext' 2 | import {Train, QueryTrainArgs} from '../../interfaces/types' 3 | import {TrainsService} from '../../services/trains/TrainsService' 4 | 5 | const resolveFunctions = { 6 | Query: { 7 | train(_, args: QueryTrainArgs, context: IAppContext): Train[] { 8 | const trainsService: TrainsService = context.trainsService 9 | 10 | return trainsService.getTrains(args.name) 11 | } 12 | } 13 | } 14 | 15 | export default resolveFunctions 16 | -------------------------------------------------------------------------------- /src/graphql/schema/genereator-schema.ts: -------------------------------------------------------------------------------- 1 | import {makeExecutableSchema} from 'graphql-tools' 2 | import {fileLoader, mergeTypes} from 'merge-graphql-schemas' 3 | import * as path from 'path' 4 | 5 | const types = mergeTypes(fileLoader(path.join(__dirname, '../types'), {recursive: true})) 6 | export const schema = makeExecutableSchema({ 7 | typeDefs: [types], 8 | allowUndefinedInResolve: true 9 | }) 10 | 11 | export default schema 12 | -------------------------------------------------------------------------------- /src/graphql/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import {GraphQLSchema} from 'graphql' 2 | import {makeExecutableSchema} from 'graphql-tools' 3 | import {fileLoader, mergeResolvers, mergeTypes} from 'merge-graphql-schemas' 4 | import * as path from 'path' 5 | 6 | const typesArray = fileLoader(path.join(__dirname, '../types'), {recursive: true}) 7 | const resolversArray = fileLoader(path.join(__dirname, '../resolvers')) 8 | const allTypes = mergeTypes(typesArray) 9 | const allResolvers = mergeResolvers(resolversArray) 10 | let schema: GraphQLSchema 11 | 12 | schema = makeExecutableSchema({ 13 | typeDefs: allTypes, 14 | resolvers: allResolvers 15 | }) 16 | 17 | export default schema 18 | -------------------------------------------------------------------------------- /src/graphql/subscriptions/Pubsub/AbstractPubsubManager.ts: -------------------------------------------------------------------------------- 1 | import {PubSubEngine} from 'graphql-subscriptions' 2 | import {TopicName} from '../Topics/IPubsubTopics' 3 | 4 | export abstract class AbstractPubsubManager { 5 | public abstract getPubSub(): PubSubEngine 6 | 7 | public publish(topic: TopicName, entity: any) { 8 | this.getPubSub().publish(topic, entity) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/graphql/subscriptions/Pubsub/PubsubManager.ts: -------------------------------------------------------------------------------- 1 | import {PubSub, PubSubEngine} from 'graphql-subscriptions' 2 | import {Injectable} from 'injection-js' 3 | import {AbstractLogger} from '../../../core/logger/AbstractLogger' 4 | import {TopicName} from '../Topics/IPubsubTopics' 5 | import {AbstractPubsubManager} from './AbstractPubsubManager' 6 | 7 | @Injectable() 8 | export class PubsubManager extends AbstractPubsubManager { 9 | protected pubsub: PubSubEngine 10 | constructor(private logger: AbstractLogger) { 11 | super() 12 | this.pubsub = new PubSub() 13 | } 14 | 15 | public getPubSub(): PubSubEngine { 16 | return this.pubsub 17 | } 18 | 19 | public publish(topic: TopicName, entity: any) { 20 | this.logger.info('Publishing on topic- ' + topic) 21 | super.publish(topic, entity) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/graphql/subscriptions/Topics/IPubsubTopics.ts: -------------------------------------------------------------------------------- 1 | export interface IPubsubTopics { 2 | CAR_CHANGED_TOPIC: TopicName 3 | TRAIN_CHANGED_TOPIC: TopicName 4 | } 5 | 6 | export type TopicName = 'car_changed' | 'train_changed' 7 | -------------------------------------------------------------------------------- /src/graphql/subscriptions/Topics/PubsubTopicsImpl.ts: -------------------------------------------------------------------------------- 1 | import {IPubsubTopics} from './IPubsubTopics' 2 | 3 | const TOPICS: IPubsubTopics = { 4 | CAR_CHANGED_TOPIC: 'car_changed', 5 | TRAIN_CHANGED_TOPIC: 'train_changed' 6 | } 7 | 8 | export default TOPICS 9 | -------------------------------------------------------------------------------- /src/graphql/types/car.ts: -------------------------------------------------------------------------------- 1 | const schema = ` 2 | type Car { 3 | _id : String 4 | name: String 5 | } 6 | 7 | # the schema allows the following query: 8 | type Query { 9 | car(name: String): [Car] 10 | } 11 | 12 | type Mutation { 13 | updateCarName(_id: String!, newName: String!): Car 14 | } 15 | 16 | type Subscription { 17 | carChanged: Car 18 | } 19 | ` 20 | 21 | export default schema 22 | -------------------------------------------------------------------------------- /src/graphql/types/train.ts: -------------------------------------------------------------------------------- 1 | const schema = ` 2 | type Train { 3 | _id : String 4 | name: String 5 | } 6 | 7 | # the schema allows the following query: 8 | type Query { 9 | train(name: String): [Train] 10 | } 11 | 12 | ` 13 | 14 | export default schema 15 | -------------------------------------------------------------------------------- /src/interfaces/IAppContext.ts: -------------------------------------------------------------------------------- 1 | import {AbstractPubsubManager} from '../graphql/subscriptions/Pubsub/AbstractPubsubManager' 2 | import {CarsService} from '../services/cars/CarsService' 3 | import {TrainsService} from '../services/trains/TrainsService' 4 | 5 | export interface IAppContext { 6 | pubsubManager: AbstractPubsubManager 7 | carsService: CarsService 8 | trainsService: TrainsService 9 | } 10 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable-next-line */ 2 | require('module-alias/register') 3 | import injector from './core/injector' 4 | import {Server} from './server' 5 | 6 | let server: Server 7 | server = injector.get(Server) 8 | server.startServer(injector) 9 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import * as cors from 'cors' 2 | import * as express from 'express' 3 | import {ApolloServer} from 'apollo-server-express' 4 | import {createServer} from 'http' 5 | import {Injectable, Injector} from 'injection-js' 6 | import {AbstractSetting} from './core/config/AbstractSetting' 7 | import {AbstractLogger} from './core/logger/AbstractLogger' 8 | import schema from './graphql/schema/schema' 9 | import {IAppContext} from './interfaces/IAppContext' 10 | import {getContext} from './context' 11 | 12 | @Injectable() 13 | export class Server { 14 | private app: express.Express 15 | private apolloServer: ApolloServer 16 | private port: number 17 | constructor(private logger: AbstractLogger, private setting: AbstractSetting) {} 18 | 19 | public startServer(injector: Injector) { 20 | this.logger.info('starting graphql server...') 21 | this.port = parseInt(this.setting.config.server.port, 10) 22 | this.app = express().use('*', cors()) 23 | const context: IAppContext = getContext(injector) 24 | this.initServer(context) 25 | } 26 | 27 | private initServer(context: IAppContext) { 28 | this.apolloServer = new ApolloServer({ 29 | schema, 30 | context 31 | }) 32 | this.apolloServer.applyMiddleware({app: this.app}) 33 | 34 | const httpServer = createServer(this.app) 35 | this.apolloServer.installSubscriptionHandlers(httpServer) 36 | 37 | httpServer.listen({port: this.port}, () => { 38 | this.logger.info(`Server is ready at http://localhost:${this.port}${this.apolloServer.graphqlPath}`) 39 | this.logger.info(`Playground is ready at http://localhost:${this.port}${this.apolloServer.graphqlPath}`) 40 | this.logger.info( 41 | `Subscriptions is ready at ws://localhost:${this.port}${this.apolloServer.subscriptionsPath}` 42 | ) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/services/cars/CarsService.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'injection-js' 2 | import {AbstractLogger} from '../../core/logger/AbstractLogger' 3 | import {AbstractPubsubManager} from '../../graphql/subscriptions/Pubsub/AbstractPubsubManager' 4 | import TOPICS from '../../graphql/subscriptions/Topics/PubsubTopicsImpl' 5 | import {Car} from '../../interfaces/types' 6 | 7 | @Injectable() 8 | export class CarsService { 9 | private carList: Car[] = [{_id: '1234', name: 'sampleCar1'}, {_id: '1244', name: 'sampleCar2'}] 10 | 11 | constructor(private logger: AbstractLogger, private pubsubManager: AbstractPubsubManager) {} 12 | 13 | public getCars(carName?: string): Promise { 14 | this.logger.info('Returning all cars...') 15 | 16 | return new Promise((resolve) => { 17 | let filteredCarsList 18 | if (carName) { 19 | filteredCarsList = this.carList.filter((car) => car.name === carName) 20 | resolve(filteredCarsList) 21 | } else { 22 | resolve(this.carList) 23 | } 24 | }) 25 | } 26 | 27 | public updateCarName(_id: string, newName: string): Promise { 28 | return new Promise((resolve) => { 29 | for (const car of this.carList) { 30 | if (car._id === _id) { 31 | car.name = newName 32 | this.pubsubManager.publish(TOPICS.CAR_CHANGED_TOPIC, {carChanged: car}) 33 | resolve(car) 34 | 35 | return 36 | } 37 | } 38 | 39 | resolve({}) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/services/trains/TrainsService.ts: -------------------------------------------------------------------------------- 1 | import {Injectable} from 'injection-js' 2 | import {AbstractLogger} from '../../core/logger/AbstractLogger' 3 | import {Train} from '../../interfaces/types' 4 | 5 | @Injectable() 6 | export class TrainsService { 7 | constructor(private logger: AbstractLogger) {} 8 | 9 | public getTrains(name?: string): Train[] { 10 | this.logger.info('Returning all trains...') 11 | 12 | return [ 13 | { 14 | _id: '1234', 15 | name: name || 'sampleTrain' 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "sourceMap": true, 7 | "experimentalDecorators": true, 8 | "skipLibCheck": true, 9 | "emitDecoratorMetadata": true, 10 | "baseUrl": ".", 11 | "paths": { 12 | "@src/*": ["src/*"] 13 | }, 14 | "lib": [ 15 | "es2017", 16 | "dom", 17 | "esnext.asynciterable" 18 | ] 19 | }, 20 | "files": [ 21 | "./node_modules/@types/node/index.d.ts" 22 | ], 23 | "include": [ 24 | "src/**/*.ts" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint:latest", "tslint-react", "tslint-eslint-rules", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "newline-before-return": true, 7 | "object-literal-sort-keys": false, 8 | "no-submodule-imports": false, 9 | "interface-name": [false, "always-prefix"], 10 | "indent": [true, "spaces", 2], 11 | "no-implicit-dependencies": false, 12 | "variable-name": [true, "allow-leading-underscore"], 13 | "ordered-imports": false 14 | }, 15 | "rulesDirectory": [] 16 | } 17 | --------------------------------------------------------------------------------