@devrev/ts-adaas 1.3.0 → 1.4.1

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/README.md CHANGED
@@ -23,275 +23,4 @@ npm install @devrev/ts-adaas
23
23
 
24
24
  ## Reference
25
25
 
26
- ### `spawn` function
27
-
28
- This function initializes a new worker thread and oversees its lifecycle.
29
- It should be invoked when the snap-in receives a message from the Airdrop platform.
30
- The worker script provided then handles the event accordingly.
31
-
32
- #### Usage
33
-
34
- ```typescript
35
- spawn({ event, initialState, workerPath, options })
36
- ```
37
-
38
- #### Parameters
39
-
40
- * _event_
41
-
42
- Required. An object of type __AirdropEvent__ that is received from the Airdrop platform.
43
-
44
- * _initialState_
45
-
46
- Required. Object of __any__ type that represents the initial state of the snap-in.
47
-
48
- * _workerPath_
49
-
50
- Required. A __string__ that represents the path to the worker file.
51
-
52
- * _options_
53
-
54
- Optional. An object of type **WorkerAdapterOptions**, which will be passed to the newly created worker. This worker will then initialize a `WorkerAdapter` by invoking the `processTask` function. The options include:
55
-
56
- * `isLocalDevelopment`
57
-
58
- A __boolean__ flag. If set to `true`, intermediary files containing extracted data will be stored on the local machine, which is useful during development. The default value is `false`.
59
-
60
- * `timeout`
61
-
62
- A __number__ that specifies the timeout duration for the lambda function, in milliseconds. The default is 10 minutes (10 * 60 * 1000 milliseconds), with a maximum allowable duration of 13 minutes (13 * 60 * 1000 milliseconds).
63
-
64
- * `batchSize`
65
-
66
- A __number__ that determines the maximum number of items to be processed and saved to an intermediary file before being sent to the Airdrop platform. The default batch size is 2,000.
67
-
68
- #### Return value
69
-
70
- A __promise__ that resolves once the worker has completed processing.
71
-
72
- #### Example
73
-
74
- ```typescript
75
- const run = async (events: AirdropEvent[]) => {
76
- for (const event of events) {
77
- const file = getWorkerPerExtractionPhase(event);
78
- await spawn<ExtractorState>({
79
- event,
80
- initialState,
81
- workerPath: file,
82
- });
83
- }
84
- };
85
- ```
86
-
87
- ### `processTask` function
88
-
89
- The `processTask` function retrieves the current state from the Airdrop platform and initializes a new `WorkerAdapter`.
90
- It executes the code specified in the `task` parameter, which contains the worker's functionality.
91
- If a timeout occurs, the function handles it by executing the `onTimeout` callback, ensuring the worker exits gracefully.
92
- Both functions receive an `adapter` parameter, representing the initialized `WorkerAdapter` object.
93
-
94
-
95
- #### Usage
96
- ```typescript
97
- processTask({ task, onTimeout })
98
- ```
99
-
100
- #### Parameters
101
-
102
- * _task_
103
-
104
- Required. A __function__ that defines the logic associated with the given event type.
105
-
106
- * _onTimeout_
107
-
108
- Required. A __function__ managing the timeout of the lambda invocation, including saving any necessary progress at the time of timeout.
109
-
110
- #### Example
111
-
112
- ````typescript
113
- // External sync units extraction
114
- processTask({
115
- task: async ({ adapter }) => {
116
- const httpClient = new HttpClient(adapter.event);
117
-
118
- const todoLists = await httpClient.getTodoLists();
119
-
120
- const externalSyncUnits: ExternalSyncUnit[] = todoLists.map((todoList) => normalizeTodoList(todoList));
121
-
122
- await adapter.emit(ExtractorEventType.ExtractionExternalSyncUnitsDone, {
123
- external_sync_units: externalSyncUnits,
124
- });
125
- },
126
- onTimeout: async ({ adapter }) => {
127
- await adapter.emit(ExtractorEventType.ExtractionExternalSyncUnitsError, {
128
- error: {
129
- message: 'Failed to extract external sync units. Lambda timeout.',
130
- },
131
- });
132
- },
133
- });
134
- ````
135
-
136
- ### `WorkerAdapter` class
137
-
138
- Used to interact with Airdrop platform.
139
- Provides utilities to emit events to the Airdrop platform, update the state of the snap-in and upload artifacts (files with data) to the platform.
140
-
141
- ### Usage
142
-
143
- ```typescript
144
- new WorkerAdapter({
145
- event,
146
- adapterState,
147
- options,
148
- });
149
- ```
150
-
151
- #### Parameters
152
-
153
- * _event_
154
-
155
- Required. An object of type __AirdropEvent__ that is received from the Airdrop platform.
156
-
157
- * _adapterState_
158
-
159
- Required. An object of type __State__, which represents the initial state of the adapter.
160
-
161
- * _options_
162
-
163
- Optional. An object of type __WorkerAdapterOptions__ that specifies additional configuration options for the `WorkerAdapter`. This object is passed via the `spawn` function.
164
-
165
- #### Example
166
-
167
- ```typescript
168
- const adapter = new WorkerAdapter<ConnectorState>({
169
- event,
170
- adapterState,
171
- options,
172
- });
173
- ```
174
-
175
- ### `WorkerAdapter.state` property
176
-
177
- Getter and setter methods for working with the adapter state.
178
-
179
- ### Usage
180
-
181
- ```typescript
182
- // get state
183
- const adapterState = adapter.state;
184
-
185
- // set state
186
- adapter.state = newAdapterState;
187
- ```
188
-
189
- #### Example
190
-
191
- ```typescript
192
- export const initialState: ExtractorState = {
193
- users: { completed: false },
194
- tasks: { completed: false },
195
- attachments: { completed: false },
196
- };
197
-
198
- adapter.state = initialState;
199
- ```
200
-
201
- ### `WorkerAdapter.initializeRepos` method
202
-
203
- Initializes a `Repo` object for each item provided.
204
-
205
- ### Usage
206
-
207
- ```typescript
208
- adapter.initializeRepos(repos);
209
- ```
210
-
211
- #### Parameters
212
-
213
- * _repos_
214
-
215
- Required. An array of objects of type `RepoInterface`.
216
-
217
- #### Example
218
-
219
- This should typically be called within the function passed as a parameter to the `processTask` function in the data extraction phase.
220
-
221
- ```typescript
222
- const repos = [
223
- {
224
- itemType: 'tasks',
225
- normalize: normalizeTask,
226
- }
227
- ];
228
-
229
- adapter.initializeRepos(repos);
230
- ```
231
-
232
- ### `WorkerAdapter.getRepo` method
233
-
234
- Finds a Repo from the initialized repos.
235
-
236
- ### Usage
237
-
238
- ```typescript
239
- adapter.getRepo(itemType);
240
- ```
241
-
242
- #### Parameters
243
-
244
- * _itemType_
245
-
246
- Required. A __string__ that represents the itemType property for the searched repo.
247
-
248
- #### Return value
249
-
250
- An object of type __Repo__ if the repo is found, otherwise __undefined__.
251
-
252
- #### Example
253
-
254
- This should typically be called within the function passed as a parameter to the `processTask` function.
255
-
256
- ```typescript
257
- // Push users to the repository designated for 'users' data.
258
- await adapter.getRepo('users')?.push(users);
259
- ```
260
-
261
- ### `WorkerAdapter.emit` method
262
-
263
- Emits an event to the Airdrop platform.
264
-
265
- ### Usage
266
-
267
- ```typescript
268
- adapter.emit( newEventType, data ):
269
- ```
270
-
271
- #### Parameters
272
-
273
- * _newEventType_
274
-
275
- Required. The event type to be emitted, of type __ExtractorEventType__ or __LoaderEventType__.
276
-
277
- * _data_
278
-
279
- Optional. An object of type __EventData__ which represents the data to be sent with the event.
280
-
281
- #### Return value
282
-
283
- A __promise__, which resolves to undefined after the emit function completes its execution or rejects with an error.
284
-
285
- #### Example
286
-
287
- This should typically be called within the function passed as a parameter to the `processTask` function.
288
-
289
- ```typescript
290
- // Emitting successfully finished data extraction.
291
- await adapter.emit(ExtractorEventType.ExtractionDataDone);
292
-
293
- // Emitting a delay in attachments extraction phase.
294
- await adapter.emit(ExtractorEventType.ExtractionAttachmentsDelay, {
295
- delay: 10,
296
- });
297
- ```
26
+ Please refer to the [REFERENCE.md](./REFERENCE.md) file for more information on the types, interfaces and functions used in the library.
@@ -11,5 +11,5 @@ export declare class Repo {
11
11
  constructor({ event, itemType, normalize, onUpload, options, }: RepoFactoryInterface);
12
12
  getItems(): (NormalizedItem | NormalizedAttachment | Item)[];
13
13
  upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void | ErrorRecord>;
14
- push(items: Item[]): Promise<boolean | ErrorRecord>;
14
+ push(items: Item[]): Promise<boolean>;
15
15
  }
@@ -36,7 +36,7 @@ export interface NormalizedAttachment {
36
36
  file_name: string;
37
37
  parent_id: string;
38
38
  author_id?: string;
39
- grand_parent_id?: number;
39
+ grand_parent_id?: number | string;
40
40
  }
41
41
  /**
42
42
  * Item is an interface that defines the structure of an item.
@@ -18,6 +18,7 @@ export interface ToDevRev {
18
18
  attachmentsMetadata: {
19
19
  artifactIds: string[];
20
20
  lastProcessed: number;
21
+ lastProcessedAttachmentsIdsList?: string[];
21
22
  };
22
23
  }
23
24
  export interface FromDevRev {
@@ -62,6 +62,7 @@ class State {
62
62
  attachmentsMetadata: {
63
63
  artifactIds: [],
64
64
  lastProcessed: 0,
65
+ lastProcessedAttachmentsIdsList: [],
65
66
  },
66
67
  },
67
68
  };
@@ -258,9 +258,10 @@ export type StreamAttachmentsReturnType = {
258
258
  delay?: number;
259
259
  error?: ErrorRecord;
260
260
  } | undefined;
261
- export type ExternalSystemAttachmentReducerFunction<Batch, NewBatch, ConnectorState> = ({ attachments, adapter, }: {
261
+ export type ExternalSystemAttachmentReducerFunction<Batch, NewBatch, ConnectorState> = ({ attachments, adapter, batchSize, }: {
262
262
  attachments: Batch;
263
263
  adapter: WorkerAdapter<ConnectorState>;
264
+ batchSize?: number;
264
265
  }) => NewBatch;
265
266
  export type ExternalProcessAttachmentFunction = ({ attachment, stream, }: {
266
267
  attachment: NormalizedAttachment;
@@ -24,10 +24,10 @@ export declare function createWorkerAdapter<ConnectorState>({ event, adapterStat
24
24
  export declare class WorkerAdapter<ConnectorState> {
25
25
  readonly event: AirdropEvent;
26
26
  readonly options?: WorkerAdapterOptions;
27
+ isTimeout: boolean;
27
28
  private adapterState;
28
29
  private _artifacts;
29
30
  private hasWorkerEmitted;
30
- private isTimeout;
31
31
  private repos;
32
32
  private loaderReports;
33
33
  private _processedFiles;
@@ -68,6 +68,27 @@ export declare class WorkerAdapter<ConnectorState> {
68
68
  item: ExternalSystemAttachment;
69
69
  create: ExternalSystemLoadingFunction<ExternalSystemAttachment>;
70
70
  }): Promise<LoadItemResponse>;
71
+ /**
72
+ * Transforms an array of attachments into array of batches of the specified size.
73
+ *
74
+ * @param {Object} parameters - The parameters object
75
+ * @param {NormalizedAttachment[]} parameters.attachments - Array of attachments to be processed
76
+ * @param {number} [parameters.batchSize=1] - The size of each batch (defaults to 1)
77
+ * @param {ConnectorState} parameters.adapter - The adapter instance
78
+ * @returns {NormalizedAttachment[][]} An array of attachment batches
79
+ */
80
+ private defaultAttachmentsReducer;
81
+ /**
82
+ * This iterator function processes attachments batch by batch, saves progress to state, and handles rate limiting.
83
+ *
84
+ * @param {Object} parameters - The parameters object
85
+ * @param {NormalizedAttachment[][]} parameters.reducedAttachments - Array of attachment batches to process
86
+ * @param {Object} parameters.adapter - The connector adapter that contains state and processing methods
87
+ * @param {Object} parameters.stream - Stream object for logging or progress reporting
88
+ * @returns {Promise<{delay?: number} | void>} Returns an object with delay information if rate-limited, otherwise void
89
+ * @throws Will not throw exceptions but will log warnings for processing failures
90
+ */
91
+ private defaultAttachmentsIterator;
71
92
  /**
72
93
  * Streams the attachments to the DevRev platform.
73
94
  * The attachments are streamed to the platform and the artifact information is returned.
@@ -75,8 +96,9 @@ export declare class WorkerAdapter<ConnectorState> {
75
96
  * @returns {Promise<StreamAttachmentsReturnType>} - The response object containing the ssorAttachment artifact information
76
97
  * or error information if there was an error
77
98
  */
78
- streamAttachments<NewBatch>({ stream, processors, }: {
99
+ streamAttachments<NewBatch>({ stream, processors, batchSize, }: {
79
100
  stream: ExternalSystemAttachmentStreamingFunction;
80
101
  processors?: ExternalSystemAttachmentProcessors<ConnectorState, NormalizedAttachment[], NewBatch>;
102
+ batchSize?: number;
81
103
  }): Promise<StreamAttachmentsReturnType>;
82
104
  }
@@ -39,6 +39,104 @@ function createWorkerAdapter({ event, adapterState, options, }) {
39
39
  class WorkerAdapter {
40
40
  constructor({ event, adapterState, options, }) {
41
41
  this.repos = [];
42
+ /**
43
+ * Transforms an array of attachments into array of batches of the specified size.
44
+ *
45
+ * @param {Object} parameters - The parameters object
46
+ * @param {NormalizedAttachment[]} parameters.attachments - Array of attachments to be processed
47
+ * @param {number} [parameters.batchSize=1] - The size of each batch (defaults to 1)
48
+ * @param {ConnectorState} parameters.adapter - The adapter instance
49
+ * @returns {NormalizedAttachment[][]} An array of attachment batches
50
+ */
51
+ this.defaultAttachmentsReducer = ({ attachments, batchSize = 1 }) => {
52
+ // Transform the attachments array into smaller batches
53
+ const batches = attachments.reduce((result, item, index) => {
54
+ // Determine the index of the current batch
55
+ const batchIndex = Math.floor(index / batchSize);
56
+ // Initialize a new batch if it doesn't already exist
57
+ if (!result[batchIndex]) {
58
+ result[batchIndex] = [];
59
+ }
60
+ // Append the current item to the current batch
61
+ result[batchIndex].push(item);
62
+ return result;
63
+ }, []);
64
+ // Return the array of batches
65
+ return batches;
66
+ };
67
+ /**
68
+ * This iterator function processes attachments batch by batch, saves progress to state, and handles rate limiting.
69
+ *
70
+ * @param {Object} parameters - The parameters object
71
+ * @param {NormalizedAttachment[][]} parameters.reducedAttachments - Array of attachment batches to process
72
+ * @param {Object} parameters.adapter - The connector adapter that contains state and processing methods
73
+ * @param {Object} parameters.stream - Stream object for logging or progress reporting
74
+ * @returns {Promise<{delay?: number} | void>} Returns an object with delay information if rate-limited, otherwise void
75
+ * @throws Will not throw exceptions but will log warnings for processing failures
76
+ */
77
+ this.defaultAttachmentsIterator = async ({ reducedAttachments, adapter, stream }) => {
78
+ if (!adapter.state.toDevRev) {
79
+ const error = new Error(`toDevRev state is not defined.`);
80
+ console.error(error.message);
81
+ return { error };
82
+ }
83
+ // Get index of the last processed batch of this artifact
84
+ const lastProcessedBatchIndex = adapter.state.toDevRev.attachmentsMetadata.lastProcessed || 0;
85
+ // Get the list of successfully processed attachments in previous (possibly incomplete) batch extraction.
86
+ // If no such list exists, create an empty one.
87
+ if (!adapter.state.toDevRev.attachmentsMetadata
88
+ .lastProcessedAttachmentsIdsList) {
89
+ adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList =
90
+ [];
91
+ }
92
+ // Loop through the batches of attachments
93
+ for (let i = lastProcessedBatchIndex; i < reducedAttachments.length; i++) {
94
+ const attachmentsBatch = reducedAttachments[i];
95
+ // Create a list of promises for parallel processing
96
+ const promises = [];
97
+ for (const attachment of attachmentsBatch) {
98
+ if (adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList.includes(attachment.id)) {
99
+ console.log(`Attachment with ID ${attachment.id} has already been processed. Skipping.`);
100
+ continue; // Skip if the attachment ID is already processed
101
+ }
102
+ const promise = adapter
103
+ .processAttachment(attachment, stream)
104
+ .then((response) => {
105
+ var _a, _b, _c;
106
+ // Check if rate limit was hit
107
+ if (response === null || response === void 0 ? void 0 : response.delay) {
108
+ // Store this promise result to be checked later
109
+ return { delay: response.delay };
110
+ }
111
+ // No rate limiting, process normally
112
+ if ((_b = (_a = adapter.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata) === null || _b === void 0 ? void 0 : _b.lastProcessedAttachmentsIdsList) {
113
+ (_c = adapter.state.toDevRev) === null || _c === void 0 ? void 0 : _c.attachmentsMetadata.lastProcessedAttachmentsIdsList.push(attachment.id);
114
+ }
115
+ return null; // Return null for successful processing
116
+ })
117
+ .catch((error) => {
118
+ console.warn(`Skipping attachment with ID ${attachment.id} due to error: ${error}`);
119
+ return null; // Return null for errors too
120
+ });
121
+ promises.push(promise);
122
+ }
123
+ // Wait for all promises to settle and check for rate limiting
124
+ const results = await Promise.all(promises);
125
+ // Check if any of the results indicate rate limiting
126
+ const rateLimit = results.find((result) => result === null || result === void 0 ? void 0 : result.delay);
127
+ if (rateLimit) {
128
+ // Return the delay information to the caller
129
+ return { delay: rateLimit.delay };
130
+ }
131
+ if (adapter.state.toDevRev) {
132
+ // Update the last processed batch index
133
+ adapter.state.toDevRev.attachmentsMetadata.lastProcessed = i + 1;
134
+ // Reset successfullyProcessedAttachments list
135
+ adapter.state.toDevRev.attachmentsMetadata.lastProcessedAttachmentsIdsList.length = 0;
136
+ }
137
+ }
138
+ return {};
139
+ };
42
140
  this.event = event;
43
141
  this.options = options;
44
142
  this.adapterState = adapterState;
@@ -88,7 +186,7 @@ class WorkerAdapter {
88
186
  getRepo(itemType) {
89
187
  const repo = this.repos.find((repo) => repo.itemType === itemType);
90
188
  if (!repo) {
91
- console.error(`Repo not found for item type: ${itemType}.`);
189
+ console.error(`Repo for item type ${itemType} not found.`);
92
190
  return;
93
191
  }
94
192
  return repo;
@@ -586,14 +684,21 @@ class WorkerAdapter {
586
684
  * @returns {Promise<StreamAttachmentsReturnType>} - The response object containing the ssorAttachment artifact information
587
685
  * or error information if there was an error
588
686
  */
589
- async streamAttachments({ stream, processors, }) {
687
+ async streamAttachments({ stream, processors, batchSize = 1, // By default, we want to stream one attachment at a time
688
+ }) {
590
689
  var _a, _b;
690
+ if (batchSize <= 0) {
691
+ const error = new Error(`Invalid attachments batch size: ${batchSize}. Batch size must be greater than 0.`);
692
+ console.error(error.message);
693
+ return { error };
694
+ }
591
695
  const repos = [
592
696
  {
593
697
  itemType: 'ssor_attachment',
594
698
  },
595
699
  ];
596
700
  this.initializeRepos(repos);
701
+ // If there are no attachments metadata artifact IDs in state, finish here
597
702
  if (!((_b = (_a = this.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata) === null || _b === void 0 ? void 0 : _b.artifactIds) ||
598
703
  this.state.toDevRev.attachmentsMetadata.artifactIds.length === 0) {
599
704
  console.log(`No attachments metadata artifact IDs found in state.`);
@@ -602,6 +707,7 @@ class WorkerAdapter {
602
707
  else {
603
708
  console.log(`Found ${this.state.toDevRev.attachmentsMetadata.artifactIds.length} attachments metadata artifact IDs in state.`);
604
709
  }
710
+ // Loop through the attachments metadata artifact IDs
605
711
  while (this.state.toDevRev.attachmentsMetadata.artifactIds.length > 0) {
606
712
  const attachmentsMetadataArtifactId = this.state.toDevRev.attachmentsMetadata.artifactIds[0];
607
713
  console.log(`Started processing attachments for attachments metadata artifact ID: ${attachmentsMetadataArtifactId}.`);
@@ -620,35 +726,34 @@ class WorkerAdapter {
620
726
  continue;
621
727
  }
622
728
  console.log(`Found ${attachments.length} attachments for artifact ID: ${attachmentsMetadataArtifactId}.`);
729
+ // Use the reducer to split into batches.
730
+ let reducer;
731
+ // Use the iterator to process each batch, streaming all attachments inside one batch in parallel.
732
+ let iterator;
623
733
  if (processors) {
624
734
  console.log(`Using custom processors for attachments.`);
625
- const { reducer, iterator } = processors;
626
- const reducedAttachments = reducer({ attachments, adapter: this });
627
- const response = await iterator({
628
- reducedAttachments,
629
- adapter: this,
630
- stream,
631
- });
632
- if ((response === null || response === void 0 ? void 0 : response.delay) || (response === null || response === void 0 ? void 0 : response.error)) {
633
- return response;
634
- }
735
+ reducer = processors.reducer;
736
+ iterator = processors.iterator;
635
737
  }
636
738
  else {
637
739
  console.log(`Using default processors for attachments.`);
638
- const startIndex = this.state.toDevRev.attachmentsMetadata.lastProcessed || 0;
639
- const attachmentsToProcess = attachments.slice(startIndex);
640
- for (const attachment of attachmentsToProcess) {
641
- const response = await this.processAttachment(attachment, stream);
642
- if (response === null || response === void 0 ? void 0 : response.delay) {
643
- return response;
644
- }
645
- else if (response === null || response === void 0 ? void 0 : response.error) {
646
- console.warn('Skipping attachment due to an error while processing', attachment);
647
- }
648
- if (this.state.toDevRev) {
649
- this.state.toDevRev.attachmentsMetadata.lastProcessed += 1;
650
- }
651
- }
740
+ reducer = this
741
+ .defaultAttachmentsReducer;
742
+ iterator = this
743
+ .defaultAttachmentsIterator;
744
+ }
745
+ const reducedAttachments = reducer({
746
+ attachments,
747
+ adapter: this,
748
+ batchSize,
749
+ });
750
+ const response = await iterator({
751
+ reducedAttachments,
752
+ adapter: this,
753
+ stream,
754
+ });
755
+ if ((response === null || response === void 0 ? void 0 : response.delay) || (response === null || response === void 0 ? void 0 : response.error)) {
756
+ return response;
652
757
  }
653
758
  console.log(`Finished processing all attachments for artifact ID: ${attachmentsMetadataArtifactId}.`);
654
759
  this.state.toDevRev.attachmentsMetadata.artifactIds.shift();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,664 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const worker_adapter_1 = require("./worker-adapter");
4
+ const state_1 = require("../state/state");
5
+ const test_helpers_1 = require("../tests/test-helpers");
6
+ const types_1 = require("../types");
7
+ // Mock dependencies
8
+ jest.mock('../common/control-protocol', () => ({
9
+ emit: jest.fn().mockResolvedValue({}),
10
+ }));
11
+ // const mockPostState = jest.spyOn(State.prototype, 'postState').mockResolvedValue(); // Mock to resolve void
12
+ // const mockFetchState = jest.spyOn(State.prototype, 'fetchState').mockResolvedValue({}); // Mock to resolve a default state
13
+ jest.mock('../mappers/mappers');
14
+ jest.mock('../uploader/uploader');
15
+ // jest.mock('../state/state');
16
+ jest.mock('../repo/repo');
17
+ jest.mock('node:worker_threads', () => ({
18
+ parentPort: {
19
+ postMessage: jest.fn(),
20
+ },
21
+ }));
22
+ describe('WorkerAdapter', () => {
23
+ let adapter;
24
+ let mockEvent;
25
+ let mockAdapterState;
26
+ beforeEach(() => {
27
+ // Reset all mocks
28
+ jest.clearAllMocks();
29
+ // Create mock objects
30
+ mockEvent = (0, test_helpers_1.createEvent)({ eventType: types_1.EventType.ExtractionDataStart });
31
+ const initialState = {
32
+ attachments: { completed: false },
33
+ lastSyncStarted: '',
34
+ lastSuccessfulSyncStarted: '',
35
+ snapInVersionId: '',
36
+ toDevRev: {
37
+ attachmentsMetadata: {
38
+ artifactIds: [],
39
+ lastProcessed: 0,
40
+ lastProcessedAttachmentsIdsList: [],
41
+ },
42
+ },
43
+ };
44
+ mockAdapterState = new state_1.State({
45
+ event: mockEvent,
46
+ initialState: initialState,
47
+ });
48
+ // Create the adapter instance
49
+ adapter = new worker_adapter_1.WorkerAdapter({
50
+ event: mockEvent,
51
+ adapterState: mockAdapterState,
52
+ });
53
+ });
54
+ describe('defaultAttachmentsReducer', () => {
55
+ it('should correctly batch attachments based on the batchSize', () => {
56
+ // Arrange
57
+ const attachments = [
58
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
59
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
60
+ { url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
61
+ { url: 'http://example.com/file4.pdf', id: 'attachment4', file_name: 'file4.pdf', parent_id: 'parent4' },
62
+ { url: 'http://example.com/file5.pdf', id: 'attachment5', file_name: 'file5.pdf', parent_id: 'parent5' },
63
+ ];
64
+ // Act - call the private method using function call notation
65
+ const result = adapter['defaultAttachmentsReducer']({
66
+ attachments,
67
+ adapter,
68
+ batchSize: 2,
69
+ });
70
+ // Assert
71
+ expect(result).toHaveLength(3);
72
+ expect(result[0]).toHaveLength(2);
73
+ expect(result[1]).toHaveLength(2);
74
+ expect(result[2]).toHaveLength(1);
75
+ expect(result[0][0].id).toBe('attachment1');
76
+ expect(result[0][1].id).toBe('attachment2');
77
+ expect(result[1][0].id).toBe('attachment3');
78
+ expect(result[1][1].id).toBe('attachment4');
79
+ expect(result[2][0].id).toBe('attachment5');
80
+ });
81
+ it('should return a single batch when batchSize equals the number of attachments', () => {
82
+ // Arrange
83
+ const attachments = [
84
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
85
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
86
+ ];
87
+ // Act
88
+ const result = adapter['defaultAttachmentsReducer']({
89
+ attachments,
90
+ adapter,
91
+ batchSize: 2,
92
+ });
93
+ // Assert
94
+ expect(result).toHaveLength(1);
95
+ expect(result[0]).toHaveLength(2);
96
+ expect(result[0][0].id).toBe('attachment1');
97
+ expect(result[0][1].id).toBe('attachment2');
98
+ });
99
+ it('should return a single batch when batchSize is bigger than the number of attachments', () => {
100
+ // Arrange
101
+ const attachments = [
102
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
103
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
104
+ ];
105
+ // Act
106
+ const result = adapter['defaultAttachmentsReducer']({
107
+ attachments,
108
+ adapter,
109
+ batchSize: 10,
110
+ });
111
+ // Assert
112
+ expect(result).toHaveLength(1);
113
+ expect(result[0]).toHaveLength(2);
114
+ expect(result[0][0].id).toBe('attachment1');
115
+ expect(result[0][1].id).toBe('attachment2');
116
+ });
117
+ it('should handle empty attachments array', () => {
118
+ // Arrange
119
+ const attachments = [];
120
+ // Act
121
+ const result = adapter['defaultAttachmentsReducer']({
122
+ attachments,
123
+ adapter,
124
+ batchSize: 2,
125
+ });
126
+ // Assert
127
+ expect(result).toHaveLength(0);
128
+ });
129
+ it('should default to batchSize of 1 if not provided', () => {
130
+ // Arrange
131
+ const attachments = [
132
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
133
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
134
+ ];
135
+ // Act
136
+ const result = adapter['defaultAttachmentsReducer']({
137
+ attachments,
138
+ adapter
139
+ });
140
+ // Assert
141
+ expect(result).toHaveLength(2);
142
+ expect(result[0]).toHaveLength(1);
143
+ expect(result[1]).toHaveLength(1);
144
+ });
145
+ it('should handle invalid (0) batch size', async () => {
146
+ var _a;
147
+ // Arrange
148
+ const mockStream = jest.fn();
149
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
150
+ // Act
151
+ const result = await adapter.streamAttachments({
152
+ stream: mockStream,
153
+ batchSize: 0,
154
+ });
155
+ // Assert
156
+ expect(consoleErrorSpy).toHaveBeenCalled();
157
+ expect(result).toEqual({
158
+ error: expect.any(Error),
159
+ });
160
+ expect((_a = result === null || result === void 0 ? void 0 : result.error) === null || _a === void 0 ? void 0 : _a.message).toContain('Invalid attachments batch size');
161
+ // Restore console.error
162
+ consoleErrorSpy.mockRestore();
163
+ });
164
+ it('should handle invalid (negative) batch size', async () => {
165
+ var _a;
166
+ // Arrange
167
+ const mockStream = jest.fn();
168
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
169
+ // Act
170
+ const result = await adapter.streamAttachments({
171
+ stream: mockStream,
172
+ batchSize: -1,
173
+ });
174
+ // Assert
175
+ expect(consoleErrorSpy).toHaveBeenCalled();
176
+ expect(result).toEqual({
177
+ error: expect.any(Error),
178
+ });
179
+ expect((_a = result === null || result === void 0 ? void 0 : result.error) === null || _a === void 0 ? void 0 : _a.message).toContain('Invalid attachments batch size');
180
+ // Restore console.error
181
+ consoleErrorSpy.mockRestore();
182
+ });
183
+ });
184
+ describe('defaultAttachmentsIterator', () => {
185
+ it('should process all batches of attachments', async () => {
186
+ var _a, _b;
187
+ // Arrange
188
+ const mockAttachments = [
189
+ [
190
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
191
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
192
+ ],
193
+ [
194
+ { url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
195
+ ],
196
+ ];
197
+ const mockStream = jest.fn();
198
+ // Mock the processAttachment method
199
+ adapter.processAttachment = jest.fn().mockResolvedValue(null);
200
+ // Act
201
+ const result = await adapter['defaultAttachmentsIterator']({
202
+ reducedAttachments: mockAttachments,
203
+ adapter: adapter,
204
+ stream: mockStream,
205
+ });
206
+ // Assert
207
+ expect(adapter.processAttachment).toHaveBeenCalledTimes(3);
208
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
209
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][1], mockStream);
210
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[1][0], mockStream);
211
+ // Verify the state was updated correctly
212
+ expect((_a = adapter.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata.lastProcessed).toBe(2);
213
+ expect((_b = adapter.state.toDevRev) === null || _b === void 0 ? void 0 : _b.attachmentsMetadata.lastProcessedAttachmentsIdsList).toEqual([]);
214
+ expect(result).toEqual({});
215
+ });
216
+ it('should handle rate limiting during processing', async () => {
217
+ var _a;
218
+ // Arrange
219
+ const mockAttachments = [
220
+ [
221
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
222
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
223
+ ],
224
+ [
225
+ { url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
226
+ ],
227
+ ];
228
+ const mockStream = jest.fn();
229
+ // Mock the processAttachment method to simulate rate limiting on the second attachment
230
+ adapter.processAttachment = jest.fn()
231
+ .mockResolvedValueOnce(null) // First attachment processes successfully
232
+ .mockResolvedValueOnce({ delay: 30 }); // Second attachment hits rate limit
233
+ // Set up adapter state
234
+ adapter.state.toDevRev = {
235
+ attachmentsMetadata: {
236
+ lastProcessed: 0,
237
+ artifactIds: [],
238
+ lastProcessedAttachmentsIdsList: [],
239
+ },
240
+ };
241
+ // Act
242
+ const result = await adapter['defaultAttachmentsIterator']({
243
+ reducedAttachments: mockAttachments,
244
+ adapter: adapter,
245
+ stream: mockStream,
246
+ });
247
+ // Assert
248
+ expect(adapter.processAttachment).toHaveBeenCalledTimes(2);
249
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
250
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][1], mockStream);
251
+ // Verify the delay was returned
252
+ expect(result).toEqual({ delay: 30 });
253
+ // And lastProcessed wasn't updated yet
254
+ expect((_a = adapter.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata.lastProcessed).toBe(0);
255
+ });
256
+ it('should skip already processed attachments', async () => {
257
+ // Arrange
258
+ const mockAttachments = [
259
+ [
260
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
261
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
262
+ ],
263
+ ];
264
+ const mockStream = jest.fn();
265
+ // Mock the processAttachment method
266
+ adapter.processAttachment = jest.fn().mockResolvedValue(null);
267
+ // Set up adapter state to indicate attachment1 was already processed
268
+ adapter.state.toDevRev = {
269
+ attachmentsMetadata: {
270
+ lastProcessed: 0,
271
+ artifactIds: [],
272
+ lastProcessedAttachmentsIdsList: ['attachment1'],
273
+ },
274
+ };
275
+ // Act
276
+ await adapter['defaultAttachmentsIterator']({
277
+ reducedAttachments: mockAttachments,
278
+ adapter: adapter,
279
+ stream: mockStream,
280
+ });
281
+ // Assert
282
+ expect(adapter.processAttachment).toHaveBeenCalledTimes(1);
283
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[0][1], mockStream);
284
+ expect(adapter.processAttachment).not.toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
285
+ });
286
+ it('should continue from last processed batch', async () => {
287
+ // Arrange
288
+ const mockAttachments = [
289
+ [
290
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
291
+ ],
292
+ [
293
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
294
+ ],
295
+ ];
296
+ const mockStream = jest.fn();
297
+ // Mock the processAttachment method
298
+ adapter.processAttachment = jest.fn().mockResolvedValue(null);
299
+ // Set up adapter state to indicate we already processed the first batch
300
+ adapter.state.toDevRev = {
301
+ attachmentsMetadata: {
302
+ lastProcessed: 1, // Skip first batch (index 0)
303
+ artifactIds: [],
304
+ lastProcessedAttachmentsIdsList: [],
305
+ },
306
+ };
307
+ // Act
308
+ await adapter['defaultAttachmentsIterator']({
309
+ reducedAttachments: mockAttachments,
310
+ adapter: adapter,
311
+ stream: mockStream,
312
+ });
313
+ // Assert
314
+ expect(adapter.processAttachment).toHaveBeenCalledTimes(1);
315
+ expect(adapter.processAttachment).toHaveBeenCalledWith(mockAttachments[1][0], mockStream);
316
+ expect(adapter.processAttachment).not.toHaveBeenCalledWith(mockAttachments[0][0], mockStream);
317
+ });
318
+ it('should handle errors during processing and continue', async () => {
319
+ // Arrange
320
+ const mockAttachments = [
321
+ [
322
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
323
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
324
+ ],
325
+ ];
326
+ const mockStream = jest.fn();
327
+ // Mock processAttachment to throw an error for the first attachment
328
+ adapter.processAttachment = jest.fn()
329
+ .mockRejectedValueOnce(new Error('Processing error'))
330
+ .mockResolvedValueOnce(null);
331
+ // Mock console.warn to avoid test output noise
332
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
333
+ // Set up adapter state
334
+ adapter.state.toDevRev = {
335
+ attachmentsMetadata: {
336
+ lastProcessed: 0,
337
+ artifactIds: [],
338
+ lastProcessedAttachmentsIdsList: [],
339
+ },
340
+ };
341
+ // Act
342
+ const result = await adapter['defaultAttachmentsIterator']({
343
+ reducedAttachments: mockAttachments,
344
+ adapter: adapter,
345
+ stream: mockStream,
346
+ });
347
+ // Assert - both attachments should have been processed
348
+ expect(adapter.processAttachment).toHaveBeenCalledTimes(2);
349
+ expect(consoleWarnSpy).toHaveBeenCalled();
350
+ expect(result).toEqual({});
351
+ // Restore console.warn
352
+ consoleWarnSpy.mockRestore();
353
+ });
354
+ });
355
+ describe('streamAttachments', () => {
356
+ it('should process all artifact batches successfully', async () => {
357
+ // Arrange
358
+ const mockStream = jest.fn();
359
+ // Set up adapter state with artifact IDs
360
+ adapter.state.toDevRev = {
361
+ attachmentsMetadata: {
362
+ artifactIds: ['artifact1', 'artifact2'],
363
+ lastProcessed: 0,
364
+ lastProcessedAttachmentsIdsList: [],
365
+ },
366
+ };
367
+ // Mock getting attachments from each artifact
368
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn()
369
+ .mockResolvedValueOnce({
370
+ attachments: [
371
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
372
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
373
+ ],
374
+ })
375
+ .mockResolvedValueOnce({
376
+ attachments: [
377
+ { url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
378
+ ],
379
+ });
380
+ // Mock the initializeRepos method
381
+ adapter.initializeRepos = jest.fn();
382
+ // Mock the defaultAttachmentsReducer and defaultAttachmentsIterator
383
+ const mockReducedAttachments = [['batch1']];
384
+ adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue(mockReducedAttachments);
385
+ adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({});
386
+ // Act
387
+ const result = await adapter.streamAttachments({
388
+ stream: mockStream,
389
+ });
390
+ // Assert
391
+ expect(adapter.initializeRepos).toHaveBeenCalledWith([
392
+ { itemType: 'ssor_attachment' },
393
+ ]);
394
+ expect(adapter['uploader'].getAttachmentsFromArtifactId).toHaveBeenCalledTimes(2);
395
+ expect(adapter['defaultAttachmentsReducer']).toHaveBeenCalledTimes(2);
396
+ expect(adapter['defaultAttachmentsIterator']).toHaveBeenCalledTimes(2);
397
+ // Verify state was updated correctly
398
+ expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toEqual([]);
399
+ expect(adapter.state.toDevRev.attachmentsMetadata.lastProcessed).toBe(0);
400
+ expect(result).toBeUndefined();
401
+ });
402
+ it('should handle invalid batch size', async () => {
403
+ var _a;
404
+ // Arrange
405
+ const mockStream = jest.fn();
406
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
407
+ // Act
408
+ const result = await adapter.streamAttachments({
409
+ stream: mockStream,
410
+ batchSize: 0,
411
+ });
412
+ // Assert
413
+ expect(consoleErrorSpy).toHaveBeenCalled();
414
+ expect(result).toEqual({
415
+ error: expect.any(Error),
416
+ });
417
+ expect((_a = result === null || result === void 0 ? void 0 : result.error) === null || _a === void 0 ? void 0 : _a.message).toContain('Invalid attachments batch size');
418
+ // Restore console.error
419
+ consoleErrorSpy.mockRestore();
420
+ });
421
+ it('should handle empty attachments metadata artifact IDs', async () => {
422
+ // Arrange
423
+ const mockStream = jest.fn();
424
+ // Set up adapter state with no artifact IDs
425
+ adapter.state.toDevRev = {
426
+ attachmentsMetadata: {
427
+ artifactIds: [],
428
+ lastProcessed: 0,
429
+ },
430
+ };
431
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
432
+ // Act
433
+ const result = await adapter.streamAttachments({
434
+ stream: mockStream,
435
+ });
436
+ // Assert
437
+ expect(consoleLogSpy).toHaveBeenCalledWith('No attachments metadata artifact IDs found in state.');
438
+ expect(result).toBeUndefined();
439
+ // Restore console.log
440
+ consoleLogSpy.mockRestore();
441
+ });
442
+ it('should handle errors when getting attachments', async () => {
443
+ // Arrange
444
+ const mockStream = jest.fn();
445
+ // Set up adapter state with artifact IDs
446
+ adapter.state.toDevRev = {
447
+ attachmentsMetadata: {
448
+ artifactIds: ['artifact1'],
449
+ lastProcessed: 0,
450
+ lastProcessedAttachmentsIdsList: [],
451
+ },
452
+ };
453
+ // Mock error when getting attachments
454
+ const mockError = new Error('Failed to get attachments');
455
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
456
+ error: mockError,
457
+ });
458
+ // Mock methods
459
+ adapter.initializeRepos = jest.fn();
460
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
461
+ // Act
462
+ const result = await adapter.streamAttachments({
463
+ stream: mockStream,
464
+ });
465
+ // Assert
466
+ expect(consoleErrorSpy).toHaveBeenCalled();
467
+ expect(result).toEqual({
468
+ error: mockError,
469
+ });
470
+ // Restore console.error
471
+ consoleErrorSpy.mockRestore();
472
+ });
473
+ it('should handle empty attachments array from artifact', async () => {
474
+ // Arrange
475
+ const mockStream = jest.fn();
476
+ // Set up adapter state with artifact IDs
477
+ adapter.state.toDevRev = {
478
+ attachmentsMetadata: {
479
+ artifactIds: ['artifact1'],
480
+ lastProcessed: 0,
481
+ lastProcessedAttachmentsIdsList: [],
482
+ },
483
+ };
484
+ // Mock getting empty attachments
485
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
486
+ attachments: [],
487
+ });
488
+ // Mock methods
489
+ adapter.initializeRepos = jest.fn();
490
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
491
+ // Act
492
+ const result = await adapter.streamAttachments({
493
+ stream: mockStream,
494
+ });
495
+ // Assert
496
+ expect(consoleWarnSpy).toHaveBeenCalled();
497
+ expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toEqual([]);
498
+ expect(result).toBeUndefined();
499
+ // Restore console.warn
500
+ consoleWarnSpy.mockRestore();
501
+ });
502
+ it('should use custom processors when provided', async () => {
503
+ // Arrange
504
+ const mockStream = jest.fn();
505
+ const mockReducer = jest.fn().mockReturnValue(['custom-reduced']);
506
+ const mockIterator = jest.fn().mockResolvedValue({});
507
+ // Set up adapter state with artifact IDs
508
+ adapter.state.toDevRev = {
509
+ attachmentsMetadata: {
510
+ artifactIds: ['artifact1'],
511
+ lastProcessed: 0,
512
+ lastProcessedAttachmentsIdsList: [],
513
+ },
514
+ };
515
+ // Mock getting attachments
516
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
517
+ attachments: [{ id: 'attachment1' }],
518
+ });
519
+ // Mock methods
520
+ adapter.initializeRepos = jest.fn();
521
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
522
+ // Act
523
+ const result = await adapter.streamAttachments({
524
+ stream: mockStream,
525
+ processors: {
526
+ reducer: mockReducer,
527
+ iterator: mockIterator,
528
+ },
529
+ });
530
+ // Assert
531
+ expect(mockReducer).toHaveBeenCalledWith({
532
+ attachments: [{ id: 'attachment1' }],
533
+ adapter: adapter,
534
+ batchSize: 1,
535
+ });
536
+ expect(mockIterator).toHaveBeenCalledWith({
537
+ reducedAttachments: ['custom-reduced'],
538
+ adapter: adapter,
539
+ stream: mockStream,
540
+ });
541
+ expect(result).toBeUndefined();
542
+ // Restore console.log
543
+ consoleLogSpy.mockRestore();
544
+ });
545
+ it('should handle rate limiting from iterator', async () => {
546
+ // Arrange
547
+ const mockStream = jest.fn();
548
+ // Set up adapter state with artifact IDs
549
+ adapter.state.toDevRev = {
550
+ attachmentsMetadata: {
551
+ artifactIds: ['artifact1'],
552
+ lastProcessed: 0,
553
+ lastProcessedAttachmentsIdsList: [],
554
+ },
555
+ };
556
+ // Mock getting attachments
557
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
558
+ attachments: [{ id: 'attachment1' }],
559
+ });
560
+ // Mock methods
561
+ adapter.initializeRepos = jest.fn();
562
+ adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue([]);
563
+ adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({
564
+ delay: 30,
565
+ });
566
+ // Act
567
+ const result = await adapter.streamAttachments({
568
+ stream: mockStream,
569
+ });
570
+ // Assert
571
+ expect(result).toEqual({
572
+ delay: 30,
573
+ });
574
+ // The artifactIds array should remain unchanged
575
+ expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toEqual(['artifact1']);
576
+ });
577
+ it('should handle error from iterator', async () => {
578
+ // Arrange
579
+ const mockStream = jest.fn();
580
+ // Set up adapter state with artifact IDs
581
+ adapter.state.toDevRev = {
582
+ attachmentsMetadata: {
583
+ artifactIds: ['artifact1'],
584
+ lastProcessed: 0,
585
+ lastProcessedAttachmentsIdsList: [],
586
+ },
587
+ };
588
+ // Mock getting attachments
589
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
590
+ attachments: [{ id: 'attachment1' }],
591
+ });
592
+ // Mock methods
593
+ adapter.initializeRepos = jest.fn();
594
+ adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue([]);
595
+ const mockError = new Error('Iterator error');
596
+ adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({
597
+ error: mockError,
598
+ });
599
+ // Act
600
+ const result = await adapter.streamAttachments({
601
+ stream: mockStream,
602
+ });
603
+ // Assert
604
+ expect(result).toEqual({
605
+ error: mockError,
606
+ });
607
+ // The artifactIds array should remain unchanged
608
+ expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toEqual(['artifact1']);
609
+ });
610
+ it('should continue processing from last processed attachment for the current artifact', async () => {
611
+ const mockStream = jest.fn();
612
+ adapter.state.toDevRev = {
613
+ attachmentsMetadata: {
614
+ artifactIds: ['artifact1'],
615
+ lastProcessed: 0,
616
+ lastProcessedAttachmentsIdsList: ['attachment1', 'attachment2'],
617
+ },
618
+ };
619
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
620
+ attachments: [
621
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
622
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
623
+ { url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
624
+ ],
625
+ });
626
+ adapter.processAttachment = jest.fn().mockResolvedValue(null);
627
+ await adapter.streamAttachments({
628
+ stream: mockStream,
629
+ batchSize: 3,
630
+ });
631
+ expect(adapter.processAttachment).toHaveBeenCalledTimes(1);
632
+ expect(adapter.processAttachment).toHaveBeenCalledWith({
633
+ url: 'http://example.com/file3.pdf',
634
+ id: 'attachment3',
635
+ file_name: 'file3.pdf',
636
+ parent_id: 'parent3'
637
+ }, mockStream);
638
+ });
639
+ it('should reset lastProcessed and attachment IDs list after processing all artifacts', async () => {
640
+ const mockStream = jest.fn();
641
+ adapter.state.toDevRev = {
642
+ attachmentsMetadata: {
643
+ artifactIds: ['artifact1'],
644
+ lastProcessed: 0,
645
+ lastProcessedAttachmentsIdsList: [],
646
+ },
647
+ };
648
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn()
649
+ .mockResolvedValueOnce({
650
+ attachments: [
651
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
652
+ { url: 'http://example.com/file2.pdf', id: 'attachment2', file_name: 'file2.pdf', parent_id: 'parent2' },
653
+ { url: 'http://example.com/file3.pdf', id: 'attachment3', file_name: 'file3.pdf', parent_id: 'parent3' },
654
+ ],
655
+ });
656
+ adapter.processAttachment = jest.fn().mockResolvedValue(null);
657
+ await adapter.streamAttachments({
658
+ stream: mockStream,
659
+ });
660
+ expect(adapter.state.toDevRev.attachmentsMetadata.artifactIds).toHaveLength(0);
661
+ expect(adapter.state.toDevRev.attachmentsMetadata.lastProcessed).toBe(0);
662
+ });
663
+ });
664
+ });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@devrev/ts-adaas",
3
- "version": "1.3.0",
4
- "description": "DevRev ADaaS (AirDrop-as-a-Service) Typescript SDK.",
3
+ "version": "1.4.1",
4
+ "description": "Typescript library containing the ADaaS(AirDrop as a Service) control protocol.",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",
7
7
  "typings": "./dist/index.d.ts",
@@ -23,22 +23,22 @@
23
23
  "author": "devrev",
24
24
  "license": "ISC",
25
25
  "devDependencies": {
26
- "@types/jest": "^29.5.12",
26
+ "@types/jest": "^29.5.14",
27
27
  "@types/lambda-log": "^3.0.3",
28
- "@types/node": "20.16.11",
28
+ "@types/node": "^22.15.18",
29
29
  "@types/yargs": "^17.0.33",
30
- "@typescript-eslint/eslint-plugin": "^7.12.0",
31
- "@typescript-eslint/parser": "^7.12.0",
32
- "eslint": "^8.57.0",
33
- "eslint-config-prettier": "^9.1.0",
30
+ "@typescript-eslint/eslint-plugin": "^8.32.1",
31
+ "@typescript-eslint/parser": "^8.32.1",
32
+ "eslint": "^9.26.0",
33
+ "eslint-config-prettier": "^10.1.5",
34
34
  "eslint-plugin-prettier": "^5.1.3",
35
35
  "jest": "^29.7.0",
36
- "ts-jest": "^29.1.2",
36
+ "ts-jest": "^29.3.3",
37
37
  "typescript": "^5.3.3"
38
38
  },
39
39
  "dependencies": {
40
- "@devrev/typescript-sdk": "^1.1.54",
41
- "axios": "^1.7.9",
40
+ "@devrev/typescript-sdk": "^1.1.59",
41
+ "axios": "^1.9.0",
42
42
  "axios-retry": "^4.5.0",
43
43
  "form-data": "^4.0.1",
44
44
  "js-jsonl": "^1.1.1",