Skip to content

Commit b270cd8

Browse files
committed
first commit
0 parents  commit b270cd8

22 files changed

+2414
-0
lines changed

.env

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlDWFFJQkFBS0JnUUNrWndLTlMzU0V5RUhRNElLamh4UjF5alFrTkNPSVl6K3BNTHdud1hlSHkreWR5VGF1Cm5MMlBtTUVPSXc0bkRCSkpySGNGVWI3R3pjSVdGd0dNSHk2aWxRSFdaWnFSUkFmdklJVHE1TkNVSXFVMzNUcEoKajlDbElNNDEyL255V1Z5R2YrYml3eWErSWpKdUJEaHEwMDJyeHhVWGFtUzgzNWZtVEs3NkxkWVFmUUlEQVFBQgpBb0dCQUlNdno5R0FESktJV2p5YmFxT2kvcWlmbWN2cDd4QytZZVpZaFV3VURaWEhIQ0VzbHYzdkJUUzQ2QlNuCjFIdEVIck83YzU1REJNRVBIM2tSRXFNRm51ZS9IWHRBS1J6U3BGQm9HUldXelp2aytndDJDM0lPdy9hRTJCQ08KaVFUK3l0QUM0b0pnNU8zVU1FVWVsVWJRR0I1ZDlqZGRDWnhxa09iaVR1NXBlT1lCQWtFQS9GRUg1VHRuREhHRQorUG9UNDZwYkVMdW5UclpqdnJBV3FzN1J3a3lFZlo5YjVpbVhYQ0FvaWRFUWIreFdGRVpJNUtvYTNyVXBLeW5mCnkvME9jbnIxRVFKQkFLYk5iQm1EQXp1ZG13RzBjTjhRSDE0RGZOR2F4SVRGaE5LOVRINGVsdEtKY0kzNmhJWFAKYVdwVjRuL3IzNTNkTjNhcFlsbWg1d1U3eGhyc3FPdUhOSzBDUUZJanN2Vk9ORXJadmRjcjJrTzRWck1JMC91TQo1c1hTSDE3MXUxV01nV2svOHJQb0FFMU9ic1FHMmxvRlR6U0VlUUJ2M0JWNlZtK2x6eVJpT2t6TWVIRUNRSGdICkE4MkwxK3l6S1pKZGZJY1crK3RUeVNLdkl0Q0RyV05UOGxJaXd0YjNMWFlOR2dXTHpjaEZ5dm5RQ3BaM1UremcKVURROWE1YjVmMEZxb05ieThQVUNRUURNQzdGcFFsbG5jUGU5L2hDenVXTXg5QnoyLzVTU3R0NFFPRmF5alJscgplNUlzdU9xWkN4RE5qei9uM3JVclZXOGlYeTdhSnAwWEF5anJzVFcrejRSMQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ
2+
PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FDa1p3S05TM1NFeUVIUTRJS2poeFIxeWpRawpOQ09JWXorcE1Md253WGVIeSt5ZHlUYXVuTDJQbU1FT0l3NG5EQkpKckhjRlViN0d6Y0lXRndHTUh5NmlsUUhXClpacVJSQWZ2SUlUcTVOQ1VJcVUzM1RwSmo5Q2xJTTQxMi9ueVdWeUdmK2Jpd3lhK0lqSnVCRGhxMDAycnh4VVgKYW1TODM1Zm1USzc2TGRZUWZRSURBUUFCCi0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export default {
2+
publicKey: "PUBLIC_KEY",
3+
privateKey: "PRIVATE_KEY",
4+
};

config/default.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default {
2+
dbUri: "mongodb://localhost:27017/graphql-api-tutorial",
3+
};

config/development.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};

config/production.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};

config/test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default {};

package.json

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "typegraphql-tutorial",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"author": "Tom Nagle",
6+
"license": "MIT",
7+
"scripts": {
8+
"dev": "ts-node-dev --respawn --transpile-only src/index.ts"
9+
},
10+
"dependencies": {
11+
"@typegoose/typegoose": "^9.2.0",
12+
"apollo-server": "^3.5.0",
13+
"bcrypt": "^5.0.1",
14+
"class-validator": "^0.13.2",
15+
"config": "^3.3.6",
16+
"cookie-parser": "^1.4.6",
17+
"dotenv": "^10.0.0",
18+
"graphql": "15.x",
19+
"jsonwebtoken": "^8.5.1",
20+
"mongoose": "^6.0.13",
21+
"nanoid": "^3.1.30",
22+
"reflect-metadata": "^0.1.13",
23+
"type-graphql": "^1.1.1"
24+
},
25+
"devDependencies": {
26+
"@types/bcrypt": "^5.0.0",
27+
"@types/config": "^0.0.40",
28+
"@types/cookie-parser": "^1.4.2",
29+
"@types/jsonwebtoken": "^8.5.6",
30+
"ts-node-dev": "^1.1.8",
31+
"typescript": "^4.5.2"
32+
}
33+
}

src/index.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import dotenv from "dotenv";
2+
dotenv.config();
3+
import "reflect-metadata";
4+
import express from "express";
5+
import { buildSchema } from "type-graphql";
6+
import cookieParser from "cookie-parser";
7+
import { ApolloServer } from "apollo-server-express";
8+
import {
9+
ApolloServerPluginLandingPageGraphQLPlayground,
10+
ApolloServerPluginLandingPageProductionDefault,
11+
} from "apollo-server-core";
12+
import { resolvers } from "./resolvers";
13+
import { connectToMongo } from "./utils/mongo";
14+
import { verifyJwt } from "./utils/jwt";
15+
import { User } from "./schema/user.schema";
16+
import Context from "./types/context";
17+
import authChecker from "./utils/authChecker";
18+
19+
async function bootstrap() {
20+
// Build the schema
21+
22+
const schema = await buildSchema({
23+
resolvers,
24+
authChecker,
25+
});
26+
27+
// Init express
28+
const app = express();
29+
30+
app.use(cookieParser());
31+
32+
// Create the apollo server
33+
const server = new ApolloServer({
34+
schema,
35+
context: (ctx: Context) => {
36+
const context = ctx;
37+
38+
if (ctx.req.cookies.accessToken) {
39+
const user = verifyJwt<User>(ctx.req.cookies.accessToken);
40+
context.user = user;
41+
}
42+
return context;
43+
},
44+
plugins: [
45+
process.env.NODE_ENV === "production"
46+
? ApolloServerPluginLandingPageProductionDefault()
47+
: ApolloServerPluginLandingPageGraphQLPlayground(),
48+
],
49+
});
50+
51+
await server.start();
52+
// apply middleware to server
53+
54+
server.applyMiddleware({ app });
55+
56+
// app.listen on express server
57+
app.listen({ port: 4000 }, () => {
58+
console.log("App is listening on http://localhost:4000");
59+
});
60+
connectToMongo();
61+
}
62+
63+
bootstrap();

src/resolvers/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import UserResolver from "./user.resolver";
2+
import ProductResolver from "./product.resolver";
3+
4+
export const resolvers = [UserResolver, ProductResolver] as const;

src/resolvers/product.resolver.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { Arg, Authorized, Ctx, Mutation, Query, Resolver } from "type-graphql";
2+
import {
3+
CreateProductInput,
4+
GetProductInput,
5+
Product,
6+
} from "../schema/product.schema";
7+
import ProductService from "../service/product.service";
8+
import Context from "../types/context";
9+
10+
@Resolver()
11+
export default class ProductResolver {
12+
constructor(private productService: ProductService) {
13+
this.productService = new ProductService();
14+
}
15+
16+
@Authorized()
17+
@Mutation(() => Product)
18+
createProduct(
19+
@Arg("input") input: CreateProductInput,
20+
@Ctx() context: Context
21+
) {
22+
const user = context.user!;
23+
return this.productService.createProduct({ ...input, user: user?._id });
24+
}
25+
26+
@Query(() => [Product])
27+
products() {
28+
return this.productService.findProducts();
29+
}
30+
31+
@Query(() => Product)
32+
product(@Arg("input") input: GetProductInput) {
33+
return this.productService.findSingleProduct(input);
34+
}
35+
}

src/resolvers/user.resolver.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { Arg, Ctx, Mutation, Query, Resolver } from "type-graphql";
2+
import { CreateUserInput, LoginInput, User } from "../schema/user.schema";
3+
import UserService from "../service/user.service";
4+
import Context from "../types/context";
5+
6+
@Resolver()
7+
export default class UserResolver {
8+
constructor(private userService: UserService) {
9+
this.userService = new UserService();
10+
}
11+
12+
@Mutation(() => User)
13+
createUser(@Arg("input") input: CreateUserInput) {
14+
return this.userService.createUser(input);
15+
}
16+
17+
@Mutation(() => String) // Returns the JWT
18+
login(@Arg("input") input: LoginInput, @Ctx() context: Context) {
19+
return this.userService.login(input, context);
20+
}
21+
22+
@Query(() => User, { nullable: true })
23+
me(@Ctx() context: Context) {
24+
return context.user;
25+
}
26+
}

src/schema/product.schema.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { getModelForClass, index, prop, Ref } from "@typegoose/typegoose";
2+
import { Field, InputType, ObjectType } from "type-graphql";
3+
import { customAlphabet } from "nanoid";
4+
import { User } from "./user.schema";
5+
import { IsNumber, MaxLength, Min, MinLength } from "class-validator";
6+
7+
const nanoid = customAlphabet("abcdefghijklmnopqrstuvwxyz123456789", 10);
8+
9+
@ObjectType()
10+
@index({ productId: 1 })
11+
export class Product {
12+
@Field(() => String)
13+
_id: string;
14+
15+
@Field(() => String)
16+
@prop({ required: true, ref: () => User })
17+
user: Ref<User>;
18+
19+
@Field(() => String)
20+
@prop({ required: true })
21+
name: string;
22+
23+
@Field(() => String)
24+
@prop({ required: true })
25+
description: string;
26+
27+
@Field(() => String)
28+
@prop({ required: true })
29+
price: string;
30+
31+
@Field(() => String)
32+
@prop({ required: true, default: () => `product_${nanoid()}, unique: true}` })
33+
productId: string;
34+
}
35+
36+
export const ProductModel = getModelForClass<typeof Product>(Product);
37+
38+
@InputType()
39+
export class CreateProductInput {
40+
@Field()
41+
name: string;
42+
43+
@MinLength(50, {
44+
message: "Description must be at least 50 characters",
45+
})
46+
@MaxLength(1000, {
47+
message: "Description must not be more than 1000 characters",
48+
})
49+
@Field()
50+
description: string;
51+
52+
@IsNumber()
53+
@Min(1)
54+
@Field()
55+
price: number;
56+
}
57+
58+
@InputType()
59+
export class GetProductInput {
60+
@Field()
61+
productId: string;
62+
}

src/schema/user.schema.ts

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
getModelForClass,
3+
prop,
4+
pre,
5+
ReturnModelType,
6+
queryMethod,
7+
index,
8+
} from "@typegoose/typegoose";
9+
import { AsQueryMethod } from "@typegoose/typegoose/lib/types";
10+
import bcrypt from "bcrypt";
11+
import { IsEmail, MaxLength, MinLength } from "class-validator";
12+
import { Field, InputType, ObjectType } from "type-graphql";
13+
14+
function findByEmail(
15+
this: ReturnModelType<typeof User, QueryHelpers>,
16+
email: User["email"]
17+
) {
18+
return this.findOne({ email });
19+
}
20+
21+
interface QueryHelpers {
22+
findByEmail: AsQueryMethod<typeof findByEmail>;
23+
}
24+
25+
@pre<User>("save", async function () {
26+
// Check that the password is being modified
27+
if (!this.isModified("password")) {
28+
return;
29+
}
30+
31+
const salt = await bcrypt.genSalt(10);
32+
33+
const hash = await bcrypt.hashSync(this.password, salt);
34+
35+
this.password = hash;
36+
})
37+
@index({ email: 1 })
38+
@queryMethod(findByEmail)
39+
@ObjectType()
40+
export class User {
41+
@Field(() => String)
42+
_id: string;
43+
44+
@Field(() => String)
45+
@prop({ required: true })
46+
name: string;
47+
48+
@Field(() => String)
49+
@prop({ required: true })
50+
email: string;
51+
52+
@prop({ required: true })
53+
password: string;
54+
}
55+
56+
export const UserModel = getModelForClass<typeof User, QueryHelpers>(User);
57+
58+
@InputType()
59+
export class CreateUserInput {
60+
@Field(() => String)
61+
name: string;
62+
63+
@IsEmail()
64+
@Field(() => String)
65+
email: string;
66+
67+
@MinLength(6, {
68+
message: "password must be at least 6 characters long",
69+
})
70+
@MaxLength(50, {
71+
message: "password must not be longer than 50 characters",
72+
})
73+
@Field(() => String)
74+
password: string;
75+
}
76+
77+
@InputType()
78+
export class LoginInput {
79+
@Field(() => String)
80+
email: string;
81+
82+
@Field(() => String)
83+
password: string;
84+
}

src/service/product.service.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import {
2+
CreateProductInput,
3+
GetProductInput,
4+
ProductModel,
5+
} from "../schema/product.schema";
6+
import { User } from "../schema/user.schema";
7+
8+
class ProductService {
9+
async createProduct(input: CreateProductInput & { user: User["_id"] }) {
10+
return ProductModel.create(input);
11+
}
12+
13+
async findProducts() {
14+
// Pagination login
15+
return ProductModel.find().lean();
16+
}
17+
18+
async findSingleProduct(input: GetProductInput) {
19+
return ProductModel.findOne(input).lean();
20+
}
21+
}
22+
23+
export default ProductService;

0 commit comments

Comments
 (0)