diff --git a/prisma/migrations/20230414070358_add_user_model/migration.sql b/prisma/migrations/20230414070358_add_user_model/migration.sql new file mode 100644 index 0000000..fe9163e --- /dev/null +++ b/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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index de3e06c..7df5e0b 100644 --- a/prisma/schema.prisma +++ b/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[] } diff --git a/prisma/seed.ts b/prisma/seed.ts index f291379..0f25731 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -6,35 +6,75 @@ 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 -main().catch((e)=>{ +main().catch((e) => { console.error(e) process.exit(1) }).finally(async () => { diff --git a/src/app.module.ts b/src/app.module.ts index a9acbae..7bd7a4b 100644 --- a/src/app.module.ts +++ b/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], }) diff --git a/src/articles/articles.controller.ts b/src/articles/articles.controller.ts index 5cc6aab..94d5891 100644 --- a/src/articles/articles.controller.ts +++ b/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)); } } diff --git a/src/articles/articles.service.ts b/src/articles/articles.service.ts index f4627d9..abbc871 100644 --- a/src/articles/articles.service.ts +++ b/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) { diff --git a/src/articles/entities/article.entity.ts b/src/articles/entities/article.entity.ts index 51028cc..3dccf57 100644 --- a/src/articles/entities/article.entity.ts +++ b/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) { + Object.assign(this, data) + + if (author) { + this.author = new UserEntity(author) + } + } } diff --git a/src/main.ts b/src/main.ts index 840caae..191bf06 100644 --- a/src/main.ts +++ b/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') diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts new file mode 100644 index 0000000..ff2a2c2 --- /dev/null +++ b/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 +} diff --git a/src/users/dto/update-user.dto.ts b/src/users/dto/update-user.dto.ts new file mode 100644 index 0000000..78ab602 --- /dev/null +++ b/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) {} diff --git a/src/users/entities/user.entity.ts b/src/users/entities/user.entity.ts new file mode 100644 index 0000000..a1d7310 --- /dev/null +++ b/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) { + Object.assign(this, partial) + } + + @ApiProperty() + id: number + + @ApiProperty() + createdAt: Date + + @ApiProperty() + updatedAt: Date + + @ApiProperty() + name: string + + @ApiProperty() + email: string + + @Exclude() + password: string +} diff --git a/src/users/users.controller.spec.ts b/src/users/users.controller.spec.ts new file mode 100644 index 0000000..a76d310 --- /dev/null +++ b/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); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts new file mode 100644 index 0000000..638ee0a --- /dev/null +++ b/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)); + } +} diff --git a/src/users/users.module.ts b/src/users/users.module.ts new file mode 100644 index 0000000..6d86488 --- /dev/null +++ b/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 { } diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts new file mode 100644 index 0000000..62815ba --- /dev/null +++ b/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); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/users/users.service.ts b/src/users/users.service.ts new file mode 100644 index 0000000..c717a38 --- /dev/null +++ b/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 } }) + } +}