diff --git a/src/app.module.ts b/src/app.module.ts index 58440a8..3459125 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -20,6 +20,7 @@ import { LoggerModule } from 'nestjs-pino'; import { EtcdModule } from 'nestjs-etcd'; import pinoPretty from 'pino-pretty'; import { fromPairs, map, pipe, toPairs } from 'ramda'; +import { ConfigurationsModule } from './configurations/configurations.module'; @Module({ imports: [ @@ -106,6 +107,7 @@ import { fromPairs, map, pipe, toPairs } from 'ramda'; }), WebhooksModule, CommonsModule, + ConfigurationsModule, ], controllers: [AppController], providers: [AppService, AppResolver], diff --git a/src/commons/commons.module.ts b/src/commons/commons.module.ts index 6be56a4..7344f59 100644 --- a/src/commons/commons.module.ts +++ b/src/commons/commons.module.ts @@ -1,8 +1,9 @@ -import { Module } from '@nestjs/common'; +import { Global, Module } from '@nestjs/common'; import { PasswordConverter } from './services/password-converter'; import { RedisMutexModule } from './redis-mutex/redis-mutex.module'; import { AuthModule } from '@nestjs-lib/auth'; +@Global() @Module({ imports: [RedisMutexModule, AuthModule], providers: [PasswordConverter], diff --git a/src/configurations/configurations.module.ts b/src/configurations/configurations.module.ts new file mode 100644 index 0000000..fb9a001 --- /dev/null +++ b/src/configurations/configurations.module.ts @@ -0,0 +1,11 @@ +import { Configuration } from './entities/configuration.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Module } from '@nestjs/common'; +import { ConfigurationsService } from './configurations.service'; +import { ConfigurationsResolver } from './configurations.resolver'; + +@Module({ + imports: [TypeOrmModule.forFeature([Configuration])], + providers: [ConfigurationsResolver, ConfigurationsService], +}) +export class ConfigurationsModule {} diff --git a/src/configurations/configurations.resolver.spec.ts b/src/configurations/configurations.resolver.spec.ts new file mode 100644 index 0000000..5be2c48 --- /dev/null +++ b/src/configurations/configurations.resolver.spec.ts @@ -0,0 +1,24 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigurationsResolver } from './configurations.resolver'; +import { ConfigurationsService } from './configurations.service'; + +describe('ConfigurationsResolver', () => { + let resolver: ConfigurationsResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: ConfigurationsService, + useValue: {}, + }, + ], + }).compile(); + + resolver = module.get(ConfigurationsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/src/configurations/configurations.resolver.ts b/src/configurations/configurations.resolver.ts new file mode 100644 index 0000000..01c3d22 --- /dev/null +++ b/src/configurations/configurations.resolver.ts @@ -0,0 +1,39 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { GetConfigurationArgs } from './dto/get-configuration.args'; +import { SetConfigurationInput } from './dto/set-configuration.input'; +import { Resolver, Mutation, Args, Query } from '@nestjs/graphql'; +import { ConfigurationsService } from './configurations.service'; +import { Configuration } from './entities/configuration.entity'; +import { any, pipe, values } from 'ramda'; +import { AccountRole, Roles } from '@nestjs-lib/auth'; + +@Roles(AccountRole.admin, AccountRole.super) +@Resolver(() => Configuration) +export class ConfigurationsResolver { + constructor(private readonly configurationsService: ConfigurationsService) {} + + @Mutation(() => Configuration) + setConfiguration( + @Args('setConfigurationInput', { type: () => SetConfigurationInput }) + setConfigurationInput: SetConfigurationInput, + ) { + return this.configurationsService.setConfiguration(setConfigurationInput); + } + + @Query(() => Configuration, { nullable: true }) + getConfiguration( + @Args() + getConfigurationArgs: GetConfigurationArgs, + ) { + if ( + pipe( + values, + any((value) => !value), + )(getConfigurationArgs) + ) { + throw new UnprocessableEntityException('Must pass a parameter'); + } + + return this.configurationsService.findOneByConditions(getConfigurationArgs); + } +} diff --git a/src/configurations/configurations.service.spec.ts b/src/configurations/configurations.service.spec.ts new file mode 100644 index 0000000..61ded7f --- /dev/null +++ b/src/configurations/configurations.service.spec.ts @@ -0,0 +1,44 @@ +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigurationsService } from './configurations.service'; +import { Configuration } from './entities/configuration.entity'; +import { IsNull } from 'typeorm'; + +describe('ConfigurationsService', () => { + let service: ConfigurationsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ConfigurationsService, + { + provide: getRepositoryToken(Configuration), + useValue: {}, + }, + ], + }).compile(); + + service = module.get(ConfigurationsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOneByConditions', () => { + it('should select by projectId only', async () => { + const entity = new Configuration(); + const findOne = jest.fn((_) => Promise.resolve(entity)); + service['repository'].findOne = findOne; + + await expect( + service.findOneByConditions({ projectId: 'uuid' }), + ).resolves.toEqual(entity); + + expect(findOne.mock.calls[0][0]).toMatchObject({ + projectId: 'uuid', + pipelineId: IsNull(), + }); + }); + }); +}); diff --git a/src/configurations/configurations.service.ts b/src/configurations/configurations.service.ts new file mode 100644 index 0000000..e2bd90f --- /dev/null +++ b/src/configurations/configurations.service.ts @@ -0,0 +1,39 @@ +import { GetConfigurationArgs } from './dto/get-configuration.args'; +import { BaseDbService } from './../commons/services/base-db.service'; +import { Injectable } from '@nestjs/common'; +import { Configuration } from './entities/configuration.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { FindConditions, IsNull, Repository } from 'typeorm'; +import { SetConfigurationInput } from './dto/set-configuration.input'; +import { pick } from 'ramda'; +import { Etcd3 } from 'etcd3'; + +@Injectable() +export class ConfigurationsService extends BaseDbService { + constructor( + @InjectRepository(Configuration) + configurationRepository: Repository, + private readonly etcd: Etcd3, + ) { + super(configurationRepository); + } + + async setConfiguration(dto: SetConfigurationInput) { + let entity = await this.repository.findOne( + pick(['pipelineId', 'projectId'], dto), + ); + if (!entity) { + entity = this.repository.create(dto); + } + entity = await this.repository.save(entity); + this.etcd.put(`share/config/${entity.id}`).value(entity.content); + return entity; + } + + async findOneByConditions(dto: FindConditions) { + if (dto.projectId && !dto.pipelineId) { + dto.pipelineId = IsNull(); + } + return await this.repository.findOne(dto); + } +} diff --git a/src/configurations/dto/get-configuration.args.ts b/src/configurations/dto/get-configuration.args.ts new file mode 100644 index 0000000..df148c0 --- /dev/null +++ b/src/configurations/dto/get-configuration.args.ts @@ -0,0 +1,17 @@ +import { ArgsType } from '@nestjs/graphql'; +import { IsUUID, IsOptional } from 'class-validator'; + +@ArgsType() +export class GetConfigurationArgs { + @IsUUID() + @IsOptional() + pipelineId?: string; + + @IsUUID() + @IsOptional() + projectId?: string; + + @IsUUID() + @IsOptional() + id?: string; +} diff --git a/src/configurations/dto/set-configuration.input.ts b/src/configurations/dto/set-configuration.input.ts new file mode 100644 index 0000000..e0c6161 --- /dev/null +++ b/src/configurations/dto/set-configuration.input.ts @@ -0,0 +1,26 @@ +import { ConfigurationLanguage } from './../enums/configuration-language.enum'; +import { IsEnum, IsString, IsUUID, Length, IsOptional } from 'class-validator'; +import { InputType } from '@nestjs/graphql'; + +@InputType() +export class SetConfigurationInput { + @IsOptional() + @IsUUID() + id?: string; + + @IsUUID() + pipelineId: string; + + @IsUUID() + projectId: string; + + @IsString() + content: string; + + @IsEnum(ConfigurationLanguage) + language: ConfigurationLanguage; + + @Length(0, 100) + @IsOptional() + name = 'Default Configuration'; +} diff --git a/src/configurations/entities/configuration.entity.ts b/src/configurations/entities/configuration.entity.ts new file mode 100644 index 0000000..226a9e4 --- /dev/null +++ b/src/configurations/entities/configuration.entity.ts @@ -0,0 +1,35 @@ +import { Project } from './../../projects/project.entity'; +import { ConfigurationLanguage } from './../enums/configuration-language.enum'; +import { Pipeline } from './../../pipelines/pipeline.entity'; +import { AppBaseEntity } from './../../commons/entities/app-base-entity'; +import { ObjectType } from '@nestjs/graphql'; +import { Column, Entity, ManyToOne } from 'typeorm'; + +@Entity() +@ObjectType() +export class Configuration extends AppBaseEntity { + @ManyToOne(() => Pipeline) + pipeline: Pipeline; + + @Column({ unique: true, nullable: true }) + pipelineId: string; + + @ManyToOne(() => Project) + project: Project; + + @Column() + projectId: string; + + @Column({ comment: 'language defined in type field.' }) + content: string; + + @Column({ comment: '配置名称' }) + name: string; + + @Column({ + type: 'enum', + enum: ConfigurationLanguage, + comment: 'configuration content language', + }) + language: ConfigurationLanguage; +} diff --git a/src/configurations/enums/configuration-language.enum.ts b/src/configurations/enums/configuration-language.enum.ts new file mode 100644 index 0000000..724e9ec --- /dev/null +++ b/src/configurations/enums/configuration-language.enum.ts @@ -0,0 +1,10 @@ +import { registerEnumType } from '@nestjs/graphql'; + +export enum ConfigurationLanguage { + JavaScript = 'JavaScript', + YAML = 'YAML', +} + +registerEnumType(ConfigurationLanguage, { + name: 'ConfigurationLanguage', +}); diff --git a/src/pipeline-tasks/pipeline-tasks.module.ts b/src/pipeline-tasks/pipeline-tasks.module.ts index 2579240..1b84cde 100644 --- a/src/pipeline-tasks/pipeline-tasks.module.ts +++ b/src/pipeline-tasks/pipeline-tasks.module.ts @@ -16,12 +16,10 @@ import { } from './pipeline-tasks.constants'; import { PipelineTaskLogger } from './pipeline-task.logger'; import { PipelineTaskFlushService } from './pipeline-task-flush.service'; -import { CommonsModule } from '../commons/commons.module'; import { DeployByPm2Service } from './runners/deploy-by-pm2/deploy-by-pm2.service'; @Module({ imports: [ - CommonsModule, TypeOrmModule.forFeature([PipelineTask, Pipeline]), RedisModule, ReposModule, diff --git a/src/pipelines/pipelines.module.ts b/src/pipelines/pipelines.module.ts index d998ad2..44b29bd 100644 --- a/src/pipelines/pipelines.module.ts +++ b/src/pipelines/pipelines.module.ts @@ -5,14 +5,11 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Pipeline } from './pipeline.entity'; import { CommitLogsResolver } from './commit-logs.resolver'; import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module'; -import { ReposModule } from '../repos/repos.module'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { ConfigModule, ConfigService } from '@nestjs/config'; -import { CommonsModule } from '../commons/commons.module'; @Module({ imports: [ - CommonsModule, TypeOrmModule.forFeature([Pipeline]), PipelineTasksModule, RabbitMQModule.forRootAsync(RabbitMQModule, { diff --git a/src/projects/projects.module.ts b/src/projects/projects.module.ts index 42aa366..f5441a8 100644 --- a/src/projects/projects.module.ts +++ b/src/projects/projects.module.ts @@ -6,11 +6,9 @@ import { Project } from './project.entity'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { EXCHANGE_PROJECT_FANOUT } from './projects.constants'; -import { CommonsModule } from '../commons/commons.module'; @Module({ imports: [ - CommonsModule, TypeOrmModule.forFeature([Project]), RabbitMQModule.forRootAsync(RabbitMQModule, { imports: [ConfigModule], diff --git a/src/repos/repos.module.ts b/src/repos/repos.module.ts index 08edf4a..7c5802d 100644 --- a/src/repos/repos.module.ts +++ b/src/repos/repos.module.ts @@ -7,14 +7,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config'; import { ProjectsModule } from '../projects/projects.module'; import { EXCHANGE_REPO } from './repos.constants'; import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'; -import { CommonsModule } from '../commons/commons.module'; @Module({ imports: [ TypeOrmModule.forFeature([Project]), ConfigModule, ProjectsModule, - CommonsModule, RabbitMQModule.forRootAsync(RabbitMQModule, { imports: [ConfigModule], useFactory: (configService: ConfigService) => ({