# ***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 }
}
```
#### 如下圖

再來,我會創建另一個名為`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 } ,
}]
}
```
#### 如下圖

最後,我們需要一個`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`

### 第三步 建置 `post-api`
#### 透過 `new` 指令來快速建立 App:
```
$ nest new post-api
```
這裡我選擇使用`npm`

接下來執行看看
```
$ cd post-api
$ npm start
```

### 第四步 `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`連線

### 第五步 實作及使用`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)`
* #### `讀取(Read)`
* #### `更新(Update)`
* #### `刪除(Delete)`
# **結束**