feat(TRUEREF-0014): implement repository version management

- VersionService with list, add, remove, getByTag, registerFromConfig
- GitHub tag discovery helper for validating tags before indexing
- Version ID format: /owner/repo/tag (e.g. /facebook/react/v18.3.0)
- GET/POST /api/v1/libs/:id/versions
- DELETE /api/v1/libs/:id/versions/:tag
- POST /api/v1/libs/:id/versions/:tag/index

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Giancarmine Salucci
2026-03-23 09:06:59 +01:00
parent f31db2db2c
commit 542f4ce66c
7 changed files with 794 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
/**
* GET /api/v1/libs/:id/versions — list all indexed versions for a repository
* POST /api/v1/libs/:id/versions — add a new version (tag or branch)
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client';
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';
function getServices() {
const db = getClient();
return {
repoService: new RepositoryService(db),
versionService: new VersionService(db)
};
}
// ---------------------------------------------------------------------------
// GET /api/v1/libs/:id/versions
// ---------------------------------------------------------------------------
export const GET: RequestHandler = ({ params }) => {
try {
const { repoService, versionService } = getServices();
const repositoryId = decodeURIComponent(params.id);
const repo = repoService.get(repositoryId);
if (!repo) {
throw new NotFoundError(`Repository ${repositoryId} not found`);
}
const versions = versionService.list(repositoryId);
return json({ versions });
} catch (err) {
return handleServiceError(err);
}
};
// ---------------------------------------------------------------------------
// POST /api/v1/libs/:id/versions
// ---------------------------------------------------------------------------
export const POST: RequestHandler = async ({ params, request }) => {
try {
const { repoService, versionService } = getServices();
const repositoryId = decodeURIComponent(params.id);
const repo = repoService.get(repositoryId);
if (!repo) {
throw new NotFoundError(`Repository ${repositoryId} not found`);
}
let body: { tag?: unknown; title?: unknown; autoIndex?: unknown };
try {
body = await request.json();
} catch {
throw new InvalidInputError('Request body must be valid JSON', [
{ field: 'body', message: 'Invalid JSON' }
]);
}
const tag = body.tag;
if (!tag || typeof tag !== 'string' || !tag.trim()) {
throw new InvalidInputError('tag is required', [
{ field: 'tag', message: 'tag must be a non-empty string' }
]);
}
const title = typeof body.title === 'string' ? body.title : undefined;
const autoIndex = body.autoIndex === true;
const version = versionService.add(repositoryId, tag.trim(), title);
let job: { id: string; status: string } | undefined;
if (autoIndex) {
const indexingJob = repoService.createIndexingJob(repositoryId, version.id);
job = { id: indexingJob.id, status: indexingJob.status };
}
return json({ version, ...(job ? { job } : {}) }, { status: 201 });
} catch (err) {
return handleServiceError(err);
}
};
// ---------------------------------------------------------------------------
// OPTIONS preflight
// ---------------------------------------------------------------------------
export const OPTIONS: RequestHandler = () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
};

View File

@@ -0,0 +1,54 @@
/**
* DELETE /api/v1/libs/:id/versions/:tag — remove a version and all its snippets
*/
import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client';
import { RepositoryService } from '$lib/server/services/repository.service';
import { VersionService } from '$lib/server/services/version.service';
import { handleServiceError, NotFoundError } from '$lib/server/utils/validation';
function getServices() {
const db = getClient();
return {
repoService: new RepositoryService(db),
versionService: new VersionService(db)
};
}
// ---------------------------------------------------------------------------
// DELETE /api/v1/libs/:id/versions/:tag
// ---------------------------------------------------------------------------
export const DELETE: RequestHandler = ({ params }) => {
try {
const { repoService, versionService } = getServices();
const repositoryId = decodeURIComponent(params.id);
const tag = decodeURIComponent(params.tag);
const repo = repoService.get(repositoryId);
if (!repo) {
throw new NotFoundError(`Repository ${repositoryId} not found`);
}
versionService.remove(repositoryId, tag);
return new Response(null, { status: 204 });
} catch (err) {
return handleServiceError(err);
}
};
// ---------------------------------------------------------------------------
// OPTIONS preflight
// ---------------------------------------------------------------------------
export const OPTIONS: RequestHandler = () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
};

View File

@@ -0,0 +1,60 @@
/**
* POST /api/v1/libs/:id/versions/:tag/index — queue an indexing job for a specific version
*/
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getClient } from '$lib/server/db/client';
import { RepositoryService } from '$lib/server/services/repository.service';
import { VersionService } from '$lib/server/services/version.service';
import { handleServiceError, NotFoundError } from '$lib/server/utils/validation';
function getServices() {
const db = getClient();
return {
repoService: new RepositoryService(db),
versionService: new VersionService(db)
};
}
// ---------------------------------------------------------------------------
// POST /api/v1/libs/:id/versions/:tag/index
// ---------------------------------------------------------------------------
export const POST: RequestHandler = ({ params }) => {
try {
const { repoService, versionService } = getServices();
const repositoryId = decodeURIComponent(params.id);
const tag = decodeURIComponent(params.tag);
const repo = repoService.get(repositoryId);
if (!repo) {
throw new NotFoundError(`Repository ${repositoryId} not found`);
}
const version = versionService.getByTag(repositoryId, tag);
if (!version) {
throw new NotFoundError(`Version ${tag} not found for repository ${repositoryId}`);
}
const job = repoService.createIndexingJob(repositoryId, version.id);
return json({ job }, { status: 202 });
} catch (err) {
return handleServiceError(err);
}
};
// ---------------------------------------------------------------------------
// OPTIONS preflight
// ---------------------------------------------------------------------------
export const OPTIONS: RequestHandler = () => {
return new Response(null, {
status: 204,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization'
}
});
};