Files
registry/ts/models/organization.ts

139 lines
3.6 KiB
TypeScript
Raw Normal View History

/**
* Organization model for Stack.Gallery Registry
*/
import * as plugins from '../plugins.ts';
import type {
IOrganization,
IOrganizationSettings,
TOrganizationPlan,
} from '../interfaces/auth.interfaces.ts';
import { getDb } from './db.ts';
const DEFAULT_SETTINGS: IOrganizationSettings = {
requireMfa: false,
allowPublicRepositories: true,
defaultRepositoryVisibility: 'private',
allowedProtocols: ['oci', 'npm', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'],
};
@plugins.smartdata.Collection(() => getDb())
export class Organization
extends plugins.smartdata.SmartDataDbDoc<Organization, Organization>
implements IOrganization
{
@plugins.smartdata.unI()
public id: string = '';
@plugins.smartdata.svDb()
@plugins.smartdata.searchable()
@plugins.smartdata.index({ unique: true })
public name: string = ''; // URL-safe slug
@plugins.smartdata.svDb()
@plugins.smartdata.searchable()
public displayName: string = '';
@plugins.smartdata.svDb()
public description?: string;
@plugins.smartdata.svDb()
public avatarUrl?: string;
@plugins.smartdata.svDb()
@plugins.smartdata.index()
public plan: TOrganizationPlan = 'free';
@plugins.smartdata.svDb()
public settings: IOrganizationSettings = DEFAULT_SETTINGS;
@plugins.smartdata.svDb()
public billingEmail?: string;
@plugins.smartdata.svDb()
public isVerified: boolean = false;
@plugins.smartdata.svDb()
public verifiedDomains: string[] = [];
@plugins.smartdata.svDb()
public storageQuotaBytes: number = 5 * 1024 * 1024 * 1024; // 5GB default
@plugins.smartdata.svDb()
public usedStorageBytes: 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 organization
*/
public static async createOrganization(data: {
name: string;
displayName: string;
description?: string;
createdById: string;
}): Promise<Organization> {
// Validate name (URL-safe)
const nameRegex = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/;
if (!nameRegex.test(data.name)) {
throw new Error(
'Organization name must be lowercase alphanumeric with optional hyphens'
);
}
const org = new Organization();
org.id = await Organization.getNewId();
org.name = data.name.toLowerCase();
org.displayName = data.displayName;
org.description = data.description;
org.createdById = data.createdById;
org.settings = { ...DEFAULT_SETTINGS };
org.createdAt = new Date();
org.updatedAt = new Date();
await org.save();
return org;
}
/**
* Find organization by name (slug)
*/
public static async findByName(name: string): Promise<Organization | null> {
return await Organization.getInstance({ name: name.toLowerCase() });
}
/**
* Check if storage quota is exceeded
*/
public hasStorageAvailable(additionalBytes: number): boolean {
if (this.storageQuotaBytes < 0) return true; // Unlimited
return this.usedStorageBytes + additionalBytes <= this.storageQuotaBytes;
}
/**
* Update storage usage
*/
public async updateStorageUsage(deltaBytes: number): Promise<void> {
this.usedStorageBytes = Math.max(0, this.usedStorageBytes + deltaBytes);
await this.save();
}
/**
* Lifecycle hook: Update timestamps before save
*/
public async beforeSave(): Promise<void> {
this.updatedAt = new Date();
if (!this.id) {
this.id = await Organization.getNewId();
}
}
}