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