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:
@@ -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) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getClient } from '$lib/server/db/client';
|
||||
import { RepositoryMapper } from '$lib/server/mappers/repository.mapper.js';
|
||||
import { RepositoryService } from '$lib/server/services/repository.service';
|
||||
import { handleServiceError } from '$lib/server/utils/validation';
|
||||
|
||||
@@ -22,8 +23,7 @@ export const GET: RequestHandler = ({ params }) => {
|
||||
return json({ error: 'Repository not found', code: 'NOT_FOUND' }, { status: 404 });
|
||||
}
|
||||
const versions = service.getVersions(id);
|
||||
const { githubToken: _token, ...safeRepo } = repo;
|
||||
return json({ ...safeRepo, versions });
|
||||
return json({ ...RepositoryMapper.toDto(repo), versions });
|
||||
} catch (err) {
|
||||
return handleServiceError(err);
|
||||
}
|
||||
@@ -40,8 +40,7 @@ export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
branch: body.branch,
|
||||
githubToken: body.githubToken
|
||||
});
|
||||
const { githubToken: _token, ...safeUpdated } = updated;
|
||||
return json(safeUpdated);
|
||||
return json(RepositoryMapper.toDto(updated));
|
||||
} catch (err) {
|
||||
return handleServiceError(err);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getClient } from '$lib/server/db/client';
|
||||
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, NotFoundError } from '$lib/server/utils/validation';
|
||||
@@ -31,7 +32,7 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
? queue.enqueue(id, versionId)
|
||||
: service.createIndexingJob(id, versionId);
|
||||
|
||||
return json({ job }, { status: 202 });
|
||||
return json({ job: IndexingJobMapper.toDto(job) }, { status: 202 });
|
||||
} catch (err) {
|
||||
return handleServiceError(err);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getClient } from '$lib/server/db/client';
|
||||
import { RepositoryVersionMapper } from '$lib/server/mappers/repository-version.mapper.js';
|
||||
import { IndexingJobMapper } from '$lib/server/mappers/indexing-job.mapper.js';
|
||||
import { RepositoryService } from '$lib/server/services/repository.service';
|
||||
import { VersionService } from '$lib/server/services/version.service';
|
||||
import { handleServiceError, NotFoundError, InvalidInputError } from '$lib/server/utils/validation';
|
||||
@@ -33,7 +35,7 @@ export const GET: RequestHandler = ({ params }) => {
|
||||
}
|
||||
|
||||
const versions = versionService.list(repositoryId);
|
||||
return json({ versions });
|
||||
return json({ versions: versions.map((version) => RepositoryVersionMapper.toDto(version)) });
|
||||
} catch (err) {
|
||||
return handleServiceError(err);
|
||||
}
|
||||
@@ -74,13 +76,16 @@ export const POST: RequestHandler = async ({ params, request }) => {
|
||||
|
||||
const version = versionService.add(repositoryId, tag.trim(), title);
|
||||
|
||||
let job: { id: string; status: string } | undefined;
|
||||
let job: ReturnType<typeof IndexingJobMapper.toDto> | undefined;
|
||||
if (autoIndex) {
|
||||
const indexingJob = repoService.createIndexingJob(repositoryId, version.id);
|
||||
job = { id: indexingJob.id, status: indexingJob.status };
|
||||
job = IndexingJobMapper.toDto(indexingJob);
|
||||
}
|
||||
|
||||
return json({ version, ...(job ? { job } : {}) }, { status: 201 });
|
||||
return json(
|
||||
{ version: RepositoryVersionMapper.toDto(version), ...(job ? { job } : {}) },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (err) {
|
||||
return handleServiceError(err);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getClient } from '$lib/server/db/client';
|
||||
import { IndexingJobMapper } from '$lib/server/mappers/indexing-job.mapper.js';
|
||||
import { RepositoryService } from '$lib/server/services/repository.service';
|
||||
import { VersionService } from '$lib/server/services/version.service';
|
||||
import { getQueue } from '$lib/server/pipeline/startup';
|
||||
@@ -42,7 +43,7 @@ export const POST: RequestHandler = ({ params }) => {
|
||||
const job = queue
|
||||
? queue.enqueue(repositoryId, version.id)
|
||||
: repoService.createIndexingJob(repositoryId, version.id);
|
||||
return json({ job }, { status: 202 });
|
||||
return json({ job: IndexingJobMapper.toDto(job) }, { status: 202 });
|
||||
} catch (err) {
|
||||
return handleServiceError(err);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getClient } from '$lib/server/db/client';
|
||||
import { dtoJsonResponse } from '$lib/server/api/dto-response';
|
||||
import { SearchService } from '$lib/server/search/search.service';
|
||||
import { formatLibrarySearchJson } from '$lib/server/api/formatters';
|
||||
import { CORS_HEADERS } from '$lib/server/api/formatters';
|
||||
@@ -44,7 +45,7 @@ export const GET: RequestHandler = ({ url }) => {
|
||||
const results = service.searchRepositories({ libraryName, query, limit });
|
||||
const body = formatLibrarySearchJson(results);
|
||||
|
||||
return json(body, {
|
||||
return dtoJsonResponse(body, {
|
||||
headers: CORS_HEADERS
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user