Browse Source

Part 4: Building a REST API with NestJS and Prisma: Handling Relational Data

master
Hendra Lin 3 years ago
parent
commit
228e25c38a
  1. 18
      prisma/migrations/20230414070358_add_user_model/migration.sql
  2. 12
      prisma/schema.prisma
  3. 46
      prisma/seed.ts
  4. 3
      src/app.module.ts
  5. 24
      src/articles/articles.controller.ts
  6. 6
      src/articles/articles.service.ts
  7. 15
      src/articles/entities/article.entity.ts
  8. 5
      src/main.ts
  9. 20
      src/users/dto/create-user.dto.ts
  10. 4
      src/users/dto/update-user.dto.ts
  11. 27
      src/users/entities/user.entity.ts
  12. 20
      src/users/users.controller.spec.ts
  13. 43
      src/users/users.controller.ts
  14. 11
      src/users/users.module.ts
  15. 18
      src/users/users.service.spec.ts
  16. 29
      src/users/users.service.ts

18
prisma/migrations/20230414070358_add_user_model/migration.sql

@ -0,0 +1,18 @@
-- AlterTable
ALTER TABLE `article` ADD COLUMN `authorId` INTEGER NULL;
-- CreateTable
CREATE TABLE `User` (
`id` INTEGER NOT NULL AUTO_INCREMENT,
`name` VARCHAR(191) NULL,
`email` VARCHAR(191) NOT NULL,
`password` VARCHAR(191) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `User_email_key`(`email`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Article` ADD CONSTRAINT `Article_authorId_fkey` FOREIGN KEY (`authorId`) REFERENCES `User`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;

12
prisma/schema.prisma

@ -18,4 +18,16 @@ model Article {
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
author User? @relation(fields: [authorId], references: [id])
authorId Int?
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
password String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
articles Article[]
}

46
prisma/seed.ts

@ -6,31 +6,71 @@ import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
// create two dummy users
const user1 = await prisma.user.upsert({
where: { email: 'sabin@adams.com' },
update: {},
create: {
email: 'sabin@adams.com',
name: 'Sabin Adams',
password: 'password-sabin',
},
});
const user2 = await prisma.user.upsert({
where: { email: 'alex@ruheni.com' },
update: {},
create: {
email: 'alex@ruheni.com',
name: 'Alex Ruheni',
password: 'password-alex',
},
});
// create two dummy articles
const post1 = await prisma.article.upsert({
where: { title: 'Prisma Adds Support for MongoDB' },
update: {},
update: {
authorId: user1.id,
},
create: {
title: 'Prisma Adds Support for MongoDB',
body: 'Support for MongoDB has been one of the most requested features since the initial release of...',
description: "We are excited to share that today's Prisma ORM release adds stable support for MongoDB!",
published: false,
authorId: user1.id,
}
})
const post2 = await prisma.article.upsert({
where: { title: "What's new in Prisma? (Q1/22)" },
update: {},
update: {
authorId: user2.id,
},
create: {
title: "What's new in Prisma? (Q1/22)",
body: 'Our engineers have been working hard, issuing new releases with many improvements...',
description:
'Learn about everything in the Prisma ecosystem and community from January to March 2022.',
published: true,
authorId: user2.id,
},
});
const post3 = await prisma.article.upsert({
where: { title: 'Prisma Client Just Became a Lot More Flexible' },
update: {},
create: {
title: 'Prisma Client Just Became a Lot More Flexible',
body: 'Prisma Client extensions provide a powerful new way to add functionality to Prisma in a type-safe manner...',
description:
'This article will explore various ways you can use Prisma Client extensions to add custom functionality to Prisma Client..',
published: true,
},
});
console.log({ post1, post2 });
console.log({ user1, user2, post1, post2, post3 });
}
// execute the main function

3
src/app.module.ts

@ -3,9 +3,10 @@ import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PrismaModule } from './prisma/prisma.module';
import { ArticlesModule } from './articles/articles.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [PrismaModule, ArticlesModule],
imports: [PrismaModule, ArticlesModule, UsersModule],
controllers: [AppController],
providers: [AppService],
})

24
src/articles/articles.controller.ts

@ -12,26 +12,28 @@ export class ArticlesController {
@Post()
@ApiCreatedResponse({ type: ArticleEntity })
create(@Body() createArticleDto: CreateArticleDto) {
return this.articlesService.create(createArticleDto);
async create(@Body() createArticleDto: CreateArticleDto) {
return new ArticleEntity(await this.articlesService.create(createArticleDto));
}
@Get('drafts')
@ApiOkResponse({ type: ArticleEntity, isArray: true })
findDrafts() {
return this.articlesService.findDrafts();
async findDrafts() {
const drafts = await this.articlesService.findDrafts()
return drafts.map((draft) => new ArticleEntity(draft))
}
@Get()
@ApiOkResponse({ type: ArticleEntity, isArray: true })
findAll() {
return this.articlesService.findAll();
async findAll() {
const articles = await this.articlesService.findAll()
return articles.map((article) => new ArticleEntity(article))
}
@Get(':id')
@ApiOkResponse({ type: ArticleEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
const article = await this.articlesService.findOne(id)
const article = new ArticleEntity(await this.articlesService.findOne(id))
if (!article) {
throw new NotFoundException(`Article with ${id} does not exist.`)
}
@ -40,13 +42,13 @@ export class ArticlesController {
@Patch(':id')
@ApiOkResponse({ type: ArticleEntity })
update(@Param('id', ParseIntPipe) id: number, @Body() updateArticleDto: UpdateArticleDto) {
return this.articlesService.update(id, updateArticleDto);
async update(@Param('id', ParseIntPipe) id: number, @Body() updateArticleDto: UpdateArticleDto) {
return new ArticleEntity(await this.articlesService.update(id, updateArticleDto));
}
@Delete(':id')
@ApiOkResponse({ type: ArticleEntity })
remove(@Param('id', ParseIntPipe) id: number) {
return this.articlesService.remove(id);
async remove(@Param('id', ParseIntPipe) id: number) {
return new ArticleEntity(await this.articlesService.remove(id));
}
}

6
src/articles/articles.service.ts

@ -20,7 +20,11 @@ export class ArticlesService {
}
findOne(id: number) {
return this.prisma.article.findUnique({ where: { id } })
return this.prisma.article.findUnique({
where: { id }, include: {
author: true
}
})
}
update(id: number, updateArticleDto: UpdateArticleDto) {

15
src/articles/entities/article.entity.ts

@ -1,5 +1,6 @@
import { Article } from '@prisma/client'
import { ApiProperty } from '@nestjs/swagger'
import { UserEntity } from 'src/users/entities/user.entity'
export class ArticleEntity implements Article {
@ApiProperty()
@ -22,4 +23,18 @@ export class ArticleEntity implements Article {
@ApiProperty()
updatedAt: Date
@ApiProperty({ required: false, nullable: true })
authorId: number | null;
@ApiProperty({ required: false, type: UserEntity })
author?: UserEntity
constructor({ author, ...data }: Partial<ArticleEntity>) {
Object.assign(this, data)
if (author) {
this.author = new UserEntity(author)
}
}
}

5
src/main.ts

@ -1,13 +1,14 @@
import { HttpAdapterHost, NestFactory } from '@nestjs/core';
import { HttpAdapterHost, NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common'
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common'
import { PrismaClientExceptionFilter } from './prisma-client-exception/prisma-client-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true }))
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)))
const config = new DocumentBuilder()
.setTitle('Median')

20
src/users/dto/create-user.dto.ts

@ -0,0 +1,20 @@
import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, IsString, MinLength } from "class-validator";
export class CreateUserDto {
@IsString()
@IsNotEmpty()
@ApiProperty()
name: string
@IsString()
@IsNotEmpty()
@ApiProperty()
email: string
@IsString()
@IsNotEmpty()
@MinLength(6)
@ApiProperty()
password: string
}

4
src/users/dto/update-user.dto.ts

@ -0,0 +1,4 @@
import { PartialType } from '@nestjs/swagger';
import { CreateUserDto } from './create-user.dto';
export class UpdateUserDto extends PartialType(CreateUserDto) {}

27
src/users/entities/user.entity.ts

@ -0,0 +1,27 @@
import { ApiProperty } from '@nestjs/swagger'
import { User } from '@prisma/client'
import { Exclude } from 'class-transformer'
export class UserEntity implements User {
constructor(partial: Partial<UserEntity>) {
Object.assign(this, partial)
}
@ApiProperty()
id: number
@ApiProperty()
createdAt: Date
@ApiProperty()
updatedAt: Date
@ApiProperty()
name: string
@ApiProperty()
email: string
@Exclude()
password: string
}

20
src/users/users.controller.spec.ts

@ -0,0 +1,20 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
describe('UsersController', () => {
let controller: UsersController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [UsersService],
}).compile();
controller = module.get<UsersController>(UsersController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

43
src/users/users.controller.ts

@ -0,0 +1,43 @@
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseIntPipe } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';
@Controller('users')
@ApiTags('users')
export class UsersController {
constructor(private readonly usersService: UsersService) { }
@Post()
@ApiCreatedResponse({ type: UserEntity })
async create(@Body() createUserDto: CreateUserDto) {
return new UserEntity(await this.usersService.create(createUserDto));
}
@Get()
@ApiOkResponse({ type: UserEntity, isArray: true })
async findAll() {
const users = await this.usersService.findAll()
return users.map((user) => new UserEntity(user))
}
@Get(':id')
@ApiOkResponse({ type: UserEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.findOne(id));
}
@Patch(':id')
@ApiOkResponse({ type: UserEntity })
async update(@Param('id', ParseIntPipe) id: number, @Body() updateUserDto: UpdateUserDto) {
return new UserEntity(await this.usersService.update(id, updateUserDto));
}
@Delete(':id')
@ApiOkResponse({ type: UserEntity })
async remove(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.remove(id));
}
}

11
src/users/users.module.ts

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [PrismaModule]
})
export class UsersModule { }

18
src/users/users.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile();
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

29
src/users/users.service.ts

@ -0,0 +1,29 @@
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) { }
create(createUserDto: CreateUserDto) {
return this.prisma.user.create({ data: createUserDto })
}
findAll() {
return this.prisma.user.findMany()
}
findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } })
}
update(id: number, updateUserDto: UpdateUserDto) {
return this.prisma.user.update({ where: { id }, data: updateUserDto })
}
remove(id: number) {
return this.prisma.user.delete({ where: { id } })
}
}
Loading…
Cancel
Save