feat: Backend typing and openapi schema & codegen

This commit is contained in:
Aleksi Lassila
2024-03-26 23:01:11 +02:00
parent b29907c0e2
commit 7318a0fa99
22 changed files with 376 additions and 179 deletions

View File

@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserController } from './user.controller';
describe('UserController', () => {
let controller: UserController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UserController],
}).compile();
controller = module.get<UserController>(UserController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -8,55 +8,65 @@ import {
Param,
Post,
UseGuards,
Request,
} from '@nestjs/common';
import { UserService } from './user.service';
import { AuthGuard } from '../auth/auth.guard';
import { AuthUser } from 'src/auth/auth.service';
import { AuthGuard, GetUser } from '../auth/auth.guard';
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { CreateUserDto, UserDto } from './user.dto';
import { User } from './user.entity';
@ApiTags('user')
@Controller('user')
export class UserController {
constructor(private userService: UserService) {}
@UseGuards(AuthGuard)
@Get()
async getProfile(@Request() req) {
const user = await this.userService.findOne((req.user as AuthUser).id);
@ApiNotFoundResponse({ description: 'User not found' })
@ApiOkResponse({ description: 'User found', type: UserDto })
async getProfile(@GetUser() user: User): Promise<UserDto> {
if (!user) {
throw new NotFoundException();
}
return user;
return UserDto.fromEntity(user);
}
@UseGuards(AuthGuard)
@Get(':id')
async findById(@Param('id') id: string) {
@ApiOkResponse({ description: 'User found', type: UserDto })
@ApiNotFoundResponse({ description: 'User not found' })
async findById(
@Param('id') id: string,
@GetUser() callerUser: User,
): Promise<UserDto> {
if (!callerUser.isAdmin && callerUser.id !== id) {
throw new NotFoundException();
}
const user = await this.userService.findOne(id);
if (!user) {
throw new NotFoundException();
}
return user;
return UserDto.fromEntity(user);
}
@HttpCode(HttpStatus.OK)
@Post()
async create(
@Body()
userCreateDto: {
name: string;
password: string;
isAdmin?: boolean;
},
userCreateDto: CreateUserDto,
) {
const canCreateAdmin = await this.userService.noPreviousAdmins();
return this.userService.create(
const user = await this.userService.create(
userCreateDto.name,
userCreateDto.password,
canCreateAdmin && userCreateDto.isAdmin,
);
return UserDto.fromEntity(user);
}
}

View File

@@ -0,0 +1,21 @@
import { OmitType, PickType } from '@nestjs/swagger';
import { User } from './user.entity';
export class UserDto extends OmitType(User, ['password'] as const) {
static fromEntity(entity: User): UserDto {
return {
id: entity.id,
name: entity.name,
isAdmin: entity.isAdmin,
settings: entity.settings,
};
}
}
export class CreateUserDto extends PickType(User, [
'name',
'password',
'isAdmin',
] as const) {}
export class UpdateUserDto extends OmitType(User, ['id'] as const) {}

View File

@@ -1,6 +1,60 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { ApiProperty } from '@nestjs/swagger';
const DEFAULT_SETTINGS = {
export class SonarrSettings {
@ApiProperty({ required: true })
apiKey: string;
@ApiProperty({ required: true })
baseUrl: string;
@ApiProperty({ required: true })
qualityProfileId: number;
@ApiProperty({ required: true })
rootFolderPath: string;
@ApiProperty({ required: true })
languageProfileId: number;
}
export class RadarrSettings {
@ApiProperty({ required: true })
apiKey: string;
@ApiProperty({ required: true })
baseUrl: string;
@ApiProperty({ required: true })
qualityProfileId: number;
@ApiProperty({ required: true })
rootFolderPath: string;
}
export class JellyfinSettings {
@ApiProperty({ required: true })
apiKey: string;
@ApiProperty({ required: true })
baseUrl: string;
@ApiProperty({ required: true })
userId: string;
}
export class Settings {
@ApiProperty({ required: true })
autoplayTrailers: boolean;
@ApiProperty({ required: true })
language: string;
@ApiProperty({ required: true })
animationDuration: number;
// discover: {
// region: string,
// excludeLibraryItems: true,
// includedLanguages: 'en'
// },
@ApiProperty({ required: true, type: SonarrSettings })
sonarr: SonarrSettings;
@ApiProperty({ required: true, type: RadarrSettings })
radarr: RadarrSettings;
@ApiProperty({ required: true, type: JellyfinSettings })
jellyfin: JellyfinSettings;
}
const DEFAULT_SETTINGS: Settings = {
autoplayTrailers: true,
language: 'en',
animationDuration: 300,
@@ -31,45 +85,23 @@ const DEFAULT_SETTINGS = {
@Entity()
export class User {
@ApiProperty({ required: true })
@PrimaryGeneratedColumn('uuid')
id: string;
@ApiProperty({ required: true })
@Column({ unique: true })
name: string;
@ApiProperty({ required: true })
@Column()
password: string;
@ApiProperty({ required: true })
@Column()
isAdmin: boolean = false;
@ApiProperty({ required: true, type: Settings })
@Column('json', { default: JSON.stringify(DEFAULT_SETTINGS) })
settings: {
autoplayTrailers: boolean;
language: string;
animationDuration: number;
// discover: {
// region: string,
// excludeLibraryItems: true,
// includedLanguages: 'en'
// },
sonarr: {
apiKey: string;
baseUrl: string;
qualityProfileId: number;
rootFolderPath: string;
languageProfileId: number;
};
radarr: {
apiKey: string;
baseUrl: string;
qualityProfileId: number;
rootFolderPath: string;
};
jellyfin: {
apiKey: string;
baseUrl: string;
userId: string;
};
} = DEFAULT_SETTINGS;
settings = DEFAULT_SETTINGS;
}

View File

@@ -1,18 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});