Files
registry/ts/models/repository.ts

156 lines
4.0 KiB
TypeScript

/**
* Repository model for Stack.Gallery Registry
*/
import * as plugins from '../plugins.ts';
import type { IRepository, TRepositoryVisibility, TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
import { db } from './db.ts';
@plugins.smartdata.Collection(() => db)
export class Repository extends plugins.smartdata.SmartDataDbDoc<Repository, Repository> implements IRepository {
@plugins.smartdata.unI()
public id: string = '';
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public organizationId: string = '';
@plugins.smartdata.svDb()
@plugins.smartdata.searchable()
public name: string = '';
@plugins.smartdata.svDb()
public description?: string;
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public protocol: TRegistryProtocol = 'npm';
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public visibility: TRepositoryVisibility = 'private';
@plugins.smartdata.svDb()
public storageNamespace: string = '';
@plugins.smartdata.svDb()
public downloadCount: number = 0;
@plugins.smartdata.svDb()
public starCount: number = 0;
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public createdAt: Date = new Date();
@plugins.smartdata.svDb()
public updatedAt: Date = new Date();
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public createdById: string = '';
/**
* Create a new repository
*/
public static async createRepository(data: {
organizationId: string;
name: string;
description?: string;
protocol: TRegistryProtocol;
visibility?: TRepositoryVisibility;
createdById: string;
}): Promise<Repository> {
// Validate name
const nameRegex = /^[a-z0-9]([a-z0-9._-]*[a-z0-9])?$/;
if (!nameRegex.test(data.name.toLowerCase())) {
throw new Error('Repository name must be lowercase alphanumeric with optional dots, hyphens, or underscores');
}
// Check for duplicate name in org + protocol
const existing = await Repository.getInstance({
organizationId: data.organizationId,
name: data.name.toLowerCase(),
protocol: data.protocol,
});
if (existing) {
throw new Error('Repository with this name and protocol already exists');
}
const repo = new Repository();
repo.id = await Repository.getNewId();
repo.organizationId = data.organizationId;
repo.name = data.name.toLowerCase();
repo.description = data.description;
repo.protocol = data.protocol;
repo.visibility = data.visibility || 'private';
repo.storageNamespace = `${data.protocol}/${data.organizationId}/${data.name.toLowerCase()}`;
repo.createdById = data.createdById;
repo.createdAt = new Date();
repo.updatedAt = new Date();
await repo.save();
return repo;
}
/**
* Find repository by org, name, and protocol
*/
public static async findByName(
organizationId: string,
name: string,
protocol: TRegistryProtocol
): Promise<Repository | null> {
return await Repository.getInstance({
organizationId,
name: name.toLowerCase(),
protocol,
});
}
/**
* Get all repositories in an organization
*/
public static async getOrgRepositories(organizationId: string): Promise<Repository[]> {
return await Repository.getInstances({
organizationId,
});
}
/**
* Get all public repositories
*/
public static async getPublicRepositories(protocol?: TRegistryProtocol): Promise<Repository[]> {
const query: Record<string, unknown> = { visibility: 'public' };
if (protocol) {
query.protocol = protocol;
}
return await Repository.getInstances(query);
}
/**
* Increment download count
*/
public async incrementDownloads(): Promise<void> {
this.downloadCount += 1;
await this.save();
}
/**
* Get full path (org/repo)
*/
public getFullPath(orgName: string): string {
return `${orgName}/${this.name}`;
}
/**
* Lifecycle hook
*/
public async beforeSave(): Promise<void> {
this.updatedAt = new Date();
if (!this.id) {
this.id = await Repository.getNewId();
}
}
}