@boostercloud/framework-provider-azure 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,10 +15,18 @@ export declare const connectionsStoreAttributes: {
15
15
  readonly partitionKey: "connectionID";
16
16
  readonly ttl: "expirationTime";
17
17
  };
18
+ export declare const dedupAttributes: {
19
+ readonly partitionKey: "primaryKey";
20
+ readonly ttl: "expirationTime";
21
+ };
18
22
  export declare const environmentVarNames: {
19
23
  readonly restAPIURL: "BOOSTER_REST_API_URL";
20
24
  readonly websocketAPIURL: "BOOSTER_WEBSOCKET_API_URL";
21
25
  readonly cosmosDbConnectionString: "COSMOSDB_CONNECTION_STRING";
26
+ readonly eventHubConnectionString: "EVENTHUB_CONNECTION_STRING";
27
+ readonly eventHubName: "EVENTHUB_NAME";
28
+ readonly eventHubMaxRetries: "EVENTHUB_MAX_RETRIES";
29
+ readonly eventHubMode: "EVENTHUB_MODE";
22
30
  };
23
31
  export declare const AZURE_CONFLICT_ERROR_CODE = 409;
24
32
  export declare const AZURE_PRECONDITION_FAILED_ERROR = 412;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AZURE_PRECONDITION_FAILED_ERROR = exports.AZURE_CONFLICT_ERROR_CODE = exports.environmentVarNames = exports.connectionsStoreAttributes = exports.subscriptionsStoreAttributes = exports.eventsStoreAttributes = void 0;
3
+ exports.AZURE_PRECONDITION_FAILED_ERROR = exports.AZURE_CONFLICT_ERROR_CODE = exports.environmentVarNames = exports.dedupAttributes = exports.connectionsStoreAttributes = exports.subscriptionsStoreAttributes = exports.eventsStoreAttributes = void 0;
4
4
  exports.eventsStoreAttributes = {
5
5
  partitionKey: 'entityTypeName_entityID_kind',
6
6
  sortKey: 'createdAt',
@@ -17,10 +17,18 @@ exports.connectionsStoreAttributes = {
17
17
  partitionKey: 'connectionID',
18
18
  ttl: 'expirationTime',
19
19
  };
20
+ exports.dedupAttributes = {
21
+ partitionKey: 'primaryKey',
22
+ ttl: 'expirationTime',
23
+ };
20
24
  exports.environmentVarNames = {
21
25
  restAPIURL: 'BOOSTER_REST_API_URL',
22
26
  websocketAPIURL: 'BOOSTER_WEBSOCKET_API_URL',
23
27
  cosmosDbConnectionString: 'COSMOSDB_CONNECTION_STRING',
28
+ eventHubConnectionString: 'EVENTHUB_CONNECTION_STRING',
29
+ eventHubName: 'EVENTHUB_NAME',
30
+ eventHubMaxRetries: 'EVENTHUB_MAX_RETRIES',
31
+ eventHubMode: 'EVENTHUB_MODE',
24
32
  };
25
33
  // Azure special error codes
26
34
  exports.AZURE_CONFLICT_ERROR_CODE = 409;
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  "use strict";
2
+ var _a;
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.Provider = exports.loadInfrastructurePackage = void 0;
4
5
  const tslib_1 = require("tslib");
@@ -14,6 +15,9 @@ const events_searcher_adapter_1 = require("./library/events-searcher-adapter");
14
15
  const subscription_adapter_1 = require("./library/subscription-adapter");
15
16
  const connections_adapter_1 = require("./library/connections-adapter");
16
17
  const rocket_adapter_1 = require("./library/rocket-adapter");
18
+ const events_stream_producer_adapter_1 = require("./library/events-stream-producer-adapter");
19
+ const event_hubs_1 = require("@azure/event-hubs");
20
+ const events_stream_consumer_adapter_1 = require("./library/events-stream-consumer-adapter");
17
21
  const health_adapter_1 = require("./library/health-adapter");
18
22
  let cosmosClient;
19
23
  if (typeof process.env[constants_1.environmentVarNames.cosmosDbConnectionString] === 'undefined') {
@@ -22,6 +26,32 @@ if (typeof process.env[constants_1.environmentVarNames.cosmosDbConnectionString]
22
26
  else {
23
27
  cosmosClient = new cosmos_1.CosmosClient(process.env[constants_1.environmentVarNames.cosmosDbConnectionString]);
24
28
  }
29
+ let producer;
30
+ const eventHubConnectionString = process.env[constants_1.environmentVarNames.eventHubConnectionString];
31
+ const eventHubName = process.env[constants_1.environmentVarNames.eventHubName];
32
+ const DEFAULT_MAX_RETRY = 5;
33
+ const DEFAULT_EVENT_HUB_MODE = event_hubs_1.RetryMode.Exponential;
34
+ if (typeof eventHubConnectionString === 'undefined' ||
35
+ typeof eventHubName === 'undefined' ||
36
+ eventHubConnectionString === '' ||
37
+ eventHubName === '') {
38
+ producer = {};
39
+ }
40
+ else {
41
+ const maxRetries = process.env[constants_1.environmentVarNames.eventHubMaxRetries]
42
+ ? Number(process.env[constants_1.environmentVarNames.eventHubMaxRetries])
43
+ : DEFAULT_MAX_RETRY;
44
+ const mode = process.env[constants_1.environmentVarNames.eventHubMaxRetries] &&
45
+ ((_a = process.env[constants_1.environmentVarNames.eventHubMode]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) === 'FIXED'
46
+ ? event_hubs_1.RetryMode.Fixed
47
+ : DEFAULT_EVENT_HUB_MODE;
48
+ producer = new event_hubs_1.EventHubProducerClient(eventHubConnectionString, eventHubName, {
49
+ retryOptions: {
50
+ maxRetries: maxRetries,
51
+ mode: mode,
52
+ },
53
+ });
54
+ }
25
55
  /* We load the infrastructure package dynamically here to avoid including it in the
26
56
  * dependencies that are deployed in the lambda functions. The infrastructure
27
57
  * package is only used during the deploy.
@@ -34,6 +64,9 @@ const Provider = (rockets) => ({
34
64
  // ProviderEventsLibrary
35
65
  events: {
36
66
  rawToEnvelopes: events_adapter_1.rawEventsToEnvelopes,
67
+ rawStreamToEnvelopes: events_stream_consumer_adapter_1.rawEventsStreamToEnvelopes,
68
+ dedupEventStream: events_stream_consumer_adapter_1.dedupEventStream.bind(null, cosmosClient),
69
+ produce: events_stream_producer_adapter_1.produceEventsStream.bind(null, producer),
37
70
  store: events_adapter_1.storeEvents.bind(null, cosmosClient),
38
71
  storeSnapshot: events_adapter_1.storeSnapshot.bind(null, cosmosClient),
39
72
  forEntitySince: events_adapter_1.readEntityEventsSince.bind(null, cosmosClient),
@@ -0,0 +1,5 @@
1
+ import { BoosterConfig, EventEnvelope, EventStream } from '@boostercloud/framework-types';
2
+ import { Context } from '@azure/functions';
3
+ import { CosmosClient } from '@azure/cosmos';
4
+ export declare function dedupEventStream(cosmosDb: CosmosClient, config: BoosterConfig, context: Context): Promise<EventStream>;
5
+ export declare function rawEventsStreamToEnvelopes(config: BoosterConfig, context: Context, dedupEventStream: EventStream): Array<EventEnvelope>;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rawEventsStreamToEnvelopes = exports.dedupEventStream = void 0;
4
+ const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
5
+ const constants_1 = require("../constants");
6
+ const DEFAULT_DEDUP_TTL = 86400;
7
+ async function dedupEventStream(cosmosDb, config, context) {
8
+ var _a, _b;
9
+ const logger = (0, framework_common_helpers_1.getLogger)(config, 'dedup-events-stream#dedupEventsStream');
10
+ const events = context.bindings.eventHubMessages || [];
11
+ logger.debug(`Dedup ${events.length} events`);
12
+ const resources = [];
13
+ for (const event of events) {
14
+ const rawParsed = JSON.parse(event);
15
+ const eventTag = {
16
+ primaryKey: rawParsed._etag,
17
+ createdAt: new Date().toISOString(),
18
+ ttl: (_b = (_a = config.eventStreamConfiguration.parameters) === null || _a === void 0 ? void 0 : _a.dedupTtl) !== null && _b !== void 0 ? _b : DEFAULT_DEDUP_TTL,
19
+ };
20
+ try {
21
+ const { resource } = await cosmosDb
22
+ .database(config.resourceNames.applicationStack)
23
+ .container(config.resourceNames.eventsDedup)
24
+ .items.create(eventTag);
25
+ if (resource) {
26
+ resources.push(event);
27
+ }
28
+ }
29
+ catch (error) {
30
+ if (error.code !== constants_1.AZURE_CONFLICT_ERROR_CODE) {
31
+ throw error;
32
+ }
33
+ logger.warn(`Ignoring duplicated event with etag ${eventTag}.`);
34
+ }
35
+ }
36
+ return resources;
37
+ }
38
+ exports.dedupEventStream = dedupEventStream;
39
+ function rawEventsStreamToEnvelopes(config, context, dedupEventStream) {
40
+ const logger = (0, framework_common_helpers_1.getLogger)(config, 'events-adapter#rawEventsStreamToEnvelopes');
41
+ logger.debug(`Mapping ${dedupEventStream.length} events`);
42
+ const bindingData = context.bindingData;
43
+ return dedupEventStream.map((message, index) => {
44
+ const rawParsed = JSON.parse(message);
45
+ const instance = process.env.WEBSITE_INSTANCE_ID;
46
+ const partitionKeyArrayElement = bindingData.partitionKeyArray[index];
47
+ const offset = bindingData.offsetArray[index];
48
+ const sequence = bindingData.sequenceNumberArray[index];
49
+ const time = bindingData.enqueuedTimeUtcArray[index];
50
+ logger.debug(`CONSUMED_EVENT:${instance}#${partitionKeyArrayElement}=>(${index}#${offset}#${sequence}#${time})`);
51
+ return rawParsed;
52
+ });
53
+ }
54
+ exports.rawEventsStreamToEnvelopes = rawEventsStreamToEnvelopes;
@@ -0,0 +1,3 @@
1
+ import { BoosterConfig, EventEnvelope } from '@boostercloud/framework-types';
2
+ import { EventHubProducerClient } from '@azure/event-hubs';
3
+ export declare function produceEventsStream(producer: EventHubProducerClient, entityName: string, entityID: string, eventEnvelopes: Array<EventEnvelope>, config: BoosterConfig): Promise<void>;
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.produceEventsStream = void 0;
4
+ const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
5
+ const partition_keys_1 = require("./partition-keys");
6
+ async function produceEventsStream(producer, entityName, entityID, eventEnvelopes, config) {
7
+ const logger = (0, framework_common_helpers_1.getLogger)(config, 'events-stream-producer#produceEventsStream');
8
+ logger.debug('Producing eventEnvelopes', eventEnvelopes);
9
+ const batchOptions = {
10
+ partitionKey: (0, partition_keys_1.partitionKeyForEvent)(entityName, entityID),
11
+ };
12
+ let batch = await producer.createBatch(batchOptions);
13
+ let numEventsSent = 0;
14
+ let i = 0;
15
+ while (i < eventEnvelopes.length) {
16
+ // messages can fail to be added to the batch if they exceed the maximum size configured for the EventHub.
17
+ const eventEnvelope = eventEnvelopes[i];
18
+ const isAdded = batch.tryAdd({ body: eventEnvelope });
19
+ if (isAdded) {
20
+ logger.info(`Added ${JSON.stringify(eventEnvelope)} to the batch`);
21
+ ++i;
22
+ continue;
23
+ }
24
+ if (batch.count === 0) {
25
+ throw new Error(`Message was too large and can't be sent until it's made smaller. ${JSON.stringify(eventEnvelope)}`);
26
+ }
27
+ // We reached the batch size limit
28
+ logger.info(`Sending ${batch.count} messages`);
29
+ await producer.sendBatch(batch);
30
+ numEventsSent += batch.count;
31
+ batch = await producer.createBatch(batchOptions);
32
+ }
33
+ if (batch.count > 0) {
34
+ logger.info(`Sending remaining ${batch.count} messages`);
35
+ await producer.sendBatch(batch);
36
+ numEventsSent += batch.count;
37
+ }
38
+ logger.info(`Sent ${numEventsSent} events`);
39
+ if (numEventsSent !== eventEnvelopes.length) {
40
+ throw new Error(`Not all messages were sent (${numEventsSent}/${eventEnvelopes.length})`);
41
+ }
42
+ }
43
+ exports.produceEventsStream = produceEventsStream;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostercloud/framework-provider-azure",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Handle Booster's integration with Azure",
5
5
  "keywords": [
6
6
  "framework-provider-azure"
@@ -26,14 +26,15 @@
26
26
  "@azure/cosmos": "^4.0.0",
27
27
  "@azure/functions": "^1.2.2",
28
28
  "@azure/identity": "~2.1.0",
29
- "@boostercloud/framework-common-helpers": "^2.1.0",
30
- "@boostercloud/framework-types": "^2.1.0",
29
+ "@azure/event-hubs": "5.11.1",
30
+ "@boostercloud/framework-common-helpers": "^2.2.0",
31
+ "@boostercloud/framework-types": "^2.2.0",
31
32
  "tslib": "^2.4.0",
32
33
  "@effect-ts/core": "^0.60.4",
33
34
  "@azure/web-pubsub": "~1.1.0"
34
35
  },
35
36
  "devDependencies": {
36
- "@boostercloud/eslint-config": "^2.1.0",
37
+ "@boostercloud/eslint-config": "^2.2.0",
37
38
  "@types/chai": "4.2.18",
38
39
  "@types/chai-as-promised": "7.1.4",
39
40
  "@types/faker": "5.1.5",