diff --git a/src/app.module.ts b/src/app.module.ts index d5ee03dc..342e79c8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -28,6 +28,7 @@ import { MailModule } from './mail/mail.module'; import { HomeModule } from './home/home.module'; import { DataSource, DataSourceOptions } from 'typeorm'; import { AllConfigType } from './config/config.type'; +import { InfoModule } from './info/info.module'; @Module({ imports: [ @@ -84,6 +85,7 @@ import { AllConfigType } from './config/config.type'; ForgotModule, MailModule, HomeModule, + InfoModule, ], }) export class AppModule {} diff --git a/src/database/migrations/1687403553617-CreateInfo.ts b/src/database/migrations/1687403553617-CreateInfo.ts new file mode 100644 index 00000000..7a059d53 --- /dev/null +++ b/src/database/migrations/1687403553617-CreateInfo.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class CreateInfo1687403553617 implements MigrationInterface { + name = 'CreateInfo1687403553617'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "info" ("id" integer NOT NULL, "name" character varying NOT NULL, "value" character varying NOT NULL, CONSTRAINT "UQ_916df6cf672c24f99ceab946a88" UNIQUE ("name"), CONSTRAINT "PK_687dc5e25f4f1ee093a45b68bb7" PRIMARY KEY ("id"))`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "info"`); + } +} diff --git a/src/database/migrations/1687533339069-ColumnInfoVersion.ts b/src/database/migrations/1687533339069-ColumnInfoVersion.ts new file mode 100644 index 00000000..87828cd8 --- /dev/null +++ b/src/database/migrations/1687533339069-ColumnInfoVersion.ts @@ -0,0 +1,15 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class ColumnInfoVersion1687533339069 implements MigrationInterface { + name = 'ColumnInfoVersion1687533339069'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "info" ADD "version" character varying`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "info" DROP COLUMN "version"`); + } +} diff --git a/src/database/seeds/info/info-data.ts b/src/database/seeds/info/info-data.ts new file mode 100644 index 00000000..a9836dc2 --- /dev/null +++ b/src/database/seeds/info/info-data.ts @@ -0,0 +1,10 @@ +import { InfoData } from 'src/info/info-data.interface'; + +export const infoData: InfoData[] = [ + // settings for any api version + { name: 'maintenance', value: 'false' }, + + // v1 + { name: 'abTestEnabled', value: 'false', version: '1' }, + { name: 'maxUploadSize', value: '10MB', version: '1' }, +]; diff --git a/src/database/seeds/info/info-seed.module.ts b/src/database/seeds/info/info-seed.module.ts new file mode 100644 index 00000000..d819dabe --- /dev/null +++ b/src/database/seeds/info/info-seed.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { InfoSeedService } from './info-seed.service'; +import { Info } from 'src/info/entities/info.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Info])], + providers: [InfoSeedService], + exports: [InfoSeedService], +}) +export class InfoSeedModule {} diff --git a/src/database/seeds/info/info-seed.service.ts b/src/database/seeds/info/info-seed.service.ts new file mode 100644 index 00000000..7d183274 --- /dev/null +++ b/src/database/seeds/info/info-seed.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Info } from 'src/info/entities/info.entity'; +import { Repository } from 'typeorm'; +import { infoData } from './info-data'; + +@Injectable() +export class InfoSeedService { + constructor( + @InjectRepository(Info) + private repository: Repository, + ) {} + + async run() { + let id = 1; + for (const item of infoData) { + const count = await this.repository.count({ + where: { + name: item.name, + version: item.version, + }, + }); + + if (!count) { + await this.repository.save( + this.repository.create({ + id: id, + name: item.name, + value: item.value, + version: item.version, + }), + ); + } + id++; + } + } +} diff --git a/src/database/seeds/run-seed.ts b/src/database/seeds/run-seed.ts index e0860129..638e1a67 100644 --- a/src/database/seeds/run-seed.ts +++ b/src/database/seeds/run-seed.ts @@ -3,6 +3,7 @@ import { RoleSeedService } from './role/role-seed.service'; import { SeedModule } from './seed.module'; import { StatusSeedService } from './status/status-seed.service'; import { UserSeedService } from './user/user-seed.service'; +import { InfoSeedService } from './info/info-seed.service'; const runSeed = async () => { const app = await NestFactory.create(SeedModule); @@ -11,6 +12,7 @@ const runSeed = async () => { await app.get(RoleSeedService).run(); await app.get(StatusSeedService).run(); await app.get(UserSeedService).run(); + await app.get(InfoSeedService).run(); await app.close(); }; diff --git a/src/database/seeds/seed.module.ts b/src/database/seeds/seed.module.ts index cfb415fa..d1c29b26 100644 --- a/src/database/seeds/seed.module.ts +++ b/src/database/seeds/seed.module.ts @@ -8,12 +8,14 @@ import { TypeOrmConfigService } from '../typeorm-config.service'; import { RoleSeedModule } from './role/role-seed.module'; import { StatusSeedModule } from './status/status-seed.module'; import { UserSeedModule } from './user/user-seed.module'; +import { InfoSeedModule } from './info/info-seed.module'; @Module({ imports: [ RoleSeedModule, StatusSeedModule, UserSeedModule, + InfoSeedModule, ConfigModule.forRoot({ isGlobal: true, load: [databaseConfig, appConfig], diff --git a/src/info/entities/info.entity.ts b/src/info/entities/info.entity.ts new file mode 100644 index 00000000..831efb7e --- /dev/null +++ b/src/info/entities/info.entity.ts @@ -0,0 +1,23 @@ +import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm'; +import { ApiProperty } from '@nestjs/swagger'; +import { Exclude } from 'class-transformer'; + +@Entity({ name: 'info' }) +export class Info extends BaseEntity { + @Exclude() + @ApiProperty({ example: 1 }) + @PrimaryColumn() + id: number; + + @ApiProperty({ example: 'version' }) + @Column({ unique: true }) + name: string; + + @ApiProperty({ example: '1' }) + @Column() + value: string; + + @ApiProperty({ example: '1' }) + @Column({ nullable: true }) + version: string; +} diff --git a/src/info/info-data.interface.ts b/src/info/info-data.interface.ts new file mode 100644 index 00000000..d8984283 --- /dev/null +++ b/src/info/info-data.interface.ts @@ -0,0 +1,6 @@ +// info-data.interface.ts +export interface InfoData { + name: string; + value: string; + version?: string; +} diff --git a/src/info/info.controller.spec.ts b/src/info/info.controller.spec.ts new file mode 100644 index 00000000..9e819b93 --- /dev/null +++ b/src/info/info.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { InfoController } from './info.controller'; + +describe('InfoController', () => { + let controller: InfoController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [InfoController], + }).compile(); + + controller = module.get(InfoController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/src/info/info.controller.ts b/src/info/info.controller.ts new file mode 100644 index 00000000..86472295 --- /dev/null +++ b/src/info/info.controller.ts @@ -0,0 +1,24 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiTags, ApiParam } from '@nestjs/swagger'; +import { InfoService } from './info.service'; +import { Info } from './entities/info.entity'; +import { NullableType } from 'src/utils/types/nullable.type'; + +@ApiTags('Info') +@Controller('info') +export class InfoController { + constructor(private readonly infoService: InfoService) {} + + @Get() + async getAll(): Promise> { + return this.infoService.find(); + } + + @Get('v:version') + @ApiParam({ name: 'version', example: '1' }) + getByVersion( + @Param('version') version: string, + ): Promise> { + return this.infoService.findByVersion(version); + } +} diff --git a/src/info/info.module.ts b/src/info/info.module.ts new file mode 100644 index 00000000..112def9c --- /dev/null +++ b/src/info/info.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { InfoController } from './info.controller'; +import { InfoService } from './info.service'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Info } from './entities/info.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Info])], + controllers: [InfoController], + providers: [InfoService], +}) +export class InfoModule {} diff --git a/src/info/info.service.spec.ts b/src/info/info.service.spec.ts new file mode 100644 index 00000000..12e5e085 --- /dev/null +++ b/src/info/info.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { InfoService } from './info.service'; + +describe('InfoService', () => { + let service: InfoService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [InfoService], + }).compile(); + + service = module.get(InfoService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/info/info.service.ts b/src/info/info.service.ts new file mode 100644 index 00000000..7c4c1f44 --- /dev/null +++ b/src/info/info.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from '@nestjs/common'; +import { IsNull, Repository } from 'typeorm'; +import { Info } from './entities/info.entity'; +import { InjectRepository } from '@nestjs/typeorm'; +import { EntityCondition } from 'src/utils/types/entity-condition.type'; +import { NullableType } from 'src/utils/types/nullable.type'; + +@Injectable() +export class InfoService { + constructor( + @InjectRepository(Info) + private readonly infoRepository: Repository, + ) {} + + async find(fields?: EntityCondition): Promise> { + return this.infoRepository.find({ + where: fields, + }); + } + + async findByVersion(version: string): Promise { + // If no version found, return empty to indicate it + const count = await this.infoRepository.count({ + where: { version: version }, + }); + + if (count === 0) { + return []; + } + + // Otherwise, return settings for given version and for any version (null) + return this.infoRepository.find({ + where: [{ version: version }, { version: IsNull() }], + }); + } +}