diff --git a/src/app.module.ts b/src/app.module.ts index a374afd..4a87cb7 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -13,6 +13,7 @@ import { PubSubModule } from './commons/pub-sub/pub-sub.module'; import { ArticlesModule } from './articles/articles.module'; import { EtcdModule } from 'nestjs-etcd'; import { CommonsModule } from './commons/commons.module'; +import { TagsModule } from './tags/tags.module'; @Module({ imports: [ @@ -84,6 +85,7 @@ import { CommonsModule } from './commons/commons.module'; }), CommonsModule, ArticlesModule, + TagsModule, ], controllers: [AppController], providers: [AppService, AppResolver], diff --git a/src/tags/dto/create-tag.input.ts b/src/tags/dto/create-tag.input.ts new file mode 100644 index 0000000..00dfd8b --- /dev/null +++ b/src/tags/dto/create-tag.input.ts @@ -0,0 +1,9 @@ +import { InputType } from '@nestjs/graphql'; +import { IsString, Length } from 'class-validator'; + +@InputType() +export class CreateTagInput { + @IsString() + @Length(1, 100) + name: string; +} diff --git a/src/tags/dto/update-tag.input.ts b/src/tags/dto/update-tag.input.ts new file mode 100644 index 0000000..996108c --- /dev/null +++ b/src/tags/dto/update-tag.input.ts @@ -0,0 +1,9 @@ +import { CreateTagInput } from './create-tag.input'; +import { InputType, PartialType } from '@nestjs/graphql'; +import { IsUUID } from 'class-validator'; + +@InputType() +export class UpdateTagInput extends PartialType(CreateTagInput) { + @IsUUID() + id: string; +} diff --git a/src/tags/entities/tag.entity.ts b/src/tags/entities/tag.entity.ts new file mode 100644 index 0000000..679b2d2 --- /dev/null +++ b/src/tags/entities/tag.entity.ts @@ -0,0 +1,11 @@ +import { AppBaseEntity } from './../../commons/entities/app-base-entity'; +import { ObjectType } from '@nestjs/graphql'; +import { Column, Index, Entity } from 'typeorm'; + +@Entity() +@ObjectType() +export class Tag extends AppBaseEntity { + @Index({ unique: true }) + @Column({ length: 100 }) + name: string; +} diff --git a/src/tags/tags.module.ts b/src/tags/tags.module.ts new file mode 100644 index 0000000..56b87c7 --- /dev/null +++ b/src/tags/tags.module.ts @@ -0,0 +1,12 @@ +import { Tag } from './entities/tag.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { CommonsModule } from './../commons/commons.module'; +import { Module } from '@nestjs/common'; +import { TagsService } from './tags.service'; +import { TagsResolver } from './tags.resolver'; + +@Module({ + imports: [CommonsModule, TypeOrmModule.forFeature([Tag])], + providers: [TagsResolver, TagsService], +}) +export class TagsModule {} diff --git a/src/tags/tags.resolver.spec.ts b/src/tags/tags.resolver.spec.ts new file mode 100644 index 0000000..bfa17ef --- /dev/null +++ b/src/tags/tags.resolver.spec.ts @@ -0,0 +1,25 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { TagsResolver } from './tags.resolver'; +import { TagsService } from './tags.service'; + +describe('TagsResolver', () => { + let resolver: TagsResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TagsResolver, + { + provide: TagsService, + useValue: {}, + }, + ], + }).compile(); + + resolver = module.get(TagsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/src/tags/tags.resolver.ts b/src/tags/tags.resolver.ts new file mode 100644 index 0000000..f66a57c --- /dev/null +++ b/src/tags/tags.resolver.ts @@ -0,0 +1,36 @@ +import { Resolver, Query, Mutation, Args } from '@nestjs/graphql'; +import { TagsService } from './tags.service'; +import { Tag } from './entities/tag.entity'; +import { CreateTagInput } from './dto/create-tag.input'; +import { UpdateTagInput } from './dto/update-tag.input'; + +@Resolver(() => Tag) +export class TagsResolver { + constructor(private readonly tagsService: TagsService) {} + + @Mutation(() => Tag) + createTag(@Args('createTagInput') createTagInput: CreateTagInput) { + return this.tagsService.create(createTagInput); + } + + @Query(() => [Tag], { name: 'tags' }) + findAll() { + return this.tagsService.findAll(); + } + + @Query(() => Tag, { name: 'tag' }) + findOne(@Args('id', { type: () => String }) id: string) { + return this.tagsService.findOne(id); + } + + @Mutation(() => Tag) + async updateTag(@Args('updateTagInput') updateTagInput: UpdateTagInput) { + const tag = await this.tagsService.findOne(updateTagInput.id); + return this.tagsService.update(tag, updateTagInput); + } + + @Mutation(() => Tag) + removeTag(@Args('id', { type: () => String }) id: string) { + return this.tagsService.remove(id); + } +} diff --git a/src/tags/tags.service.spec.ts b/src/tags/tags.service.spec.ts new file mode 100644 index 0000000..aac0b98 --- /dev/null +++ b/src/tags/tags.service.spec.ts @@ -0,0 +1,27 @@ +import { Tag } from './entities/tag.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TagsService } from './tags.service'; +import { Repository } from 'typeorm'; + +describe('TagsService', () => { + let service: TagsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TagsService, + { + provide: getRepositoryToken(Tag), + useValue: new Repository(), + }, + ], + }).compile(); + + service = module.get(TagsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/tags/tags.service.ts b/src/tags/tags.service.ts new file mode 100644 index 0000000..f2dd6db --- /dev/null +++ b/src/tags/tags.service.ts @@ -0,0 +1,45 @@ +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { BaseDbService } from './../commons/services/base-db.service'; +import { Injectable } from '@nestjs/common'; +import { CreateTagInput } from './dto/create-tag.input'; +import { UpdateTagInput } from './dto/update-tag.input'; +import { Tag } from './entities/tag.entity'; + +@Injectable() +export class TagsService extends BaseDbService { + readonly uniqueFields: Array = ['name']; + constructor(@InjectRepository(Tag) readonly repository: Repository) { + super(repository); + } + + /** + * create or recover a tag + */ + async create(createTagInput: CreateTagInput): Promise { + const old = await this.repository.findOne({ name: createTagInput.name }); + return this.repository.save( + old + ? this.repository.merge(old, createTagInput) + : this.repository.create(createTagInput), + ); + } + + async findAll() { + return await this.repository.find({ + order: { createdAt: 'DESC' }, + }); + } + + async update(tag: Tag, updateTagInput: UpdateTagInput) { + await this.isDuplicateEntityForUpdate(tag.id, updateTagInput); + return await this.repository.save( + this.repository.merge(tag, updateTagInput), + ); + } + + async remove(id: string) { + await this.canRemove([id]); + return await this.repository.softDelete({ id }).then((d) => d.affected); + } +}