forked from Talenttrust/Talenttrust-Backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patharchival.ts
More file actions
426 lines (389 loc) · 13.9 KB
/
Copy patharchival.ts
File metadata and controls
426 lines (389 loc) · 13.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
/**
* Data Archival Service
*
* Handles secure archival of expired or archived data, including
* encryption, storage management, and lifecycle operations.
*
* @module retention/archival
*/
import { RetainedData, ArchivalStorageType, DataClassification } from './types';
import { StorageManager } from './storage';
import { RetentionPolicyEngine } from './policies';
/**
* Options for archival operations
* @interface ArchivalOptions
*/
export interface ArchivalOptions {
encrypted?: boolean;
location?: string;
metadata?: Record<string, unknown>;
}
/**
* Archival result information
* @interface ArchivalResult
*/
export interface ArchivalResult {
success: boolean;
dataId: string;
archivedAt: Date;
location: string;
encrypted: boolean;
sizeBytes?: number;
metadata?: Record<string, unknown>;
}
/**
* Data archival service
*
* Manages the lifecycle of archiving data, including secure storage,
* retrieval, restoration, and compliance tracking.
*
* @class DataArchivalService
*/
export class DataArchivalService {
private storageManager: StorageManager;
private policyEngine: RetentionPolicyEngine;
private encryptionEnabled: boolean;
/**
* Initialize the archival service
* @param {StorageManager} storageManager - Storage management service
* @param {RetentionPolicyEngine} policyEngine - Policy enforcement engine
* @param {boolean} [encryptionEnabled=false] - Enable encryption for archives
*/
constructor(
storageManager: StorageManager,
policyEngine: RetentionPolicyEngine,
encryptionEnabled: boolean = false,
) {
this.storageManager = storageManager;
this.policyEngine = policyEngine;
this.encryptionEnabled = encryptionEnabled;
}
/**
* Archive data based on policy requirements
*
* Moves data to archival storage with optional encryption based on
* its classification level and retention policy.
*
* @param {RetainedData} data - Data to archive
* @param {ArchivalOptions} [options] - Archival configuration
* @returns {Promise<ArchivalResult>} Archival operation result
* @throws {Error} If archival fails
*/
async archiveData(data: RetainedData, options?: ArchivalOptions): Promise<ArchivalResult> {
if (data.isArchived) {
throw new Error(`Data ${data.id} is already archived`);
}
const now = new Date();
const policy = data.retentionPolicyId ? this.policyEngine.getPolicy(data.retentionPolicyId) : null;
const archivalStorageType = policy?.archivalType || ArchivalStorageType.COLD_STORAGE;
const policyEncryptionRequirement = policy?.encryptArchive ?? true;
const shouldEncrypt =
this.shouldEncryptArchive(data.classification, policyEncryptionRequirement) ||
(options?.encrypted ?? false);
const archivedData: RetainedData = {
...data,
isArchived: true,
archivedAt: now,
archivedLocation: options?.location || this.generateArchiveLocation(data, archivalStorageType),
metadata: {
...data.metadata,
encrypted: shouldEncrypt,
...options?.metadata,
},
};
try {
const location = await this.storageManager.store(archivedData, archivalStorageType);
return {
success: true,
dataId: data.id,
archivedAt: now,
location,
encrypted: shouldEncrypt,
metadata: archivedData.metadata,
};
} catch (error) {
throw new Error(`Failed to archive data ${data.id}: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
/**
* Restore archived data back to active storage
*
* Retrieves archived data and restores it to the active data store
* with updated metadata reflecting the restoration.
*
* @param {string} dataId - ID of archived data to restore
* @param {ArchivalStorageType} [fromLocation] - Archival storage type
* @returns {Promise<RetainedData>} Restored data
* @throws {Error} If restoration fails
*/
async restoreArchivedData(
dataId: string,
fromLocation?: ArchivalStorageType,
): Promise<RetainedData> {
const location = fromLocation || ArchivalStorageType.COLD_STORAGE;
const archivedData = await this.storageManager.retrieve(dataId, location);
if (!archivedData) {
throw new Error(`Archived data not found: ${dataId}`);
}
const restoredData: RetainedData = {
...archivedData,
isArchived: false,
archivedAt: undefined,
archivedLocation: undefined,
};
// Move data back to local storage
const success = await this.storageManager.moveData(dataId, location, ArchivalStorageType.LOCAL);
if (!success) {
throw new Error(`Failed to restore data ${dataId} from archive`);
}
return restoredData;
}
/**
* Retrieve archived data (read-only)
*
* @param {string} dataId - ID of archived data
* @param {ArchivalStorageType} [fromLocation] - Archival storage type
* @returns {Promise<RetainedData | null>} Archived data or null if not found
*/
async getArchivedData(
dataId: string,
fromLocation?: ArchivalStorageType,
): Promise<RetainedData | null> {
const location = fromLocation || ArchivalStorageType.COLD_STORAGE;
return this.storageManager.retrieve(dataId, location);
}
/**
* Permanently delete archived data
*
* Securely removes archived data that has exceeded post-archival
* retention period.
*
* @param {string} dataId - ID of archived data to delete
* @param {ArchivalStorageType} [fromLocation] - Archival storage type
* @returns {Promise<boolean>} Success status
*/
async permanentlyDeleteArchived(
dataId: string,
fromLocation?: ArchivalStorageType,
): Promise<boolean> {
const location = fromLocation || ArchivalStorageType.COLD_STORAGE;
return this.storageManager.delete(dataId, location);
}
/**
* Get archival status for data
*
* @param {string} dataId - Data identifier
* @param {ArchivalStorageType} [fromLocation] - Archival storage type
* @returns {Promise<{archived: boolean; location?: string; timestamp?: Date}>} Archival status
*/
async getArchivalStatus(
dataId: string,
fromLocation?: ArchivalStorageType,
): Promise<{ archived: boolean; location?: string; timestamp?: Date }> {
const location = fromLocation || ArchivalStorageType.COLD_STORAGE;
const data = await this.storageManager.retrieve(dataId, location);
if (!data) {
return { archived: false };
}
return {
archived: data.isArchived,
location: data.archivedLocation,
timestamp: data.archivedAt,
};
}
/**
* Determine if data should be encrypted based on classification and policy
* @private
* @param {DataClassification} classification - Data sensitivity level
* @param {boolean} policyRequires - Whether policy requires encryption
* @returns {boolean}
*/
private shouldEncryptArchive(classification: DataClassification, policyRequires: boolean): boolean {
if (!this.encryptionEnabled) return false;
// Always encrypt restricted and confidential data
if (
classification === DataClassification.RESTRICTED ||
classification === DataClassification.CONFIDENTIAL
) {
return true;
}
// Respect policy requirement for other classifications
return policyRequires;
}
/**
* Generate archive location path based on data and storage type
* @private
* @param {RetainedData} data - Data being archived
* @param {ArchivalStorageType} storageType - Storage type
* @returns {string}
*/
private generateArchiveLocation(data: RetainedData, storageType: ArchivalStorageType): string {
const timestamp = data.archivedAt || new Date();
const year = timestamp.getUTCFullYear();
const month = String(timestamp.getUTCMonth() + 1).padStart(2, '0');
// Format: /archive/{storageType}/{entityType}/{year}/{month}/{dataId}
return `/archive/${storageType}/${data.entityType}/${year}/${month}/${data.id}`;
}
/**
* List all archived data
*
* @param {ArchivalStorageType} [storageType] - Filter by storage type
* @returns {Promise<RetainedData[]>} All archived data
*/
/**
* List archived data with optional storage filter and pagination.
*
* @param {ArchivalStorageType} [storageType] - Filter by storage type.
* @param {number} [limit] - Maximum number of records to return. If omitted, returns all.
* @param {number} [offset] - Number of records to skip before returning results. Defaults to 0.
* @returns {Promise<RetainedData[]>} Archived data matching criteria.
*/
async listArchivedData(
storageType?: ArchivalStorageType,
limit?: number,
offset: number = 0,
): Promise<RetainedData[]> {
// Helper to apply pagination
const paginate = (items: RetainedData[]): RetainedData[] => {
if (limit !== undefined) {
return items.slice(offset, offset + limit);
}
return items.slice(offset);
};
// If a specific storage type is provided, query the backing provider and keep
// only the records that actually belong to that storage type. Several storage
// types can share a single physical provider (e.g. COLD_STORAGE and
// ENCRYPTED_ARCHIVE both map to the archive provider), so listing the provider
// alone would return records from sibling storage types too.
if (storageType) {
const provider = this.storageManager.getProvider(storageType);
const all = await provider.list();
const filtered = all.filter(
(item) => this.resolveStorageType(item) === storageType,
);
return paginate(filtered);
}
// No storage filter: aggregate a de-duplicated view across all storage types.
// Because multiple enum values can resolve to the same provider, iterating the
// enum naively would visit shared providers more than once and double-count
// their records. De-duplicate by id to return each archived item exactly once.
const allTypes = Object.values(ArchivalStorageType) as ArchivalStorageType[];
const seen = new Set<string>();
const aggregated: RetainedData[] = [];
for (const type of allTypes) {
const provider = this.storageManager.getProvider(type);
const list = await provider.list();
for (const item of list) {
if (seen.has(item.id)) continue;
seen.add(item.id);
aggregated.push(item);
}
}
return paginate(aggregated);
}
/**
* Resolve the archival storage type an item lives in from its archive location.
*
* Archive locations are shaped like `/archive/{storageType}/...`, so the storage
* type is recovered by finding the first path segment that matches a known
* {@link ArchivalStorageType} value. Returns `undefined` when the location is
* missing or does not encode a recognised storage type.
*
* @private
*/
private resolveStorageType(item: RetainedData): ArchivalStorageType | undefined {
const location = item.archivedLocation;
if (!location) return undefined;
const knownTypes = Object.values(ArchivalStorageType) as ArchivalStorageType[];
for (const segment of location.split('/')) {
if (!segment) continue;
if (knownTypes.includes(segment as ArchivalStorageType)) {
return segment as ArchivalStorageType;
}
}
return undefined;
}
/**
* Calculate archive statistics
*
* @returns {Promise<{totalArchived: number; byStorageType: Record<string, number>}>}
*/
/**
* Calculate archive statistics across all storage backends.
*
* @returns {Promise<{ totalArchived: number; byStorageType: Record<string, number> }>}
* Object containing the total number of archived records and a breakdown per storage type.
*/
async getArchiveStats(): Promise<{
totalArchived: number;
byStorageType: Record<string, number>;
}> {
const allTypes = Object.values(ArchivalStorageType) as ArchivalStorageType[];
// Seed every known storage type with a zero count so callers always see the
// full set of buckets, even for storage types that hold no records.
const stats: Record<string, number> = {};
for (const type of allTypes) {
stats[type] = 0;
}
// Providers can be shared across storage types, so de-duplicate by id and
// attribute each record to the storage type encoded in its archive location.
const seen = new Set<string>();
let total = 0;
for (const type of allTypes) {
const provider = this.storageManager.getProvider(type);
const list = await provider.list();
for (const item of list) {
if (seen.has(item.id)) continue;
seen.add(item.id);
const resolved = this.resolveStorageType(item) ?? type;
stats[resolved] = (stats[resolved] ?? 0) + 1;
total += 1;
}
}
return { totalArchived: total, byStorageType: stats };
}
/**
* Export data in specified format for compliance
*
* @param {string} dataId - Data identifier
* @param {'json' | 'csv'} format - Export format
* @param {ArchivalStorageType} [fromLocation] - Archival storage type
* @returns {Promise<string>} Serialized data
*/
async exportData(
dataId: string,
format: 'json' | 'csv',
fromLocation?: ArchivalStorageType,
): Promise<string> {
const data = await this.getArchivedData(dataId, fromLocation);
if (!data) {
throw new Error(`Data not found for export: ${dataId}`);
}
if (format === 'json') {
return JSON.stringify(data, null, 2);
} else {
// Basic CSV implementation
const headers = ['id', 'entityType', 'classification', 'createdAt', 'expiresAt', 'isArchived', 'archivedAt'];
const values = [
data.id,
data.entityType,
data.classification,
data.createdAt.toISOString(),
data.expiresAt.toISOString(),
data.isArchived.toString(),
data.archivedAt?.toISOString() || '',
];
// Flatten data payload if it's an object
if (typeof data.data === 'object' && data.data !== null) {
Object.entries(data.data).forEach(([key, val]) => {
headers.push(`data.${key}`);
values.push(String(val));
});
} else {
headers.push('data');
values.push(String(data.data));
}
return `${headers.join(',')}\n${values.join(',')}`;
}
}
}