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:
134
src/lib/server/services/version.service.ts
Normal file
134
src/lib/server/services/version.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user