Source

infohub/UserGroup.js

// $Id: UserGroup.js 8636 2025-09-09 14:21:04Z amr.abdou $
// @ts-check

"use strict";

import { RestServices } from "../tools/RestServices.js";
import { cnContext } from "./cnContext.js";
import { cacheStore } from "./cache.internal.js";
import { ApiResponse } from "../tools/ApiResponse.js";
/**
 * @fileoverview
 *  - User group domain classes and services.
 *  - Types and services to manage groups and memberships.
 */

/**
 * @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 {cnContext} ctx - Context instance
   */
  constructor(ctx = /** @type {cnContext} */ ({})) {
    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(
          cnContext.SERVICE.USR
        )}/${this._ctx.getScope()}/usergroups`;
      } else if (type === "project") {
        // Project group.
        url = `${this._ctx.getServiceUrl(
          cnContext.SERVICE.USR
        )}/${this._ctx.getScope()}/projects/${projCatId}/usergroups`;
      } else if (type === "catalogue") {
        // Catalogue group.
        url = `${this._ctx.getServiceUrl(
          cnContext.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) {
        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(
      cnContext.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) {
        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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups/${groupid}`;
    try {
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.DELETE,
        url
      );
      if (res.status < 300) {
        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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups/${groupid}/userrelations`;
    try {
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.POST,
        url,
        data
      );
      if (res.status < 300) {
        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);
    }
  }

  /**
   * 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/userrelations/${userrelationId}`;
    try {
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.PATCH,
        url,
        data
      );
      if (res.status < 300) {
        const scope = this._ctx.getScope?.() || "";
        const payload = /** @type {{ id: (unknown|undefined) }} */ (
          res.data ?? {}
        );
        const gid = typeof payload.id === "string" ? payload.id : "";
        this.#cache?.markNeedUpdateWhere?.(
          (key) =>
            key === `scope:${scope}|group:${gid}` ||
            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);
    }
  }

  /**
   * 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups/${groupid}/userrelations`;
    try {
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.DELETE,
        url,
        data
      );
      if (res.status < 300) {
        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);
    }
  }

  /**
   * 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups/${groupid}/invitations`;
    try {
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.POST,
        url,
        data
      );
      if (res.status < 300) {
        const scope = this._ctx.getScope?.() || "";
        this.#cache?.markNeedUpdateWhere?.((key) =>
          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);
    }
  }

  /**
   * 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups/${groupid}${queryString}`;

    const scope = this._ctx.getScope?.() || "";
    // get the cached data
    // build the key
    const key = `scope:${scope}|group:${groupid}|q:${queryString}`;
    const cached = this.#cache?.getEntry?.(key);
    // if the cached data is not stale, return it
    if (cached && cached.needUpdate === false) {
      // return the cached data
      return new ApiResponse(200, cached.value);
    }

    try {
      // make the api call
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.GET,
        url
      );
      if (res.status < 300) {
        // create a new UserGroup object
        const processedData = this.#processUserGroupObject([res.data]);
        // cache the processed data
        this.#cache?.set?.(key, processedData, {
          tags: { type: "group", kind: "single", groupid, scope },
        });
        // return the processed data
        return new ApiResponse(200, processedData[0]);
      } else {
        return new ApiResponse(res.status, null);
      }
    } 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/projects/${projectid}/usergroups${queryString}`;

    const scope = this._ctx.getScope?.() || "";
    // build the key
    const key = `scope:${scope}|groups:project:${projectid}|q:${queryString}`;
    // get the cached data
    const cachedEntry = this.#cache?.getEntry?.(key);
    // if the cached data is not stale, return it
    if (cachedEntry && cachedEntry.needUpdate === false) {
      return new ApiResponse(200, cachedEntry.value);
    }

    try {
      // make the api call
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.GET,
        url
      );
      if (res.status < 300) {
        // process the data
        const list = Array.isArray(res.data) ? res.data : [];
        if (list.length > 0) {
          const processedData = this.#processUserGroupObject(list);

          // cache the processed data
          this.#cache?.set?.(key, processedData, {
            tags: { type: "group", kind: "list", scope, projectid },
          });
          // return the processed data
          return new ApiResponse(200, processedData);
        }
        return new ApiResponse(200, null);
      } else {
        return new ApiResponse(res.status, null);
      }
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/catalogues/${catalogueid}/usergroups${queryString}`;

    const scope = this._ctx.getScope?.() || "";
    // build the key
    const key = `scope:${scope}|groups:catalogue:${catalogueid}|q:${queryString}`;
    // get the cached data
    const cachedEntry = this.#cache?.getEntry?.(key);
    // if the cached data is not stale, return it
    if (cachedEntry && cachedEntry.needUpdate === false) {
      return new ApiResponse(200, cachedEntry.value ?? null);
    }

    try {
      // make the api call
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.GET,
        url
      );
      if (res.status < 300) {
        // process the data
        const list = Array.isArray(res.data) ? res.data : [];
        if (list.length > 0) {
          const processedData = this.#processUserGroupObject(list);
          // cache the processed data
          this.#cache?.set?.(key, processedData, {
            tags: { type: "group", kind: "list", scope, catalogueid },
          });
          // return the processed data
          return new ApiResponse(200, processedData ?? null);
        } else {
          // return an empty array
          return new ApiResponse(200, null);
        }
      } else {
        return new ApiResponse(res.status, null);
      }
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * 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(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups${queryString}`;

    const scope = this._ctx.getScope?.() || "";
    // build the key
    const key = `scope:${scope}|groups:global|q:${queryString}`;
    // get the cached data
    const cachedEntry = this.#cache?.getEntry?.(key);
    // if the cached data is not stale, return it
    if (cachedEntry && cachedEntry.needUpdate === false) {
      return new ApiResponse(200, cachedEntry.value);
    }

    try {
      // make the api call
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.GET,
        url
      );
      if (res.status < 300) {
        // process the data
        const list = Array.isArray(res.data) ? res.data : [];
        if (list.length > 0) {
          const processedData = this.#processUserGroupObject(list);
          // cache the processed data
          this.#cache?.set?.(key, processedData, {
            tags: { type: "group", kind: "list", scope, global: true },
          });
          // return the processed data
          return new ApiResponse(200, processedData);
        }

        // return an empty array
        return new ApiResponse(200, null);
      } else {
        return new ApiResponse(res.status, null);
      }
    } catch (error) {
      return new ApiResponse(500, null);
    }
  }

  /**
   * 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);
    }
  }

  /**
   * Fetch a group's avatar (base64 string).
   * @param {string} groupid - The group ID
   * @returns {Promise<ApiResponse>} - The response object
   */
  async getGroupAvatar(groupid) {
    const key = `group:avatar:${groupid}`;
    const avatar = this.#cache?.get?.(key);
    if (avatar && avatar !== "") return new ApiResponse(200, avatar);
    // Get the avatar from the server.
    const url = `${this._ctx.getServiceUrl(
      cnContext.SERVICE.USR
    )}/${this._ctx.getScope()}/usergroups/${groupid}/avatar`;
    try {
      const res = await RestServices.makeApiCall(
        this._ctx.getToken(),
        RestServices.METHODS.GET,
        url
      );
      if (res.status < 300) {
        this.#cache?.set?.(key, res.data, {
          tags: { type: "group", part: "avatar", groupid },
        });
        return new ApiResponse(200, res.data || "");
      } else {
        return new ApiResponse(res.status, "");
      }
    } catch (error) {
      return new ApiResponse(500, "");
    }
  }

  /**
   * 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(
      cnContext.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
      );
      if (res.status < 300) {
        // delete the cached data
        const key = `group:avatar:${groupid}`;
        this.#cache?.delete?.(key);
        // cache the new data
        this.#cache?.set?.(key, res.data, {
          tags: { type: "group", part: "avatar", groupid },
        });
        // return the new data
        return new ApiResponse(200, res.data || "");
      } else {
        // return the error
        return new ApiResponse(res.status, "");
      }
    } catch (error) {
      // return the error
      return new ApiResponse(500, "");
    }
  }

  /** \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 */
}