Source

infohub/UserGroup.js

// $Id: UserGroup.js 8978 2025-10-09 12:54:32Z jochen.hanff $
// @ts-check

'use strict';

import { RestServices } from '../tools/RestServices.js';
import { Context } from './Context.js';
// import { cacheStore } from './cache.internal.js';
import { ApiResponse } from '../tools/ApiResponse.js';

/**
 * @classdesc Group aggregate (info, avatar, permissions, users, roles).
 * @category Usermanagement
 * @property {Object} info
 * @property {string} avatar
 * @property {Object} permissions
 * @property {Object[]} userrelations
 * @property {Object[]} invitations
 * @property {string[]} users
 * @property {Object.<string, any>} roles
 */
export class UserGroup {
  constructor(userGroup = /** @type {UserGroup} */ ({})) {
    this.info = userGroup.info || {};
    this.avatar = userGroup.avatar || ''; // Base64
    this.permissions = userGroup.permissions || {}; // GroupPermissions enum
    this.userrelations = userGroup.userrelations || []; // UserRelation[]
    this.invitations = userGroup.invitations || []; // Invitation[]
    this.users = userGroup.users || []; // string[]
    this.roles = userGroup.roles || {}; // Record<string, TRoleInfo>
  }

  /**
   * Set the user group from a JSON object.
   * @param {UserGroup} userGroup
   */
  setFromJSON(userGroup = /** @type {UserGroup} */ ({})) {
    this.info = userGroup.info || {};
    this.avatar = userGroup.avatar || ''; // Base64
    this.permissions = userGroup.permissions || {}; // GroupPermissions enum
    this.userrelations = userGroup.userrelations || []; // UserRelation[]
    this.invitations = userGroup.invitations || []; // Invitation[]
    this.users = userGroup.users || []; // string[]
    this.roles = userGroup.roles || {}; // Record<string, TRoleInfo>
  }

  /**
   * Create a user group from a JSON object.
   * @param {UserGroup} userGroup
   * @returns {UserGroup}
   */
  static createFromJSON(userGroup = /** @type {UserGroup} */ ({})) {
    const obj = new UserGroup();
    obj.setFromJSON(userGroup);
    return obj;
  }

  /**
   * Convert the user group to a JSON object.
   * @returns {object}
   */
  toJSON() {
    return Object.fromEntries(Object.entries(this).filter(([_, v]) => typeof v !== 'function'));
  }

  /**
   * Set the avatar of the user group.
   * @param {string} avatar - The avatar base64 string
   * @returns {void}
   */
  setAvatar(avatar) {
    this.avatar = avatar;
  }

  /**
   * Get the avatar of the user group.
   * @returns {string} - The avatar base64 string
   */
  getAvatar() {
    return this.avatar;
  }
}

/**
 * @classdesc Service layer for CRUD operations on user groups.
 * @category Usermanagement
 */
export class UserGroupServices {
  // #cache = cacheStore;

  /**
   * @since 09.2025, aab
   * @param {Context} ctx - Context instance
   */
  constructor(ctx = /** @type {Context} */ ({})) {
    this._ctx = ctx;
  }

  /**
   * Create a new user group.
   * @param {string} projCatId - The project or catalogue ID
   * @param {string} type - The type of group (project or catalogue)
   * @param {object} [data] - Group payload
   * @param {string} [data.name] - The name of the group
   * @param {string} [data.description] - The description of the group
   * @param {object} [data.permissions] - The permissions of the group
   * @param {string[]} [data.users] - The users of the group
   * @param {string[]} [data.roles] - The roles of the group
   * @param {string[]} [data.userrelations] - The user relations of the group
   * @param {string[]} [data.invitations] - The invitations of the group
   * @returns {Promise<ApiResponse>}
   */
  async createGroup(projCatId, type, data) {
    if (!data) {
      return new ApiResponse(400, null);
    }
    if (!data.name || !type) {
      return new ApiResponse(400, null);
    }

    let url = '';
    try {
      if (!projCatId) {
        // Global group.
        url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups`;
      } else if (type === 'project') {
        // Project group.
        url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/projects/${projCatId}/usergroups`;
      } else if (type === 'catalogue') {
        // Catalogue group.
        url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/catalogues/${projCatId}/usergroups`;
      } else {
        return new ApiResponse(400, null);
      }

      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.POST, url, data);

      if (res.status < 300) {
        // look up in cache
        // const scope = this._ctx.getScope?.() || '';
        // this.#cache?.deleteWhere?.((key) => key.startsWith(`scope:${scope}|group:`) || key.startsWith(`scope:${scope}|groups:`));

        return new ApiResponse(200, res.data);
      } else {
        return new ApiResponse(res.status, null);
      }
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * Update an existing user group.
   * @param {string} groupid - The group ID
   * @param {object} [data] - Group payload
   * @param {string} [data.name] - The name of the group
   * @param {string} [data.description] - The description of the group
   * @param {object} [data.permissions] - The permissions of the group
   * @param {string[]} [data.users] - The users of the group
   * @param {string[]} [data.roles] - The roles of the group
   * @param {string[]} [data.userrelations] - The user relations of the group
   * @param {string[]} [data.invitations] - The invitations of the group
   *
   * @returns {Promise<ApiResponse>}
   */
  async updateGroup(groupid, data) {
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.PATCH, url, data);
      if (res.status < 300) {
        // look up in cache
        // const scope = this._ctx.getScope?.() || '';
        // this.#cache?.markNeedUpdateWhere?.((key) => key === `scope:${scope}|group:${groupid}` || key.startsWith(`scope:${scope}|groups:`));

        return new ApiResponse(200, res.data);
      } else {
        return new ApiResponse(res.status, null);
      }
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * Delete a user group.
   * @param {string} groupid - The group ID
   * @returns {Promise<ApiResponse>}
   */
  async deleteGroup(groupid) {
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.DELETE, url);
      if (res.status < 300) {
        // look up in cache
        // const scope = this._ctx.getScope?.() || '';
        // this.#cache?.deleteWhere?.((key) => key === `scope:${scope}|group:${groupid}` || key.startsWith(`scope:${scope}|groups:`));

        return new ApiResponse(200, res.data);
      } else {
        return new ApiResponse(res.status, null);
      }
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * Add a user to a group.
   * @param {string} groupid - The group ID
   * @param {object} [data] - Payload
   * @param {string} [data.parentid] - The parent ID
   * @param {object[]} [data.userrelations] - The user relations of the group
   * @param {string} [data.userrelations[].userid] - The user ID
   * @param {string[]} [data.userrelations[].roles] - The roles of the user
   * @param {string} [data.userrelations[].companyid] - The company ID
   * @param {object} [data.userrelations[].userconfig] - The user config
   *
   * @returns {Promise<ApiResponse>}
   */
  async addUserToGroup(groupid, data) {
    if (!data) {
      return new ApiResponse(400, null);
    }
    if (!data.userrelations) {
      return new ApiResponse(400, null);
    }
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}/userrelations`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.POST, url, data);
      return res.status < 300 ? new ApiResponse(200, res.data) : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * Update a user's membership in a group.
   * @param {string} userrelationId - The user relation ID
   * @param {object} [data] - Payload
   * @param {string} [data.userid] - The user ID
   * @param {string[]} [data.roles] - The roles of the user
   * @param {string} [data.companyid] - The company ID
   * @param {object} [data.userconfig] - The user config
   *
   * @returns {Promise<ApiResponse>} - The response object
   */
  async updateUserInGroup(userrelationId, data) {
    if (!data || !userrelationId) {
      return new ApiResponse(400, null);
    }
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/userrelations/${userrelationId}`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.PATCH, url, data);
      return res.status < 300 ? new ApiResponse(200, res.data) : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * Remove a user from a group.
   * @param {string} groupid - The group ID
   * @param {object} [data] - Payload
   * @param {string} [data.userid] - The user ID
   * @param {string} [data.roleid] - The role ID
   * @param {string} [data.companyid] - The company ID
   *
   * @returns {Promise<ApiResponse>} - The response object
   */
  async removeUserFromGroup(groupid, data) {
    if (!data) {
      return new ApiResponse(400, null);
    }
    if (!data.userid && !data.roleid && !data.companyid) {
      return new ApiResponse(400, null);
    }
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}/userrelations`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.DELETE, url, data);
      return res.status < 300 ? new ApiResponse(200, res.data) : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null, error.message);
    }
  }

  /**
   * Invite a user to a group (creates a pending invitation).
   * @param {string} groupid - The group ID
   * @param {object} [data] - Payload
   * @param {string} [data.parentid] - The parent ID
   * @param {string} [data.message] - The message
   * @param {string} [data.subject] - The subject
   * @param {object[]} [data.userrelations] - The user relations of the group
   * @param {string} [data.userrelations[].userid] - The user ID
   * @param {string[]} [data.userrelations[].roles] - The roles of the user
   * @param {string} [data.userrelations[].companyid] - The company ID
   * @param {object} [data.userrelations[].userconfig] - The user config
   *
   * @returns {Promise<ApiResponse>} - The response object
   */
  async inviteUserToGroup(groupid, data) {
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}/invitations`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.POST, url, data);
      return res.status < 300 ? new ApiResponse(200, res.data) : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * Fetch a single group by ID.
   * @param {string} groupid - The group ID
   * @param {object} [query] - The query parameters
   * @param {string} [query.filter] - The filter
   * @param {string} [query.members] - The members
   * @returns {Promise<ApiResponse>} - The response object
   */
  async getGroupById(groupid, query) {
    const queryString = this.#buildQueryString(query);
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}${queryString}`;

    try {
      // make the api call
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.GET, url);
      return res.status < 300
        ? new ApiResponse(200, this.#processUserGroupObject([res.data]))
        : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * List groups in a project.
   * @param {string} projectid - The project ID
   * @param {object} [query] - The query parameters
   * @param {string} [query.filter] - The filter
   * @param {string} [query.members] - The members
   * @returns {Promise<ApiResponse>}
   */
  async getGroupsInProject(projectid, query) {
    if (!projectid) {
      return new ApiResponse(400, []);
    }
    const queryString = this.#buildQueryString(query);
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/projects/${projectid}/usergroups${queryString}`;
    try {
      // make the api call
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.GET, url);
      const list = Array.isArray(res.data) ? res.data : [];
      return res.status < 300
        ? new ApiResponse(200, list.length > 0 ? this.#processUserGroupObject(res.data ?? []) : null)
        : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null, error.message);
    }
  }

  /**
   * List groups in a catalogue.
   * @param {string} catalogueid - The catalogue ID
   * @param {object} [query] - The query parameters
   * @param {string} [query.filter] - The filter
   * @param {string} [query.members] - The members
   * @returns {Promise<ApiResponse>}
   */
  async getGroupsInCatalogue(catalogueid, query) {
    if (!catalogueid) {
      return new ApiResponse(400, null);
    }
    const queryString = this.#buildQueryString(query);
    const url = `${this._ctx.getServiceUrl(
      Context.SERVICE.USR,
    )}/${this._ctx.getScope()}/catalogues/${catalogueid}/usergroups${queryString}`;

    try {
      // make the api call
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.GET, url);
      const list = Array.isArray(res.data) ? res.data : [];
      return res.status < 300
        ? new ApiResponse(200, list.length > 0 ? this.#processUserGroupObject(res.data ?? []) : null)
        : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null, error.message);
    }
  }

  /**
   * List global (scope‑wide) groups.
   * @param {object} [query] - The query parameters
   * @param {string} [query.filter] - The filter
   * @param {string} [query.members] - The members
   * @returns {Promise<ApiResponse>}
   */
  async getGlobalGroups(query) {
    const queryString = this.#buildQueryString(query);
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups${queryString}`;

    try {
      // make the api call
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.GET, url);
      const list = Array.isArray(res.data) ? res.data : [];
      return res.status < 300
        ? new ApiResponse(200, list.length > 0 ? this.#processUserGroupObject(res.data ?? []) : null)
        : new ApiResponse(res.status, null, res.message);
    } catch (error) {
      return new ApiResponse(500, null, error.message);
    }
  }

  /**
   * List groups across projects, catalogues, and/or global scope.
   * @param {string[]} projectList - The project ids
   * @param {string[]} catalogueList - The catalogue ids
   * @param {boolean} globalGroups - Whether to include global user groups
   * @param {object} [query] - The query parameters
   * @param {string} [query.filter] - The filter
   * @param {string} [query.members] - The members
   * @param {boolean} withAvatar - Whether to include the avatar of the groups
   * @returns {Promise<ApiResponse>} - The response object
   */
  async getGroups( projectList = [], catalogueList = [], globalGroups = false, query, withAvatar = false) {
    if (projectList.length === 0 && catalogueList.length === 0 && !globalGroups) {
      return new ApiResponse(400, []);
    }
    let groupList = [];
    try {
      if (projectList.length > 0) {
        // get the groups in the projects
        const projectGroups = await Promise.all(
          projectList.map(async (projectId) => {
            const res = await this.getGroupsInProject(projectId, query);
            return res.status < 300 ? res.data : null;
          }),
        );
        // if the groups are found, add them to the list
        if (projectGroups.length > 0) {
          if (projectGroups.flat().length > 0) {
            groupList.push(...projectGroups.flat());
          }
        }
      }
      if (catalogueList.length > 0) {
        // get the groups in the catalogues
        const catalogueGroups = await Promise.all(
          catalogueList.map(async (catalogueId) => {
            const res = await this.getGroupsInCatalogue(catalogueId, query);
            return res.status < 300 ? res.data : null;
          }),
        );
        // if the groups are found, add them to the list
        if (catalogueGroups.length > 0) {
          if (catalogueGroups.flat().length > 0) {
            groupList.push(...catalogueGroups.flat());
          }
        }
      }
      if (globalGroups) {
        // get the global groups
        const res = await this.getGlobalGroups(query);
        if (res.status < 300) {
          const list = Array.isArray(res.data) ? res.data : [];
          if (list.length > 0) {
            groupList.push(...list);
          }
        }
      }
      // if the avatar is requested, get the avatar for each group
      if (withAvatar) {
        const avatarList = await Promise.all(
          groupList.map(async (group) => {
            // @ts-ignore
            const res = await this.getGroupAvatar(group.info?.id);
            return res.status < 300 ? res.data : '';
          }),
        );
        // add the avatar to the group
        groupList.forEach((group, index) => {
          group.avatar = avatarList[index];
        });
      }

      // return the list of groups
      return new ApiResponse(200, groupList);
    } catch (error) {
      return new ApiResponse(500, null, error.message);
    }
  }
  
  /**
   * Fetch a group's avatar (base64 string).
   * @param {string} groupid - The group ID
   * @returns {Promise<ApiResponse>} - The response object
   */
  async getGroupAvatar(groupid) {
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}/avatar`;
    try {
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.GET, url);
      return res.status < 300 ? new ApiResponse(200, res.data || '') : new ApiResponse(res.status, '', res.message);
    } catch (error) {
      return new ApiResponse(500, '', error.message);
    }
  }

  /**
   * Update a group's avatar (base64 string).
   * @param {string} groupid - The group ID
   * @param {string} avatar - The avatar base64 string
   * @returns {Promise<ApiResponse>} - The response object
   */
  async updateGroupAvatar(groupid, avatar) {
    const url = `${this._ctx.getServiceUrl(Context.SERVICE.USR)}/${this._ctx.getScope()}/usergroups/${groupid}/avatar`;
    try {
      // make the api call
      const res = await RestServices.makeApiCall(this._ctx.getToken(), RestServices.METHODS.PUT, url, avatar);

      return res.status < 300 ? new ApiResponse(200, res.data || '') : new ApiResponse(res.status, '', res.message);
    } catch (error) {
      // return the error
      return new ApiResponse(500, '', error.message);
    }
  }

  /** \cond DOXYGEN_EXCLUDE */

  /**
   * Query string builder.
   * @param {object} [query] - Query object containing filter, members, etc.
   * @param {string} defaultMember - Default member value if not specified
   * @returns {string} - Properly formatted query string
   */
  #buildQueryString(query, defaultMember = 'info') {
    let queryString = '';
    let c = '?';
    if (query['filter']) {
      queryString += c + 'filter=' + encodeURIComponent(query['filter']);
      c = '&';
    }
    if (query['members']) {
      let membersString = query['members'];
      if (defaultMember && !membersString.includes(defaultMember)) {
        membersString = membersString + ',' + defaultMember;
      }
      queryString += c + 'members=' + encodeURIComponent(membersString);
      c = '&';
    } else if (defaultMember) {
      queryString += c + 'members=' + encodeURIComponent(defaultMember);
      c = '&';
    }
    return queryString;
  }

  /**
   * Process user group objects.
   * @param {Object[]} objects - The objects to process
   * @returns {UserGroup[]} - The processed objects
   */
  #processUserGroupObject(objects = []) {
    let groupsList = [];
    for (const object of objects) {
      groupsList.push(UserGroup.createFromJSON(object));
    }
    return groupsList;
  }
  /** \endcond */
}