diff --git a/CHANGELOG.md b/CHANGELOG.md index 222d90dd..3f2e9bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] +### Added +- Added TypeScript typings for the Express helper and storage exports. +### Changed +- Updated partner-facing README guidance. +- Removed legacy webhook subscriber API integrations in favor of v3 upsert flows. +- Webhook notification email now comes from extension details instead of webhook config. + ## [v1.1.2] - 2025-03-20 ### Changed - Fixed peer dependency minimum version requirement. diff --git a/README.md b/README.md index 8cb4c1f2..794bd1b5 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,21 @@ -# fdk-extension-javascript +# FDK Extension JavaScript (External Partner Guide) -FDK Extension Helper Library +Build Fynd platform extensions with an Express-first helper library that handles authentication, API clients, and webhook registration for you. This guide is tailored for external partners who want fast, production-ready integration. -#### Initial Setup +## Install + +```bash +npm install @gofynd/fdk-extension-javascript +``` + +## Quick Start ```javascript const bodyParser = require("body-parser"); const express = require("express"); const cookieParser = require("cookie-parser"); -const { setupFdk } = require("fdk-extension-javascript/express"); -const { RedisStorage } = require("fdk-extension-javascript/express/storage"); +const { setupFdk } = require("@gofynd/fdk-extension-javascript/express"); +const { RedisStorage } = require("@gofynd/fdk-extension-javascript/express/storage"); const Redis = require("ioredis"); const app = express(); @@ -18,40 +24,52 @@ app.use(bodyParser.json({ limit: "2mb" })); const redis = new Redis(); -let extensionHandler = { +const extensionHandler = { auth: async function (data) { console.log("called auth callback"); }, - uninstall: async function (data) { console.log("called uninstall callback"); }, }; -let fdkClient = setupFdk({ +const fdkClient = setupFdk({ api_key: "", api_secret: "", - base_url: baseUrl, // this is optional - scopes: ["company/products"], // this is optional + base_url: "https://your-domain.com", // optional, defaults to extension base_url + scopes: ["company/products"], // optional; defaults to extension scopes callbacks: extensionHandler, storage: new RedisStorage(redis), access_mode: "offline", }); -app.use(fdkClient.fdkHandler); +app.use(fdkClient.fdkHandler); app.listen(8080); ``` -#### How to call platform apis? +## Configuration reference -To call platform api you need to have instance of `PlatformClient`. Instance holds methods for SDK classes. All routes registered under `platformApiRoutes` express router will have `platformClient` under request object which is instance of `PlatformClient`. +| Key | Required | Description | +| --- | --- | --- | +| `api_key` | Yes | Extension API key from Fynd partner dashboard. | +| `api_secret` | Yes | Extension API secret from Fynd partner dashboard. | +| `base_url` | No | Public URL for your extension (used for redirects and webhooks). | +| `scopes` | No | Override extension scopes. Must be a subset of registered scopes. | +| `callbacks` | Yes | `auth` and `uninstall` handlers. | +| `storage` | Yes | Session storage adapter (Redis, SQLite, memory, or custom). | +| `access_mode` | No | `offline` (default) or `online`. | +| `webhook_config` | No | Webhook registration configuration. | +| `debug` | No | Enable debug logging. | +| `cluster` | No | API cluster override. | -> Here `platformApiRoutes` has middleware attached which allows passing such request which are called after launching extension under any company. +## Call platform APIs (HTTP routes) + +When you register routes under `platformApiRoutes`, the middleware injects a `platformClient` into the request. ```javascript fdkClient.platformApiRoutes.get("/test/routes", async (req, res, next) => { try { - let data = await req.platformClient.lead.getTickets(); + const data = await req.platformClient.lead.getTickets(); res.json(data); } catch (error) { console.error(error); @@ -62,61 +80,29 @@ fdkClient.platformApiRoutes.get("/test/routes", async (req, res, next) => { app.use(fdkClient.platformApiRoutes); ``` -#### How to call platform apis in background tasks? - -Background tasks running under some consumer or webhook or under any queue can get platform client via method `getPlatformClient`. It will return instance of `PlatformClient` as well. - -> Here FdkClient `access_mode` should be **offline**. Cause such client can only access PlatformClient in background task. - -```javascript -function backgroundHandler(companyId) { - try { - const platformClient = await fdkExtension.getPlatformClient(companyId); - let data = await platformClient.lead.getTickets(); - // Some business logic here - res.json({ success: true }); - } catch (err) { - console.error(err); - res.status(404).json({ success: false }); - } -} -``` - - -#### How to call partner apis in background tasks? - -Background tasks running under some consumer or webhook or under any queue can get partner client via method `getPartnerClient`. It will return instance of `PartnerClient` as well. - -> Here FdkClient `access_mode` should be **offline**. Cause such client can only access PartnerClient in background task. +## Call platform APIs (background jobs) ```javascript -function backgroundHandler(organizationId) { +async function backgroundHandler(companyId) { try { - const partnerClient = await fdkExtension.getPartnerClient(organizationId); - let data = await partnerClient.webhook.responseTimeSummary({ - extensionId: '', - startDate: '', - endDate: '' - }); - // Some business logic here - res.json({ success: true }); + const platformClient = await fdkClient.getPlatformClient(companyId); + const data = await platformClient.lead.getTickets(); + return data; } catch (err) { console.error(err); - res.status(404).json({ success: false }); + throw err; } } ``` -#### How to call partner apis? - -To call partner api you need to have instance of `PartnerClient`. Instance holds methods for SDK classes. All routes registered under `partnerApiRoutes` express router will have `partnerClient` under request object which is instance of `PartnerClient`. +> Background access requires `access_mode: "offline"`. -> Here `partnerApiRoutes` has middleware attached which allows passing such request which are called after launching extension under any company. +## Call partner APIs (HTTP routes) ```javascript fdkClient.partnerApiRoutes.get("/test/routes", async (req, res, next) => { try { - let data = await req.partnerClient.lead.getTickets(); + const data = await req.partnerClient.lead.getTickets(); res.json(data); } catch (error) { console.error(error); @@ -127,130 +113,120 @@ fdkClient.partnerApiRoutes.get("/test/routes", async (req, res, next) => { app.use(fdkClient.partnerApiRoutes); ``` -#### How to register for webhook events? +## Call partner APIs (background jobs) -Webhook events can be helpful to handle tasks when certan events occur on platform. You can subscribe to such events by passing `webhook_config` in setupFdk function. - ```javascript +async function backgroundHandler(organizationId) { + try { + const partnerClient = await fdkClient.getPartnerClient(organizationId); + const data = await partnerClient.webhook.responseTimeSummary({ + extensionId: "", + startDate: "", + endDate: "", + }); + return data; + } catch (err) { + console.error(err); + throw err; + } +} +``` + +> Background access requires `access_mode: "offline"`. + +## Webhooks + +Webhook notification email is taken from your extension details in the partner dashboard and does not need to be provided in `webhook_config`. + +### Register webhook events -let fdkClient = setupFdk({ +```javascript +const fdkClient = setupFdk({ api_key: "", api_secret: "", - base_url: baseUrl, // this is optional - scopes: ["company/products"], // this is optional - callbacks: { - auth: async function (data) { - console.log("called auth callback"); - }, - uninstall: async function (data) { - console.log("called uninstall callback"); - }, - }, + callbacks: extensionHandler, storage: new RedisStorage(redis), access_mode: "offline", webhook_config: { - api_path: "/api/v1/webhooks", // required - notification_email: "test@abc.com", // required - subscribe_on_install: false, //optional. Default true - subscribed_saleschannel: 'specific', //optional. Default all - marketplace: true, // to receive marketplace saleschannel events. Only allowed when subscribed_saleschannel is set to specific - event_map: { // required - 'company/brand/create': { - version: '1', + api_path: "/api/v1/webhooks", + subscribe_on_install: true, + subscribed_saleschannel: "all", + event_map: { + "company/brand/create": { + version: "1", handler: handleBrandCreate, - provider: 'rest' // if not provided, Default is `rest` - }, - 'company/location/update': { - version: '1', - handler: handleLocationUpdate, - }, - 'application/coupon/create': { - version: '1', - topic: 'coupon_create_kafka_topic', - provider: 'kafka' - }, - 'company/brand/update': { - version: '1', - topic: "company-brand-create", - provider: 'pub_sub' + provider: "rest", }, - 'extension/extension/install': { - version: '1', - queue: "extension-install", - workflow_name: "extension", - provider: 'temporal' + "application/coupon/create": { + version: "1", + topic: "coupon_create_kafka_topic", + provider: "kafka", }, - 'company/location/create': { - version: '1', - queue: "company-location-create", - provider: 'sqs' - }, - 'company/product-size/create': { - version: '1', - event_bridge_name: "company-product-size-create", - provider: 'event_bridge' - } - } + }, }, - debug: true // optional. Enables debug logs if `true`. Default `false` + debug: true, }); - ``` -> By default all webhook events all subscribed for all companies whenever they are installed. To disable this behavior set `subscribe_on_install` to `false`. If `subscribe_on_install` is set to false, you need to manually enable webhook event subscription by calling `syncEvents` method of `webhookRegistry` - -There should be view on given api path to receive webhook call. It should be `POST` api path. Api view should call `processWebhook` method of `webhookRegistry` object available under `fdkClient` here. -> Here `processWebhook` will do payload validation with signature and calls individual handlers for event passed with webhook config. +### Handle webhook requests ```javascript - -app.post('/api/v1/webhooks', async (req, res, next) => { +app.post("/api/v1/webhooks", async (req, res) => { try { await fdkClient.webhookRegistry.processWebhook(req); - return res.status(200).json({"success": true}); - } - catch(err) { - logger.error(err); - return res.status(500).json({"success": false}); + return res.status(200).json({ success: true }); + } catch (err) { + console.error(err); + return res.status(500).json({ success: false }); } }); - ``` -> Setting `subscribed_saleschannel` as "specific" means, you will have to manually subscribe saleschannel level event for individual saleschannel. Default value here is "all" and event will be subscribed for all sales channels. For enabling events manually use function `enableSalesChannelWebhook`. To disable receiving events for a saleschannel use function `disableSalesChannelWebhook`. +### Saleschannel scoping + +If you want to subscribe only to specific sales channels, set: + +```javascript +webhook_config: { + subscribed_saleschannel: "specific", + subscribed_saleschannel_ids: ["application_id_1", "application_id_2"], +} +``` -#### Filters and reducers in webhook events +> If `subscribed_saleschannel` is set to `specific` with an empty `subscribed_saleschannel_ids` list, no saleschannel-specific events will be delivered. -A filter and reducer can be provided to refine the data delivered for each subscribed event. The Filter functionality allows selective delivery of data by specifying conditions based on JSONPath queries and logical operators. Reducer allow customization of the payload structure by specifying only the fields needed by the subscriber. The reducer extracts fields from the event’s data and restructures them as needed. +### Filters and reducers ```javascript webhook_config: { - api_path: "/v1.0/webhooks", - notification_email: "rahultambe@gofynd.com", - marketplace: true, - subscribed_saleschannel: 'specific', - event_map: { - 'company/brand/update': { - version: '1', - handler: handleExtensionUninstall, - filters: { - query: "$.brand.uid", - condition: "(uid) => uid === 238" - }, - reducer: { - brand_name: "$.brand.name", - logo_link: "$.brand.logo" - } - }] - } + api_path: "/v1.0/webhooks", + subscribed_saleschannel: "specific", + subscribed_saleschannel_ids: ["application_id_1"], + event_map: { + "company/brand/update": { + version: "1", + handler: handleExtensionUninstall, + filters: { + query: "$.brand.uid", + condition: "(uid) => uid === 238", + }, + reducer: { + brand_name: "$.brand.name", + logo_link: "$.brand.logo", + }, + }, + }, } ``` -##### How webhook registery subscribes to webhooks on Fynd Platform? -After webhook config is passed to setupFdk whenever extension is launched to any of companies where extension is installed or to be installed, webhook config data is used to create webhook subscriber on Fynd Platform for that company. -> Any update to webhook config will not automatically update subscriber data on Fynd Platform for a company until extension is opened atleast once after the update. +### Sync webhook configuration + +If you update your webhook configuration, call `syncEvents` to upsert the configuration for a specific company: + +```javascript +await fdkClient.webhookRegistry.syncEvents(req.platformClient); +``` -Other way to update webhook config manually for a company is to call `syncEvents` function of webhookRegistery. +## Custom storage -# [Custom storage class](/express/storage/README.md) -The FDK Extension JavaScript library provides built-in support for SQLite, Redis and in-memory storage options as default choices for session data storage. However, if you require a different storage option, this readme will guide you through the process of implementing a custom storage class. +See the custom storage guide: [express/storage/README.md](express/storage/README.md). diff --git a/examples/test.js b/examples/test.js index 46d37efa..dd6b1b68 100644 --- a/examples/test.js +++ b/examples/test.js @@ -57,7 +57,6 @@ let fdkExtension = setupFdk({ cluster: "https://api.fyndx0.de", // this is optional by default it points to prod. webhook_config: { api_path: "/webhook", - notification_email: "test2@abc.com", // required subscribed_saleschannel: 'specific', //optional event_map: { // required 'application/coupon/update': { @@ -121,26 +120,6 @@ webhookRouter.post("/webhook", async (req, res, next) => { } }); -fdkExtension.apiRoutes.post("/webhook/application/:application_id/subscribe", async (req, res, next) => { - try { - await fdkExtension.webhookRegistry.enableSalesChannelWebhook(req.platformClient, req.params.application_id); - res.json({ "success": true }); - } catch (err) { - console.error(err); - res.status(500).json({ "success": false }); - } -}); - -fdkExtension.apiRoutes.post("/webhook/application/:application_id/unsubscribe", async (req, res, next) => { - try { - await fdkExtension.webhookRegistry.disableSalesChannelWebhook(req.platformClient, req.params.application_id); - res.json({ "success": true }); - } catch (err) { - console.error(err); - res.status(500).json({ "success": false }); - } -}); - app.use(webhookRouter); app.use(fdkExtension.applicationProxyRoutes); app.use(fdkExtension.apiRoutes); @@ -174,4 +153,4 @@ app.use(function onError(err, req, res, next) { res.status(statusCode).json(resData); }); -app.listen(5070); \ No newline at end of file +app.listen(5070); diff --git a/express/extension.js b/express/extension.js index d04217cb..fb206bde 100644 --- a/express/extension.js +++ b/express/extension.js @@ -63,6 +63,7 @@ class Extension { this.webhookRegistry = new WebhookRegistry(this._retryManager); await this.getExtensionDetails(); + data.extension_details = this.extensionData; if (data.base_url && !validator.isURL(data.base_url)) { throw new FdkInvalidExtensionConfig("Invalid base_url value. Invalid value: " + data.base_url); @@ -80,6 +81,13 @@ class Extension { logger.debug(`Extension initialized`); if (data.webhook_config && Object.keys(data.webhook_config)) { + const notificationEmail = this.extensionData?.notification_email + || this.extensionData?.contact_email + || this.extensionData?.email; + if (!notificationEmail) { + throw new FdkInvalidExtensionConfig("Missing notification email in extension details."); + } + data.webhook_config.notification_email = notificationEmail; await this.webhookRegistry.initialize(data.webhook_config, data); } diff --git a/express/index.d.ts b/express/index.d.ts new file mode 100644 index 00000000..91ba6278 --- /dev/null +++ b/express/index.d.ts @@ -0,0 +1,84 @@ +import type { Router } from "express"; +import type { BaseStorage } from "./storage"; + +export type AccessMode = "online" | "offline"; + +export interface StorageAdapter { + get(key: string): Promise; + set(key: string, value: unknown): Promise; + del(key: string): Promise; + setex(key: string, value: unknown, ttl: number): Promise; +} + +export interface ExtensionCallbacks { + auth(data: unknown): Promise | void; + uninstall(data: unknown): Promise | void; +} + +export type WebhookProvider = "rest" | "kafka" | "pub_sub" | "temporal" | "sqs" | "event_bridge"; + +export interface WebhookEventConfig { + version: string | number; + handler?: (eventName: string, payload: unknown, companyId: number, applicationId?: number) => Promise | void; + provider?: WebhookProvider; + topic?: string; + queue?: string; + workflow_name?: string; + event_bridge_name?: string; + filters?: Record; + reducer?: Record; +} + +export interface WebhookConfig { + api_path: string; + subscribe_on_install?: boolean; + subscribed_saleschannel?: "all" | "specific"; + subscribed_saleschannel_ids?: Array; + marketplace?: boolean; + event_map: Record; +} + +export interface SetupFdkConfig { + api_key: string; + api_secret: string; + base_url?: string; + scopes?: string[]; + callbacks: ExtensionCallbacks; + storage: BaseStorage | StorageAdapter; + access_mode?: AccessMode; + webhook_config?: WebhookConfig; + debug?: boolean; + cluster?: string; +} + +export interface WebhookRegistry { + readonly isInitialized: boolean; + syncEvents(platformClient: unknown, config?: WebhookConfig, enableWebhooks?: boolean): Promise; + processWebhook(req: unknown): Promise; + verifySignature(req: unknown): void; +} + +export interface Extension { + readonly isInitialized: boolean; + getPlatformConfig(companyId: number | string): Promise; + getPlatformClient(companyId: number | string, session: unknown): Promise; + getPartnerConfig(organizationId: number | string): unknown; + getPartnerClient(organizationId: number | string, session: unknown): Promise; + webhookRegistry: WebhookRegistry; +} + +export interface FdkClient { + fdkHandler: Router; + extension: Extension; + apiRoutes: Router; + platformApiRoutes: Router; + partnerApiRoutes: Router; + applicationProxyRoutes: Router; + webhookRegistry: WebhookRegistry; + getPlatformClient(companyId: number | string): Promise; + getPartnerClient(organizationId: number | string): Promise; + getApplicationClient(applicationId: number | string, applicationToken: string): Promise; +} + +export function setupFdk(config: SetupFdkConfig, syncInitialization: true): Promise; +export function setupFdk(config: SetupFdkConfig, syncInitialization?: false): FdkClient; diff --git a/express/storage/index.d.ts b/express/storage/index.d.ts new file mode 100644 index 00000000..285be16c --- /dev/null +++ b/express/storage/index.d.ts @@ -0,0 +1,21 @@ +export class BaseStorage { + constructor(prefixKey?: string); + get(key: string): Promise; + set(key: string, value: unknown): Promise; + del(key: string): Promise; + setex(key: string, value: unknown, ttl: number): Promise; +} + +export class MemoryStorage extends BaseStorage {} + +export class RedisStorage extends BaseStorage { + constructor(client: unknown, prefixKey?: string); +} + +export class SQLiteStorage extends BaseStorage { + constructor(dbPath: string, prefixKey?: string); +} + +export class MultiLevelStorage extends BaseStorage { + constructor(storages: BaseStorage[], prefixKey?: string); +} diff --git a/express/webhook.js b/express/webhook.js index 52bfe212..550a438a 100644 --- a/express/webhook.js +++ b/express/webhook.js @@ -7,7 +7,6 @@ const { TEST_WEBHOOK_EVENT_NAME, ASSOCIATION_CRITERIA } = require("./constants") const { FdkWebhookProcessError, FdkWebhookHandlerNotFound, FdkWebhookRegistrationError, FdkInvalidHMacError, FdkInvalidWebhookConfig } = require("./error_code"); const logger = require("./logger"); const { RetryManger } = require("./retry_manager"); -const _ = require('lodash'); let eventConfig = {} class WebhookRegistry { @@ -20,16 +19,24 @@ class WebhookRegistry { async initialize(config, fdkConfig) { + const notificationEmail = config.notification_email + || fdkConfig?.extension_details?.notification_email + || fdkConfig?.extension_details?.contact_email + || fdkConfig?.extension_details?.email; const emailRegex = new RegExp(/^\S+@\S+\.\S+$/, 'gi'); - if (!config.notification_email || !emailRegex.test(config.notification_email)) { + if (!notificationEmail || !emailRegex.test(notificationEmail)) { throw new FdkInvalidWebhookConfig(`Invalid or missing "notification_email"`); } + config.notification_email = notificationEmail; if (!config.api_path || config.api_path[0] !== '/') { throw new FdkInvalidWebhookConfig(`Invalid or missing "api_path"`); } if(config.marketplace == true && config.subscribed_saleschannel != "specific"){ throw new FdkInvalidWebhookConfig(`marketplace is only allowed when subscribed_saleschannel is specific"`); } + if (config.subscribed_saleschannel === 'specific' && 'subscribed_saleschannel_ids' in config && !Array.isArray(config.subscribed_saleschannel_ids)) { + throw new FdkInvalidWebhookConfig(`subscribed_saleschannel_ids must be an array when subscribed_saleschannel is specific`); + } if (!config.event_map || !Object.keys(config.event_map).length) { throw new FdkInvalidWebhookConfig(`Invalid or missing "event_map"`); } @@ -143,53 +150,6 @@ class WebhookRegistry { return `${this._fdkConfig.base_url}${this._config.api_path}`; } - _isConfigUpdated(subscriberConfig) { - let updated = false; - const configCriteria = this._associationCriteria(subscriberConfig.association.application_id); - if (configCriteria !== subscriberConfig.association.criteria) { - if (configCriteria === ASSOCIATION_CRITERIA.ALL) { - subscriberConfig.association.application_id = []; - } - logger.debug(`Webhook association criteria updated from ${subscriberConfig.association.criteria} to ${configCriteria}`); - subscriberConfig.association.criteria = configCriteria; - updated = true; - } - - if (this._config.notification_email !== subscriberConfig.email_id) { - logger.debug(`Webhook notification email updated from ${subscriberConfig.email_id} to ${this._config.notification_email}`); - subscriberConfig.email_id = this._config.notification_email; - updated = true; - } - - if (subscriberConfig.provider === 'rest' && this._webhookUrl !== subscriberConfig.webhook_url) { - logger.debug(`Webhook url updated from ${subscriberConfig.webhook_url} to ${this._webhookUrl}`); - subscriberConfig.webhook_url = this._webhookUrl; - updated = true; - } - - // type marketplace is allowed only when association criteria is specific - if(configCriteria == ASSOCIATION_CRITERIA.SPECIFIC){ - if((subscriberConfig.type == 'marketplace' && !this._config.marketplace)){ - logger.debug(`Type updated from ${subscriberConfig.type} to null`); - subscriberConfig.type = null; - updated = true; - }else if (((!subscriberConfig.type || subscriberConfig.type != "marketplace") && this._config.marketplace) ){ - logger.debug(`Type updated from ${subscriberConfig.type} to marketplace`); - subscriberConfig.type = "marketplace"; - updated = true - } - }else { - if(subscriberConfig.type == 'marketplace'){ - logger.debug(`Type updated from ${subscriberConfig.type} to null`); - subscriberConfig.type = null; - updated = true; - } - } - - - return updated; - } - async syncEvents(platformClient, config = null, enableWebhooks) { if (config) { await this.initialize(config, this._fdkConfig); @@ -198,33 +158,12 @@ class WebhookRegistry { throw new FdkInvalidWebhookConfig('Webhook registry not initialized'); } logger.debug('Webhook sync events started'); - - let subscriberConfigList = await this.getSubscriberConfig(platformClient); - - let subscriberSyncedForAllProvider = await this.syncSubscriberConfigForAllProviders(platformClient, subscriberConfigList) - - // v3.0 upsert put api does not exist - if(!subscriberSyncedForAllProvider){ - let subscriberConfigList = await this.getSubscriberConfig(platformClient); - await this.syncSubscriberConfig(subscriberConfigList.rest, 'rest', this._eventMap.rest, platformClient, enableWebhooks); - - await this.syncSubscriberConfig(subscriberConfigList.kafka, 'kafka', this._eventMap.kafka , platformClient, enableWebhooks); - - await this.syncSubscriberConfig(subscriberConfigList.pub_sub, 'pub_sub', this._eventMap.pub_sub , platformClient, enableWebhooks); - - await this.syncSubscriberConfig(subscriberConfigList.sqs, 'sqs', this._eventMap.sqs , platformClient, enableWebhooks); - - await this.syncSubscriberConfig(subscriberConfigList.event_bridge, 'event_bridge', this._eventMap.event_bridge , platformClient, enableWebhooks); - - await this.syncSubscriberConfig(subscriberConfigList.temporal, 'temporal', this._eventMap.temporal , platformClient, enableWebhooks); - } - + await this.syncSubscriberConfigForAllProviders(platformClient, enableWebhooks); } - /* this will call the v3.0 upsert put api which will handle syncing in a single api call. - In case the api does not exist we need to fallback to v2.0 api */ - async syncSubscriberConfigForAllProviders(platformClient, subscriberConfigList){ - let payload = this.createRegisterPayloadData(subscriberConfigList); + /* this will call the v3.0 upsert put api which will handle syncing in a single api call. */ + async syncSubscriberConfigForAllProviders(platformClient, enableWebhooks){ + let payload = this.createRegisterPayloadData(enableWebhooks); const uniqueKey = `registerSubscriberToEventForAllProvider_${platformClient.config.companyId}_${this._fdkConfig.api_key}`; const token = await platformClient.config.oauthClient.getAccessToken(); const retryInfo = this._retryManager.retryInfoMap.get(uniqueKey); @@ -232,26 +171,18 @@ class WebhookRegistry { this._retryManager.resetRetryState(uniqueKey); } try { - try{ - const rawRequest = { - method: "put", - url: `${this._fdkConfig.cluster}/service/platform/webhook/v3.0/company/${platformClient.config.companyId}/subscriber`, - data: payload, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "x-ext-lib-version": `js/${version}` - } - } - let response = await fdkAxios.request(rawRequest); - return true; - } - catch(err){ - if(err.code != '404'){ - throw err; + const rawRequest = { + method: "put", + url: `${this._fdkConfig.cluster}/service/platform/webhook/v3.0/company/${platformClient.config.companyId}/subscriber`, + data: payload, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + "x-ext-lib-version": `js/${version}` } - return false; } + await fdkAxios.request(rawRequest); + return true; } catch(err) { if ( RetryManger.shouldRetryOnError(err) @@ -260,7 +191,8 @@ class WebhookRegistry { return await this._retryManager.retry( uniqueKey, this.syncSubscriberConfigForAllProviders.bind(this), - platformClient + platformClient, + enableWebhooks ); } throw new FdkWebhookRegistrationError(`Error while updating webhook subscriber configuration for all providers. Reason: ${err.message}. Errors: ${JSON.stringify(err?.details)}`); @@ -268,32 +200,26 @@ class WebhookRegistry { } - createRegisterPayloadData(subscriberConfigList){ + createRegisterPayloadData(enableWebhooks){ + const applicationIds = Array.isArray(this._config.subscribed_saleschannel_ids) + ? this._config.subscribed_saleschannel_ids + : []; let payload = { "webhook_config": { notification_email: this._config.notification_email, name: this._fdkConfig.api_key, association: { "extension_id": this._fdkConfig.api_key, - "application_id": [], - "criteria": this._associationCriteria([]) + "application_id": applicationIds, + "criteria": this._associationCriteria(applicationIds) }, - status: "active", + status: enableWebhooks === undefined ? "active" : (enableWebhooks ? "active" : "inactive"), event_map: { } } }; - const configKeys = Object.keys(subscriberConfigList); - //Every provider has same association. Get the first one. - if (this._config.subscribed_saleschannel === 'specific' && configKeys.length > 0) { - const firstConfig = subscriberConfigList[configKeys[0]]; - if ( firstConfig?.association?.criteria == ASSOCIATION_CRITERIA.SPECIFIC) { - payload.webhook_config.association = firstConfig.association; - } - } - let payloadEventMap = payload.webhook_config.event_map; for(let [key, event] of Object.entries(this._config.event_map)) { if(!payloadEventMap[event.provider]){ @@ -331,260 +257,6 @@ class WebhookRegistry { }; payloadEventMap[event.provider].events.push(eventData); }; - return payload; - } - - async syncSubscriberConfig(subscriberConfig, configType, currentEventMapConfig, platformClient, enableWebhooks){ - - let registerNew = false; - let configUpdated = false; - let existingEvents = []; - - if (!subscriberConfig) { - subscriberConfig = { - "name": this._fdkConfig.api_key, - "association": { - "company_id": platformClient.config.companyId, - "application_id": [], - "criteria": this._associationCriteria([]) - }, - "status": "active", - "auth_meta": { - "type": "hmac", - "secret": this._fdkConfig.api_secret - }, - "events": [], - "provider": configType, - "email_id": this._config.notification_email, - } - if(configType === 'rest'){ - subscriberConfig['webhook_url'] = this._webhookUrl; - } - registerNew = true; - if (enableWebhooks !== undefined) { - subscriberConfig.status = enableWebhooks ? 'active' : 'inactive'; - } - } - else { - logger.debug(`Webhook ${configType} config on platform side for company id ${platformClient.config.companyId}: ${JSON.stringify(subscriberConfig)}`) - - const { id, name, webhook_url, provider="rest", association, status, auth_meta, event_configs, email_id, type } = subscriberConfig - subscriberConfig = { id, name, webhook_url, provider, association, status, auth_meta, email_id, type }; - subscriberConfig.events = []; - existingEvents = event_configs.map(event => { - return { - 'slug': `${event.event_category}/${event.event_name}/${event.event_type}/v${event.version}`, - 'topic': event?.subscriber_event_mapping?.broadcaster_config?.topic, - 'queue': event?.subscriber_event_mapping?.broadcaster_config?.queue, - 'event_bridge_name': event?.subscriber_event_mapping?.broadcaster_config?.event_bridge_name, - 'workflow_name': event?.subscriber_event_mapping?.broadcaster_config?.workflow_name, - 'filters': event?.subscriber_event_mapping?.filters, - 'reducer': event?.subscriber_event_mapping?.reducer - } - }); - // Checking Configuration Updates - if (provider == 'rest' && (auth_meta.secret !== this._fdkConfig.api_secret)) { - auth_meta.secret = this._fdkConfig.api_secret; - configUpdated = true; - } - if (enableWebhooks !== undefined) { - const newStatus = enableWebhooks ? 'active' : 'inactive'; - if(newStatus !== subscriberConfig.status){ - subscriberConfig.status = newStatus; - configUpdated = true; - } - } - if (this._isConfigUpdated(subscriberConfig)) { - configUpdated = true; - } - } - - // Adding all events to subscriberConfig if it is valid event - for (let eventName of Object.keys(currentEventMapConfig)) { - let event_id = eventConfig.eventsMap[eventName] - if (event_id) { - const event = { - slug: eventName, - topic: currentEventMapConfig[eventName]?.topic, - queue: currentEventMapConfig[eventName]?.queue, - event_bridge_name: currentEventMapConfig[eventName]?.event_bridge_name, - workflow_name: currentEventMapConfig[eventName]?.workflow_name, - filters: currentEventMapConfig[eventName]?.filters, - reducer: currentEventMapConfig[eventName]?.reducer - } - if(currentEventMapConfig[eventName].hasOwnProperty('topic')){ - event['topic'] = currentEventMapConfig[eventName].topic; - } - subscriberConfig.events.push(event); - } - } - - try { - if (registerNew) { - if(subscriberConfig.events.length == 0){ - logger.debug(`Skipped registerSubscriber API call as no ${configType} based events found`); - return; - } - await this.registerSubscriberConfig(platformClient, subscriberConfig); - if (this._fdkConfig.debug) { - subscriberConfig.events = subscriberConfig.events.map(event => event.slug); - logger.debug(`Webhook ${configType} config registered for company: ${platformClient.config.companyId}, config: ${JSON.stringify(subscriberConfig)}`); - } - } - else { - const eventDiff = [ - ...subscriberConfig.events.filter(event => !existingEvents.find(item => item.slug === event.slug)), - ...existingEvents.filter(event => !subscriberConfig.events.find(item => item.slug === event.slug)) - ] - - //keys to check for updates in subscriberConfig for different config type - let configTypeKeysToCheck = { - 'kafka': ['topic'], - 'pub_sub': ['topic'], - 'temporal': ['queue', 'workflow_name'], - 'sqs': ['queue'], - 'event_bridge': ['event_bridge_name'], - 'rest': [] - } - - //key to check which are common across all config type - let commonKeys = ['filters','reducer'] - - // check if these keys have changed - if(!configUpdated){ - for(const event of subscriberConfig.events){ - const existingEvent = existingEvents.find(e => e.slug === event.slug); - - if(existingEvent){ - - //compare config related keys - for(let key of configTypeKeysToCheck[configType]){ - if(!(event[key] === existingEvent[key])){ - configUpdated = true; - break - } - } - - //compare common keys - for(let key of commonKeys){ - if(!_.isEqual(event[key], existingEvent[key])){ - configUpdated = true; - break - } - } - } - } - } - - if (eventDiff.length || configUpdated) { - await this.updateSubscriberConfig(platformClient, subscriberConfig); - if (this._fdkConfig.debug) { - subscriberConfig.events = subscriberConfig.events?.map(event => event.slug); - logger.debug(`Webhook ${configType} config updated for company: ${platformClient.config.companyId}, config: ${JSON.stringify(subscriberConfig)}`); - } - } - } - } - catch (ex) { - throw new FdkWebhookRegistrationError(`Failed to sync webhook ${configType} events. Reason: ${ex.message}`); - } - } - - async enableSalesChannelWebhook(platformClient, applicationId) { - if (!this.isInitialized){ - await this.initialize(this._config, this._fdkConfig); - } - if (this._config.subscribed_saleschannel !== 'specific') { - throw new FdkWebhookRegistrationError('`subscribed_saleschannel` is not set to `specific` in webhook config'); - } - try { - let subscriberConfigList = await this.getSubscriberConfig(platformClient); - if (Object.keys(subscriberConfigList).length === 0) { - throw new FdkWebhookRegistrationError(`Subscriber config not found`); - } - for(const subscriberConfigType in subscriberConfigList) { - let subscriberConfig = subscriberConfigList[subscriberConfigType]; - const { id, name, webhook_url, provider="rest", association, status, auth_meta, event_configs, email_id } = subscriberConfig; - subscriberConfig = { id, name, webhook_url, provider, association, status, auth_meta, email_id }; - subscriberConfig.events = event_configs.map(event => { - const eventObj = { - 'slug': `${event.event_category}/${event.event_name}/${event.event_type}/v${event.version}`, - 'topic': event?.subscriber_event_mapping?.broadcaster_config?.topic, - 'queue': event?.subscriber_event_mapping?.broadcaster_config?.queue, - 'event_bridge_name': event?.subscriber_event_mapping?.broadcaster_config?.event_bridge_name, - 'workflow_name': event?.subscriber_event_mapping?.broadcaster_config?.workflow_name - } - return eventObj; - }); - const arrApplicationId = subscriberConfig.association.application_id || []; - const rmIndex = arrApplicationId.indexOf(applicationId); - if (rmIndex === -1) { - arrApplicationId.push(applicationId); - subscriberConfig.association.application_id = arrApplicationId; - subscriberConfig.association.criteria = this._associationCriteria(subscriberConfig.association.application_id); - - if(subscriberConfig?.association?.criteria == ASSOCIATION_CRITERIA.SPECIFIC){ - subscriberConfig.type = this._config.marketplace ? 'marketplace' : null; - } - await this.updateSubscriberConfig(platformClient, subscriberConfig); - logger.debug(`Webhook enabled for saleschannel: ${applicationId}`); - } - } - } - catch (ex) { - throw new FdkWebhookRegistrationError(`Failed to add saleschannel webhook. Reason: ${ex.message}`); - } - } - - async disableSalesChannelWebhook(platformClient, applicationId) { - if (!this.isInitialized){ - await this.initialize(this._config, this._fdkConfig); - } - if (this._config.subscribed_saleschannel !== 'specific') { - throw new FdkWebhookRegistrationError('`subscribed_saleschannel` is not set to `specific` in webhook config'); - } - try { - let subscriberConfigList = await this.getSubscriberConfig(platformClient); - if (Object.keys(subscriberConfigList).length == 0) { - throw new FdkWebhookRegistrationError(`Subscriber config not found`); - } - for(const subscriberConfigType in subscriberConfigList) { - let subscriberConfig = subscriberConfigList[subscriberConfigType]; - const { id, name, webhook_url, provider="rest", association, status, auth_meta, event_configs, email_id } = subscriberConfig; - subscriberConfig = { id, name, webhook_url, provider, association, status, auth_meta, email_id }; - subscriberConfig.events = event_configs.map(event => { - const eventObj = { - 'slug': `${event.event_category}/${event.event_name}/${event.event_type}/v${event.version}`, - 'topic': event?.subscriber_event_mapping?.broadcaster_config?.topic, - 'queue': event?.subscriber_event_mapping?.broadcaster_config?.queue, - 'event_bridge_name': event?.subscriber_event_mapping?.broadcaster_config?.event_bridge_name, - 'workflow_name': event?.subscriber_event_mapping?.broadcaster_config?.workflow_name - } - - return eventObj; - }); - const arrApplicationId = subscriberConfig.association.application_id; - if (arrApplicationId && arrApplicationId.length) { - const rmIndex = arrApplicationId.indexOf(applicationId); - if (rmIndex > -1) { - arrApplicationId.splice(rmIndex, 1); - subscriberConfig.association.criteria = this._associationCriteria(subscriberConfig.association.application_id); - if(subscriberConfig?.association?.criteria == ASSOCIATION_CRITERIA.SPECIFIC){ - subscriberConfig.type = this._config.marketplace ? 'marketplace' : null; - }else{ - subscriberConfig.type = null; - } - await this.updateSubscriberConfig(platformClient, subscriberConfig); - logger.debug(`Webhook disabled for saleschannel: ${applicationId}`); - } - } - } - } - catch (ex) { - throw new FdkWebhookRegistrationError(`Failed to remove saleschannel webhook. Reason: ${ex.message}`); - } - } - verifySignature(req) { const reqSignature = req.headers['x-fp-signature']; const { body } = req; @@ -624,196 +296,6 @@ class WebhookRegistry { } - async registerSubscriberConfig(platformClient, subscriberConfig) { - const uniqueKey = `registerSubscriberToEvent_${platformClient.config.companyId}_${this._fdkConfig.api_key}`; - const token = await platformClient.config.oauthClient.getAccessToken(); - const retryInfo = this._retryManager.retryInfoMap.get(uniqueKey); - if (retryInfo && !retryInfo.isRetry) { - this._retryManager.resetRetryState(uniqueKey); - } - - try { - try{ - const rawRequest = { - method: "post", - url: `${this._fdkConfig.cluster}/service/platform/webhook/v2.0/company/${platformClient.config.companyId}/subscriber`, - data: subscriberConfig, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "x-ext-lib-version": `js/${version}` - } - } - return await fdkAxios.request(rawRequest); - } - catch(err){ - if(subscriberConfig.provider !== "rest"){ - logger.debug(`Webhook Subscriber Config type ${subscriberConfig.provider} is not supported with current fp version`) - return; - } - if(err.code != '404'){ - throw err; - } - - const eventsList = subscriberConfig.events; - delete subscriberConfig.events; - const provider = subscriberConfig.provider; - delete subscriberConfig.provider; - subscriberConfig.event_id = []; - eventsList.forEach((event) => { - subscriberConfig.event_id.push(eventConfig.eventsMap[event.slug]); - }) - - const rawRequest = { - method: "post", - url: `${this._fdkConfig.cluster}/service/platform/webhook/v1.0/company/${platformClient.config.companyId}/subscriber`, - data: subscriberConfig, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "x-ext-lib-version": `js/${version}` - } - } - const response = await fdkAxios.request(rawRequest); - subscriberConfig.events = eventsList; - subscriberConfig.provider = provider; - return response; - } - } catch(err) { - if ( - RetryManger.shouldRetryOnError(err) - && !this._retryManager.isRetryInProgress(uniqueKey) - ) { - return await this._retryManager.retry( - uniqueKey, - this.registerSubscriberConfig.bind(this), - platformClient, - subscriberConfig - ) - } - throw new FdkWebhookRegistrationError(`Error while registering webhook subscriber configuration, Reason: ${err.message}`); - } - - } - - async updateSubscriberConfig(platformClient, subscriberConfig) { - const uniqueKey = `updateSubscriberConfig_${platformClient.config.companyId}_${this._fdkConfig.api_key}`; - const token = await platformClient.config.oauthClient.getAccessToken(); - const retryInfo = this._retryManager.retryInfoMap.get(uniqueKey); - if (retryInfo && !retryInfo.isRetry) { - this._retryManager.resetRetryState(uniqueKey); - } - - try { - if(subscriberConfig.events.length == 0){ - subscriberConfig.status = 'inactive'; - delete subscriberConfig.events - } - try{ - const rawRequest = { - method: "put", - url: `${this._fdkConfig.cluster}/service/platform/webhook/v2.0/company/${platformClient.config.companyId}/subscriber`, - data: subscriberConfig, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "x-ext-lib-version": `js/${version}` - } - } - return await fdkAxios.request(rawRequest); - } - catch(err){ - if(subscriberConfig.provider !== "rest"){ - logger.debug(`Webhook Subscriber Config type ${subscriberConfig.provider} is not supported with current fp version`) - return; - } - if(err.code != '404'){ - throw err; - } - const eventsList = subscriberConfig.events; - delete subscriberConfig.events; - const provider = subscriberConfig.provider; - delete subscriberConfig.provider; - subscriberConfig.event_id = []; - eventsList?.forEach((event) => { - subscriberConfig.event_id.push(eventConfig.eventsMap[event.slug]); - }) - - const rawRequest = { - method: "put", - url: `${this._fdkConfig.cluster}/service/platform/webhook/v1.0/company/${platformClient.config.companyId}/subscriber`, - data: subscriberConfig, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "x-ext-lib-version": `js/${version}` - } - } - const response = await fdkAxios.request(rawRequest); - subscriberConfig.events = eventsList; - subscriberConfig.provider = provider; - return response; - } - } catch(err) { - if ( - RetryManger.shouldRetryOnError(err) - && !this._retryManager.isRetryInProgress(uniqueKey) - ) { - return await this._retryManager.retry( - uniqueKey, - this.updateSubscriberConfig.bind(this), - platformClient, - subscriberConfig - ); - } - throw new FdkWebhookRegistrationError(`Error while updating webhook subscriber configuration. Reason: ${err.message}`); - } - } - - async getSubscriberConfig(platformClient) { - const uniqueKey = `getSubscribersByExtensionId_${platformClient.config.companyId}_${this._fdkConfig.api_key}`; - const token = await platformClient.config.oauthClient.getAccessToken(); - const retryInfo = this._retryManager.retryInfoMap.get(uniqueKey); - if (retryInfo && !retryInfo.isRetry) { - this._retryManager.resetRetryState(uniqueKey); - } - - try { - const rawRequest = { - method: "get", - url: `${this._fdkConfig.cluster}/service/platform/webhook/v1.0/company/${platformClient.config.companyId}/extension/${this._fdkConfig.api_key}/subscriber`, - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - "x-ext-lib-version": `js/${version}` - } - } - const subscriberConfigResponse = await fdkAxios.request(rawRequest); - - const subscriberConfig = {}; - subscriberConfigResponse.items.forEach((config) => { - if(config.provider){ - subscriberConfig[config.provider] = config - } - }) - - return subscriberConfig; - } - catch(err){ - if ( - RetryManger.shouldRetryOnError(err) - && !this._retryManager.isRetryInProgress(uniqueKey) - ) { - return await this._retryManager.retry( - uniqueKey, - this.getSubscriberConfig.bind(this), - platformClient - ); - } - throw new FdkInvalidWebhookConfig(`Error while fetching webhook subscriber configuration, Reason: ${err.message}`); - } - } - async getEventConfig(handlerConfig) { let url = `${this._fdkConfig.cluster}/service/common/webhook/v1.0/events/query-event-details`; const uniqueKey = `${url}_${this._fdkConfig.api_key}`; @@ -866,4 +348,4 @@ class WebhookRegistry { module.exports = { WebhookRegistry -} \ No newline at end of file +} diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 00000000..6b5f6511 --- /dev/null +++ b/index.d.ts @@ -0,0 +1 @@ +export * from "./express"; diff --git a/package.json b/package.json index 820393d8..b396d4c1 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.1.5", "description": "FDK Extension Helper Library", "main": "index.js", + "types": "index.d.ts", "directories": { "example": "examples" },