├── .nvmrc ├── .gitignore ├── src ├── auth │ ├── interfaces │ │ ├── index.ts │ │ └── jwt-payload.interface.ts │ ├── index.ts │ ├── auth.module.ts │ ├── jwt.strategy.ts │ ├── auth.service.ts │ ├── auth.controller.ts │ └── auth.service.spec.ts ├── user │ ├── index.ts │ ├── user.module.ts │ ├── user.controller.ts │ ├── user.service.ts │ └── user.service.spec.ts ├── config │ ├── slugify.ts │ ├── jwt.ts │ └── database.ts ├── entities │ ├── index.ts │ ├── base.entity.ts │ ├── user.entity.ts │ └── blog.entity.ts ├── models │ ├── index.ts │ ├── auth.model.ts │ ├── blog.model.ts │ └── user.model.ts ├── blog │ ├── index.ts │ ├── slug.provider.ts │ ├── blog.module.ts │ ├── slug.provider.spec.ts │ ├── blog.controller.ts │ ├── blog.service.ts │ └── blog.service.spec.ts ├── paginate │ ├── pagination.options.interface.ts │ ├── index.ts │ ├── pagination.results.interface.ts │ └── pagination.ts ├── main.ts ├── main.hmr.ts └── app.module.ts ├── .prettierrc ├── testdb.sh ├── nodemon.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── docker-compose.yml ├── tsconfig.json ├── .env.dist ├── webpack.config.js ├── tslint.json ├── LICENSE ├── .github └── workflows │ └── main.yml ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | dist/ 4 | coverage/ -------------------------------------------------------------------------------- /src/auth/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jwt-payload.interface'; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } -------------------------------------------------------------------------------- /src/user/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user.module'; 2 | export * from './user.service'; 3 | -------------------------------------------------------------------------------- /src/config/slugify.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | replacement: '-', 3 | lower: true, 4 | }; 5 | -------------------------------------------------------------------------------- /src/entities/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blog.entity'; 2 | export * from './user.entity'; 3 | -------------------------------------------------------------------------------- /src/auth/interfaces/jwt-payload.interface.ts: -------------------------------------------------------------------------------- 1 | export interface JwtPayloadInterface { 2 | id: number; 3 | } 4 | -------------------------------------------------------------------------------- /src/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from './auth.module'; 2 | export * from './auth.service'; 3 | export * from './interfaces'; 4 | -------------------------------------------------------------------------------- /src/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blog.model'; 2 | export * from './user.model'; 3 | export * from './auth.model'; 4 | -------------------------------------------------------------------------------- /src/blog/index.ts: -------------------------------------------------------------------------------- 1 | export * from './blog.service'; 2 | export * from './blog.controller'; 3 | export * from './blog.module'; 4 | -------------------------------------------------------------------------------- /src/paginate/pagination.options.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationOptionsInterface { 2 | limit: number; 3 | page: number; 4 | } 5 | -------------------------------------------------------------------------------- /src/paginate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pagination.options.interface'; 2 | export * from './pagination.results.interface'; 3 | export * from './pagination'; 4 | -------------------------------------------------------------------------------- /src/config/jwt.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | secretOrPrivateKey: process.env.JWT_SECRET, 3 | signOptions: { 4 | expiresIn: process.env.JWT_EXPIRES, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /testdb.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | while [ "$(docker inspect -f '{{.State.Health.Status}}' $(docker ps --last 1 --format '{{.Names}}'))" != "healthy" ]; 3 | do 4 | docker-compose logs db 5 | sleep 1 6 | done -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "src" 4 | ], 5 | "ext": "ts", 6 | "ignore": [ 7 | "src/**/*.spec.ts" 8 | ], 9 | "exec": "ts-node -r tsconfig-paths/register src/main.ts" 10 | } -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testRegex": ".e2e-spec.ts$", 5 | "transform": { 6 | "^.+\\.(t|j)s$": "ts-jest" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/models/auth.model.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class AuthModel { 4 | @IsEmail() 5 | email: string; 6 | 7 | @IsString() 8 | password: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/paginate/pagination.results.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PaginationResultInterface { 2 | results: PaginationEntity[]; 3 | total: number; 4 | next?: string; 5 | previous?: string; 6 | } 7 | -------------------------------------------------------------------------------- /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(3000); 7 | } 8 | bootstrap(); 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | services: 3 | db: 4 | image: mysql:5.7 5 | env_file: .env 6 | ports: 7 | - 3307:3306 8 | healthcheck: 9 | test: "cat /proc/net/tcp /proc/net/tcp6 | grep ':0CEB'" 10 | interval: 10s 11 | timeout: 5s 12 | retries: 5 -------------------------------------------------------------------------------- /src/models/blog.model.ts: -------------------------------------------------------------------------------- 1 | import { IsString, IsDate } from 'class-validator'; 2 | 3 | export class BlogModel { 4 | readonly id: number; 5 | 6 | slug: string; 7 | 8 | @IsString() 9 | title: string; 10 | 11 | @IsString() 12 | content: string; 13 | 14 | @IsDate() 15 | publish_at?: Date; 16 | } 17 | -------------------------------------------------------------------------------- /src/models/user.model.ts: -------------------------------------------------------------------------------- 1 | import { IsEmail, IsString } from 'class-validator'; 2 | 3 | export class UserModel { 4 | readonly id: number; 5 | 6 | @IsEmail() 7 | email: string; 8 | 9 | @IsString() 10 | password: string; 11 | 12 | @IsString() 13 | firstname: string; 14 | 15 | @IsString() 16 | lastname: string; 17 | } 18 | -------------------------------------------------------------------------------- /src/entities/base.entity.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PrimaryGeneratedColumn, 3 | CreateDateColumn, 4 | UpdateDateColumn, 5 | } from 'typeorm'; 6 | 7 | abstract class BaseEntity { 8 | @PrimaryGeneratedColumn() 9 | id: number; 10 | 11 | @CreateDateColumn() 12 | created; 13 | 14 | @UpdateDateColumn() 15 | updated; 16 | } 17 | 18 | export default BaseEntity; 19 | -------------------------------------------------------------------------------- /src/main.hmr.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { AppModule } from './app.module'; 3 | 4 | declare const module: any; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | await app.listen(3000); 9 | 10 | if (module.hot) { 11 | module.hot.accept(); 12 | module.hot.dispose(() => app.close()); 13 | } 14 | } 15 | bootstrap(); 16 | -------------------------------------------------------------------------------- /src/entities/user.entity.ts: -------------------------------------------------------------------------------- 1 | import BaseEntity from './base.entity'; 2 | import { Entity, Column } from 'typeorm'; 3 | 4 | @Entity() 5 | export class UserEntity extends BaseEntity { 6 | @Column({ 7 | unique: true, 8 | }) 9 | email: string; 10 | 11 | @Column({ 12 | select: false, 13 | }) 14 | password: string; 15 | 16 | @Column() 17 | firstname: string; 18 | 19 | @Column() 20 | lastname: string; 21 | } 22 | -------------------------------------------------------------------------------- /src/blog/slug.provider.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectConfig } from 'nestjs-config'; 3 | const slugify = require('slugify'); 4 | 5 | @Injectable() 6 | export class SlugProvider { 7 | constructor(@InjectConfig() private readonly config) {} 8 | 9 | slugify(slug: string): Promise { 10 | return slugify(slug, this.config.get('slugify')); 11 | } 12 | 13 | replacement(): string { 14 | return this.config.get('slugify.replacement'); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/entities/blog.entity.ts: -------------------------------------------------------------------------------- 1 | import BaseEntity from './base.entity'; 2 | import { Entity, Column } from 'typeorm'; 3 | 4 | @Entity() 5 | export class BlogEntity extends BaseEntity { 6 | @Column() 7 | title: string; 8 | 9 | @Column({ 10 | unique: true, 11 | }) 12 | slug: string; 13 | 14 | @Column() 15 | content: string; 16 | 17 | @Column({}) 18 | published: boolean = false; 19 | 20 | @Column({ 21 | type: Date, 22 | nullable: true, 23 | }) 24 | publish_at: Date | null; 25 | } 26 | -------------------------------------------------------------------------------- /src/config/database.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | host: process.env.DB_HOST, 3 | type: 'mysql', 4 | port: process.env.DB_PORT, 5 | username: process.env.DB_USER, 6 | password: process.env.DB_PASSWORD, 7 | database: process.env.DB_DATABASE, 8 | entities: [process.env.DB_ENTITIES], 9 | synchronize: process.env.DB_SYNCRONIZE === 'true', 10 | logging: process.env.DB_LOGGING === 'true', 11 | migrationsRun: process.env.DB_MIGRATIONS_RUN === 'true', 12 | migrationsDir: [process.env.DB_MIGRATIONS_DIR], 13 | }; 14 | -------------------------------------------------------------------------------- /src/paginate/pagination.ts: -------------------------------------------------------------------------------- 1 | import { PaginationResultInterface } from './pagination.results.interface'; 2 | 3 | export class Pagination { 4 | public results: PaginationEntity[]; 5 | public page_total: number; 6 | public total: number; 7 | 8 | constructor(paginationResults: PaginationResultInterface) { 9 | this.results = paginationResults.results; 10 | this.page_total = paginationResults.results.length; 11 | this.total = paginationResults.total; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/user/user.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserEntity } from './../entities'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import { UserController } from './user.controller'; 5 | import { UserService } from './user.service'; 6 | import { ConfigModule } from 'nestjs-config'; 7 | 8 | @Module({ 9 | imports: [ConfigModule, TypeOrmModule.forFeature([UserEntity])], 10 | controllers: [UserController], 11 | providers: [UserService], 12 | exports: [UserService], 13 | }) 14 | export class UserModule {} 15 | -------------------------------------------------------------------------------- /src/blog/blog.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { TypeOrmModule } from '@nestjs/typeorm'; 3 | import { BlogEntity } from './../entities'; 4 | import { BlogService } from './blog.service'; 5 | import { ConfigModule } from 'nestjs-config'; 6 | import { BlogController } from './blog.controller'; 7 | import { SlugProvider } from './slug.provider'; 8 | 9 | @Module({ 10 | imports: [ConfigModule, TypeOrmModule.forFeature([BlogEntity])], 11 | controllers: [BlogController], 12 | providers: [SlugProvider, BlogService], 13 | }) 14 | export class BlogModule {} 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": false, 5 | "noImplicitAny": false, 6 | "removeComments": true, 7 | "noLib": false, 8 | "allowSyntheticDefaultImports": true, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es6", 12 | "sourceMap": true, 13 | "allowJs": true, 14 | "outDir": "./dist", 15 | "baseUrl": "./src" 16 | }, 17 | "include": [ 18 | "src/**/*" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "**/*.spec.ts" 23 | ] 24 | } -------------------------------------------------------------------------------- /.env.dist: -------------------------------------------------------------------------------- 1 | TYPEORM_CONNECTION=mysql 2 | TYPEORM_HOST=localhost 3 | TYPEORM_PORT=3307 4 | TYPEORM_USER=root 5 | TYPEORM_PASSWORD=root 6 | TYPEORM_ENTITIES=src/entities/*.entity.ts 7 | TYPEORM_DATABASE=blog 8 | TYPEORM_SYNCHRONIZE=true 9 | TYPEORM_LOGGING=false 10 | TYPEORM_MIGRATIONS_RUN=true 11 | TYPEORM_MIGRATIONS_DIR=migrations 12 | 13 | DB_HOST=localhost 14 | DB_PORT=3307 15 | DB_USER=root 16 | DB_PASSWORD=root 17 | DB_DATABASE=blog 18 | DB_ENTITIES=src/entities/*.entity.ts 19 | DB_SYNCRONIZE=true 20 | DB_LOGGING=true 21 | DB_MIGRATIONS_RUN=true 22 | DB_MIGRATIONS_DIR=migrations 23 | 24 | MYSQL_ROOT_PASSWORD=root 25 | MYSQL_DATABASE=blog 26 | 27 | JWT_SECRET=IamNotAsecret 28 | JWT_EXPIRES=3600 29 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { ConfigModule, ConfigService } from 'nestjs-config'; 3 | import { TypeOrmModule } from '@nestjs/typeorm'; 4 | import * as path from 'path'; 5 | import { UserModule } from './user'; 6 | import { AuthModule } from './auth'; 7 | import { BlogModule } from './blog'; 8 | 9 | @Module({ 10 | imports: [ 11 | ConfigModule.load(path.resolve(__dirname, 'config', '*.{ts,js}')), 12 | TypeOrmModule.forRootAsync({ 13 | useFactory: (config: ConfigService) => config.get('database'), 14 | inject: [ConfigService], 15 | }), 16 | UserModule, 17 | AuthModule, 18 | BlogModule, 19 | ], 20 | }) 21 | export class AppModule {} 22 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { ApplicationModule } from './../src/app.module'; 4 | import { INestApplication } from '@nestjs/common'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [ApplicationModule], 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 | afterAll(() => { 26 | app.close(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { UserModule } from './../user'; 3 | import { AuthService } from './auth.service'; 4 | import { JwtModule } from '@nestjs/jwt'; 5 | import { PassportModule } from '@nestjs/passport'; 6 | import { ConfigModule, ConfigService } from 'nestjs-config'; 7 | import { AuthController } from './auth.controller'; 8 | import { JwtStrategy } from './jwt.strategy'; 9 | 10 | @Module({ 11 | imports: [ 12 | UserModule, 13 | ConfigModule, 14 | PassportModule.register({ defaultStrategy: 'jwt' }), 15 | JwtModule.registerAsync({ 16 | useFactory: (config: ConfigService) => config.get('jwt'), 17 | inject: [ConfigService], 18 | }), 19 | ], 20 | providers: [AuthService, JwtStrategy], 21 | controllers: [AuthController], 22 | }) 23 | export class AuthModule {} 24 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const path = require('path'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | 5 | module.exports = { 6 | entry: ['webpack/hot/poll?1000', './src/main.hmr.ts'], 7 | watch: true, 8 | target: 'node', 9 | externals: [ 10 | nodeExternals({ 11 | whitelist: ['webpack/hot/poll?1000'], 12 | }), 13 | ], 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.tsx?$/, 18 | use: 'ts-loader', 19 | exclude: /node_modules/, 20 | }, 21 | ], 22 | }, 23 | mode: "development", 24 | resolve: { 25 | extensions: ['.tsx', '.ts', '.js'], 26 | }, 27 | plugins: [ 28 | new webpack.HotModuleReplacementPlugin(), 29 | ], 30 | output: { 31 | path: path.join(__dirname, 'dist'), 32 | filename: 'server.js', 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { AuthService } from './auth.service'; 3 | import { PassportStrategy } from '@nestjs/passport'; 4 | import { Injectable, UnauthorizedException } from '@nestjs/common'; 5 | import { JwtPayloadInterface } from './interfaces'; 6 | import { UserEntity } from 'entities'; 7 | import { InjectConfig, ConfigService } from 'nestjs-config'; 8 | 9 | @Injectable() 10 | export class JwtStrategy extends PassportStrategy(Strategy) { 11 | constructor( 12 | private readonly authService: AuthService, 13 | @InjectConfig() config: ConfigService, 14 | ) { 15 | super({ 16 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 17 | secretOrKey: config.get('jwt.secretOrPrivateKey'), 18 | }); 19 | } 20 | 21 | async validate(payload: JwtPayloadInterface): Promise { 22 | const user = await this.authService.validateUser(payload); 23 | if (!user) { 24 | throw new UnauthorizedException(); 25 | } 26 | return user; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, BadRequestException } from '@nestjs/common'; 2 | import { UserService } from './../user'; 3 | import { UserEntity } from 'entities'; 4 | import { JwtPayloadInterface } from './interfaces'; 5 | import { AuthModel } from 'models'; 6 | import { JwtService } from '@nestjs/jwt'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor( 11 | private readonly userService: UserService, 12 | private readonly jwtService: JwtService, 13 | ) {} 14 | 15 | async validateUser(payload: JwtPayloadInterface): Promise { 16 | return await this.userService.findById(payload.id); 17 | } 18 | 19 | async authenticate(auth: AuthModel): Promise { 20 | const user = await this.userService.findByEmailWithPassword(auth.email); 21 | if (!user) { 22 | throw new BadRequestException(); 23 | } 24 | 25 | if (!this.userService.compareHash(user.password, user.password)) { 26 | throw new BadRequestException('Invalid credentials'); 27 | } 28 | 29 | return this.jwtService.sign({ id: user.id }); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "jsRules": { 7 | "no-unused-expression": true 8 | }, 9 | "rules": { 10 | "eofline": false, 11 | "quotemark": [ 12 | true, 13 | "single" 14 | ], 15 | "indent": false, 16 | "member-access": [ 17 | false 18 | ], 19 | "ordered-imports": [ 20 | false 21 | ], 22 | "max-line-length": [ 23 | true, 24 | 150 25 | ], 26 | "member-ordering": [ 27 | false 28 | ], 29 | "curly": false, 30 | "interface-name": [ 31 | false 32 | ], 33 | "array-type": [ 34 | false 35 | ], 36 | "no-empty-interface": false, 37 | "no-empty": false, 38 | "arrow-parens": false, 39 | "object-literal-sort-keys": false, 40 | "no-unused-expression": false, 41 | "max-classes-per-file": [ 42 | false 43 | ], 44 | "variable-name": [ 45 | false 46 | ], 47 | "one-line": [ 48 | false 49 | ], 50 | "one-variable-per-declaration": [ 51 | false 52 | ] 53 | }, 54 | "rulesDirectory": [] 55 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Aaryanna Simonelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Body, ValidationPipe, UnprocessableEntityException } from '@nestjs/common'; 2 | import { JwtPayloadInterface } from './interfaces'; 3 | import { AuthModel, UserModel } from './../models'; 4 | import { AuthService } from './auth.service'; 5 | import {UserService} from './../user'; 6 | 7 | @Controller('auth') 8 | export class AuthController { 9 | constructor(private readonly authService: AuthService, private readonly userService: UserService) {} 10 | 11 | @Post('/login') 12 | async login(@Body(new ValidationPipe()) auth: AuthModel): Promise { 13 | return this.authService.authenticate(auth); 14 | } 15 | 16 | @Post('/register') 17 | async register(@Body(new ValidationPipe()) userModel: UserModel): Promise { 18 | const emailExists = await this.userService.findByEmail(userModel.email); 19 | console.log('email', emailExists); 20 | 21 | if (emailExists) { 22 | throw new UnprocessableEntityException(); 23 | } 24 | 25 | const user = await this.userService.create(userModel); 26 | 27 | return this.authService.authenticate(user); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/blog/slug.provider.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import {SlugProvider} from './slug.provider'; 3 | import {ConfigModule, ConfigService} from 'nestjs-config'; 4 | import * as path from 'path'; 5 | import { INestApplication } from '@nestjs/common'; 6 | 7 | let module: TestingModule; 8 | let app: INestApplication; 9 | let slugProvider: SlugProvider; 10 | let config: ConfigService; 11 | 12 | describe('SlugProvider', async () => { 13 | beforeAll(async () => { 14 | module = await Test.createTestingModule({ 15 | imports: [ 16 | ConfigModule.load(path.resolve(__dirname, '../', 'config', '*.ts')), 17 | ], 18 | providers: [SlugProvider], 19 | }).compile(); 20 | 21 | app = module.createNestApplication(); 22 | await app.init(); 23 | 24 | slugProvider = module.get(SlugProvider); 25 | config = module.get(ConfigService); 26 | }); 27 | 28 | it('replacement', () => { 29 | expect(slugProvider.replacement()).toEqual(config.get('slugify.replacement')); 30 | }); 31 | 32 | it('slugify', () => { 33 | expect(slugProvider.slugify('test test')).toEqual('test-test'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Tests 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | test: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | env: 23 | COMPOSE_FILE: ./docker-compose.yml 24 | 25 | # Steps represent a sequence of tasks that will be executed as part of the job 26 | steps: 27 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 28 | - uses: actions/checkout@v2 29 | 30 | - name: env sync 31 | run: cp .env.dist .env && export $(cat ./.env | xargs) 32 | 33 | # Runs a single command using the runners shell 34 | - name: build docker db 35 | run: docker-compose up -d 36 | 37 | - name: install 38 | run: yarn install --ignore-scripts 39 | 40 | - name: build 41 | run: yarn build 42 | 43 | - name: check docker 44 | run: docker-compose up -d 45 | 46 | - name: docker logs 47 | run: docker-compose logs && docker-compose ps 48 | 49 | # Runs a set of commands using the runners shell 50 | - name: tests 51 | run: yarn test --coverage 52 | -------------------------------------------------------------------------------- /src/user/user.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Post, 5 | Put, 6 | Body, 7 | ValidationPipe, 8 | UnprocessableEntityException, 9 | Param, 10 | NotFoundException, 11 | Request, 12 | } from '@nestjs/common'; 13 | import { UserEntity } from './../entities'; 14 | import { Pagination } from './../paginate'; 15 | import { UserService } from './user.service'; 16 | import { UserModel } from './../models'; 17 | import { UpdateResult } from 'typeorm'; 18 | 19 | @Controller('users') 20 | export class UserController { 21 | constructor(private readonly userService: UserService) {} 22 | 23 | @Get() 24 | async index(@Request() request): Promise> { 25 | // TODO make PaginationOptionsInterface an object so it can be defaulted 26 | return await this.userService.paginate({ 27 | limit: request.query.hasOwnProperty('limit') ? request.query.limit : 10, 28 | page: request.query.hasOwnProperty('page') ? request.query.page : 0, 29 | }); 30 | } 31 | 32 | @Post() 33 | async store( 34 | @Body(new ValidationPipe()) user: UserModel, 35 | ): Promise { 36 | const emailExists = await this.userService.findByEmail(user.email); 37 | 38 | if (emailExists) { 39 | throw new UnprocessableEntityException(); 40 | } 41 | 42 | return await this.userService.create(user); 43 | } 44 | 45 | @Put('/{id}') 46 | async update( 47 | @Param('id') id: number, 48 | @Body(new ValidationPipe()) user: UserModel, 49 | ): Promise { 50 | const userEntity = await this.userService.findById(id); 51 | 52 | if (!userEntity) { 53 | throw new NotFoundException(); 54 | } 55 | 56 | return await this.userService.update({ 57 | ...userEntity, 58 | ...user, 59 | }); 60 | } 61 | 62 | @Get('/{id}') 63 | async show(@Param('id') id: number): Promise { 64 | const user = this.userService.findById(id); 65 | 66 | if (!user) { 67 | throw new NotFoundException(); 68 | } 69 | 70 | return user; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/blog/blog.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Controller, 3 | Get, 4 | Put, 5 | Post, 6 | Param, 7 | Request, 8 | NotFoundException, 9 | Body, 10 | ValidationPipe, 11 | UnprocessableEntityException, 12 | UseGuards, 13 | } from '@nestjs/common'; 14 | import { BlogEntity } from './../entities'; 15 | import { Pagination } from './../paginate'; 16 | import { BlogService } from './blog.service'; 17 | import { BlogModel } from './../models'; 18 | import { UpdateResult } from 'typeorm'; 19 | import { AuthGuard } from '@nestjs/passport'; 20 | 21 | @UseGuards(AuthGuard('jwt')) 22 | @Controller('blog') 23 | export class BlogController { 24 | constructor(private readonly blogService: BlogService) {} 25 | 26 | @Get() 27 | async index(@Request() request): Promise> { 28 | return await this.blogService.paginate({ 29 | limit: request.query.hasOwnProperty('limit') ? request.query.limit : 10, 30 | page: request.query.hasOwnProperty('page') ? request.query.page : 0, 31 | }); 32 | } 33 | 34 | @Get('/{slug}') 35 | async show(@Param('slug') slug: string): Promise { 36 | const blog = await this.blogService.findBySlug(slug); 37 | 38 | if (!blog) { 39 | throw new NotFoundException(); 40 | } 41 | return blog; 42 | } 43 | 44 | @Post() 45 | async create( 46 | @Body(new ValidationPipe()) body: BlogModel, 47 | ): Promise { 48 | const exists = await this.blogService.findBySlug(body.slug); 49 | 50 | if (exists) { 51 | throw new UnprocessableEntityException(); 52 | } 53 | 54 | return await this.blogService.create(body); 55 | } 56 | 57 | @Put('/{id}') 58 | async update( 59 | @Param('id') id: number, 60 | @Body(new ValidationPipe()) body: BlogModel, 61 | ): Promise { 62 | const blog = await this.blogService.findById(body.id); 63 | 64 | if (!blog) { 65 | throw new NotFoundException(); 66 | } 67 | 68 | return await this.blogService.update({ 69 | ...blog, 70 | ...body, 71 | }); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { INestApplication, BadRequestException } from '@nestjs/common'; 2 | import { Test, TestingModule } from '@nestjs/testing'; 3 | import { AuthService } from './auth.service'; 4 | import { ConfigModule, ConfigService } from 'nestjs-config'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import * as path from 'path'; 7 | import { UserModule, UserService } from '../user'; 8 | import { AuthModule } from './auth.module'; 9 | import { UserEntity } from '../entities'; 10 | 11 | describe('AuthService', () => { 12 | let app: INestApplication; 13 | let module: TestingModule; 14 | let authService: AuthService; 15 | let payload: string; 16 | let userService: UserService; 17 | let user: UserEntity; 18 | 19 | beforeAll(async () => { 20 | module = await Test.createTestingModule({ 21 | imports: [ 22 | ConfigModule.load(path.resolve(__dirname, '../', 'config', '*.ts')), 23 | TypeOrmModule.forRootAsync({ 24 | useFactory: (config: ConfigService) => config.get('database'), 25 | inject: [ConfigService], 26 | }), 27 | UserModule, 28 | AuthModule, 29 | ], 30 | }).compile(); 31 | 32 | app = module.createNestApplication(); 33 | await app.init(); 34 | 35 | authService = module.get(AuthService); 36 | userService = module.get(UserService); 37 | }); 38 | 39 | it('authenticate fail', async () => { 40 | let error; 41 | try { 42 | await authService.authenticate({ 43 | email: 'no email', 44 | password: 'df', 45 | }); 46 | } catch (e) { 47 | error = e; 48 | } 49 | 50 | expect(error).toBeInstanceOf(BadRequestException); 51 | }); 52 | 53 | it('authenticate', async () => { 54 | user = await userService.create({ 55 | email: 'email@email.com', 56 | password: 'testtest', 57 | firstname: 'test', 58 | lastname: 'test', 59 | }); 60 | 61 | payload = await authService.authenticate({ 62 | email: 'email@email.com', 63 | password: 'testtest', 64 | }); 65 | }); 66 | 67 | it('validateUser', async () => { 68 | const result = await authService.validateUser(user); 69 | expect(result).toBeInstanceOf(UserEntity); 70 | }); 71 | 72 | afterAll(async () => { 73 | await userService.destroy(user.id); 74 | app.close(); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nestjs-blog", 3 | "version": "1.0.0", 4 | "description": "nestjs-blog-example", 5 | "author": "ashleigh simonelli", 6 | "license": "MIT", 7 | "repository": "git@github.com:bashleigh/nestjs-blog.git", 8 | "scripts": { 9 | "format": "prettier --write \"**/*.ts\"", 10 | "start": "ts-node -r tsconfig-paths/register src/main.ts", 11 | "start:dev": "nodemon", 12 | "prestart:prod": "rm -rf dist && tsc", 13 | "start:prod": "node dist/main.js", 14 | "start:hmr": "node dist/server", 15 | "build": "tsc", 16 | "test": "jest", 17 | "test:cov": "jest --coverage", 18 | "test:e2e": "jest --config ./test/jest-e2e.json", 19 | "webpack": "webpack --config webpack.config.js", 20 | "coveralls": "yarn run test:cov --coverageReporters=text-lcov | coveralls" 21 | }, 22 | "dependencies": { 23 | "@nestjs/common": "^7.0.0", 24 | "@nestjs/core": "^7.0.0", 25 | "@nestjs/jwt": "^7.0.0", 26 | "@nestjs/passport": "^7.0.0", 27 | "@nestjs/platform-express": "^7.6.16", 28 | "@nestjs/testing": "^7.0.0", 29 | "@nestjs/typeorm": "^7.0.0", 30 | "bcrypt": "^3.0.1", 31 | "class-transformer": "^0.3.1", 32 | "class-validator": "^0.9.1", 33 | "coveralls": "^3.0.2", 34 | "fastify-formbody": "^2.0.0", 35 | "mysql": "^2.16.0", 36 | "nestjs-config": "^1.4.0", 37 | "passport": "^0.4.0", 38 | "passport-http-bearer": "^1.0.1", 39 | "passport-jwt": "^4.0.0", 40 | "reflect-metadata": "^0.1.12", 41 | "rxjs": "^6.0.0", 42 | "slugify": "^1.3.1", 43 | "typeorm": "^0.2.7", 44 | "typescript": "^3.4.3" 45 | }, 46 | "devDependencies": { 47 | "@types/express": "^4.0.39", 48 | "@types/jest": "^21.1.8", 49 | "@types/node": "^15.3.0", 50 | "@types/supertest": "^2.0.4", 51 | "jest": "^26.0.0", 52 | "nodemon": "^1.14.1", 53 | "prettier": "^1.11.1", 54 | "supertest": "^3.0.0", 55 | "ts-jest": "^26.5.6", 56 | "ts-loader": "^4.1.0", 57 | "ts-node": "^4.1.0", 58 | "tsconfig-paths": "^3.1.1", 59 | "tslint": "5.3.2", 60 | "webpack": "^4.2.0", 61 | "webpack-cli": "^2.0.13", 62 | "webpack-node-externals": "^1.6.0" 63 | }, 64 | "jest": { 65 | "moduleFileExtensions": [ 66 | "js", 67 | "json", 68 | "ts" 69 | ], 70 | "rootDir": "src", 71 | "testRegex": ".spec.ts$", 72 | "transform": { 73 | "^.+\\.(t|j)s$": "ts-jest" 74 | }, 75 | "coverageDirectory": "../coverage", 76 | "collectCoverageFrom": [ 77 | "!src/config/**", 78 | "!src/models/**", 79 | "!src/entities/**" 80 | ] 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/user/user.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository, UpdateResult, DeleteResult } from 'typeorm'; 4 | import { UserEntity as User, UserEntity } from './../entities'; 5 | import { Pagination, PaginationOptionsInterface } from './../paginate'; 6 | import { UserModel } from 'models'; 7 | import * as bcrypt from 'bcrypt'; 8 | import { InjectConfig, ConfigService } from 'nestjs-config'; 9 | 10 | @Injectable() 11 | export class UserService { 12 | private saltRounds: number; 13 | 14 | constructor( 15 | @InjectRepository(User) private readonly userRepository: Repository, 16 | @InjectConfig() private readonly config: ConfigService, 17 | ) { 18 | this.saltRounds = config.get('app.salt_rounds', 10); 19 | } 20 | 21 | async paginate( 22 | options: PaginationOptionsInterface, 23 | ): Promise> { 24 | const [results, total] = await this.userRepository.findAndCount({ 25 | take: options.limit, 26 | skip: options.page, // think this needs to be page * limit 27 | }); 28 | 29 | // TODO add more tests for paginate 30 | 31 | return new Pagination({ 32 | results, 33 | total, 34 | }); 35 | } 36 | 37 | async create(user: UserModel): Promise { 38 | user.password = await this.getHash(user.password); 39 | 40 | const result = await this.userRepository.save( 41 | this.userRepository.create(user), 42 | ); 43 | 44 | delete result.password; 45 | return result; 46 | } 47 | 48 | async update(user: UserEntity): Promise { 49 | return await this.userRepository.update(user.id, user); 50 | } 51 | 52 | async findByEmail(email: string): Promise { 53 | return await this.userRepository.findOne({ 54 | where: { 55 | email, 56 | }, 57 | }); 58 | } 59 | 60 | async findById(id: number): Promise { 61 | return await this.userRepository.findOneOrFail(id); 62 | } 63 | 64 | async getHash(password: string): Promise { 65 | return await bcrypt.hash(password, this.saltRounds); 66 | } 67 | 68 | async compareHash(password: string, hash: string): Promise { 69 | return await bcrypt.compare(password, hash); 70 | } 71 | 72 | async findByEmailWithPassword(email: string): Promise | null { 73 | return await this.userRepository.findOne( 74 | { 75 | email, 76 | }, 77 | { 78 | select: ['email', 'password'], 79 | }, 80 | ); 81 | } 82 | 83 | async destroy(id: number): Promise { 84 | return await this.userRepository.delete(id); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/blog/blog.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { InjectRepository } from '@nestjs/typeorm'; 3 | import { Repository, UpdateResult, DeleteResult } from 'typeorm'; 4 | import { BlogEntity } from './../entities'; 5 | import { Pagination, PaginationOptionsInterface } from './../paginate'; 6 | import { SlugProvider } from './slug.provider'; 7 | import { BlogModel } from './../models'; 8 | 9 | @Injectable() 10 | export class BlogService { 11 | constructor( 12 | @InjectRepository(BlogEntity) 13 | private readonly blogRepository: Repository, 14 | private readonly slugProvider: SlugProvider, 15 | ) {} 16 | 17 | async paginate( 18 | options: PaginationOptionsInterface, 19 | ): Promise> { 20 | const [results, total] = await this.blogRepository.findAndCount({ 21 | take: options.limit, 22 | skip: options.page, // think this needs to be page * limit 23 | }); 24 | 25 | // TODO add more tests for paginate 26 | 27 | return new Pagination({ 28 | results, 29 | total, 30 | }); 31 | } 32 | 33 | async create(blog: BlogModel): Promise { 34 | blog = await this.uniqueSlug(blog); 35 | return await this.blogRepository.save(this.blogRepository.create(blog)); 36 | } 37 | 38 | async update(blog: BlogEntity): Promise { 39 | return await this.blogRepository.update(blog.id, blog); 40 | } 41 | 42 | async findById(id: number): Promise { 43 | return await this.blogRepository.findOne(id); 44 | } 45 | 46 | async findBySlug(slug: string): Promise { 47 | return await this.blogRepository.findOne({ 48 | where: { 49 | slug, 50 | }, 51 | }); 52 | } 53 | 54 | async destroy(id: number): Promise { 55 | return await this.blogRepository.delete(id); 56 | } 57 | 58 | async uniqueSlug(blog: BlogModel): Promise { 59 | blog.slug = await this.slugProvider.slugify(blog.title); 60 | const exists = await this.findSlugs(blog.slug); 61 | 62 | // if slug doesn't already exists 63 | if (!exists || exists.length === 0) { 64 | return blog; 65 | } 66 | 67 | // Omit if same entity 68 | if (exists.length === 1 && blog.id === exists[0].id) { 69 | return blog; 70 | } 71 | 72 | // Add to suffix 73 | blog.slug = blog.slug + this.slugProvider.replacement() + exists.length; 74 | 75 | return blog; 76 | } 77 | 78 | private async findSlugs(slug: string): Promise { 79 | return await this.blogRepository 80 | .createQueryBuilder('blog') 81 | .where('slug like :slug', { slug: `${slug}%` }) 82 | .getMany(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NestJS Blog 2 | 3 | 5 | 6 | This is an example of how to use [nestjs](https://github.com/nestjs/nest) with jwt and typeorm to build a basic blog API. 7 | 8 | ## Use 9 | 10 | - Start the mysql container using docker 11 | 12 | ```bash 13 | $ docker-compose up -d 14 | ``` 15 | - Start the nestjs process using to following 16 | 17 | ```bash 18 | $ yarn start 19 | ``` 20 | 21 | ### Production 22 | 23 | If you're going to use this example in production (or your own verison of it) it's recommended to run using the 'complied' JS version from dist. You can do this by using the following command 24 | 25 | ```bash 26 | $ yarn start:prod 27 | ``` 28 | > This command will also clean and build your dist folder 29 | 30 | ## Development 31 | 32 | For development, the best command to use is 33 | 34 | ```bash 35 | $ yarn start:dev 36 | ``` 37 | 38 | This will start nodemon to reload our script when there's been any changes in the src directory 39 | 40 | ## Testing 41 | 42 | #### Unit testing 43 | Unit tests can be ran by simply using the `test` script 44 | 45 | ```bash 46 | $ yarn test 47 | ``` 48 | This will run jest on all `.spec.ts` files. 49 | 50 | #### End to End testing (E2E) 51 | 52 | End to end tests can be run by using the following command 53 | 54 | ```bash 55 | $ yarn test:e2e 56 | ``` 57 | this will run jest on all `.e2e-spec.ts` files. 58 | 59 | #### Coverage 60 | 61 | Use jest to show you a coverage of your tests 62 | 63 | ```bash 64 | $ yarn test:cov 65 | ``` 66 | 67 | ## Build your own NestJS application 68 | 69 | Want to get started on your own NestJS application? Simply install the [nest-cli](https://github.com/nestjs/nest-cli) `npm i -g @nestjs/cli` and use the command `nest new my-application` to create a new directory called `my-application` with nestjs ready to go! 70 | 71 | # Packages 72 | 73 | I used a variety of packages to develop this example api. Here's a list of them and where I got them from 74 | 75 | - Nestjs 76 | - [@nestjs/typeorm](https://github.com/nestjs/typeorm) A typeorm module for nestjs 77 | - [@nestjs/passport](https://github.com/nestjs/passport) An easy to use module for passport include AuthGuards 78 | - [@nestjs/jwt](https://github.com/nestjs/jwt) A JWT module for nestjs 79 | - nestjs-community 80 | - [nestjs-config](https://github.com/nestjs-community/nestjs-config) A config module for nestjs (envs) 81 | - [typeorm](https://github.com/typeorm/typeorm) typeorm is an orm for TypeScript -------------------------------------------------------------------------------- /src/blog/blog.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { BlogService, BlogModule } from './'; 3 | import { ConfigModule, ConfigService } from 'nestjs-config'; 4 | import * as path from 'path'; 5 | import { TypeOrmModule, getConnectionToken } from '@nestjs/typeorm'; 6 | import { Pagination } from '../paginate'; 7 | import { INestApplication } from '@nestjs/common'; 8 | import { BlogEntity } from '../entities'; 9 | import { UpdateResult, DeleteResult, Connection } from 'typeorm'; 10 | 11 | describe('BlogService', () => { 12 | let app: INestApplication; 13 | let module: TestingModule; 14 | let blogService: BlogService; 15 | let blog: BlogEntity; 16 | const blogs: BlogEntity[] = []; 17 | 18 | beforeAll(async () => { 19 | module = await Test.createTestingModule({ 20 | imports: [ 21 | ConfigModule.load(path.resolve(__dirname, '../', 'config', '*.ts')), 22 | TypeOrmModule.forRootAsync({ 23 | useFactory: (config: ConfigService) => config.get('database'), 24 | inject: [ConfigService], 25 | }), 26 | BlogModule, 27 | ], 28 | }).compile(); 29 | 30 | app = module.createNestApplication(); 31 | await app.init(); 32 | 33 | blogService = module.get(BlogService); 34 | }); 35 | 36 | it('paginate', async () => { 37 | expect( 38 | await blogService.paginate({ 39 | limit: 10, 40 | page: 0, 41 | }), 42 | ).toBeInstanceOf(Pagination); 43 | }); 44 | 45 | it('create', async () => { 46 | expect( 47 | (blog = await blogService.create({ 48 | title: 'test', 49 | content: '#Content', 50 | })), 51 | ).toBeInstanceOf(BlogEntity); 52 | }); 53 | 54 | it('update', async () => { 55 | blog.content = '#Conent Updated'; 56 | 57 | const result = await blogService.update(blog); 58 | 59 | expect(result).toBeInstanceOf(UpdateResult); 60 | }); 61 | 62 | it('delete', async () => { 63 | const result = await blogService.destroy(blog.id); 64 | expect(result).toBeInstanceOf(DeleteResult); 65 | }); 66 | 67 | it('uniqueSlug', async () => { 68 | 69 | let sluggedBlog: BlogEntity; 70 | 71 | sluggedBlog = await blogService.uniqueSlug({ 72 | title: 'hello', 73 | content: 'test', 74 | }); 75 | 76 | expect(sluggedBlog.slug).toBe('hello'); 77 | 78 | sluggedBlog = await blogService.uniqueSlug({ 79 | title: 'I have spaces', 80 | content: 'test', 81 | }); 82 | 83 | expect(sluggedBlog.slug).toBe('i-have-spaces'); 84 | 85 | blogs.push( 86 | await blogService.create({ 87 | title: 'title', 88 | content: 'test', 89 | }), 90 | ); 91 | 92 | blogs.push( 93 | await blogService.create({ 94 | title: 'title', 95 | content: 'test', 96 | }), 97 | ); 98 | 99 | expect(blogs[blogs.length - 1].slug).toEqual('title-1'); 100 | }); 101 | 102 | afterAll(async () => { 103 | const connection = module.get(getConnectionToken('default')); 104 | await connection.query('TRUNCATE blog_entity'); 105 | app.close(); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/user/user.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { UserService, UserModule } from './'; 3 | import { ConfigModule, ConfigService } from 'nestjs-config'; 4 | import * as path from 'path'; 5 | import { TypeOrmModule } from '@nestjs/typeorm'; 6 | import { Pagination } from '../paginate'; 7 | import { INestApplication } from '@nestjs/common'; 8 | import { UserEntity } from '../entities'; 9 | import { UpdateResult, DeleteResult } from 'typeorm'; 10 | 11 | describe('UserService', () => { 12 | let module: TestingModule; 13 | let userService: UserService; 14 | let app: INestApplication; 15 | let user: UserEntity; 16 | let auth: Partial; 17 | 18 | beforeAll(async () => { 19 | module = await Test.createTestingModule({ 20 | imports: [ 21 | ConfigModule.load(path.resolve(__dirname, '../', 'config', '*.ts')), 22 | TypeOrmModule.forRootAsync({ 23 | useFactory: (config: ConfigService) => config.get('database'), 24 | inject: [ConfigService], 25 | }), 26 | UserModule, 27 | ], 28 | }).compile(); 29 | 30 | app = await module.createNestApplication(); 31 | 32 | userService = module.get(UserService); 33 | }); 34 | 35 | it('paginate', async () => { 36 | expect( 37 | await userService.paginate({ 38 | limit: 10, 39 | page: 0, 40 | }), 41 | ).toBeInstanceOf(Pagination); 42 | }); 43 | 44 | it('create', async () => { 45 | const number = Math.floor(Math.random() * Math.floor(20)); 46 | expect( 47 | (user = await userService.create({ 48 | email: `test${number}@test.com`, 49 | firstname: 'test', 50 | lastname: 'test', 51 | password: 'password', 52 | })), 53 | ).toBeInstanceOf(UserEntity); 54 | expect(user).not.toHaveProperty('password'); 55 | }); 56 | 57 | it('Update', async () => { 58 | user.firstname = 'updated'; 59 | 60 | const result = await userService.update(user); 61 | expect(result).toBeInstanceOf(UpdateResult); 62 | expect(user.firstname).toEqual('updated'); 63 | }); 64 | 65 | it('findByEmailWithPassword', async () => { 66 | auth = await userService.findByEmailWithPassword(user.email); 67 | 68 | expect(auth).toHaveProperty('password'); 69 | }); 70 | 71 | it('CompareHash', async () => { 72 | const result = await userService.compareHash('password', auth.password); 73 | 74 | expect(result).toBeTruthy(); 75 | }); 76 | 77 | it('findByEmail', async () => { 78 | const result = await userService.findByEmail(user.email); 79 | 80 | expect(result).toBeInstanceOf(UserEntity); 81 | 82 | delete user.updated; 83 | 84 | expect(result).toEqual(expect.objectContaining(user)); 85 | }); 86 | 87 | it('findById', async () => { 88 | const result = await userService.findById(user.id); 89 | 90 | expect(result).toBeInstanceOf(UserEntity); 91 | expect(result).toEqual(expect.objectContaining(user)); 92 | }); 93 | 94 | it('delete', async () => { 95 | const result = await userService.destroy(user.id); 96 | expect(result).toBeInstanceOf(DeleteResult); 97 | }); 98 | 99 | afterAll(async () => app.close()); 100 | }); 101 | --------------------------------------------------------------------------------