mirror of
https://github.com/hantsy/nestjs-rest-sample.git
synced 2025-12-08 20:36:27 +00:00
feat(mongo): connect to mongo
This commit is contained in:
parent
e37124bfd0
commit
0c985e505f
@ -5,6 +5,7 @@ A NestJS RESTful APIs sample project.
|
|||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
* [Building RESTful APIs with NestJS](./docs/guide.md)
|
* [Building RESTful APIs with NestJS](./docs/guide.md)
|
||||||
|
* [Connecting to MongoDB](./docs/mongo.md)
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
@ -44,4 +45,6 @@ $ npm run test:cov
|
|||||||
|
|
||||||
## Reference
|
## Reference
|
||||||
|
|
||||||
* [The official Nestjs documentation](https://docs.nestjs.com/first-steps)
|
* [The official Nestjs documentation](https://docs.nestjs.com/first-steps)
|
||||||
|
* [Unit testing NestJS applications with Jest](https://blog.logrocket.com/unit-testing-nestjs-applications-with-jest/)
|
||||||
|
* [ts-mockito: Mocking library for TypeScript inspired by http://mockito.org/](https://github.com/NagRock/ts-mockito)
|
||||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
version: '3.8' # specify docker-compose version
|
||||||
|
|
||||||
|
# Define the services/containers to be run
|
||||||
|
services:
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
image: mongo
|
||||||
|
volumes:
|
||||||
|
- mongodata:/data/db
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
728
docs/mongo.md
Normal file
728
docs/mongo.md
Normal file
@ -0,0 +1,728 @@
|
|||||||
|
# Connecting to MongoDB
|
||||||
|
|
||||||
|
In the last post , we created a RESTful API application for simple CRUD functionalities. In this post, we will enrich it:
|
||||||
|
|
||||||
|
* Add Mongo support
|
||||||
|
* Change dummy data storage to use MongoDB
|
||||||
|
* Clean the testing codes
|
||||||
|
|
||||||
|
Let's go.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Adding MongooseModule
|
||||||
|
|
||||||
|
[MongoDB](https://www.mongodb.com) is a leading document-based NoSQL database. Nestjs has official support for MongoDB via its [Mongoosejs](https://mongoosejs.com/) integration.
|
||||||
|
|
||||||
|
Firstly, install the following dependencies.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save @nestjs/mongoose mongoose
|
||||||
|
npm install --save-dev @types/mongoose
|
||||||
|
```
|
||||||
|
|
||||||
|
Declare a `MongooseModule ` in the top-level `AppModule` to activate Mongoose support.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
//... other imports
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
//...other modules imports
|
||||||
|
// add MongooseModule
|
||||||
|
MongooseModule.forRoot('mongodb://localhost/blog')
|
||||||
|
],
|
||||||
|
//... providers, controllers
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
Mongoose requires a Schema definition to describe every document in MongoDB. Nestjs provides a simple to combine the schema definition and document POJO in the same form.
|
||||||
|
|
||||||
|
Rename our former *post.interface.ts* to *post.model.ts*, the *.model* suffix means it is a Mongoose managed `Model`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { SchemaFactory, Schema, Prop } from '@nestjs/mongoose';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
|
||||||
|
@Schema()
|
||||||
|
export class Post extends Document {
|
||||||
|
@Prop({ required: true })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
createdAt?: Date;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
export const PostSchema = SchemaFactory.createForClass(Post);
|
||||||
|
```
|
||||||
|
|
||||||
|
The annotations `@Schema`, `@Prop` defines the schema definitions on the document class instead of a [mongoose Schema](https://mongoosejs.com/docs/guide.html#definition) function.
|
||||||
|
|
||||||
|
Open `PostModule`, declare a `posts` collection for storing `Post` document via importing a `MongooseModule.forFeature`.
|
||||||
|
|
||||||
|
```
|
||||||
|
import { PostSchema } from './post.model';
|
||||||
|
//other imports
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [MongooseModule.forFeature([{ name: 'posts', schema: PostSchema }])],
|
||||||
|
// providers, controllers
|
||||||
|
})
|
||||||
|
export class PostModule {}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Refactoring PostService
|
||||||
|
|
||||||
|
When a Mongoose schema (eg. `PostSchame`) is mapped to a document collection(eg. `posts`), a Mongoose `Model` is ready for the operations of this collections in MongoDB.
|
||||||
|
|
||||||
|
Open *post.service.ts* file.
|
||||||
|
|
||||||
|
Change the content to the following:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
export class PostService {
|
||||||
|
constructor(@InjectModel('posts') private postModel: Model<Post>) {}
|
||||||
|
|
||||||
|
findAll(keyword?: string, skip = 0, limit = 10): Observable<Post[]> {
|
||||||
|
if (keyword) {
|
||||||
|
return from(
|
||||||
|
this.postModel
|
||||||
|
.find({ title: { $regex: '.*' + keyword + '.*' } })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.exec(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return from(
|
||||||
|
this.postModel
|
||||||
|
.find({})
|
||||||
|
.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.exec(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string): Observable<Post> {
|
||||||
|
return from(this.postModel.findOne({ _id: id }).exec());
|
||||||
|
}
|
||||||
|
|
||||||
|
save(data: CreatePostDto): Observable<Post> {
|
||||||
|
const createPost = this.postModel.create({ ...data });
|
||||||
|
return from(createPost);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, data: UpdatePostDto): Observable<Post> {
|
||||||
|
return from(this.postModel.findOneAndUpdate({ _id: id }, data).exec());
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteById(id: string): Observable<Post> {
|
||||||
|
return from(this.postModel.findOneAndDelete({ _id: id }).exec());
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll(): Observable<any> {
|
||||||
|
return from(this.postModel.deleteMany({}).exec());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
In the constructor of `PostService` class, use a `@InjectMock('posts')` to bind the `posts` collection to a parameterized Model handler.
|
||||||
|
|
||||||
|
The usage of all mongoose related functions can be found in [the official Mongoose docs](https://mongoosejs.com/docs/api/query.html).
|
||||||
|
|
||||||
|
As you see, we also add two classes `CreatePostDto` and `UpdatePostDto` instead of the original `Post` for the case of creating and updating posts.
|
||||||
|
|
||||||
|
Following the principle [separation of concerns](https://en.wikipedia.org/wiki/Separation_of_concerns), `CreatePostDto` and `UpdatePostDto` are only used for transform data from client, add `readonly` modifier to keep the data *immutable* in the transforming progress.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// create-post.dto.ts
|
||||||
|
export class CreatePostDto {
|
||||||
|
readonly title: string;
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
// update-post.dto.ts
|
||||||
|
export class UpdatePostDto {
|
||||||
|
readonly title: string;
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clean up PostController
|
||||||
|
|
||||||
|
Clean the `post.controller.ts` correspondingly.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Controller('posts')
|
||||||
|
export class PostController {
|
||||||
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
|
@Get('')
|
||||||
|
getAllPosts(
|
||||||
|
@Query('q') keyword?: string,
|
||||||
|
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
|
||||||
|
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip?: number,
|
||||||
|
): Observable<BlogPost[]> {
|
||||||
|
return this.postService.findAll(keyword, skip, limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
getPostById(@Param('id') id: string): Observable<BlogPost> {
|
||||||
|
return this.postService.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('')
|
||||||
|
createPost(@Body() post: CreatePostDto): Observable<BlogPost> {
|
||||||
|
return this.postService.save(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
updatePost(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body() post: UpdatePostDto,
|
||||||
|
): Observable<BlogPost> {
|
||||||
|
return this.postService.update(id, post);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
deletePostById(@Param('id') id: string): Observable<BlogPost> {
|
||||||
|
return this.postService.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> Unluckily, Mongoose APIs has no built-in Rxjs's `Observable` support, if you are stick on Rxjs, you have to use `from` to wrap an existing `Promise` to Rxjs's `Observable`. Check [this topic on stackoverflow to know more details about the difference bwteen Promise and Observable](https://stackoverflow.com/questions/37364973/what-is-the-difference-between-promises-and-observables).
|
||||||
|
|
||||||
|
## Run the application
|
||||||
|
|
||||||
|
To run the application, a running MongoDB server is required. You can download a copy from [MongoDB](https://www.mongodb.com), and follow the [installation guide](https://docs.mongodb.com/manual/installation/) to install it into your system.
|
||||||
|
|
||||||
|
Simply, prepare a *docker-compose.yaml* to run the dependent servers in the development stage.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3.8' # specify docker-compose version
|
||||||
|
|
||||||
|
# Define the services/containers to be run
|
||||||
|
services:
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
image: mongo
|
||||||
|
volumes:
|
||||||
|
- mongodata:/data/db
|
||||||
|
ports:
|
||||||
|
- "27017:27017"
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
mongodata:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the following command to start up a mongo service in a Docker container.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute the following command in the project root folder to start up the application.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
Now open your terminal and use `curl` to test the endpoints, and make sure it works as expected.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
>curl http://localhost:3000/posts/
|
||||||
|
[]
|
||||||
|
```
|
||||||
|
|
||||||
|
There is no sample data in the MongoDB. Utilizing with [the lifecycle events](https://docs.nestjs.com/fundamentals/lifecycle-events), it is easy to add some sample data for the test purpose.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
@Injectable()
|
||||||
|
export class DataInitializerService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private data: CreatePostDto[] = [
|
||||||
|
{
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Create CRUD RESTful APIs',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Connect to MongoDB',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private postService: PostService) {}
|
||||||
|
onModuleInit(): void {
|
||||||
|
this.data.forEach(d => {
|
||||||
|
this.postService.save(d).subscribe(saved => console.log(saved));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onModuleDestroy(): void {
|
||||||
|
console.log('module is be destroying...');
|
||||||
|
this.postService
|
||||||
|
.deleteAll()
|
||||||
|
.subscribe(del => console.log(`deleted ${del} records.`));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Register it in `PostModule`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
//other imports
|
||||||
|
import { DataInitializerService } from './data-initializer.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
//imports, controllers...
|
||||||
|
providers: [//other services...
|
||||||
|
DataInitializerService],
|
||||||
|
})
|
||||||
|
export class PostModule {}
|
||||||
|
```
|
||||||
|
Run the application again. Now you will see some data when hinting *http://localhost:3000/posts/*.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
>curl http://localhost:3000/posts/
|
||||||
|
[{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb732f","title":"Create CRUD RESTful APIs","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb7330","title":"Connect to MongoDB","content":"content","__v":0}]
|
||||||
|
|
||||||
|
>curl http://localhost:3000/posts/5ee49c3115a4e75254bb732e
|
||||||
|
{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0}
|
||||||
|
|
||||||
|
>curl http://localhost:3000/posts/ -d "{\"title\":\"new post\",\"content\":\"content of my new post\"}" -H "Content-Type:application/json" -X POST
|
||||||
|
{"_id":"5ee49ca915a4e75254bb7331","title":"new post","content":"content of my new post","__v":0}
|
||||||
|
|
||||||
|
>curl http://localhost:3000/posts/5ee49ca915a4e75254bb7331 -d "{\"title\":\"my updated post\",\"content\":\"content of my new post\"}" -H "Content-Type:application/json" -X PUT
|
||||||
|
{"_id":"5ee49ca915a4e75254bb7331","title":"new post","content":"content of my new post","__v":0}
|
||||||
|
|
||||||
|
>curl http://localhost:3000/posts
|
||||||
|
[{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb732f","title":"Create CRUD RESTful APIs","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb7330","title":"Connect to MongoDB","content":"content","__v":0},{"_id":"5ee49ca915a4e75254bb7331","title":"my updated post","content":"content of my new post","__v":0}]
|
||||||
|
|
||||||
|
>curl http://localhost:3000/posts/5ee49ca915a4e75254bb7331 -X DELETE
|
||||||
|
{"_id":"5ee49ca915a4e75254bb7331","title":"my updated post","content":"content of my new post","__v":0}
|
||||||
|
|
||||||
|
>curl http://localhost:3000/posts
|
||||||
|
[{"_id":"5ee49c3115a4e75254bb732e","title":"Generate a NestJS project","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb732f","title":"Create CRUD RESTful APIs","content":"content","__v":0},{"_id":"5ee49c3115a4e75254bb7330","title":"Connect to MongoDB","content":"content","__v":0}]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Clean the testing codes
|
||||||
|
|
||||||
|
When switching to real data storage instead of the dummy array, we face the first issue is how to treat with the Mongo database dependency in our *post.service.spec.ts*.
|
||||||
|
|
||||||
|
Jest provides comprehensive mocking features to isolate the dependencies in test cases. Let's have a look at the whole content of the refactored *post.service.spec.ts* file.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('PostService', () => {
|
||||||
|
let service: PostService;
|
||||||
|
let model: Model<Post>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
PostService,
|
||||||
|
{
|
||||||
|
provide: getModelToken('posts'),
|
||||||
|
useValue: {
|
||||||
|
new: jest.fn(),
|
||||||
|
constructor: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
exec: jest.fn(),
|
||||||
|
findOneAndUpdate: jest.fn(),
|
||||||
|
findOneAndDelete: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<PostService>(PostService);
|
||||||
|
model = module.get<Model<Post>>(getModelToken('posts'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAllPosts should return 3 posts', async () => {
|
||||||
|
const posts = [
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb732f',
|
||||||
|
title: 'Create CRUD RESTful APIs',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb7330',
|
||||||
|
title: 'Connect to MongoDB',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
jest.spyOn(model, 'find').mockReturnValue({
|
||||||
|
skip: jest.fn().mockReturnValue({
|
||||||
|
limit: jest.fn().mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce(posts) as any,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
const data = await service.findAll().toPromise();
|
||||||
|
expect(data.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getPostById with existing id should return 1 post', done => {
|
||||||
|
const found = {
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(model, 'findOne').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce(found) as any,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
service.findById('1').subscribe({
|
||||||
|
next: data => {
|
||||||
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
|
expect(data.title).toEqual('Generate a NestJS project');
|
||||||
|
},
|
||||||
|
error: error => console.log(error),
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should save post', async () => {
|
||||||
|
const toCreated = {
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
};
|
||||||
|
|
||||||
|
const toReturned = {
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
...toCreated,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(model, 'create').mockResolvedValue(toReturned as Post);
|
||||||
|
|
||||||
|
const data = await service.save(toCreated).toPromise();
|
||||||
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
|
expect(model.create).toBeCalledWith(toCreated);
|
||||||
|
expect(model.create).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update post', done => {
|
||||||
|
const toUpdated = {
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(model, 'findOneAndUpdate').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce(toUpdated) as any,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
|
||||||
|
next: data => {
|
||||||
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
|
},
|
||||||
|
error: error => console.log(error),
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete post', done => {
|
||||||
|
jest.spyOn(model, 'findOneAndDelete').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce({
|
||||||
|
deletedCount: 1,
|
||||||
|
}),
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
service.deleteById('anystring').subscribe({
|
||||||
|
next: data => expect(data).toBeTruthy,
|
||||||
|
error: error => console.log(error),
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
In the above codes,
|
||||||
|
|
||||||
|
* Use a custom *Provider* to provide a `PostModel` dependency for `PostService`, the Model is provided in `useValue` which hosted a mocked object instance for PostModel at runtime.
|
||||||
|
* In every test case, use `jest.spyOn` to assume some mocked behaviors of PostModel happened before the service is executed.
|
||||||
|
* You can use the `toBeCalledWith` like assertions on mocked object or spied object.
|
||||||
|
|
||||||
|
> For me, most of time working as a Java/Spring developers, constructing such a simple Jest based test is not easy, [jmcdo29/testing-nestjs](https://github.com/jmcdo29/testing-nestjs) is very helpful for me to jump into jest testing work.
|
||||||
|
>
|
||||||
|
> The jest mock is every different from Mockito in Java. Luckily there is a ts-mockito which port Mockito to the Typescript world, check [this link](https://github.com/NagRock/ts-mockito) for more details .
|
||||||
|
|
||||||
|
OK, let's move to *post.controller.spec.ts*.
|
||||||
|
|
||||||
|
Similarly, `PostController` depends on `PostService`. To test the functionalities of `PostController`, we should mock it.
|
||||||
|
|
||||||
|
Like the method we used in `post.service.spec.ts`, we can mock it in a `Provider`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Post Controller(useValue jest mocking)', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
let postService: PostService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useValue: {
|
||||||
|
findAll: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(_keyword?: string, _skip?: number, _limit?: number) =>
|
||||||
|
of<any[]>([
|
||||||
|
{
|
||||||
|
_id: 'testid',
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [PostController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PostController>(PostController);
|
||||||
|
postService = module.get<PostService>(PostService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts(useValue: jest mocking)', async () => {
|
||||||
|
const result = await controller.getAllPosts('test', 10, 0).toPromise();
|
||||||
|
expect(result[0]._id).toEqual('testid');
|
||||||
|
expect(postService.findAll).toBeCalled();
|
||||||
|
expect(postService.findAll).lastCalledWith('test', 0, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead of the jest mocking, you can use a dummy implementation directly in the `Provider`.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
describe('Post Controller(useValue fake object)', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useValue: {
|
||||||
|
findAll: (_keyword?: string, _skip?: number, _limit?: number) =>
|
||||||
|
of<any[]>([
|
||||||
|
{ _id: 'testid', title: 'test title', content: 'test content' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [PostController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PostController>(PostController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts(useValue: fake object)', async () => {
|
||||||
|
const result = await controller.getAllPosts().toPromise();
|
||||||
|
expect(result[0]._id).toEqual('testid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use fake class to replace the real `PostService` in the tests.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
class PostServiceFake {
|
||||||
|
private posts = [
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb732f',
|
||||||
|
title: 'Create CRUD RESTful APIs',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb7330',
|
||||||
|
title: 'Connect to MongoDB',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return of(this.posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string) {
|
||||||
|
const { title, content } = this.posts[0];
|
||||||
|
return of({ _id: id, title, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
save(data: CreatePostDto) {
|
||||||
|
return of({ _id: this.posts[0]._id, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, data: UpdatePostDto) {
|
||||||
|
return of({ _id: id, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteById(id: string) {
|
||||||
|
return of({ ...this.posts[0], _id: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Post Controller', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useClass: PostServiceFake,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [PostController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PostController>(PostController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET on /posts should return all posts', async () => {
|
||||||
|
const posts = await controller.getAllPosts().toPromise();
|
||||||
|
expect(posts.length).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET on /posts/1 should return one post ', done => {
|
||||||
|
controller.getPostById('1').subscribe(data => {
|
||||||
|
expect(data._id).toEqual('1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST on /posts should return all posts', async () => {
|
||||||
|
const post: CreatePostDto = {
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
};
|
||||||
|
const saved = await controller.createPost(post).toPromise();
|
||||||
|
expect(saved.title).toEqual('test title');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('PUT on /posts/1 should return all posts', done => {
|
||||||
|
const post: UpdatePostDto = {
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
};
|
||||||
|
controller.updatePost('1', post).subscribe(data => {
|
||||||
|
expect(data.title).toEqual('test title');
|
||||||
|
expect(data.content).toEqual('test content');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE on /posts/1 should return true', done => {
|
||||||
|
controller.deletePostById('1').subscribe(data => {
|
||||||
|
expect(data).toBeTruthy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
The above codes are more close the ones in the first article, it is simple and easy to understand.
|
||||||
|
|
||||||
|
> To ensure the fake PostService has the exact method signature of the real PostService, it is better to use an interface to define the methods if you prefer this apporach.
|
||||||
|
|
||||||
|
I have mentioned *ts-mockito*, for me it is easier to boost up a Mockito like test.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save-dev ts-mockito
|
||||||
|
```
|
||||||
|
A simple mockito based test looks like this.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// import facilites from ts-mockito
|
||||||
|
import { mock, verify, instance, anyString, anyNumber, when } from 'ts-mockito';
|
||||||
|
|
||||||
|
describe('Post Controller(using ts-mockito)', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
const mockedPostService: PostService = mock(PostService);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
controller = new PostController(instance(mockedPostService));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts(ts-mockito)', async () => {
|
||||||
|
when(
|
||||||
|
mockedPostService.findAll(anyString(), anyNumber(), anyNumber()),
|
||||||
|
).thenReturn(
|
||||||
|
of([
|
||||||
|
{ _id: 'testid', title: 'test title', content: 'content' },
|
||||||
|
]) as Observable<Post[]>,
|
||||||
|
);
|
||||||
|
const result = await controller.getAllPosts('', 10, 0).toPromise();
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].title).toBe('test title');
|
||||||
|
verify(
|
||||||
|
mockedPostService.findAll(anyString(), anyNumber(), anyNumber()),
|
||||||
|
).once();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
Now run the tests again. All tests should pass.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
> npm run test
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
PASS src/app.controller.spec.ts
|
||||||
|
PASS src/post/post.service.spec.ts (10.307 s)
|
||||||
|
PASS src/post/post.controller.spec.ts (10.471 s)
|
||||||
|
|
||||||
|
Test Suites: 3 passed, 3 total
|
||||||
|
Tests: 17 passed, 17 total
|
||||||
|
Snapshots: 0 total
|
||||||
|
Time: 11.481 s, estimated 12 s
|
||||||
|
Ran all test suites.
|
||||||
|
```
|
||||||
|
|
||||||
|
In this post, we connected to the real MongoDB instead of the dummy data storage, correspond to the changes , we have refactored all tests, and discuss some approaches to isolate the dependencies in tests. But we have not test all functionalities in a real integrated environment, Nestjs provides e2e testing skeleton, we will discuss it in a future post.
|
||||||
221
package-lock.json
generated
221
package-lock.json
generated
@ -1759,6 +1759,11 @@
|
|||||||
"uuid": "8.0.0"
|
"uuid": "8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@nestjs/mongoose": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@nestjs/mongoose/download/@nestjs/mongoose-7.0.1.tgz",
|
||||||
|
"integrity": "sha1-gy7HJiHMkQPAPcKUgYwa50mkg+M="
|
||||||
|
},
|
||||||
"@nestjs/platform-express": {
|
"@nestjs/platform-express": {
|
||||||
"version": "7.1.2",
|
"version": "7.1.2",
|
||||||
"resolved": "https://registry.npm.taobao.org/@nestjs/platform-express/download/@nestjs/platform-express-7.1.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/@nestjs/platform-express/download/@nestjs/platform-express-7.1.2.tgz",
|
||||||
@ -1887,6 +1892,15 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/bson": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/bson/download/@types/bson-4.0.2.tgz",
|
||||||
|
"integrity": "sha1-esy4WUL8ObvbdRXU3kN8BPaYEV8=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/color-name": {
|
"@types/color-name": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npm.taobao.org/@types/color-name/download/@types/color-name-1.1.1.tgz?cache=0&sync_timestamp=1588200011932&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fcolor-name%2Fdownload%2F%40types%2Fcolor-name-1.1.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/@types/color-name/download/@types/color-name-1.1.1.tgz?cache=0&sync_timestamp=1588200011932&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fcolor-name%2Fdownload%2F%40types%2Fcolor-name-1.1.1.tgz",
|
||||||
@ -2005,6 +2019,26 @@
|
|||||||
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
|
"integrity": "sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"@types/mongodb": {
|
||||||
|
"version": "3.5.20",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/mongodb/download/@types/mongodb-3.5.20.tgz",
|
||||||
|
"integrity": "sha1-QGWvxVtiddDdjoMfpCEBhHXVTVY=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/bson": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@types/mongoose": {
|
||||||
|
"version": "5.7.24",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/@types/mongoose/download/@types/mongoose-5.7.24.tgz",
|
||||||
|
"integrity": "sha1-WSJJQ1oun+4CyellCv6sSAfU7wY=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/mongodb": "*",
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "13.13.9",
|
"version": "13.13.9",
|
||||||
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-13.13.9.tgz",
|
"resolved": "https://registry.npm.taobao.org/@types/node/download/@types/node-13.13.9.tgz",
|
||||||
@ -3022,6 +3056,44 @@
|
|||||||
"file-uri-to-path": "1.0.0"
|
"file-uri-to-path": "1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"bl": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/bl/download/bl-2.2.0.tgz",
|
||||||
|
"integrity": "sha1-4aV0zfUo5AUwGbuACwQcCsiNpJM=",
|
||||||
|
"requires": {
|
||||||
|
"readable-stream": "^2.3.5",
|
||||||
|
"safe-buffer": "^5.1.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/isarray/download/isarray-1.0.0.tgz?cache=0&sync_timestamp=1562592096220&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fisarray%2Fdownload%2Fisarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
|
||||||
|
},
|
||||||
|
"readable-stream": {
|
||||||
|
"version": "2.3.7",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/readable-stream/download/readable-stream-2.3.7.tgz?cache=0&sync_timestamp=1581624324274&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Freadable-stream%2Fdownload%2Freadable-stream-2.3.7.tgz",
|
||||||
|
"integrity": "sha1-Hsoc9xGu+BTAT2IlKjamL2yyO1c=",
|
||||||
|
"requires": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.3",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~2.0.0",
|
||||||
|
"safe-buffer": "~5.1.1",
|
||||||
|
"string_decoder": "~1.1.1",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"string_decoder": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/string_decoder/download/string_decoder-1.1.1.tgz",
|
||||||
|
"integrity": "sha1-nPFhG6YmhdcDCunkujQUnDrwP8g=",
|
||||||
|
"requires": {
|
||||||
|
"safe-buffer": "~5.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"bluebird": {
|
"bluebird": {
|
||||||
"version": "3.7.2",
|
"version": "3.7.2",
|
||||||
"resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.7.2.tgz",
|
||||||
@ -3224,6 +3296,11 @@
|
|||||||
"node-int64": "^0.4.0"
|
"node-int64": "^0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"bson": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/bson/download/bson-1.1.4.tgz",
|
||||||
|
"integrity": "sha1-92hw15nxW4VN/7fuMvCodHl/fok="
|
||||||
|
},
|
||||||
"buffer": {
|
"buffer": {
|
||||||
"version": "4.9.2",
|
"version": "4.9.2",
|
||||||
"resolved": "https://registry.npm.taobao.org/buffer/download/buffer-4.9.2.tgz?cache=0&sync_timestamp=1588706716358&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbuffer%2Fdownload%2Fbuffer-4.9.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/buffer/download/buffer-4.9.2.tgz?cache=0&sync_timestamp=1588706716358&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fbuffer%2Fdownload%2Fbuffer-4.9.2.tgz",
|
||||||
@ -4328,6 +4405,11 @@
|
|||||||
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"denque": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/denque/download/denque-1.4.1.tgz",
|
||||||
|
"integrity": "sha1-Z0T/dkHBSMP4ppwwflEjXB9KN88="
|
||||||
|
},
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/depd/download/depd-1.1.2.tgz",
|
||||||
@ -9431,6 +9513,11 @@
|
|||||||
"verror": "1.10.0"
|
"verror": "1.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"kareem": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/kareem/download/kareem-2.3.1.tgz",
|
||||||
|
"integrity": "sha1-3vEtnJQQF/q/sA+HOvlenJnhvoc="
|
||||||
|
},
|
||||||
"kind-of": {
|
"kind-of": {
|
||||||
"version": "6.0.3",
|
"version": "6.0.3",
|
||||||
"resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-6.0.3.tgz",
|
"resolved": "https://registry.npm.taobao.org/kind-of/download/kind-of-6.0.3.tgz",
|
||||||
@ -9731,6 +9818,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"memory-pager": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/memory-pager/download/memory-pager-1.5.0.tgz",
|
||||||
|
"integrity": "sha1-2HUWVdItOEaCdByXLyw9bfo+ZrU=",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"meow": {
|
"meow": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/meow/download/meow-5.0.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/meow/download/meow-5.0.0.tgz",
|
||||||
@ -10199,6 +10292,49 @@
|
|||||||
"minimist": "^1.2.5"
|
"minimist": "^1.2.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mongodb": {
|
||||||
|
"version": "3.5.8",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/mongodb/download/mongodb-3.5.8.tgz?cache=0&sync_timestamp=1591967946651&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmongodb%2Fdownload%2Fmongodb-3.5.8.tgz",
|
||||||
|
"integrity": "sha1-NFUIVkSbdF0UWHNzS/kiwS1rnKo=",
|
||||||
|
"requires": {
|
||||||
|
"bl": "^2.2.0",
|
||||||
|
"bson": "^1.1.4",
|
||||||
|
"denque": "^1.4.1",
|
||||||
|
"require_optional": "^1.0.1",
|
||||||
|
"safe-buffer": "^5.1.2",
|
||||||
|
"saslprep": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mongoose": {
|
||||||
|
"version": "5.9.18",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/mongoose/download/mongoose-5.9.18.tgz",
|
||||||
|
"integrity": "sha1-0DHtU7HmC6ShSR+1QZBorLHzPQA=",
|
||||||
|
"requires": {
|
||||||
|
"bson": "^1.1.4",
|
||||||
|
"kareem": "2.3.1",
|
||||||
|
"mongodb": "3.5.8",
|
||||||
|
"mongoose-legacy-pluralize": "1.0.2",
|
||||||
|
"mpath": "0.7.0",
|
||||||
|
"mquery": "3.2.2",
|
||||||
|
"ms": "2.1.2",
|
||||||
|
"regexp-clone": "1.0.0",
|
||||||
|
"safe-buffer": "5.1.2",
|
||||||
|
"sift": "7.0.1",
|
||||||
|
"sliced": "1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ms": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/ms/download/ms-2.1.2.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.1.2.tgz",
|
||||||
|
"integrity": "sha1-0J0fNXtEP0kzgqjrPM0YOHKuYAk="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"mongoose-legacy-pluralize": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/mongoose-legacy-pluralize/download/mongoose-legacy-pluralize-1.0.2.tgz",
|
||||||
|
"integrity": "sha1-O6n5H6UHtRhtOZ+0CFS/8Y+1Y+Q="
|
||||||
|
},
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz",
|
||||||
@ -10224,6 +10360,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"mpath": {
|
||||||
|
"version": "0.7.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/mpath/download/mpath-0.7.0.tgz",
|
||||||
|
"integrity": "sha1-IOgQLidrcXCdbgfp+NTQ9kGvv7g="
|
||||||
|
},
|
||||||
|
"mquery": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/mquery/download/mquery-3.2.2.tgz",
|
||||||
|
"integrity": "sha1-4Tg6OVGFLOI+N/YZqbNQ8fs2ZOc=",
|
||||||
|
"requires": {
|
||||||
|
"bluebird": "3.5.1",
|
||||||
|
"debug": "3.1.0",
|
||||||
|
"regexp-clone": "^1.0.0",
|
||||||
|
"safe-buffer": "5.1.2",
|
||||||
|
"sliced": "1.0.1"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bluebird": {
|
||||||
|
"version": "3.5.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/bluebird/download/bluebird-3.5.1.tgz",
|
||||||
|
"integrity": "sha1-2VUfnemPH82h5oPRfukaBgLuLrk="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/ms/download/ms-2.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fms%2Fdownload%2Fms-2.0.0.tgz",
|
||||||
@ -11368,6 +11528,11 @@
|
|||||||
"safe-regex": "^1.1.0"
|
"safe-regex": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"regexp-clone": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/regexp-clone/download/regexp-clone-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-Ii25Z2IydwViYLmSYmNUoEzpv2M="
|
||||||
|
},
|
||||||
"regexpp": {
|
"regexpp": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/regexpp/download/regexpp-3.1.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/regexpp/download/regexpp-3.1.0.tgz",
|
||||||
@ -11488,6 +11653,22 @@
|
|||||||
"integrity": "sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=",
|
"integrity": "sha1-0LMp7MfMD2Fkn2IhW+aa9UqomJs=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"require_optional": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/require_optional/download/require_optional-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-TPNaQkf2TKPfjC7yCMxJSxyo/C4=",
|
||||||
|
"requires": {
|
||||||
|
"resolve-from": "^2.0.0",
|
||||||
|
"semver": "^5.1.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"resolve-from": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/resolve-from/download/resolve-from-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
"version": "1.17.0",
|
"version": "1.17.0",
|
||||||
"resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.17.0.tgz",
|
"resolved": "https://registry.npm.taobao.org/resolve/download/resolve-1.17.0.tgz",
|
||||||
@ -11662,6 +11843,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"saslprep": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/saslprep/download/saslprep-1.0.3.tgz",
|
||||||
|
"integrity": "sha1-TAL5RrVs9UKX40e6EJPnrKxM8iY=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"sparse-bitfield": "^3.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"saxes": {
|
"saxes": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npm.taobao.org/saxes/download/saxes-5.0.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/saxes/download/saxes-5.0.1.tgz",
|
||||||
@ -11685,8 +11875,7 @@
|
|||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npm.taobao.org/semver/download/semver-5.7.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/semver/download/semver-5.7.1.tgz",
|
||||||
"integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc=",
|
"integrity": "sha1-qVT5Ma66UI0we78Gnv8MAclhFvc="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"semver-compare": {
|
"semver-compare": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@ -11842,6 +12031,11 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"sift": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/sift/download/sift-7.0.1.tgz",
|
||||||
|
"integrity": "sha1-R9YsULFZ0xbxNy+LU/nBDNIaSwg="
|
||||||
|
},
|
||||||
"signal-exit": {
|
"signal-exit": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz",
|
"resolved": "https://registry.npm.taobao.org/signal-exit/download/signal-exit-3.0.3.tgz",
|
||||||
@ -11879,6 +12073,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"sliced": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/sliced/download/sliced-1.0.1.tgz",
|
||||||
|
"integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E="
|
||||||
|
},
|
||||||
"snapdragon": {
|
"snapdragon": {
|
||||||
"version": "0.8.2",
|
"version": "0.8.2",
|
||||||
"resolved": "https://registry.npm.taobao.org/snapdragon/download/snapdragon-0.8.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/snapdragon/download/snapdragon-0.8.2.tgz",
|
||||||
@ -12056,6 +12255,15 @@
|
|||||||
"integrity": "sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=",
|
"integrity": "sha1-6oBL2UhXQC5pktBaOO8a41qatMQ=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"sparse-bitfield": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/sparse-bitfield/download/sparse-bitfield-3.0.3.tgz",
|
||||||
|
"integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=",
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"memory-pager": "^1.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"spdx-correct": {
|
"spdx-correct": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npm.taobao.org/spdx-correct/download/spdx-correct-3.1.1.tgz?cache=0&sync_timestamp=1590161967473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fspdx-correct%2Fdownload%2Fspdx-correct-3.1.1.tgz",
|
"resolved": "https://registry.npm.taobao.org/spdx-correct/download/spdx-correct-3.1.1.tgz?cache=0&sync_timestamp=1590161967473&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fspdx-correct%2Fdownload%2Fspdx-correct-3.1.1.tgz",
|
||||||
@ -12926,6 +13134,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ts-mockito": {
|
||||||
|
"version": "2.6.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/ts-mockito/download/ts-mockito-2.6.1.tgz",
|
||||||
|
"integrity": "sha1-vJ7iYZAzk05vrRxEVayltazjTnM=",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"lodash": "^4.17.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"ts-node": {
|
"ts-node": {
|
||||||
"version": "8.10.2",
|
"version": "8.10.2",
|
||||||
"resolved": "https://registry.npm.taobao.org/ts-node/download/ts-node-8.10.2.tgz",
|
"resolved": "https://registry.npm.taobao.org/ts-node/download/ts-node-8.10.2.tgz",
|
||||||
|
|||||||
@ -23,7 +23,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^7.0.0",
|
"@nestjs/common": "^7.0.0",
|
||||||
"@nestjs/core": "^7.0.0",
|
"@nestjs/core": "^7.0.0",
|
||||||
|
"@nestjs/mongoose": "^7.0.1",
|
||||||
"@nestjs/platform-express": "^7.0.0",
|
"@nestjs/platform-express": "^7.0.0",
|
||||||
|
"mongoose": "^5.9.18",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs": "^6.5.4"
|
"rxjs": "^6.5.4"
|
||||||
@ -36,6 +38,7 @@
|
|||||||
"@nestjs/testing": "^7.0.0",
|
"@nestjs/testing": "^7.0.0",
|
||||||
"@types/express": "^4.17.3",
|
"@types/express": "^4.17.3",
|
||||||
"@types/jest": "25.2.3",
|
"@types/jest": "25.2.3",
|
||||||
|
"@types/mongoose": "^5.7.24",
|
||||||
"@types/node": "^13.9.1",
|
"@types/node": "^13.9.1",
|
||||||
"@types/supertest": "^2.0.8",
|
"@types/supertest": "^2.0.8",
|
||||||
"@typescript-eslint/eslint-plugin": "3.0.2",
|
"@typescript-eslint/eslint-plugin": "3.0.2",
|
||||||
@ -49,6 +52,7 @@
|
|||||||
"supertest": "^4.0.2",
|
"supertest": "^4.0.2",
|
||||||
"ts-jest": "26.1.0",
|
"ts-jest": "26.1.0",
|
||||||
"ts-loader": "^6.2.1",
|
"ts-loader": "^6.2.1",
|
||||||
|
"ts-mockito": "^2.6.1",
|
||||||
"ts-node": "^8.6.2",
|
"ts-node": "^8.6.2",
|
||||||
"tsconfig-paths": "^3.9.0",
|
"tsconfig-paths": "^3.9.0",
|
||||||
"typescript": "^3.7.4"
|
"typescript": "^3.7.4"
|
||||||
|
|||||||
@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
|
|||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { PostModule } from './post/post.module';
|
import { PostModule } from './post/post.module';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PostModule],
|
imports: [PostModule, MongooseModule.forRoot('mongodb://localhost/blog')],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import { AppModule } from './app.module';
|
|||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
// enable shutdown hooks explicitly.
|
||||||
|
app.enableShutdownHooks();
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
bootstrap();
|
bootstrap();
|
||||||
|
|||||||
4
src/post/create-post.dto.ts
Normal file
4
src/post/create-post.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class CreatePostDto {
|
||||||
|
readonly title: string;
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
43
src/post/data-initializer.service.ts
Normal file
43
src/post/data-initializer.service.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { PostService } from './post.service';
|
||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||||
|
import { CreatePostDto } from './create-post.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DataInitializerService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private data: CreatePostDto[] = [
|
||||||
|
{
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Create CRUD RESTful APIs',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Connect to MongoDB',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
constructor(private postService: PostService) {}
|
||||||
|
onModuleInit(): void {
|
||||||
|
this.data.forEach(d => {
|
||||||
|
this.postService.save(d).subscribe(saved => console.log(saved));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onModuleDestroy(): void {
|
||||||
|
console.log('module is be destroying...');
|
||||||
|
this.postService
|
||||||
|
.deleteAll()
|
||||||
|
.subscribe(del => console.log(`deleted ${del} records.`));
|
||||||
|
}
|
||||||
|
// onApplicationBootstrap(): void {
|
||||||
|
// this.data.forEach(d => {
|
||||||
|
// this.save(d).subscribe(saved => console.log(saved));
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// onApplicationShutdown(signal?: string): void {
|
||||||
|
// console.log(signal);
|
||||||
|
// this.postModel.deleteMany({}).exec();
|
||||||
|
// }
|
||||||
|
}
|
||||||
@ -1,17 +1,64 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { PostController } from './post.controller';
|
import { PostController } from './post.controller';
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { async } from 'rxjs/internal/scheduler/async';
|
import { CreatePostDto } from './create-post.dto';
|
||||||
import { Post } from '../../dist/post/post.interface';
|
import { UpdatePostDto } from './update-post.dto';
|
||||||
|
import { of, Observable } from 'rxjs';
|
||||||
|
import { mock, verify, instance, anyString, anyNumber, when } from 'ts-mockito';
|
||||||
|
import { Post } from './post.model';
|
||||||
|
|
||||||
//jest.mock('./post.service');
|
class PostServiceFake {
|
||||||
|
private posts = [
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb732f',
|
||||||
|
title: 'Create CRUD RESTful APIs',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb7330',
|
||||||
|
title: 'Connect to MongoDB',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return of(this.posts);
|
||||||
|
}
|
||||||
|
|
||||||
|
findById(id: string) {
|
||||||
|
const { title, content } = this.posts[0];
|
||||||
|
return of({ _id: id, title, content });
|
||||||
|
}
|
||||||
|
|
||||||
|
save(data: CreatePostDto) {
|
||||||
|
return of({ _id: this.posts[0]._id, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
update(id: string, data: UpdatePostDto) {
|
||||||
|
return of({ _id: id, ...data });
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteById(id: string) {
|
||||||
|
return of({ ...this.posts[0], _id: id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
describe('Post Controller', () => {
|
describe('Post Controller', () => {
|
||||||
let controller: PostController;
|
let controller: PostController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [PostService],
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useClass: PostServiceFake,
|
||||||
|
},
|
||||||
|
],
|
||||||
controllers: [PostController],
|
controllers: [PostController],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@ -28,40 +75,159 @@ describe('Post Controller', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('GET on /posts/1 should return one post ', done => {
|
it('GET on /posts/1 should return one post ', done => {
|
||||||
controller.getPostById(1).subscribe(data => {
|
controller.getPostById('1').subscribe(data => {
|
||||||
expect(data.id).toEqual(1);
|
expect(data._id).toEqual('1');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('POST on /posts should return all posts', async () => {
|
it('POST on /posts should return all posts', async () => {
|
||||||
const post:Post = {id:4, title:'test title', content:'test content'};
|
const post: CreatePostDto = {
|
||||||
const posts = await controller.createPost(post).toPromise();
|
title: 'test title',
|
||||||
expect(posts.length).toBe(4);
|
content: 'test content',
|
||||||
|
};
|
||||||
|
const saved = await controller.createPost(post).toPromise();
|
||||||
|
expect(saved.title).toEqual('test title');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('PUT on /posts/1 should return all posts', done => {
|
it('PUT on /posts/1 should return all posts', done => {
|
||||||
const post:Post = {id:4, title:'test title', content:'test content'};
|
const post: UpdatePostDto = {
|
||||||
controller.updatePost(1, post).subscribe(data => {
|
title: 'test title',
|
||||||
expect(data.length).toBe(3);
|
content: 'test content',
|
||||||
expect(data[0].title).toEqual('test title');
|
};
|
||||||
expect(data[0].content).toEqual('test content');
|
controller.updatePost('1', post).subscribe(data => {
|
||||||
expect(data[0].updatedAt).toBeTruthy();
|
expect(data.title).toEqual('test title');
|
||||||
|
expect(data.content).toEqual('test content');
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DELETE on /posts/1 should return true', done => {
|
it('DELETE on /posts/1 should return true', done => {
|
||||||
controller.deletePostById(1).subscribe(data => {
|
controller.deletePostById('1').subscribe(data => {
|
||||||
expect(data).toBeTruthy();
|
expect(data).toBeTruthy();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('DELETE on /posts/1001 should return false', done => {
|
describe('Post Controller(useValue fake object)', () => {
|
||||||
controller.deletePostById(1001).subscribe(data => {
|
let controller: PostController;
|
||||||
expect(data).toBeFalsy();
|
|
||||||
done();
|
beforeEach(async () => {
|
||||||
});
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useValue: {
|
||||||
|
findAll: () =>
|
||||||
|
of<any[]>([
|
||||||
|
{ _id: 'testid', title: 'title', content: 'test content' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [PostController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PostController>(PostController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts', async () => {
|
||||||
|
const result = await controller.getAllPosts().toPromise();
|
||||||
|
expect(result[0]._id).toEqual('testid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('Post Controller(useValue: fake object)', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useValue: {
|
||||||
|
findAll: (_keyword?: string, _skip?: number, _limit?: number) =>
|
||||||
|
of<any[]>([
|
||||||
|
{ _id: 'testid', title: 'test title', content: 'test content' },
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [PostController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PostController>(PostController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts(useValue: fake object)', async () => {
|
||||||
|
const result = await controller.getAllPosts().toPromise();
|
||||||
|
expect(result[0]._id).toEqual('testid');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Post Controller(using ts-mockito)', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
const mockedPostService: PostService = mock(PostService);
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
controller = new PostController(instance(mockedPostService));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts(ts-mockito)', async () => {
|
||||||
|
when(
|
||||||
|
mockedPostService.findAll(anyString(), anyNumber(), anyNumber()),
|
||||||
|
).thenReturn(
|
||||||
|
of([
|
||||||
|
{ _id: 'testid', title: 'test title', content: 'content' },
|
||||||
|
]) as Observable<Post[]>,
|
||||||
|
);
|
||||||
|
const result = await controller.getAllPosts('', 10, 0).toPromise();
|
||||||
|
expect(result.length).toEqual(1);
|
||||||
|
expect(result[0].title).toBe('test title');
|
||||||
|
verify(
|
||||||
|
mockedPostService.findAll(anyString(), anyNumber(), anyNumber()),
|
||||||
|
).once();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Post Controller(useValue: jest mocked object)', () => {
|
||||||
|
let controller: PostController;
|
||||||
|
let postService: PostService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: PostService,
|
||||||
|
useValue: {
|
||||||
|
findAll: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation(
|
||||||
|
(_keyword?: string, _skip?: number, _limit?: number) =>
|
||||||
|
of<any[]>([
|
||||||
|
{
|
||||||
|
_id: 'testid',
|
||||||
|
title: 'test title',
|
||||||
|
content: 'test content',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
controllers: [PostController],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<PostController>(PostController);
|
||||||
|
postService = module.get<PostService>(PostService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get all posts(useValue: jest mocking)', async () => {
|
||||||
|
const result = await controller.getAllPosts('test', 10, 0).toPromise();
|
||||||
|
expect(result[0]._id).toEqual('testid');
|
||||||
|
expect(postService.findAll).toBeCalled();
|
||||||
|
expect(postService.findAll).lastCalledWith('test', 0, 10);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,35 +1,54 @@
|
|||||||
import { Controller, Query, Get, Param, Post, Body, HttpStatus, Put, Delete, ParseIntPipe } from '@nestjs/common';
|
import {
|
||||||
|
Controller,
|
||||||
|
Query,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
ParseIntPipe,
|
||||||
|
DefaultValuePipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { take, toArray } from 'rxjs/operators';
|
import { Post as BlogPost } from './post.model';
|
||||||
import { Post as BlogPost } from './post.interface';
|
import { UpdatePostDto } from './update-post.dto';
|
||||||
|
import { CreatePostDto } from './create-post.dto';
|
||||||
|
|
||||||
@Controller('posts')
|
@Controller('posts')
|
||||||
export class PostController {
|
export class PostController {
|
||||||
constructor(private postService: PostService) {}
|
constructor(private postService: PostService) {}
|
||||||
|
|
||||||
@Get('')
|
@Get('')
|
||||||
getAllPosts(@Query('q') keyword?: string): Observable<BlogPost[]> {
|
getAllPosts(
|
||||||
return this.postService.findAll(keyword).pipe(take(10), toArray());
|
@Query('q') keyword?: string,
|
||||||
|
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit?: number,
|
||||||
|
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip?: number,
|
||||||
|
): Observable<BlogPost[]> {
|
||||||
|
return this.postService.findAll(keyword, skip, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(':id')
|
||||||
getPostById(@Param('id', ParseIntPipe) id: number): Observable<BlogPost> {
|
getPostById(@Param('id') id: string): Observable<BlogPost> {
|
||||||
return this.postService.findById(id);
|
return this.postService.findById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('')
|
@Post('')
|
||||||
createPost(@Body() post: BlogPost):Observable<BlogPost[]> {
|
createPost(@Body() post: CreatePostDto): Observable<BlogPost> {
|
||||||
return this.postService.save(post).pipe(toArray());
|
return this.postService.save(post);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id')
|
@Put(':id')
|
||||||
updatePost(@Param('id', ParseIntPipe) id: number, @Body() post: BlogPost): Observable<BlogPost[]> {
|
updatePost(
|
||||||
return this.postService.update(id, post).pipe(toArray());
|
@Param('id') id: string,
|
||||||
|
@Body() post: UpdatePostDto,
|
||||||
|
): Observable<BlogPost> {
|
||||||
|
return this.postService.update(id, post);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
deletePostById(@Param('id', ParseIntPipe) id: number): Observable<boolean> {
|
deletePostById(@Param('id') id: string): Observable<BlogPost> {
|
||||||
return this.postService.deleteById(id);
|
return this.postService.deleteById(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
export interface Post {
|
|
||||||
id?: number;
|
|
||||||
title:string;
|
|
||||||
content: string;
|
|
||||||
createdAt?: Date,
|
|
||||||
updatedAt?: Date
|
|
||||||
}
|
|
||||||
18
src/post/post.model.ts
Normal file
18
src/post/post.model.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { SchemaFactory, Schema, Prop } from '@nestjs/mongoose';
|
||||||
|
import { Document } from 'mongoose';
|
||||||
|
|
||||||
|
@Schema()
|
||||||
|
export class Post extends Document {
|
||||||
|
@Prop({ required: true })
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Prop({ required: true })
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
createdAt?: Date;
|
||||||
|
|
||||||
|
@Prop()
|
||||||
|
updatedAt?: Date;
|
||||||
|
}
|
||||||
|
export const PostSchema = SchemaFactory.createForClass(Post);
|
||||||
@ -1,9 +1,13 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { PostController } from './post.controller';
|
import { PostController } from './post.controller';
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
|
import { MongooseModule } from '@nestjs/mongoose';
|
||||||
|
import { PostSchema } from './post.model';
|
||||||
|
import { DataInitializerService } from './data-initializer.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports: [MongooseModule.forFeature([{ name: 'posts', schema: PostSchema }])],
|
||||||
controllers: [PostController],
|
controllers: [PostController],
|
||||||
providers: [PostService]
|
providers: [PostService, DataInitializerService],
|
||||||
})
|
})
|
||||||
export class PostModule {}
|
export class PostModule {}
|
||||||
|
|||||||
@ -1,49 +1,87 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { PostService } from './post.service';
|
import { PostService } from './post.service';
|
||||||
import { toArray, take, throttleTime } from 'rxjs/operators';
|
import { getModelToken } from '@nestjs/mongoose';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
import { Post } from './post.model';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
|
||||||
describe('PostService', () => {
|
describe('PostService', () => {
|
||||||
let service: PostService;
|
let service: PostService;
|
||||||
|
let model: Model<Post>;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
providers: [PostService],
|
providers: [
|
||||||
|
PostService,
|
||||||
|
{
|
||||||
|
provide: getModelToken('posts'),
|
||||||
|
useValue: {
|
||||||
|
new: jest.fn(),
|
||||||
|
constructor: jest.fn(),
|
||||||
|
find: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
create: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
exec: jest.fn(),
|
||||||
|
findOneAndUpdate: jest.fn(),
|
||||||
|
findOneAndDelete: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
service = module.get<PostService>(PostService);
|
service = module.get<PostService>(PostService);
|
||||||
|
model = module.get<Model<Post>>(getModelToken('posts'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getAllPosts should return 3 posts', done => {
|
it('getAllPosts should return 3 posts', async () => {
|
||||||
service
|
const posts = [
|
||||||
.findAll()
|
{
|
||||||
.pipe(take(3), toArray())
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
.subscribe({
|
title: 'Generate a NestJS project',
|
||||||
next: data => expect(data.length).toBe(3),
|
content: 'content',
|
||||||
error: error => console.log(error),
|
},
|
||||||
complete: done(),
|
{
|
||||||
});
|
_id: '5ee49c3115a4e75254bb732f',
|
||||||
});
|
title: 'Create CRUD RESTful APIs',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
_id: '5ee49c3115a4e75254bb7330',
|
||||||
|
title: 'Connect to MongoDB',
|
||||||
|
content: 'content',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
jest.spyOn(model, 'find').mockReturnValue({
|
||||||
|
skip: jest.fn().mockReturnValue({
|
||||||
|
limit: jest.fn().mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce(posts) as any,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
} as any);
|
||||||
|
|
||||||
it('getAllPosts with keyword should return 1 post', done => {
|
const data = await service.findAll().toPromise();
|
||||||
service
|
expect(data.length).toBe(3);
|
||||||
.findAll('Generate')
|
|
||||||
.pipe(take(3), toArray())
|
|
||||||
.subscribe({
|
|
||||||
next: data => expect(data.length).toBe(1),
|
|
||||||
error: error => console.log(error),
|
|
||||||
complete: done(),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getPostById with existing id should return 1 post', done => {
|
it('getPostById with existing id should return 1 post', done => {
|
||||||
service.findById(1).subscribe({
|
const found = {
|
||||||
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
|
title: 'Generate a NestJS project',
|
||||||
|
content: 'content',
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(model, 'findOne').mockReturnValue({
|
||||||
|
exec: jest.fn().mockResolvedValueOnce(found) as any,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
service.findById('1').subscribe({
|
||||||
next: data => {
|
next: data => {
|
||||||
expect(data.id).toBe(1);
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
expect(data.title).toEqual('Generate a NestJS project');
|
expect(data.title).toEqual('Generate a NestJS project');
|
||||||
},
|
},
|
||||||
error: error => console.log(error),
|
error: error => console.log(error),
|
||||||
@ -51,125 +89,56 @@ describe('PostService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getPostById with none existing id should return empty', done => {
|
it('should save post', async () => {
|
||||||
service
|
const toCreated = {
|
||||||
.findById(10001)
|
title: 'test title',
|
||||||
.pipe(toArray())
|
content: 'test content',
|
||||||
.subscribe({
|
};
|
||||||
next: data => expect(data.length).toBe(0),
|
|
||||||
error: error => console.log(error),
|
const toReturned = {
|
||||||
complete: done(),
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
});
|
...toCreated,
|
||||||
|
};
|
||||||
|
|
||||||
|
jest.spyOn(model, 'create').mockResolvedValue(toReturned as Post);
|
||||||
|
|
||||||
|
const data = await service.save(toCreated).toPromise();
|
||||||
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
|
expect(model.create).toBeCalledWith(toCreated);
|
||||||
|
expect(model.create).toBeCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('save should increase the length of posts', done => {
|
it('should update post', done => {
|
||||||
service
|
const toUpdated = {
|
||||||
.save({
|
_id: '5ee49c3115a4e75254bb732e',
|
||||||
id: 4,
|
title: 'test title',
|
||||||
title: 'test title',
|
content: 'test content',
|
||||||
content: 'test content',
|
};
|
||||||
})
|
|
||||||
.pipe(take(4), toArray())
|
jest.spyOn(model, 'findOneAndUpdate').mockReturnValue({
|
||||||
.subscribe({
|
exec: jest.fn().mockResolvedValueOnce(toUpdated) as any,
|
||||||
next: data => {
|
} as any);
|
||||||
expect(data.length).toBe(4);
|
|
||||||
expect(data[3].createdAt).toBeTruthy();
|
service.update('5ee49c3115a4e75254bb732e', toUpdated).subscribe({
|
||||||
},
|
next: data => {
|
||||||
error: error => console.log(error),
|
expect(data._id).toBe('5ee49c3115a4e75254bb732e');
|
||||||
complete: done(),
|
},
|
||||||
});
|
error: error => console.log(error),
|
||||||
|
complete: done(),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('update should change the content of post', done => {
|
it('should delete post', done => {
|
||||||
service
|
jest.spyOn(model, 'findOneAndDelete').mockReturnValue({
|
||||||
.update(1, {
|
exec: jest.fn().mockResolvedValueOnce({
|
||||||
id: 1,
|
deletedCount: 1,
|
||||||
title: 'test title',
|
}),
|
||||||
content: 'test content',
|
} as any);
|
||||||
createdAt: new Date(),
|
|
||||||
})
|
|
||||||
.pipe(take(4), toArray())
|
|
||||||
.subscribe({
|
|
||||||
next: data => {
|
|
||||||
expect(data.length).toBe(3);
|
|
||||||
expect(data[0].title).toBe('test title');
|
|
||||||
expect(data[0].content).toBe('test content');
|
|
||||||
expect(data[0].updatedAt).not.toBeNull();
|
|
||||||
},
|
|
||||||
error: error => console.log(error),
|
|
||||||
complete: done(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('deleteById with existing id should return true', done => {
|
service.deleteById('anystring').subscribe({
|
||||||
service.deleteById(1).subscribe({
|
|
||||||
next: data => expect(data).toBeTruthy,
|
next: data => expect(data).toBeTruthy,
|
||||||
error: error => console.log(error),
|
error: error => console.log(error),
|
||||||
complete: done(),
|
complete: done(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('deleteById with none existing id should return false', done => {
|
|
||||||
service.deleteById(10001).subscribe({
|
|
||||||
next: data => expect(data).toBeFalsy,
|
|
||||||
error: error => console.log(error),
|
|
||||||
complete: done(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// see: https://stackoverflow.com/questions/62208107/how-test-a-rxjs-empty-operater-using-jest
|
|
||||||
// https://rxjs-dev.firebaseapp.com/guide/testing/marble-testing
|
|
||||||
describe('PostService(test for empty())', () => {
|
|
||||||
let service: PostService;
|
|
||||||
let testScheduler: TestScheduler;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
providers: [PostService],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
service = module.get<PostService>(PostService);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
testScheduler = new TestScheduler((actual, expected) => {
|
|
||||||
// asserting the two objects are equal
|
|
||||||
// e.g. using chai.
|
|
||||||
expect(actual).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// This test will actually run *synchronously*
|
|
||||||
it('test complete for empty()', () => {
|
|
||||||
testScheduler.run(helpers => {
|
|
||||||
const { cold, expectObservable, expectSubscriptions } = helpers;
|
|
||||||
// const e1 = cold('-a--b--c---|');
|
|
||||||
// const subs = '^----------!';
|
|
||||||
// const expected = '-a-----c---|';
|
|
||||||
|
|
||||||
// expectObservable(e1.pipe(throttleTime(3, testScheduler))).toBe(expected);
|
|
||||||
// expectSubscriptions(e1.subscriptions).toBe(subs);
|
|
||||||
expectObservable(service.findById(10001)).toBe('|');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('test complete for empty():getPostById with none existing id', done => {
|
|
||||||
let called = false;
|
|
||||||
service.findById(10001).subscribe({
|
|
||||||
next: data => {
|
|
||||||
console.log(data);
|
|
||||||
called = true;
|
|
||||||
},
|
|
||||||
error: error => {
|
|
||||||
console.log(error);
|
|
||||||
called = true;
|
|
||||||
},
|
|
||||||
complete: () => {
|
|
||||||
console.log("calling complete");
|
|
||||||
expect(called).toBeFalsy();
|
|
||||||
done();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,74 +1,53 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Post } from './post.interface';
|
import { Post } from './post.model';
|
||||||
import { of, from, Observable, empty, EMPTY } from 'rxjs';
|
import { from, Observable } from 'rxjs';
|
||||||
|
import { InjectModel } from '@nestjs/mongoose';
|
||||||
|
import { Model } from 'mongoose';
|
||||||
|
import { CreatePostDto } from './create-post.dto';
|
||||||
|
import { UpdatePostDto } from './update-post.dto';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PostService {
|
export class PostService {
|
||||||
private posts: Post[] = [
|
constructor(@InjectModel('posts') private postModel: Model<Post>) {}
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
title: 'Generate a NestJS project',
|
|
||||||
content: 'content',
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 2,
|
|
||||||
title: 'Create CRUD RESTful APIs',
|
|
||||||
content: 'content',
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 3,
|
|
||||||
title: 'Connect to MongoDB',
|
|
||||||
content: 'content',
|
|
||||||
createdAt: new Date(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
findAll(keyword?: string): Observable<Post> {
|
findAll(keyword?: string, skip = 0, limit = 10): Observable<Post[]> {
|
||||||
if (keyword) {
|
if (keyword) {
|
||||||
return from(this.posts.filter(post => post.title.indexOf(keyword) >= 0));
|
return from(
|
||||||
|
this.postModel
|
||||||
|
.find({ title: { $regex: '.*' + keyword + '.*' } })
|
||||||
|
.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.exec(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return from(
|
||||||
|
this.postModel
|
||||||
|
.find({})
|
||||||
|
.skip(skip)
|
||||||
|
.limit(limit)
|
||||||
|
.exec(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return from(this.posts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
findById(id: number): Observable<Post> {
|
findById(id: string): Observable<Post> {
|
||||||
const found = this.posts.find(post => post.id === id);
|
return from(this.postModel.findOne({ _id: id }).exec());
|
||||||
if (found) {
|
|
||||||
return of(found);
|
|
||||||
}
|
|
||||||
return EMPTY;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save(data: Post): Observable<Post> {
|
save(data: CreatePostDto): Observable<Post> {
|
||||||
const post = { ...data, id: this.posts.length + 1, createdAt: new Date() };
|
const createPost = this.postModel.create({ ...data });
|
||||||
this.posts = [...this.posts, post];
|
return from(createPost);
|
||||||
return from(this.posts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
update(id: number, data: Post): Observable<Post> {
|
update(id: string, data: UpdatePostDto): Observable<Post> {
|
||||||
this.posts = this.posts.map(post => {
|
return from(this.postModel.findOneAndUpdate({ _id: id }, data).exec());
|
||||||
if (id === post.id) {
|
|
||||||
post.title = data.title;
|
|
||||||
post.content = data.content;
|
|
||||||
post.updatedAt = new Date();
|
|
||||||
}
|
|
||||||
return post;
|
|
||||||
});
|
|
||||||
return from(this.posts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteById(id: number): Observable<boolean> {
|
deleteById(id: string): Observable<Post> {
|
||||||
const idx: number = this.posts.findIndex(post => post.id === id);
|
return from(this.postModel.findOneAndDelete({ _id: id }).exec());
|
||||||
if (idx >= 0) {
|
}
|
||||||
// this.posts.splice(idx, 1);
|
|
||||||
this.posts = [
|
deleteAll(): Observable<any> {
|
||||||
...this.posts.slice(0, idx),
|
return from(this.postModel.deleteMany({}).exec());
|
||||||
...this.posts.slice(idx + 1),
|
|
||||||
];
|
|
||||||
return of(true);
|
|
||||||
}
|
|
||||||
return of(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/post/update-post.dto.ts
Normal file
4
src/post/update-post.dto.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export class UpdatePostDto {
|
||||||
|
readonly title: string;
|
||||||
|
readonly content: string;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user