Source: api/subscription/shopify.js

import { CACHE, ShopIdService } from 'cpb-api';
import { isIterable, isObject, stringifyJSON, title } from 'cpb-common';
import Datastore from 'cpb-datastore';
import Shopify from 'cpb-shopify';
/**
 * @module cpb-api/subscription.shopify
 * @description
 * ### Shopify Subscription Management
 */

/**
 * @exports  module:cpb-api/subscription/shopify
 */
export default {
  /**
   * @property {CACHE} CACHE
   */
  CACHE,
  settings: {
    kind: 'custom-product-builder-production-tokens',
    autoLimit: true,
  },
  /**
   * get uninstalled stores data
   * @returns {Promise<Map>}
   */
  async getUninstalledStores() {
    const data = await this.getTokens({
      filter: { uninstalled: true },
    });
    return this.fillUninstalledStoresCache(data);
  },

  /**
   * populates cache for Uninstalled stores with given data
   * @param {object[]} data
   * @return {*}
   */
  fillUninstalledStoresCache(data) {
    const name = 'uninstalledStores',
      key = 'shopName';
    return this.fillCache({ name, data, key });
  },

  /**
   * populates cache for installed stores with given data
   * @param {object[]} data
   * @return {*}
   */
  fillInstalledStoresCache(data) {
    return this.fillCache({ name: 'installedStores', data, key: 'shopName' });
  },

  /**
   *  Updates the CACHE[name][entry[key]] = entry;
   * @param {string} name - cached entity name
   * @param {object|object[]} data - data to stor by the `key`
   * @param {string} key - object key to use for lookup (usually **shopName**)
   * @return {*} CACHE[name] -  cached entity map
   */
  fillCache({ name, data, key }) {
    if (!key) throw new TypeError('!key');
    if (!data) throw new TypeError('!data');
    if (!name) throw new TypeError('!name');

    CACHE[name] = CACHE[name] || new Map();

    if (isIterable(data)) {
      for (const entry of data) CACHE[name][entry[key]] = entry;
    } else if (isObject(data)) {
      for (const [, entry] of Object.entries(data)) CACHE[name][entry[key]] = entry;
    } else console.error(`CACHE_IS_NOT_ITERABLE_OR_OBJECT[${name}]`, data);
    return CACHE[name];
  },

  /**
   * @description
   * ### Get installed shopify shops information from the datastore
   * If the `shopName` is given - retrieves a particular record. Otherwise, returns a full list.
   * @param {?string} [shopName]
   * @return {Promise<module:cpb-api/subscription/shopify.CACHE.installedStores|Map>}
   */
  async getInstalledStores(shopName) {
    const key = 'shopName',
      params = {
        filter: [
          ['uninstalled', false],
          //          ['paymentProblem', false],
        ],
      };
    if (shopName) params.filter.push([key, shopName]);

    const data = await this.getTokens({ ...this.settings, ...params });

    return this.fillCache({ name: 'installedStores', data, key });
  },

  //  createQueryFilter: Datastore.createQueryFilter,
  /**
   * ### Get Shopify Authentication Token record  from the datastore.
   * @param {object} [filterObject={}]
   * @param {*} fetch
   * @return {Promise<object>} tpken
   * @example
   * const token = Shopify.getToken({shopName: '02bp2018'}, true);
   * console.info(token);
   *  {
   *     shopName: '02bp2018',
   *     shopID: null,
   *     paymentProblem: false,
   *     datastore_inserted_at: 2021-12-30T07:51:27.128Z,
   *     uninstalled: false,
   *     info: '!fetch',
   *     datastore_updated_at: 2022-01-13T04:34:56.816Z,
   *     planName: 'default',
   *     token: '42b71e125874a649225b191f23780d93',
   *     error: 'GET_SHOP_ID',
   *     [Symbol(KEY)]: Key {
   *       namespace: undefined,
   *       name: '02bp2018',
   *       kind: 'custom-product-builder-production-tokens',
   *       path: [Getter]
   *     }
   */
  async getToken(filter = {}, fetch) {
    CACHE.tokens = CACHE.tokens || new Map();
    const querySettings = { orderBy: 'shopName', filter },
      cacheKey = stringifyJSON(querySettings);
    //    title('getToken', { cacheKey, arguments });
    if (!fetch && CACHE.tokens.has(cacheKey)) return CACHE.tokens.get(cacheKey);
    const tokens = await this.getTokens(querySettings, fetch);

    if (tokens.length > 1) {
      const keys = Datastore.Keys.get(tokens);
      // proper record is one which key has the name attribute instead of id
      const properKey = keys.find(k => k.name);
      console.warn(`[api/subscription/getToken][${filter.shopName}] MULTIPLE_TOKENS_FOUND: ${tokens.length}`, keys, properKey);

      if (properKey) {
        const forRemoval = [];
        for (const key of keys) if (!Datastore.Keys.compare(key, properKey)) forRemoval.push(key);
        Datastore.delete({ keys: forRemoval }).catch(console.error);
      }
    }
    if (!tokens.length) console.error(`[api/subscription/getToken][${filter.shopName}] NO_TOKENS_FOUND`);
    return tokens.length ? tokens[0] : undefined;
  },
  /**
   * get the shops authentication tokens records from the datastore
   * @param opts
   * @param fetch
   * @return {Promise<Array()>}
   */
  async getTokens(opts = {}, fetch) {
    CACHE.tokens = CACHE.tokens || new Map();
    const tokens = [],
      stats = {
        errors: 0,
        shops: 0,
      },
      querySettings = { ...this.settings, ...opts },
      cacheKey = stringifyJSON(querySettings);
    //    title('Subscriptions.Shopify.getTokens', { opts, fetch, cacheKey, querySettings });
    if (!fetch && CACHE.tokens.has(cacheKey)) return CACHE.tokens.get(cacheKey);

    //    const datastoreTokens = await Datastore.query(querySettings);
    //    const storesFromStatsMap = ShopIdService.ids;
    //    console.debug({ storesFromTokens });
    for (const tokenInfo of await Datastore.query(querySettings)) {
      if (!tokenInfo.shopName) {
        console.error('[cpb-api/subscription/shopify/getTokens] ERROR_NO_TOKEN_SHOPNAME', { ...arguments });
        continue;
      }
      if (!tokenInfo.token) {
        console.error(`[cpb-api/subscription/shopify/getTokens][${tokenInfo.shopName}] ERROR_NO_TOKEN`);
        continue;
      }
      //      const shopName = tokenInfo.shopName;
      let { id: shopId, name: shopName } = await ShopIdService.getIdName(tokenInfo.shopName);
      //      let shopId = tokenInfo.shopID || storesFromStatsMap.get(shopName);

      if (shopId && !tokenInfo.shopID) {
        stats.shops++;
        tokenInfo.shopID = shopId;
        tokenInfo.uninstalled = false;
        //        if (tokenInfo.error) {
        delete tokenInfo.error;
        delete tokenInfo.info;
        delete tokenInfo._DEBUG;
        Shopify.saveToken(tokenInfo).catch(console.error);
        //        }
        tokens.push(tokenInfo);
        continue;
      }

      if (fetch) {
        //        paymentProblem = (tokenInfo.paymentProblem || false).toString(),
        try {
          if (!shopId) shopId = await Shopify.getShopData(shopName, fetch)?.id;
          console.debug(`[cpb-api/subscription/shopify/getTokens][${shopName}] fetched shopify.shopData.shopId`, {
            fetch,
            shopName,
            shopId,
          });
        } catch (error) {
          console.error(`[cpb-api/subscription/shopify/getTokens][${shopName}]`, { error });
          tokenInfo.error = error;
          tokens.push(tokenInfo);
          stats.errors++;
          await Shopify.saveToken(tokenInfo).catch(console.error);
          continue;
        }
      }
      //      else {
      //        shopId = storesFromStatsMap.get(shopName);
      //        tokenInfo.info = '!fetch.storesFromStatsMap';
      //      }

      if (!shopId) {
        tokenInfo.error = `[${tokenInfo.shopName}] ERROR_GET_SHOP_ID`;
        tokenInfo.shopID = null;
        stats.errors++;
        console.warn(tokenInfo.error, tokenInfo.shopName, { stats });
        continue;
      }

      if (shopId) {
        delete tokenInfo.error;
        tokenInfo.uninstalled = false;
        tokenInfo.shopID = +shopId;
        stats.shops++;
        console.info(`[${tokenInfo.shopName}] found shopID: ${shopId}`, { stats });
      }
      // need to update the datastore record with shopID
      //      await Shopify.saveToken(tokenInfo).catch(console.error);
      tokens.push(tokenInfo);
    }

    this.fillCache({
      name: 'tokens',
      data: tokens,
      key: cacheKey,
    });

    return tokens;
  },
};