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;