Source: shopify/webhook/index.js

import ShopIdService from 'cpb-api/shop/shopId.js';
import { isArray } from 'cpb-common';
import Datastore from 'cpb-datastore';
import Shopify from 'cpb-shopify';

const ALL_WEBHOOK_TOPICS = [
    //    'carts/create',
    //    'carts/update',
    //    'checkouts/create',
    //    'checkouts/delete',
    //    'checkouts/update',
    //    'collections/create',
    //    'collections/delete',
    //    'collections/update',
    //    'order_transactions/create',
    'orders/cancelled',
    //    'orders/create',
    //    'orders/delete',
    //    'orders/edited',
    //    'orders/fulfilled',
    //    'orders/paid',
    //    'orders/partially_fulfilled',
    //    'orders/updated',
    'products/create',
    'products/delete',
    'products/update',
    //    'refunds/create',
    'shop/update',
    //    'themes/create',
    //    'themes/delete',
    //    'themes/publish',
    //    'themes/update',
    //    'inventory_levels/connect',
    //    'inventory_levels/update',
    //    'inventory_levels/disconnect',
    //    'inventory_items/create',
    //    'inventory_items/update',
    //    'inventory_items/delete',
    //    'tender_transactions/create',
    //    'app_purchases_one_time/update',
    //    'app_subscriptions/update',
    //    'domains/create',
    //    'domains/update',
    //    'domains/destroy',
    //    'profiles/create',
    //    'profiles/update',
    //    'profiles/delete',
    //    'selling_plan_groups/create',
    //    'selling_plan_groups/update',
    //    'selling_plan_groups/delete',
    //    'bulk_operations/finish',
  ],
  DEFAULT_SHOPIFY_PAGE_SIZE = 250,
  //  TOPICS = {
  //    APP_UNINSTALLED: 'app/uninstalled',
  //    //    APP_SUBSCRIPTIONS_UPDATE: 'app_subscriptions/update',
  //    //    SUBSCRIPTION_CONTRACTS_CREATE: 'subscription_contracts/create',
  //    //    SUBSCRIPTION_CONTRACTS_UPDATE: 'subscription_contracts/update',
  //    PRODUCTS_CREATE: 'products/create',
  //    PRODUCTS_DELETE: 'products/delete',
  //    PRODUCTS_UPDATE: 'products/update',
  //    SHOP_UPDATE: 'shop/update',
  //    //    SUBSCRIPTION_BILLING_ATTEMPTS_CHALLENGED: 'subscription_billing_attempts/challenged',
  //    //    SUBSCRIPTION_BILLING_ATTEMPTS_FAILURE: 'subscription_billing_attempts/failure',
  //    //    SUBSCRIPTION_BILLING_ATTEMPTS_SUCCESS: 'subscription_billing_attempts/success',
  //  },
  LEGACY_ENDPOINT = 'https://api.thecustomproductbuilder.com/webhooks',
  REST_ENDPOINT = process.env.WEBHOOKS_REST_ENDPOINT || process.env.API_URL || 'data.apis.thecustomproductbuilder.com/wh/',
  PUBSUB_ENDPOINT = process.env.WEBHOOKS_PUBSUB_ENDPOINT || 'pubsub://buildateam-52:customproductbuilder-test',
  /**
   * @module cpb-shopify/webhook
   */
    /**
    module cpb-shopify/webhook
    !!!type {{settings: {kind: string, fields: Array()}, save((!string|number), (Object|Object[])): Promise<unknown[]>, create((!string|number), {address:
        !string, topic: !string}=): Promise<T>, list((!string|number), {id: ?number, address: ?string, topic: ?string}=): Promise<Array()|*>,
        delete(*=, *=): Promise<void>}}
     */
  Webhooks = {
    settings: {
      kind: 'shopify_webhooks',
      fields: [],
      topics: ALL_WEBHOOK_TOPICS,
      endpoints: {
        pubsub: PUBSUB_ENDPOINT,
        rest: REST_ENDPOINT,
        legacy: LEGACY_ENDPOINT,
      },
    },

    /**
     * ### Get Shopify Webhooks
     * @see https://shopify.dev/api/admin-graphql/2022-01/enums/WebhookSubscriptionTopic
     * @see https://shopify.dev/api/admin-rest/2022-01/resources/webhook#top
     * @param {!string|number}  shopNameOrId
     * @param fetch

     * @return {Promise<Array()|*>}
     */
    async list(shopNameOrId, fetch) {
      if (!shopNameOrId) throw new TypeError(`[shopify/webhooks/list]!shopNameOrId`);

      const { id: shopID, name: shopName } = await ShopIdService.getIdName(shopNameOrId);
      if (!shopID) throw new TypeError(`[shopify/webhooks/list]!id`);
      let result = [],
        error;
      try {
        if (fetch) {
          const connection = await Shopify.connect({ shopName }, { apiVersion: '2022-01' });
          console.info(`[shopify/webhooks/list][${shopName}] requesting new data...`);
          let pagination = {
            limit: (this.settings.limit || DEFAULT_SHOPIFY_PAGE_SIZE) > DEFAULT_SHOPIFY_PAGE_SIZE ? DEFAULT_SHOPIFY_PAGE_SIZE : this.settings.limit,
          };
          do {
            const page = await connection.webhook.list(pagination);
            pagination = page.nextPageParameters;
            //            console.debug({ page });
            if (page) for (const webhook of page) result.push({ ...webhook, shopID, shopName });
          } while (pagination);
          if (result.length) this.save(shopName, result).catch(console.error);
        } else result = await Datastore.query({ kind: this.settings.kind, filter: { shopName } });

        console.info(`[shopify/webhooks/list][${shopID},${shopName}] length:${result.length}`);
      } catch (error) {
        console.error(`[shopify/webhooks/list][${shopName}][${error.code}] NO_SHOPIFY_CONNECTION`, { error });
      }
      return [result, error];
    },

    /**
     * ### Create all webhooks topics
     * @param {string|number} shopIdOrName
     * @param {boolean|number|undefined} appendTopic - if set appends the topic to the endpoint url
     * @return {Promise<unknown[]>} result
     */
    async createAll(shop, endpoint = PUBSUB_ENDPOINT, appendTopic) {
      for (const topic of this.settings.topics) {
        console.info(`[shopify/webhook/createAll][${shop}][${topic}`);
        let address = endpoint;
        if (appendTopic) address = [endpoint, topic].join('/');
        await this.create(shop, { topic, address, format: 'json' });
      }
      return await this.list(shop, 1);
    },

    /**
     * ### Creates legacy webhooks with endpoint
     * LEGACY_ENDPOINT='https://api.thecustomproductbuilder.com/webhooks'
     * @param {string|number} shopIdOrName
     * @return {Promise<unknown[]>}
     */
    async createAllLegacy(shopIdOrName) {
      return await this.createAll(shopIdOrName, this.settings.endpoints.legacy, true);
    },
    /**
     * ### Create Webhook
     * @param {!string|number} shopNameOrId
     * @param {!string} address
     * @param {!string} topic
     * @return {Promise<T>}
     * @throws {Error}
     */
    async create(shopNameOrId, { address, topic, format = 'json' } = {}) {
      if (!shopNameOrId) throw new TypeError(`[webhooks/create]!shopNameOrId`);
      if (!address) throw new TypeError(`[webhooks/create]!address`);
      if (!topic) throw new TypeError(`[webhooks/create]!topic`);

      const { id: shopID, name: shopName } = await ShopIdService.getIdName(shopNameOrId);
      if (!shopID) throw new TypeError(`[webhooks/create]!id`);
      const connection = await Shopify.connect({ shopName });
      const [webhooks, error] = await this.list(shopName, true);

      console.debug(`[webhooks/create][${shopName}][${topic}]`, { shopID, shopName, address, topic });
      if (error) {
        //        console.error({ error });
        throw error;
      }

      const currentWebhook = (webhooks || []).find(w => w.address === address && w.topic === topic);

      if (!currentWebhook) {
        try {
          await connection.webhook.create({ address, topic, format });
        } catch (error) {
          console.error(`[shopify/webhook/create][${shopName}][${topic}][${address}] ERROR`, error);
          //          throw error;
        }
      } else console.debug(`[shopify/webhook/create][${shopName}][${topic}] Webhook already exists`, currentWebhook);
      return currentWebhook;
    },

    /**
     * ### Delete Webhook from Shopify and Datastore
     * @param {!string|number} shopNameOrId
     * @param {!number} id - Webhook ID
     * @return {Promise<void>}
     * @throws {Error}
     */
    async delete(shopNameOrId, id) {
      if (!shopNameOrId) throw new TypeError(`[webhooks/delete]!shopNameOrId`);
      if (!id) throw new TypeError(`[webhooks/delete]!id`);
      const connection = await Shopify.connect({ shopName: shopNameOrId });
      await connection.webhook.delete(id);
      return await Datastore.delete({ kind: this.settings.kind, id });
    },

    /**
     * ### Delete all webhooks for the shop
     * @param {!string|number} shopNameOrId
     * @return {Promise<void>}
     * @throws {Error}
     */
    async deleteAll(shopNameOrId) {
      const [webhooks, error] = await this.list(shopNameOrId, 1);
      if (error) throw error;
      for (const wh of webhooks) {
        console.info('DELETE', wh.id, wh.address, wh.topic);
        await this.delete(shopNameOrId, wh.id);
      }
      return undefined;
    },
    /**
     * ### Save Webhook[s] Info into the datastore
     * @param {!string|number} shopIdOrName
     * @param {object|object[]} data
     * @return {Promise<unknown[]>}
     */
    async save(shopIdOrName, data) {
      if (!shopIdOrName) throw new TypeError(`[webhooks/save]!shopIdOrName`);
      if (!data) throw new TypeError(`[webhooks/save]!data`);
      if (!isArray(data)) data = [data];

      const promises = [],
        { id: shopID, name: shopName } = (await ShopIdService.getIdName(shopIdOrName)) || { id: shopID, name: shopName };
      if (!shopID) throw new TypeError(`[webhooks/save]!shopID`);

      for (const webhook of data) {
        webhook.shopID = shopID;
        webhook.shopName = shopName;
        webhook.datastore_updated_at = new Date();

        promises.push(
          Datastore.save({
            kind: this.settings.kind,
            data: webhook,
            key: Datastore.datastore.key([this.settings.kind, [webhook.shopName, webhook.id].join()]),
            useEmulator: process.env.USE_EMULATOR_DATASTORE,
          }),
        );
      }
      return Promise.all(promises);
    },
  };
export default Webhooks;