@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 +1 -272
- package/dist/repo/repo.d.ts +1 -1
- package/dist/repo/repo.interfaces.d.ts +1 -1
- package/dist/state/state.interfaces.d.ts +1 -0
- package/dist/state/state.js +1 -0
- package/dist/types/extraction.d.ts +2 -1
- package/dist/workers/worker-adapter.d.ts +24 -2
- package/dist/workers/worker-adapter.js +131 -26
- package/dist/workers/worker-adapter.test.d.ts +1 -0
- package/dist/workers/worker-adapter.test.js +664 -0
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -23,275 +23,4 @@ npm install @devrev/ts-adaas
|
|
|
23
23
|
|
|
24
24
|
## Reference
|
|
25
25
|
|
|
26
|
-
|
|
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.
|
package/dist/repo/repo.d.ts
CHANGED
|
@@ -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
|
|
14
|
+
push(items: Item[]): Promise<boolean>;
|
|
15
15
|
}
|
package/dist/state/state.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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.
|
|
4
|
-
"description": "
|
|
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.
|
|
26
|
+
"@types/jest": "^29.5.14",
|
|
27
27
|
"@types/lambda-log": "^3.0.3",
|
|
28
|
-
"@types/node": "
|
|
28
|
+
"@types/node": "^22.15.18",
|
|
29
29
|
"@types/yargs": "^17.0.33",
|
|
30
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
31
|
-
"@typescript-eslint/parser": "^
|
|
32
|
-
"eslint": "^
|
|
33
|
-
"eslint-config-prettier": "^
|
|
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.
|
|
36
|
+
"ts-jest": "^29.3.3",
|
|
37
37
|
"typescript": "^5.3.3"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@devrev/typescript-sdk": "^1.1.
|
|
41
|
-
"axios": "^1.
|
|
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",
|