/** * Repository model for Stack.Gallery Registry */ import * as plugins from '../plugins.ts'; import type { IRepository, TRepositoryVisibility, TRegistryProtocol } from '../interfaces/auth.interfaces.ts'; import { getDb } from './db.ts'; @plugins.smartdata.Collection(() => getDb()) export class Repository extends plugins.smartdata.SmartDataDbDoc 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 { // 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 { return await Repository.getInstance({ organizationId, name: name.toLowerCase(), protocol, }); } /** * Get all repositories in an organization */ public static async getOrgRepositories(organizationId: string): Promise { return await Repository.getInstances({ organizationId, }); } /** * Get all public repositories */ public static async getPublicRepositories(protocol?: TRegistryProtocol): Promise { const query: Record = { visibility: 'public' }; if (protocol) { query.protocol = protocol; } return await Repository.getInstances(query); } /** * Increment download count */ public async incrementDownloads(): Promise { 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 { this.updatedAt = new Date(); if (!this.id) { this.id = await Repository.getNewId(); } } }