@boostercloud/framework-provider-azure 2.9.2 → 2.10.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.
package/dist/index.js CHANGED
@@ -19,6 +19,7 @@ const events_stream_producer_adapter_1 = require("./library/events-stream-produc
19
19
  const event_hubs_1 = require("@azure/event-hubs");
20
20
  const events_stream_consumer_adapter_1 = require("./library/events-stream-consumer-adapter");
21
21
  const health_adapter_1 = require("./library/health-adapter");
22
+ const events_store_adapter_1 = require("./library/events-store-adapter");
22
23
  let cosmosClient;
23
24
  if (typeof process.env[constants_1.environmentVarNames.cosmosDbConnectionString] === 'undefined') {
24
25
  cosmosClient = {};
@@ -67,7 +68,7 @@ const Provider = (rockets) => ({
67
68
  rawStreamToEnvelopes: events_stream_consumer_adapter_1.rawEventsStreamToEnvelopes,
68
69
  dedupEventStream: events_stream_consumer_adapter_1.dedupEventStream.bind(null, cosmosClient),
69
70
  produce: events_stream_producer_adapter_1.produceEventsStream.bind(null, producer),
70
- store: events_adapter_1.storeEvents.bind(null, cosmosClient),
71
+ store: events_store_adapter_1.storeEvents.bind(null, cosmosClient),
71
72
  storeSnapshot: events_adapter_1.storeSnapshot.bind(null, cosmosClient),
72
73
  forEntitySince: events_adapter_1.readEntityEventsSince.bind(null, cosmosClient),
73
74
  latestEntitySnapshot: events_adapter_1.readEntityLatestSnapshot.bind(null, cosmosClient),
@@ -1,9 +1,8 @@
1
1
  import { CosmosClient } from '@azure/cosmos';
2
- import { BoosterConfig, EntitySnapshotEnvelope, EventEnvelope, NonPersistedEntitySnapshotEnvelope, NonPersistedEventEnvelope, UUID } from '@boostercloud/framework-types';
2
+ import { BoosterConfig, EntitySnapshotEnvelope, EventEnvelope, NonPersistedEntitySnapshotEnvelope, UUID } from '@boostercloud/framework-types';
3
3
  import { Context } from '@azure/functions';
4
4
  export declare function rawEventsToEnvelopes(context: Context): Array<EventEnvelope>;
5
5
  export declare function readEntityEventsSince(cosmosDb: CosmosClient, config: BoosterConfig, entityTypeName: string, entityID: UUID, since?: string): Promise<Array<EventEnvelope>>;
6
6
  export declare function readEntityLatestSnapshot(cosmosDb: CosmosClient, config: BoosterConfig, entityTypeName: string, entityID: UUID): Promise<EntitySnapshotEnvelope | undefined>;
7
- export declare function storeEvents(cosmosDb: CosmosClient, eventEnvelopes: Array<NonPersistedEventEnvelope>, config: BoosterConfig): Promise<Array<EventEnvelope>>;
8
7
  export declare function storeSnapshot(cosmosDb: CosmosClient, snapshotEnvelope: NonPersistedEntitySnapshotEnvelope, config: BoosterConfig): Promise<EntitySnapshotEnvelope>;
9
8
  export declare function storeDispatchedEvent(cosmosDb: CosmosClient, eventEnvelope: EventEnvelope, config: BoosterConfig): Promise<boolean>;
@@ -1,10 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.storeDispatchedEvent = exports.storeSnapshot = exports.storeEvents = exports.readEntityLatestSnapshot = exports.readEntityEventsSince = exports.rawEventsToEnvelopes = void 0;
3
+ exports.storeDispatchedEvent = exports.storeSnapshot = exports.readEntityLatestSnapshot = exports.readEntityEventsSince = exports.rawEventsToEnvelopes = void 0;
4
4
  const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
5
5
  const constants_1 = require("../constants");
6
6
  const partition_keys_1 = require("./partition-keys");
7
- // eslint-disable-next-line @typescript-eslint/no-magic-numbers
8
7
  const originOfTime = new Date(0).toISOString();
9
8
  function rawEventsToEnvelopes(context) {
10
9
  return context.bindings.rawEvent;
@@ -61,32 +60,9 @@ async function readEntityLatestSnapshot(cosmosDb, config, entityTypeName, entity
61
60
  }
62
61
  }
63
62
  exports.readEntityLatestSnapshot = readEntityLatestSnapshot;
64
- async function storeEvents(cosmosDb, eventEnvelopes, config) {
65
- const logger = (0, framework_common_helpers_1.getLogger)(config, 'events-adapter#storeEvents');
66
- logger.debug('[EventsAdapter#storeEvents] Storing EventEnvelopes with eventEnvelopes:', eventEnvelopes);
67
- const persistableEvents = [];
68
- for (const eventEnvelope of eventEnvelopes) {
69
- const persistableEvent = {
70
- ...eventEnvelope,
71
- createdAt: new Date().toISOString(),
72
- };
73
- await cosmosDb
74
- .database(config.resourceNames.applicationStack)
75
- .container(config.resourceNames.eventsStore)
76
- .items.create({
77
- ...persistableEvent,
78
- [constants_1.eventsStoreAttributes.partitionKey]: (0, partition_keys_1.partitionKeyForEvent)(eventEnvelope.entityTypeName, eventEnvelope.entityID),
79
- [constants_1.eventsStoreAttributes.sortKey]: persistableEvent.createdAt,
80
- });
81
- persistableEvents.push(persistableEvent);
82
- }
83
- logger.debug('[EventsAdapter#storeEvents] EventEnvelope stored');
84
- return persistableEvents;
85
- }
86
- exports.storeEvents = storeEvents;
87
63
  async function storeSnapshot(cosmosDb, snapshotEnvelope, config) {
88
64
  const logger = (0, framework_common_helpers_1.getLogger)(config, 'events-adapter#storeSnapshot');
89
- logger.debug('[EventsAdapter#storeSnapshot] Storing snapshot with snapshotEnvelope:', snapshotEnvelope);
65
+ logger.debug('Storing snapshot with snapshotEnvelope:', snapshotEnvelope);
90
66
  const partitionKey = (0, partition_keys_1.partitionKeyForSnapshot)(snapshotEnvelope.entityTypeName, snapshotEnvelope.entityID);
91
67
  /**
92
68
  * The sort key of the snapshot matches the sort key of the last event that generated it.
@@ -0,0 +1,15 @@
1
+ import { BoosterConfig, EventEnvelope, NonPersistedEventEnvelope } from '@boostercloud/framework-types';
2
+ import { CosmosClient } from '@azure/cosmos';
3
+ /**
4
+ * Limits: The Azure Cosmos DB request size limit constrains the size of the TransactionalBatch payload to not exceed 2 MB,
5
+ * and the maximum execution time is 5 seconds. There's a current limit of 100 operations per TransactionalBatch to ensure
6
+ * the performance is as expected and within SLAs.
7
+ *
8
+ * Errors: If there's a failure, the failed operation will have a status code of its corresponding error. All the other
9
+ * operations will have a 424 status code (failed dependency). The status code enables one to identify the cause of transaction failure.
10
+ *
11
+ * @param cosmosDb
12
+ * @param eventEnvelopes
13
+ * @param config
14
+ */
15
+ export declare function storeEvents(cosmosDb: CosmosClient, eventEnvelopes: Array<NonPersistedEventEnvelope>, config: BoosterConfig): Promise<Array<EventEnvelope>>;
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.storeEvents = void 0;
4
+ const framework_common_helpers_1 = require("@boostercloud/framework-common-helpers");
5
+ const partition_keys_1 = require("./partition-keys");
6
+ const constants_1 = require("../constants");
7
+ const DEFAULT_CHUNK_SIZE = 100;
8
+ /**
9
+ * Limits: The Azure Cosmos DB request size limit constrains the size of the TransactionalBatch payload to not exceed 2 MB,
10
+ * and the maximum execution time is 5 seconds. There's a current limit of 100 operations per TransactionalBatch to ensure
11
+ * the performance is as expected and within SLAs.
12
+ *
13
+ * Errors: If there's a failure, the failed operation will have a status code of its corresponding error. All the other
14
+ * operations will have a 424 status code (failed dependency). The status code enables one to identify the cause of transaction failure.
15
+ *
16
+ * @param cosmosDb
17
+ * @param eventEnvelopes
18
+ * @param config
19
+ */
20
+ async function storeEvents(cosmosDb, eventEnvelopes, config) {
21
+ const logger = (0, framework_common_helpers_1.getLogger)(config, 'store-events-adapter#storeEvents');
22
+ logger.debug('Storing EventEnvelopes with eventEnvelopes:', eventEnvelopes);
23
+ const envelopesWithCreatedAt = [];
24
+ const eventsPerPartitionKey = eventEnvelopes.reduce(groupByPartitionKey, {});
25
+ for (const [partitionKey, eventsInPartitionKey] of Object.entries(eventsPerPartitionKey)) {
26
+ const chunksOfEventsInPartitionKey = chunkEvents(eventsInPartitionKey, DEFAULT_CHUNK_SIZE);
27
+ for (const eventListInChunk of chunksOfEventsInPartitionKey) {
28
+ const eventEnvelopesChunkWithCreatedAt = toEventEnvelopes(eventListInChunk);
29
+ envelopesWithCreatedAt.push(...eventEnvelopesChunkWithCreatedAt);
30
+ const inputOperations = toInputOperations(eventEnvelopesChunkWithCreatedAt, config);
31
+ const batchResponse = await batchEvents(cosmosDb, config, inputOperations, partitionKey);
32
+ // Batch is transactional and will roll back all operations if one fails
33
+ if ((batchResponse === null || batchResponse === void 0 ? void 0 : batchResponse.code) !== 200) {
34
+ logger.error(`An error ocurred storing a batch of events. Batch response: ${JSON.stringify(batchResponse)}`);
35
+ throw new Error(`Error ${batchResponse.substatus} storing events: ${JSON.stringify(batchResponse)}`);
36
+ }
37
+ const result = batchResponse.result;
38
+ logger.debug(`EventEnvelopes with ${partitionKey} stored: ${result}`);
39
+ }
40
+ }
41
+ return envelopesWithCreatedAt;
42
+ }
43
+ exports.storeEvents = storeEvents;
44
+ function toEventEnvelopes(events) {
45
+ return events.map((eventEnvelope) => ({
46
+ ...eventEnvelope,
47
+ createdAt: new Date().toISOString(),
48
+ }));
49
+ }
50
+ function toInputOperations(eventEnvelopesChunkWithCreatedAt, config) {
51
+ return eventEnvelopesChunkWithCreatedAt.map((eventEnvelopeWithCreatedAt) => {
52
+ const body = storableResource(config, eventEnvelopeWithCreatedAt);
53
+ return {
54
+ operationType: 'Create',
55
+ resourceBody: body,
56
+ };
57
+ });
58
+ }
59
+ async function batchEvents(cosmosDb, config, inputOperations, partitionKey) {
60
+ const logger = (0, framework_common_helpers_1.getLogger)(config, 'store-events-adapter#batchEvents');
61
+ try {
62
+ logger.debug('Storing EventEnvelopes with inputOperations:', inputOperations);
63
+ return await cosmosDb
64
+ .database(config.resourceNames.applicationStack)
65
+ .container(config.resourceNames.eventsStore)
66
+ .items.batch(inputOperations, partitionKey);
67
+ }
68
+ catch (e) {
69
+ logger.error('Unexpected error storing events', e);
70
+ throw e;
71
+ }
72
+ }
73
+ function groupByPartitionKey(eventsPerPartitionKey, event) {
74
+ const partitionKey = (0, partition_keys_1.partitionKeyForEvent)(event.entityTypeName, event.entityID);
75
+ if (!eventsPerPartitionKey[partitionKey]) {
76
+ eventsPerPartitionKey[partitionKey] = [];
77
+ }
78
+ eventsPerPartitionKey[partitionKey].push(event);
79
+ return eventsPerPartitionKey;
80
+ }
81
+ /**
82
+ * Batch method expects a JSONObject. JSONObject valid types are: boolean | number | string | null | JSONArray | JSONObject
83
+ * This method tries to build a JSONObject from an EventEnvelope.
84
+ * It could fail if value contains a circular reference or a BigInt value is encountered.
85
+ * @param config
86
+ * @param eventEnvelope
87
+ */
88
+ function storableResource(config, eventEnvelope) {
89
+ const logger = (0, framework_common_helpers_1.getLogger)(config, 'store-events-adapter#storableResource');
90
+ try {
91
+ const partitionKey = (0, partition_keys_1.partitionKeyForEvent)(eventEnvelope.entityTypeName, eventEnvelope.entityID);
92
+ const eventEnvelopeWithEntityID = {
93
+ ...eventEnvelope,
94
+ [constants_1.eventsStoreAttributes.partitionKey]: partitionKey,
95
+ [constants_1.eventsStoreAttributes.sortKey]: eventEnvelope.createdAt,
96
+ };
97
+ return eventEnvelopeWithEntityID;
98
+ }
99
+ catch (e) {
100
+ logger.error(`Could not parse eventEnvelope ${eventEnvelope} to JSONObject`, e);
101
+ throw e;
102
+ }
103
+ }
104
+ /**
105
+ * Split events in chunks of size DEFAULT_CHUNK_SIZE
106
+ *
107
+ * @param arr
108
+ * @param size
109
+ */
110
+ function chunkEvents(arr, size) {
111
+ return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@boostercloud/framework-provider-azure",
3
- "version": "2.9.2",
3
+ "version": "2.10.0",
4
4
  "description": "Handle Booster's integration with Azure",
5
5
  "keywords": [
6
6
  "framework-provider-azure"
@@ -27,14 +27,14 @@
27
27
  "@azure/functions": "^1.2.2",
28
28
  "@azure/identity": "~2.1.0",
29
29
  "@azure/event-hubs": "5.11.1",
30
- "@boostercloud/framework-common-helpers": "^2.9.2",
31
- "@boostercloud/framework-types": "^2.9.2",
30
+ "@boostercloud/framework-common-helpers": "^2.10.0",
31
+ "@boostercloud/framework-types": "^2.10.0",
32
32
  "tslib": "^2.4.0",
33
33
  "@effect-ts/core": "^0.60.4",
34
34
  "@azure/web-pubsub": "~1.1.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@boostercloud/eslint-config": "^2.9.2",
37
+ "@boostercloud/eslint-config": "^2.10.0",
38
38
  "@types/chai": "4.2.18",
39
39
  "@types/chai-as-promised": "7.1.4",
40
40
  "@types/faker": "5.1.5",