All files / src/models ids-resolver.js

76.73% Statements 155/202
91.42% Branches 32/35
62.5% Functions 5/8
76.73% Lines 155/202

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 12x 12x 193x 193x 26x 36x 36x 16x 16x 26x 22x 6x 6x 16x 26x 193x 38x 38x 38x 38x 15x 15x 23x 23x 16x 16x 16x 16x 7x 7x 9x 9x 38x 193x 23x 23x 23x 23x 23x 23x 23x 16x 16x 16x 16x 32x 11x 11x 32x 12x 12x 12x 12x 12x 12x 32x     32x 16x 16x 23x 193x 193x 193x 193x 193x 193x 193x 193x 193x 193x 10x 8x 8x 8x 8x 8x 8x 8x 9x 9x 9x 9x 9x 9x 9x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 8x 10x 193x 193x 193x 193x 193x 193x 193x 193x 193x                                                     193x 193x 193x 193x 193x 193x 193x 193x 193x                                   193x 193x 193x 193x 193x 193x 193x      
import { getSummary } from '@clevercloud/client/esm/api/v2/user.js';
import { loadIdsCache, writeIdsCache } from '../config/cache.js';
import { Logger } from '../logger.js';
import * as User from '../models/user.js';
import * as Organisation from './organisation.js';
import { sendToApi } from './send-to-api.js';
 
/*
This system uses a simplified representation of the summary to expose IDs links:
 
* app ID => owner ID
* add-on ID => owner ID
* real add-on ID => owner ID
* add-on ID => real add-on ID
* real add-on ID => add-on ID
 
{
  owners: {
    [appid]: [ownerId],
    [addonId]: [ownerId],
    [realId]: [ownerId],
  },
  addons: {
    [addonId]: { realId: [realId], addonId: [addonId] },
    [realId]: { realId: [realId], addonId: [addonId] },
  },
}
 */
 
export async function resolveOwnerId(id) {
  return getIdFromCacheOrSummary((ids) => ids.owners[id]);
}
 
export async function resolveAddon(addonIdOrRealId) {
  const result = await getIdFromCacheOrSummary((ids) => {
    const addon = ids.addons[addonIdOrRealId];
    if (addon == null) return null;
    const ownerId = ids.owners[addon.realId];
    return { ownerId, addonId: addon.addonId, realId: addon.realId };
  });
  if (result == null) {
    throw new Error(`Add-on ${addonIdOrRealId} does not exist`);
  }
  return result;
}
 
async function getIdFromCacheOrSummary(callback) {
  const idsFromCache = await loadIdsCache();
  const idFromCache = callback(idsFromCache);
  if (idFromCache != null) {
    return idFromCache;
  }
 
  const idsFromSummary = await getIdsFromSummary();
  await writeIdsCache(idsFromSummary);
 
  const idFromSummary = callback(idsFromSummary);
  if (idFromSummary != null) {
    return idFromSummary;
  }
 
  return null;
}
 
async function getIdsFromSummary() {
  const ids = {
    owners: {},
    addons: {},
  };
 
  const summary = await getSummary().then(sendToApi);
 
  const owners = [summary.user, ...summary.organisations];
 
  for (const owner of owners) {
    for (const app of owner.applications) {
      ids.owners[app.id] = owner.id;
    }
    for (const addon of owner.addons) {
      ids.owners[addon.id] = owner.id;
      ids.owners[addon.realId] = owner.id;
      const addonIds = { addonId: addon.id, realId: addon.realId };
      ids.addons[addon.id] = addonIds;
      ids.addons[addon.realId] = addonIds;
    }
    for (const consumer of owner.consumers) {
      ids.owners[consumer.key] = owner.id;
    }
  }
 
  return ids;
}
 
/**
 * Get the IDs and owners of found add-ons from a name, ID or real ID
 * @param {string} addonIdOrRealIdOrName
 * @param {{ orga_name?: string, orga_id?: string }} ownerNameOrId
 * @throws {Error} if no add-on is found
 * @throws {Error} if several add-ons are found
 * @returns {Object} The name, IDs and owner ID of the add-on { name, addonId, realId, ownerId }
 */
export async function findAddonsByNameOrId(addonIdOrRealIdOrName, ownerNameOrId) {
  const summary = await getSummary().then(sendToApi);
 
  Logger.debug(
    `Searching for add-on '${addonIdOrRealIdOrName}' in ${summary.user.id} and ${summary.organisations.map((org) => org.id).join(', ')}`,
  );
  const candidates = [summary.user, ...summary.organisations]
    .flatMap((owner) => owner.addons.map((addon) => ({ addon, owner })))
    .filter(({ addon, owner }) => {
      const matchOwner =
        ownerNameOrId == null || owner.id === ownerNameOrId.orga_id || owner.name === ownerNameOrId.orga_name;
      const matchAddon =
        addon.name === addonIdOrRealIdOrName ||
        addon.realId === addonIdOrRealIdOrName ||
        addon.id === addonIdOrRealIdOrName;
      return matchOwner && matchAddon;
    })
    .map(({ addon, owner }) => ({
      name: addon.name,
      addonId: addon.id,
      realId: addon.realId,
      ownerId: owner.id,
    }));
 
  Logger.debug(`Found ${candidates.length} candidate(s):`);
  for (const candidate of candidates) {
    Logger.debug(`  - ${candidate.addonId} (${candidate.ownerId})`);
  }
 
  return candidates;
}
 
/**
 * Get the IDs and owners of found add-ons from a name, ID or real ID
 * @param {string} addonIdOrRealIdOrName
 * @throws {Error} if no add-on is found
 * @throws {Error} if several add-ons are found
 * @returns {Object} The name, IDs and owner ID of the add-on { name, addonId, realId, ownerId }
 */
export async function findAddonsByAddonProvider(provider) {
  const summary = await getSummary().then(sendToApi);

  Logger.debug(
    `Searching for ${provider} add-ons in ${summary.user.id} and ${summary.organisations.map((org) => org.id).join(', ')}`,
  );
  const candidates = [summary.user, ...summary.organisations].flatMap((owner) => {
    return owner.addons
      .filter((addon) => addon.providerId === provider)
      .map((addon) => {
        return {
          name: addon.name,
          addonId: addon.id,
          realId: addon.realId,
          ownerId: owner.id,
          ownerName: owner.name,
        };
      });
  });

  Logger.debug(`Found ${candidates.length} candidate(s) for provider ${provider}:`);
  for (const candidate of candidates) {
    Logger.debug(`  - ${candidate.addonId} (${candidate.ownerId})`);
  }

  return candidates;
}
 
/**
 * Find OAuth consumers by key or name.
 * Fast path: tries the IDs cache first (works for keys).
 * Fallback: fetches the summary and searches by key or name.
 * @param {string} keyOrName
 * @returns {Promise<Array<{ ownerId: string, key: string, name: string }>>}
 */
export async function findOauthConsumersByKeyOrName(keyOrName) {
  const ownerIdFromCache = await resolveOwnerId(keyOrName);
  if (ownerIdFromCache != null) {
    return [{ ownerId: ownerIdFromCache, key: keyOrName }];
  }

  const summary = await getSummary().then(sendToApi);
  const consumers = [summary.user, ...summary.organisations].flatMap((owner) => {
    return owner.consumers.map((c) => ({ ownerId: owner.id, name: c.name, key: c.key }));
  });

  const matchByKey = consumers.find((c) => c.key === keyOrName);
  if (matchByKey) {
    return [matchByKey];
  }

  return consumers.filter((c) => c.name === keyOrName);
}
 
/**
 * Get the owner ID from an Organisation ID or name
 * @param {object} orgIdOrName The Organisation ID or name
 * @returns {Promise<string>} The owner ID
 */
export async function getOwnerIdFromOrgIdOrName(orgIdOrName) {
  return orgIdOrName != null ? Organisation.getId(orgIdOrName) : User.getCurrentId();
}