Source: models/workspace.ts

/**
 * @fileoverview Defines the Workspace class and related types for managing Shortcut workspace data.
 * This module handles workspace configuration, story management, and caching functionality.
 */

import { Story } from "./story";
import { BaseModel } from "./base";
import { MemberInfo } from "./memberInfo";
import { Workflow } from "./workflow";
import { ShortcutClient } from "@shortcut/client";

/** Base type for model attributes */
type BaseAttributes = Record<string, unknown>;

/** Interface defining the structure of workspace attributes */
interface WorkspaceAttributes extends BaseAttributes {
    name: string;
    apiToken: string;
    pendingStories: Story[];
    workflows: Workflow[];
    estimate_scale?: number[] | undefined;
    url_slug?: string | undefined;
}

/**
 * Class representing a Shortcut workspace.
 * Handles workspace data, story management, and interactions with the Shortcut API.
 * @extends BaseModel
 */
export class Workspace extends BaseModel {
    name: string;
    apiToken: string;
    client: ShortcutClient;
    pendingStories: Story[];
    workflows: Workflow[] | [];
    memberInfo: MemberInfo | undefined = undefined;
    estimate_scale: number[] | undefined;
    url_slug: string | undefined;
    
    /** Key used for caching workspace data */
    static cacheKey: string = "workspaces";

    /**
     * Creates a new Workspace instance
     * @param {WorkspaceAttributes} attributes - The workspace attributes
     */
    constructor(attributes: WorkspaceAttributes) {
        super();

        this.name = attributes.name ?? "";
        this.apiToken = attributes.apiToken ?? "";
        this.pendingStories = attributes.pendingStories ?? [];
        this.workflows = attributes.workflows ?? [];
        this.client = new ShortcutClient(attributes.apiToken);
        this.estimate_scale = attributes.estimate_scale ?? [];
        this.url_slug = attributes.url_slug ?? "";
    }
    
    /**
     * Creates Workspace instances from JSON string
     * @param {string} json - JSON string containing workspace data
     * @returns {Workspace[]} Array of Workspace instances
     */
    static fromJSON(json: string): Workspace[] {
        return JSON.parse(json).map((workspace: WorkspaceAttributes) => {
            const attributes: WorkspaceAttributes = {
                name: workspace.name,
                apiToken: workspace.apiToken,
                pendingStories: workspace.pendingStories,
                workflows: workspace.workflows,
                estimate_scale: workspace.estimate_scale,
                url_slug: workspace.url_slug
            };
            return new Workspace(attributes);
        });
    }

    /**
     * Converts workspace instance to JSON string
     * @returns {string} JSON string representation of the workspace
     */
    toJSON(): string {
        return JSON.stringify(this.toObject());
    }

    /**
     * Converts workspace instance to plain object
     * @returns {WorkspaceAttributes} Plain object representation of the workspace
     */
    toObject(): WorkspaceAttributes {
        return {
            name: this.name,
            apiToken: this.apiToken,
            pendingStories: this.pendingStories,
            workflows: this.workflows,
            estimate_scale: this.estimate_scale,
            url_slug: this.url_slug
        };
    }

    /**
     * Updates workspace attributes with new data
     * @param {Object} data - New workspace data
     * @param {MemberInfo} memberInfo - Updated member information
     */
    updateAttributes(data: { workspace2: { estimate_scale: number[] | undefined, url_slug: string | undefined }}, memberInfo: MemberInfo) {
            this.estimate_scale = data.workspace2.estimate_scale;
            this.url_slug = data.workspace2.url_slug;
            this.memberInfo = memberInfo;
    }

    /**
     * Retrieves workspaces from cache or creates new ones from API tokens
     * @param {object | undefined} apiTokens - API tokens for workspace creation
     * @returns {Promise<Workspace[]>} Array of workspace instances
     */
    static async get(apiTokens: object | undefined): Promise<Workspace[]> {
        let workspaces = Workspace.getFromCache();

        if (workspaces) {
            console.log("loaded from cache");
            for (const workspace of workspaces) {
                workspace.memberInfo = await MemberInfo.get(workspace);
                workspace.workflows = await Workflow.get(workspace);
            }

            return workspaces;
        }

        workspaces = [];
        
        if (!apiTokens) {
            return workspaces;
        } 
        
        for (const [key, value] of Object.entries(apiTokens)) {
			const workspace = new Workspace({
                name: key,
                apiToken: value as string,
                pendingStories: [],
                workflows: [],
            });
            workspace.memberInfo = await MemberInfo.get(workspace);
            workspace.workflows = await Workflow.get(workspace);
			workspaces.push(workspace);
        }

        Workspace.saveToCache(workspaces);
        
        return workspaces;
    }

    /**
     * Retrieves workspaces from cache
     * @returns {Workspace[] | undefined} Array of workspace instances or undefined if cache is empty
     */
    static getFromCache(): Workspace[] | undefined {
        const cache = Workspace.context.globalState.get(Workspace.cacheKey);
        
        if (!cache) {
            return undefined;
        }

        return Workspace.fromJSON(cache as string);
    }

    /**
     * Saves workspaces to cache
     * @param {Workspace[]} workspaces - Array of workspaces to cache
     */
    static saveToCache(workspaces: Workspace[]) {
        const object = workspaces.map(workspace => workspace.toObject());
        Workspace.context.globalState.update(Workspace.cacheKey, JSON.stringify(object));
    }

    /**
     * Deletes workspace cache and related caches
     * @param {Workspace[] | undefined} workspaces - Optional workspaces to clear related caches
     */
    static deleteCache(workspaces: Workspace[] | undefined = undefined) {
        Workspace.context.globalState.update(Workspace.cacheKey, undefined);
        
        if (workspaces) {
            for (const workspace of workspaces) {
                MemberInfo.deleteCache(workspace.name);
                Workflow.deleteCache(workspace.name);
            }
        }
    }

    /**
     * Fetches and updates pending stories for the workspace
     * @returns {Promise<void>}
     */
    async getPendingStories(): Promise<void> {
        this.pendingStories = await Story.pendingTasks(this, this.client);
    }

    /**
     * Fetches and updates workflows for the workspace
     * @returns {Promise<Workflow[]>} Array of workflows
     */
    async getWorkflows(): Promise<Workflow[]> {
        this.workflows = await Workflow.get(this);
        return this.workflows;
    }

    /**
     * Fetches and organizes stories assigned to the workspace member
     * Groups stories by workflow and workflow state
     * @returns {Promise<void>}
     */
    async getAssignedStories(): Promise<void> {
        const stories = await Story.assignedToMember(this);
        const groupedStories = stories.reduce((acc: Record<number, Record<number, Story[]>>, story) => {
            acc[story.workflow_id] ||= {};
            acc[story.workflow_id][story.workflow_state_id] ||= [];
            acc[story.workflow_id][story.workflow_state_id].push(story);
            return acc;
        }, {});

        this.workflows.forEach(workflow => {
            workflow.states.forEach(state => {
                state.stories = groupedStories[workflow.id]?.[state.id] ?? [];
            });
        });
    }
}