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,134 @@
/**
* VersionService — CRUD operations for repository_versions.
*
* Operates directly on the raw better-sqlite3 client for synchronous queries,
* matching the pattern used by RepositoryService.
*/
import type Database from 'better-sqlite3';
import type { RepositoryVersion } from '$lib/types';
import { AlreadyExistsError, NotFoundError } from '$lib/server/utils/validation';
export class VersionService {
constructor(private readonly db: Database.Database) {}
/**
* List all versions for a repository, newest first.
*/
list(repositoryId: string): RepositoryVersion[] {
return this.db
.prepare(
`SELECT * FROM repository_versions
WHERE repository_id = ?
ORDER BY created_at DESC`
)
.all(repositoryId) as RepositoryVersion[];
}
/**
* Add a new version record for a repository.
* The version ID follows the convention: {repositoryId}/{tag}
*
* @throws NotFoundError when the parent repository does not exist
* @throws AlreadyExistsError when the tag is already registered
*/
add(repositoryId: string, tag: string, title?: string): RepositoryVersion {
// Verify parent repository exists.
const repo = this.db
.prepare(`SELECT id FROM repositories WHERE id = ?`)
.get(repositoryId) as { id: string } | undefined;
if (!repo) {
throw new NotFoundError(`Repository ${repositoryId} not found`);
}
const id = `${repositoryId}/${tag}`;
// Check for collision.
const existing = this.getByTag(repositoryId, tag);
if (existing) {
throw new AlreadyExistsError(`Version ${tag} already exists for repository ${repositoryId}`);
}
const now = Math.floor(Date.now() / 1000);
this.db
.prepare(
`INSERT INTO repository_versions
(id, repository_id, tag, title, state, total_snippets, indexed_at, created_at)
VALUES (?, ?, ?, ?, 'pending', 0, NULL, ?)`
)
.run(id, repositoryId, tag, title ?? null, now);
return this.db
.prepare(`SELECT * FROM repository_versions WHERE id = ?`)
.get(id) as RepositoryVersion;
}
/**
* Delete a version and all associated documents / snippets (cascades via FK).
*
* @throws NotFoundError when the version does not exist
*/
remove(repositoryId: string, tag: string): void {
const version = this.getByTag(repositoryId, tag);
if (!version) {
throw new NotFoundError(`Version ${tag} not found for repository ${repositoryId}`);
}
this.db
.prepare(`DELETE FROM repository_versions WHERE repository_id = ? AND tag = ?`)
.run(repositoryId, tag);
}
/**
* Get a single version by tag.
* Returns `null` when not found.
*/
getByTag(repositoryId: string, tag: string): RepositoryVersion | null {
return (
(this.db
.prepare(
`SELECT * FROM repository_versions WHERE repository_id = ? AND tag = ?`
)
.get(repositoryId, tag) as RepositoryVersion | undefined) ?? null
);
}
/**
* Register multiple versions from a trueref.json `previousVersions` array.
* Silently skips tags that are already registered (idempotent).
* All new records are created with state = 'pending'.
*
* @throws NotFoundError when the parent repository does not exist
*/
registerFromConfig(
repositoryId: string,
previousVersions: { tag: string; title: string }[]
): RepositoryVersion[] {
// Verify parent repository exists.
const repo = this.db
.prepare(`SELECT id FROM repositories WHERE id = ?`)
.get(repositoryId) as { id: string } | undefined;
if (!repo) {
throw new NotFoundError(`Repository ${repositoryId} not found`);
}
const registered: RepositoryVersion[] = [];
for (const { tag, title } of previousVersions) {
const existing = this.getByTag(repositoryId, tag);
if (existing) {
// Already registered — skip silently.
registered.push(existing);
continue;
}
const version = this.add(repositoryId, tag, title);
registered.push(version);
}
return registered;
}
}