├── NOTES.md ├── README.md ├── part-one ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package.json ├── schema.gql ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── author │ │ ├── author.module.ts │ │ ├── author.resolver.spec.ts │ │ ├── author.resolver.ts │ │ ├── author.schema.ts │ │ ├── author.service.spec.ts │ │ └── author.service.ts │ ├── book │ │ ├── book.module.ts │ │ ├── book.resolver.spec.ts │ │ ├── book.resolver.ts │ │ ├── book.schema.ts │ │ ├── book.service.spec.ts │ │ └── book.service.ts │ ├── data │ │ ├── authors.ts │ │ └── books.ts │ └── main.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock ├── part-three ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package-lock.json ├── package.json ├── schema.gql ├── src │ ├── app.controller.spec.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── author │ │ ├── author.module.ts │ │ ├── author.resolver.spec.ts │ │ ├── author.resolver.ts │ │ ├── author.schema.ts │ │ ├── author.service.spec.ts │ │ └── author.service.ts │ ├── book │ │ ├── book.module.ts │ │ ├── book.resolver.spec.ts │ │ ├── book.resolver.ts │ │ ├── book.schema.ts │ │ ├── book.service.spec.ts │ │ └── book.service.ts │ ├── data │ │ ├── authors.ts │ │ └── books.ts │ ├── main.ts │ ├── types │ │ └── context.type.ts │ ├── user │ │ ├── user.module.ts │ │ ├── user.resolver.spec.ts │ │ ├── user.resolver.ts │ │ ├── user.schema.ts │ │ ├── user.service.spec.ts │ │ └── user.service.ts │ └── utils │ │ └── jwt.utils.ts ├── test │ ├── app.e2e-spec.ts │ └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock └── part-two ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── nest-cli.json ├── package.json ├── schema.gql ├── src ├── app.controller.spec.ts ├── app.controller.ts ├── app.module.ts ├── app.service.ts ├── author │ ├── author.module.ts │ ├── author.resolver.spec.ts │ ├── author.resolver.ts │ ├── author.schema.ts │ ├── author.service.spec.ts │ └── author.service.ts ├── book │ ├── book.module.ts │ ├── book.resolver.spec.ts │ ├── book.resolver.ts │ ├── book.schema.ts │ ├── book.service.spec.ts │ └── book.service.ts ├── data │ ├── authors.ts │ └── books.ts └── main.ts ├── test ├── app.e2e-spec.ts └── jest-e2e.json ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /NOTES.md: -------------------------------------------------------------------------------- 1 | ## Part 1 2 | `yarn add @nestjs/graphql graphql-tools graphql apollo-server-express` 3 | 4 | ### Generate author 5 | ``` 6 | nest g module author 7 | nest g resolver author 8 | nest g service author 9 | touch src/author/author.schema.ts 10 | ``` 11 | 12 | ### Generate book 13 | ``` 14 | nest g module book 15 | nest g resolver book 16 | nest g service book 17 | touch src/book/book.schema.ts 18 | ``` 19 | ## Part 2 20 | 21 | `yarn add @nestjs/mongoose mongoose` 22 | 23 | ## Part 3 24 | `nest g resource users` 25 | 26 | `yarn add bcrypt jsonwebtoken nanoid cookie-parser` 27 | 28 | `yarn add @types/jsonwebtoken @types/nanoid @types/cookie-parser -D` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS GraphQL TypeGraphQL 2 | 3 | ## Part 1 4 | Video: https://www.youtube.com/watch?v=bQPk4S_W-pc 5 | 6 | * Build a GraphQL API with NestJS & TypeGraphQL 7 | 8 | ## Part two 9 | Video: https://www.youtube.com/watch?v=xnyULqzQ-Rk 10 | 11 | * Adding a MongoDB database instance to a GraphQL API in NestJS 12 | 13 | ## Part three 14 | Video: https://youtu.be/PXwnT25SZro 15 | 16 | * GraphQL authentication 17 | 18 | 19 | ### Who is this for? 20 | * Those familiar with what NestJS is 21 | * Those familiar with some GraphQL concepts 22 | * Those familiar with TypeScript 23 | 24 | ### What does this guide not cover? 25 | * MongoDB database connection 26 | * REST API data source connection 27 | 28 | 29 | ## Let's keep in touch 30 | - [Subscribe on YouTube](https://www.youtube.com/TomDoesTech) 31 | - [Discord](https://discord.gg/4ae2Esm6P7) 32 | - [Twitter](https://twitter.com/tomdoes_tech) 33 | - [TikTok](https://www.tiktok.com/@tomdoestech) 34 | - [Facebook](https://www.facebook.com/tomdoestech) 35 | - [Instagram](https://www.instagram.com/tomdoestech) 36 | 37 | [Buy me a Coffee](https://www.buymeacoffee.com/tomn) 38 | 39 | [Sign up to DigitalOcean 💖](https://m.do.co/c/1b74cb8c56f4) 40 | -------------------------------------------------------------------------------- /part-one/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /part-one/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /part-one/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /part-one/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /part-one/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part-one", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.6.15", 25 | "@nestjs/core": "^7.6.15", 26 | "@nestjs/graphql": "^7.10.6", 27 | "@nestjs/platform-express": "^7.6.15", 28 | "apollo-server-express": "^2.25.0", 29 | "graphql": "^15.5.0", 30 | "graphql-tools": "^7.0.5", 31 | "reflect-metadata": "^0.1.13", 32 | "rimraf": "^3.0.2", 33 | "rxjs": "^6.6.6" 34 | }, 35 | "devDependencies": { 36 | "@nestjs/cli": "^7.6.0", 37 | "@nestjs/schematics": "^7.3.0", 38 | "@nestjs/testing": "^7.6.15", 39 | "@types/express": "^4.17.11", 40 | "@types/jest": "^26.0.22", 41 | "@types/node": "^14.14.36", 42 | "@types/supertest": "^2.0.10", 43 | "@typescript-eslint/eslint-plugin": "^4.19.0", 44 | "@typescript-eslint/parser": "^4.19.0", 45 | "eslint": "^7.22.0", 46 | "eslint-config-prettier": "^8.1.0", 47 | "eslint-plugin-prettier": "^3.3.1", 48 | "jest": "^26.6.3", 49 | "prettier": "^2.2.1", 50 | "supertest": "^6.1.3", 51 | "ts-jest": "^26.5.4", 52 | "ts-loader": "^8.0.18", 53 | "ts-node": "^9.1.1", 54 | "tsconfig-paths": "^3.9.0", 55 | "typescript": "^4.2.3" 56 | }, 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "js", 60 | "json", 61 | "ts" 62 | ], 63 | "rootDir": "src", 64 | "testRegex": ".*\\.spec\\.ts$", 65 | "transform": { 66 | "^.+\\.(t|j)s$": "ts-jest" 67 | }, 68 | "collectCoverageFrom": [ 69 | "**/*.(t|j)s" 70 | ], 71 | "coverageDirectory": "../coverage", 72 | "testEnvironment": "node" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /part-one/schema.gql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type Book { 6 | id: ID! 7 | title: String! 8 | isbn: String! 9 | author: Author! 10 | } 11 | 12 | type Author { 13 | id: ID! 14 | name: String! 15 | books: [Book!]! 16 | } 17 | 18 | type Query { 19 | authors: [Author!]! 20 | books: [Book!]! 21 | book(input: FindBookInput!): Book! 22 | } 23 | 24 | input FindBookInput { 25 | id: Int! 26 | } 27 | 28 | type Mutation { 29 | createBook(input: CreateBookInput!): Book! 30 | } 31 | 32 | input CreateBookInput { 33 | id: ID! 34 | title: String! 35 | isbn: String! 36 | author: Int! 37 | } 38 | -------------------------------------------------------------------------------- /part-one/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /part-one/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /part-one/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { GraphQLModule } from '@nestjs/graphql'; 3 | import { AppController } from './app.controller'; 4 | import { AppService } from './app.service'; 5 | import { AuthorModule } from './author/author.module'; 6 | import { BookModule } from './book/book.module'; 7 | 8 | @Module({ 9 | imports: [ 10 | GraphQLModule.forRoot({ 11 | autoSchemaFile: 'schema.gql', 12 | }), 13 | 14 | AuthorModule, 15 | BookModule, 16 | ], 17 | controllers: [AppController], 18 | providers: [AppService], 19 | }) 20 | export class AppModule {} 21 | -------------------------------------------------------------------------------- /part-one/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /part-one/src/author/author.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { BookService } from '../book/book.service'; 3 | import { AuthorResolver } from './author.resolver'; 4 | import { AuthorService } from './author.service'; 5 | 6 | @Module({ 7 | providers: [AuthorResolver, AuthorService, BookService], 8 | }) 9 | export class AuthorModule {} 10 | -------------------------------------------------------------------------------- /part-one/src/author/author.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthorResolver } from './author.resolver'; 3 | 4 | describe('AuthorResolver', () => { 5 | let resolver: AuthorResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthorResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(AuthorResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-one/src/author/author.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Resolver, Query, ResolveField, Parent } from '@nestjs/graphql'; 2 | import { BookService } from '../book/book.service'; 3 | import { Author } from './author.schema'; 4 | import { AuthorService } from './author.service'; 5 | 6 | @Resolver(() => Author) 7 | export class AuthorResolver { 8 | constructor( 9 | private authorsService: AuthorService, 10 | private bookService: BookService, 11 | ) {} 12 | 13 | @Query(() => [Author]) 14 | async authors() { 15 | return this.authorsService.findMany(); 16 | } 17 | 18 | @ResolveField() 19 | async books(@Parent() parent: Author) { 20 | return this.bookService.findByAuthorId(parent.id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /part-one/src/author/author.schema.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, ID } from '@nestjs/graphql'; 2 | import { Book } from '../book/book.schema'; 3 | 4 | @ObjectType() 5 | export class Author { 6 | @Field(() => ID) // <-- GraphQL type 7 | id: string; // <-- TypeScript type 8 | 9 | @Field() 10 | name: string; 11 | 12 | @Field(() => [Book]) 13 | books: Book[]; 14 | } 15 | -------------------------------------------------------------------------------- /part-one/src/author/author.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthorService } from './author.service'; 3 | 4 | describe('AuthorService', () => { 5 | let service: AuthorService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthorService], 10 | }).compile(); 11 | 12 | service = module.get(AuthorService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-one/src/author/author.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import authors from '../data/authors'; 3 | 4 | @Injectable() 5 | export class AuthorService { 6 | async findById(id) { 7 | const result = authors.filter((item) => item.id === id); 8 | 9 | return result.length ? result[0] : null; 10 | } 11 | 12 | async findMany() { 13 | return authors; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /part-one/src/book/book.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AuthorService } from 'src/author/author.service'; 3 | import { BookResolver } from './book.resolver'; 4 | import { BookService } from './book.service'; 5 | 6 | @Module({ 7 | providers: [BookResolver, BookService, AuthorService], 8 | }) 9 | export class BookModule {} 10 | -------------------------------------------------------------------------------- /part-one/src/book/book.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookResolver } from './book.resolver'; 3 | 4 | describe('BookResolver', () => { 5 | let resolver: BookResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(BookResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-one/src/book/book.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | ResolveField, 5 | Parent, 6 | Args, 7 | Mutation, 8 | } from '@nestjs/graphql'; 9 | import { AuthorService } from '../author/author.service'; 10 | import { Author } from '../author/author.schema'; 11 | import { Book, CreateBookInput, FindBookInput } from './book.schema'; 12 | import { BookService } from './book.service'; 13 | 14 | @Resolver(() => Book) 15 | export class BookResolver { 16 | constructor( 17 | private bookService: BookService, 18 | private authorService: AuthorService, 19 | ) {} 20 | 21 | @Query(() => [Book]) // <-- what will the query return? 22 | async books /* <-- Query name */() { 23 | return this.bookService.findMany(); // Resolve the query 24 | } 25 | 26 | @Query(() => Book) 27 | async book(@Args('input') { id }: FindBookInput) { 28 | return this.bookService.findById(id); 29 | } 30 | 31 | @Mutation(() => Book) 32 | async createBook(@Args('input') book: CreateBookInput) { 33 | return this.bookService.createBook(book); 34 | } 35 | 36 | @ResolveField(() => Author) 37 | async author(@Parent() book: Book) { 38 | return this.authorService.findById(book.author); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /part-one/src/book/book.schema.ts: -------------------------------------------------------------------------------- 1 | import { ObjectType, Field, ID, InputType, Int } from '@nestjs/graphql'; 2 | import { Author } from '../author/author.schema'; 3 | 4 | @ObjectType() 5 | export class Book { 6 | @Field(() => ID) 7 | id: number; 8 | 9 | @Field() 10 | title: string; 11 | 12 | @Field() 13 | isbn: string; 14 | 15 | @Field(() => Author) 16 | author: Author | number; 17 | } 18 | 19 | @InputType() 20 | export class CreateBookInput { 21 | @Field(() => ID) 22 | id: number; 23 | 24 | @Field() 25 | title: string; 26 | 27 | @Field() 28 | isbn: string; 29 | 30 | @Field(() => Int) 31 | author: number; 32 | } 33 | 34 | @InputType() 35 | export class FindBookInput { 36 | @Field(() => Int) 37 | id: number; 38 | } 39 | -------------------------------------------------------------------------------- /part-one/src/book/book.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookService } from './book.service'; 3 | 4 | describe('BookService', () => { 5 | let service: BookService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookService], 10 | }).compile(); 11 | 12 | service = module.get(BookService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-one/src/book/book.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import books from '../data/books'; 3 | import { Book, CreateBookInput } from './book.schema'; 4 | 5 | @Injectable() 6 | export class BookService { 7 | books: Partial[]; 8 | constructor() { 9 | this.books = books; 10 | } 11 | 12 | async findMany() { 13 | return this.books; 14 | } 15 | 16 | async findById(id) { 17 | const books = this.books.filter((book) => book.id === id); 18 | if (books.length) { 19 | return books[0]; 20 | } 21 | 22 | return null; 23 | } 24 | 25 | async findByAuthorId(authorId) { 26 | return this.books.filter((book) => book.author === authorId); 27 | } 28 | 29 | async createBook(book: CreateBookInput) { 30 | this.books = [book, ...this.books]; 31 | return book; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /part-one/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(5000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /part-one/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /part-one/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /part-one/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /part-one/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /part-three/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /part-three/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /part-three/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /part-three/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /part-three/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part-one", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.6.15", 25 | "@nestjs/core": "^7.6.15", 26 | "@nestjs/graphql": "^7.10.6", 27 | "@nestjs/mapped-types": "*", 28 | "@nestjs/mongoose": "^7.2.4", 29 | "@nestjs/platform-express": "^7.6.15", 30 | "apollo-server-express": "^2.25.0", 31 | "bcrypt": "^5.0.1", 32 | "cookie-parser": "^1.4.5", 33 | "graphql": "^15.5.0", 34 | "graphql-tools": "^7.0.5", 35 | "jsonwebtoken": "^8.5.1", 36 | "mongoose": "^5.12.12", 37 | "nanoid": "^3.1.23", 38 | "reflect-metadata": "^0.1.13", 39 | "rimraf": "^3.0.2", 40 | "rxjs": "^6.6.6" 41 | }, 42 | "devDependencies": { 43 | "@nestjs/cli": "^7.6.0", 44 | "@nestjs/schematics": "^7.3.0", 45 | "@nestjs/testing": "^7.6.15", 46 | "@types/cookie-parser": "^1.4.2", 47 | "@types/express": "^4.17.11", 48 | "@types/jest": "^26.0.22", 49 | "@types/jsonwebtoken": "^8.5.1", 50 | "@types/nanoid": "^3.0.0", 51 | "@types/node": "^14.14.36", 52 | "@types/supertest": "^2.0.10", 53 | "@typescript-eslint/eslint-plugin": "^4.19.0", 54 | "@typescript-eslint/parser": "^4.19.0", 55 | "eslint": "^7.22.0", 56 | "eslint-config-prettier": "^8.1.0", 57 | "eslint-plugin-prettier": "^3.3.1", 58 | "jest": "^26.6.3", 59 | "prettier": "^2.2.1", 60 | "supertest": "^6.1.3", 61 | "ts-jest": "^26.5.4", 62 | "ts-loader": "^8.0.18", 63 | "ts-node": "^9.1.1", 64 | "tsconfig-paths": "^3.9.0", 65 | "typescript": "^4.2.3" 66 | }, 67 | "jest": { 68 | "moduleFileExtensions": [ 69 | "js", 70 | "json", 71 | "ts" 72 | ], 73 | "rootDir": "src", 74 | "testRegex": ".*\\.spec\\.ts$", 75 | "transform": { 76 | "^.+\\.(t|j)s$": "ts-jest" 77 | }, 78 | "collectCoverageFrom": [ 79 | "**/*.(t|j)s" 80 | ], 81 | "coverageDirectory": "../coverage", 82 | "testEnvironment": "node" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /part-three/schema.gql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type Author { 6 | _id: ID! 7 | name: String! 8 | books: [Book!]! 9 | } 10 | 11 | type Book { 12 | _id: ID! 13 | title: String! 14 | isbn: String! 15 | author: Author! 16 | } 17 | 18 | type User { 19 | _id: ID! 20 | email: String! 21 | name: String! 22 | } 23 | 24 | type Query { 25 | authors: [Author!]! 26 | books: [Book!]! 27 | book(input: FindBookInput!): Book! 28 | login(input: LoginInput!): User 29 | me: User 30 | logout: User 31 | } 32 | 33 | input FindBookInput { 34 | _id: String! 35 | } 36 | 37 | input LoginInput { 38 | email: String! 39 | password: String! 40 | } 41 | 42 | type Mutation { 43 | createAuthor(input: CreateAuthorInput!): Author! 44 | createBook(input: CreateBookInput!): Book! 45 | registerUser(input: CreateUserInput!): User! 46 | confirmUser(input: ConfirmUserInput!): User! 47 | } 48 | 49 | input CreateAuthorInput { 50 | name: String! 51 | } 52 | 53 | input CreateBookInput { 54 | title: String! 55 | isbn: String! 56 | author: String! 57 | } 58 | 59 | input CreateUserInput { 60 | name: String! 61 | email: String! 62 | password: String! 63 | } 64 | 65 | input ConfirmUserInput { 66 | email: String! 67 | confirmToken: String! 68 | } 69 | -------------------------------------------------------------------------------- /part-three/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /part-three/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /part-three/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { get, set } from 'lodash'; 2 | import { Module } from '@nestjs/common'; 3 | import { MongooseModule } from '@nestjs/mongoose'; 4 | import { GraphQLModule } from '@nestjs/graphql'; 5 | import { AppController } from './app.controller'; 6 | import { AppService } from './app.service'; 7 | import { AuthorModule } from './author/author.module'; 8 | import { BookModule } from './book/book.module'; 9 | import { UserModule } from './user/user.module'; 10 | import { decode } from './utils/jwt.utils'; 11 | 12 | @Module({ 13 | imports: [ 14 | MongooseModule.forRoot('mongodb://localhost/nestjs-graphql'), 15 | GraphQLModule.forRoot({ 16 | autoSchemaFile: 'schema.gql', 17 | context: ({ req, res }) => { 18 | // Get the cookie from request 19 | const token = get(req, 'cookies.token'); 20 | 21 | console.log({ token }); 22 | // Verify the cookie 23 | 24 | const user = token ? decode(get(req, 'cookies.token')) : null; 25 | 26 | // Attach the user object to the request object 27 | if (user) { 28 | set(req, 'user', user); 29 | } 30 | 31 | return { req, res }; 32 | }, 33 | }), 34 | 35 | AuthorModule, 36 | BookModule, 37 | UserModule, 38 | ], 39 | controllers: [AppController], 40 | providers: [AppService], 41 | }) 42 | export class AppModule {} 43 | -------------------------------------------------------------------------------- /part-three/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /part-three/src/author/author.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { Book, BookSchema } from 'src/book/book.schema'; 4 | import { BookService } from '../book/book.service'; 5 | import { AuthorResolver } from './author.resolver'; 6 | import { Author, AuthorSchema } from './author.schema'; 7 | import { AuthorService } from './author.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: Book.name, schema: BookSchema }, 13 | { name: Author.name, schema: AuthorSchema }, 14 | ]), 15 | ], 16 | providers: [AuthorResolver, AuthorService, BookService], 17 | }) 18 | export class AuthorModule {} 19 | -------------------------------------------------------------------------------- /part-three/src/author/author.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthorResolver } from './author.resolver'; 3 | 4 | describe('AuthorResolver', () => { 5 | let resolver: AuthorResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthorResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(AuthorResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-three/src/author/author.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | ResolveField, 5 | Parent, 6 | Mutation, 7 | Args, 8 | } from '@nestjs/graphql'; 9 | import { BookService } from '../book/book.service'; 10 | import { Author, CreateAuthorInput } from './author.schema'; 11 | import { AuthorService } from './author.service'; 12 | 13 | @Resolver(() => Author) 14 | export class AuthorResolver { 15 | constructor( 16 | private authorsService: AuthorService, 17 | private bookService: BookService, 18 | ) {} 19 | 20 | @Query(() => [Author]) 21 | async authors() { 22 | return this.authorsService.findMany(); 23 | } 24 | 25 | @Mutation(() => Author) 26 | async createAuthor(@Args('input') input: CreateAuthorInput) { 27 | return this.authorsService.createAuthor(input); 28 | } 29 | 30 | @ResolveField() 31 | async books(@Parent() parent: Author) { 32 | return this.bookService.findByAuthorId(parent._id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /part-three/src/author/author.schema.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, ID, InputType } from '@nestjs/graphql'; 2 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 3 | import * as mongoose from 'mongoose'; 4 | import { Book } from '../book/book.schema'; 5 | 6 | export type AuthorDocument = Author & mongoose.Document; 7 | 8 | @Schema() 9 | @ObjectType() 10 | export class Author { 11 | @Field(() => ID) // <-- GraphQL type 12 | _id: string; // <-- TypeScript type 13 | 14 | @Prop() 15 | @Field() 16 | name: string; 17 | 18 | @Prop({ type: { type: mongoose.Schema.Types.ObjectId, ref: 'Book' } }) 19 | @Field(() => [Book]) 20 | books: Book[]; 21 | } 22 | 23 | export const AuthorSchema = SchemaFactory.createForClass(Author); 24 | 25 | @InputType() 26 | export class CreateAuthorInput { 27 | @Field() 28 | name: string; 29 | } 30 | -------------------------------------------------------------------------------- /part-three/src/author/author.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthorService } from './author.service'; 3 | 4 | describe('AuthorService', () => { 5 | let service: AuthorService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthorService], 10 | }).compile(); 11 | 12 | service = module.get(AuthorService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-three/src/author/author.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import authors from '../data/authors'; 5 | import { Author, AuthorDocument } from './author.schema'; 6 | 7 | @Injectable() 8 | export class AuthorService { 9 | constructor( 10 | @InjectModel(Author.name) private authorModel: Model, 11 | ) {} 12 | 13 | async findById(id) { 14 | return this.authorModel.findById(id).lean(); 15 | } 16 | 17 | async findMany() { 18 | return this.authorModel.find().lean(); 19 | } 20 | 21 | async createAuthor(input) { 22 | return this.authorModel.create(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /part-three/src/book/book.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { Author, AuthorSchema } from 'src/author/author.schema'; 4 | import { AuthorService } from 'src/author/author.service'; 5 | import { BookResolver } from './book.resolver'; 6 | import { Book, BookSchema } from './book.schema'; 7 | import { BookService } from './book.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: Book.name, schema: BookSchema }, 13 | { name: Author.name, schema: AuthorSchema }, 14 | ]), 15 | ], 16 | providers: [BookResolver, BookService, AuthorService], 17 | }) 18 | export class BookModule {} 19 | -------------------------------------------------------------------------------- /part-three/src/book/book.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookResolver } from './book.resolver'; 3 | 4 | describe('BookResolver', () => { 5 | let resolver: BookResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(BookResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-three/src/book/book.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | ResolveField, 5 | Parent, 6 | Args, 7 | Mutation, 8 | } from '@nestjs/graphql'; 9 | import { AuthorService } from '../author/author.service'; 10 | import { Author } from '../author/author.schema'; 11 | import { Book, CreateBookInput, FindBookInput } from './book.schema'; 12 | import { BookService } from './book.service'; 13 | 14 | @Resolver(() => Book) 15 | export class BookResolver { 16 | constructor( 17 | private bookService: BookService, 18 | private authorService: AuthorService, 19 | ) {} 20 | 21 | @Query(() => [Book]) // <-- what will the query return? 22 | async books /* <-- Query name */() { 23 | return this.bookService.findMany(); // Resolve the query 24 | } 25 | 26 | @Query(() => Book) 27 | async book(@Args('input') { _id }: FindBookInput) { 28 | return this.bookService.findById(_id); 29 | } 30 | 31 | @Mutation(() => Book) 32 | async createBook(@Args('input') book: CreateBookInput) { 33 | return this.bookService.createBook(book); 34 | } 35 | 36 | @ResolveField(() => Author) 37 | async author(@Parent() book: Book) { 38 | return this.authorService.findById(book.author); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /part-three/src/book/book.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import * as mongoose from 'mongoose'; 3 | import { ObjectType, Field, ID, InputType, Int } from '@nestjs/graphql'; 4 | import { Author } from '../author/author.schema'; 5 | 6 | export type BookDocument = Book & mongoose.Document; 7 | 8 | @Schema() 9 | @ObjectType() 10 | export class Book { 11 | @Field(() => ID) 12 | _id: number; 13 | 14 | @Prop({ required: true }) 15 | @Field() 16 | title: string; 17 | 18 | @Prop({ required: true }) 19 | @Field() 20 | isbn: string; 21 | 22 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: Author.name }) 23 | @Field(() => Author) 24 | author: Author | number; 25 | } 26 | 27 | export const BookSchema = SchemaFactory.createForClass(Book); 28 | 29 | BookSchema.index({ author: 1 }); 30 | 31 | @InputType() 32 | export class CreateBookInput { 33 | @Field() 34 | title: string; 35 | 36 | @Field() 37 | isbn: string; 38 | 39 | @Field() 40 | author: string; 41 | } 42 | 43 | @InputType() 44 | export class FindBookInput { 45 | @Field() 46 | _id: string; 47 | } 48 | -------------------------------------------------------------------------------- /part-three/src/book/book.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookService } from './book.service'; 3 | 4 | describe('BookService', () => { 5 | let service: BookService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookService], 10 | }).compile(); 11 | 12 | service = module.get(BookService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-three/src/book/book.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import books from '../data/books'; 5 | import { Book, CreateBookInput, BookDocument } from './book.schema'; 6 | 7 | @Injectable() 8 | export class BookService { 9 | books: Partial[]; 10 | constructor(@InjectModel(Book.name) private bookModel: Model) { 11 | this.books = books; 12 | } 13 | 14 | async findMany() { 15 | return this.bookModel.find().lean(); 16 | } 17 | 18 | async findById(id) { 19 | return this.bookModel.findById(id).lean(); 20 | } 21 | 22 | async findByAuthorId(authorId) { 23 | return this.bookModel.find({ author: authorId }); 24 | } 25 | 26 | async createBook(book: CreateBookInput) { 27 | return this.bookModel.create(book); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /part-three/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | import cookieParser from 'cookie-parser'; 4 | 5 | async function bootstrap() { 6 | const app = await NestFactory.create(AppModule); 7 | 8 | app.use(cookieParser()); 9 | 10 | await app.listen(5000); 11 | } 12 | bootstrap(); 13 | -------------------------------------------------------------------------------- /part-three/src/types/context.type.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | import { User } from '../user/user.schema'; 3 | 4 | type Ctx = { 5 | req: Request & { user?: Pick }; 6 | res: Response; 7 | }; 8 | 9 | export default Ctx; 10 | -------------------------------------------------------------------------------- /part-three/src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserService } from './user.service'; 3 | import { UserResolver } from './user.resolver'; 4 | import { MongooseModule } from '@nestjs/mongoose'; 5 | import { User, UserSchema } from './user.schema'; 6 | 7 | @Module({ 8 | imports: [ 9 | MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]), 10 | ], 11 | providers: [UserResolver, UserService], 12 | }) 13 | export class UserModule {} 14 | -------------------------------------------------------------------------------- /part-three/src/user/user.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserResolver } from './user.resolver'; 3 | import { UserService } from './user.service'; 4 | 5 | describe('UserResolver', () => { 6 | let resolver: UserResolver; 7 | 8 | beforeEach(async () => { 9 | const module: TestingModule = await Test.createTestingModule({ 10 | providers: [UserResolver, UserService], 11 | }).compile(); 12 | 13 | resolver = module.get(UserResolver); 14 | }); 15 | 16 | it('should be defined', () => { 17 | expect(resolver).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /part-three/src/user/user.resolver.ts: -------------------------------------------------------------------------------- 1 | import { Args, Mutation, Resolver, Query, Context } from '@nestjs/graphql'; 2 | import { 3 | ConfirmUserInput, 4 | CreateUserInput, 5 | LoginInput, 6 | User, 7 | } from './user.schema'; 8 | import { UserService } from './user.service'; 9 | import Ctx from '../types/context.type'; 10 | 11 | @Resolver('User') 12 | export class UserResolver { 13 | constructor(private readonly userService: UserService) {} 14 | 15 | @Mutation(() => User) 16 | async registerUser(@Args('input') input: CreateUserInput) { 17 | return this.userService.createUser(input); 18 | } 19 | 20 | @Mutation(() => User) 21 | async confirmUser(@Args('input') input: ConfirmUserInput) { 22 | return this.userService.confirmUser(input); 23 | } 24 | 25 | @Query(() => User, { nullable: true }) 26 | async login(@Args('input') input: LoginInput, @Context() context: Ctx) { 27 | return this.userService.login(input, context); 28 | } 29 | 30 | @Query(() => User, { nullable: true }) 31 | async me(@Context() context: Ctx) { 32 | return context.req.user; 33 | } 34 | 35 | @Query(() => User, { nullable: true }) 36 | async logout(@Context() context: Ctx) { 37 | return this.userService.logout(context); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /part-three/src/user/user.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import * as mongoose from 'mongoose'; 3 | import * as bcrypt from 'bcrypt'; 4 | import { Field, ID, InputType, ObjectType } from '@nestjs/graphql'; 5 | 6 | @Schema() 7 | @ObjectType() 8 | export class User { 9 | @Field(() => ID) //<- GraphQL 10 | _id: number; //<- TypeScript 11 | 12 | @Prop({ required: true, unique: true }) //<- Mongoose 13 | @Field() 14 | email: string; 15 | 16 | @Prop({ required: true }) 17 | @Field() 18 | name: string; 19 | 20 | @Prop({ required: true }) 21 | password: string; 22 | 23 | @Prop({ required: true }) 24 | confirmToken: string; 25 | 26 | @Prop({ required: true, default: false }) 27 | active: boolean; 28 | 29 | comparePassword: (candidatePassword: string) => boolean; 30 | } 31 | 32 | export type UserDocument = User & mongoose.Document; 33 | 34 | export const UserSchema = SchemaFactory.createForClass(User); 35 | 36 | UserSchema.index({ email: 1 }); 37 | 38 | UserSchema.pre('save', async function (next: mongoose.HookNextFunction) { 39 | let user = this as UserDocument; 40 | 41 | // only hash the password if it has been modified (or is new) 42 | if (!user.isModified('password')) { 43 | return next(); 44 | } 45 | 46 | // Random additional data 47 | const salt = await bcrypt.genSalt(10); 48 | 49 | const hash = await bcrypt.hashSync(user.password, salt); 50 | 51 | // Replace the password with the hash 52 | user.password = hash; 53 | 54 | return next(); 55 | }); 56 | 57 | UserSchema.methods.comparePassword = async function ( 58 | candidatePassword: string, 59 | ) { 60 | const user = this as UserDocument; 61 | 62 | return bcrypt.compare(candidatePassword, user.password).catch((e) => false); 63 | }; 64 | 65 | @InputType() 66 | export class CreateUserInput { 67 | @Field() 68 | name: string; 69 | 70 | @Field() 71 | email: string; 72 | 73 | @Field() 74 | password: string; 75 | } 76 | 77 | @InputType() 78 | export class ConfirmUserInput { 79 | @Field() 80 | email: string; 81 | 82 | @Field() 83 | confirmToken: string; 84 | } 85 | 86 | @InputType() 87 | export class LoginInput { 88 | @Field() 89 | email: string; 90 | 91 | @Field() 92 | password: string; 93 | } 94 | -------------------------------------------------------------------------------- /part-three/src/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService } from './user.service'; 3 | 4 | describe('UserService', () => { 5 | let service: UserService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [UserService], 10 | }).compile(); 11 | 12 | service = module.get(UserService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-three/src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { omit } from 'lodash'; 2 | import { Injectable } from '@nestjs/common'; 3 | import { InjectModel } from '@nestjs/mongoose'; 4 | import { Model } from 'mongoose'; 5 | import { nanoid } from 'nanoid'; 6 | import Ctx from 'src/types/context.type'; 7 | import { 8 | User, 9 | UserDocument, 10 | CreateUserInput, 11 | ConfirmUserInput, 12 | LoginInput, 13 | } from './user.schema'; 14 | import { signJwt } from '../utils/jwt.utils'; 15 | import { CookieOptions } from 'express'; 16 | 17 | const cookieOptions: CookieOptions = { 18 | domain: 'localhost', // <- Change to your client domain 19 | secure: false, // <- Should be true if !development 20 | sameSite: 'strict', 21 | httpOnly: true, 22 | path: '/', 23 | }; 24 | 25 | @Injectable() 26 | export class UserService { 27 | constructor(@InjectModel(User.name) private userModel: Model) {} 28 | 29 | async createUser(input: CreateUserInput) { 30 | const confirmToken = nanoid(32); 31 | return this.userModel.create({ ...input, confirmToken }); 32 | } 33 | 34 | async confirmUser({ email, confirmToken }: ConfirmUserInput) { 35 | // find our user 36 | const user = await this.userModel.findOne({ email }); 37 | // Check if the user exists 38 | // Check if the confirmation tokens === confirmToken 39 | if (!user || confirmToken !== user.confirmToken) { 40 | throw new Error('Email or confirm token are incorrect'); 41 | } 42 | 43 | // change active to true 44 | user.active = true; 45 | 46 | // save the user 47 | await user.save(); 48 | 49 | // return user 50 | return user; 51 | } 52 | 53 | async login({ email, password }: LoginInput, context: Ctx) { 54 | // Find our user 55 | const user = await this.userModel 56 | .findOne({ email }) 57 | .select('-__v -confirmToken'); 58 | 59 | // Check that user exists 60 | // Compare input password with the user's hashed password 61 | if (!user || !(await user.comparePassword(password))) { 62 | throw new Error('Invalid email or password'); 63 | } 64 | 65 | // Check that the user is active 66 | if (!user.active) { 67 | throw new Error('Please confirm your email address'); 68 | } 69 | // Create a JWT 70 | const jwt = signJwt(omit(user.toJSON(), ['password', 'active'])); 71 | 72 | // Set the JWT in a cookie 73 | context.res.cookie('token', jwt, cookieOptions); 74 | 75 | // return the user 76 | return user; 77 | } 78 | 79 | async logout(context) { 80 | context.res.cookie('token', '', { ...cookieOptions, maxAge: 0 }); 81 | return null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /part-three/src/utils/jwt.utils.ts: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | const privateKey = `-----BEGIN RSA PRIVATE KEY----- 4 | MIICXAIBAAKBgQCGhLlpouwodWJ7O+ErKGXSGw18oKN7OKgaxxa878PWRAdKGxDC 5 | Lape3t/MS12R8kq98qppBqaTq9whX1k/cYIPsBqbLFib1ZXwlJ2635lp+X1h2ij8 6 | w1IMYkCtkpcaS9J1NWyRp2YGRFIb4fSu1DLVeBEt+JoQhUikQmmbYEgrqQIDAQAB 7 | AoGAA9BLeWU3EMaTLi86aQDSMiy2KyxatimbCndOe7jIcpCnLECsLvUgeWQUMDSK 8 | yJJ37/6rOiMIIfOjYAbV/uh26XReoxgrrd+keiohSAmoS8J+gTZbPyniLbnpc/em 9 | BfdiF2etJ6G5RD4bAWSnYZMFR3zLTv0w+bUhK5W3bYuyHSECQQDNvjAMT9JSRB3e 10 | s0zsDFjtqY2ogDNkEzhOPJc7+cT9K9LSEZpdq9HrDGih714Y/NlkM8Lublfsxz2O 11 | qJh3173NAkEAp2CgMqqJrOGx1a6b12W9t3fUoF+Nay9zV6R7mDNLMW7ckHiL+Nzu 12 | KoehHBcyFpS1hdbVnVPUadBj0wdh0rdpTQJACAiYGbL4NGwienKn20O0KTuIo362 13 | Av9ZIHzvLtFW5sfSSI+VZnjyDmqCn2gYVYNx/Z6jyumWF1HETpC4u106NQJBAJfP 14 | At2RYdekyLfna7+crsrl67sdj1WlLvdR3yu3cj6+r1x1iXbJY1a9tzmULEDg1hcz 15 | OII+k0z5zmCLtIT7B40CQFRsWGw6bw2z+QHfdLTClV3OQndvPMV4VAdnJFxBqcAA 16 | 2ZRCLbcV9QV/YsXf/k0xT0l8tuWgfR0RSKR8WTSikzU= 17 | -----END RSA PRIVATE KEY-----`; 18 | 19 | const publicKey = `-----BEGIN PUBLIC KEY----- 20 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCGhLlpouwodWJ7O+ErKGXSGw18 21 | oKN7OKgaxxa878PWRAdKGxDCLape3t/MS12R8kq98qppBqaTq9whX1k/cYIPsBqb 22 | LFib1ZXwlJ2635lp+X1h2ij8w1IMYkCtkpcaS9J1NWyRp2YGRFIb4fSu1DLVeBEt 23 | +JoQhUikQmmbYEgrqQIDAQAB 24 | -----END PUBLIC KEY-----`; 25 | 26 | export function signJwt(payload) { 27 | return jwt.sign(payload, privateKey, { algorithm: 'RS256' }); 28 | } 29 | 30 | export function decode(token: string) { 31 | if (!token) return null; 32 | try { 33 | const decoded = jwt.verify(token, publicKey); 34 | 35 | return decoded; 36 | } catch (error) { 37 | console.error(`error`, error); 38 | return null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /part-three/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /part-three/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /part-three/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /part-three/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "resolveJsonModule": true, 15 | "esModuleInterop": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /part-two/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /part-two/.gitignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist 3 | /node_modules 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # OS 14 | .DS_Store 15 | 16 | # Tests 17 | /coverage 18 | /.nyc_output 19 | 20 | # IDEs and editors 21 | /.idea 22 | .project 23 | .classpath 24 | .c9/ 25 | *.launch 26 | .settings/ 27 | *.sublime-workspace 28 | 29 | # IDE - VSCode 30 | .vscode/* 31 | !.vscode/settings.json 32 | !.vscode/tasks.json 33 | !.vscode/launch.json 34 | !.vscode/extensions.json -------------------------------------------------------------------------------- /part-two/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /part-two/nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /part-two/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "part-one", 3 | "version": "0.0.1", 4 | "description": "", 5 | "author": "", 6 | "private": true, 7 | "license": "UNLICENSED", 8 | "scripts": { 9 | "prebuild": "rimraf dist", 10 | "build": "nest build", 11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 12 | "start": "nest start", 13 | "dev": "nest start --watch", 14 | "start:debug": "nest start --debug --watch", 15 | "start:prod": "node dist/main", 16 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 17 | "test": "jest", 18 | "test:watch": "jest --watch", 19 | "test:cov": "jest --coverage", 20 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 21 | "test:e2e": "jest --config ./test/jest-e2e.json" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^7.6.15", 25 | "@nestjs/core": "^7.6.15", 26 | "@nestjs/graphql": "^7.10.6", 27 | "@nestjs/mongoose": "^7.2.4", 28 | "@nestjs/platform-express": "^7.6.15", 29 | "apollo-server-express": "^2.25.0", 30 | "graphql": "^15.5.0", 31 | "graphql-tools": "^7.0.5", 32 | "mongoose": "^5.12.12", 33 | "reflect-metadata": "^0.1.13", 34 | "rimraf": "^3.0.2", 35 | "rxjs": "^6.6.6" 36 | }, 37 | "devDependencies": { 38 | "@nestjs/cli": "^7.6.0", 39 | "@nestjs/schematics": "^7.3.0", 40 | "@nestjs/testing": "^7.6.15", 41 | "@types/express": "^4.17.11", 42 | "@types/jest": "^26.0.22", 43 | "@types/node": "^14.14.36", 44 | "@types/supertest": "^2.0.10", 45 | "@typescript-eslint/eslint-plugin": "^4.19.0", 46 | "@typescript-eslint/parser": "^4.19.0", 47 | "eslint": "^7.22.0", 48 | "eslint-config-prettier": "^8.1.0", 49 | "eslint-plugin-prettier": "^3.3.1", 50 | "jest": "^26.6.3", 51 | "prettier": "^2.2.1", 52 | "supertest": "^6.1.3", 53 | "ts-jest": "^26.5.4", 54 | "ts-loader": "^8.0.18", 55 | "ts-node": "^9.1.1", 56 | "tsconfig-paths": "^3.9.0", 57 | "typescript": "^4.2.3" 58 | }, 59 | "jest": { 60 | "moduleFileExtensions": [ 61 | "js", 62 | "json", 63 | "ts" 64 | ], 65 | "rootDir": "src", 66 | "testRegex": ".*\\.spec\\.ts$", 67 | "transform": { 68 | "^.+\\.(t|j)s$": "ts-jest" 69 | }, 70 | "collectCoverageFrom": [ 71 | "**/*.(t|j)s" 72 | ], 73 | "coverageDirectory": "../coverage", 74 | "testEnvironment": "node" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /part-two/schema.gql: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------ 2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) 3 | # ------------------------------------------------------ 4 | 5 | type Author { 6 | _id: ID! 7 | name: String! 8 | books: [Book!]! 9 | } 10 | 11 | type Book { 12 | _id: ID! 13 | title: String! 14 | isbn: String! 15 | author: Author! 16 | } 17 | 18 | type Query { 19 | authors: [Author!]! 20 | books: [Book!]! 21 | book(input: FindBookInput!): Book! 22 | } 23 | 24 | input FindBookInput { 25 | _id: String! 26 | } 27 | 28 | type Mutation { 29 | createAuthor(input: CreateAuthorInput!): Author! 30 | createBook(input: CreateBookInput!): Book! 31 | } 32 | 33 | input CreateAuthorInput { 34 | name: String! 35 | } 36 | 37 | input CreateBookInput { 38 | title: String! 39 | isbn: String! 40 | author: String! 41 | } 42 | -------------------------------------------------------------------------------- /part-two/src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let appController: AppController; 7 | 8 | beforeEach(async () => { 9 | const app: TestingModule = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | 14 | appController = app.get(AppController); 15 | }); 16 | 17 | describe('root', () => { 18 | it('should return "Hello World!"', () => { 19 | expect(appController.getHello()).toBe('Hello World!'); 20 | }); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /part-two/src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /part-two/src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { GraphQLModule } from '@nestjs/graphql'; 4 | import { AppController } from './app.controller'; 5 | import { AppService } from './app.service'; 6 | import { AuthorModule } from './author/author.module'; 7 | import { BookModule } from './book/book.module'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forRoot('mongodb://localhost/nestjs-graphql'), 12 | GraphQLModule.forRoot({ 13 | autoSchemaFile: 'schema.gql', 14 | }), 15 | 16 | AuthorModule, 17 | BookModule, 18 | ], 19 | controllers: [AppController], 20 | providers: [AppService], 21 | }) 22 | export class AppModule {} 23 | -------------------------------------------------------------------------------- /part-two/src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /part-two/src/author/author.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { Book, BookSchema } from 'src/book/book.schema'; 4 | import { BookService } from '../book/book.service'; 5 | import { AuthorResolver } from './author.resolver'; 6 | import { Author, AuthorSchema } from './author.schema'; 7 | import { AuthorService } from './author.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: Book.name, schema: BookSchema }, 13 | { name: Author.name, schema: AuthorSchema }, 14 | ]), 15 | ], 16 | providers: [AuthorResolver, AuthorService, BookService], 17 | }) 18 | export class AuthorModule {} 19 | -------------------------------------------------------------------------------- /part-two/src/author/author.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthorResolver } from './author.resolver'; 3 | 4 | describe('AuthorResolver', () => { 5 | let resolver: AuthorResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthorResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(AuthorResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-two/src/author/author.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | ResolveField, 5 | Parent, 6 | Mutation, 7 | Args, 8 | } from '@nestjs/graphql'; 9 | import { BookService } from '../book/book.service'; 10 | import { Author, CreateAuthorInput } from './author.schema'; 11 | import { AuthorService } from './author.service'; 12 | 13 | @Resolver(() => Author) 14 | export class AuthorResolver { 15 | constructor( 16 | private authorsService: AuthorService, 17 | private bookService: BookService, 18 | ) {} 19 | 20 | @Query(() => [Author]) 21 | async authors() { 22 | return this.authorsService.findMany(); 23 | } 24 | 25 | @Mutation(() => Author) 26 | async createAuthor(@Args('input') input: CreateAuthorInput) { 27 | return this.authorsService.createAuthor(input); 28 | } 29 | 30 | @ResolveField() 31 | async books(@Parent() parent: Author) { 32 | return this.bookService.findByAuthorId(parent._id); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /part-two/src/author/author.schema.ts: -------------------------------------------------------------------------------- 1 | import { Field, ObjectType, ID, InputType } from '@nestjs/graphql'; 2 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 3 | import * as mongoose from 'mongoose'; 4 | import { Book } from '../book/book.schema'; 5 | 6 | export type AuthorDocument = Author & mongoose.Document; 7 | 8 | @Schema() 9 | @ObjectType() 10 | export class Author { 11 | @Field(() => ID) // <-- GraphQL type 12 | _id: string; // <-- TypeScript type 13 | 14 | @Prop() 15 | @Field() 16 | name: string; 17 | 18 | @Prop({ type: { type: mongoose.Schema.Types.ObjectId, ref: 'Book' } }) 19 | @Field(() => [Book]) 20 | books: Book[]; 21 | } 22 | 23 | export const AuthorSchema = SchemaFactory.createForClass(Author); 24 | 25 | @InputType() 26 | export class CreateAuthorInput { 27 | @Field() 28 | name: string; 29 | } 30 | -------------------------------------------------------------------------------- /part-two/src/author/author.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthorService } from './author.service'; 3 | 4 | describe('AuthorService', () => { 5 | let service: AuthorService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthorService], 10 | }).compile(); 11 | 12 | service = module.get(AuthorService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-two/src/author/author.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import authors from '../data/authors'; 5 | import { Author, AuthorDocument } from './author.schema'; 6 | 7 | @Injectable() 8 | export class AuthorService { 9 | constructor( 10 | @InjectModel(Author.name) private authorModel: Model, 11 | ) {} 12 | 13 | async findById(id) { 14 | return this.authorModel.findById(id).lean(); 15 | } 16 | 17 | async findMany() { 18 | return this.authorModel.find().lean(); 19 | } 20 | 21 | async createAuthor(input) { 22 | return this.authorModel.create(input); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /part-two/src/book/book.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MongooseModule } from '@nestjs/mongoose'; 3 | import { Author, AuthorSchema } from 'src/author/author.schema'; 4 | import { AuthorService } from 'src/author/author.service'; 5 | import { BookResolver } from './book.resolver'; 6 | import { Book, BookSchema } from './book.schema'; 7 | import { BookService } from './book.service'; 8 | 9 | @Module({ 10 | imports: [ 11 | MongooseModule.forFeature([ 12 | { name: Book.name, schema: BookSchema }, 13 | { name: Author.name, schema: AuthorSchema }, 14 | ]), 15 | ], 16 | providers: [BookResolver, BookService, AuthorService], 17 | }) 18 | export class BookModule {} 19 | -------------------------------------------------------------------------------- /part-two/src/book/book.resolver.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookResolver } from './book.resolver'; 3 | 4 | describe('BookResolver', () => { 5 | let resolver: BookResolver; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookResolver], 10 | }).compile(); 11 | 12 | resolver = module.get(BookResolver); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(resolver).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-two/src/book/book.resolver.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Resolver, 3 | Query, 4 | ResolveField, 5 | Parent, 6 | Args, 7 | Mutation, 8 | } from '@nestjs/graphql'; 9 | import { AuthorService } from '../author/author.service'; 10 | import { Author } from '../author/author.schema'; 11 | import { Book, CreateBookInput, FindBookInput } from './book.schema'; 12 | import { BookService } from './book.service'; 13 | 14 | @Resolver(() => Book) 15 | export class BookResolver { 16 | constructor( 17 | private bookService: BookService, 18 | private authorService: AuthorService, 19 | ) {} 20 | 21 | @Query(() => [Book]) // <-- what will the query return? 22 | async books /* <-- Query name */() { 23 | return this.bookService.findMany(); // Resolve the query 24 | } 25 | 26 | @Query(() => Book) 27 | async book(@Args('input') { _id }: FindBookInput) { 28 | return this.bookService.findById(_id); 29 | } 30 | 31 | @Mutation(() => Book) 32 | async createBook(@Args('input') book: CreateBookInput) { 33 | return this.bookService.createBook(book); 34 | } 35 | 36 | @ResolveField(() => Author) 37 | async author(@Parent() book: Book) { 38 | return this.authorService.findById(book.author); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /part-two/src/book/book.schema.ts: -------------------------------------------------------------------------------- 1 | import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; 2 | import * as mongoose from 'mongoose'; 3 | import { ObjectType, Field, ID, InputType, Int } from '@nestjs/graphql'; 4 | import { Author } from '../author/author.schema'; 5 | 6 | export type BookDocument = Book & mongoose.Document; 7 | 8 | @Schema() 9 | @ObjectType() 10 | export class Book { 11 | @Field(() => ID) 12 | _id: number; 13 | 14 | @Prop({ required: true }) 15 | @Field() 16 | title: string; 17 | 18 | @Prop({ required: true }) 19 | @Field() 20 | isbn: string; 21 | 22 | @Prop({ type: mongoose.Schema.Types.ObjectId, ref: Author.name }) 23 | @Field(() => Author) 24 | author: Author | number; 25 | } 26 | 27 | export const BookSchema = SchemaFactory.createForClass(Book); 28 | 29 | BookSchema.index({ author: 1 }); 30 | 31 | @InputType() 32 | export class CreateBookInput { 33 | @Field() 34 | title: string; 35 | 36 | @Field() 37 | isbn: string; 38 | 39 | @Field() 40 | author: string; 41 | } 42 | 43 | @InputType() 44 | export class FindBookInput { 45 | @Field() 46 | _id: string; 47 | } 48 | -------------------------------------------------------------------------------- /part-two/src/book/book.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BookService } from './book.service'; 3 | 4 | describe('BookService', () => { 5 | let service: BookService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [BookService], 10 | }).compile(); 11 | 12 | service = module.get(BookService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /part-two/src/book/book.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectModel } from '@nestjs/mongoose'; 3 | import { Model } from 'mongoose'; 4 | import books from '../data/books'; 5 | import { Book, CreateBookInput, BookDocument } from './book.schema'; 6 | 7 | @Injectable() 8 | export class BookService { 9 | books: Partial[]; 10 | constructor(@InjectModel(Book.name) private bookModel: Model) { 11 | this.books = books; 12 | } 13 | 14 | async findMany() { 15 | return this.bookModel.find().lean(); 16 | } 17 | 18 | async findById(id) { 19 | return this.bookModel.findById(id).lean(); 20 | } 21 | 22 | async findByAuthorId(authorId) { 23 | return this.bookModel.find({ author: authorId }); 24 | } 25 | 26 | async createBook(book: CreateBookInput) { 27 | return this.bookModel.create(book); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /part-two/src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | async function bootstrap() { 5 | const app = await NestFactory.create(AppModule); 6 | await app.listen(5000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /part-two/test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { INestApplication } from '@nestjs/common'; 3 | import * as request from 'supertest'; 4 | import { AppModule } from './../src/app.module'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeEach(async () => { 10 | const moduleFixture: TestingModule = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /part-two/test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /part-two/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /part-two/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true 14 | } 15 | } 16 | --------------------------------------------------------------------------------