├── .DS_Store ├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── .vscode ├── README.md ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE ├── Procfile ├── README.md ├── __tests__ ├── middlewares │ └── isAuth.spec.ts ├── src │ ├── resolvers │ │ ├── config │ │ │ └── config.resolver.spec.ts │ │ └── user │ │ │ └── user.resolver.spec.ts │ └── utils │ │ ├── Queue.spec.ts │ │ └── globalMethods.spec.ts └── test.utils.ts ├── app.json ├── docker-compose.yml ├── jest.config.js ├── nodemon.json ├── ormconfig.example.json ├── package.json ├── prettier.config.js ├── scripts └── build.pkg.sh ├── src ├── config │ └── defaults.ts ├── entity │ └── User.ts ├── index.ts ├── interfaces │ ├── Config.ts │ ├── Mail.ts │ ├── MiddlewareBaseResolver.ts │ ├── MyContext.ts │ ├── Pagination.ts │ └── index.ts ├── jobs │ ├── Purge.ts │ ├── Recovery.ts │ ├── RegistrationMailer.ts │ └── index.ts ├── middlewares │ └── isAuth.ts ├── resolvers │ ├── config │ │ └── config.resolver.ts │ └── user │ │ ├── Inputs.ts │ │ └── user.resolver.ts ├── utils │ ├── Mail.ts │ ├── Queue.ts │ ├── contants.ts │ ├── createBaseResolver.ts │ ├── createSchema.ts │ ├── globalMethods.ts │ ├── logger.ts │ └── typeORMConn.ts └── worker.ts └── tsconfig.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kevenleone/graphscript/830eab7802741adb0df011a2b3fc90b82498cf0c/.DS_Store -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | data -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3333 2 | MAIL_FROM= 3 | AUTH_MIDDLEWARE_ENABLED= 4 | REDIS_URL=localhost 5 | REDIS_PORT=6379 6 | SENDGRID_USERNAME= 7 | SENDGRID_PASSWORD= -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .eslintrc.js 2 | packages/**/*.js 3 | packages/**/*.json 4 | node_modules 5 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es2020: true, 4 | node: true, 5 | }, 6 | extends: ['plugin:@typescript-eslint/recommended', 'prettier/@typescript-eslint', 'plugin:prettier/recommended'], 7 | globals: { 8 | Atomics: 'readonly', 9 | SharedArrayBuffer: 'readonly', 10 | }, 11 | parser: '@typescript-eslint/parser', 12 | parserOptions: { 13 | ecmaVersion: 2018, 14 | sourceType: 'module', 15 | }, 16 | plugins: ['@typescript-eslint', 'prettier', 'simple-import-sort', 'sort-destructure-keys', 'sort-keys-fix'], 17 | rules: { 18 | '@typescript-eslint/no-explicit-any': 0, 19 | camelcase: 'off', 20 | 'no-explicit-any': 'off', 21 | semi: ['error', 'always'], 22 | 'simple-import-sort/sort': 'error', 23 | 'sort-destructure-keys/sort-destructure-keys': [2, { caseSensitive: false }], 24 | 'sort-keys': ['error', 'asc', { caseSensitive: true, minKeys: 2, natural: false }], 25 | 'sort-keys-fix/sort-keys-fix': 'warn', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | ormconfig.json 3 | dist 4 | .env 5 | data 6 | log 7 | coverage 8 | yarn-error.log 9 | *.sqlite -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - node 7 | 8 | env: 9 | - PORT=3333 10 | - NODE_ENV=test 11 | 12 | before_script: 13 | - cp ormconfig.example.json ormconfig.json 14 | 15 | script: 16 | - npm run lint 17 | - npm run test:cicov -------------------------------------------------------------------------------- /.vscode/README.md: -------------------------------------------------------------------------------- 1 | ### VSCode Debugger 2 | 3 | Hello folks, this folder exists on purpose. 4 | In case you need debug the application, without drop tears, you can use VSCode Debug Tool, with this configuration setted on launch.json. 5 | 6 | Just confirm the configuration of .env file and others things you may like. 7 | 8 | After that, press Play button and insert brakepoints and interect with the debug console whenever necessary. -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "TypeScript Inspector", 9 | "type": "node", 10 | "request": "launch", 11 | "args": ["${workspaceRoot}/src/index.ts"], 12 | "runtimeArgs": ["-r", "ts-node/register"], 13 | "cwd": "${workspaceRoot}", 14 | "protocol": "inspector", 15 | "internalConsoleOptions": "openOnSessionStart", 16 | "outputCapture": "std", 17 | "envFile": "${workspaceFolder}/.env", 18 | "env": { 19 | "TS_NODE_IGNORE": "false" 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.classpath": true, 4 | "**/.project": true, 5 | "**/.settings": true, 6 | "**/.factorypath": true 7 | } 8 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10.17.0-alpine 2 | 3 | # Create app directory 4 | WORKDIR /app 5 | 6 | COPY ./package.json . 7 | 8 | RUN yarn install --production 9 | # RUN npm install 10 | 11 | COPY ./dist/ ./dist/ 12 | COPY ./.env ./.env 13 | COPY ./ormconfig.json . 14 | 15 | ENV NODE_ENV production 16 | 17 | EXPOSE 3333 18 | 19 | CMD [ "node", "dist/src/index.js" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Keven Leone 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm run start:server 2 | worker: npm run start:worker 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |
 3 | 
 4 |    ____                 _                   _       _   
 5 |   / ___|_ __ __ _ _ __ | |__  ___  ___ _ __(_)_ __ | |_ 
 6 |  | |  _| '__/ _` | '_ \| '_ \/ __|/ __| '__| | '_ \| __|
 7 |  | |_| | | | (_| | |_) | | | \__ | (__| |  | | |_) | |_ 
 8 |   \____|_|  \__,_| .__/|_| |_|___/\___|_|  |_| .__/ \__|
 9 |                  |_|                         |_|        
10 | 
11 | 12 |

A simple GraphQL boilerplate using TypeScript and TypeORM

13 | 14 |

15 | Coverage Status 16 | 17 | 18 | PRs Welcome 19 | 20 | 21 | License MIT 22 | 23 |

24 | 25 | 26 | 27 | ## :bulb: Introduction 28 | 29 | Graphscript is a simple boilerplate using the most recents technologies of Javascript, made with TypeScript. Creating a layer of service that possibility the management of Middlewares and Schemas. 30 | 31 | ## :house: Getting started 32 | 33 | 1. Clone this repo using: `https://github.com/kevenleone/graphscript.git` 34 | 2. Install the packages using your preference package manager ( yarn install or npm install ) 35 | 3. Rename the files 36 | 1. ormconfig.example.json to ormconfig.json 37 | 2. .env.example to .env 38 | 4. Inside ormconfig.json configure with your preferences, the database, you can check the TypeORM Docs and select the best database option. https://typeorm.io/#/ 39 | 5. Run ( yarn dev or npm run dev ) and open on browser: http://localhost:3333/graphql 40 | 41 | ## :tada: Features 42 | 43 | Graphscript implement the following features 44 | 45 | - :zap: **Apollo GraphQL** - A GraphQL Server for Express Library 46 | - :books: **TypeORM** - ORM for TypeScript and JavaScript 47 | - :whale: **Docker** - To setup all the environment needs, ready to deploy 48 | - :clipboard: **Winston** - A logger for just about everything. 49 | - :passport_control: **JWT** - For protection of GraphQL Content 50 | 51 | ## :zap: Commands 52 | - `npm run dev` - start the playground with hot-reload at `http://localhost:3333/playground` 53 | - `npm start` - start the playground pointing for dist index at `http://localhost:3333/playground` 54 | - `npm run build` - Builds the project: Typescript to Javascript 55 | 56 | ## :handshake: **Contributing** 57 | If you liked the project and want to cooperate feel free to fork this repository and send Pull Requests. 58 | 59 | All kinds of contributions are very welcome and appreciated 60 | 61 | - ⭐️ Star the project 62 | - 🐛 Find and report issues 63 | - 📥 Submit PRs to help solve issues or add features 64 | 65 | ## :package: Deployment 66 | 67 | This project comes with a `app.json` file for heroku, that can be used to create an app on heroku from a GitHub repository. 68 | 69 | After setting up the project, you can init a repository and push it on GitHub. If your repository is public, you can use the following button: 70 | 71 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/kevenleone/graphscript.git) 72 | 73 | 74 | If you are in a private repository, access the following link replacing `$YOUR_REPOSITORY_LINK$` with your repository link. 75 | 76 | - `https://heroku.com/deploy?template=$YOUR_REPOSITORY_LINK$` 77 | 78 | ## :book: License 79 | MIT license, Copyright (c) 2020 Keven Leone. 80 | -------------------------------------------------------------------------------- /__tests__/middlewares/isAuth.spec.ts: -------------------------------------------------------------------------------- 1 | import { isAuth } from '@middlewares/isAuth'; 2 | import { constants } from '@utils/globalMethods'; 3 | 4 | import { ctx, next } from '../test.utils'; 5 | const { AUTH_INVALID_TOKEN, AUTH_NOT_FOUND } = constants; 6 | 7 | describe('Auth Middleware', () => { 8 | it(`Should pass through Auth Middleware without authorization and get ${AUTH_NOT_FOUND}`, async () => { 9 | isAuth(ctx, next).catch((err) => 10 | expect(err.message).toStrictEqual(AUTH_NOT_FOUND), 11 | ); 12 | }); 13 | 14 | it(`Should pass through Auth Middleware without authorization and get ${AUTH_INVALID_TOKEN}`, async () => { 15 | ctx.context.req.headers.authorization = 'ey...'; 16 | isAuth(ctx, next).catch((err) => 17 | expect(err.message).toStrictEqual(AUTH_INVALID_TOKEN), 18 | ); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /__tests__/src/resolvers/config/config.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import PKG from '../../../../package.json'; 4 | import defaults from '../../../../src/config/defaults'; 5 | import { Configuration } from '../../../../src/interfaces'; 6 | import { ConfigResolver } from '../../../../src/resolvers/config/config.resolver'; 7 | 8 | const Config = new ConfigResolver(); 9 | 10 | describe('Config Resolver', () => { 11 | it('Should validate Config Data', () => { 12 | const config: Configuration = { 13 | APP_NAME: defaults.APP_NAME, 14 | APP_VERSION: PKG.version, 15 | }; 16 | expect(Config.getConfig()).toStrictEqual(config); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /__tests__/src/resolvers/user/user.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { request } from 'graphql-request'; 4 | import { Connection } from 'typeorm'; 5 | 6 | import { User } from '../../../../src/entity/User'; 7 | import { isAuth } from '../../../../src/middlewares/isAuth'; 8 | import { UserResolver } from '../../../../src/resolvers/user/user.resolver'; 9 | import { constants, defaults } from '../../../../src/utils/globalMethods'; 10 | import Queue from '../../../../src/utils/Queue'; 11 | import { createTypeormConn } from '../../../../src/utils/typeORMConn'; 12 | import { ctx, next } from '../../../test.utils'; 13 | 14 | const { 15 | JOB_REGISTRATION_MAILER, 16 | USER_NOT_FOUND, 17 | USER_PASSWORD_INVALID, 18 | } = constants; 19 | const { createUser, forgotPassword, login } = new UserResolver(); 20 | const INVALID_EMAIL = 'invalid@email.com'; 21 | const user: any = {}; 22 | 23 | let ormConn: Connection; 24 | let token: string | Error = ''; 25 | 26 | describe('Should test user resolver', () => { 27 | beforeAll(async () => { 28 | ormConn = await createTypeormConn(); 29 | }); 30 | 31 | afterAll(async (done) => { 32 | await ormConn.close(); 33 | jest.restoreAllMocks(); 34 | done(); 35 | }); 36 | 37 | it('Get all users and length equal 0', async () => { 38 | const users = await User.find(); 39 | expect(users.length).toBe(0); 40 | }); 41 | 42 | it('Should create an user with HTTP', async () => { 43 | const name = `user${(Math.random() * 1000).toFixed(0)}`; 44 | user.email = `${name}@gmail.com`; 45 | user.firstName = 'name'; 46 | user.lastName = 'leone'; 47 | user.password = '123456'; 48 | const mutation = ` 49 | mutation { 50 | createUser(data: { 51 | firstName: "${user.firstName}", 52 | lastName: "${user.lastName}", 53 | email: "${user.email}" 54 | password: "${user.password}", 55 | }) { 56 | firstName, 57 | lastName, 58 | fullName 59 | } 60 | } 61 | `; 62 | 63 | const response = await request(defaults.TEST_HOST, mutation); 64 | expect(response).toEqual({ 65 | createUser: { 66 | firstName: user.firstName, 67 | fullName: `${user.firstName} ${user.lastName}`, 68 | lastName: user.lastName, 69 | }, 70 | }); 71 | const users = await User.find({ where: { email: user.email } }); 72 | expect(users.length).toBe(1); 73 | }); 74 | 75 | it('Should create an user with success', async () => { 76 | const name = `user${(Math.random() * 1000).toFixed(0)}`; 77 | const _user = { 78 | email: `${name}@gmail.com`, 79 | firstName: name, 80 | lastName: 'leone', 81 | password: '123456', 82 | }; 83 | const spy = jest.spyOn(Queue, 'add').mockImplementation(() => ({})); 84 | const response = await createUser(_user); 85 | expect(spy).toBeCalledWith(JOB_REGISTRATION_MAILER, { 86 | email: _user.email, 87 | firstName: _user.firstName, 88 | }); 89 | spy.mockRestore(); 90 | expect(response).toBeTruthy(); 91 | const users = await User.find(); 92 | expect(users.length).toBe(2); 93 | }); 94 | 95 | it('Should create an user and return the same user', async () => { 96 | const response = await createUser(user); 97 | expect(response).toBeTruthy(); 98 | const users = await User.find(); 99 | expect(users.length).toBe(2); 100 | }); 101 | 102 | it('Should login and get token', async () => { 103 | token = await login(user.email, user.password); 104 | expect(token).toBeTruthy(); 105 | }); 106 | 107 | it(`Should pass through Auth Middleware`, async () => { 108 | ctx.context.req.headers.authorization = `Bearer ${token}`; 109 | isAuth(ctx, next).then((response) => expect(response).toStrictEqual({})); 110 | }); 111 | 112 | it('Should login with invalid email', async () => { 113 | const token = await login(INVALID_EMAIL, user.password); 114 | expect(token).toStrictEqual(new Error(USER_NOT_FOUND)); 115 | }); 116 | 117 | it('Should login with invalid password', async () => { 118 | const token = await login(user.email, 'errrrrr'); 119 | expect(token).toStrictEqual(new Error(USER_PASSWORD_INVALID)); 120 | }); 121 | 122 | it('Should forgot the password and add event to queue', async () => { 123 | const spy = jest.spyOn(Queue, 'add').mockImplementation(() => ({})); 124 | const response = await forgotPassword(user.email); 125 | expect(spy).toBeCalled(); 126 | spy.mockRestore(); 127 | expect(response).toBeTruthy(); 128 | }); 129 | 130 | it('Should try forgot the password with invalid email', async () => { 131 | const response = await forgotPassword(INVALID_EMAIL); 132 | expect(response).toBeFalsy(); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /__tests__/src/utils/Queue.spec.ts: -------------------------------------------------------------------------------- 1 | import constants from '../../../src/utils/contants'; 2 | import Queue from '../../../src/utils/Queue'; 3 | 4 | const { JOB_NOT_FOUND } = constants; 5 | 6 | describe('Test Queue Utils', () => { 7 | it('Try to add on queue a job that not exists and return nothing', () => { 8 | const JOB = 'REMOVE_ALL_USERS'; 9 | const q = Queue.add(JOB, {}); 10 | expect(q).toStrictEqual(new Error(JOB_NOT_FOUND(JOB))); 11 | }); 12 | 13 | it('Process Queue', () => { 14 | Queue.process(); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /__tests__/src/utils/globalMethods.spec.ts: -------------------------------------------------------------------------------- 1 | import { Pagination } from '../../../src/interfaces'; 2 | import { 3 | getGraphqlOperation, 4 | normalizePagination, 5 | } from '../../../src/utils/globalMethods'; 6 | 7 | const page: Pagination = { 8 | pageIndex: 1, 9 | pageSize: 1, 10 | skip: 1, 11 | take: 1, 12 | }; 13 | 14 | describe('Should works', () => { 15 | it('Get Pagination basic', () => { 16 | const getPage = normalizePagination(page); 17 | expect(getPage).toStrictEqual({ ...page, skip: 0 }); 18 | }); 19 | 20 | it('Get Pagination dynamic', () => { 21 | page.pageIndex = 2; 22 | page.pageSize = 10; 23 | const getPage = normalizePagination(page); 24 | expect(getPage).toStrictEqual({ ...page, skip: 10, take: 10 }); 25 | }); 26 | 27 | it('Get Query Operation by Query', () => { 28 | const query = `{ 29 | getAllUser { 30 | id 31 | firstName 32 | lastName 33 | } 34 | }`; 35 | const operation = getGraphqlOperation(query); 36 | expect(operation).toBe('[query getAllUser]'); 37 | }); 38 | 39 | it('Get Query Operation by Mutation', () => { 40 | const mutation = `mutation { 41 | createMultiUser(data: {firstName: "Keven", lastName: "Leone", email: "keven.santos.sz@gmail.com", password: "123456"}) { 42 | id 43 | firstName 44 | } 45 | } 46 | `; 47 | const operation = getGraphqlOperation(mutation); 48 | expect(operation).toBe('[mutation createMultiUser]'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /__tests__/test.utils.ts: -------------------------------------------------------------------------------- 1 | const next: any = () => ({}); 2 | 3 | const ctx: any = { 4 | context: { 5 | req: { 6 | body: { 7 | query: ` 8 | { 9 | getAllUser { 10 | id 11 | firstName 12 | lastName 13 | } 14 | }`, 15 | }, 16 | headers: { 17 | authorization: '', 18 | }, 19 | }, 20 | res: {}, 21 | }, 22 | }; 23 | 24 | export { next, ctx }; 25 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphscript", 3 | "description": "Graphscript boilerplate", 4 | "keywords": [ 5 | "GraphQL", 6 | "NodeJS", 7 | "Typescript", 8 | "Javascript", 9 | "Boilerplate" 10 | ], 11 | "env": { 12 | "AUTH_MIDDLEWARE_ENABLED": { 13 | "description": "If true the authorization header is required to access some data", 14 | "value": "true" 15 | }, 16 | "JWT_SECRET": { 17 | "description": "JWT Secret key", 18 | "generator": "secret" 19 | }, 20 | "MAIL_FROM": { 21 | "description": "Mailer config for mailer sender", 22 | "value": "Fred Foo " 23 | } 24 | }, 25 | "formation": { 26 | "web": { 27 | "quantity": 1, 28 | "size": "free" 29 | }, 30 | "worker": { 31 | "quantity": 1, 32 | "size": "free" 33 | } 34 | }, 35 | "addons": [ 36 | { 37 | "plan": "heroku-postgresql:hobby-dev", 38 | "as": "POSTGRES" 39 | }, 40 | { 41 | "plan": "heroku-redis:hobby-dev", 42 | "as": "REDIS" 43 | }, 44 | { 45 | "plan": "sendgrid:starter" 46 | } 47 | ], 48 | "buildpacks": [ 49 | { 50 | "url": "heroku/nodejs" 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | postgres: 4 | image: postgres:latest 5 | container_name: postgres 6 | expose: 7 | - 5432 8 | environment: 9 | POSTGRES_USER: postgres 10 | POSTGRES_PASSWORD: root 11 | POSTGRES_DB: graphscript 12 | volumes: 13 | - './data:/data/db' 14 | redis: 15 | image: redis:latest 16 | container_name: redis 17 | ports: 18 | - '6379:6379' 19 | api: 20 | build: . 21 | container_name: graphscript 22 | command: npm run dev 23 | ports: 24 | - '3333:3333' 25 | volumes: 26 | - .:/app 27 | depends_on: 28 | - postgres 29 | - redis 30 | links: 31 | - postgres 32 | - redis 33 | 34 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | modulePaths: ['src'], 4 | collectCoverage: true, 5 | testEnvironment: 'node', 6 | testMatch: ['/**/*.spec.ts'], 7 | collectCoverageFrom: ['src/**/*.ts'], 8 | 9 | coverageThreshold: { 10 | global: { 11 | statements: 10, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": ["src"], 3 | "ext": "ts", 4 | "exec": "ts-node ./src/index.ts" 5 | } -------------------------------------------------------------------------------- /ormconfig.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "default", 4 | "type": "postgres", 5 | "host": "localhost", 6 | "port": 5432, 7 | "username": "postgres", 8 | "password": "root", 9 | "database": "graphscript", 10 | "synchronize": true, 11 | "logging": false, 12 | "entities": ["./src/entity/*.ts"] 13 | }, 14 | { 15 | "name": "test", 16 | "type": "sqlite", 17 | "database": "graphscript.sqlite", 18 | "dropSchema": true, 19 | "synchronize": true, 20 | "logging": true, 21 | "entities": ["./src/entity/*.ts"] 22 | }, 23 | { 24 | "name": "production", 25 | "type": "postgres", 26 | "host": "localhost", 27 | "port": 5432, 28 | "username": "postgres", 29 | "password": "root", 30 | "database": "graphscript", 31 | "synchronize": true, 32 | "logging": false, 33 | "entities": ["./src/entity/*.js"] 34 | } 35 | ] 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphscript", 3 | "version": "2.1.0", 4 | "main": "src/index.ts", 5 | "repository": "https://github.com/kevenleone/graphscript.git", 6 | "author": "Keven ", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "npm-run-all -p dev:*", 10 | "dev:server": "nodemon src/index.ts", 11 | "dev:worker": "nodemon --exec ts-node src/worker.ts", 12 | "start:server": "node dist/src/index.js", 13 | "start:worker": "node dist/src/worker.js", 14 | "build": "sh scripts/build.pkg.sh", 15 | "test:server": "NODE_ENV=test ts-node src/index.ts", 16 | "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", 17 | "test:coverage": "NODE_ENV=test jest --coverage", 18 | "test:coveralls": "NODE_ENV=test jest --forceExit --detectOpenHandles --coverage --coverageReporters=text-lcov | coveralls", 19 | "lint": "eslint src/**/*.ts", 20 | "lint:fix": "eslint '*/**/*.{js,ts,tsx}' --quiet --fix" 21 | }, 22 | "devDependencies": { 23 | "@types/bcryptjs": "^2.4.2", 24 | "@types/express": "^4.17.2", 25 | "@types/graphql": "^14.5.0", 26 | "@types/jest": "^25.1.0", 27 | "@types/jsonwebtoken": "^8.3.5", 28 | "@types/nodemailer": "^6.2.2", 29 | "@types/uuid": "^3.4.6", 30 | "@typescript-eslint/eslint-plugin": "^2.10.0", 31 | "@typescript-eslint/parser": "^2.10.0", 32 | "coveralls": "^3.0.9", 33 | "cross-env": "^7.0.0", 34 | "eslint": "^7.12.0", 35 | "eslint-config-prettier": "^6.14.0", 36 | "eslint-plugin-prettier": "^3.1.1", 37 | "eslint-plugin-simple-import-sort": "^5.0.3", 38 | "eslint-plugin-sort-destructure-keys": "^1.3.5", 39 | "eslint-plugin-sort-keys-fix": "^1.1.1", 40 | "graphql-request": "^1.8.2", 41 | "jest": "^25.1.0", 42 | "nodemon": "^2.0.2", 43 | "npm-run-all": "^4.1.5", 44 | "prettier": "^2.1.2", 45 | "rimraf": "^3.0.0", 46 | "sqlite3": "^4.1.1", 47 | "ts-jest": "^25.1.0", 48 | "typescript": "^4.0.3" 49 | }, 50 | "dependencies": { 51 | "apollo-server-express": "^2.18.2", 52 | "bcryptjs": "^2.4.3", 53 | "bull-board": "^1.0.0-alpha.11", 54 | "bullmq": "^1.10.0", 55 | "class-validator": "^0.11.0", 56 | "dotenv": "^8.2.0", 57 | "express": "^4.17.1", 58 | "graphql": "^15.3.0", 59 | "jsonwebtoken": "^8.5.1", 60 | "nodemailer": "^6.3.1", 61 | "pg": "^8.4.1", 62 | "reflect-metadata": "^0.1.13", 63 | "ts-node": "^9.0.0", 64 | "type-graphql": "^1.1.0", 65 | "typeorm": "^0.2.28", 66 | "winston": "^3.2.1", 67 | "winston-daily-rotate-file": "^4.2.1" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | semi: true, 4 | singleQuote: true, 5 | tabWidth: 2, 6 | trailingComma: 'all', 7 | }; 8 | -------------------------------------------------------------------------------- /scripts/build.pkg.sh: -------------------------------------------------------------------------------- 1 | #!bin/bash 2 | 3 | echo "Deleting Dist Folder and generating new dist folder" 4 | rimraf ./dist && tsc 5 | 6 | 7 | echo "Copying ormconfig and .env files to /dist" 8 | cp ormconfig.json .env dist/ 9 | 10 | echo "Build finished" -------------------------------------------------------------------------------- /src/config/defaults.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | 3 | import CONSTANTS from '../utils/contants'; 4 | config(); 5 | 6 | function normalizeBool( 7 | value: string | undefined, 8 | defaultValue: boolean, 9 | ): boolean { 10 | const expectedBooleans = ['false', 'true']; 11 | if (value && expectedBooleans.indexOf(value) > -1) { 12 | return JSON.parse(value); 13 | } 14 | return defaultValue; 15 | } 16 | 17 | const { 18 | APP_NAME, 19 | AUTH_MIDDLEWARE_ENABLED, 20 | JWT_SECRET, 21 | MAIL_FROM, 22 | MAIL_HOST, 23 | MAIL_PASS, 24 | MAIL_PORT, 25 | MAIL_USER, 26 | NODE_ENV, 27 | POSTGRES_URL, 28 | REDIS_PORT, 29 | REDIS_URL, 30 | RUN_PLAYGROUND, 31 | SENDGRID_PASSWORD, 32 | SENDGRID_USERNAME, 33 | } = process.env; 34 | 35 | export default { 36 | APP_NAME: APP_NAME || 'Graphscript', 37 | AUTH_MIDDLEWARE_ENABLED: normalizeBool(AUTH_MIDDLEWARE_ENABLED, false), 38 | CONSTANTS, 39 | ENVIRONMENT: NODE_ENV || 'development', 40 | JWT_SECRET: JWT_SECRET || 'MY_SECRET_SECRET', 41 | MAIL_FROM: MAIL_FROM || '', 42 | MAIL_HOST: MAIL_HOST || '', 43 | MAIL_PASS: MAIL_PASS || '', 44 | MAIL_PORT: Number(MAIL_PORT) || 527, 45 | MAIL_USER: MAIL_USER || '', 46 | POSTGRES_URL, 47 | REDIS_PORT, 48 | REDIS_URL, 49 | RUN_PLAYGROUND: normalizeBool(RUN_PLAYGROUND, NODE_ENV !== 'production'), 50 | SENDGRID_PASSWORD, 51 | SENDGRID_USERNAME, 52 | TEST_HOST: 'http://localhost:3333/graphql', 53 | }; 54 | -------------------------------------------------------------------------------- /src/entity/User.ts: -------------------------------------------------------------------------------- 1 | import { Field, ID, ObjectType } from 'type-graphql'; 2 | import { BaseEntity, Column, Entity, Index, PrimaryColumn } from 'typeorm'; 3 | 4 | @ObjectType() 5 | @Entity() 6 | export class User extends BaseEntity { 7 | @Field(() => ID) 8 | @PrimaryColumn({ generated: 'uuid' }) 9 | id: string; 10 | 11 | @Field() 12 | @Column() 13 | firstName: string; 14 | 15 | @Field() 16 | @Column() 17 | lastName: string; 18 | 19 | @Field() 20 | fullName(): string { 21 | const { firstName, lastName } = this; 22 | return `${firstName} ${lastName}`; 23 | } 24 | 25 | @Field() 26 | @Column() 27 | @Index({ unique: true }) 28 | email: string; 29 | 30 | @Field() 31 | @Column() 32 | password: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | 3 | import { ApolloServer, Config } from 'apollo-server-express'; 4 | import { router as BullRouter, setQueues } from 'bull-board'; 5 | import { config } from 'dotenv'; 6 | import Express from 'express'; 7 | 8 | import createSchema from './utils/createSchema'; 9 | import { defaults, logger } from './utils/globalMethods'; 10 | import Queue from './utils/Queue'; 11 | import { createTypeormConn } from './utils/typeORMConn'; 12 | 13 | (async (): Promise => { 14 | config(); 15 | const { APP_NAME, ENVIRONMENT, RUN_PLAYGROUND } = defaults; 16 | const { PORT } = process.env; 17 | const httpPort = PORT || 3333; 18 | 19 | logger.debug(`Starting ${APP_NAME} Server`); 20 | 21 | setQueues(Queue.queues.map((queue) => queue.bull)); 22 | 23 | await createTypeormConn(); 24 | 25 | const apolloServerConfig: Config = { 26 | cacheControl: { defaultMaxAge: 30 }, 27 | context: ({ req, res }: any) => ({ req, res }), 28 | formatError: (error) => { 29 | const { message, path } = error; 30 | logger.error( 31 | `Message: ${message.toUpperCase()} / On Path: ${JSON.stringify(path)}`, 32 | ); 33 | return error; 34 | }, 35 | playground: RUN_PLAYGROUND 36 | ? { title: APP_NAME, workspaceName: ENVIRONMENT } 37 | : false, 38 | schema: await createSchema(), 39 | }; 40 | 41 | if (ENVIRONMENT === 'production') { 42 | apolloServerConfig.introspection = true; 43 | } 44 | 45 | const apolloServer = new ApolloServer(apolloServerConfig); 46 | const server = Express(); 47 | 48 | apolloServer.applyMiddleware({ 49 | app: server, 50 | cors: true, 51 | }); 52 | 53 | server.use('/admin/queues', BullRouter); 54 | 55 | server.get('/', (_, res) => 56 | res.json({ message: `${defaults.APP_NAME} is Running` }), 57 | ); 58 | 59 | server.listen(httpPort, () => { 60 | logger.debug(`${APP_NAME} has started | PORT: ${httpPort}`); 61 | }); 62 | })(); 63 | -------------------------------------------------------------------------------- /src/interfaces/Config.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType } from 'type-graphql'; 2 | 3 | @ObjectType() 4 | export class Configuration { 5 | @Field({ nullable: true }) 6 | APP_NAME: string; 7 | 8 | @Field({ nullable: true }) 9 | APP_VERSION: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/interfaces/Mail.ts: -------------------------------------------------------------------------------- 1 | export interface MailOption { 2 | to: string; 3 | content: string; 4 | subject: string; 5 | } 6 | 7 | export interface MailConfig { 8 | host: string; 9 | port: number; 10 | auth: { 11 | user: string; 12 | pass: string; 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /src/interfaces/MiddlewareBaseResolver.ts: -------------------------------------------------------------------------------- 1 | export interface MiddlewareBaseResolver { 2 | create?: Function[]; 3 | update?: Function[]; 4 | delete?: Function[]; 5 | } 6 | -------------------------------------------------------------------------------- /src/interfaces/MyContext.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | export interface MyContext { 4 | req: Request; 5 | res: Response; 6 | } 7 | -------------------------------------------------------------------------------- /src/interfaces/Pagination.ts: -------------------------------------------------------------------------------- 1 | import { Field, InputType } from 'type-graphql'; 2 | 3 | export interface Pagination { 4 | pageIndex?: number; 5 | pageSize?: number; 6 | take?: number; 7 | skip?: number; 8 | } 9 | 10 | @InputType() 11 | export class PaginationQL { 12 | @Field({ nullable: true }) 13 | pageIndex?: number; 14 | 15 | @Field({ nullable: true }) 16 | pageSize?: number; 17 | } 18 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export { MiddlewareBaseResolver } from './MiddlewareBaseResolver'; 2 | export { Pagination, PaginationQL } from './Pagination'; 3 | export { MailConfig, MailOption } from './Mail'; 4 | export { Configuration } from './Config'; 5 | export { MyContext } from './MyContext'; 6 | -------------------------------------------------------------------------------- /src/jobs/Purge.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../entity/User'; 2 | import { constants } from '../utils/globalMethods'; 3 | 4 | const { JOB_PURGE, PRIORITY_HIGH } = constants; 5 | 6 | export default { 7 | active: true, 8 | config: { 9 | priority: PRIORITY_HIGH, 10 | repeat: { 11 | every: 86400000 * 7, // time in milisseconds 12 | }, 13 | }, 14 | async handle(): Promise { 15 | /** 16 | * do something to run repeatedly according to repeat time; 17 | * example, run a report every day and send via-email for subscribed users 18 | */ 19 | await User.findAndCount(); 20 | }, 21 | name: JOB_PURGE, 22 | selfRegister: true, 23 | }; 24 | -------------------------------------------------------------------------------- /src/jobs/Recovery.ts: -------------------------------------------------------------------------------- 1 | import { constants, defaults } from '../utils/globalMethods'; 2 | import Mail from '../utils/Mail'; 3 | 4 | const { APP_NAME, MAIL_FROM } = defaults; 5 | const { JOB_RECOVERY_MAILER } = constants; 6 | 7 | export default { 8 | async handle({ data }: any): Promise { 9 | const { email, name, token } = data; 10 | const config = { 11 | from: `${APP_NAME} Team <${MAIL_FROM}>`, 12 | html: `Hey ${name}, Here's the recovery link: ${token}.`, 13 | subject: 'Account Recovery', 14 | to: `${name} <${email}>`, 15 | }; 16 | await Mail.sendMail(config); 17 | }, 18 | name: JOB_RECOVERY_MAILER, 19 | }; 20 | -------------------------------------------------------------------------------- /src/jobs/RegistrationMailer.ts: -------------------------------------------------------------------------------- 1 | import { constants, defaults } from '../utils/globalMethods'; 2 | import Mail from '../utils/Mail'; 3 | 4 | const { APP_NAME, MAIL_FROM } = defaults; 5 | const { JOB_REGISTRATION_MAILER } = constants; 6 | 7 | export default { 8 | async handle({ data }: any): Promise { 9 | const { email, firstName } = data; 10 | await Mail.sendMail({ 11 | from: MAIL_FROM, 12 | html: `Hello, ${firstName}. you have been registered on ${APP_NAME}`, 13 | subject: `${APP_NAME} Sign Up`, 14 | to: `${firstName} <${email}>`, 15 | }); 16 | }, 17 | name: JOB_REGISTRATION_MAILER, 18 | }; 19 | -------------------------------------------------------------------------------- /src/jobs/index.ts: -------------------------------------------------------------------------------- 1 | export { default as RegistrationMailer } from './RegistrationMailer'; 2 | export { default as Recovery } from './Recovery'; 3 | export { default as Purge } from './Purge'; 4 | -------------------------------------------------------------------------------- /src/middlewares/isAuth.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | import { MiddlewareFn } from 'type-graphql'; 3 | import { promisify } from 'util'; 4 | 5 | import { MyContext } from '../interfaces'; 6 | import { defaults, getGraphqlOperation, logger } from '../utils/globalMethods'; 7 | 8 | export const isAuth: MiddlewareFn = async (ctx, next) => { 9 | const { 10 | AUTH_MIDDLEWARE_ENABLED, 11 | CONSTANTS: { AUTH_INVALID_TOKEN, AUTH_NOT_FOUND }, 12 | JWT_SECRET, 13 | } = defaults; 14 | if (AUTH_MIDDLEWARE_ENABLED) { 15 | const { 16 | body, 17 | headers: { authorization }, 18 | } = ctx.context.req; 19 | const operationName = getGraphqlOperation(body.query); 20 | if (authorization) { 21 | const token: string = authorization.split(' ').pop() || ''; 22 | try { 23 | const user: any = await promisify(jwt.verify)(token, JWT_SECRET); 24 | ctx.context.req.headers.loggedUser = user; 25 | logger.debug( 26 | `${user.firstName} is running a graphQL request to ${operationName}`, 27 | ); 28 | return next(); 29 | } catch (e) { 30 | throw new Error(AUTH_INVALID_TOKEN); 31 | } 32 | } 33 | throw new Error(AUTH_NOT_FOUND); 34 | } 35 | return next(); 36 | }; 37 | -------------------------------------------------------------------------------- /src/resolvers/config/config.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Query, Resolver } from 'type-graphql'; 2 | 3 | import PKG from '../../../package.json'; 4 | import defaults from '../../config/defaults'; 5 | import { Configuration } from '../../interfaces'; 6 | 7 | @Resolver() 8 | export class ConfigResolver { 9 | @Query(() => Configuration, { name: `getConfig` }) 10 | getConfig(): Configuration { 11 | const { version: APP_VERSION } = PKG; 12 | const { APP_NAME } = defaults; 13 | return { 14 | APP_NAME, 15 | APP_VERSION, 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/resolvers/user/Inputs.ts: -------------------------------------------------------------------------------- 1 | import { MinLength } from 'class-validator'; 2 | import { Field, InputType } from 'type-graphql'; 3 | 4 | @InputType() 5 | class UserBaseInput { 6 | @Field() 7 | firstName: string; 8 | 9 | @Field() 10 | lastName: string; 11 | 12 | @Field() 13 | email: string; 14 | 15 | @Field() 16 | @MinLength(5) 17 | password: string; 18 | } 19 | 20 | @InputType() 21 | export class CreateUserInput extends UserBaseInput {} 22 | 23 | @InputType() 24 | export class UpdateUserInput extends UserBaseInput {} 25 | 26 | @InputType() 27 | export class FilterUserInput { 28 | @Field({ nullable: true }) 29 | firstName?: string; 30 | 31 | @Field({ nullable: true }) 32 | email?: string; 33 | } 34 | -------------------------------------------------------------------------------- /src/resolvers/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | import jsonwebtoken from 'jsonwebtoken'; 3 | import { Arg, Mutation, Resolver } from 'type-graphql'; 4 | import { promisify } from 'util'; 5 | import { v4 } from 'uuid'; 6 | 7 | import { User } from '../../entity/User'; 8 | import { createBaseResolver } from '../../utils/createBaseResolver'; 9 | import { constants, defaults, logger } from '../../utils/globalMethods'; 10 | import Queue from '../../utils/Queue'; 11 | import { CreateUserInput, FilterUserInput, UpdateUserInput } from './Inputs'; 12 | 13 | const { JOB_RECOVERY_MAILER, JOB_REGISTRATION_MAILER } = constants; 14 | 15 | const Inputs = { 16 | create: CreateUserInput, 17 | filter: FilterUserInput, 18 | update: UpdateUserInput, 19 | }; 20 | 21 | const BaseResolver = createBaseResolver('User', User, User, Inputs); 22 | 23 | @Resolver(User) 24 | export class UserResolver extends BaseResolver { 25 | @Mutation(() => User, { name: `createUser` }) 26 | async createUser( 27 | @Arg('data', () => CreateUserInput) data: CreateUserInput, 28 | ): Promise { 29 | const { email, firstName } = data; 30 | let user = await User.findOne({ 31 | where: { 32 | email, 33 | }, 34 | }); 35 | 36 | if (!user) { 37 | const hashedPassword = await bcrypt.hash(data.password, 12); 38 | user = await super.create({ 39 | ...data, 40 | password: hashedPassword, 41 | }); 42 | Queue.add(JOB_REGISTRATION_MAILER, { email, firstName }); 43 | } 44 | return user; 45 | } 46 | 47 | @Mutation(() => String) 48 | async login( 49 | @Arg('email') email: string, 50 | @Arg('password') password: string, 51 | ): Promise { 52 | const { 53 | CONSTANTS: { USER_NOT_FOUND, USER_PASSWORD_INVALID }, 54 | JWT_SECRET, 55 | } = defaults; 56 | const user = await User.findOne({ where: { email } }); 57 | 58 | if (!user) { 59 | return new Error(USER_NOT_FOUND); 60 | } 61 | 62 | const passwordsMatch = await bcrypt.compare(password, user.password); 63 | 64 | if (!passwordsMatch) { 65 | return new Error(USER_PASSWORD_INVALID); 66 | } 67 | 68 | const userData = { ...user, password: '' }; 69 | 70 | try { 71 | const token: any = await promisify(jsonwebtoken.sign)( 72 | userData, 73 | JWT_SECRET, 74 | ); 75 | logger.info(`Token generated for ${email}`); 76 | return token; 77 | } catch (e) { 78 | return new Error(e.message); 79 | } 80 | } 81 | 82 | @Mutation(() => Boolean) 83 | async forgotPassword(@Arg('email') email: string): Promise { 84 | const user = await User.findOne({ where: { email } }); 85 | if (!user) { 86 | return false; 87 | } 88 | Queue.add(JOB_RECOVERY_MAILER, { 89 | email, 90 | firstName: user.firstName, 91 | token: v4(), 92 | }); 93 | return true; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/utils/Mail.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | 3 | import { MailerCredentials } from './globalMethods'; 4 | 5 | export default nodemailer.createTransport(MailerCredentials()); 6 | -------------------------------------------------------------------------------- /src/utils/Queue.ts: -------------------------------------------------------------------------------- 1 | import { JobsOptions, Queue, QueueScheduler, Worker } from 'bullmq'; 2 | 3 | import * as Jobs from '../jobs'; 4 | import { constants, defaults, logger } from '../utils/globalMethods'; 5 | const { REDIS_URL } = defaults; 6 | 7 | const redisOptions = { 8 | host: REDIS_URL, 9 | }; 10 | 11 | const getQueueData = (job: any) => { 12 | const { 13 | active = true, 14 | config, 15 | data, 16 | handle, 17 | name, 18 | selfRegister = false, 19 | }: any = job; 20 | const bull = new Queue(name, { connection: { ...redisOptions } }); 21 | 22 | if (selfRegister && active) { 23 | // eslint-disable-next-line no-new 24 | new QueueScheduler(name); 25 | bull.add(name, data, config); 26 | } 27 | 28 | return { 29 | active, 30 | bull, 31 | handle, 32 | name, 33 | }; 34 | }; 35 | 36 | interface QueueInterface { 37 | active: boolean; 38 | bull: Queue; 39 | handle(): any; 40 | name: string; 41 | } 42 | 43 | const queues: QueueInterface[] = []; 44 | 45 | Object.values(Jobs).map((job) => { 46 | if (Array.isArray(job)) { 47 | return job.forEach((jobData) => { 48 | queues.push(getQueueData(jobData)); 49 | }); 50 | } 51 | queues.push(getQueueData(job)); 52 | }); 53 | 54 | export default { 55 | add(name: string, data?: any, options?: JobsOptions): any { 56 | const queue = this.queues.find((queue) => queue.name === name); 57 | if (queue) { 58 | queue.bull.add(name, data, options); 59 | logger.info(`Job: ${name} added to Queue`); 60 | return queue; 61 | } 62 | logger.warn(`Job: [${name}] wasn't found, and nothing was add to Queue`); 63 | return new Error(constants.JOB_NOT_FOUND(name)); 64 | }, 65 | process(): void { 66 | logger.debug('Queue Process initialized'); 67 | 68 | for (const queue of this.queues) { 69 | const { active, name } = queue; 70 | if (active) { 71 | const worker = new Worker(name, queue.handle); 72 | 73 | worker.on('completed', () => { 74 | logger.info(`[${name}] | [COMPLETED]`); 75 | }); 76 | 77 | worker.on('failed', (_, err) => { 78 | logger.error(`[${name}] | [FAILED] -> ${err.message}`); 79 | }); 80 | } 81 | } 82 | }, 83 | queues, 84 | }; 85 | -------------------------------------------------------------------------------- /src/utils/contants.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | AUTH_INVALID_TOKEN: 'Token is invalid', 3 | 4 | AUTH_NOT_FOUND: 'Authorization Header not found', 5 | JOB_NOT_FOUND: (name: string): string => 6 | `JOB ${name.toUpperCase()} NOT FOUND`, 7 | JOB_PURGE: 'JOB_PURGE', 8 | 9 | JOB_RECOVERY_MAILER: 'JOB_RECOVERY_MAILER', 10 | JOB_REGISTRATION_MAILER: 'JOB_REGISTRATION_MAILER', 11 | PRIORITY_CRITICAL: 1, 12 | 13 | PRIORITY_HIGH: 2, 14 | PRIORITY_LOW: 3, 15 | USER_NOT_FOUND: 'User not found', 16 | USER_PASSWORD_INVALID: 'Password is wrong, try again', 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/createBaseResolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Arg, 3 | ClassType, 4 | Mutation, 5 | Query, 6 | Resolver, 7 | UseMiddleware, 8 | } from 'type-graphql'; 9 | 10 | import { MiddlewareBaseResolver, PaginationQL } from '../interfaces'; 11 | import { isAuth } from '../middlewares/isAuth'; 12 | import { execMiddleware, normalizePagination } from './globalMethods'; 13 | 14 | /** 15 | * @param suffix Suffix is used on queryNames, example suffix: getAllUser 16 | * @param entity TypeORM Entity 17 | * @param inputTypes object with create, update and optionally update inputTypes 18 | * @param returnType return classType 19 | * @param middlewares optional middlewares to be applied in defaults functions 20 | */ 21 | export function createBaseResolver( 22 | suffix: string, 23 | entity: any, 24 | returnType: classType, 25 | inputTypes: { create: classType; update: classType; filter?: classType }, 26 | middlewares?: MiddlewareBaseResolver, 27 | ): any { 28 | @Resolver({ isAbstract: true }) 29 | abstract class BaseResolver { 30 | @UseMiddleware(isAuth) 31 | @Query(() => [returnType], { name: `getAll${suffix}` }) 32 | async getAll(): Promise { 33 | return entity.find(); 34 | } 35 | 36 | @UseMiddleware(isAuth) 37 | @Query(() => [returnType], { name: `getAll${suffix}Filter` }) 38 | async getAllFiltered( 39 | @Arg('data', () => inputTypes.filter || inputTypes.create) data: any, 40 | ): Promise { 41 | return entity.find({ where: data }); 42 | } 43 | 44 | @UseMiddleware(isAuth) 45 | @Query(() => [returnType], { name: `getAll${suffix}Paginate` }) 46 | async getAllPagination( 47 | @Arg('data', () => PaginationQL) data: PaginationQL, 48 | ): Promise { 49 | const { skip, take } = normalizePagination(data); 50 | return entity.find({ skip, take }); 51 | } 52 | 53 | @UseMiddleware(isAuth) 54 | @Query(() => returnType, { name: `get${suffix}` }) 55 | async get(@Arg('id', () => String) id: string): Promise { 56 | const content = await entity.findOne(id); 57 | if (!content) { 58 | throw new Error(`${suffix} not found`); 59 | } 60 | return content; 61 | } 62 | 63 | @UseMiddleware(isAuth) 64 | @Mutation(() => returnType, { name: `create${suffix}` }) 65 | async create( 66 | @Arg('data', () => inputTypes.create) data: any, 67 | ): Promise { 68 | if (middlewares && middlewares.create) { 69 | await execMiddleware(entity, data, ...middlewares.create); 70 | } 71 | return entity.create(data).save(); 72 | } 73 | 74 | @UseMiddleware(isAuth) 75 | @Mutation(() => returnType, { name: `updateBy${suffix}ID` }) 76 | async updateByID( 77 | @Arg('data', () => inputTypes.update) data: any, 78 | @Arg('id') id: string, 79 | ): Promise { 80 | const entityData = await this.get(id); 81 | return this.update(data, entityData); 82 | } 83 | 84 | @UseMiddleware(isAuth) 85 | @Mutation(() => [returnType], { name: `createMulti${suffix}` }) 86 | async createMulti( 87 | @Arg('data', () => [inputTypes.create]) data: any[], 88 | ): Promise { 89 | const promises = data.map((obj) => entity.create(obj).save()); 90 | const insertedData = await Promise.all(promises); 91 | return insertedData; 92 | } 93 | 94 | @UseMiddleware(isAuth) 95 | @Mutation(() => Boolean, { name: `deleteBy${suffix}ID` }) 96 | async deleteByID(@Arg('id', () => String) id: string): Promise { 97 | if (middlewares && middlewares.delete) { 98 | await execMiddleware(entity, id, ...middlewares.delete); 99 | } 100 | 101 | const _entity = await this.get(id); 102 | if (!_entity) { 103 | throw new Error(`No data found on Entity: ${suffix}, ID: ${id}`); 104 | } 105 | const data = await entity.remove(_entity); 106 | return !!data; 107 | } 108 | 109 | async update(data: any, entityData: any): Promise { 110 | for (const field in data) { 111 | entityData[field] = data[field]; 112 | } 113 | return entity.save(entityData); 114 | } 115 | } 116 | 117 | return BaseResolver; 118 | } 119 | -------------------------------------------------------------------------------- /src/utils/createSchema.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema } from 'type-graphql'; 2 | 3 | const createSchema = (): Promise => 4 | buildSchema({ 5 | authChecker: ({ context: { req } }) => !!req.context.req.headers.loggedUser, 6 | resolvers: [`${__dirname}/../resolvers/**/*.resolver.{ts,js}`], 7 | }); 8 | 9 | export default createSchema; 10 | -------------------------------------------------------------------------------- /src/utils/globalMethods.ts: -------------------------------------------------------------------------------- 1 | import { gql } from 'apollo-server-express'; 2 | import { EntityOptions } from 'typeorm'; 3 | 4 | import Defaults from '../config/defaults'; 5 | import { MailConfig, Pagination } from '../interfaces'; 6 | import Constants from '../utils/contants'; 7 | import Logger from '../utils/logger'; 8 | 9 | export const constants = Constants; 10 | export const defaults = Defaults; 11 | export const logger = Logger; 12 | 13 | /** 14 | * @description Return the mailer credentials 15 | * @returns MailConfig 16 | */ 17 | 18 | export function MailerCredentials(): MailConfig { 19 | const { MAIL_HOST, MAIL_PASS, MAIL_PORT, MAIL_USER } = defaults; 20 | return { 21 | auth: { 22 | pass: MAIL_PASS, 23 | user: MAIL_USER, 24 | }, 25 | host: MAIL_HOST, 26 | port: MAIL_PORT, 27 | }; 28 | } 29 | 30 | /** 31 | * 32 | * @param graphqlQuery GQL Request 33 | * @returns A string with the operation name 34 | */ 35 | 36 | export function getGraphqlOperation(graphqlQuery: any): string { 37 | try { 38 | const GQL = gql` 39 | ${graphqlQuery} 40 | `; 41 | const operations = GQL.definitions.map( 42 | (query: any) => 43 | `${query.operation} ${ 44 | query.name 45 | ? query.name.value 46 | : query.selectionSet.selections[0].name.value 47 | }`, 48 | ); 49 | return `[${operations.join(', ')}]`; 50 | } catch (e) { 51 | logger.error(`Error in getGraphqlOperation, reason: ${e.message}`); 52 | return 'Unknown'; 53 | } 54 | } 55 | 56 | /** 57 | * 58 | * @param pagination Pagination Object 59 | * @param defaultSize How many items will be displayed, default = 20 60 | */ 61 | 62 | export function normalizePagination( 63 | pagination: Pagination, 64 | defaultSize = 20, 65 | ): Pagination { 66 | const pageSize = pagination.pageSize || defaultSize; 67 | const pageIndex = pagination.pageIndex || 1; 68 | const take = pageSize; 69 | let skip = 0; 70 | 71 | if (pageIndex > 1) { 72 | skip = take * (pageIndex - 1); 73 | } 74 | 75 | return { 76 | pageIndex, 77 | pageSize, 78 | skip, 79 | take, 80 | }; 81 | } 82 | 83 | export async function execMiddleware( 84 | entity: EntityOptions, 85 | data: any, 86 | ...middlewares: Function[] 87 | ): Promise { 88 | for (const middleware of middlewares) { 89 | await middleware(entity, data); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as winston from 'winston'; 2 | import WinstonDailyRotateFile from 'winston-daily-rotate-file'; 3 | 4 | const logger = winston.createLogger({ 5 | format: winston.format.combine( 6 | winston.format.timestamp(), 7 | winston.format.json(), 8 | winston.format.colorize(), 9 | ), 10 | level: 'debug', 11 | transports: [ 12 | new WinstonDailyRotateFile({ 13 | auditFile: './log/audit.json', 14 | datePattern: 'YYYY-MM-DD', 15 | filename: './log/app-%DATE%.log', 16 | maxFiles: '3d', 17 | maxSize: '10m', 18 | }), 19 | ], 20 | }); 21 | 22 | if (process.env.NODE_ENV !== 'production') { 23 | logger.add( 24 | new winston.transports.Console({ 25 | format: winston.format.simple(), 26 | }), 27 | ); 28 | } 29 | 30 | export default logger; 31 | -------------------------------------------------------------------------------- /src/utils/typeORMConn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Connection, 3 | ConnectionOptions, 4 | createConnection, 5 | getConnectionOptions, 6 | } from 'typeorm'; 7 | 8 | import { defaults, logger } from '../utils/globalMethods'; 9 | const { ENVIRONMENT, POSTGRES_URL } = defaults; 10 | 11 | export const createTypeormConn = async (): Promise => { 12 | logger.debug(`TypeORM Environment: ${ENVIRONMENT}`); 13 | if (ENVIRONMENT === 'production') { 14 | return createConnection({ 15 | entities: ['./src/entity/*.js'], 16 | logging: true, 17 | name: 'default', 18 | ssl: true, 19 | synchronize: true, 20 | type: 'postgres', 21 | url: POSTGRES_URL, 22 | }); 23 | } 24 | const connectionOptions: ConnectionOptions = await getConnectionOptions( 25 | ENVIRONMENT, 26 | ); 27 | return createConnection({ ...connectionOptions, name: 'default' }); 28 | }; 29 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | 3 | import { defaults, logger } from './utils/globalMethods'; 4 | import Queue from './utils/Queue'; 5 | import { createTypeormConn } from './utils/typeORMConn'; 6 | 7 | (async (): Promise => { 8 | config(); 9 | const { APP_NAME } = defaults; 10 | await createTypeormConn(); 11 | 12 | logger.debug(`Starting Worker ${APP_NAME} Server`); 13 | Queue.process(); 14 | })(); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], 6 | "sourceMap": true, 7 | "outDir": "./dist", 8 | "moduleResolution": "node", 9 | "declaration": false, 10 | 11 | "composite": false, 12 | "removeComments": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "strictFunctionTypes": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "noImplicitReturns": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "esModuleInterop": true, 22 | "allowSyntheticDefaultImports": true, 23 | "emitDecoratorMetadata": true, 24 | "experimentalDecorators": true, 25 | "skipLibCheck": true, 26 | "resolveJsonModule": true, 27 | "baseUrl": "./", 28 | "typeRoots": [ "./types", "./node_modules/@types"] 29 | }, 30 | "exclude": ["node_modules"], 31 | "include": ["./src/**/*.tsx", "./src/**/*.ts"], 32 | } 33 | --------------------------------------------------------------------------------