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<AuthorBook[]>;
}
================================================
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<Author>;
@ManyToOne(() => Book, book => book.authorConnection, {
primary: true
})
@JoinColumn({ name: "bookId" })
book: Promise<Book>;
}
================================================
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<AuthorBook[]>;
@Field(() => [Author])
async authors(@Ctx() { authorsLoader }: MyContext): Promise<Author[]> {
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<MyContext> = 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<MyContext> = 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 = <T extends ClassType>(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 = <T extends ClassType>(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<User | null> {
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<boolean> {
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<T extends ClassType, X extends ClassType>(
suffix: string,
returnType: T,
inputType: X,
entity: any,
middleware?: Middleware<any>[]
) {
@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<boolean> {
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<User | null> {
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<Boolean> {
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<User | undefined> {
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<boolean> {
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<User> {
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 👻" <foo@example.com>', // sender address
to: email, // list of receivers
subject: "Hello ✔", // Subject line
text: "Hello world?", // plain text body
html: `<a href="${url}">${url}</a>` // 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<typeof createAuthorsLoader>;
}
================================================
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"]
}
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
SYMBOL INDEX (43 symbols across 24 files)
FILE: src/entity/Author.ts
class Author (line 13) | class Author extends BaseEntity {
FILE: src/entity/AuthorBook.ts
class AuthorBook (line 12) | class AuthorBook extends BaseEntity {
FILE: src/entity/Book.ts
class Book (line 15) | class Book extends BaseEntity {
method authors (line 28) | async authors(@Ctx() { authorsLoader }: MyContext): Promise<Author[]> {
FILE: src/entity/Product.ts
class Product (line 6) | class Product extends BaseEntity {
FILE: src/entity/User.ts
class User (line 6) | class User extends BaseEntity {
method name (line 24) | name(@Root() parent: User): string {
FILE: src/modules/author-book/AuthorBookResolver.ts
class AuthorBookResolver (line 7) | class AuthorBookResolver {
method createBook (line 9) | async createBook(@Arg("name") name: string) {
method createAuthor (line 14) | async createAuthor(@Arg("name") name: string) {
method addAuthorBook (line 19) | async addAuthorBook(
method deleteBook (line 28) | async deleteBook(@Arg("bookId", () => Int) bookId: number) {
method books (line 35) | async books() {
FILE: src/modules/shared/OkMixin.ts
class OkInput (line 4) | @InputType()
FILE: src/modules/shared/PasswordInput.ts
class PasswordInput (line 5) | @InputType()
FILE: src/modules/user/ChangePassword.ts
class ChangePasswordResolver (line 11) | class ChangePasswordResolver {
method changePassword (line 13) | async changePassword(
FILE: src/modules/user/ConfirmUser.ts
class ConfirmUserResolver (line 8) | class ConfirmUserResolver {
method confirmUser (line 10) | async confirmUser(@Arg("token") token: string): Promise<boolean> {
FILE: src/modules/user/CreateUser.ts
function createResolver (line 15) | function createResolver<T extends ClassType, X extends ClassType>(
class ProductInput (line 34) | @InputType()
FILE: src/modules/user/ForgotPassword.ts
class ForgotPasswordResolver (line 10) | class ForgotPasswordResolver {
method forgotPassword (line 12) | async forgotPassword(@Arg("email") email: string): Promise<boolean> {
FILE: src/modules/user/Login.ts
class LoginResolver (line 7) | class LoginResolver {
method login (line 9) | async login(
FILE: src/modules/user/Logout.ts
class LogoutResolver (line 5) | class LogoutResolver {
method logout (line 7) | async logout(@Ctx() ctx: MyContext): Promise<Boolean> {
FILE: src/modules/user/Me.ts
class MeResolver (line 7) | class MeResolver {
method me (line 9) | async me(@Ctx() ctx: MyContext): Promise<User | undefined> {
FILE: src/modules/user/ProfilePicture.ts
class ProfilePictureResolver (line 8) | class ProfilePictureResolver {
method addProfilePicture (line 10) | async addProfilePicture(@Arg("picture", () => GraphQLUpload)
FILE: src/modules/user/Register.ts
class RegisterResolver (line 12) | class RegisterResolver {
method hello (line 15) | async hello() {
method register (line 20) | async register(@Arg("data")
FILE: src/modules/user/changePassword/ChangePasswordInput.ts
class ChangePasswordInput (line 5) | class ChangePasswordInput extends PasswordMixin(class {}) {
FILE: src/modules/user/register/RegisterInput.ts
class RegisterInput (line 7) | class RegisterInput extends PasswordMixin(class {}) {
FILE: src/modules/user/register/isEmailAlreadyExist.ts
class IsEmailAlreadyExistConstraint (line 11) | class IsEmailAlreadyExistConstraint
method validate (line 13) | validate(email: string) {
function IsEmailAlreadyExist (line 21) | function IsEmailAlreadyExist(validationOptions?: ValidationOptions) {
FILE: src/modules/utils/sendEmail.ts
function sendEmail (line 3) | async function sendEmail(email: string, url: string) {
FILE: src/test-utils/gCall.ts
type Options (line 6) | interface Options {
FILE: src/types/MyContext.ts
type MyContext (line 4) | interface MyContext {
FILE: src/types/Upload.ts
type Upload (line 3) | interface Upload {
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (31K chars).
[
{
"path": ".gitignore",
"chars": 26,
"preview": "node_modules\nimages\n*.logs"
},
{
"path": "README.md",
"chars": 1283,
"preview": "# type-graphql-series\n\nCode for: https://www.youtube.com/playlist?list=PLN3n1USn4xlma1bBu3Tloe4NyYn9Ko8Gs\n\n# Installing\n"
},
{
"path": "jest.config.js",
"chars": 69,
"preview": "module.exports = {\n preset: 'ts-jest',\n testEnvironment: 'node',\n};"
},
{
"path": "ormconfig.json",
"chars": 250,
"preview": "{\n \"name\": \"default\",\n \"type\": \"postgres\",\n \"host\": \"localhost\",\n \"port\": 5432,\n \"username\": \"postgres\",\n \"passwor"
},
{
"path": "package.json",
"chars": 1393,
"preview": "{\n \"name\": \"type-graphql-series\",\n \"version\": \"1.0.0\",\n \"main\": \"index.js\",\n \"license\": \"MIT\",\n \"dependencies\": {\n "
},
{
"path": "src/entity/Author.ts",
"chars": 455,
"preview": "import { Field, ID, ObjectType } from \"type-graphql\";\nimport {\n BaseEntity,\n Column,\n Entity,\n OneToMany,\n PrimaryG"
},
{
"path": "src/entity/AuthorBook.ts",
"chars": 577,
"preview": "import {\n BaseEntity,\n Entity,\n JoinColumn,\n ManyToOne,\n PrimaryColumn\n} from \"typeorm\";\nimport { Author } from \"./"
},
{
"path": "src/entity/Book.ts",
"chars": 685,
"preview": "import { Ctx, Field, ID, ObjectType } from \"type-graphql\";\nimport {\n BaseEntity,\n Column,\n Entity,\n OneToMany,\n Pri"
},
{
"path": "src/entity/Product.ts",
"chars": 302,
"preview": "import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from \"typeorm\";\nimport { ObjectType, Field, ID } from \"typ"
},
{
"path": "src/entity/User.ts",
"chars": 638,
"preview": "import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from \"typeorm\";\nimport { ObjectType, Field, ID, Root } fro"
},
{
"path": "src/index.ts",
"chars": 2688,
"preview": "import { ApolloServer } from \"apollo-server-express\";\nimport connectRedis from \"connect-redis\";\nimport cors from \"cors\";"
},
{
"path": "src/modules/author-book/AuthorBookResolver.ts",
"chars": 993,
"preview": "import { Arg, Int, Mutation, Query, Resolver } from \"type-graphql\";\nimport { Author } from \"../../entity/Author\";\nimport"
},
{
"path": "src/modules/constants/redisPrefixes.ts",
"chars": 111,
"preview": "export const confirmUserPrefix = \"user-confirmation:\";\nexport const forgotPasswordPrefix = \"forgot-password:\";\n"
},
{
"path": "src/modules/middleware/isAuth.ts",
"chars": 281,
"preview": "import { MiddlewareFn } from \"type-graphql\";\n\nimport { MyContext } from \"../../types/MyContext\";\n\nexport const isAuth: M"
},
{
"path": "src/modules/middleware/logger.ts",
"chars": 225,
"preview": "import { MiddlewareFn } from \"type-graphql\";\n\nimport { MyContext } from \"../../types/MyContext\";\n\nexport const logger: M"
},
{
"path": "src/modules/shared/OkMixin.ts",
"chars": 231,
"preview": "import { ClassType, Field, InputType } from \"type-graphql\";\n\nexport const OkMixin = <T extends ClassType>(BaseClass: T) "
},
{
"path": "src/modules/shared/PasswordInput.ts",
"chars": 317,
"preview": "import { MinLength } from \"class-validator\";\nimport { Field, InputType, ClassType } from \"type-graphql\";\n\nexport const P"
},
{
"path": "src/modules/user/ChangePassword.ts",
"chars": 996,
"preview": "import { Resolver, Mutation, Arg, Ctx } from \"type-graphql\";\nimport bcrypt from \"bcryptjs\";\n\nimport { User } from \"../.."
},
{
"path": "src/modules/user/ConfirmUser.ts",
"chars": 580,
"preview": "import { Resolver, Mutation, Arg } from \"type-graphql\";\n\nimport { redis } from \"../../redis\";\nimport { User } from \"../."
},
{
"path": "src/modules/user/CreateUser.ts",
"chars": 1041,
"preview": "import {\n Resolver,\n Mutation,\n Arg,\n ClassType,\n InputType,\n Field,\n UseMiddleware\n} from \"type-graphql\";\nimport"
},
{
"path": "src/modules/user/ForgotPassword.ts",
"chars": 781,
"preview": "import { Resolver, Mutation, Arg } from \"type-graphql\";\nimport { v4 } from \"uuid\";\n\nimport { sendEmail } from \"../utils/"
},
{
"path": "src/modules/user/Login.ts",
"chars": 741,
"preview": "import bcrypt from \"bcryptjs\";\nimport { Arg, Ctx, Mutation, Resolver } from \"type-graphql\";\nimport { User } from \"../../"
},
{
"path": "src/modules/user/Logout.ts",
"chars": 483,
"preview": "import { Resolver, Mutation, Ctx } from \"type-graphql\";\nimport { MyContext } from \"../../types/MyContext\";\n\n@Resolver()\n"
},
{
"path": "src/modules/user/Me.ts",
"chars": 428,
"preview": "import { Resolver, Query, Ctx } from \"type-graphql\";\n\nimport { User } from \"../../entity/User\";\nimport { MyContext } fro"
},
{
"path": "src/modules/user/ProfilePicture.ts",
"chars": 653,
"preview": "import { Resolver, Mutation, Arg } from \"type-graphql\";\nimport { GraphQLUpload } from \"graphql-upload\";\nimport { createW"
},
{
"path": "src/modules/user/Register.ts",
"chars": 1004,
"preview": "import { Resolver, Query, Mutation, Arg, UseMiddleware } from \"type-graphql\";\nimport bcrypt from \"bcryptjs\";\n\nimport { U"
},
{
"path": "src/modules/user/changePassword/ChangePasswordInput.ts",
"chars": 220,
"preview": "import { Field, InputType } from \"type-graphql\";\nimport { PasswordMixin } from \"../../shared/PasswordInput\";\n\n@InputType"
},
{
"path": "src/modules/user/me/Me.test.ts",
"chars": 1213,
"preview": "import { Connection } from \"typeorm\";\nimport faker from \"faker\";\n\nimport { testConn } from \"../../../test-utils/testConn"
},
{
"path": "src/modules/user/register/Register.test.ts",
"chars": 1391,
"preview": "import faker from \"faker\";\nimport { Connection } from \"typeorm\";\nimport { User } from \"../../../entity/User\";\nimport { g"
},
{
"path": "src/modules/user/register/RegisterInput.ts",
"chars": 500,
"preview": "import { Length, IsEmail } from \"class-validator\";\nimport { Field, InputType } from \"type-graphql\";\nimport { IsEmailAlre"
},
{
"path": "src/modules/user/register/isEmailAlreadyExist.ts",
"chars": 802,
"preview": "import {\n registerDecorator,\n ValidationOptions,\n ValidatorConstraint,\n ValidatorConstraintInterface\n} from \"class-v"
},
{
"path": "src/modules/utils/createConfirmationUrl.ts",
"chars": 370,
"preview": "import { v4 } from \"uuid\";\nimport { redis } from \"../../redis\";\nimport { confirmUserPrefix } from \"../constants/redisPre"
},
{
"path": "src/modules/utils/sendEmail.ts",
"chars": 963,
"preview": "import nodemailer from \"nodemailer\";\n\nexport async function sendEmail(email: string, url: string) {\n const account = aw"
},
{
"path": "src/redis.ts",
"chars": 64,
"preview": "import Redis from \"ioredis\";\n\nexport const redis = new Redis();\n"
},
{
"path": "src/test-utils/gCall.ts",
"chars": 646,
"preview": "import { graphql, GraphQLSchema } from \"graphql\";\nimport Maybe from \"graphql/tsutils/Maybe\";\n\nimport { createSchema } fr"
},
{
"path": "src/test-utils/setup.ts",
"chars": 83,
"preview": "import { testConn } from \"./testConn\";\n\ntestConn(true).then(() => process.exit());\n"
},
{
"path": "src/test-utils/testConn.ts",
"chars": 401,
"preview": "import { createConnection } from \"typeorm\";\n\nexport const testConn = (drop: boolean = false) => {\n return createConnect"
},
{
"path": "src/types/MyContext.ts",
"chars": 229,
"preview": "import { Request, Response } from \"express\";\nimport { createAuthorsLoader } from \"../utils/authorsLoader\";\n\nexport inter"
},
{
"path": "src/types/Upload.ts",
"chars": 156,
"preview": "import { Stream } from \"stream\";\n\nexport interface Upload {\n filename: string;\n mimetype: string;\n encoding: string;\n"
},
{
"path": "src/utils/authorsLoader.ts",
"chars": 935,
"preview": "import DataLoader from \"dataloader\";\nimport { In } from \"typeorm\";\nimport { Author } from \"../entity/Author\";\nimport { A"
},
{
"path": "src/utils/createSchema.ts",
"chars": 1173,
"preview": "import { buildSchema } from \"type-graphql\";\nimport { AuthorBookResolver } from \"../modules/author-book/AuthorBookResolve"
},
{
"path": "tsconfig.json",
"chars": 836,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"es6\",\n \"module\": \"commonjs\",\n \"lib\": [\"dom\", \"es6\", \"es2017\", \"esnext.asyn"
}
]
About this extraction
This page contains the full source code of the benawad/type-graphql-series GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (26.6 KB), approximately 8.1k tokens, and a symbol index with 43 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.