@devrev/ts-adaas 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,8 +4,7 @@
4
4
 
5
5
  ## Overview
6
6
 
7
- The Airdrop SDK for TypeScript helps developers build snap-ins that integrate with DevRev’s Airdrop platform.
8
- This SDK simplifies the workflow for handling data extraction and loading, event-driven actions, state management, and artifact handling.
7
+ The Airdrop SDK for TypeScript helps developers build snap-ins that integrate with DevRev’s Airdrop platform. This SDK simplifies the workflow for handling data extraction and loading, event-driven actions, state management, and artifact handling.
9
8
 
10
9
  It provides features such as:
11
10
 
@@ -1,8 +1,8 @@
1
1
  import { AxiosResponse } from 'axios';
2
2
  import { MappersFactoryInterface, MappersCreateParams, MappersCreateResponse, MappersGetByTargetIdParams, MappersGetByTargetIdResponse, MappersUpdateParams, MappersUpdateResponse } from './mappers.interface';
3
3
  export declare class Mappers {
4
- private endpoint;
5
- private token;
4
+ private devrevApiEndpoint;
5
+ private devrevApiToken;
6
6
  constructor({ event }: MappersFactoryInterface);
7
7
  getByTargetId(params: MappersGetByTargetIdParams): Promise<AxiosResponse<MappersGetByTargetIdResponse>>;
8
8
  create(params: MappersCreateParams): Promise<AxiosResponse<MappersCreateResponse>>;
@@ -4,29 +4,29 @@ exports.Mappers = void 0;
4
4
  const axios_client_1 = require("../http/axios-client");
5
5
  class Mappers {
6
6
  constructor({ event }) {
7
- this.endpoint = event.execution_metadata.devrev_endpoint;
8
- this.token = event.context.secrets.service_account_token;
7
+ this.devrevApiEndpoint = event.execution_metadata.devrev_endpoint;
8
+ this.devrevApiToken = event.context.secrets.service_account_token;
9
9
  }
10
10
  async getByTargetId(params) {
11
11
  const { sync_unit, target } = params;
12
- return axios_client_1.axiosClient.get(`${this.endpoint}/internal/airdrop.sync-mapper-record.get-by-target`, {
12
+ return axios_client_1.axiosClient.get(`${this.devrevApiEndpoint}/internal/airdrop.sync-mapper-record.get-by-target`, {
13
13
  headers: {
14
- Authorization: this.token,
14
+ Authorization: this.devrevApiToken,
15
15
  },
16
16
  params: { sync_unit, target },
17
17
  });
18
18
  }
19
19
  async create(params) {
20
- return axios_client_1.axiosClient.post(`${this.endpoint}/internal/airdrop.sync-mapper-record.create`, params, {
20
+ return axios_client_1.axiosClient.post(`${this.devrevApiEndpoint}/internal/airdrop.sync-mapper-record.create`, params, {
21
21
  headers: {
22
- Authorization: this.token,
22
+ Authorization: this.devrevApiToken,
23
23
  },
24
24
  });
25
25
  }
26
26
  async update(params) {
27
- return axios_client_1.axiosClient.post(`${this.endpoint}/internal/airdrop.sync-mapper-record.update`, params, {
27
+ return axios_client_1.axiosClient.post(`${this.devrevApiEndpoint}/internal/airdrop.sync-mapper-record.update`, params, {
28
28
  headers: {
29
- Authorization: this.token,
29
+ Authorization: this.devrevApiToken,
30
30
  },
31
31
  });
32
32
  }
@@ -77,6 +77,13 @@ export interface ExternalSyncUnit {
77
77
  item_count?: number;
78
78
  item_type?: string;
79
79
  }
80
+ /**
81
+ * InitialSyncScope is an enum that defines the different scopes of initial sync that can be used by the external extractor.
82
+ */
83
+ export declare enum InitialSyncScope {
84
+ FULL_HISTORY = "full-history",
85
+ TIME_SCOPED = "time-scoped"
86
+ }
80
87
  /**
81
88
  * EventContextIn is an interface that defines the structure of the input event context that is sent to the external extractor from ADaaS.
82
89
  * @deprecated
@@ -114,7 +121,7 @@ export interface EventContextOut {
114
121
  sync_unit?: string;
115
122
  }
116
123
  /**
117
- * EventContext is an interface that defines the structure of the event context that is sent to and from the external connector.
124
+ * EventContext is an interface that defines the structure of the event context that is sent to the external connector from Airdrop.
118
125
  */
119
126
  export interface EventContext {
120
127
  callback_url: string;
@@ -127,9 +134,12 @@ export interface EventContext {
127
134
  external_sync_unit_name: string;
128
135
  external_system: string;
129
136
  external_system_type: string;
137
+ extract_from?: string;
130
138
  import_slug: string;
139
+ initial_sync_scope?: InitialSyncScope;
131
140
  mode: string;
132
141
  request_id: string;
142
+ reset_extraction?: boolean;
133
143
  snap_in_slug: string;
134
144
  snap_in_version_id: string;
135
145
  sync_run: string;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SyncMode = exports.ExtractionMode = exports.ExtractorEventType = exports.EventType = void 0;
3
+ exports.InitialSyncScope = exports.SyncMode = exports.ExtractionMode = exports.ExtractorEventType = exports.EventType = void 0;
4
4
  /**
5
5
  * EventType is an enum that defines the different types of events that can be sent to the external extractor from ADaaS.
6
6
  * The external extractor can use these events to know what to do next in the extraction process.
@@ -70,3 +70,11 @@ var SyncMode;
70
70
  SyncMode["INCREMENTAL"] = "INCREMENTAL";
71
71
  SyncMode["LOADING"] = "LOADING";
72
72
  })(SyncMode || (exports.SyncMode = SyncMode = {}));
73
+ /**
74
+ * InitialSyncScope is an enum that defines the different scopes of initial sync that can be used by the external extractor.
75
+ */
76
+ var InitialSyncScope;
77
+ (function (InitialSyncScope) {
78
+ InitialSyncScope["FULL_HISTORY"] = "full-history";
79
+ InitialSyncScope["TIME_SCOPED"] = "time-scoped";
80
+ })(InitialSyncScope || (exports.InitialSyncScope = InitialSyncScope = {}));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const extraction_1 = require("./extraction");
4
+ const test_helpers_1 = require("../tests/test-helpers");
5
+ describe('EventContext type tests', () => {
6
+ const baseEvent = (0, test_helpers_1.createEvent)({ eventType: extraction_1.EventType.ExtractionDataStart });
7
+ it('should handle context without optional fields', () => {
8
+ const event = Object.assign({}, baseEvent);
9
+ // If this compiles, the test passes
10
+ expect(event).toBeDefined();
11
+ });
12
+ it('should handle context with all optional fields', () => {
13
+ const event = Object.assign({}, baseEvent);
14
+ event.payload.event_context = Object.assign(Object.assign({}, baseEvent.payload.event_context), { extract_from: '2024-01-01T00:00:00Z', initial_sync_scope: extraction_1.InitialSyncScope.TIME_SCOPED, reset_extraction: true });
15
+ // Test with all optionals present
16
+ expect(event).toBeDefined();
17
+ });
18
+ it('should handle partial optional fields', () => {
19
+ const event = Object.assign({}, baseEvent);
20
+ event.payload.event_context = Object.assign(Object.assign({}, baseEvent.payload.event_context), { extract_from: '2024-01-01T00:00:00Z' });
21
+ expect(event).toBeDefined();
22
+ });
23
+ });
@@ -1,11 +1,13 @@
1
1
  import { NormalizedAttachment } from '../repo/repo.interfaces';
2
- import { ArtifactsPrepareResponse, UploadResponse, UploaderFactoryInterface } from './uploader.interfaces';
2
+ import { UploadResponse, UploaderFactoryInterface, ArtifactToUpload } from './uploader.interfaces';
3
3
  import { AxiosResponse } from 'axios';
4
4
  export declare class Uploader {
5
5
  private event;
6
6
  private isLocalDevelopment?;
7
7
  private devrevApiEndpoint;
8
8
  private devrevApiToken;
9
+ private requestId;
10
+ private defaultHeaders;
9
11
  constructor({ event, options }: UploaderFactoryInterface);
10
12
  /**
11
13
  * Uploads the fetched objects to the DevRev platform.
@@ -17,9 +19,10 @@ export declare class Uploader {
17
19
  * or error information if there was an error
18
20
  */
19
21
  upload(itemType: string, fetchedObjects: object[] | object): Promise<UploadResponse>;
20
- prepareArtifact(filename: string, fileType: string): Promise<ArtifactsPrepareResponse | void>;
21
- private uploadToArtifact;
22
- streamToArtifact(preparedArtifact: ArtifactsPrepareResponse, fileStreamResponse: any): Promise<AxiosResponse | void>;
22
+ getArtifactUploadUrl(filename: string, fileType: string): Promise<ArtifactToUpload | void>;
23
+ uploadArtifact(artifact: ArtifactToUpload, file: Buffer): Promise<AxiosResponse | void>;
24
+ streamArtifact(artifact: ArtifactToUpload, fileStream: any): Promise<AxiosResponse | void>;
25
+ confirmArtifactUpload(artifactId: string): Promise<AxiosResponse | void>;
23
26
  getAttachmentsFromArtifactId({ artifact, }: {
24
27
  artifact: string;
25
28
  }): Promise<{
@@ -25,6 +25,17 @@ export interface ArtifactsPrepareResponse {
25
25
  value: string;
26
26
  }[];
27
27
  }
28
+ /**
29
+ * ArtifactToUpload is an interface that defines the structure of the response from the get upload url endpoint.
30
+ */
31
+ export interface ArtifactToUpload {
32
+ upload_url: string;
33
+ artifact_id: string;
34
+ form_data: {
35
+ key: string;
36
+ value: string;
37
+ }[];
38
+ }
28
39
  /**
29
40
  * UploadResponse is an interface that defines the structure of the response from upload through Uploader.
30
41
  */
@@ -49,7 +49,11 @@ class Uploader {
49
49
  this.event = event;
50
50
  this.devrevApiEndpoint = event.execution_metadata.devrev_endpoint;
51
51
  this.devrevApiToken = event.context.secrets.service_account_token;
52
+ this.requestId = event.payload.event_context.request_id;
52
53
  this.isLocalDevelopment = options === null || options === void 0 ? void 0 : options.isLocalDevelopment;
54
+ this.defaultHeaders = {
55
+ Authorization: `Bearer ${this.devrevApiToken}`,
56
+ };
53
57
  }
54
58
  /**
55
59
  * Uploads the fetched objects to the DevRev platform.
@@ -64,7 +68,7 @@ class Uploader {
64
68
  if (this.isLocalDevelopment) {
65
69
  await this.downloadToLocal(itemType, fetchedObjects);
66
70
  }
67
- // compress the fetched objects to a gzipped jsonl object
71
+ // Compress the fetched objects to a gzipped jsonl object
68
72
  const file = this.compressGzip(js_jsonl_1.jsonl.stringify(fetchedObjects));
69
73
  if (!file) {
70
74
  return {
@@ -73,58 +77,65 @@ class Uploader {
73
77
  }
74
78
  const filename = itemType + '.jsonl.gz';
75
79
  const fileType = 'application/x-gzip';
76
- // prepare the artifact for uploading
77
- const preparedArtifact = await this.prepareArtifact(filename, fileType);
80
+ // Get upload url
81
+ const preparedArtifact = await this.getArtifactUploadUrl(filename, fileType);
78
82
  if (!preparedArtifact) {
79
83
  return {
80
- error: { message: 'Error while preparing artifact.' },
84
+ error: { message: 'Error while getting artifact upload URL.' },
81
85
  };
82
86
  }
83
- // upload the file to the prepared artifact
84
- const uploadedArtifact = await this.uploadToArtifact(preparedArtifact, file);
85
- if (!uploadedArtifact) {
87
+ // Upload prepared artifact to the given url
88
+ const uploadItemResponse = await this.uploadArtifact(preparedArtifact, file);
89
+ if (!uploadItemResponse) {
86
90
  return {
87
91
  error: { message: 'Error while uploading artifact.' },
88
92
  };
89
93
  }
90
- // return the artifact information to the platform
94
+ // Confirm upload
95
+ const confirmArtifactUploadResponse = await this.confirmArtifactUpload(preparedArtifact.artifact_id);
96
+ if (!confirmArtifactUploadResponse) {
97
+ return {
98
+ error: { message: 'Error while confirming artifact upload.' },
99
+ };
100
+ }
101
+ // Return the artifact information to the platform
91
102
  const artifact = {
92
- id: preparedArtifact.id,
103
+ id: preparedArtifact.artifact_id,
93
104
  item_type: itemType,
94
105
  item_count: Array.isArray(fetchedObjects) ? fetchedObjects.length : 1,
95
106
  };
96
- console.log('Successful upload of artifact', artifact);
97
107
  return { artifact };
98
108
  }
99
- async prepareArtifact(filename, fileType) {
109
+ async getArtifactUploadUrl(filename, fileType) {
110
+ const url = `${this.devrevApiEndpoint}/internal/airdrop.artifacts.upload-url`;
100
111
  try {
101
- const response = await axios_client_1.axiosClient.post(`${this.devrevApiEndpoint}/artifacts.prepare`, {
102
- file_name: filename,
103
- file_type: fileType,
104
- }, {
105
- headers: {
106
- Authorization: `Bearer ${this.devrevApiToken}`,
112
+ const response = await axios_client_1.axiosClient.get(url, {
113
+ headers: Object.assign({}, this.defaultHeaders),
114
+ params: {
115
+ request_id: this.requestId,
116
+ file_type: fileType,
117
+ file_name: filename,
107
118
  },
108
119
  });
109
120
  return response.data;
110
121
  }
111
122
  catch (error) {
112
123
  if (axios_client_1.axios.isAxiosError(error)) {
113
- console.error('Error while preparing artifact.', (0, logger_1.serializeAxiosError)(error));
124
+ console.error('Error while getting artifact upload URL.', (0, logger_1.serializeAxiosError)(error));
114
125
  }
115
126
  else {
116
- console.error('Error while preparing artifact.', error);
127
+ console.error('Error while getting artifact upload URL.', error);
117
128
  }
118
129
  }
119
130
  }
120
- async uploadToArtifact(preparedArtifact, file) {
131
+ async uploadArtifact(artifact, file) {
121
132
  const formData = new form_data_1.default();
122
- for (const field of preparedArtifact.form_data) {
123
- formData.append(field.key, field.value);
133
+ for (const field in artifact.form_data) {
134
+ formData.append(field, artifact.form_data[field]);
124
135
  }
125
136
  formData.append('file', file);
126
137
  try {
127
- const response = await axios_client_1.axiosClient.post(preparedArtifact.url, formData, {
138
+ const response = await axios_client_1.axiosClient.post(artifact.upload_url, formData, {
128
139
  headers: Object.assign({}, formData.getHeaders()),
129
140
  });
130
141
  return response;
@@ -138,19 +149,19 @@ class Uploader {
138
149
  }
139
150
  }
140
151
  }
141
- async streamToArtifact(preparedArtifact, fileStreamResponse) {
152
+ async streamArtifact(artifact, fileStream) {
142
153
  const formData = new form_data_1.default();
143
- for (const field of preparedArtifact.form_data) {
144
- formData.append(field.key, field.value);
154
+ for (const field in artifact.form_data) {
155
+ formData.append(field, artifact.form_data[field]);
145
156
  }
146
- formData.append('file', fileStreamResponse.data);
147
- if (fileStreamResponse.headers['content-length'] > constants_1.MAX_DEVREV_ARTIFACT_SIZE) {
157
+ formData.append('file', fileStream.data);
158
+ if (fileStream.headers['content-length'] > constants_1.MAX_DEVREV_ARTIFACT_SIZE) {
148
159
  console.warn(`File size exceeds the maximum limit of ${constants_1.MAX_DEVREV_ARTIFACT_SIZE} bytes.`);
149
160
  return;
150
161
  }
151
162
  try {
152
- const response = await axios_client_1.axiosClient.post(preparedArtifact.url, formData, {
153
- headers: Object.assign(Object.assign({}, formData.getHeaders()), (!fileStreamResponse.headers['content-length']
163
+ const response = await axios_client_1.axiosClient.post(artifact.upload_url, formData, {
164
+ headers: Object.assign(Object.assign({}, formData.getHeaders()), (!fileStream.headers['content-length']
154
165
  ? {
155
166
  'Content-Length': constants_1.MAX_DEVREV_ARTIFACT_SIZE,
156
167
  }
@@ -168,6 +179,26 @@ class Uploader {
168
179
  return;
169
180
  }
170
181
  }
182
+ async confirmArtifactUpload(artifactId) {
183
+ const url = `${this.devrevApiEndpoint}/internal/airdrop.artifacts.confirm-upload`;
184
+ try {
185
+ const response = await axios_client_1.axiosClient.post(url, {
186
+ request_id: this.requestId,
187
+ artifact_id: artifactId,
188
+ }, {
189
+ headers: Object.assign({}, this.defaultHeaders),
190
+ });
191
+ return response;
192
+ }
193
+ catch (error) {
194
+ if (axios_client_1.axios.isAxiosError(error)) {
195
+ console.error('Error while confirming artifact upload.', (0, logger_1.serializeAxiosError)(error));
196
+ }
197
+ else {
198
+ console.error('Error while confirming artifact upload.', error);
199
+ }
200
+ }
201
+ }
171
202
  async getAttachmentsFromArtifactId({ artifact, }) {
172
203
  // Get the URL of the attachments metadata artifact
173
204
  const artifactUrl = await this.getArtifactDownloadUrl(artifact);
@@ -200,18 +231,24 @@ class Uploader {
200
231
  return { attachments: jsonObject };
201
232
  }
202
233
  async getArtifactDownloadUrl(artifactId) {
234
+ const url = `${this.devrevApiEndpoint}/internal/airdrop.artifacts.download-url`;
203
235
  try {
204
- const response = await axios_client_1.axiosClient.post(`${this.devrevApiEndpoint}/artifacts.locate`, {
205
- id: artifactId,
206
- }, {
207
- headers: {
208
- Authorization: `Bearer ${this.devrevApiToken}`,
236
+ const response = await axios_client_1.axiosClient.get(url, {
237
+ headers: Object.assign({}, this.defaultHeaders),
238
+ params: {
239
+ request_id: this.requestId,
240
+ artifact_id: artifactId,
209
241
  },
210
242
  });
211
- return response.data.url;
243
+ return response.data.download_url;
212
244
  }
213
245
  catch (error) {
214
- console.error('Error while getting artifact download URL.', error);
246
+ if (axios_client_1.axios.isAxiosError(error)) {
247
+ console.error('Error while getting artifact download URL.', (0, logger_1.serializeAxiosError)(error));
248
+ }
249
+ else {
250
+ console.error('Error while getting artifact download URL.', error);
251
+ }
215
252
  }
216
253
  }
217
254
  async downloadArtifact(artifactUrl) {
@@ -3,29 +3,89 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const test_helpers_1 = require("../tests/test-helpers");
4
4
  const types_1 = require("../types");
5
5
  const uploader_1 = require("./uploader");
6
- // mock uploader.upload method
7
- jest.mock('./uploader', () => {
8
- return {
9
- Uploader: jest.fn().mockImplementation(() => {
10
- return {
11
- upload: jest.fn().mockResolvedValue({
12
- artifact: { key: 'value' },
13
- error: undefined,
14
- }),
15
- };
16
- }),
17
- };
6
+ const axios_client_1 = require("../http/axios-client");
7
+ jest.mock('../http/axios-client', () => {
8
+ const originalModule = jest.requireActual('../http/axios-client');
9
+ return Object.assign(Object.assign({}, originalModule), { axiosClient: {
10
+ get: jest.fn(),
11
+ post: jest.fn(),
12
+ } });
18
13
  });
19
- describe('uploader.ts', () => {
14
+ const getSuccessResponse = () => ({
15
+ data: {
16
+ message: 'Success',
17
+ },
18
+ status: 200,
19
+ statusText: 'OK',
20
+ headers: {},
21
+ config: {},
22
+ });
23
+ const getArtifactUploadUrlMockResponse = {
24
+ data: {
25
+ artifact_id: 'mockArtifactId',
26
+ upload_url: 'mockUploadUrl',
27
+ form_data: [],
28
+ },
29
+ };
30
+ describe('Uploader Class Tests', () => {
20
31
  const mockEvent = (0, test_helpers_1.createEvent)({ eventType: types_1.EventType.ExtractionDataStart });
21
- const uploader = new uploader_1.Uploader({ event: mockEvent });
32
+ let uploader;
33
+ beforeEach(() => {
34
+ uploader = new uploader_1.Uploader({ event: mockEvent });
35
+ });
36
+ afterEach(() => {
37
+ jest.clearAllMocks();
38
+ });
22
39
  it('should upload the file to the DevRev platform and return the artifact information', async () => {
40
+ // Mock successful response from getArtifactUploadUrl
41
+ axios_client_1.axiosClient.get.mockResolvedValueOnce(getArtifactUploadUrlMockResponse);
42
+ // Mock successful response from confirmArtifactUpload and uploadArtifact
43
+ axios_client_1.axiosClient.post.mockResolvedValue(getSuccessResponse());
44
+ const entity = 'entity';
45
+ const fetchedObjects = [{ key: 'value' }];
46
+ const uploadResponse = await uploader.upload(entity, fetchedObjects);
47
+ expect(uploadResponse).toEqual({
48
+ artifact: {
49
+ id: 'mockArtifactId',
50
+ item_type: entity,
51
+ item_count: fetchedObjects.length,
52
+ },
53
+ });
54
+ });
55
+ it('should handle failure in getArtifactUploadUrl', async () => {
56
+ // Mock unsuccessful response for getArtifactUploadUrl
57
+ axios_client_1.axiosClient.get.mockResolvedValueOnce(undefined);
58
+ const entity = 'entity';
59
+ const fetchedObjects = [{ key: 'value' }];
60
+ const uploadResponse = await uploader.upload(entity, fetchedObjects);
61
+ expect(uploadResponse).toEqual({
62
+ error: { message: 'Error while getting artifact upload URL.' },
63
+ });
64
+ });
65
+ it('should handle failure in uploadArtifact', async () => {
66
+ // Mock successful response for getArtifactUploadUrl
67
+ axios_client_1.axiosClient.get.mockResolvedValueOnce(getArtifactUploadUrlMockResponse);
68
+ // Mock unsuccessful response for uploadArtifact
69
+ axios_client_1.axiosClient.post.mockResolvedValueOnce(undefined);
70
+ const entity = 'entity';
71
+ const fetchedObjects = [{ key: 'value' }];
72
+ const uploadResponse = await uploader.upload(entity, fetchedObjects);
73
+ expect(uploadResponse).toEqual({
74
+ error: { message: 'Error while uploading artifact.' },
75
+ });
76
+ });
77
+ it('should handle failure in confirmArtifactUpload', async () => {
78
+ // Mock successful response for getArtifactUploadUrl
79
+ axios_client_1.axiosClient.get.mockResolvedValueOnce(getArtifactUploadUrlMockResponse);
80
+ // Mock successful response from uploadArtifact
81
+ axios_client_1.axiosClient.post.mockResolvedValueOnce(getSuccessResponse());
82
+ // Mock unsuccessful response from confirmArtifactUpload
83
+ axios_client_1.axiosClient.post.mockResolvedValueOnce(undefined);
23
84
  const entity = 'entity';
24
85
  const fetchedObjects = [{ key: 'value' }];
25
86
  const uploadResponse = await uploader.upload(entity, fetchedObjects);
26
87
  expect(uploadResponse).toEqual({
27
- artifact: { key: 'value' },
28
- error: undefined,
88
+ error: { message: 'Error while confirming artifact upload.' },
29
89
  });
30
90
  });
31
91
  });
@@ -617,19 +617,27 @@ class WorkerAdapter {
617
617
  }
618
618
  if (httpStream) {
619
619
  const fileType = ((_a = httpStream.headers) === null || _a === void 0 ? void 0 : _a['content-type']) || 'application/octet-stream';
620
- const preparedArtifact = await this.uploader.prepareArtifact(attachment.file_name, fileType);
620
+ // Get upload URL
621
+ const preparedArtifact = await this.uploader.getArtifactUploadUrl(attachment.file_name, fileType);
621
622
  if (!preparedArtifact) {
622
623
  console.warn(`Error while preparing artifact for attachment ID ${attachment.id}. Skipping attachment.`);
623
624
  return;
624
625
  }
625
- const uploadedArtifact = await this.uploader.streamToArtifact(preparedArtifact, httpStream);
626
+ // Stream attachment
627
+ const uploadedArtifact = await this.uploader.streamArtifact(preparedArtifact, httpStream);
626
628
  if (!uploadedArtifact) {
627
629
  console.warn(`Error while streaming to artifact for attachment ID ${attachment.id}. Skipping attachment.`);
628
630
  return;
629
631
  }
632
+ // Confirm attachment upload
633
+ const confirmArtifactUploadResponse = await this.uploader.confirmArtifactUpload(preparedArtifact.artifact_id);
634
+ if (!confirmArtifactUploadResponse) {
635
+ console.warn('Error while confirming upload for attachment ID ' + attachment.id);
636
+ return;
637
+ }
630
638
  const ssorAttachment = {
631
639
  id: {
632
- devrev: preparedArtifact.id,
640
+ devrev: preparedArtifact.artifact_id,
633
641
  external: attachment.id,
634
642
  },
635
643
  parent_id: {
@@ -688,9 +696,12 @@ class WorkerAdapter {
688
696
  }) {
689
697
  var _a, _b;
690
698
  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 };
699
+ console.warn(`The specified batch size (${batchSize}) is invalid. Using 1 instead.`);
700
+ batchSize = 1;
701
+ }
702
+ if (batchSize > 50) {
703
+ console.warn(`The specified batch size (${batchSize}) is too large. Using 50 instead.`);
704
+ batchSize = 50;
694
705
  }
695
706
  const repos = [
696
707
  {
@@ -142,44 +142,6 @@ describe('WorkerAdapter', () => {
142
142
  expect(result[0]).toHaveLength(1);
143
143
  expect(result[1]).toHaveLength(1);
144
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
145
  });
184
146
  describe('defaultAttachmentsIterator', () => {
185
147
  it('should process all batches of attachments', async () => {
@@ -400,23 +362,84 @@ describe('WorkerAdapter', () => {
400
362
  expect(result).toBeUndefined();
401
363
  });
402
364
  it('should handle invalid batch size', async () => {
403
- var _a;
404
365
  // Arrange
405
366
  const mockStream = jest.fn();
406
- const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
367
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
368
+ // Set up adapter state with artifact IDs
369
+ adapter.state.toDevRev = {
370
+ attachmentsMetadata: {
371
+ artifactIds: ['artifact1'],
372
+ lastProcessed: 0,
373
+ lastProcessedAttachmentsIdsList: [],
374
+ },
375
+ };
376
+ // Mock getting attachments
377
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
378
+ attachments: [
379
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
380
+ ],
381
+ });
382
+ // Mock the required methods
383
+ adapter.initializeRepos = jest.fn();
384
+ const mockReducedAttachments = [['batch1']];
385
+ adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue(mockReducedAttachments);
386
+ adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({});
407
387
  // Act
408
388
  const result = await adapter.streamAttachments({
409
389
  stream: mockStream,
410
390
  batchSize: 0,
411
391
  });
412
392
  // Assert
413
- expect(consoleErrorSpy).toHaveBeenCalled();
414
- expect(result).toEqual({
415
- error: expect.any(Error),
393
+ expect(consoleWarnSpy).toHaveBeenCalledWith('The specified batch size (0) is invalid. Using 1 instead.');
394
+ // Verify that the reducer was called with batchSize 50 (not 100)
395
+ expect(adapter['defaultAttachmentsReducer']).toHaveBeenCalledWith({
396
+ attachments: expect.any(Array),
397
+ adapter: adapter,
398
+ batchSize: 1,
416
399
  });
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();
400
+ expect(result).toBeUndefined();
401
+ // Restore console.warn
402
+ consoleWarnSpy.mockRestore();
403
+ });
404
+ it('should cap batch size to 50 when batchSize is greater than 50', async () => {
405
+ // Arrange
406
+ const mockStream = jest.fn();
407
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
408
+ // Set up adapter state with artifact IDs
409
+ adapter.state.toDevRev = {
410
+ attachmentsMetadata: {
411
+ artifactIds: ['artifact1'],
412
+ lastProcessed: 0,
413
+ lastProcessedAttachmentsIdsList: [],
414
+ },
415
+ };
416
+ // Mock getting attachments
417
+ adapter['uploader'].getAttachmentsFromArtifactId = jest.fn().mockResolvedValue({
418
+ attachments: [
419
+ { url: 'http://example.com/file1.pdf', id: 'attachment1', file_name: 'file1.pdf', parent_id: 'parent1' },
420
+ ],
421
+ });
422
+ // Mock the required methods
423
+ adapter.initializeRepos = jest.fn();
424
+ const mockReducedAttachments = [['batch1']];
425
+ adapter['defaultAttachmentsReducer'] = jest.fn().mockReturnValue(mockReducedAttachments);
426
+ adapter['defaultAttachmentsIterator'] = jest.fn().mockResolvedValue({});
427
+ // Act
428
+ const result = await adapter.streamAttachments({
429
+ stream: mockStream,
430
+ batchSize: 100, // Set batch size greater than 50
431
+ });
432
+ // Assert
433
+ expect(consoleWarnSpy).toHaveBeenCalledWith('The specified batch size (100) is too large. Using 50 instead.');
434
+ // Verify that the reducer was called with batchSize 50 (not 100)
435
+ expect(adapter['defaultAttachmentsReducer']).toHaveBeenCalledWith({
436
+ attachments: expect.any(Array),
437
+ adapter: adapter,
438
+ batchSize: 50, // Should be capped at 50
439
+ });
440
+ expect(result).toBeUndefined();
441
+ // Restore console.warn
442
+ consoleWarnSpy.mockRestore();
420
443
  });
421
444
  it('should handle empty attachments metadata artifact IDs', async () => {
422
445
  // Arrange
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devrev/ts-adaas",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "Typescript library containing the ADaaS(AirDrop as a Service) control protocol.",
5
5
  "type": "commonjs",
6
6
  "main": "./dist/index.js",