@devrev/ts-adaas 1.19.7 → 1.19.9-beta.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.
Files changed (49) hide show
  1. package/dist/common/control-protocol.d.ts +3 -2
  2. package/dist/common/control-protocol.d.ts.map +1 -1
  3. package/dist/common/control-protocol.js +2 -4
  4. package/dist/common/control-protocol.test.d.ts +2 -0
  5. package/dist/common/control-protocol.test.d.ts.map +1 -0
  6. package/dist/common/control-protocol.test.js +126 -0
  7. package/dist/common/errors.d.ts +4 -1
  8. package/dist/common/errors.d.ts.map +1 -1
  9. package/dist/http/axios-client-internal.d.ts.map +1 -1
  10. package/dist/http/axios-client-internal.js +19 -1
  11. package/dist/mock-server/mock-server.d.ts +4 -0
  12. package/dist/mock-server/mock-server.d.ts.map +1 -1
  13. package/dist/mock-server/mock-server.interfaces.d.ts +9 -0
  14. package/dist/mock-server/mock-server.interfaces.d.ts.map +1 -1
  15. package/dist/mock-server/mock-server.js +28 -6
  16. package/dist/multithreading/spawn/spawn.d.ts +1 -1
  17. package/dist/multithreading/spawn/spawn.d.ts.map +1 -1
  18. package/dist/multithreading/spawn/spawn.js +11 -6
  19. package/dist/multithreading/spawn/spawn.test.js +1 -0
  20. package/dist/multithreading/worker-adapter/worker-adapter.artifacts.test.js +1 -1
  21. package/dist/multithreading/worker-adapter/worker-adapter.d.ts +10 -0
  22. package/dist/multithreading/worker-adapter/worker-adapter.d.ts.map +1 -1
  23. package/dist/multithreading/worker-adapter/worker-adapter.emit.test.js +90 -9
  24. package/dist/multithreading/worker-adapter/worker-adapter.extraction.test.js +1 -1
  25. package/dist/multithreading/worker-adapter/worker-adapter.js +91 -46
  26. package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.d.ts +2 -0
  27. package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.d.ts.map +1 -0
  28. package/dist/multithreading/worker-adapter/worker-adapter.upload-failure.test.js +135 -0
  29. package/dist/repo/repo.d.ts +11 -2
  30. package/dist/repo/repo.d.ts.map +1 -1
  31. package/dist/repo/repo.interfaces.d.ts +2 -0
  32. package/dist/repo/repo.interfaces.d.ts.map +1 -1
  33. package/dist/repo/repo.js +37 -11
  34. package/dist/repo/repo.test.js +184 -0
  35. package/dist/types/extraction.d.ts +8 -1
  36. package/dist/types/extraction.d.ts.map +1 -1
  37. package/dist/types/workers.d.ts +4 -0
  38. package/dist/types/workers.d.ts.map +1 -1
  39. package/dist/uploader/uploader.d.ts.map +1 -1
  40. package/dist/uploader/uploader.helpers.d.ts +6 -1
  41. package/dist/uploader/uploader.helpers.d.ts.map +1 -1
  42. package/dist/uploader/uploader.helpers.js +65 -0
  43. package/dist/uploader/uploader.helpers.test.js +148 -7
  44. package/dist/uploader/uploader.interfaces.d.ts +11 -0
  45. package/dist/uploader/uploader.interfaces.d.ts.map +1 -1
  46. package/dist/uploader/uploader.interfaces.js +11 -0
  47. package/dist/uploader/uploader.js +2 -5
  48. package/dist/uploader/uploader.test.js +54 -3
  49. package/package.json +1 -1
@@ -22,6 +22,13 @@ const workers_1 = require("../../types/workers");
22
22
  const uploader_1 = require("../../uploader/uploader");
23
23
  const event_type_translation_1 = require("../../common/event-type-translation");
24
24
  const helpers_1 = require("../../common/helpers");
25
+ const spawn_helpers_1 = require("../spawn/spawn.helpers");
26
+ function toRfc3339Timestamp(ms) {
27
+ if (!Number.isFinite(ms) || ms === 0) {
28
+ return undefined;
29
+ }
30
+ return new Date(ms).toISOString();
31
+ }
25
32
  function createWorkerAdapter({ event, adapterState, options, }) {
26
33
  return new WorkerAdapter({
27
34
  event,
@@ -102,6 +109,7 @@ class WorkerAdapter {
102
109
  repo.itemType !== constants_1.SSOR_ATTACHMENT;
103
110
  return new repo_1.Repo(Object.assign(Object.assign({ event: this.event, itemType: repo.itemType }, (shouldNormalize && { normalize: repo.normalize })), { onUpload: (artifact) => {
104
111
  var _a;
112
+ this.lastExtractedItemType = repo.itemType;
105
113
  // We need to store artifacts ids in state for later use when streaming attachments
106
114
  if (repo.itemType === constants_1.AirSyncDefaultItemTypes.ATTACHMENTS) {
107
115
  (_a = this.state.toDevRev) === null || _a === void 0 ? void 0 : _a.attachmentsMetadata.artifactIds.push(artifact.id);
@@ -146,7 +154,7 @@ class WorkerAdapter {
146
154
  */
147
155
  async emit(newEventType, data) {
148
156
  return (0, logger_context_1.runWithSdkLogContext)(async () => {
149
- var _a, _b;
157
+ var _a;
150
158
  newEventType = (0, event_type_translation_1.translateOutgoingEventType)(newEventType);
151
159
  if (this.hasWorkerEmitted) {
152
160
  console.warn(`Trying to emit event with event type: ${newEventType}. Ignoring emit request because it has already been emitted.`);
@@ -178,8 +186,13 @@ class WorkerAdapter {
178
186
  }
179
187
  catch (error) {
180
188
  console.error('Error while uploading repos', error);
181
- node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(workers_1.WorkerMessageSubject.WorkerMessageExit);
182
- this.hasWorkerEmitted = true;
189
+ for (const repo of this.repos) {
190
+ this.artifacts.push(...repo.uploadedArtifacts);
191
+ }
192
+ const { eventType } = (0, spawn_helpers_1.getTimeoutErrorEventType)(this.event.payload.event_type);
193
+ await this.emitToPlatform(eventType, {
194
+ error: { message: (0, logger_1.serializeError)(error) },
195
+ });
183
196
  return;
184
197
  }
185
198
  // If the extraction is done, we want to save the timestamp of the last successful sync
@@ -207,55 +220,87 @@ class WorkerAdapter {
207
220
  this.state.workersNewest = extractionEnd;
208
221
  }
209
222
  }
210
- // We want to save the state every time we emit an event, except for the start and delete events
211
- if (!constants_1.STATELESS_EVENT_TYPES.includes(this.event.payload.event_type)) {
212
- console.log(`Saving state before emitting event with event type: ${newEventType}.`);
213
- try {
214
- await this.adapterState.postState(this.state);
215
- }
216
- catch (error) {
217
- console.error('Error while posting state', error);
218
- node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(workers_1.WorkerMessageSubject.WorkerMessageExit);
219
- this.hasWorkerEmitted = true;
220
- return;
221
- }
222
- }
223
- try {
224
- // Always prune error messages to make them shorter before emit
225
- if ((_b = data === null || data === void 0 ? void 0 : data.error) === null || _b === void 0 ? void 0 : _b.message) {
226
- data.error.message = (0, helpers_1.truncateMessage)(data.error.message);
227
- }
228
- const isExtractionEvent = Object.values(extraction_1.ExtractorEventType).includes(newEventType);
229
- const isLoaderEvent = Object.values(loading_1.LoaderEventType).includes(newEventType);
230
- await (0, control_protocol_1.emit)({
231
- eventType: newEventType,
232
- event: this.event,
233
- data: Object.assign(Object.assign(Object.assign({}, data), (isExtractionEvent ? { artifacts: this.artifacts } : {})), (isLoaderEvent
234
- ? { reports: this.reports, processed_files: this.processedFiles }
235
- : {})),
236
- });
237
- const message = {
238
- subject: workers_1.WorkerMessageSubject.WorkerMessageEmitted,
239
- payload: { eventType: newEventType },
240
- };
241
- this.artifacts = [];
242
- node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(message);
243
- this.hasWorkerEmitted = true;
244
- }
245
- catch (error) {
246
- console.error(`Error while emitting event with event type: ${newEventType}.`, (0, logger_1.serializeError)(error));
247
- node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(workers_1.WorkerMessageSubject.WorkerMessageExit);
248
- this.hasWorkerEmitted = true;
249
- }
223
+ await this.emitToPlatform(newEventType, data);
250
224
  });
251
225
  }
252
226
  async uploadAllRepos() {
253
227
  for (const repo of this.repos) {
254
- const error = await repo.upload();
228
+ await repo.upload();
255
229
  this.artifacts.push(...repo.uploadedArtifacts);
256
- if (error) {
257
- throw error;
230
+ }
231
+ }
232
+ /**
233
+ * Persists adapter state before emitting, except for stateless worker phases.
234
+ */
235
+ async postStateBeforeEmit(eventType) {
236
+ if (constants_1.STATELESS_EVENT_TYPES.includes(this.event.payload.event_type)) {
237
+ return true;
238
+ }
239
+ console.log(`Saving state before emitting event with event type: ${eventType}.`);
240
+ try {
241
+ await this.adapterState.postState(this.state);
242
+ return true;
243
+ }
244
+ catch (error) {
245
+ console.error('Error while posting state', error);
246
+ node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(workers_1.WorkerMessageSubject.WorkerMessageExit);
247
+ this.hasWorkerEmitted = true;
248
+ return false;
249
+ }
250
+ }
251
+ /**
252
+ * Posts state (when required), emits to the platform, and notifies the parent
253
+ * worker that an event was emitted.
254
+ */
255
+ async emitToPlatform(eventType, data) {
256
+ var _a;
257
+ const posted = await this.postStateBeforeEmit(eventType);
258
+ if (!posted) {
259
+ return;
260
+ }
261
+ try {
262
+ if ((_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message) {
263
+ data.error.message = (0, helpers_1.truncateMessage)(data.error.message);
264
+ }
265
+ const isExtractionEvent = Object.values(extraction_1.ExtractorEventType).includes(eventType);
266
+ const isLoaderEvent = Object.values(loading_1.LoaderEventType).includes(eventType);
267
+ const progress_data = {};
268
+ if (isExtractionEvent &&
269
+ (eventType == extraction_1.ExtractorEventType.DataExtractionDone ||
270
+ eventType == extraction_1.ExtractorEventType.DataExtractionProgress ||
271
+ eventType == extraction_1.ExtractorEventType.AttachmentExtractionDone ||
272
+ eventType == extraction_1.ExtractorEventType.AttachmentExtractionProgress)) {
273
+ const repo = this.lastExtractedItemType
274
+ ? this.repos.find((r) => r.itemType === this.lastExtractedItemType)
275
+ : undefined;
276
+ if (repo) {
277
+ progress_data.item_type = repo.itemType;
278
+ progress_data.newest_created_date = toRfc3339Timestamp(repo.dateRanges.creationDate.newest);
279
+ progress_data.oldest_created_date = toRfc3339Timestamp(repo.dateRanges.creationDate.oldest);
280
+ progress_data.newest_modified_date = toRfc3339Timestamp(repo.dateRanges.modifiedDate.newest);
281
+ progress_data.oldest_modified_date = toRfc3339Timestamp(repo.dateRanges.modifiedDate.oldest);
282
+ }
258
283
  }
284
+ await (0, control_protocol_1.emit)({
285
+ eventType,
286
+ event: this.event,
287
+ data: Object.assign(Object.assign(Object.assign({}, data), (isExtractionEvent ? { artifacts: this.artifacts } : {})), (isLoaderEvent
288
+ ? { reports: this.reports, processed_files: this.processedFiles }
289
+ : {})),
290
+ worker_metadata: Object.assign({}, progress_data),
291
+ });
292
+ const message = {
293
+ subject: workers_1.WorkerMessageSubject.WorkerMessageEmitted,
294
+ payload: { eventType },
295
+ };
296
+ this.artifacts = [];
297
+ node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(message);
298
+ this.hasWorkerEmitted = true;
299
+ }
300
+ catch (error) {
301
+ console.error(`Error while emitting event with event type: ${eventType}.`, (0, logger_1.serializeError)(error));
302
+ node_worker_threads_1.parentPort === null || node_worker_threads_1.parentPort === void 0 ? void 0 : node_worker_threads_1.parentPort.postMessage(workers_1.WorkerMessageSubject.WorkerMessageExit);
303
+ this.hasWorkerEmitted = true;
259
304
  }
260
305
  }
261
306
  async loadItemTypes({ itemTypesToLoad, }) {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-adapter.upload-failure.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-adapter.upload-failure.test.d.ts","sourceRoot":"","sources":["../../../src/multithreading/worker-adapter/worker-adapter.upload-failure.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const state_1 = require("../../state/state");
4
+ const jest_setup_1 = require("../../tests/jest.setup");
5
+ const test_helpers_1 = require("../../tests/test-helpers");
6
+ const test_utils_1 = require("../../common/test-utils");
7
+ const types_1 = require("../../types");
8
+ const worker_adapter_1 = require("./worker-adapter");
9
+ /* eslint-disable @typescript-eslint/no-require-imports */
10
+ jest.mock('../../common/control-protocol', () => ({
11
+ emit: jest.fn().mockResolvedValue({}),
12
+ }));
13
+ jest.mock('../../mappers/mappers');
14
+ jest.mock('node:worker_threads', () => ({
15
+ parentPort: { postMessage: jest.fn() },
16
+ }));
17
+ jest.mock('../../attachments-streaming/attachments-streaming-pool', () => ({
18
+ AttachmentsStreamingPool: jest.fn().mockImplementation(() => ({
19
+ streamAll: jest.fn().mockResolvedValue(undefined),
20
+ })),
21
+ }));
22
+ const UPLOAD_URL_PATH = '/internal/airdrop.artifacts.upload-url';
23
+ function makeAdapter() {
24
+ const event = (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
25
+ payload: { event_type: types_1.EventType.StartExtractingData },
26
+ });
27
+ const initialState = {
28
+ attachments: { completed: false },
29
+ lastSyncStarted: '',
30
+ lastSuccessfulSyncStarted: '',
31
+ snapInVersionId: '',
32
+ toDevRev: {
33
+ attachmentsMetadata: {
34
+ artifactIds: [],
35
+ lastProcessed: 0,
36
+ lastProcessedAttachmentsIdsList: [],
37
+ },
38
+ },
39
+ };
40
+ const adapterState = new state_1.State({ event, initialState });
41
+ const adapter = new worker_adapter_1.WorkerAdapter({ event, adapterState });
42
+ const workerThreads = require('node:worker_threads');
43
+ const mockPostMessage = jest.fn();
44
+ if (workerThreads.parentPort) {
45
+ jest
46
+ .spyOn(workerThreads.parentPort, 'postMessage')
47
+ .mockImplementation(mockPostMessage);
48
+ }
49
+ else {
50
+ workerThreads.parentPort = { postMessage: mockPostMessage };
51
+ }
52
+ return { adapter, mockPostMessage };
53
+ }
54
+ describe(`${worker_adapter_1.WorkerAdapter.name} upload failure (near-integration)`, () => {
55
+ let adapter;
56
+ let mockPostMessage;
57
+ beforeEach(() => {
58
+ jest.clearAllMocks();
59
+ jest_setup_1.mockServer.resetRoutes();
60
+ ({ adapter, mockPostMessage } = makeAdapter());
61
+ adapter['adapterState'].postState = jest.fn().mockResolvedValue(undefined);
62
+ });
63
+ afterEach(() => {
64
+ jest.restoreAllMocks();
65
+ });
66
+ it('should throw from repo.push when upload-url returns 400 during batch upload', async () => {
67
+ var _a, _b;
68
+ jest_setup_1.mockServer.setRoute({
69
+ path: UPLOAD_URL_PATH,
70
+ method: 'GET',
71
+ status: 400,
72
+ });
73
+ adapter.initializeRepos([
74
+ {
75
+ itemType: 'tasks',
76
+ overridenOptions: { batchSize: 10 },
77
+ },
78
+ ]);
79
+ await expect((_a = adapter.getRepo('tasks')) === null || _a === void 0 ? void 0 : _a.push((0, test_helpers_1.createItems)(20))).rejects.toThrow('artifact upload URL');
80
+ expect((_b = adapter.getRepo('tasks')) === null || _b === void 0 ? void 0 : _b.getItems().length).toBe(20);
81
+ });
82
+ it('should emit DataExtractionError via emitToPlatform when uploadAllRepos fails', async () => {
83
+ var _a;
84
+ const { emit: mockEmit } = require('../../common/control-protocol');
85
+ jest_setup_1.mockServer.setRoute({
86
+ path: UPLOAD_URL_PATH,
87
+ method: 'GET',
88
+ status: 400,
89
+ });
90
+ adapter.initializeRepos([{ itemType: 'tasks' }]);
91
+ await ((_a = adapter.getRepo('tasks')) === null || _a === void 0 ? void 0 : _a.push((0, test_helpers_1.createItems)(5)));
92
+ await adapter.emit(types_1.ExtractorEventType.DataExtractionDone);
93
+ expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
94
+ eventType: types_1.ExtractorEventType.DataExtractionError,
95
+ data: expect.objectContaining({
96
+ error: expect.objectContaining({
97
+ message: expect.stringContaining('artifact upload URL'),
98
+ }),
99
+ }),
100
+ }));
101
+ expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({
102
+ subject: 'emit',
103
+ payload: { eventType: types_1.ExtractorEventType.DataExtractionError },
104
+ }));
105
+ });
106
+ it('should include partial artifacts when push batch succeeded before uploadAllRepos fails', async () => {
107
+ var _a;
108
+ const { emit: mockEmit } = require('../../common/control-protocol');
109
+ jest_setup_1.mockServer.setRoute({
110
+ path: UPLOAD_URL_PATH,
111
+ method: 'GET',
112
+ status: 200,
113
+ succeedThenFail: {
114
+ successCount: 1,
115
+ errorStatus: 400,
116
+ },
117
+ });
118
+ adapter.initializeRepos([
119
+ {
120
+ itemType: 'tasks',
121
+ overridenOptions: { batchSize: 10 },
122
+ },
123
+ ]);
124
+ await ((_a = adapter.getRepo('tasks')) === null || _a === void 0 ? void 0 : _a.push((0, test_helpers_1.createItems)(15)));
125
+ await adapter.emit(types_1.ExtractorEventType.DataExtractionDone);
126
+ expect(mockEmit).toHaveBeenCalledWith(expect.objectContaining({
127
+ eventType: types_1.ExtractorEventType.DataExtractionError,
128
+ data: expect.objectContaining({
129
+ artifacts: expect.arrayContaining([
130
+ expect.objectContaining({ item_count: 10, item_type: 'tasks' }),
131
+ ]),
132
+ }),
133
+ }));
134
+ });
135
+ });
@@ -1,5 +1,4 @@
1
1
  import { Item } from '../repo/repo.interfaces';
2
- import { ErrorRecord } from '../types/common';
3
2
  import { Artifact } from '../uploader/uploader.interfaces';
4
3
  import { NormalizedAttachment, NormalizedItem, RepoFactoryInterface } from './repo.interfaces';
5
4
  export declare class Repo {
@@ -10,9 +9,19 @@ export declare class Repo {
10
9
  private onUpload;
11
10
  private options?;
12
11
  uploadedArtifacts: Artifact[];
12
+ dateRanges: {
13
+ creationDate: {
14
+ oldest: number;
15
+ newest: number;
16
+ };
17
+ modifiedDate: {
18
+ oldest: number;
19
+ newest: number;
20
+ };
21
+ };
13
22
  constructor({ event, itemType, normalize, onUpload, options, }: RepoFactoryInterface);
14
23
  getItems(): (NormalizedItem | NormalizedAttachment | Item)[];
15
- upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void | ErrorRecord>;
24
+ upload(batch?: (NormalizedItem | NormalizedAttachment | Item)[]): Promise<void>;
16
25
  push(items: Item[]): Promise<boolean>;
17
26
  }
18
27
  //# sourceMappingURL=repo.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/repo/repo.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAE9C,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAI3D,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAE3B,qBAAa,IAAI;IACf,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAmD;IAChE,OAAO,CAAC,SAAS,CAAC,CAAwD;IAC1E,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,OAAO,CAAC,CAAuB;IAChC,iBAAiB,EAAE,QAAQ,EAAE,CAAC;gBAEzB,EACV,KAAK,EACL,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,OAAO,GACR,EAAE,oBAAoB;IAUvB,QAAQ,IAAI,CAAC,cAAc,GAAG,oBAAoB,GAAG,IAAI,CAAC,EAAE;IAItD,MAAM,CACV,KAAK,CAAC,EAAE,CAAC,cAAc,GAAG,oBAAoB,GAAG,IAAI,CAAC,EAAE,GACvD,OAAO,CAAC,IAAI,GAAG,WAAW,CAAC;IAqCxB,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAyC5C"}
1
+ {"version":3,"file":"repo.d.ts","sourceRoot":"","sources":["../../src/repo/repo.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAI3D,OAAO,EACL,oBAAoB,EACpB,cAAc,EACd,oBAAoB,EACrB,MAAM,mBAAmB,CAAC;AAmB3B,qBAAa,IAAI;IACf,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,KAAK,CAAmD;IAChE,OAAO,CAAC,SAAS,CAAC,CAAwD;IAC1E,OAAO,CAAC,QAAQ,CAAW;IAC3B,OAAO,CAAC,QAAQ,CAA+B;IAC/C,OAAO,CAAC,OAAO,CAAC,CAAuB;IAChC,iBAAiB,EAAE,QAAQ,EAAE,CAAC;IAC9B,UAAU;;;;;;;;;MAGf;gBAEU,EACV,KAAK,EACL,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,OAAO,GACR,EAAE,oBAAoB;IAUvB,QAAQ,IAAI,CAAC,cAAc,GAAG,oBAAoB,GAAG,IAAI,CAAC,EAAE;IAItD,MAAM,CACV,KAAK,CAAC,EAAE,CAAC,cAAc,GAAG,oBAAoB,GAAG,IAAI,CAAC,EAAE,GACvD,OAAO,CAAC,IAAI,CAAC;IAyDV,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;CAkC5C"}
@@ -39,6 +39,8 @@ export interface NormalizedAttachment {
39
39
  author_id?: string;
40
40
  inline?: boolean;
41
41
  content_type?: string;
42
+ created_date?: string;
43
+ modified_date?: string;
42
44
  grand_parent_id?: number | string;
43
45
  }
44
46
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"repo.interfaces.d.ts","sourceRoot":"","sources":["../../src/repo/repo.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,oBAAoB,CAAC;IACtE,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,oBAAoB,CAAC;IACtE,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAItB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC;AAED;;GAEG;AAEH,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC"}
1
+ {"version":3,"file":"repo.interfaces.d.ts","sourceRoot":"","sources":["../../src/repo/repo.interfaces.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,oBAAoB,CAAC;IACtE,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,YAAY,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,cAAc,GAAG,oBAAoB,CAAC;IACtE,QAAQ,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IACvC,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IAIvB,eAAe,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACnC;AAED;;GAEG;AAEH,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC"}
package/dist/repo/repo.js CHANGED
@@ -4,8 +4,24 @@ exports.Repo = void 0;
4
4
  const constants_1 = require("../common/constants");
5
5
  const uploader_1 = require("../uploader/uploader");
6
6
  const logger_context_1 = require("../logger/logger.context");
7
+ function updateRange(range, ms) {
8
+ if (range.oldest === 0 || ms < range.oldest) {
9
+ range.oldest = ms;
10
+ }
11
+ if (range.newest === 0 || ms > range.newest) {
12
+ range.newest = ms;
13
+ }
14
+ }
15
+ function toValidTimestamp(value) {
16
+ const ms = new Date(value).getTime();
17
+ return Number.isFinite(ms) ? ms : undefined;
18
+ }
7
19
  class Repo {
8
20
  constructor({ event, itemType, normalize, onUpload, options, }) {
21
+ this.dateRanges = {
22
+ creationDate: { oldest: 0, newest: 0 },
23
+ modifiedDate: { oldest: 0, newest: 0 },
24
+ };
9
25
  this.items = [];
10
26
  this.itemType = itemType;
11
27
  this.normalize = normalize;
@@ -18,13 +34,30 @@ class Repo {
18
34
  return this.items;
19
35
  }
20
36
  async upload(batch) {
37
+ var _a;
21
38
  const itemsToUpload = batch || this.items;
22
39
  if (itemsToUpload.length > 0) {
40
+ for (const item of itemsToUpload) {
41
+ const createdDate = item === null || item === void 0 ? void 0 : item.created_date;
42
+ if (createdDate != null) {
43
+ const createdMs = toValidTimestamp(createdDate);
44
+ if (createdMs !== undefined) {
45
+ updateRange(this.dateRanges.creationDate, createdMs);
46
+ }
47
+ }
48
+ const modifiedDate = item === null || item === void 0 ? void 0 : item.modified_date;
49
+ if (modifiedDate != null && modifiedDate !== '') {
50
+ const modifiedMs = toValidTimestamp(modifiedDate);
51
+ if (modifiedMs !== undefined) {
52
+ updateRange(this.dateRanges.modifiedDate, modifiedMs);
53
+ }
54
+ }
55
+ }
23
56
  console.log(`Uploading ${itemsToUpload.length} items of type ${this.itemType}. `);
24
57
  const { artifact, error } = await this.uploader.upload(this.itemType, itemsToUpload);
25
58
  if (error || !artifact) {
26
59
  console.error('Error while uploading batch', error);
27
- return error;
60
+ throw new Error((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : `Upload failed for item type "${this.itemType}" without artifact.`);
28
61
  }
29
62
  this.onUpload(artifact);
30
63
  this.uploadedArtifacts.push(artifact);
@@ -59,16 +92,9 @@ class Repo {
59
92
  // Upload in batches while the number of items exceeds the batch size
60
93
  const batchSize = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.batchSize) || constants_1.ARTIFACT_BATCH_SIZE;
61
94
  while (this.items.length >= batchSize) {
62
- // Slice out a batch of batchSize items to upload
63
- const batch = this.items.splice(0, batchSize);
64
- try {
65
- // Upload the batch
66
- await this.upload(batch);
67
- }
68
- catch (error) {
69
- console.error('Error while uploading batch', error);
70
- return false;
71
- }
95
+ const batch = this.items.slice(0, batchSize);
96
+ await this.upload(batch);
97
+ this.items.splice(0, batchSize);
72
98
  }
73
99
  return true;
74
100
  }
@@ -7,6 +7,22 @@ const test_utils_1 = require("../common/test-utils");
7
7
  const types_1 = require("../types");
8
8
  const repo_1 = require("./repo");
9
9
  jest.mock('../tests/test-helpers', () => (Object.assign(Object.assign({}, jest.requireActual('../tests/test-helpers')), { normalizeItem: jest.fn() })));
10
+ const mockUploadFn = jest.fn().mockResolvedValue({
11
+ error: null,
12
+ artifact: { id: 'art-1', item_type: 'test', item_count: 0 },
13
+ });
14
+ jest.mock('../uploader/uploader', () => ({
15
+ Uploader: jest.fn().mockImplementation(() => ({
16
+ upload: mockUploadFn,
17
+ })),
18
+ }));
19
+ function itemWithDate(id, created_date) {
20
+ return { id, created_date, modified_date: created_date, data: {} };
21
+ }
22
+ function itemWithDates(id, created_date, modified_date) {
23
+ return { id, created_date, modified_date, data: {} };
24
+ }
25
+ const ts = (iso) => new Date(iso).getTime();
10
26
  describe(repo_1.Repo.name, () => {
11
27
  let repo;
12
28
  let normalize;
@@ -181,4 +197,172 @@ describe(repo_1.Repo.name, () => {
181
197
  expect(repo.getItems().length).toBe(5);
182
198
  });
183
199
  });
200
+ describe('dateRanges', () => {
201
+ beforeEach(() => {
202
+ mockUploadFn.mockResolvedValue({
203
+ error: null,
204
+ artifact: { id: 'art-1', item_type: 'test', item_count: 0 },
205
+ });
206
+ });
207
+ it('should track min and max created_date from a single upload batch', async () => {
208
+ await repo.upload([
209
+ itemWithDate('1', '2023-06-15T12:00:00.000Z'),
210
+ itemWithDate('2', '2020-01-01T00:00:00.000Z'),
211
+ itemWithDate('3', '2021-03-01T00:00:00.000Z'),
212
+ ]);
213
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2020-01-01T00:00:00.000Z'));
214
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2023-06-15T12:00:00.000Z'));
215
+ expect(repo.dateRanges.modifiedDate.oldest).toBe(ts('2020-01-01T00:00:00.000Z'));
216
+ expect(repo.dateRanges.modifiedDate.newest).toBe(ts('2023-06-15T12:00:00.000Z'));
217
+ });
218
+ it('should skip items without created_date', async () => {
219
+ const attachmentWithoutDate = {
220
+ id: 'att-1',
221
+ url: 'https://example.com/file',
222
+ file_name: 'file.txt',
223
+ parent_id: 'parent-1',
224
+ };
225
+ await repo.upload([
226
+ itemWithDate('1', '2022-06-01T00:00:00.000Z'),
227
+ { id: '2', created_date: null, modified_date: '', data: {} },
228
+ { id: '3', modified_date: '', data: {} },
229
+ attachmentWithoutDate,
230
+ ]);
231
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2022-06-01T00:00:00.000Z'));
232
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2022-06-01T00:00:00.000Z'));
233
+ });
234
+ it('should leave timestamps at zero when no items have created_date', async () => {
235
+ await repo.upload([
236
+ { id: '1', modified_date: '', data: {} },
237
+ {
238
+ id: 'att-1',
239
+ url: 'https://example.com/file',
240
+ file_name: 'file.txt',
241
+ parent_id: 'parent-1',
242
+ },
243
+ ]);
244
+ expect(repo.dateRanges).toEqual({
245
+ creationDate: { oldest: 0, newest: 0 },
246
+ modifiedDate: { oldest: 0, newest: 0 },
247
+ });
248
+ });
249
+ it('should not update timestamps or call uploader on empty upload', async () => {
250
+ await repo.upload([]);
251
+ expect(repo.dateRanges).toEqual({
252
+ creationDate: { oldest: 0, newest: 0 },
253
+ modifiedDate: { oldest: 0, newest: 0 },
254
+ });
255
+ expect(mockUploadFn).not.toHaveBeenCalled();
256
+ });
257
+ it('should accumulate min and max across multiple upload batches via push', async () => {
258
+ repo = new repo_1.Repo({
259
+ event: (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
260
+ payload: { event_type: types_1.EventType.ExtractionDataStart },
261
+ }),
262
+ itemType: 'test_item_type',
263
+ onUpload: jest.fn(),
264
+ options: { batchSize: 3 },
265
+ });
266
+ const dates = [
267
+ '2020-01-01T00:00:00.000Z',
268
+ '2021-01-01T00:00:00.000Z',
269
+ '2022-01-01T00:00:00.000Z',
270
+ '2023-01-01T00:00:00.000Z',
271
+ '2024-06-01T00:00:00.000Z',
272
+ '2024-12-01T00:00:00.000Z',
273
+ '2024-12-31T00:00:00.000Z',
274
+ ];
275
+ const items = dates.map((created_date, index) => itemWithDate(String(index), created_date));
276
+ await repo.push(items);
277
+ expect(repo.getItems()).toHaveLength(1);
278
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2020-01-01T00:00:00.000Z'));
279
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2024-12-01T00:00:00.000Z'));
280
+ await repo.push([
281
+ itemWithDate('7', '2019-01-01T00:00:00.000Z'),
282
+ itemWithDate('8', '2025-01-01T00:00:00.000Z'),
283
+ ]);
284
+ expect(repo.getItems()).toHaveLength(0);
285
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2019-01-01T00:00:00.000Z'));
286
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2025-01-01T00:00:00.000Z'));
287
+ });
288
+ it('should extend min and max when subsequent batches have wider date range', async () => {
289
+ await repo.upload([
290
+ itemWithDate('1', '2022-06-01T00:00:00.000Z'),
291
+ itemWithDate('2', '2023-06-01T00:00:00.000Z'),
292
+ ]);
293
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2022-06-01T00:00:00.000Z'));
294
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2023-06-01T00:00:00.000Z'));
295
+ await repo.upload([
296
+ itemWithDate('3', '2020-01-01T00:00:00.000Z'),
297
+ itemWithDate('4', '2024-01-01T00:00:00.000Z'),
298
+ ]);
299
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2020-01-01T00:00:00.000Z'));
300
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2024-01-01T00:00:00.000Z'));
301
+ });
302
+ it('should update timestamps even when upload fails', async () => {
303
+ mockUploadFn.mockResolvedValueOnce({
304
+ error: new Error('fail'),
305
+ artifact: null,
306
+ });
307
+ await expect(repo.upload([itemWithDate('1', '2022-01-01T00:00:00.000Z')])).rejects.toThrow('fail');
308
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2022-01-01T00:00:00.000Z'));
309
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2022-01-01T00:00:00.000Z'));
310
+ });
311
+ it('should ignore invalid created_date and modified_date values', async () => {
312
+ await repo.upload([
313
+ {
314
+ id: '1',
315
+ created_date: 'not-a-date',
316
+ modified_date: 'still-not-a-date',
317
+ data: {},
318
+ },
319
+ itemWithDates('2', '2022-01-01T00:00:00.000Z', '2023-01-01T00:00:00.000Z'),
320
+ ]);
321
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2022-01-01T00:00:00.000Z'));
322
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2022-01-01T00:00:00.000Z'));
323
+ expect(repo.dateRanges.modifiedDate.oldest).toBe(ts('2023-01-01T00:00:00.000Z'));
324
+ expect(repo.dateRanges.modifiedDate.newest).toBe(ts('2023-01-01T00:00:00.000Z'));
325
+ });
326
+ it('should track modified_date independently from created_date', async () => {
327
+ await repo.upload([
328
+ itemWithDates('1', '2020-01-01T00:00:00.000Z', '2023-01-01T00:00:00.000Z'),
329
+ itemWithDates('2', '2024-01-01T00:00:00.000Z', '2021-06-01T00:00:00.000Z'),
330
+ ]);
331
+ expect(repo.dateRanges.creationDate.oldest).toBe(ts('2020-01-01T00:00:00.000Z'));
332
+ expect(repo.dateRanges.creationDate.newest).toBe(ts('2024-01-01T00:00:00.000Z'));
333
+ expect(repo.dateRanges.modifiedDate.oldest).toBe(ts('2021-06-01T00:00:00.000Z'));
334
+ expect(repo.dateRanges.modifiedDate.newest).toBe(ts('2023-01-01T00:00:00.000Z'));
335
+ });
336
+ });
337
+ it('should throw when upload fails', async () => {
338
+ mockUploadFn.mockResolvedValueOnce({
339
+ error: { message: 'upload failed' },
340
+ });
341
+ await expect(repo.upload((0, test_helpers_1.createItems)(1))).rejects.toThrow('upload failed');
342
+ });
343
+ it('should retain items in repo when batch upload fails during push', async () => {
344
+ repo = new repo_1.Repo({
345
+ event: (0, test_utils_1.createMockEvent)(jest_setup_1.mockServer.baseUrl, {
346
+ payload: { event_type: types_1.EventType.ExtractionDataStart },
347
+ }),
348
+ itemType: 'test_item_type',
349
+ normalize,
350
+ onUpload: jest.fn(),
351
+ options: { batchSize: 10 },
352
+ });
353
+ const items = (0, test_helpers_1.createItems)(20);
354
+ mockUploadFn
355
+ .mockResolvedValueOnce({
356
+ artifact: {
357
+ id: 'artifact-1',
358
+ item_type: 'test_item_type',
359
+ item_count: 10,
360
+ },
361
+ })
362
+ .mockResolvedValueOnce({
363
+ error: { message: 'second batch failed' },
364
+ });
365
+ await expect(repo.push(items)).rejects.toThrow('second batch failed');
366
+ expect(repo.getItems().length).toBe(10);
367
+ });
184
368
  });
@@ -388,7 +388,14 @@ export interface EventData {
388
388
  * WorkerMetadata is an interface that defines the structure of the worker metadata that is sent from the external extractor to ADaaS.
389
389
  */
390
390
  export interface WorkerMetadata {
391
- adaas_library_version: string;
391
+ adaas_library_version?: string;
392
+ item_type?: string;
393
+ oldest_created_date?: string;
394
+ newest_created_date?: string;
395
+ oldest_modified_date?: string;
396
+ newest_modified_date?: string;
397
+ oldest_state_date?: string;
398
+ newest_state_date?: string;
392
399
  }
393
400
  /**
394
401
  * DomainObject is an interface that defines the structure of a domain object that can be extracted.