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

@@ -4,60 +4,13 @@
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import type { Repository } from '$lib/types';
import { getClient } from '$lib/server/db/client';
import { RepositoryMapper } from '$lib/server/mappers/repository.mapper.js';
import { IndexingJobMapper } from '$lib/server/mappers/indexing-job.mapper.js';
import { RepositoryService } from '$lib/server/services/repository.service';
import { getQueue } from '$lib/server/pipeline/startup';
import { handleServiceError } from '$lib/server/utils/validation';
// ---------------------------------------------------------------------------
// Row mapper — better-sqlite3 returns snake_case column names from SELECT *;
// this converts them to the camelCase shape expected by the Repository type
// and by client components (e.g. RepositoryCard reads repo.trustScore).
// ---------------------------------------------------------------------------
interface RawRepoRow {
id: string;
title: string;
description: string | null;
source: string;
source_url: string;
branch: string | null;
state: string;
total_snippets: number;
total_tokens: number;
trust_score: number;
benchmark_score: number;
stars: number | null;
github_token: string | null;
last_indexed_at: number | null;
created_at: number;
updated_at: number;
}
function mapRepo(raw: Repository): Repository {
const r = raw as unknown as RawRepoRow;
return {
id: r.id,
title: r.title,
description: r.description ?? null,
source: r.source as Repository['source'],
sourceUrl: r.source_url,
branch: r.branch ?? null,
state: r.state as Repository['state'],
totalSnippets: r.total_snippets ?? 0,
totalTokens: r.total_tokens ?? 0,
trustScore: r.trust_score ?? 0,
benchmarkScore: r.benchmark_score ?? 0,
stars: r.stars ?? null,
githubToken: r.github_token ?? null,
lastIndexedAt:
r.last_indexed_at != null ? new Date(r.last_indexed_at * 1000) : null,
createdAt: new Date(r.created_at * 1000),
updatedAt: new Date(r.updated_at * 1000)
};
}
function getService() {
return new RepositoryService(getClient());
}
@@ -77,14 +30,10 @@ export const GET: RequestHandler = ({ url }) => {
const libraries = service.list({ state: state ?? undefined, limit, offset });
const total = service.count(state ?? undefined);
// Map raw snake_case rows to camelCase, augment with versions, strip sensitive fields.
const enriched = libraries.map((rawRepo) => {
const { githubToken: _token, ...repo } = mapRepo(rawRepo);
return {
...repo,
versions: service.getVersions(repo.id)
};
});
const enriched = libraries.map((repo) => ({
...RepositoryMapper.toDto(repo),
versions: service.getVersions(repo.id)
}));
return json({ libraries: enriched, total, limit, offset });
} catch (err) {
@@ -106,18 +55,17 @@ export const POST: RequestHandler = async ({ request }) => {
githubToken: body.githubToken
});
let jobResponse: { id: string; status: string } | null = null;
let jobResponse: ReturnType<typeof IndexingJobMapper.toDto> | null = null;
if (body.autoIndex !== false) {
const queue = getQueue();
const job = queue
? queue.enqueue(repo.id)
: service.createIndexingJob(repo.id);
jobResponse = { id: job.id, status: job.status };
jobResponse = IndexingJobMapper.toDto(job);
}
const { githubToken: _token, ...safeRepo } = repo;
return json(
{ library: safeRepo, ...(jobResponse ? { job: jobResponse } : {}) },
{ library: RepositoryMapper.toDto(repo), ...(jobResponse ? { job: jobResponse } : {}) },
{ status: 201 }
);
} catch (err) {