import { ConflictException, Injectable, NotFoundException, OnModuleInit, } from "@nestjs/common"; import { InjectRepository } from "@nestjs/typeorm"; import { Repository } from "typeorm"; import { PaginatedResult, PaginationQueryDto, } from "../common/dto/pagination.dto"; import { CreateSnippetDto } from "./dto/create-snippet.dto"; import { SnippetExportDto } from "./dto/snippet-export.dto"; import { UpdateSnippetDto } from "./dto/update-snippet.dto"; import { SnippetEntity } from "./snippet.entity"; export type SnippetOrderBy = | "id" | "alias" | "title" | "createdAt" | "updatedAt"; @Injectable() export class SnippetService implements OnModuleInit { constructor( @InjectRepository(SnippetEntity) private readonly repo: Repository, ) {} async onModuleInit(): Promise { // Best-effort backfill for existing DBs that still have legacy `name` values. try { await this.repo.query( "UPDATE snippets SET alias = name WHERE (alias IS NULL OR alias = '') AND name IS NOT NULL", ); } catch { // Ignore when legacy `name` column does not exist. } try { await this.repo.query( "UPDATE snippets SET title = COALESCE(alias, name, '') WHERE title IS NULL OR title = ''", ); } catch { await this.repo.query( "UPDATE snippets SET title = COALESCE(alias, '') WHERE title IS NULL OR title = ''", ); } } async create(dto: CreateSnippetDto): Promise { const existing = await this.repo.findOneBy({ alias: dto.alias }); if (existing) { throw new ConflictException( `Snippet alias "${dto.alias}" already exists`, ); } return this.repo.save( this.repo.create({ alias: dto.alias, title: dto.title, description: dto.description ?? null, code: dto.code, }), ); } async findAll( query: PaginationQueryDto = {}, ): Promise> { const page = query.page ?? 1; const limit = query.limit ?? 50; const orderBy = query.orderBy ?? "id"; const orderDir = query.orderDir ?? "ASC"; const [data, total] = await this.repo.findAndCount({ order: { [orderBy]: orderDir }, skip: (page - 1) * limit, take: limit, }); return { data, total, page, limit }; } async findOne(id: string): Promise { const snippet = await this.repo.findOneBy({ id }); if (!snippet) throw new NotFoundException(`Snippet ${id} not found`); return snippet; } async update(id: string, dto: UpdateSnippetDto): Promise { const snippet = await this.findOne(id); if (dto.alias && dto.alias !== snippet.alias) { const conflict = await this.repo.findOneBy({ alias: dto.alias }); if (conflict) throw new ConflictException( `Snippet alias "${dto.alias}" already exists`, ); } Object.assign(snippet, dto); return this.repo.save(snippet); } async remove(id: string): Promise { await this.findOne(id); await this.repo.delete(id); } /** Returns an alias→code map for all snippets (used by the executor). */ async buildSnippetMap(): Promise> { const { data } = await this.findAll({ limit: 1000 }); return Object.fromEntries(data.map((s) => [s.alias, s.code])); } exportSnippet(snippet: SnippetEntity): SnippetExportDto { return { kind: "snippet", id: snippet.id, alias: snippet.alias, title: snippet.title, description: snippet.description, code: snippet.code, }; } async importSnippet(dto: SnippetExportDto): Promise { const alias = dto.alias ?? dto.name; if (!alias) { throw new ConflictException("Snippet alias is required"); } const title = dto.title ?? alias; if (dto.id) { const existing = await this.repo.findOneBy({ id: dto.id }); if (existing) { Object.assign(existing, { alias, title, description: dto.description ?? null, code: dto.code, }); return this.repo.save(existing); } return this.repo.save( this.repo.create({ id: dto.id, alias, title, description: dto.description ?? null, code: dto.code, }), ); } return this.repo.save( this.repo.create({ alias, title, description: dto.description ?? null, code: dto.code, }), ); } }