import { Entity } from "https://viapak.xyz/@db/entity";
import { User } from "./user.ts";
import { Token } from "./token.ts";
import { join } from "node:path";
import { ValidPackageDirectories } from "../utils/valid_package_directories.ts";
import { IsValidVersionNumber } from "../utils/version_helpers.ts";
import { ScanFiles } from "../utils/scan_files.ts";
import { Sort } from "../utils/sort_utils.ts";
import { Postgres } from "https://viapak.xyz/@db/pg/mod.ts";
export const ValidateNamespacedString = (input: string): input is Viapak.Package.NamespacedString => {
if(!input.startsWith('@')) return false;
if(input.split('/').length < 2) return false;
return true;
}
export interface Package extends Viapak.Package {}
export class Package extends Entity {
constructor(params: Viapak.Package) {
super(params);
if(typeof params.events === 'string') {
this.events = JSON.parse(params.events)
}
if(typeof params.versions === 'string') {
this.versions = JSON.parse(params.versions)
}
if(typeof params.tags === 'string') {
this.tags = JSON.parse(params.tags)
}
}
get versioned_url() {
return `/${this.namespace}/${this.name}@${this.versions.current}`;
}
get url() {
if(this.versions.current && this.versions.current !== this.versions.latest) {
return `/${this.namespace}/${this.name}@${this.versions.current}`;
}
return `/${this.namespace}/${this.name}`;
}
get path() {
if(!this.versions.current) return null;
return join(this.root_path, this.versions.current);
}
get root_path() {
return join(Deno.cwd(), ValidPackageDirectories.test_packages ? 'test_packages' : 'packages', this.namespace, this.name);
}
get files() {
const scan_path = this.versions.current
? join(this.root_path, this.versions.current)
: this.root_path;
return ScanFiles(scan_path);
}
get files_and_folders() {
const scan_path = this.versions.current
? join(this.root_path, this.versions.current)
: this.root_path;
return ScanFiles(scan_path, { includeFolders: true });
}
get last_updated() {
return this.events.sort(Sort.Event.Descending)[0].timestamp;
}
get scripts() {
if(!this.path) return [];
return ScanFiles(join(this.path, '/.viapak/'), { maxDepth: 1 })
.filter(f => f.name.endsWith('.ts'))
.map(f => f.name.replace(/\.ts$/, '').toLowerCase());
}
async Clean(): Promise<Viapak.Package.Clean> {
const token_uuids = this.events.map(e => e.token);
const tokens = await Token.find({ uuid: { $in: token_uuids }});
const users = await User.find({
$relationship: {
via: Token,
match: {
uuid: { $in: token_uuids }
},
select: 'user_id'
}
});
const token_users = tokens.map(token => {
return {
token: token.uuid,
username: users.find(u => u.id == token.user_id)?.username,
}
});
return {
namespace: this.namespace,
name: this.name,
tags: this.tags,
versions: this.versions,
deprecated: this.deprecated,
scripts: this.scripts,
events: this.events.map((e: Viapak.Package.Event.ToClean): Viapak.Package.Event.Clean => {
e.user = token_users.find(t => t.token == e.token)?.username;
delete e.token;
return e;
})
}
}
static BreakLocation(location: string) {
if(!ValidateNamespacedString(location)) return null;
const [namespace, package_and_version] = location.split('/');
let [package_name, package_version] = package_and_version.split('@') as [string, string | null];
package_version ??= null;
return {
namespace,
package: {
name: package_name,
version: package_version,
version_or_latest: package_version || 'latest'
}
}
}
static async FindByTags(...tags: string[]): Promise<Package[]> {
const { rows } = await Postgres.query<Viapak.Package>(`SELECT * FROM ${this.table} WHERE tags @> $1::jsonb`, [JSON.stringify(tags)]);
if(!rows?.length) return [];
return rows.map(r => new Package(r));
}
static async FindByPath(location: string) {
const location_data = this.BreakLocation(location);
if(!location_data) return null;
const { namespace, package: { name, version_or_latest }} = location_data;
const _package = await Package.findOne({
namespace,
name
});
if(!_package) return null;
_package.versions.current = version_or_latest;
if(_package.versions.current == 'latest') _package.versions.current = _package.versions.latest;
return _package;
}
static async Lookup(location: Viapak.Package.NamespacedString) {
const _package = await this.FindByPath(location);
return await _package?.Clean() || null;
}
static Info(location: Viapak.Package.NamespacedString) {
const location_data = this.BreakLocation(location);
if(!location_data) return null;
let { namespace, package: { name, version }} = location_data;
version ??= 'latest';
const packages_dir = ValidPackageDirectories.test_packages ? 'test_packages' : 'packages';
const path = join(Deno.cwd(), packages_dir, location_data.namespace, location_data.package.name);
const versions = Deno.readDirSync(path)
.filter(e => e.isDirectory && IsValidVersionNumber(e.name))
.map(e => e.name)
.toArray()
.sort(Sort.Version);
const latest = versions[versions.length - 1] || null;
if(version == 'latest') version = latest;
if(version && !versions.includes(version)) version = null;
return {
namespace,
name,
versions: {
all: versions,
latest: versions[versions.length - 1],
current: version
} satisfies Viapak.Package.VersionMap
}
}
async addEvent(token: string, event: string) {
this.events.push({
token,
event,
timestamp: Date.now(),
version: this.versions.current!
});
await this.save();
}
async setDeprecationStatus(token: string, status: boolean) {
if(this.deprecated == status) return;
this.deprecated = status;
await this.addEvent(token, `marked as ${status ? '' : 'not '}deprecated`);
}
override async save(): Promise<this> {
const copy: any = new Package(this);
copy.tags = JSON.stringify(this.tags);
copy.events = JSON.stringify(this.events);
copy.versions = JSON.stringify(this.versions);
await super.save.apply(copy);
return this;
}
}