From 65b5a3c29c2c4cbdafbd4a5312ebd50cd36dcdaa Mon Sep 17 00:00:00 2001 From: chyoo Date: Wed, 8 Jul 2026 13:56:01 +0900 Subject: [PATCH] change module delete logic... --- src/studio/studio.controller.ts | 16 +++- src/studio/studio.service.ts | 134 +++++++++++++++++++++++++++----- 2 files changed, 128 insertions(+), 22 deletions(-) diff --git a/src/studio/studio.controller.ts b/src/studio/studio.controller.ts index 1cd2382..7337831 100644 --- a/src/studio/studio.controller.ts +++ b/src/studio/studio.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Post, Req } from '@nestjs/common'; +import { Body, Controller, Delete, Post, Req } from '@nestjs/common'; import { Request } from 'express'; import { StudioService } from './studio.service'; @@ -37,6 +37,20 @@ export class StudioController { return this.studioService.recordSubmission(this.getBearerToken(request), body); } + @Delete('repositories') + deleteRepository( + @Req() request: Request, + @Body() + body: { + courseId?: string; + week?: number; + moduleName?: string; + folderName?: string; + }, + ) { + return this.studioService.deleteRepository(this.getBearerToken(request), body); + } + @Post('learning-time') recordLearningTime( @Req() request: Request, diff --git a/src/studio/studio.service.ts b/src/studio/studio.service.ts index 419c2a7..e6a5dc4 100644 --- a/src/studio/studio.service.ts +++ b/src/studio/studio.service.ts @@ -102,34 +102,40 @@ export class StudioService { throw new BadRequestException('templateRepositoryUrl is required'); } - const repoName = `${this.slug(authUser.email.split('@')[0] || authUser.email)}-week-${courseWeek.week}-${this.slug(module.name)}`; - const repositoryUrl = `${this.giteaBaseUrl}/${this.studentOwner}/${repoName}.git`; - - await this.ensureGiteaRepository(repoName, `Studio workspace for ${authUser.email} ${courseWeek.week}W ${module.name}`); - - const repository = await this.prisma.studioRepository.upsert({ + const existingRepository = await this.prisma.studioRepository.findUnique({ where: { userId_moduleId: { userId: lmsUser.id, moduleId: module.id, }, }, - update: { - repoOwner: this.studentOwner, - repoName, - repositoryUrl, - templateRepositoryUrl, - }, - create: { - userId: lmsUser.id, - weekId: courseWeek.id, - moduleId: module.id, - repoOwner: this.studentOwner, - repoName, - repositoryUrl, - templateRepositoryUrl, - }, }); + const courseIdPrefix = courseWeek.courseId.replace(/-/g, '').slice(0, 8); + const repoName = existingRepository?.repoName + ?? `${this.slug(authUser.email.split('@')[0] || authUser.email)}-course-${courseIdPrefix}-week-${courseWeek.week}-${this.slug(module.name)}`; + const repositoryUrl = existingRepository?.repositoryUrl + ?? `${this.giteaBaseUrl}/${this.studentOwner}/${repoName}.git`; + + if (!existingRepository) { + await this.ensureGiteaRepository(repoName, `Studio workspace for ${authUser.email} ${courseWeek.week}W ${module.name}`); + } + + const repository = existingRepository + ? await this.prisma.studioRepository.update({ + where: { id: existingRepository.id }, + data: { templateRepositoryUrl }, + }) + : await this.prisma.studioRepository.create({ + data: { + userId: lmsUser.id, + weekId: courseWeek.id, + moduleId: module.id, + repoOwner: this.studentOwner, + repoName, + repositoryUrl, + templateRepositoryUrl, + }, + }); const moduleWithArtifacts = module as typeof module & { sourceType?: string | null; @@ -249,6 +255,81 @@ export class StudioService { }; } + async deleteRepository( + accessToken: string | undefined, + input: { + courseId?: string; + week?: number; + moduleName?: string; + folderName?: string; + }, + ) { + if (!this.giteaToken) { + throw new BadGatewayException('GITEA_ADMIN_TOKEN is not configured'); + } + + const { user: authUser } = await this.authService.me(accessToken); + if (!authUser?.email) throw new UnauthorizedException('Invalid user'); + if (!input.week || input.week < 1) throw new BadRequestException('week is required'); + + const moduleName = input.moduleName?.trim(); + const folderName = input.folderName?.trim(); + if (!moduleName && !folderName) throw new BadRequestException('moduleName or folderName is required'); + + const lmsUser = await this.prisma.user.findUnique({ + where: { email: authUser.email }, + }); + if (!lmsUser) throw new NotFoundException('LMS user not found'); + + const courseId = input.courseId?.trim(); + if (!courseId) throw new BadRequestException('courseId is required'); + + const courseWeek = await this.prisma.courseWeek.findFirst({ + where: { + courseId, + week: input.week, + course: { + isActive: true, + enrollments: { + some: { + userId: lmsUser.id, + status: 'ACTIVE', + }, + }, + }, + }, + }); + if (!courseWeek) throw new NotFoundException('Course week not found'); + + const moduleConditions = [ + moduleName ? { name: moduleName } : undefined, + folderName ? { folderName } : undefined, + ].filter((condition): condition is { name: string } | { folderName: string } => Boolean(condition)); + const module = await this.prisma.courseWeekModule.findFirst({ + where: { + weekId: courseWeek.id, + OR: moduleConditions, + }, + }); + if (!module) throw new NotFoundException('Course week module not found'); + + const repository = await this.prisma.studioRepository.findUnique({ + where: { + userId_moduleId: { + userId: lmsUser.id, + moduleId: module.id, + }, + }, + }); + + if (!repository) return { ok: true, deleted: false }; + + await this.deleteGiteaRepository(repository.repoOwner, repository.repoName); + await this.prisma.studioRepository.delete({ where: { id: repository.id } }); + + return { ok: true, deleted: true }; + } + async recordLearningTime( accessToken: string | undefined, input: { @@ -360,6 +441,17 @@ export class StudioService { } } + private async deleteGiteaRepository(owner: string, repoName: string) { + const response = await fetch(`${this.giteaBaseUrl}/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repoName)}`, { + method: 'DELETE', + headers: this.giteaHeaders(), + }); + + if (!response.ok && response.status !== 404) { + throw new BadGatewayException(await response.text()); + } + } + private giteaHeaders() { return { Authorization: `token ${this.giteaToken}`,