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:
163
src/routes/api/v1/api-contract.integration.test.ts
Normal file
163
src/routes/api/v1/api-contract.integration.test.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Database from 'better-sqlite3';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { RepositoryService } from '$lib/server/services/repository.service';
|
||||
import { VersionService } from '$lib/server/services/version.service';
|
||||
|
||||
let db: Database.Database;
|
||||
let queue: null = null;
|
||||
|
||||
vi.mock('$lib/server/db/client', () => ({
|
||||
getClient: () => db
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/db/client.js', () => ({
|
||||
getClient: () => db
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/pipeline/startup', () => ({
|
||||
getQueue: () => queue
|
||||
}));
|
||||
|
||||
vi.mock('$lib/server/pipeline/startup.js', () => ({
|
||||
getQueue: () => queue
|
||||
}));
|
||||
|
||||
import { POST as postLibraries } from './libs/+server.js';
|
||||
import { GET as getLibrary } from './libs/[id]/+server.js';
|
||||
import { GET as getJobs } from './jobs/+server.js';
|
||||
import { GET as getJob } from './jobs/[id]/+server.js';
|
||||
import { GET as getVersions, POST as postVersions } from './libs/[id]/versions/+server.js';
|
||||
|
||||
function createTestDb(): Database.Database {
|
||||
const client = new Database(':memory:');
|
||||
client.pragma('foreign_keys = ON');
|
||||
|
||||
const migrationsFolder = join(import.meta.dirname, '../../../lib/server/db/migrations');
|
||||
const migrationSql = readFileSync(join(migrationsFolder, '0000_large_master_chief.sql'), 'utf-8');
|
||||
|
||||
const statements = migrationSql
|
||||
.split('--> statement-breakpoint')
|
||||
.map((statement) => statement.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const statement of statements) {
|
||||
client.exec(statement);
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
describe('API contract integration', () => {
|
||||
beforeEach(() => {
|
||||
db = createTestDb();
|
||||
queue = null;
|
||||
});
|
||||
|
||||
it('POST /api/v1/libs returns repository and job DTOs in camelCase', async () => {
|
||||
const response = await postLibraries({
|
||||
request: new Request('http://test/api/v1/libs', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
source: 'github',
|
||||
sourceUrl: 'https://github.com/facebook/react'
|
||||
})
|
||||
}),
|
||||
url: new URL('http://test/api/v1/libs')
|
||||
} as never);
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.library.sourceUrl).toBe('https://github.com/facebook/react');
|
||||
expect(body.library.totalSnippets).toBe(0);
|
||||
expect(body.library.lastIndexedAt).toBeNull();
|
||||
expect(body.library).not.toHaveProperty('source_url');
|
||||
expect(body.library).not.toHaveProperty('total_snippets');
|
||||
|
||||
expect(body.job.repositoryId).toBe('/facebook/react');
|
||||
expect(body.job.processedFiles).toBe(0);
|
||||
expect(body.job).not.toHaveProperty('repository_id');
|
||||
expect(body.job).not.toHaveProperty('processed_files');
|
||||
});
|
||||
|
||||
it('GET /api/v1/libs/:id returns repository DTO plus version tags', async () => {
|
||||
const repoService = new RepositoryService(db);
|
||||
const versionService = new VersionService(db);
|
||||
repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
|
||||
versionService.add('/facebook/react', 'v18.3.0', 'React v18.3.0');
|
||||
|
||||
const response = await getLibrary({
|
||||
params: { id: encodeURIComponent('/facebook/react') }
|
||||
} as never);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.sourceUrl).toBe('https://github.com/facebook/react');
|
||||
expect(body.totalSnippets).toBe(0);
|
||||
expect(body.versions).toEqual(['v18.3.0']);
|
||||
expect(body).not.toHaveProperty('source_url');
|
||||
expect(body).not.toHaveProperty('total_snippets');
|
||||
});
|
||||
|
||||
it('GET /api/v1/jobs and /api/v1/jobs/:id return job DTOs in camelCase', async () => {
|
||||
const repoService = new RepositoryService(db);
|
||||
repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
|
||||
const createdJob = repoService.createIndexingJob('/facebook/react');
|
||||
|
||||
const listResponse = await getJobs({
|
||||
url: new URL('http://test/api/v1/jobs?repositoryId=%2Ffacebook%2Freact')
|
||||
} as never);
|
||||
const listBody = await listResponse.json();
|
||||
|
||||
expect(listBody.jobs).toHaveLength(1);
|
||||
expect(listBody.jobs[0].repositoryId).toBe('/facebook/react');
|
||||
expect(listBody.jobs[0].totalFiles).toBe(0);
|
||||
expect(listBody.jobs[0]).not.toHaveProperty('repository_id');
|
||||
|
||||
const itemResponse = await getJob({
|
||||
params: { id: createdJob.id }
|
||||
} as never);
|
||||
const itemBody = await itemResponse.json();
|
||||
|
||||
expect(itemBody.job.repositoryId).toBe('/facebook/react');
|
||||
expect(itemBody.job.processedFiles).toBe(0);
|
||||
expect(itemBody.job).not.toHaveProperty('processed_files');
|
||||
});
|
||||
|
||||
it('GET and POST /api/v1/libs/:id/versions return version and job DTOs in camelCase', async () => {
|
||||
const repoService = new RepositoryService(db);
|
||||
repoService.add({ source: 'github', sourceUrl: 'https://github.com/facebook/react' });
|
||||
|
||||
const postResponse = await postVersions({
|
||||
params: { id: encodeURIComponent('/facebook/react') },
|
||||
request: new Request('http://test/api/v1/libs/%2Ffacebook%2Freact/versions', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ tag: 'v18.3.0', title: 'React v18.3.0', autoIndex: true })
|
||||
})
|
||||
} as never);
|
||||
const postBody = await postResponse.json();
|
||||
|
||||
expect(postResponse.status).toBe(201);
|
||||
expect(postBody.version.repositoryId).toBe('/facebook/react');
|
||||
expect(postBody.version.totalSnippets).toBe(0);
|
||||
expect(postBody.version).not.toHaveProperty('repository_id');
|
||||
expect(postBody.job.repositoryId).toBe('/facebook/react');
|
||||
expect(postBody.job).not.toHaveProperty('repository_id');
|
||||
|
||||
const getResponse = await getVersions({
|
||||
params: { id: encodeURIComponent('/facebook/react') }
|
||||
} as never);
|
||||
const getBody = await getResponse.json();
|
||||
|
||||
expect(getBody.versions).toHaveLength(1);
|
||||
expect(getBody.versions[0].repositoryId).toBe('/facebook/react');
|
||||
expect(getBody.versions[0].totalSnippets).toBe(0);
|
||||
expect(getBody.versions[0]).not.toHaveProperty('repository_id');
|
||||
expect(getBody.versions[0]).not.toHaveProperty('total_snippets');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user