From 95cee29b4438667d3e50c8e14bac8eb688bc8a92 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Thu, 5 Dec 2024 12:53:11 +0100 Subject: [PATCH] build: release v5.1.0 --- dist/client.cjs | 507 +++++++++++++++++++-------- dist/client.d.ts | 90 ++--- dist/client.d.ts.map | 2 +- dist/client.js | 507 +++++++++++++++++++-------- dist/utils/configuration.d.ts | 12 +- dist/utils/configuration.d.ts.map | 2 +- dist/utils/toolingApiHelper.d.ts | 9 + dist/utils/toolingApiHelper.d.ts.map | 1 + dist/utils/types.d.ts | 78 +++++ dist/utils/types.d.ts.map | 1 + package-lock.json | 61 ++-- package.json | 12 +- 12 files changed, 886 insertions(+), 396 deletions(-) create mode 100644 dist/utils/toolingApiHelper.d.ts create mode 100644 dist/utils/toolingApiHelper.d.ts.map create mode 100644 dist/utils/types.d.ts create mode 100644 dist/utils/types.d.ts.map diff --git a/dist/client.cjs b/dist/client.cjs index bef943b..db6f768 100644 --- a/dist/client.cjs +++ b/dist/client.cjs @@ -68,6 +68,26 @@ var SchemaCache = class { } }; +// src/utils/types.js +var SubscribeCallbackType = { + EVENT: "event", + LAST_EVENT: "lastEvent", + ERROR: "error", + END: "end", + GRPC_STATUS: "grpcStatus", + GRPC_KEEP_ALIVE: "grpcKeepAlive" +}; +var AuthType = { + USER_SUPPLIED: "user-supplied", + USERNAME_PASSWORD: "username-password", + OAUTH_CLIENT_CREDENTIALS: "oauth-client-credentials", + OAUTH_JWT_BEARER: "oauth-jwt-bearer" +}; +var EventSubscriptionAdminState = { + RUN: "RUN", + STOP: "STOP" +}; + // src/utils/eventParseError.js var EventParseError = class extends Error { /** @@ -147,12 +167,6 @@ var CustomLongAvroType = import_avro_js.default.types.LongType.using({ // src/utils/configuration.js var DEFAULT_PUB_SUB_ENDPOINT = "api.pubsub.salesforce.com:7443"; -var AuthType = { - USER_SUPPLIED: "user-supplied", - USERNAME_PASSWORD: "username-password", - OAUTH_CLIENT_CREDENTIALS: "oauth-client-credentials", - OAUTH_JWT_BEARER: "oauth-jwt-bearer" -}; var Configuration = class _Configuration { /** * @param {Configuration} config the client configuration @@ -378,9 +392,38 @@ function hexToBin(hex) { return bin; } +// src/utils/toolingApiHelper.js +var import_jsforce = __toESM(require("jsforce"), 1); +var API_VERSION = "62.0"; +var MANAGED_SUBSCRIPTION_KEY_PREFIX = "18x"; +async function getManagedSubscription(instanceUrl, accessToken, subscriptionIdOrName) { + const conn = new import_jsforce.default.Connection({ instanceUrl, accessToken }); + if (subscriptionIdOrName.indexOf("'") !== -1) { + throw new Error( + `Suspected SOQL injection in subscription ID or name string value: ${subscriptionIdOrName}` + ); + } + let filter; + if ((subscriptionIdOrName.length === 15 || subscriptionIdOrName.length === 18) && subscriptionIdOrName.toLowerCase().startsWith(MANAGED_SUBSCRIPTION_KEY_PREFIX)) { + filter = `Id='${subscriptionIdOrName}'`; + } else { + filter = `DeveloperName='${subscriptionIdOrName}'`; + } + const query = `SELECT Id, DeveloperName, Metadata FROM ManagedEventSubscription WHERE ${filter} LIMIT 1`; + const res = await conn.request( + `/services/data/v${API_VERSION}/tooling/query/?q=${encodeURIComponent(query)}` + ); + if (res.size === 0) { + throw new Error( + `Failed to retrieve managed event subscription with ${filter}` + ); + } + return res.records[0]; +} + // src/utils/auth.js var import_crypto = __toESM(require("crypto"), 1); -var import_jsforce = __toESM(require("jsforce"), 1); +var import_jsforce2 = __toESM(require("jsforce"), 1); var import_undici = require("undici"); var SalesforceAuth = class { /** @@ -431,7 +474,7 @@ var SalesforceAuth = class { */ async #authWithUsernamePassword() { const { loginUrl, username, password, userToken } = this.#config; - const sfConnection = new import_jsforce.default.Connection({ + const sfConnection = new import_jsforce2.default.Connection({ loginUrl }); await sfConnection.login(username, `${password}${userToken}`); @@ -526,14 +569,6 @@ function base64url(input) { } // src/client.js -var SubscribeCallbackType = { - EVENT: "event", - LAST_EVENT: "lastEvent", - ERROR: "error", - END: "end", - GRPC_STATUS: "grpcStatus", - GRPC_KEEP_ALIVE: "grpcKeepAlive" -}; var MAX_EVENT_BATCH_SIZE = 100; var PubSubApiClient = class { /** @@ -556,6 +591,11 @@ var PubSubApiClient = class { * @type {Map} */ #subscriptions; + /** + * Map of managed subscriptions indexed by subscription ID + * @type {Map} + */ + #managedSubscriptions; /** * Logger * @type {Logger} @@ -570,6 +610,7 @@ var PubSubApiClient = class { this.#logger = logger; this.#schemaChache = new SchemaCache(); this.#subscriptions = /* @__PURE__ */ new Map(); + this.#managedSubscriptions = /* @__PURE__ */ new Map(); try { this.#config = Configuration.load(config); } catch (error) { @@ -638,7 +679,7 @@ var PubSubApiClient = class { } } /** - * Get connectivity state from current channel. + * Gets the gRPC connectivity state from the current channel. * @returns {Promise} Promise that holds channel's connectivity information {@link connectivityState} * @memberof PubSubApiClient.prototype */ @@ -713,22 +754,7 @@ var PubSubApiClient = class { isInfiniteEventRequest = true; subscribeRequest.numRequested = numRequested = MAX_EVENT_BATCH_SIZE; } else { - if (typeof numRequested !== "number") { - throw new Error( - `Expected a number type for number of requested events but got ${typeof numRequested}` - ); - } - if (!Number.isSafeInteger(numRequested) || numRequested < 1) { - throw new Error( - `Expected an integer greater than 1 for number of requested events but got ${numRequested}` - ); - } - if (numRequested > MAX_EVENT_BATCH_SIZE) { - this.#logger.warn( - `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` - ); - subscribeRequest.numRequested = MAX_EVENT_BATCH_SIZE; - } + subscribeRequest.numRequested = this.#validateRequestedEventCount(topicName, numRequested); } if (!this.#client) { throw new Error("Pub/Sub API client is not connected."); @@ -742,6 +768,7 @@ var PubSubApiClient = class { grpcSubscription = subscription.grpcSubscription; subscription.info.receivedEventCount = 0; subscription.info.requestedEventCount = subscribeRequest.numRequested; + subscription.info.isInfiniteEventRequest = isInfiniteEventRequest; } else { this.#logger.debug( `${topicName} - Establishing new gRPC subscription` @@ -749,6 +776,7 @@ var PubSubApiClient = class { grpcSubscription = this.#client.Subscribe(); subscription = { info: { + isManaged: false, topicName, requestedEventCount: subscribeRequest.numRequested, receivedEventCount: 0, @@ -759,123 +787,102 @@ var PubSubApiClient = class { }; this.#subscriptions.set(topicName, subscription); } - grpcSubscription.on("data", async (data) => { - const latestReplayId = decodeReplayId(data.latestReplayId); - subscription.info.lastReplayId = latestReplayId; - if (data.events) { - this.#logger.info( - `${topicName} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` - ); - for (const event of data.events) { - try { - this.#logger.debug( - `${topicName} - Raw event: ${toJsonString(event)}` - ); - this.#logger.debug( - `${topicName} - Retrieving schema ID: ${event.event.schemaId}` - ); - const schema = await this.#getEventSchemaFromId( - event.event.schemaId - ); - const subscription2 = this.#subscriptions.get(topicName); - if (!subscription2) { - throw new Error( - `Failed to retrieve subscription for topic ${topicName}.` - ); - } - subscription2.info.receivedEventCount++; - const parsedEvent = parseEvent(schema, event); - this.#logger.debug( - `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` - ); - subscribeCallback( - subscription2.info, - SubscribeCallbackType.EVENT, - parsedEvent - ); - } catch (error) { - let replayId; - try { - if (event.replayId) { - replayId = decodeReplayId(event.replayId); - } - } catch (error2) { - } - const message = replayId ? `Failed to parse event with replay ID ${replayId}` : `Failed to parse event with unknown replay ID (latest replay ID was ${latestReplayId})`; - const parseError = new EventParseError( - message, - error, - replayId, - event, - latestReplayId - ); - subscribeCallback( - subscription.info, - SubscribeCallbackType.ERROR, - parseError - ); - this.#logger.error(parseError); - } - if (subscription.info.receivedEventCount === subscription.info.requestedEventCount) { - this.#logger.debug( - `${topicName} - Reached last of ${subscription.info.requestedEventCount} requested event on channel.` - ); - if (isInfiniteEventRequest) { - this.requestAdditionalEvents( - subscription.info.topicName, - subscription.info.requestedEventCount - ); - } else { - subscribeCallback( - subscription.info, - SubscribeCallbackType.LAST_EVENT - ); - } - } - } - } else { - this.#logger.debug( - `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` - ); - data.latestReplayId = latestReplayId; - subscribeCallback( - subscription.info, - SubscribeCallbackType.GRPC_KEEP_ALIVE - ); - } - }); - grpcSubscription.on("end", () => { - this.#subscriptions.delete(topicName); - this.#logger.info(`${topicName} - gRPC stream ended`); - subscribeCallback(subscription.info, SubscribeCallbackType.END); - }); - grpcSubscription.on("error", (error) => { - this.#logger.error( - `${topicName} - gRPC stream error: ${JSON.stringify(error)}` - ); - subscribeCallback( - subscription.info, - SubscribeCallbackType.ERROR, - error + this.#injectEventHandlingLogic(subscription, subscribeCallback); + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `${topicName} - Subscribe request sent for ${numRequested} events` + ); + } catch (error) { + throw new Error( + `Failed to subscribe to events for topic ${topicName}`, + { cause: error } + ); + } + } + /** + * Subscribes to a topic thanks to a managed subscription. + * @param {string} subscriptionIdOrName managed subscription ID or developer name + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events + * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. + * @throws Throws an error if the managed subscription does not exist or is not in the `RUN` state. + * @memberof PubSubApiClient.prototype + */ + async subscribeWithManagedSubscription(subscriptionIdOrName, subscribeCallback, numRequested = null) { + this.#logger.debug( + `Preparing managed subscribe request: ${JSON.stringify({ subscriptionIdOrName, numRequested })}` + ); + const managedSubscription = await getManagedSubscription( + this.#config.instanceUrl, + this.#config.accessToken, + subscriptionIdOrName + ); + const subscriptionId = managedSubscription.Id; + const subscriptionName = managedSubscription.DeveloperName; + const subscriptionLabel = `${subscriptionName} (${subscriptionId})`; + const { topicName, state } = managedSubscription.Metadata; + this.#logger.info( + `Retrieved managed subscription ${subscriptionLabel}: ${JSON.stringify(managedSubscription.Metadata)}` + ); + if (state !== EventSubscriptionAdminState.RUN) { + throw new Error( + `Can't subscribe to managed subscription ${subscriptionLabel}: subscription is in ${state} state` + ); + } + try { + let isInfiniteEventRequest = false; + if (numRequested === null || numRequested === void 0) { + isInfiniteEventRequest = true; + numRequested = MAX_EVENT_BATCH_SIZE; + } else { + numRequested = this.#validateRequestedEventCount( + topicName, + numRequested ); - }); - grpcSubscription.on("status", (status) => { - this.#logger.info( - `${topicName} - gRPC stream status: ${JSON.stringify(status)}` + } + if (!this.#client) { + throw new Error("Pub/Sub API client is not connected."); + } + let subscription = this.#managedSubscriptions.get(subscriptionId); + let grpcSubscription; + if (subscription) { + this.#logger.debug( + `${topicName} - Reusing cached gRPC subscription` ); - subscribeCallback( - subscription.info, - SubscribeCallbackType.GRPC_STATUS, - status + grpcSubscription = subscription.grpcSubscription; + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = numRequested; + subscription.info.isInfiniteEventRequest = isInfiniteEventRequest; + } else { + this.#logger.debug( + `${topicName} - Establishing new gRPC subscription` ); + grpcSubscription = this.#client.ManagedSubscribe(); + subscription = { + info: { + isManaged: true, + topicName, + subscriptionId, + subscriptionName, + requestedEventCount: numRequested, + receivedEventCount: 0, + lastReplayId: null + }, + grpcSubscription, + subscribeCallback + }; + this.#managedSubscriptions.set(subscriptionId, subscription); + } + this.#injectEventHandlingLogic(subscription, subscribeCallback); + grpcSubscription.write({ + subscriptionId, + numRequested }); - grpcSubscription.write(subscribeRequest); this.#logger.info( - `${topicName} - Subscribe request sent for ${numRequested} events` + `${topicName} - Managed subscribe request sent to ${subscriptionLabel} for ${numRequested} events` ); } catch (error) { throw new Error( - `Failed to subscribe to events for topic ${topicName}`, + `Failed to subscribe to managed subscription ${subscriptionLabel}`, { cause: error } ); } @@ -883,13 +890,13 @@ var PubSubApiClient = class { /** * Request additional events on an existing subscription. * @param {string} topicName topic name - * @param {number} numRequested number of events requested. + * @param {number} numRequested number of events requested */ requestAdditionalEvents(topicName, numRequested) { const subscription = this.#subscriptions.get(topicName); if (!subscription) { throw new Error( - `Failed to request additional events for topic ${topicName}, no active subscription found.` + `Failed to request additional events for topic ${topicName}: no active subscription found.` ); } subscription.info.receivedEventCount = 0; @@ -902,6 +909,56 @@ var PubSubApiClient = class { `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } + /** + * Request additional events on an existing managed subscription. + * @param {string} subscriptionId managed subscription ID + * @param {number} numRequested number of events requested + */ + requestAdditionalManagedEvents(subscriptionId, numRequested) { + const subscription = this.#managedSubscriptions.get(subscriptionId); + if (!subscription) { + throw new Error( + `Failed to request additional events for managed subscription with ID ${subscriptionId}: no active subscription found.` + ); + } + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = numRequested; + subscription.grpcSubscription.write({ + subscriptionId, + numRequested + }); + const { subscriptionName } = subscription.info; + this.#logger.debug( + `${subscriptionName} (${subscriptionId}) - Resubscribing to a batch of ${numRequested} events` + ); + } + /** + * Commits a replay ID on a managed subscription. + * @param {string} subscriptionId managed subscription ID + * @param {number} replayId event replay ID + * @returns {string} commit request UUID + */ + commitReplayId(subscriptionId, replayId) { + const subscription = this.#managedSubscriptions.get(subscriptionId); + if (!subscription) { + throw new Error( + `Failed to commit a replay ID on managed subscription with ID ${subscriptionId}: no active subscription found.` + ); + } + const commitRequestId = import_crypto2.default.randomUUID(); + subscription.grpcSubscription.write({ + subscriptionId, + commitReplayIdRequest: { + commitRequestId, + replayId: encodeReplayId(replayId) + } + }); + const { subscriptionName } = subscription.info; + this.#logger.debug( + `${subscriptionName} (${subscriptionId}) - Sent replay ID commit request (request ID: ${commitRequestId}, replay ID: ${replayId})` + ); + return commitRequestId; + } /** * Publishes a payload to a topic using the gRPC client. * @param {string} topicName name of the topic that we're subscribing to @@ -958,9 +1015,146 @@ var PubSubApiClient = class { close() { this.#logger.info("Clear subscriptions"); this.#subscriptions.clear(); + this.#managedSubscriptions.clear(); this.#logger.info("Closing gRPC stream"); this.#client?.close(); } + /** + * Injects the standard event handling logic on a subscription + * @param {Subscription} subscription + * @param {SubscribeCallback} subscribeCallback + */ + #injectEventHandlingLogic(subscription, subscribeCallback) { + const { grpcSubscription } = subscription; + const { topicName, subscriptionId, subscriptionName, isManaged } = subscription.info; + const logLabel = subscription.info.isManaged ? `${subscriptionName} (${subscriptionId})` : topicName; + grpcSubscription.on("data", async (data) => { + const latestReplayId = decodeReplayId(data.latestReplayId); + subscription.info.lastReplayId = latestReplayId; + if (data.events) { + this.#logger.info( + `${logLabel} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` + ); + for (const event of data.events) { + try { + this.#logger.debug( + `${logLabel} - Raw event: ${toJsonString(event)}` + ); + this.#logger.debug( + `${logLabel} - Retrieving schema ID: ${event.event.schemaId}` + ); + const schema = await this.#getEventSchemaFromId( + event.event.schemaId + ); + let subscription2; + if (isManaged) { + subscription2 = this.#managedSubscriptions.get(subscriptionId); + } else { + subscription2 = this.#subscriptions.get(topicName); + } + if (!subscription2) { + throw new Error( + `Failed to retrieve ${isManaged ? "managed " : ""}subscription: ${logLabel}.` + ); + } + subscription2.info.receivedEventCount++; + const parsedEvent = parseEvent(schema, event); + this.#logger.debug( + `${logLabel} - Parsed event: ${toJsonString(parsedEvent)}` + ); + subscribeCallback( + subscription2.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); + } catch (error) { + let replayId; + try { + if (event.replayId) { + replayId = decodeReplayId(event.replayId); + } + } catch (error2) { + } + const message = replayId ? `Failed to parse event with replay ID ${replayId}` : `Failed to parse event with unknown replay ID (latest replay ID was ${latestReplayId})`; + const parseError = new EventParseError( + message, + error, + replayId, + event, + latestReplayId + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + parseError + ); + this.#logger.error(parseError); + } + if (subscription.info.receivedEventCount === subscription.info.requestedEventCount) { + this.#logger.debug( + `${logLabel} - Reached last of ${subscription.info.requestedEventCount} requested event on channel.` + ); + if (subscription.info.isInfiniteEventRequest) { + if (isManaged) { + this.requestAdditionalManagedEvents( + subscription.info.subscriptionId, + subscription.info.requestedEventCount + ); + } else { + this.requestAdditionalEvents( + subscription.info.topicName, + subscription.info.requestedEventCount + ); + } + } else { + subscribeCallback( + subscription.info, + SubscribeCallbackType.LAST_EVENT + ); + } + } + } + } else { + this.#logger.debug( + `${logLabel} - Received keepalive message. Latest replay ID: ${latestReplayId}` + ); + data.latestReplayId = latestReplayId; + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_KEEP_ALIVE + ); + } + }); + grpcSubscription.on("end", () => { + if (isManaged) { + this.#managedSubscriptions.delete(subscriptionId); + } else { + this.#subscriptions.delete(topicName); + } + this.#logger.info(`${logLabel} - gRPC stream ended`); + subscribeCallback(subscription.info, SubscribeCallbackType.END); + }); + grpcSubscription.on("error", (error) => { + this.#logger.error( + `${logLabel} - gRPC stream error: ${JSON.stringify(error)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error + ); + }); + grpcSubscription.on("status", (status) => { + this.#logger.info( + `${logLabel} - gRPC stream status: ${JSON.stringify(status)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status + ); + }); + } /** * Retrieves an event schema from the cache based on its ID. * If it's not cached, fetches the shema with the gRPC client. @@ -1033,4 +1227,29 @@ var PubSubApiClient = class { }); }); } + /** + * Validates the number of requested events + * @param {string} topicName for logging purposes + * @param {number} numRequested number of requested events + * @returns safe value for number of requested events + */ + #validateRequestedEventCount(topicName, numRequested) { + if (typeof numRequested !== "number") { + throw new Error( + `Expected a number type for number of requested events but got ${typeof numRequested}` + ); + } + if (!Number.isSafeInteger(numRequested) || numRequested < 1) { + throw new Error( + `Expected an integer greater than 1 for number of requested events but got ${numRequested}` + ); + } + if (numRequested > MAX_EVENT_BATCH_SIZE) { + this.#logger.warn( + `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` + ); + return MAX_EVENT_BATCH_SIZE; + } + return numRequested; + } }; diff --git a/dist/client.d.ts b/dist/client.d.ts index 839e9ee..cd9dcdd 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -18,7 +18,7 @@ export default class PubSubApiClient { */ connect(): Promise; /** - * Get connectivity state from current channel. + * Gets the gRPC connectivity state from the current channel. * @returns {Promise} Promise that holds channel's connectivity information {@link connectivityState} * @memberof PubSubApiClient.prototype */ @@ -48,12 +48,34 @@ export default class PubSubApiClient { * @memberof PubSubApiClient.prototype */ subscribe(topicName: string, subscribeCallback: SubscribeCallback, numRequested?: number | null): void; + /** + * Subscribes to a topic thanks to a managed subscription. + * @param {string} subscriptionIdOrName managed subscription ID or developer name + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events + * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. + * @throws Throws an error if the managed subscription does not exist or is not in the `RUN` state. + * @memberof PubSubApiClient.prototype + */ + subscribeWithManagedSubscription(subscriptionIdOrName: string, subscribeCallback: SubscribeCallback, numRequested?: number | null): Promise; /** * Request additional events on an existing subscription. * @param {string} topicName topic name - * @param {number} numRequested number of events requested. + * @param {number} numRequested number of events requested */ requestAdditionalEvents(topicName: string, numRequested: number): void; + /** + * Request additional events on an existing managed subscription. + * @param {string} subscriptionId managed subscription ID + * @param {number} numRequested number of events requested + */ + requestAdditionalManagedEvents(subscriptionId: string, numRequested: number): void; + /** + * Commits a replay ID on a managed subscription. + * @param {string} subscriptionId managed subscription ID + * @param {number} replayId event replay ID + * @returns {string} commit request UUID + */ + commitReplayId(subscriptionId: string, replayId: number): string; /** * Publishes a payload to a topic using the gRPC client. * @param {string} topicName name of the topic that we're subscribing to @@ -70,62 +92,12 @@ export default class PubSubApiClient { close(): void; #private; } -export type PublishResult = { - replayId: number; - correlationKey: string; -}; -export type SubscribeCallback = (subscription: SubscriptionInfo, callbackType: SubscribeCallbackType, data?: any) => any; -export type Subscription = { - info: SubscriptionInfo; - grpcSubscription: any; - subscribeCallback: SubscribeCallback; -}; -export type SubscriptionInfo = { - topicName: string; - requestedEventCount: number; - receivedEventCount: number; - lastReplayId: number; -}; -export type Configuration = { - authType: AuthType; - pubSubEndpoint: string; - loginUrl: string; - username: string; - password: string; - userToken: string; - clientId: string; - clientSecret: string; - privateKey: string; - accessToken: string; - instanceUrl: string; - organizationId: string; -}; -export type Logger = { - debug: Function; - info: Function; - error: Function; - warn: Function; -}; -export type SubscribeRequest = { - topicName: string; - numRequested: number; - replayPreset?: number; - replayId?: number; -}; +export type PublishResult = import("./utils/types.js").PublishResult; +export type Subscription = import("./utils/types.js").Subscription; +export type SubscriptionInfo = import("./utils/types.js").SubscriptionInfo; +export type Configuration = import("./utils/types.js").Configuration; +export type Logger = import("./utils/types.js").Logger; +export type SubscribeRequest = import("./utils/types.js").SubscribeRequest; import { connectivityState } from '@grpc/grpc-js'; -import { Configuration } from './utils/configuration.js'; -/** - * Enum for subscripe callback type values - */ -type SubscribeCallbackType = string; -declare namespace SubscribeCallbackType { - let EVENT: string; - let LAST_EVENT: string; - let ERROR: string; - let END: string; - let GRPC_STATUS: string; - let GRPC_KEEP_ALIVE: string; -} -import { AuthType } from './utils/configuration.js'; -export {}; +import Configuration from './utils/configuration.js'; //# sourceMappingURL=client.d.ts.map \ No newline at end of file diff --git a/dist/client.d.ts.map b/dist/client.d.ts.map index b7502b0..7e51f7f 100644 --- a/dist/client.d.ts.map +++ b/dist/client.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.js"],"names":[],"mappings":"AA6GA;;;;GAIG;AACH;IA+BI;;;;OAIG;IACH,oBAHW,aAAa,WACb,MAAM,EAehB;IAED;;;;;OAKG;IACH,WAHa,OAAO,CAAC,IAAI,CAAC,CAkEzB;IAED;;;;OAIG;IACH,wBAHa,OAAO,CAAC,iBAAiB,CAAC,CAKtC;IAED;;;;;;OAMG;IACH,sCALW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,QAgBvB;IAED;;;;;;;OAOG;IACH,iCANW,MAAM,qBACN,iBAAiB,gBACjB,MAAM,GAAG,IAAI,YACb,MAAM,QAkBhB;IAED;;;;;;OAMG;IACH,qBALW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,QAWvB;IAqND;;;;OAIG;IACH,mCAHW,MAAM,gBACN,MAAM,QAqBhB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CA4ClC;IAED;;;OAGG;IACH,cAMC;;CAkFJ;;cA3oBa,MAAM;oBACN,MAAM;;+CAMT,gBAAgB,gBAChB,qBAAqB;;UAOlB,gBAAgB;;uBAEhB,iBAAiB;;;eAMjB,MAAM;yBACN,MAAM;wBACN,MAAM;kBACN,MAAM;;;cAMN,QAAQ;oBACR,MAAM;cACN,MAAM;cACN,MAAM;cACN,MAAM;eACN,MAAM;cACN,MAAM;kBACN,MAAM;gBACN,MAAM;iBACN,MAAM;iBACN,MAAM;oBACN,MAAM;;;;;;;;;eAeN,MAAM;kBACN,MAAM;mBACN,MAAM;eACN,MAAM;;kCA1Fc,eAAe;8BAKT,0BAA0B;;;;6BAWxD,MAAM;;;;;;;;;yBAXwB,0BAA0B"} \ No newline at end of file +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.js"],"names":[],"mappings":"AA4CA;;;;GAIG;AACH;IAqCI;;;;OAIG;IACH,oBAHW,aAAa,WACb,MAAM,EAgBhB;IAED;;;;;OAKG;IACH,WAHa,OAAO,CAAC,IAAI,CAAC,CAkEzB;IAED;;;;OAIG;IACH,wBAHa,OAAO,CAAC,iBAAiB,CAAC,CAKtC;IAED;;;;;;OAMG;IACH,sCALW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,QAgBvB;IAED;;;;;;;OAOG;IACH,iCANW,MAAM,qBACN,iBAAiB,gBACjB,MAAM,GAAG,IAAI,YACb,MAAM,QAkBhB;IAED;;;;;;OAMG;IACH,qBALW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,QAWvB;IA8ED;;;;;;;OAOG;IACH,uDANW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,iBAsGvB;IAED;;;;OAIG;IACH,mCAHW,MAAM,gBACN,MAAM,QAqBhB;IAED;;;;OAIG;IACH,+CAHW,MAAM,gBACN,MAAM,QAsBhB;IAED;;;;;OAKG;IACH,+BAJW,MAAM,YACN,MAAM,GACJ,MAAM,CAyBlB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CA4ClC;IAED;;;OAGG;IACH,cAMC;;CAyQJ;4BA/yBY,OAAO,kBAAkB,EAAE,aAAa;2BACxC,OAAO,kBAAkB,EAAE,YAAY;+BACvC,OAAO,kBAAkB,EAAE,gBAAgB;4BAC3C,OAAO,kBAAkB,EAAE,aAAa;qBACxC,OAAO,kBAAkB,EAAE,MAAM;+BACjC,OAAO,kBAAkB,EAAE,gBAAgB;kCA1BtB,eAAe;0BAUvB,0BAA0B"} \ No newline at end of file diff --git a/dist/client.js b/dist/client.js index 8f4200b..5f25189 100644 --- a/dist/client.js +++ b/dist/client.js @@ -35,6 +35,26 @@ var SchemaCache = class { } }; +// src/utils/types.js +var SubscribeCallbackType = { + EVENT: "event", + LAST_EVENT: "lastEvent", + ERROR: "error", + END: "end", + GRPC_STATUS: "grpcStatus", + GRPC_KEEP_ALIVE: "grpcKeepAlive" +}; +var AuthType = { + USER_SUPPLIED: "user-supplied", + USERNAME_PASSWORD: "username-password", + OAUTH_CLIENT_CREDENTIALS: "oauth-client-credentials", + OAUTH_JWT_BEARER: "oauth-jwt-bearer" +}; +var EventSubscriptionAdminState = { + RUN: "RUN", + STOP: "STOP" +}; + // src/utils/eventParseError.js var EventParseError = class extends Error { /** @@ -114,12 +134,6 @@ var CustomLongAvroType = avro.types.LongType.using({ // src/utils/configuration.js var DEFAULT_PUB_SUB_ENDPOINT = "api.pubsub.salesforce.com:7443"; -var AuthType = { - USER_SUPPLIED: "user-supplied", - USERNAME_PASSWORD: "username-password", - OAUTH_CLIENT_CREDENTIALS: "oauth-client-credentials", - OAUTH_JWT_BEARER: "oauth-jwt-bearer" -}; var Configuration = class _Configuration { /** * @param {Configuration} config the client configuration @@ -345,9 +359,38 @@ function hexToBin(hex) { return bin; } +// src/utils/toolingApiHelper.js +import jsforce from "jsforce"; +var API_VERSION = "62.0"; +var MANAGED_SUBSCRIPTION_KEY_PREFIX = "18x"; +async function getManagedSubscription(instanceUrl, accessToken, subscriptionIdOrName) { + const conn = new jsforce.Connection({ instanceUrl, accessToken }); + if (subscriptionIdOrName.indexOf("'") !== -1) { + throw new Error( + `Suspected SOQL injection in subscription ID or name string value: ${subscriptionIdOrName}` + ); + } + let filter; + if ((subscriptionIdOrName.length === 15 || subscriptionIdOrName.length === 18) && subscriptionIdOrName.toLowerCase().startsWith(MANAGED_SUBSCRIPTION_KEY_PREFIX)) { + filter = `Id='${subscriptionIdOrName}'`; + } else { + filter = `DeveloperName='${subscriptionIdOrName}'`; + } + const query = `SELECT Id, DeveloperName, Metadata FROM ManagedEventSubscription WHERE ${filter} LIMIT 1`; + const res = await conn.request( + `/services/data/v${API_VERSION}/tooling/query/?q=${encodeURIComponent(query)}` + ); + if (res.size === 0) { + throw new Error( + `Failed to retrieve managed event subscription with ${filter}` + ); + } + return res.records[0]; +} + // src/utils/auth.js import crypto from "crypto"; -import jsforce from "jsforce"; +import jsforce2 from "jsforce"; import { fetch } from "undici"; var SalesforceAuth = class { /** @@ -398,7 +441,7 @@ var SalesforceAuth = class { */ async #authWithUsernamePassword() { const { loginUrl, username, password, userToken } = this.#config; - const sfConnection = new jsforce.Connection({ + const sfConnection = new jsforce2.Connection({ loginUrl }); await sfConnection.login(username, `${password}${userToken}`); @@ -493,14 +536,6 @@ function base64url(input) { } // src/client.js -var SubscribeCallbackType = { - EVENT: "event", - LAST_EVENT: "lastEvent", - ERROR: "error", - END: "end", - GRPC_STATUS: "grpcStatus", - GRPC_KEEP_ALIVE: "grpcKeepAlive" -}; var MAX_EVENT_BATCH_SIZE = 100; var PubSubApiClient = class { /** @@ -523,6 +558,11 @@ var PubSubApiClient = class { * @type {Map} */ #subscriptions; + /** + * Map of managed subscriptions indexed by subscription ID + * @type {Map} + */ + #managedSubscriptions; /** * Logger * @type {Logger} @@ -537,6 +577,7 @@ var PubSubApiClient = class { this.#logger = logger; this.#schemaChache = new SchemaCache(); this.#subscriptions = /* @__PURE__ */ new Map(); + this.#managedSubscriptions = /* @__PURE__ */ new Map(); try { this.#config = Configuration.load(config); } catch (error) { @@ -605,7 +646,7 @@ var PubSubApiClient = class { } } /** - * Get connectivity state from current channel. + * Gets the gRPC connectivity state from the current channel. * @returns {Promise} Promise that holds channel's connectivity information {@link connectivityState} * @memberof PubSubApiClient.prototype */ @@ -680,22 +721,7 @@ var PubSubApiClient = class { isInfiniteEventRequest = true; subscribeRequest.numRequested = numRequested = MAX_EVENT_BATCH_SIZE; } else { - if (typeof numRequested !== "number") { - throw new Error( - `Expected a number type for number of requested events but got ${typeof numRequested}` - ); - } - if (!Number.isSafeInteger(numRequested) || numRequested < 1) { - throw new Error( - `Expected an integer greater than 1 for number of requested events but got ${numRequested}` - ); - } - if (numRequested > MAX_EVENT_BATCH_SIZE) { - this.#logger.warn( - `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` - ); - subscribeRequest.numRequested = MAX_EVENT_BATCH_SIZE; - } + subscribeRequest.numRequested = this.#validateRequestedEventCount(topicName, numRequested); } if (!this.#client) { throw new Error("Pub/Sub API client is not connected."); @@ -709,6 +735,7 @@ var PubSubApiClient = class { grpcSubscription = subscription.grpcSubscription; subscription.info.receivedEventCount = 0; subscription.info.requestedEventCount = subscribeRequest.numRequested; + subscription.info.isInfiniteEventRequest = isInfiniteEventRequest; } else { this.#logger.debug( `${topicName} - Establishing new gRPC subscription` @@ -716,6 +743,7 @@ var PubSubApiClient = class { grpcSubscription = this.#client.Subscribe(); subscription = { info: { + isManaged: false, topicName, requestedEventCount: subscribeRequest.numRequested, receivedEventCount: 0, @@ -726,123 +754,102 @@ var PubSubApiClient = class { }; this.#subscriptions.set(topicName, subscription); } - grpcSubscription.on("data", async (data) => { - const latestReplayId = decodeReplayId(data.latestReplayId); - subscription.info.lastReplayId = latestReplayId; - if (data.events) { - this.#logger.info( - `${topicName} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` - ); - for (const event of data.events) { - try { - this.#logger.debug( - `${topicName} - Raw event: ${toJsonString(event)}` - ); - this.#logger.debug( - `${topicName} - Retrieving schema ID: ${event.event.schemaId}` - ); - const schema = await this.#getEventSchemaFromId( - event.event.schemaId - ); - const subscription2 = this.#subscriptions.get(topicName); - if (!subscription2) { - throw new Error( - `Failed to retrieve subscription for topic ${topicName}.` - ); - } - subscription2.info.receivedEventCount++; - const parsedEvent = parseEvent(schema, event); - this.#logger.debug( - `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` - ); - subscribeCallback( - subscription2.info, - SubscribeCallbackType.EVENT, - parsedEvent - ); - } catch (error) { - let replayId; - try { - if (event.replayId) { - replayId = decodeReplayId(event.replayId); - } - } catch (error2) { - } - const message = replayId ? `Failed to parse event with replay ID ${replayId}` : `Failed to parse event with unknown replay ID (latest replay ID was ${latestReplayId})`; - const parseError = new EventParseError( - message, - error, - replayId, - event, - latestReplayId - ); - subscribeCallback( - subscription.info, - SubscribeCallbackType.ERROR, - parseError - ); - this.#logger.error(parseError); - } - if (subscription.info.receivedEventCount === subscription.info.requestedEventCount) { - this.#logger.debug( - `${topicName} - Reached last of ${subscription.info.requestedEventCount} requested event on channel.` - ); - if (isInfiniteEventRequest) { - this.requestAdditionalEvents( - subscription.info.topicName, - subscription.info.requestedEventCount - ); - } else { - subscribeCallback( - subscription.info, - SubscribeCallbackType.LAST_EVENT - ); - } - } - } - } else { - this.#logger.debug( - `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` - ); - data.latestReplayId = latestReplayId; - subscribeCallback( - subscription.info, - SubscribeCallbackType.GRPC_KEEP_ALIVE - ); - } - }); - grpcSubscription.on("end", () => { - this.#subscriptions.delete(topicName); - this.#logger.info(`${topicName} - gRPC stream ended`); - subscribeCallback(subscription.info, SubscribeCallbackType.END); - }); - grpcSubscription.on("error", (error) => { - this.#logger.error( - `${topicName} - gRPC stream error: ${JSON.stringify(error)}` - ); - subscribeCallback( - subscription.info, - SubscribeCallbackType.ERROR, - error + this.#injectEventHandlingLogic(subscription, subscribeCallback); + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `${topicName} - Subscribe request sent for ${numRequested} events` + ); + } catch (error) { + throw new Error( + `Failed to subscribe to events for topic ${topicName}`, + { cause: error } + ); + } + } + /** + * Subscribes to a topic thanks to a managed subscription. + * @param {string} subscriptionIdOrName managed subscription ID or developer name + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events + * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. + * @throws Throws an error if the managed subscription does not exist or is not in the `RUN` state. + * @memberof PubSubApiClient.prototype + */ + async subscribeWithManagedSubscription(subscriptionIdOrName, subscribeCallback, numRequested = null) { + this.#logger.debug( + `Preparing managed subscribe request: ${JSON.stringify({ subscriptionIdOrName, numRequested })}` + ); + const managedSubscription = await getManagedSubscription( + this.#config.instanceUrl, + this.#config.accessToken, + subscriptionIdOrName + ); + const subscriptionId = managedSubscription.Id; + const subscriptionName = managedSubscription.DeveloperName; + const subscriptionLabel = `${subscriptionName} (${subscriptionId})`; + const { topicName, state } = managedSubscription.Metadata; + this.#logger.info( + `Retrieved managed subscription ${subscriptionLabel}: ${JSON.stringify(managedSubscription.Metadata)}` + ); + if (state !== EventSubscriptionAdminState.RUN) { + throw new Error( + `Can't subscribe to managed subscription ${subscriptionLabel}: subscription is in ${state} state` + ); + } + try { + let isInfiniteEventRequest = false; + if (numRequested === null || numRequested === void 0) { + isInfiniteEventRequest = true; + numRequested = MAX_EVENT_BATCH_SIZE; + } else { + numRequested = this.#validateRequestedEventCount( + topicName, + numRequested ); - }); - grpcSubscription.on("status", (status) => { - this.#logger.info( - `${topicName} - gRPC stream status: ${JSON.stringify(status)}` + } + if (!this.#client) { + throw new Error("Pub/Sub API client is not connected."); + } + let subscription = this.#managedSubscriptions.get(subscriptionId); + let grpcSubscription; + if (subscription) { + this.#logger.debug( + `${topicName} - Reusing cached gRPC subscription` ); - subscribeCallback( - subscription.info, - SubscribeCallbackType.GRPC_STATUS, - status + grpcSubscription = subscription.grpcSubscription; + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = numRequested; + subscription.info.isInfiniteEventRequest = isInfiniteEventRequest; + } else { + this.#logger.debug( + `${topicName} - Establishing new gRPC subscription` ); + grpcSubscription = this.#client.ManagedSubscribe(); + subscription = { + info: { + isManaged: true, + topicName, + subscriptionId, + subscriptionName, + requestedEventCount: numRequested, + receivedEventCount: 0, + lastReplayId: null + }, + grpcSubscription, + subscribeCallback + }; + this.#managedSubscriptions.set(subscriptionId, subscription); + } + this.#injectEventHandlingLogic(subscription, subscribeCallback); + grpcSubscription.write({ + subscriptionId, + numRequested }); - grpcSubscription.write(subscribeRequest); this.#logger.info( - `${topicName} - Subscribe request sent for ${numRequested} events` + `${topicName} - Managed subscribe request sent to ${subscriptionLabel} for ${numRequested} events` ); } catch (error) { throw new Error( - `Failed to subscribe to events for topic ${topicName}`, + `Failed to subscribe to managed subscription ${subscriptionLabel}`, { cause: error } ); } @@ -850,13 +857,13 @@ var PubSubApiClient = class { /** * Request additional events on an existing subscription. * @param {string} topicName topic name - * @param {number} numRequested number of events requested. + * @param {number} numRequested number of events requested */ requestAdditionalEvents(topicName, numRequested) { const subscription = this.#subscriptions.get(topicName); if (!subscription) { throw new Error( - `Failed to request additional events for topic ${topicName}, no active subscription found.` + `Failed to request additional events for topic ${topicName}: no active subscription found.` ); } subscription.info.receivedEventCount = 0; @@ -869,6 +876,56 @@ var PubSubApiClient = class { `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } + /** + * Request additional events on an existing managed subscription. + * @param {string} subscriptionId managed subscription ID + * @param {number} numRequested number of events requested + */ + requestAdditionalManagedEvents(subscriptionId, numRequested) { + const subscription = this.#managedSubscriptions.get(subscriptionId); + if (!subscription) { + throw new Error( + `Failed to request additional events for managed subscription with ID ${subscriptionId}: no active subscription found.` + ); + } + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = numRequested; + subscription.grpcSubscription.write({ + subscriptionId, + numRequested + }); + const { subscriptionName } = subscription.info; + this.#logger.debug( + `${subscriptionName} (${subscriptionId}) - Resubscribing to a batch of ${numRequested} events` + ); + } + /** + * Commits a replay ID on a managed subscription. + * @param {string} subscriptionId managed subscription ID + * @param {number} replayId event replay ID + * @returns {string} commit request UUID + */ + commitReplayId(subscriptionId, replayId) { + const subscription = this.#managedSubscriptions.get(subscriptionId); + if (!subscription) { + throw new Error( + `Failed to commit a replay ID on managed subscription with ID ${subscriptionId}: no active subscription found.` + ); + } + const commitRequestId = crypto2.randomUUID(); + subscription.grpcSubscription.write({ + subscriptionId, + commitReplayIdRequest: { + commitRequestId, + replayId: encodeReplayId(replayId) + } + }); + const { subscriptionName } = subscription.info; + this.#logger.debug( + `${subscriptionName} (${subscriptionId}) - Sent replay ID commit request (request ID: ${commitRequestId}, replay ID: ${replayId})` + ); + return commitRequestId; + } /** * Publishes a payload to a topic using the gRPC client. * @param {string} topicName name of the topic that we're subscribing to @@ -925,9 +982,146 @@ var PubSubApiClient = class { close() { this.#logger.info("Clear subscriptions"); this.#subscriptions.clear(); + this.#managedSubscriptions.clear(); this.#logger.info("Closing gRPC stream"); this.#client?.close(); } + /** + * Injects the standard event handling logic on a subscription + * @param {Subscription} subscription + * @param {SubscribeCallback} subscribeCallback + */ + #injectEventHandlingLogic(subscription, subscribeCallback) { + const { grpcSubscription } = subscription; + const { topicName, subscriptionId, subscriptionName, isManaged } = subscription.info; + const logLabel = subscription.info.isManaged ? `${subscriptionName} (${subscriptionId})` : topicName; + grpcSubscription.on("data", async (data) => { + const latestReplayId = decodeReplayId(data.latestReplayId); + subscription.info.lastReplayId = latestReplayId; + if (data.events) { + this.#logger.info( + `${logLabel} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` + ); + for (const event of data.events) { + try { + this.#logger.debug( + `${logLabel} - Raw event: ${toJsonString(event)}` + ); + this.#logger.debug( + `${logLabel} - Retrieving schema ID: ${event.event.schemaId}` + ); + const schema = await this.#getEventSchemaFromId( + event.event.schemaId + ); + let subscription2; + if (isManaged) { + subscription2 = this.#managedSubscriptions.get(subscriptionId); + } else { + subscription2 = this.#subscriptions.get(topicName); + } + if (!subscription2) { + throw new Error( + `Failed to retrieve ${isManaged ? "managed " : ""}subscription: ${logLabel}.` + ); + } + subscription2.info.receivedEventCount++; + const parsedEvent = parseEvent(schema, event); + this.#logger.debug( + `${logLabel} - Parsed event: ${toJsonString(parsedEvent)}` + ); + subscribeCallback( + subscription2.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); + } catch (error) { + let replayId; + try { + if (event.replayId) { + replayId = decodeReplayId(event.replayId); + } + } catch (error2) { + } + const message = replayId ? `Failed to parse event with replay ID ${replayId}` : `Failed to parse event with unknown replay ID (latest replay ID was ${latestReplayId})`; + const parseError = new EventParseError( + message, + error, + replayId, + event, + latestReplayId + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + parseError + ); + this.#logger.error(parseError); + } + if (subscription.info.receivedEventCount === subscription.info.requestedEventCount) { + this.#logger.debug( + `${logLabel} - Reached last of ${subscription.info.requestedEventCount} requested event on channel.` + ); + if (subscription.info.isInfiniteEventRequest) { + if (isManaged) { + this.requestAdditionalManagedEvents( + subscription.info.subscriptionId, + subscription.info.requestedEventCount + ); + } else { + this.requestAdditionalEvents( + subscription.info.topicName, + subscription.info.requestedEventCount + ); + } + } else { + subscribeCallback( + subscription.info, + SubscribeCallbackType.LAST_EVENT + ); + } + } + } + } else { + this.#logger.debug( + `${logLabel} - Received keepalive message. Latest replay ID: ${latestReplayId}` + ); + data.latestReplayId = latestReplayId; + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_KEEP_ALIVE + ); + } + }); + grpcSubscription.on("end", () => { + if (isManaged) { + this.#managedSubscriptions.delete(subscriptionId); + } else { + this.#subscriptions.delete(topicName); + } + this.#logger.info(`${logLabel} - gRPC stream ended`); + subscribeCallback(subscription.info, SubscribeCallbackType.END); + }); + grpcSubscription.on("error", (error) => { + this.#logger.error( + `${logLabel} - gRPC stream error: ${JSON.stringify(error)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error + ); + }); + grpcSubscription.on("status", (status) => { + this.#logger.info( + `${logLabel} - gRPC stream status: ${JSON.stringify(status)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status + ); + }); + } /** * Retrieves an event schema from the cache based on its ID. * If it's not cached, fetches the shema with the gRPC client. @@ -1000,6 +1194,31 @@ var PubSubApiClient = class { }); }); } + /** + * Validates the number of requested events + * @param {string} topicName for logging purposes + * @param {number} numRequested number of requested events + * @returns safe value for number of requested events + */ + #validateRequestedEventCount(topicName, numRequested) { + if (typeof numRequested !== "number") { + throw new Error( + `Expected a number type for number of requested events but got ${typeof numRequested}` + ); + } + if (!Number.isSafeInteger(numRequested) || numRequested < 1) { + throw new Error( + `Expected an integer greater than 1 for number of requested events but got ${numRequested}` + ); + } + if (numRequested > MAX_EVENT_BATCH_SIZE) { + this.#logger.warn( + `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` + ); + return MAX_EVENT_BATCH_SIZE; + } + return numRequested; + } }; export { PubSubApiClient as default diff --git a/dist/utils/configuration.d.ts b/dist/utils/configuration.d.ts index 73cc818..b34d065 100644 --- a/dist/utils/configuration.d.ts +++ b/dist/utils/configuration.d.ts @@ -1,14 +1,4 @@ -/** - * Enum for auth type values - */ -export type AuthType = string; -export namespace AuthType { - let USER_SUPPLIED: string; - let USERNAME_PASSWORD: string; - let OAUTH_CLIENT_CREDENTIALS: string; - let OAUTH_JWT_BEARER: string; -} -export class Configuration { +export default class Configuration { /** * @param {Configuration} config the client configuration * @returns {Configuration} the sanitized client configuration diff --git a/dist/utils/configuration.d.ts.map b/dist/utils/configuration.d.ts.map index 593c0cc..d44659b 100644 --- a/dist/utils/configuration.d.ts.map +++ b/dist/utils/configuration.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../../src/utils/configuration.js"],"names":[],"mappings":";;;uBAIU,MAAM;;;;;;;AAShB;IACI;;;OAGG;IACH,oBAHW,aAAa,GACX,aAAa,CAyCzB;IAED;;;OAGG;IACH,4CAHW,aAAa,GACX,aAAa,CAoCzB;IAED,yEAQC;CACJ"} \ No newline at end of file +{"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../../src/utils/configuration.js"],"names":[],"mappings":"AAIA;IACI;;;OAGG;IACH,oBAHW,aAAa,GACX,aAAa,CAyCzB;IAED;;;OAGG;IACH,4CAHW,aAAa,GACX,aAAa,CAoCzB;IAED,yEAQC;CACJ"} \ No newline at end of file diff --git a/dist/utils/toolingApiHelper.d.ts b/dist/utils/toolingApiHelper.d.ts new file mode 100644 index 0000000..70d5a26 --- /dev/null +++ b/dist/utils/toolingApiHelper.d.ts @@ -0,0 +1,9 @@ +/** + * Calls the Tooling API to retrieve a managed subscription from its ID or name + * @param {string} instanceUrl + * @param {string} accessToken + * @param {string} subscriptionIdOrName + * @returns topic name + */ +export function getManagedSubscription(instanceUrl: string, accessToken: string, subscriptionIdOrName: string): Promise; +//# sourceMappingURL=toolingApiHelper.d.ts.map \ No newline at end of file diff --git a/dist/utils/toolingApiHelper.d.ts.map b/dist/utils/toolingApiHelper.d.ts.map new file mode 100644 index 0000000..971f01b --- /dev/null +++ b/dist/utils/toolingApiHelper.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"toolingApiHelper.d.ts","sourceRoot":"","sources":["../../src/utils/toolingApiHelper.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,oDALW,MAAM,eACN,MAAM,wBACN,MAAM,gBAwChB"} \ No newline at end of file diff --git a/dist/utils/types.d.ts b/dist/utils/types.d.ts new file mode 100644 index 0000000..9dc854d --- /dev/null +++ b/dist/utils/types.d.ts @@ -0,0 +1,78 @@ +/** + * Enum for subscribe callback type values + */ +export type SubscribeCallbackType = string; +export namespace SubscribeCallbackType { + let EVENT: string; + let LAST_EVENT: string; + let ERROR: string; + let END: string; + let GRPC_STATUS: string; + let GRPC_KEEP_ALIVE: string; +} +/** + * Enum for auth type values + */ +export type AuthType = string; +export namespace AuthType { + let USER_SUPPLIED: string; + let USERNAME_PASSWORD: string; + let OAUTH_CLIENT_CREDENTIALS: string; + let OAUTH_JWT_BEARER: string; +} +/** + * Enum for managed subscription state values + */ +export type EventSubscriptionAdminState = string; +export namespace EventSubscriptionAdminState { + let RUN: string; + let STOP: string; +} +export type PublishResult = { + replayId: number; + correlationKey: string; +}; +export type SubscribeCallback = (subscription: SubscriptionInfo, callbackType: SubscribeCallbackType, data?: any) => any; +export type Subscription = { + info: SubscriptionInfo; + grpcSubscription: any; + subscribeCallback: SubscribeCallback; +}; +export type SubscriptionInfo = { + isManaged: boolean; + topicName: string; + subscriptionId: string; + subscriptionName: string; + requestedEventCount: number; + receivedEventCount: number; + lastReplayId: number; + isInfiniteEventRequest: boolean; +}; +export type Configuration = { + authType: AuthType; + pubSubEndpoint: string; + loginUrl: string; + username: string; + password: string; + userToken: string; + clientId: string; + clientSecret: string; + privateKey: string; + accessToken: string; + instanceUrl: string; + organizationId: string; +}; +export type Logger = { + debug: Function; + info: Function; + error: Function; + warn: Function; +}; +export type SubscribeRequest = { + topicName: string; + numRequested: number; + subscriptionId?: string; + replayPreset?: number; + replayId?: number; +}; +//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/dist/utils/types.d.ts.map b/dist/utils/types.d.ts.map new file mode 100644 index 0000000..1156728 --- /dev/null +++ b/dist/utils/types.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/utils/types.js"],"names":[],"mappings":";;;oCAEU,MAAM;;;;;;;;;;;;uBAaN,MAAM;;;;;;;;;;0CAWN,MAAM;;;;;;cASF,MAAM;oBACN,MAAM;;+CAMT,gBAAgB,gBAChB,qBAAqB;;UAOlB,gBAAgB;;uBAEhB,iBAAiB;;;eAMjB,OAAO;eACP,MAAM;oBACN,MAAM;sBACN,MAAM;yBACN,MAAM;wBACN,MAAM;kBACN,MAAM;4BACN,OAAO;;;cAMP,QAAQ;oBACR,MAAM;cACN,MAAM;cACN,MAAM;cACN,MAAM;eACN,MAAM;cACN,MAAM;kBACN,MAAM;gBACN,MAAM;iBACN,MAAM;iBACN,MAAM;oBACN,MAAM;;;;;;;;;eAeN,MAAM;kBACN,MAAM;qBACN,MAAM;mBACN,MAAM;eACN,MAAM"} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8beaff7..49f0a55 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "salesforce-pubsub-api-client", - "version": "5.0.3", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "salesforce-pubsub-api-client", - "version": "5.0.3", + "version": "5.1.0", "license": "CC0-1.0", "dependencies": { - "@grpc/grpc-js": "^1.12.2", + "@grpc/grpc-js": "^1.12.4", "@grpc/proto-loader": "^0.7.13", "avro-js": "^1.12.0", "certifi": "^14.5.15", @@ -18,13 +18,13 @@ }, "devDependencies": { "@chialab/esbuild-plugin-meta-url": "^0.18.2", - "dotenv": "^16.4.5", - "eslint": "^9.15.0", + "dotenv": "^16.4.7", + "eslint": "^9.16.0", "eslint-plugin-jasmine": "^4.2.2", "husky": "^9.1.7", - "jasmine": "^5.4.0", + "jasmine": "^5.5.0", "lint-staged": "^15.2.10", - "prettier": "^3.4.1", + "prettier": "^3.4.2", "tsup": "^8.3.5", "typescript": "^5.7.2" } @@ -592,9 +592,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.15.0.tgz", - "integrity": "sha512-tMTqrY+EzbXmKJR5ToI8lxu7jaN5EdmrBFJpQk5JmSlyLsx6o4t27r883K5xsLuCYCpfKBCGswMSWXsM+jB7lg==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.16.0.tgz", + "integrity": "sha512-tw2HxzQkrbeuvyj1tG2Yqq+0H9wGoI2IMk4EOsQeX+vmd75FtJAzf+gTA69WF+baUKRYQ3x2kbLE08js5OsTVg==", "dev": true, "license": "MIT", "engines": { @@ -625,9 +625,9 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", - "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", + "version": "1.12.4", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.4.tgz", + "integrity": "sha512-NBhrxEWnFh0FxeA0d//YP95lRFsSx2TNLEUQg4/W+5f/BMxcCjgOOIT24iD+ZB/tZw057j44DaIxja7w4XMrhg==", "license": "Apache-2.0", "dependencies": { "@grpc/proto-loader": "^0.7.13", @@ -1800,10 +1800,11 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -1896,9 +1897,9 @@ } }, "node_modules/eslint": { - "version": "9.15.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.15.0.tgz", - "integrity": "sha512-7CrWySmIibCgT1Os28lUU6upBshZ+GxybLOrmRzi08kS8MBuO8QA7pXEgYgY5W8vK3e74xv0lpjo9DbaGU9Rkw==", + "version": "9.16.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.16.0.tgz", + "integrity": "sha512-whp8mSQI4C8VXd+fLgSM0lh3UlmcFtVwUQjyKCFfsp+2ItAIYhlq/hqGahGqHE6cv9unM41VlqKk2VtKYR2TaA==", "dev": true, "license": "MIT", "dependencies": { @@ -1907,7 +1908,7 @@ "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.9.0", "@eslint/eslintrc": "^3.2.0", - "@eslint/js": "9.15.0", + "@eslint/js": "9.16.0", "@eslint/plugin-kit": "^0.2.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2785,23 +2786,23 @@ } }, "node_modules/jasmine": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.4.0.tgz", - "integrity": "sha512-E2u4ylX5tgGYvbynImU6EUBKKrSVB1L72FEPjGh4M55ov1VsxR26RA2JU91L9YSPFgcjo4mCLyKn/QXvEYGBkA==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.5.0.tgz", + "integrity": "sha512-JKlEVCVD5QBPYLsg/VE+IUtjyseDCrW8rMBu8la+9ysYashDgavMLM9Kotls1FhI6dCJLJ40dBCIfQjGLPZI1Q==", "dev": true, "license": "MIT", "dependencies": { "glob": "^10.2.2", - "jasmine-core": "~5.4.0" + "jasmine-core": "~5.5.0" }, "bin": { "jasmine": "bin/jasmine.js" } }, "node_modules/jasmine-core": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.4.0.tgz", - "integrity": "sha512-T4fio3W++llLd7LGSGsioriDHgWyhoL6YTu4k37uwJLF7DzOzspz7mNxRoM3cQdLWtL/ebazQpIf/yZGJx/gzg==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.5.0.tgz", + "integrity": "sha512-NHOvoPO6o9gVR6pwqEACTEpbgcH+JJ6QDypyymGbSUIFIFsMMbBJ/xsFNud8MSClfnWclXd7RQlAZBz7yVo5TQ==", "dev": true, "license": "MIT" }, @@ -3635,9 +3636,9 @@ } }, "node_modules/prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.1.tgz", - "integrity": "sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "license": "MIT", "bin": { diff --git a/package.json b/package.json index ccd678a..5c3b53f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salesforce-pubsub-api-client", - "version": "5.0.3", + "version": "5.1.0", "type": "module", "description": "A node client for the Salesforce Pub/Sub API", "author": "pozil", @@ -24,7 +24,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@grpc/grpc-js": "^1.12.2", + "@grpc/grpc-js": "^1.12.4", "@grpc/proto-loader": "^0.7.13", "avro-js": "^1.12.0", "certifi": "^14.5.15", @@ -33,13 +33,13 @@ }, "devDependencies": { "@chialab/esbuild-plugin-meta-url": "^0.18.2", - "dotenv": "^16.4.5", - "eslint": "^9.15.0", + "dotenv": "^16.4.7", + "eslint": "^9.16.0", "eslint-plugin-jasmine": "^4.2.2", "husky": "^9.1.7", - "jasmine": "^5.4.0", + "jasmine": "^5.5.0", "lint-staged": "^15.2.10", - "prettier": "^3.4.1", + "prettier": "^3.4.2", "tsup": "^8.3.5", "typescript": "^5.7.2" },