├── .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 |
16 |
17 |
18 |
19 |
20 |
21 |
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 | [](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 |
--------------------------------------------------------------------------------