refactor: introduce domain model classes and mapper layer

Replace ad-hoc inline row casting (snake_case → camelCase) spread across
services, routes, and the indexing pipeline with explicit model classes
(Repository, IndexingJob, RepositoryVersion, Snippet, SearchResult) and
dedicated mapper classes that own the DB → domain conversion.

- Add src/lib/server/models/ with typed model classes for all domain entities
- Add src/lib/server/mappers/ with mapper classes per entity
- Remove duplicated RawRow interfaces and inline map functions from
  job-queue, repository.service, indexing.pipeline, and all API routes
- Add dtoJsonResponse helper to standardise JSON responses via SvelteKit json()
- Add api-contract.integration.test.ts as a regression baseline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-25 14:29:49 +01:00
parent 7994254e23
commit 215cadf070
39 changed files with 1339 additions and 562 deletions

View File

@@ -0,0 +1,71 @@
import {
CodeListItemDto,
CodeSnippetJsonDto,
ContextJsonResponseDto,
InfoSnippetJsonDto,
LibrarySearchJsonResponseDto,
LibrarySearchJsonResultDto,
type SnippetJsonDto
} from '$lib/server/models/context-response.js';
import { LibrarySearchResult, SnippetSearchResult } from '$lib/server/models/search-result.js';
export class ContextResponseMapper {
static toLibrarySearchJson(results: LibrarySearchResult[]): LibrarySearchJsonResponseDto {
return new LibrarySearchJsonResponseDto(
results.map(
({ repository, versions }) =>
new LibrarySearchJsonResultDto({
id: repository.id,
title: repository.title,
description: repository.description ?? null,
branch: repository.branch ?? null,
lastUpdateDate: repository.lastIndexedAt
? repository.lastIndexedAt.toISOString()
: null,
state: repository.state === 'indexed' ? 'finalized' : repository.state === 'error' ? 'error' : 'initial',
totalTokens: repository.totalTokens ?? null,
totalSnippets: repository.totalSnippets ?? null,
stars: repository.stars ?? null,
trustScore: repository.trustScore ?? null,
benchmarkScore: repository.benchmarkScore ?? null,
versions: versions.map((version) => version.tag),
source: repository.sourceUrl
})
)
);
}
static toContextJson(snippets: SnippetSearchResult[], rules: string[]): ContextJsonResponseDto {
const mapped: SnippetJsonDto[] = snippets.map(({ snippet }) => {
if (snippet.type === 'code') {
return new CodeSnippetJsonDto({
title: snippet.title ?? null,
description: snippet.breadcrumb ?? null,
language: snippet.language ?? null,
codeList: [
new CodeListItemDto({
language: snippet.language ?? '',
code: snippet.content
})
],
id: snippet.id,
tokenCount: snippet.tokenCount ?? null,
pageTitle: snippet.breadcrumb ? snippet.breadcrumb.split('>')[0].trim() || null : null
});
}
return new InfoSnippetJsonDto({
text: snippet.content,
breadcrumb: snippet.breadcrumb ?? null,
pageId: snippet.id,
tokenCount: snippet.tokenCount ?? null
});
});
return new ContextJsonResponseDto({
snippets: mapped,
rules,
totalTokens: snippets.reduce((sum, result) => sum + (result.snippet.tokenCount ?? 0), 0)
});
}
}