Repository: benawad/type-graphql-series Branch: master Commit: 768d12cc9e35 Files: 42 Total size: 26.6 KB Directory structure: gitextract__c9f4m58/ ├── .gitignore ├── README.md ├── jest.config.js ├── ormconfig.json ├── package.json ├── src/ │ ├── entity/ │ │ ├── Author.ts │ │ ├── AuthorBook.ts │ │ ├── Book.ts │ │ ├── Product.ts │ │ └── User.ts │ ├── index.ts │ ├── modules/ │ │ ├── author-book/ │ │ │ └── AuthorBookResolver.ts │ │ ├── constants/ │ │ │ └── redisPrefixes.ts │ │ ├── middleware/ │ │ │ ├── isAuth.ts │ │ │ └── logger.ts │ │ ├── shared/ │ │ │ ├── OkMixin.ts │ │ │ └── PasswordInput.ts │ │ ├── user/ │ │ │ ├── ChangePassword.ts │ │ │ ├── ConfirmUser.ts │ │ │ ├── CreateUser.ts │ │ │ ├── ForgotPassword.ts │ │ │ ├── Login.ts │ │ │ ├── Logout.ts │ │ │ ├── Me.ts │ │ │ ├── ProfilePicture.ts │ │ │ ├── Register.ts │ │ │ ├── changePassword/ │ │ │ │ └── ChangePasswordInput.ts │ │ │ ├── me/ │ │ │ │ └── Me.test.ts │ │ │ └── register/ │ │ │ ├── Register.test.ts │ │ │ ├── RegisterInput.ts │ │ │ └── isEmailAlreadyExist.ts │ │ └── utils/ │ │ ├── createConfirmationUrl.ts │ │ └── sendEmail.ts │ ├── redis.ts │ ├── test-utils/ │ │ ├── gCall.ts │ │ ├── setup.ts │ │ └── testConn.ts │ ├── types/ │ │ ├── MyContext.ts │ │ └── Upload.ts │ └── utils/ │ ├── authorsLoader.ts │ └── createSchema.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules images *.logs ================================================ FILE: README.md ================================================ # type-graphql-series Code for: https://www.youtube.com/playlist?list=PLN3n1USn4xlma1bBu3Tloe4NyYn9Ko8Gs # Installing 1. Clone and install dependecies ``` git clone https://github.com/benawad/type-graphql-series.git cd type-graphql-series yarn ``` 2. Make sure you have PostgreSQL running on your computer with a database called `typegraphql-example` and a user who has access to that database with the username `postgres` and password `postgres` * Mac: https://www.codementor.io/engineerapart/getting-started-with-postgresql-on-mac-osx-are8jcopb * Windows: https://www.guru99.com/download-install-postgresql.html * Docker: https://www.youtube.com/watch?v=G3gnMSyX-XM * Linux: you know what you're doing * How to create a user: https://medium.com/coding-blocks/creating-user-database-and-adding-access-on-postgresql-8bfcd2f4a91e 3. Make sure you have Redis running on your computer * Mac: https://medium.com/@petehouston/install-and-config-redis-on-mac-os-x-via-homebrew-eb8df9a4f298 * Windows: https://redislabs.com/blog/redis-on-windows-10/ * Linux: you know what you're doing 4. Start the server ``` yarn start ``` To verified it worked, you can go to http://localhost:4000 If you need any help setting this up feel free to message me on Discord: https://discord.gg/Vehs99V ================================================ FILE: jest.config.js ================================================ module.exports = { preset: 'ts-jest', testEnvironment: 'node', }; ================================================ FILE: ormconfig.json ================================================ { "name": "default", "type": "postgres", "host": "localhost", "port": 5432, "username": "postgres", "password": "postgres", "database": "typegraphql-example", "synchronize": true, "logging": true, "entities": ["src/entity/*.*"] } ================================================ FILE: package.json ================================================ { "name": "type-graphql-series", "version": "1.0.0", "main": "index.js", "license": "MIT", "dependencies": { "apollo-server-express": "^2.3.1", "bcryptjs": "^2.4.3", "class-validator": "^0.9.1", "connect-redis": "^3.4.0", "cors": "^2.8.5", "dataloader": "^1.4.0", "express": "^4.16.4", "express-session": "^1.15.6", "graphql": "^14.0.2", "graphql-query-complexity": "^0.2.2", "ioredis": "^4.3.0", "nodemailer": "^5.1.1", "pg": "^7.7.1", "reflect-metadata": "^0.1.12", "type-graphql": "^0.16.0", "typeorm": "^0.2.11", "uuid": "^3.3.2" }, "devDependencies": { "@types/bcryptjs": "^2.4.2", "@types/connect-redis": "^0.0.8", "@types/cors": "^2.8.4", "@types/express": "^4.16.0", "@types/express-session": "^1.15.11", "@types/faker": "^4.1.4", "@types/graphql": "^14.0.4", "@types/ioredis": "^4.0.4", "@types/jest": "^23.3.12", "@types/node": "^10.12.18", "@types/nodemailer": "^4.6.5", "@types/uuid": "^3.4.4", "faker": "^4.1.0", "jest": "^23.6.0", "nodemon": "^1.18.9", "ts-jest": "^23.10.5", "ts-node": "^7.0.1", "ts-node-dev": "^1.0.0-pre.32", "typescript": "^3.2.2" }, "scripts": { "start": "ts-node-dev --respawn src/index.ts", "db:setup": "ts-node ./src/test-utils/setup.ts", "test": "npm run db:setup && jest" } } ================================================ FILE: src/entity/Author.ts ================================================ import { Field, ID, ObjectType } from "type-graphql"; import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { AuthorBook } from "./AuthorBook"; @ObjectType() @Entity() export class Author extends BaseEntity { @Field(() => ID) @PrimaryGeneratedColumn() id: number; @Field() @Column() name: string; @OneToMany(() => AuthorBook, ab => ab.author) bookConnection: Promise; } ================================================ FILE: src/entity/AuthorBook.ts ================================================ import { BaseEntity, Entity, JoinColumn, ManyToOne, PrimaryColumn } from "typeorm"; import { Author } from "./Author"; import { Book } from "./Book"; @Entity() export class AuthorBook extends BaseEntity { @PrimaryColumn() authorId: number; @PrimaryColumn() bookId: number; @ManyToOne(() => Author, author => author.bookConnection, { primary: true }) @JoinColumn({ name: "authorId" }) author: Promise; @ManyToOne(() => Book, book => book.authorConnection, { primary: true }) @JoinColumn({ name: "bookId" }) book: Promise; } ================================================ FILE: src/entity/Book.ts ================================================ import { Ctx, Field, ID, ObjectType } from "type-graphql"; import { BaseEntity, Column, Entity, OneToMany, PrimaryGeneratedColumn } from "typeorm"; import { MyContext } from "../types/MyContext"; import { Author } from "./Author"; import { AuthorBook } from "./AuthorBook"; @ObjectType() @Entity() export class Book extends BaseEntity { @Field(() => ID) @PrimaryGeneratedColumn() id: number; @Field() @Column() name: string; @OneToMany(() => AuthorBook, ab => ab.book) authorConnection: Promise; @Field(() => [Author]) async authors(@Ctx() { authorsLoader }: MyContext): Promise { return authorsLoader.load(this.id); } } ================================================ FILE: src/entity/Product.ts ================================================ import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm"; import { ObjectType, Field, ID } from "type-graphql"; @ObjectType() @Entity() export class Product extends BaseEntity { @Field(() => ID) @PrimaryGeneratedColumn() id: number; @Field() @Column() name: string; } ================================================ FILE: src/entity/User.ts ================================================ import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm"; import { ObjectType, Field, ID, Root } from "type-graphql"; @ObjectType() @Entity() export class User extends BaseEntity { @Field(() => ID) @PrimaryGeneratedColumn() id: number; @Field() @Column() firstName: string; @Field() @Column() lastName: string; @Field() @Column("text", { unique: true }) email: string; @Field({ complexity: 3 }) name(@Root() parent: User): string { return `${parent.firstName} ${parent.lastName}`; } @Column() password: string; @Column("bool", { default: false }) confirmed: boolean; } ================================================ FILE: src/index.ts ================================================ import { ApolloServer } from "apollo-server-express"; import connectRedis from "connect-redis"; import cors from "cors"; import Express from "express"; import session from "express-session"; import "reflect-metadata"; import { formatArgumentValidationError } from "type-graphql"; import { createConnection } from "typeorm"; import { redis } from "./redis"; import { createAuthorsLoader } from "./utils/authorsLoader"; import { createSchema } from "./utils/createSchema"; const main = async () => { await createConnection(); const schema = await createSchema(); const apolloServer = new ApolloServer({ schema, formatError: formatArgumentValidationError, context: ({ req, res }: any) => ({ req, res, authorsLoader: createAuthorsLoader() }), validationRules: [ // queryComplexity({ // // The maximum allowed query complexity, queries above this threshold will be rejected // maximumComplexity: 8, // // The query variables. This is needed because the variables are not available // // in the visitor of the graphql-js library // variables: {}, // // Optional callback function to retrieve the determined query complexity // // Will be invoked weather the query is rejected or not // // This can be used for logging or to implement rate limiting // onComplete: (complexity: number) => { // console.log("Query Complexity:", complexity); // }, // estimators: [ // // Using fieldConfigEstimator is mandatory to make it work with type-graphql // fieldConfigEstimator(), // // This will assign each field a complexity of 1 if no other estimator // // returned a value. We can define the default value for field not explicitly annotated // simpleEstimator({ // defaultComplexity: 1 // }) // ] // }) as any ] }); const app = Express(); const RedisStore = connectRedis(session); app.use( cors({ credentials: true, origin: "http://localhost:3000" }) ); app.use( session({ store: new RedisStore({ client: redis as any }), name: "qid", secret: "aslkdfjoiq12312", resave: false, saveUninitialized: false, cookie: { httpOnly: true, secure: process.env.NODE_ENV === "production", maxAge: 1000 * 60 * 60 * 24 * 7 * 365 // 7 years } }) ); apolloServer.applyMiddleware({ app, cors: false }); app.listen(4000, () => { console.log("server started on http://localhost:4000/graphql"); }); }; main().catch(err => console.error(err)); ================================================ FILE: src/modules/author-book/AuthorBookResolver.ts ================================================ import { Arg, Int, Mutation, Query, Resolver } from "type-graphql"; import { Author } from "../../entity/Author"; import { AuthorBook } from "../../entity/AuthorBook"; import { Book } from "../../entity/Book"; @Resolver() export class AuthorBookResolver { @Mutation(() => Book) async createBook(@Arg("name") name: string) { return Book.create({ name }).save(); } @Mutation(() => Author) async createAuthor(@Arg("name") name: string) { return Author.create({ name }).save(); } @Mutation(() => Boolean) async addAuthorBook( @Arg("authorId", () => Int) authorId: number, @Arg("bookId", () => Int) bookId: number ) { await AuthorBook.create({ authorId, bookId }).save(); return true; } @Mutation(() => Boolean) async deleteBook(@Arg("bookId", () => Int) bookId: number) { await AuthorBook.delete({ bookId }); await Book.delete({ id: bookId }); return true; } @Query(() => [Book]) async books() { return Book.find(); } } ================================================ FILE: src/modules/constants/redisPrefixes.ts ================================================ export const confirmUserPrefix = "user-confirmation:"; export const forgotPasswordPrefix = "forgot-password:"; ================================================ FILE: src/modules/middleware/isAuth.ts ================================================ import { MiddlewareFn } from "type-graphql"; import { MyContext } from "../../types/MyContext"; export const isAuth: MiddlewareFn = async ({ context }, next) => { if (!context.req.session!.userId) { throw new Error("not authenticated"); } return next(); }; ================================================ FILE: src/modules/middleware/logger.ts ================================================ import { MiddlewareFn } from "type-graphql"; import { MyContext } from "../../types/MyContext"; export const logger: MiddlewareFn = async ({ args }, next) => { console.log("args: ", args); return next(); }; ================================================ FILE: src/modules/shared/OkMixin.ts ================================================ import { ClassType, Field, InputType } from "type-graphql"; export const OkMixin = (BaseClass: T) => { @InputType() class OkInput extends BaseClass { @Field() ok: boolean; } return OkInput; }; ================================================ FILE: src/modules/shared/PasswordInput.ts ================================================ import { MinLength } from "class-validator"; import { Field, InputType, ClassType } from "type-graphql"; export const PasswordMixin = (BaseClass: T) => { @InputType() class PasswordInput extends BaseClass { @Field() @MinLength(5) password: string; } return PasswordInput; }; ================================================ FILE: src/modules/user/ChangePassword.ts ================================================ import { Resolver, Mutation, Arg, Ctx } from "type-graphql"; import bcrypt from "bcryptjs"; import { User } from "../../entity/User"; import { redis } from "../../redis"; import { forgotPasswordPrefix } from "../constants/redisPrefixes"; import { ChangePasswordInput } from "./changePassword/ChangePasswordInput"; import { MyContext } from "../../types/MyContext"; @Resolver() export class ChangePasswordResolver { @Mutation(() => User, { nullable: true }) async changePassword( @Arg("data") { token, password }: ChangePasswordInput, @Ctx() ctx: MyContext ): Promise { const userId = await redis.get(forgotPasswordPrefix + token); if (!userId) { return null; } const user = await User.findOne(userId); if (!user) { return null; } await redis.del(forgotPasswordPrefix + token); user.password = await bcrypt.hash(password, 12); await user.save(); ctx.req.session!.userId = user.id; return user; } } ================================================ FILE: src/modules/user/ConfirmUser.ts ================================================ import { Resolver, Mutation, Arg } from "type-graphql"; import { redis } from "../../redis"; import { User } from "../../entity/User"; import { confirmUserPrefix } from "../constants/redisPrefixes"; @Resolver() export class ConfirmUserResolver { @Mutation(() => Boolean) async confirmUser(@Arg("token") token: string): Promise { const userId = await redis.get(confirmUserPrefix + token); if (!userId) { return false; } await User.update({ id: parseInt(userId, 10) }, { confirmed: true }); await redis.del(token); return true; } } ================================================ FILE: src/modules/user/CreateUser.ts ================================================ import { Resolver, Mutation, Arg, ClassType, InputType, Field, UseMiddleware } from "type-graphql"; import { RegisterInput } from "./register/RegisterInput"; import { User } from "../../entity/User"; import { Product } from "../../entity/Product"; import { Middleware } from "type-graphql/interfaces/Middleware"; function createResolver( suffix: string, returnType: T, inputType: X, entity: any, middleware?: Middleware[] ) { @Resolver() class BaseResolver { @Mutation(() => returnType, { name: `create${suffix}` }) @UseMiddleware(...(middleware || [])) async create(@Arg("data", () => inputType) data: any) { return entity.create(data).save(); } } return BaseResolver; } @InputType() class ProductInput { @Field() name: string; } export const CreateUserResolver = createResolver( "User", User, RegisterInput, User ); export const CreateProductResolver = createResolver( "Product", Product, ProductInput, Product ); ================================================ FILE: src/modules/user/ForgotPassword.ts ================================================ import { Resolver, Mutation, Arg } from "type-graphql"; import { v4 } from "uuid"; import { sendEmail } from "../utils/sendEmail"; import { User } from "../../entity/User"; import { redis } from "../../redis"; import { forgotPasswordPrefix } from "../constants/redisPrefixes"; @Resolver() export class ForgotPasswordResolver { @Mutation(() => Boolean) async forgotPassword(@Arg("email") email: string): Promise { const user = await User.findOne({ where: { email } }); if (!user) { return true; } const token = v4(); await redis.set(forgotPasswordPrefix + token, user.id, "ex", 60 * 60 * 24); // 1 day expiration await sendEmail( email, `http://localhost:3000/user/change-password/${token}` ); return true; } } ================================================ FILE: src/modules/user/Login.ts ================================================ import bcrypt from "bcryptjs"; import { Arg, Ctx, Mutation, Resolver } from "type-graphql"; import { User } from "../../entity/User"; import { MyContext } from "../../types/MyContext"; @Resolver() export class LoginResolver { @Mutation(() => User, { nullable: true }) async login( @Arg("email") email: string, @Arg("password") password: string, @Ctx() ctx: MyContext ): Promise { const user = await User.findOne({ where: { email } }); if (!user) { return null; } const valid = await bcrypt.compare(password, user.password); if (!valid) { return null; } if (!user.confirmed) { return null; } ctx.req.session!.userId = user.id; return user; } } ================================================ FILE: src/modules/user/Logout.ts ================================================ import { Resolver, Mutation, Ctx } from "type-graphql"; import { MyContext } from "../../types/MyContext"; @Resolver() export class LogoutResolver { @Mutation(() => Boolean) async logout(@Ctx() ctx: MyContext): Promise { return new Promise((res, rej) => ctx.req.session!.destroy(err => { if (err) { console.log(err); return rej(false); } ctx.res.clearCookie("qid"); return res(true); }) ); } } ================================================ FILE: src/modules/user/Me.ts ================================================ import { Resolver, Query, Ctx } from "type-graphql"; import { User } from "../../entity/User"; import { MyContext } from "../../types/MyContext"; @Resolver() export class MeResolver { @Query(() => User, { nullable: true, complexity: 5 }) async me(@Ctx() ctx: MyContext): Promise { if (!ctx.req.session!.userId) { return undefined; } return User.findOne(ctx.req.session!.userId); } } ================================================ FILE: src/modules/user/ProfilePicture.ts ================================================ import { Resolver, Mutation, Arg } from "type-graphql"; import { GraphQLUpload } from "graphql-upload"; import { createWriteStream } from "fs"; import { Upload } from "../../types/Upload"; @Resolver() export class ProfilePictureResolver { @Mutation(() => Boolean) async addProfilePicture(@Arg("picture", () => GraphQLUpload) { createReadStream, filename }: Upload): Promise { return new Promise(async (resolve, reject) => createReadStream() .pipe(createWriteStream(__dirname + `/../../../images/${filename}`)) .on("finish", () => resolve(true)) .on("error", () => reject(false)) ); } } ================================================ FILE: src/modules/user/Register.ts ================================================ import { Resolver, Query, Mutation, Arg, UseMiddleware } from "type-graphql"; import bcrypt from "bcryptjs"; import { User } from "../../entity/User"; import { RegisterInput } from "./register/RegisterInput"; import { isAuth } from "../middleware/isAuth"; import { logger } from "../middleware/logger"; import { sendEmail } from "../utils/sendEmail"; import { createConfirmationUrl } from "../utils/createConfirmationUrl"; @Resolver() export class RegisterResolver { @UseMiddleware(isAuth, logger) @Query(() => String) async hello() { return "Hello World!"; } @Mutation(() => User) async register(@Arg("data") { email, firstName, lastName, password }: RegisterInput): Promise { const hashedPassword = await bcrypt.hash(password, 12); const user = await User.create({ firstName, lastName, email, password: hashedPassword }).save(); await sendEmail(email, await createConfirmationUrl(user.id)); return user; } } ================================================ FILE: src/modules/user/changePassword/ChangePasswordInput.ts ================================================ import { Field, InputType } from "type-graphql"; import { PasswordMixin } from "../../shared/PasswordInput"; @InputType() export class ChangePasswordInput extends PasswordMixin(class {}) { @Field() token: string; } ================================================ FILE: src/modules/user/me/Me.test.ts ================================================ import { Connection } from "typeorm"; import faker from "faker"; import { testConn } from "../../../test-utils/testConn"; import { gCall } from "../../../test-utils/gCall"; import { User } from "../../../entity/User"; let conn: Connection; beforeAll(async () => { conn = await testConn(); }); afterAll(async () => { await conn.close(); }); const meQuery = ` { me { id firstName lastName email name } } `; describe("Me", () => { it("get user", async () => { const user = await User.create({ firstName: faker.name.firstName(), lastName: faker.name.lastName(), email: faker.internet.email(), password: faker.internet.password() }).save(); const response = await gCall({ source: meQuery, userId: user.id }); expect(response).toMatchObject({ data: { me: { id: `${user.id}`, firstName: user.firstName, lastName: user.lastName, email: user.email } } }); }); it("return null", async () => { const response = await gCall({ source: meQuery }); expect(response).toMatchObject({ data: { me: null } }); }); }); ================================================ FILE: src/modules/user/register/Register.test.ts ================================================ import faker from "faker"; import { Connection } from "typeorm"; import { User } from "../../../entity/User"; import { gCall } from "../../../test-utils/gCall"; import { testConn } from "../../../test-utils/testConn"; let conn: Connection; beforeAll(async () => { conn = await testConn(); }); afterAll(async () => { await conn.close(); }); const registerMutation = ` mutation Register($data: RegisterInput!) { register( data: $data ) { id firstName lastName email name } } `; describe("Register", () => { it.only("create user", async () => { const user = { firstName: faker.name.firstName(), lastName: faker.name.lastName(), email: faker.internet.email(), password: faker.internet.password() }; const response = await gCall({ source: registerMutation, variableValues: { data: user } }); if (response.errors) { console.log(response.errors[0].originalError); } expect(response).toMatchObject({ data: { register: { firstName: user.firstName, lastName: user.lastName, email: user.email } } }); const dbUser = await User.findOne({ where: { email: user.email } }); expect(dbUser).toBeDefined(); expect(dbUser!.confirmed).toBeFalsy(); expect(dbUser!.firstName).toBe(user.firstName); }); }); ================================================ FILE: src/modules/user/register/RegisterInput.ts ================================================ import { Length, IsEmail } from "class-validator"; import { Field, InputType } from "type-graphql"; import { IsEmailAlreadyExist } from "./isEmailAlreadyExist"; import { PasswordMixin } from "../../shared/PasswordInput"; @InputType() export class RegisterInput extends PasswordMixin(class {}) { @Field() @Length(1, 255) firstName: string; @Field() @Length(1, 255) lastName: string; @Field() @IsEmail() @IsEmailAlreadyExist({ message: "email already in use" }) email: string; } ================================================ FILE: src/modules/user/register/isEmailAlreadyExist.ts ================================================ import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface } from "class-validator"; import { User } from "../../../entity/User"; @ValidatorConstraint({ async: true }) export class IsEmailAlreadyExistConstraint implements ValidatorConstraintInterface { validate(email: string) { return User.findOne({ where: { email } }).then(user => { if (user) return false; return true; }); } } export function IsEmailAlreadyExist(validationOptions?: ValidationOptions) { return function(object: Object, propertyName: string) { registerDecorator({ target: object.constructor, propertyName: propertyName, options: validationOptions, constraints: [], validator: IsEmailAlreadyExistConstraint }); }; } ================================================ FILE: src/modules/utils/createConfirmationUrl.ts ================================================ import { v4 } from "uuid"; import { redis } from "../../redis"; import { confirmUserPrefix } from "../constants/redisPrefixes"; export const createConfirmationUrl = async (userId: number) => { const token = v4(); await redis.set(confirmUserPrefix + token, userId, "ex", 60 * 60 * 24); // 1 day expiration return `http://localhost:3000/user/confirm/${token}`; }; ================================================ FILE: src/modules/utils/sendEmail.ts ================================================ import nodemailer from "nodemailer"; export async function sendEmail(email: string, url: string) { const account = await nodemailer.createTestAccount(); const transporter = nodemailer.createTransport({ host: "smtp.ethereal.email", port: 587, secure: false, // true for 465, false for other ports auth: { user: account.user, // generated ethereal user pass: account.pass // generated ethereal password } }); const mailOptions = { from: '"Fred Foo 👻" ', // sender address to: email, // list of receivers subject: "Hello ✔", // Subject line text: "Hello world?", // plain text body html: `${url}` // html body }; const info = await transporter.sendMail(mailOptions); console.log("Message sent: %s", info.messageId); // Preview only available when sending through an Ethereal account console.log("Preview URL: %s", nodemailer.getTestMessageUrl(info)); } ================================================ FILE: src/redis.ts ================================================ import Redis from "ioredis"; export const redis = new Redis(); ================================================ FILE: src/test-utils/gCall.ts ================================================ import { graphql, GraphQLSchema } from "graphql"; import Maybe from "graphql/tsutils/Maybe"; import { createSchema } from "../utils/createSchema"; interface Options { source: string; variableValues?: Maybe<{ [key: string]: any; }>; userId?: number; } let schema: GraphQLSchema; export const gCall = async ({ source, variableValues, userId }: Options) => { if (!schema) { schema = await createSchema(); } return graphql({ schema, source, variableValues, contextValue: { req: { session: { userId } }, res: { clearCookie: jest.fn() } } }); }; ================================================ FILE: src/test-utils/setup.ts ================================================ import { testConn } from "./testConn"; testConn(true).then(() => process.exit()); ================================================ FILE: src/test-utils/testConn.ts ================================================ import { createConnection } from "typeorm"; export const testConn = (drop: boolean = false) => { return createConnection({ name: "default", type: "postgres", host: "localhost", port: 5432, username: "postgres", password: "postgres", database: "typegraphql-example-test", synchronize: drop, dropSchema: drop, entities: [__dirname + "/../entity/*.*"] }); }; ================================================ FILE: src/types/MyContext.ts ================================================ import { Request, Response } from "express"; import { createAuthorsLoader } from "../utils/authorsLoader"; export interface MyContext { req: Request; res: Response; authorsLoader: ReturnType; } ================================================ FILE: src/types/Upload.ts ================================================ import { Stream } from "stream"; export interface Upload { filename: string; mimetype: string; encoding: string; createReadStream: () => Stream; } ================================================ FILE: src/utils/authorsLoader.ts ================================================ import DataLoader from "dataloader"; import { In } from "typeorm"; import { Author } from "../entity/Author"; import { AuthorBook } from "../entity/AuthorBook"; const batchAuthors = async (bookIds: number[]) => { const authorBooks = await AuthorBook.find({ join: { alias: "authorBook", innerJoinAndSelect: { author: "authorBook.author" } }, where: { bookId: In(bookIds) } }); const bookIdToAuthors: { [key: number]: Author[] } = {}; /* { authorId: 1, bookId: 1, __author__: { id: 1, name: 'author1' } } */ authorBooks.forEach(ab => { if (ab.bookId in bookIdToAuthors) { bookIdToAuthors[ab.bookId].push((ab as any).__author__); } else { bookIdToAuthors[ab.bookId] = [(ab as any).__author__]; } }); return bookIds.map(bookId => bookIdToAuthors[bookId]); }; export const createAuthorsLoader = () => new DataLoader(batchAuthors); ================================================ FILE: src/utils/createSchema.ts ================================================ import { buildSchema } from "type-graphql"; import { AuthorBookResolver } from "../modules/author-book/AuthorBookResolver"; import { ChangePasswordResolver } from "../modules/user/ChangePassword"; import { ConfirmUserResolver } from "../modules/user/ConfirmUser"; import { CreateProductResolver, CreateUserResolver } from "../modules/user/CreateUser"; import { ForgotPasswordResolver } from "../modules/user/ForgotPassword"; import { LoginResolver } from "../modules/user/Login"; import { LogoutResolver } from "../modules/user/Logout"; import { MeResolver } from "../modules/user/Me"; import { ProfilePictureResolver } from "../modules/user/ProfilePicture"; import { RegisterResolver } from "../modules/user/Register"; export const createSchema = () => buildSchema({ resolvers: [ ChangePasswordResolver, ConfirmUserResolver, ForgotPasswordResolver, LoginResolver, LogoutResolver, MeResolver, RegisterResolver, CreateUserResolver, CreateProductResolver, ProfilePictureResolver, AuthorBookResolver ], authChecker: ({ context: { req } }) => { return !!req.session.userId; } }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "target": "es6", "module": "commonjs", "lib": ["dom", "es6", "es2017", "esnext.asynciterable"], "sourceMap": true, "outDir": "./dist", "moduleResolution": "node", "declaration": false, "composite": false, "removeComments": true, "noImplicitAny": true, "strictNullChecks": true, "strictFunctionTypes": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "allowSyntheticDefaultImports": true, "esModuleInterop": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "skipLibCheck": true, "baseUrl": ".", "rootDir": "src" }, "exclude": ["node_modules"], "include": ["./src/**/*.tsx", "./src/**/*.ts"] }