Files
registry/ts/models/package.ts

196 lines
5.0 KiB
TypeScript
Raw Normal View History

/**
* Package model for Stack.Gallery Registry
*/
import * as plugins from '../plugins.ts';
import type {
IPackage,
IPackageVersion,
IProtocolMetadata,
} from '../interfaces/package.interfaces.ts';
import type { TRegistryProtocol } from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
@plugins.smartdata.Collection(() => getDb())
export class Package extends plugins.smartdata.SmartDataDbDoc<Package, Package> implements IPackage {
@plugins.smartdata.unI()
public id: string = ''; // {protocol}:{org}:{name}
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public organizationId: string = '';
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public repositoryId: string = '';
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public protocol: TRegistryProtocol = 'npm';
@plugins.smartdata.svDb()
@plugins.smartdata.searchable()
@plugins.smartdata.index()
public name: string = '';
@plugins.smartdata.svDb()
@plugins.smartdata.searchable()
public description?: string;
@plugins.smartdata.svDb()
public versions: Record<string, IPackageVersion> = {};
@plugins.smartdata.svDb()
public distTags: Record<string, string> = {}; // e.g., { latest: "1.0.0" }
@plugins.smartdata.svDb()
public metadata: IProtocolMetadata = { type: 'npm' };
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public isPrivate: boolean = true;
@plugins.smartdata.svDb()
public storageBytes: number = 0;
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public downloadCount: number = 0;
@plugins.smartdata.svDb()
public starCount: number = 0;
@plugins.smartdata.svDb()
public cacheExpiresAt?: Date;
@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 = '';
/**
* Generate package ID
*/
public static generateId(protocol: TRegistryProtocol, orgName: string, name: string): string {
return `${protocol}:${orgName}:${name}`;
}
/**
* Find package by ID
*/
public static async findById(id: string): Promise<Package | null> {
return await Package.getInstance({ id });
}
/**
* Find package by protocol, org, and name
*/
public static async findByName(
protocol: TRegistryProtocol,
orgName: string,
name: string
): Promise<Package | null> {
const id = Package.generateId(protocol, orgName, name);
return await Package.findById(id);
}
/**
* Get packages in an organization
*/
public static async getOrgPackages(organizationId: string): Promise<Package[]> {
return await Package.getInstances({ organizationId });
}
/**
* Search packages
*/
public static async search(
query: string,
options?: {
protocol?: TRegistryProtocol;
organizationId?: string;
isPrivate?: boolean;
limit?: number;
offset?: number;
}
): Promise<Package[]> {
const filter: Record<string, unknown> = {};
if (options?.protocol) filter.protocol = options.protocol;
if (options?.organizationId) filter.organizationId = options.organizationId;
if (options?.isPrivate !== undefined) filter.isPrivate = options.isPrivate;
// Simple text search - in production, would use MongoDB text index
const allPackages = await Package.getInstances(filter);
// Filter by query
const lowerQuery = query.toLowerCase();
const filtered = allPackages.filter(
(pkg) =>
pkg.name.toLowerCase().includes(lowerQuery) ||
pkg.description?.toLowerCase().includes(lowerQuery)
);
// Apply pagination
const offset = options?.offset || 0;
const limit = options?.limit || 50;
return filtered.slice(offset, offset + limit);
}
/**
* Add a new version
*/
public addVersion(version: IPackageVersion): void {
this.versions[version.version] = version;
this.storageBytes += version.size;
this.updatedAt = new Date();
}
/**
* Get specific version
*/
public getVersion(version: string): IPackageVersion | undefined {
return this.versions[version];
}
/**
* Get latest version
*/
public getLatestVersion(): IPackageVersion | undefined {
const latest = this.distTags['latest'];
if (latest) {
return this.versions[latest];
}
// Fallback to most recent
const versionList = Object.keys(this.versions);
if (versionList.length === 0) return undefined;
return this.versions[versionList[versionList.length - 1]];
}
/**
* Increment download count
*/
public async incrementDownloads(version?: string): Promise<void> {
this.downloadCount += 1;
if (version && this.versions[version]) {
this.versions[version].downloads += 1;
}
await this.save();
}
/**
* Lifecycle hook
*/
public async beforeSave(): Promise<void> {
this.updatedAt = new Date();
if (!this.id) {
this.id = await Package.getNewId();
}
}
}