1.1.1Updated a month ago
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;
  }
}