diff --git a/.env b/.env index 8672cee..4506c35 100644 --- a/.env +++ b/.env @@ -12,4 +12,7 @@ DB_USERNAME=root DB_PASSWORD=zhxiao1124.. DB_DATABASE=onion_tiktok +# CORS settings +CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173 + DB_SYNCHRONIZE = true diff --git a/.env.example b/.env.example index d404830..88345f8 100644 --- a/.env.example +++ b/.env.example @@ -12,4 +12,7 @@ DB_USERNAME=root DB_PASSWORD= DB_DATABASE=cth_tk_backend +# CORS settings +CORS_ORIGIN=http://localhost:5173,http://127.0.0.1:5173 + DB_SYNCHRONIZE = true diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0f88dd5 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,9 @@ +{ + "tabWidth": 4, + "singleQuote": false, + "trailingComma": "all", + "printWidth": 390, + "eslintIntegration": true, + "bracketSpacing": true, + "stylelintIntegration": true +} diff --git a/src/app.module.ts b/src/app.module.ts index 8b79a55..aa978ef 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,31 +1,31 @@ -import { Module } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; -import { AdminModule } from './modules/admin/admin.module'; -import { AuthModule } from './modules/auth/auth.module'; -import { DataModule } from './modules/data/data.module'; -import { DatabaseModule } from './modules/database/database.module'; -import { DramasModule } from './modules/dramas/dramas.module'; -import { HealthModule } from './modules/health/health.module'; -import { LogsModule } from './modules/logs/logs.module'; -import { MediaModule } from './modules/media/media.module'; -import { OrdersModule } from './modules/orders/orders.module'; -import { PlayerModule } from './modules/player/player.module'; +import { Module } from "@nestjs/common"; +import { ConfigModule } from "@nestjs/config"; +import { AdminModule } from "./modules/admin/admin.module"; +import { AuthModule } from "./modules/auth/auth.module"; +import { DataModule } from "./modules/data/data.module"; +import { DatabaseModule } from "./modules/database/database.module"; +import { DramasModule } from "./modules/dramas/dramas.module"; +import { HealthModule } from "./modules/health/health.module"; +import { LogsModule } from "./modules/logs/logs.module"; +import { MediaModule } from "./modules/media/media.module"; +import { OrdersModule } from "./modules/orders/orders.module"; +import { PlayerModule } from "./modules/player/player.module"; @Module({ - imports: [ - ConfigModule.forRoot({ - isGlobal: true, - }), - DatabaseModule, - AdminModule, - HealthModule, - LogsModule, - DramasModule, - DataModule, - MediaModule, - AuthModule, - OrdersModule, - PlayerModule, - ], + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + }), + DatabaseModule, + AdminModule, + HealthModule, + LogsModule, + DramasModule, + DataModule, + MediaModule, + AuthModule, + OrdersModule, + PlayerModule, + ], }) export class AppModule {} diff --git a/src/bootstrap.ts b/src/bootstrap.ts index 5e1e4dd..2429517 100644 --- a/src/bootstrap.ts +++ b/src/bootstrap.ts @@ -8,35 +8,27 @@ import { RequestIdMiddleware } from "./common/middleware/request-id.middleware"; import { LogsService } from "./modules/logs/logs.service"; export function bootstrapApp(app: INestApplication) { - app.enableCors({ - origin: process.env.CORS_ORIGIN?.split(',') ?? [ - 'http://localhost:5173', - 'http://127.0.0.1:5173', - ], - credentials: true, - }); - app.use(RequestIdMiddleware); - app.useGlobalPipes( - new ValidationPipe({ - whitelist: true, - transform: true, - forbidNonWhitelisted: true, - }), - ); + app.enableCors({ + origin: process.env.CORS_ORIGIN?.split(","), + credentials: true, + }); + app.use(RequestIdMiddleware); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: true, + }), + ); - const logsService = app.get(LogsService); - app.useGlobalFilters(new HttpExceptionFilter(logsService)); - app.useGlobalInterceptors( - new RequestLoggingInterceptor(logsService, app.get(Reflector)), - new ResponseInterceptor(), - ); + const logsService = app.get(LogsService); - const config = new DocumentBuilder() - .setTitle("TikTok Short Drama Backend") - .setDescription("RESTful API for TikTok short drama backend services.") - .setVersion("1.0") - .addBearerAuth() - .build(); - const document = SwaggerModule.createDocument(app, config); - SwaggerModule.setup("/api/docs", app, document); + app.useGlobalFilters(new HttpExceptionFilter(logsService)); + app.useGlobalInterceptors(new RequestLoggingInterceptor(logsService, app.get(Reflector)), new ResponseInterceptor()); + + const config = new DocumentBuilder().setTitle("TikTok Short Drama Backend").setDescription("RESTful API for TikTok short drama backend services.").setVersion("1.0").addBearerAuth().build(); + + const document = SwaggerModule.createDocument(app, config); + + SwaggerModule.setup("/api/docs", app, document); } diff --git a/src/main.ts b/src/main.ts index 2f934fd..d99b050 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,12 +1,13 @@ -import 'dotenv/config'; -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { bootstrapApp } from './bootstrap'; +import "dotenv/config"; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { bootstrapApp } from "./bootstrap"; async function bootstrap() { - const app = await NestFactory.create(AppModule); - bootstrapApp(app); - await app.listen(process.env.PORT ?? 3000); + const app = await NestFactory.create(AppModule); + bootstrapApp(app); + await app.listen(process.env.PORT ?? 3000); + console.log(`Application is running on: ${await app.getUrl()}`); } void bootstrap(); diff --git a/src/modules/dramas/dramas.controller.ts b/src/modules/dramas/dramas.controller.ts index ed84335..8d502ef 100644 --- a/src/modules/dramas/dramas.controller.ts +++ b/src/modules/dramas/dramas.controller.ts @@ -1,172 +1,134 @@ -import { - Body, - Controller, - Get, - Param, - Patch, - Post, - Query, - UseGuards, -} from '@nestjs/common'; -import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; -import { RequirePermissions } from '../admin/decorators/require-permissions.decorator'; -import { AdminJwtAuthGuard } from '../admin/guards/admin-jwt-auth.guard'; -import { PermissionsGuard } from '../admin/guards/permissions.guard'; -import { ConfigureEpisodesDto } from './dto/configure-episodes.dto'; -import { CreateDramaDto } from './dto/create-drama.dto'; -import { CreateEpisodeDto } from './dto/create-episode.dto'; -import { UpdateDramaDto } from './dto/update-drama.dto'; -import { UpdateEpisodeDto } from './dto/update-episode.dto'; -import { DramasService } from './dramas.service'; -import { PublishStatus } from './drama-status.enum'; +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common"; +import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; +import { RequirePermissions } from "../admin/decorators/require-permissions.decorator"; +import { AdminJwtAuthGuard } from "../admin/guards/admin-jwt-auth.guard"; +import { PermissionsGuard } from "../admin/guards/permissions.guard"; +import { ConfigureEpisodesDto } from "./dto/configure-episodes.dto"; +import { CreateDramaDto } from "./dto/create-drama.dto"; +import { CreateEpisodeDto } from "./dto/create-episode.dto"; +import { UpdateDramaDto } from "./dto/update-drama.dto"; +import { UpdateEpisodeDto } from "./dto/update-episode.dto"; +import { DramasService } from "./dramas.service"; +import { PublishStatus } from "./drama-status.enum"; -@ApiTags('Dramas') +@ApiTags("Dramas") @Controller() export class DramasController { - constructor(private readonly dramasService: DramasService) {} + constructor(private readonly dramasService: DramasService) {} - @Post('/api/admin/v1/dramas') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('drama:create') - createDrama(@Body() dto: CreateDramaDto) { - return this.dramasService.createDrama(dto); - } + @Post("/api/admin/v1/dramas") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("drama:create") + createDrama(@Body() dto: CreateDramaDto) { + return this.dramasService.createDrama(dto); + } - @Get('/api/admin/v1/dramas') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('drama:read') - listAdminDramas(@Query('page') page?: string, @Query('pageSize') pageSize?: string) { - return this.dramasService.listAdminDramas({ - page: Number(page) || 1, - pageSize: Number(pageSize) || 20, - }); - } + @Get("/api/admin/v1/dramas") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("drama:read") + listAdminDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string) { + return this.dramasService.listAdminDramas({ + page: Number(page) || 1, + pageSize: Number(pageSize) || 20, + }); + } - @Get('/api/admin/v1/dramas/:id') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('drama:read') - getDrama(@Param('id') id: string) { - return this.dramasService.getDramaOrThrow(Number(id)); - } + @Get("/api/admin/v1/dramas/:id") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("drama:read") + getDrama(@Param("id") id: string) { + return this.dramasService.getDramaOrThrow(Number(id)); + } - @Patch('/api/admin/v1/dramas/:id') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('drama:update') - updateDrama(@Param('id') id: string, @Body() dto: UpdateDramaDto) { - return this.dramasService.updateDrama(Number(id), dto); - } + @Patch("/api/admin/v1/dramas/:id") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("drama:update") + updateDrama(@Param("id") id: string, @Body() dto: UpdateDramaDto) { + return this.dramasService.updateDrama(Number(id), dto); + } - @Post('/api/admin/v1/dramas/:id/episodes/batch') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('episode:create') - configureEpisodes( - @Param('id') id: string, - @Body() dto: ConfigureEpisodesDto, - ) { - return this.dramasService.configureEpisodes(Number(id), dto); - } + @Post("/api/admin/v1/dramas/:id/episodes/batch") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("episode:create") + configureEpisodes(@Param("id") id: string, @Body() dto: ConfigureEpisodesDto) { + return this.dramasService.configureEpisodes(Number(id), dto); + } - @Get('/api/admin/v1/dramas/:id/episodes') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('episode:update') - listAdminEpisodes( - @Param('id') id: string, - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - ) { - return this.dramasService.listAdminEpisodes(Number(id), { - page: Number(page) || 1, - pageSize: Number(pageSize) || 20, - }); - } + @Get("/api/admin/v1/dramas/:id/episodes") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("episode:update") + listAdminEpisodes(@Param("id") id: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string) { + return this.dramasService.listAdminEpisodes(Number(id), { + page: Number(page) || 1, + pageSize: Number(pageSize) || 20, + }); + } - @Post('/api/admin/v1/episodes') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('episode:create') - createEpisode(@Body() dto: CreateEpisodeDto) { - return this.dramasService.createEpisode(dto); - } + @Post("/api/admin/v1/episodes") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("episode:create") + createEpisode(@Body() dto: CreateEpisodeDto) { + return this.dramasService.createEpisode(dto); + } - @Patch('/api/admin/v1/episodes/:id') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('episode:update') - updateEpisode(@Param('id') id: string, @Body() dto: UpdateEpisodeDto) { - return this.dramasService.updateEpisode(Number(id), dto); - } + @Patch("/api/admin/v1/episodes/:id") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("episode:update") + updateEpisode(@Param("id") id: string, @Body() dto: UpdateEpisodeDto) { + return this.dramasService.updateEpisode(Number(id), dto); + } - @Patch('/api/admin/v1/dramas/:dramaId/episodes/:episodeId') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('episode:update') - updateDramaEpisode( - @Param('dramaId') dramaId: string, - @Param('episodeId') episodeId: string, - @Body() dto: UpdateEpisodeDto, - ) { - return this.dramasService.updateDramaEpisode( - Number(dramaId), - Number(episodeId), - dto, - ); - } + @Patch("/api/admin/v1/dramas/:dramaId/episodes/:episodeId") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("episode:update") + updateDramaEpisode(@Param("dramaId") dramaId: string, @Param("episodeId") episodeId: string, @Body() dto: UpdateEpisodeDto) { + return this.dramasService.updateDramaEpisode(Number(dramaId), Number(episodeId), dto); + } - @Post('/api/admin/v1/dramas/:id/submit-review') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('drama:update') - submitReview(@Param('id') id: string) { - return this.dramasService.submitReview(Number(id)); - } + @Post("/api/admin/v1/dramas/:id/submit-review") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("drama:update") + submitReview(@Param("id") id: string) { + return this.dramasService.submitReview(Number(id)); + } - @Patch('/api/admin/v1/episodes/:id/publish-status') - @ApiBearerAuth() - @UseGuards(AdminJwtAuthGuard, PermissionsGuard) - @RequirePermissions('episode:update') - updatePublishStatus( - @Param('id') id: string, - @Body() dto: { publishStatus: PublishStatus }, - ) { - return this.dramasService.updateEpisodePublishStatus( - Number(id), - dto.publishStatus, - ); - } + @Patch("/api/admin/v1/episodes/:id/publish-status") + @ApiBearerAuth() + @UseGuards(AdminJwtAuthGuard, PermissionsGuard) + @RequirePermissions("episode:update") + updatePublishStatus(@Param("id") id: string, @Body() dto: { publishStatus: PublishStatus }) { + return this.dramasService.updateEpisodePublishStatus(Number(id), dto.publishStatus); + } - @Get('/api/app/v1/dramas') - listPublishedDramas( - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - @Query('title') title?: string, - ) { - return this.dramasService.listPublishedDramas({ - page: Number(page) || 1, - pageSize: Number(pageSize) || 10, - title, - }); - } + @Get("/api/app/v1/dramas") + listPublishedDramas(@Query("page") page?: string, @Query("pageSize") pageSize?: string, @Query("title") title?: string) { + return this.dramasService.listPublishedDramas({ + page: Number(page) || 1, + pageSize: Number(pageSize) || 10, + title, + }); + } - @Get('/api/app/v1/dramas/:id/episodes') - listPublishedEpisodes( - @Param('id') id: string, - @Query('page') page?: string, - @Query('pageSize') pageSize?: string, - ) { - return this.dramasService.listPublishedEpisodes(Number(id), { - page: Number(page) || 1, - pageSize: Number(pageSize) || 20, - }); - } + @Get("/api/app/v1/dramas/:id/episodes") + listPublishedEpisodes(@Param("id") id: string, @Query("page") page?: string, @Query("pageSize") pageSize?: string) { + return this.dramasService.listPublishedEpisodes(Number(id), { + page: Number(page) || 1, + pageSize: Number(pageSize) || 20, + }); + } - @Get('/api/app/v1/dramas/:id') - getPublishedDrama(@Param('id') id: string) { - return this.dramasService.getPublishedDramaOrThrow(Number(id)); - } + @Get("/api/app/v1/dramas/:id") + getPublishedDrama(@Param("id") id: string) { + return this.dramasService.getPublishedDramaOrThrow(Number(id)); + } } diff --git a/src/modules/media/media.controller.ts b/src/modules/media/media.controller.ts index 2e5ac05..586de67 100644 --- a/src/modules/media/media.controller.ts +++ b/src/modules/media/media.controller.ts @@ -1,13 +1,4 @@ -import { - Body, - Controller, - Get, - Param, - Patch, - Post, - Query, - UseGuards, -} from "@nestjs/common"; +import { Body, Controller, Get, Param, Patch, Post, Query, UseGuards } from "@nestjs/common"; import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { RequirePermissions } from "../admin/decorators/require-permissions.decorator"; import { AdminJwtAuthGuard } from "../admin/guards/admin-jwt-auth.guard"; @@ -22,59 +13,56 @@ import { MediaService } from "./media.service"; @ApiBearerAuth() @UseGuards(AdminJwtAuthGuard, PermissionsGuard) export class MediaController { - constructor(private readonly mediaService: MediaService) {} + constructor(private readonly mediaService: MediaService) {} - @Post("/covers") - @RequirePermissions("media:create") - createCover(@Body() dto: CreateCoverDto) { - return this.mediaService.createCover(dto); - } + @Post("/covers") + @RequirePermissions("media:create") + createCover(@Body() dto: CreateCoverDto) { + return this.mediaService.createCover(dto); + } - @Post("/upload-jobs") - @RequirePermissions("media:create") - createUploadJob(@Body() dto: CreateUploadJobDto) { - return this.mediaService.createUploadJob(dto); - } + @Post("/upload-jobs") + @RequirePermissions("media:create") + createUploadJob(@Body() dto: CreateUploadJobDto) { + return this.mediaService.createUploadJob(dto); + } - @Post("/upload-tasks") - @RequirePermissions("media:create") - createDirectUploadTask(@Body() dto: CreateDirectUploadTaskDto) { - return this.mediaService.createDirectUploadTask(dto); - } + @Post("/upload-tasks") + @RequirePermissions("media:create") + createDirectUploadTask(@Body() dto: CreateDirectUploadTaskDto) { + return this.mediaService.createDirectUploadTask(dto); + } - @Get("/upload-tasks/:jobId") - @RequirePermissions("media:read") - getUploadTask(@Param("jobId") jobId: string) { - return this.mediaService.getUploadTask(jobId); - } + @Get("/upload-tasks/:jobId") + @RequirePermissions("media:read") + getUploadTask(@Param("jobId") jobId: string) { + return this.mediaService.getUploadTask(jobId); + } - @Patch("/upload-tasks/:jobId/complete") - @RequirePermissions("media:create") - completeDirectUploadTask(@Param("jobId") jobId: string) { - return this.mediaService.completeDirectUploadTask(jobId); - } + @Patch("/upload-tasks/:jobId/complete") + @RequirePermissions("media:create") + completeDirectUploadTask(@Param("jobId") jobId: string) { + return this.mediaService.completeDirectUploadTask(jobId); + } - @Post("/upload-tasks/:jobId/review/retry") - @RequirePermissions("media:create") - retryReview(@Param("jobId") jobId: string) { - return this.mediaService.retryReview(jobId); - } + @Post("/upload-tasks/:jobId/review/retry") + @RequirePermissions("media:create") + retryReview(@Param("jobId") jobId: string) { + return this.mediaService.retryReview(jobId); + } - @Post("/review-status/sync") - @RequirePermissions("media:create") - syncReviewStatuses() { - return this.mediaService.syncReviewStatuses(); - } + @Post("/review-status/sync") + @RequirePermissions("media:create") + syncReviewStatuses() { + return this.mediaService.syncReviewStatuses(); + } - @Get("/upload-jobs") - @RequirePermissions("media:read") - listUploadJobs( - @Query("page") page?: string, - @Query("pageSize") pageSize?: string, - ) { - return this.mediaService.listUploadJobs({ - page: Number(page) || 1, - pageSize: Number(pageSize) || 20, - }); - } + @Get("/upload-jobs") + @RequirePermissions("media:read") + listUploadJobs(@Query("page") page?: string, @Query("pageSize") pageSize?: string) { + return this.mediaService.listUploadJobs({ + page: Number(page) || 1, + pageSize: Number(pageSize) || 20, + }); + } } diff --git a/src/modules/users/users.service.ts b/src/modules/users/users.service.ts index 4c71627..53edc0b 100644 --- a/src/modules/users/users.service.ts +++ b/src/modules/users/users.service.ts @@ -1,177 +1,172 @@ -import { Injectable, NotFoundException, Optional } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; -import { DataSource } from 'typeorm'; -import { PaginatedResponse } from '../../common/dto/paginated-response.dto'; -import { User } from './entities/user.entity'; -import { UserRecord } from './interfaces/user-record.interface'; +import { Injectable, NotFoundException, Optional } from "@nestjs/common"; +import { InjectDataSource } from "@nestjs/typeorm"; +import { DataSource } from "typeorm"; +import { PaginatedResponse } from "../../common/dto/paginated-response.dto"; +import { User } from "./entities/user.entity"; +import { UserRecord } from "./interfaces/user-record.interface"; interface UpsertTikTokUserInput { - tiktokOpenId: string; - tiktokUnionId?: string; - nickname?: string; - avatarUrl?: string; + tiktokOpenId: string; + tiktokUnionId?: string; + nickname?: string; + avatarUrl?: string; } interface PaginationInput { - page: number; - pageSize: number; + page: number; + pageSize: number; } @Injectable() export class UsersService { - private userSequence = 1; - private readonly users: UserRecord[] = []; + private userSequence = 1; + private readonly users: UserRecord[] = []; - constructor( - @Optional() - @InjectDataSource() - readonly dataSource?: DataSource, - ) {} + constructor( + @Optional() + @InjectDataSource() + readonly dataSource?: DataSource, + ) {} - async upsertTikTokUser(input: UpsertTikTokUserInput) { - if (this.hasDatabase()) { - const repository = this.dataSource.getRepository(User); - const existing = await repository.findOne({ - where: { tiktokOpenId: input.tiktokOpenId }, - }); + async upsertTikTokUser(input: UpsertTikTokUserInput) { + if (this.hasDatabase()) { + const repository = this.dataSource.getRepository(User); + const existing = await repository.findOne({ + where: { tiktokOpenId: input.tiktokOpenId }, + }); - if (existing) { - existing.tiktokUnionId = input.tiktokUnionId ?? existing.tiktokUnionId; - existing.nickname = input.nickname ?? existing.nickname; - existing.avatarUrl = input.avatarUrl ?? existing.avatarUrl; - const saved = await repository.save(existing); - return { user: this.toRecord(saved), isNewUser: false }; - } + if (existing) { + existing.tiktokUnionId = input.tiktokUnionId ?? existing.tiktokUnionId; + existing.nickname = input.nickname ?? existing.nickname; + existing.avatarUrl = input.avatarUrl ?? existing.avatarUrl; + const saved = await repository.save(existing); + return { user: this.toRecord(saved), isNewUser: false }; + } - const user = repository.create({ - tiktokOpenId: input.tiktokOpenId, - tiktokUnionId: input.tiktokUnionId, - nickname: input.nickname, - avatarUrl: input.avatarUrl, - status: 'active', - }); - const saved = await repository.save(user); - return { user: this.toRecord(saved), isNewUser: true }; + const user = repository.create({ + tiktokOpenId: input.tiktokOpenId, + tiktokUnionId: input.tiktokUnionId, + nickname: input.nickname, + avatarUrl: input.avatarUrl, + status: "active", + }); + const saved = await repository.save(user); + return { user: this.toRecord(saved), isNewUser: true }; + } + + const existing = this.users.find((user) => user.tiktokOpenId === input.tiktokOpenId); + const now = new Date().toISOString(); + + if (existing) { + existing.tiktokUnionId = input.tiktokUnionId ?? existing.tiktokUnionId; + existing.nickname = input.nickname ?? existing.nickname; + existing.avatarUrl = input.avatarUrl ?? existing.avatarUrl; + existing.updatedAt = now; + return { user: existing, isNewUser: false }; + } + + const user: UserRecord = { + id: this.userSequence++, + tiktokOpenId: input.tiktokOpenId, + tiktokUnionId: input.tiktokUnionId, + nickname: input.nickname, + avatarUrl: input.avatarUrl, + status: "active", + createdAt: now, + updatedAt: now, + }; + + this.users.push(user); + return { user, isNewUser: true }; } - const existing = this.users.find( - (user) => user.tiktokOpenId === input.tiktokOpenId, - ); - const now = new Date().toISOString(); + async findById(id: number) { + if (this.hasDatabase()) { + const user = await this.dataSource.getRepository(User).findOne({ + where: { id }, + }); + return user ? this.toRecord(user) : undefined; + } - if (existing) { - existing.tiktokUnionId = input.tiktokUnionId ?? existing.tiktokUnionId; - existing.nickname = input.nickname ?? existing.nickname; - existing.avatarUrl = input.avatarUrl ?? existing.avatarUrl; - existing.updatedAt = now; - return { user: existing, isNewUser: false }; + return this.users.find((user) => user.id === id); } - const user: UserRecord = { - id: this.userSequence++, - tiktokOpenId: input.tiktokOpenId, - tiktokUnionId: input.tiktokUnionId, - nickname: input.nickname, - avatarUrl: input.avatarUrl, - status: 'active', - createdAt: now, - updatedAt: now, - }; + async listUsers(pagination: PaginationInput): Promise> { + if (this.hasDatabase()) { + const page = Math.max(pagination.page, 1); + const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100); + const repository = this.dataSource.getRepository(User); + const [records, total] = await repository.findAndCount({ + order: { id: "DESC" }, + skip: (page - 1) * pageSize, + take: pageSize, + }); - this.users.push(user); - return { user, isNewUser: true }; - } + return { + list: records.map((user) => this.toRecord(user)), + pagination: { page, pageSize, total }, + }; + } - async findById(id: number) { - if (this.hasDatabase()) { - const user = await this.dataSource.getRepository(User).findOne({ - where: { id }, - }); - return user ? this.toRecord(user) : undefined; + return this.paginate([...this.users].reverse(), pagination); } - return this.users.find((user) => user.id === id); - } + async getUserOrThrow(id: number) { + const user = await this.findById(id); + if (!user) { + throw new NotFoundException("User not found"); + } - async listUsers(pagination: PaginationInput): Promise> { - if (this.hasDatabase()) { - const page = Math.max(pagination.page, 1); - const pageSize = Math.min(Math.max(pagination.pageSize, 1), 100); - const repository = this.dataSource.getRepository(User); - const [records, total] = await repository.findAndCount({ - order: { id: 'DESC' }, - skip: (page - 1) * pageSize, - take: pageSize, - }); - - return { - list: records.map((user) => this.toRecord(user)), - pagination: { page, pageSize, total }, - }; + return user; } - return this.paginate([...this.users].reverse(), pagination); - } + async updateStatus(id: number, status: "active" | "disabled") { + if (this.hasDatabase()) { + const repository = this.dataSource.getRepository(User); + const user = await repository.findOne({ where: { id } }); + if (!user) { + throw new NotFoundException("User not found"); + } - async getUserOrThrow(id: number) { - const user = await this.findById(id); - if (!user) { - throw new NotFoundException('User not found'); + user.status = status; + return this.toRecord(await repository.save(user)); + } + + const user = await this.getUserOrThrow(id); + user.status = status; + user.updatedAt = new Date().toISOString(); + return user; } - return user; - } - - async updateStatus(id: number, status: 'active' | 'disabled') { - if (this.hasDatabase()) { - const repository = this.dataSource.getRepository(User); - const user = await repository.findOne({ where: { id } }); - if (!user) { - throw new NotFoundException('User not found'); - } - - user.status = status; - return this.toRecord(await repository.save(user)); + private hasDatabase(): this is this & { dataSource: DataSource } { + return Boolean(this.dataSource?.isInitialized); } - const user = await this.getUserOrThrow(id); - user.status = status; - user.updatedAt = new Date().toISOString(); - return user; - } + private toRecord(user: User): UserRecord { + return { + id: Number(user.id), + tiktokOpenId: user.tiktokOpenId, + tiktokUnionId: user.tiktokUnionId, + nickname: user.nickname, + avatarUrl: user.avatarUrl, + status: user.status as "active" | "disabled", + createdAt: user.createdAt?.toISOString?.() ?? new Date().toISOString(), + updatedAt: user.updatedAt?.toISOString?.() ?? new Date().toISOString(), + }; + } - private hasDatabase(): this is this & { dataSource: DataSource } { - return Boolean(this.dataSource?.isInitialized); - } + private paginate(records: T[], input: PaginationInput): PaginatedResponse { + const page = Math.max(input.page, 1); + const pageSize = Math.min(Math.max(input.pageSize, 1), 100); + const start = (page - 1) * pageSize; - private toRecord(user: User): UserRecord { - return { - id: Number(user.id), - tiktokOpenId: user.tiktokOpenId, - tiktokUnionId: user.tiktokUnionId, - nickname: user.nickname, - avatarUrl: user.avatarUrl, - status: user.status as 'active' | 'disabled', - createdAt: user.createdAt?.toISOString?.() ?? new Date().toISOString(), - updatedAt: user.updatedAt?.toISOString?.() ?? new Date().toISOString(), - }; - } - - private paginate( - records: T[], - input: PaginationInput, - ): PaginatedResponse { - const page = Math.max(input.page, 1); - const pageSize = Math.min(Math.max(input.pageSize, 1), 100); - const start = (page - 1) * pageSize; - - return { - list: records.slice(start, start + pageSize), - pagination: { - page, - pageSize, - total: records.length, - }, - }; - } + return { + list: records.slice(start, start + pageSize), + pagination: { + page, + pageSize, + total: records.length, + }, + }; + } } diff --git a/tsconfig.json b/tsconfig.json index c5be626..8acc204 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,19 @@ { - "compilerOptions": { - "module": "commonjs", - "declaration": true, - "removeComments": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "target": "ES2023", - "sourceMap": true, - "outDir": "./dist", - "baseUrl": "./", - "incremental": true, - "strict": true, - "skipLibCheck": true, - "strictPropertyInitialization": false, - "noImplicitAny": false - } + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "strict": true, + "skipLibCheck": true, + "strictPropertyInitialization": false, + "noImplicitAny": false + } }