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

@@ -115,26 +115,19 @@ describe('VersionService.list()', () => {
describe('VersionService.add()', () => {
it('creates a version with the correct ID format', () => {
const { versionService } = setup();
const version = versionService.add(
'/facebook/react',
'v18.3.0',
'React v18.3.0'
) as unknown as RawVersion;
const version = versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
expect(version.id).toBe('/facebook/react/v18.3.0');
expect(version.repository_id).toBe('/facebook/react');
expect(version.repositoryId).toBe('/facebook/react');
expect(version.tag).toBe('v18.3.0');
expect(version.title).toBe('React v18.3.0');
expect(version.state).toBe('pending');
expect(version.total_snippets).toBe(0);
expect(version.indexed_at).toBeNull();
expect(version.totalSnippets).toBe(0);
expect(version.indexedAt).toBeNull();
});
it('creates a version without a title', () => {
const { versionService } = setup();
const version = versionService.add(
'/facebook/react',
'v18.3.0'
) as unknown as RawVersion;
const version = versionService.add('/facebook/react', 'v18.3.0');
expect(version.title).toBeNull();
});
@@ -154,7 +147,7 @@ describe('VersionService.add()', () => {
// Use a repo name without dots so resolveGitHubId produces a predictable ID.
repoService.add({ source: 'github', sourceUrl: 'https://github.com/vercel/nextjs' });
versionService.add('/facebook/react', 'v18.3.0');
const v = versionService.add('/vercel/nextjs', 'v18.3.0') as unknown as RawVersion;
const v = versionService.add('/vercel/nextjs', 'v18.3.0');
expect(v.id).toBe('/vercel/nextjs/v18.3.0');
});
});
@@ -211,13 +204,11 @@ describe('VersionService.getByTag()', () => {
it('returns the version record when it exists', () => {
const { versionService } = setup();
versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
const version = versionService.getByTag(
'/facebook/react',
'v18.3.0'
) as unknown as RawVersion;
const version = versionService.getByTag('/facebook/react', 'v18.3.0');
expect(version).not.toBeNull();
if (!version) throw new Error('Expected version to exist');
expect(version.tag).toBe('v18.3.0');
expect(version.repository_id).toBe('/facebook/react');
expect(version?.repositoryId).toBe('/facebook/react');
});
});
@@ -266,10 +257,8 @@ describe('VersionService.registerFromConfig()', () => {
versionService.registerFromConfig('/facebook/react', [
{ tag: 'v18.3.0', title: 'React v18.3.0' }
]);
const version = versionService.getByTag(
'/facebook/react',
'v18.3.0'
) as unknown as RawVersion;
const version = versionService.getByTag('/facebook/react', 'v18.3.0');
if (!version) throw new Error('Expected version to exist');
expect(version.state).toBe('pending');
});
});