# ***Post & Comment API*** 建立一個擁有基本貼文及留言功能的API ## 初步功能需求 1. 使用者可以在app中`貼文 (post)` 2. 使用者可以在貼文底下`留言`, 或是留言別的使用者的`留言 (comment)` 3. 取得貼文`留言`最多的10筆貼文 4. `新增/修改/刪除` 貼文 5. `新增/修改/刪除` 留言 ## 實作步驟 ### 第一步 `MongoDB`的結構 首先,我會創建一個名為`posts`的`collection`, 接下來再想想`posts`內部資料的結構 #### `posts`的資料結構 ``` { _id : ObjectId('...') , Text : { type: String, required: true } , Date : { type: Date, required: true } , Author : { type: String, required: true } } ``` #### 如下圖 ![](https://i.imgur.com/MIoDKmv.png) 再來,我會創建另一個名為`comments`的`collection`, 一樣來想一下`comments`內部資料的結構 #### `comments`的資料結構 ``` { _id : ObjectId('...') , Post : ObjectId('...') , //ref to posts Text : { type: String, required: true } , Date : { type: Date, required: true } , Author : { type: String, required: true } , Replies : [{ _id : ObjectId('...') , Text : { type: String, required: true } , Date : { type: Date, required: true } , Author : { type: String, required: true } , }] } ``` #### 如下圖 ![](https://i.imgur.com/nwn8Hsz.png) 最後,我們需要一個`aggregations`來取得`留言`最多的10筆貼文資料 #### `aggregations` for `Nodejs` ``` [ { '$lookup': { 'from': 'comments', 'localField': '_id', 'foreignField': 'Post', 'as': 'Comments' } }, { '$project': { 'Text': 1, 'Author': 1, 'Comments': 1, 'Comments_Size': { '$size': '$Comments' } } }, { '$unwind': { 'path': '$Comments' } }, { '$project': { 'Text': 1, 'Author': 1, 'Comments': 1, 'Comments_Size': 1, 'Replies_Size': { '$size': '$Comments.Replies' } } }, { '$project': { '_id': 1, 'Text': 1, 'Author': 1, 'Comments': 1, 'Comments_Count': { '$sum': [ '$Comments_Size', '$Replies_Size' ] } } }, { '$group': { '_id': '$_id', 'Comments': { '$push': '$$ROOT' }, 'totalSaleAmount': { '$sum': '$Comments_Count' } } }, { '$sort': { 'totalSaleAmount': -1 } }, { '$limit': 10 } ] ``` ### 第二步 安裝 `NestCLI` #### 透過 `npm` 進行全域安裝 ``` $ npm install -g @nestjs/cli ``` 安裝完就可以在終端機使用 `NestCLI` ![](https://i.imgur.com/E5c7EDV.png) ### 第三步 建置 `post-api` #### 透過 `new` 指令來快速建立 App: ``` $ nest new post-api ``` 這裡我選擇使用`npm` ![](https://i.imgur.com/AYYa01r.png) 接下來執行看看 ``` $ cd post-api $ npm start ``` ![](https://i.imgur.com/EAHFGv0.png) ### 第四步 `mongoose` 環境建置 #### 安裝 `mongoose` 前面使用`npm`,所以這裡使用`npm install` ``` $ npm install @nestjs/mongoose mongoose ``` #### 連線 `MongoDB` 前置作業 首先,在 `post-api` 資料夾下新增`.env` ``` MONGO_USERNAME=<username> MONGO_PASSWORD=<password> MONGO_RESOURCE=<DB_URL> ``` 接下來,在 `src/config` 資料夾下新增 `mongo.config.ts` ``` import { registerAs } from '@nestjs/config'; export default registerAs('mongo', () => { const username = process.env.MONGO_USERNAME; const password = encodeURIComponent(process.env.MONGO_PASSWORD); const resource = process.env.MONGO_RESOURCE; const uri = `mongodb+srv://${username}:${password}@${resource}?retryWrites=true&w=majority`; return { username, password, resource, uri }; }); ``` > 這裡可能會找不到`@nestjs/config`模組 > 一樣使用`npm`來安裝 > `$ npm install @nestjs/config` #### `app.module.ts` 引入 `ConfigModule`, `ConfigService` 及 `MongooseModule` ``` import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { MongooseModule } from '@nestjs/mongoose'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { PostModule } from './features/post/post.module'; import MongoConfigFactory from './config/mongo.config'; @Module({ imports: [ ConfigModule.forRoot({ load: [MongoConfigFactory] }), MongooseModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: async (config: ConfigService) => ({ uri: config.get<string>('mongo.uri') }) }), PostModule ], controllers: [AppController], providers: [AppService], }) export class AppModule {} ``` 到這裡應該就可以成功與`MongoDB`連線 ![](https://i.imgur.com/BXGH5mC.png) ### 第五步 實作及使用`Model` #### 建立及產生 `Schema` 在`src/common/models`下,建立`posts.model.ts`和`comments.model.ts` #### `posts.model.ts` ``` import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; export type PostsDocument = Posts & Document; @Schema() export class Posts { @Prop({ required: true }) Text: String; @Prop({ required: true }) Date: Date; @Prop({ required: true }) Author: String; } export const PostsSchema = SchemaFactory.createForClass(Posts); ``` #### `comments.model.ts` ``` import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document, Types, SchemaTypes } from 'mongoose'; import { Posts } from './posts.model'; export type CommentsDocument = Comments & Document; @Schema() export class Comments { @Prop({ type: String, required: true }) Text: String; @Prop({ type: Date, required: true }) Date: Date; @Prop({ type: String, required: true }) Author: String; @Prop({ required: false, type:[{ Text:{type: String, required: true}, Date:{type: Date, required: true}, Author:{type: String, required: true} }] }) Replies: { Text: String; Date: Date; Author: String}[]; @Prop({ type: SchemaTypes.ObjectId, ref: Posts.name, required:true }) Post: Types.ObjectId; } export const CommentsSchema = SchemaFactory.createForClass(Comments); ``` #### 實作`Model` 使用`NestCLI`建立`PostModule`, `PostController`及`PostService` ``` $ nest generate module features/post $ nest generate controller features/post $ nest generate service features/post ``` 使用`NestCLI`建立`CommentModule`, `CommentController`及`CommentService` ``` $ nest generate module features/comment $ nest generate controller features/comment $ nest generate service features/comment ``` #### `post.module.ts` 引入`MongooseModule`,並使用`Posts`的`Model` ``` import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Posts, PostsSchema } from '../../common/models/posts.model'; import { PostController } from './post.controller'; import { PostService } from './post.service'; @Module({ imports: [ MongooseModule.forFeature([ { name: Posts.name, schema: PostsSchema } ]) ], controllers: [PostController], providers: [PostService] }) export class PostModule {} ``` #### `post.service.ts` 注入`PostModel`到`PostService`的`@InjectModel` ``` import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Posts, PostsDocument } from '../../common/models/posts.model'; @Injectable() export class PostService { constructor( @InjectModel(Posts.name) private readonly postModel: Model<PostsDocument> ) {} } ``` #### `comment.module.ts` 引入`MongooseModule`,並使用`Comment`的`Model` ``` import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { Comments, CommentsSchema } from '../../common/models/comments.models'; import { CommentController } from './comment.controller'; import { CommentService } from './comment.service'; @Module({ imports: [ MongooseModule.forFeature([ { name: Comments.name, schema: CommentsSchema } ]) ], controllers: [CommentController], providers: [CommentService] }) export class CommentModule {} ``` #### `comment.service.ts` 注入`CommentModel`到`CommentService`的`@InjectModel` ``` import { Injectable } from '@nestjs注入Model/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Comments, CommentsDocument } from '../../common/models/comments.model'; @Injectable() export class CommentService { constructor( @InjectModel(Comments.name) private readonly commentModel: Model<CommentsDocument> ) {} } ``` ### 第六步 實作 `API` #### 實作`CRUD` * 新增 `create`, `findById`, `updateById`, `removeById`,並呼叫`Model`來達到實現`CRUD`的目的 #### `post.service.ts` ``` import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Posts, PostsDocument } from '../../common/models/posts.model'; @Injectable() export class PostService { constructor( @InjectModel(Posts.name) private readonly postModel: Model<PostsDocument> ) {} create(post: any) { return this.postModel.create(post); } findById(id: string) { return this.postModel.findById(id); } updateById(id: string, data: any) { return this.postModel.findByIdAndUpdate(id, data, { new: true }); } removeById(id: string) { return this.postModel.deleteOne({ _id: id }); } } ``` #### `comment.service.ts` ``` import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Comments, CommentsDocument } from '../../common/models/comments.model'; @Injectable() export class CommentService { constructor( @InjectModel(Comments.name) private readonly commentModel: Model<CommentsDocument> ) {} create(post: any) { return this.commentModel.create(post); } findById(id: string) { return this.commentModel.findById(id); } updateById(id: string, data: any) { return this.commentModel.findByIdAndUpdate(id, data, { new: true }); } removeById(id: string) { return this.commentModel.deleteOne({ _id: id }); } } ``` * 新增 `@POST`, `@GET`, `@Patch`, `@Delete`實現`CRUD`,並且回傳執行後的`document`結果 #### `post.controller.ts` ``` import { Body, Controller, Post, Get, Param, Patch, Delete} from '@nestjs/common'; import { PostService } from './post.service'; @Controller('post') export class PostController { constructor( private readonly postService: PostService ) {} @Post() create(@Body() body: any) { return this.postService.create(body); } @Get(':id') findById(@Param('id') id: string) { return this.postService.findById(id); } @Patch(':id') updateById( @Param('id') id: string, @Body() body: any ) { return this.postService.updateById(id, body); } @Delete(':id') removeById(@Param('id') id: string) { return this.postService.removeById(id); } } ``` #### `comment.controller.ts` ``` import { Body, Controller, Post, Get, Param, Patch, Delete} from '@nestjs/common'; import { CommentService } from './comment.service'; @Controller('comment') export class CommentController { constructor( private readonly commentService: CommentService ) {} @Post() create(@Body() body: any) { return this.commentService.create(body); } @Get(':id') findById(@Param('id') id: string) { return this.commentService.findById(id); } @Patch(':id') updateById( @Param('id') id: string, @Body() body: any ) { return this.commentService.updateById(id, body); } @Delete(':id') removeById(@Param('id') id: string) { return this.commentService.removeById(id); } } ``` ### 第七步 測試 `API` #### 使用`Postman`進行測試 * #### `新增(Create)`![](https://i.imgur.com/0F1IMN6.png)![](https://i.imgur.com/jEGvyiT.png) * #### `讀取(Read)`![](https://i.imgur.com/aolOAJV.png)![](https://i.imgur.com/NzxBNVg.png) * #### `更新(Update)`![](https://i.imgur.com/81q3sv1.png)![](https://i.imgur.com/hYBD2C3.png) * #### `刪除(Delete)`![](https://i.imgur.com/28tm5wa.png)![](https://i.imgur.com/U8uQyiH.png) # **結束**