-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(repository): add the entity archival feature
will allow to archive soft deleted data GH-00
- Loading branch information
1 parent
85757a8
commit 28c8644
Showing
22 changed files
with
4,313 additions
and
1,465 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { | ||
Binding, | ||
Component, | ||
ContextTags, | ||
ControllerClass, | ||
CoreBindings, | ||
ProviderMap, | ||
ServiceOrProviderClass, | ||
inject, | ||
injectable, | ||
} from '@loopback/core'; | ||
import {Class, Repository} from '@loopback/repository'; | ||
import {Model, RestApplication} from '@loopback/rest'; | ||
import {ArchivalComponentBindings} from './keys'; | ||
import { | ||
ArchivalMappingRepository, | ||
RetrievalJobDetailsRepository, | ||
} from './repositories'; | ||
import {ProcessRetrievedDataProvider} from './providers'; | ||
import { | ||
BuildWhereConditionService, | ||
ImportArchivedDataService, | ||
} from './services'; | ||
import {ArchiveMapping, RetrievalJobDetails} from './models'; | ||
|
||
// Configure the binding for ArchivalComponent | ||
@injectable({tags: {[ContextTags.KEY]: ArchivalComponentBindings.COMPONENT}}) | ||
export class ArchivalComponent implements Component { | ||
constructor( | ||
@inject(CoreBindings.APPLICATION_INSTANCE) | ||
private readonly application: RestApplication, | ||
) { | ||
this.providers = {}; | ||
|
||
this.providers[ArchivalComponentBindings.PROCESS_RETRIEVED_DATA.key] = | ||
ProcessRetrievedDataProvider; | ||
|
||
this.application | ||
.bind('services.BuildWhereConditionService') | ||
.toClass(BuildWhereConditionService); | ||
this.application | ||
.bind('services.ImportArchivedDataService') | ||
.toClass(ImportArchivedDataService); | ||
|
||
this.repositories = [ | ||
ArchivalMappingRepository, | ||
RetrievalJobDetailsRepository, | ||
]; | ||
|
||
this.models = [ArchiveMapping, RetrievalJobDetails]; | ||
} | ||
providers?: ProviderMap = {}; | ||
|
||
bindings?: Binding[] = []; | ||
|
||
services?: ServiceOrProviderClass[]; | ||
|
||
/** | ||
* An optional list of Repository classes to bind for dependency injection | ||
* via `app.repository()` API. | ||
*/ | ||
repositories?: Class<Repository<Model>>[]; | ||
|
||
/** | ||
* An optional list of Model classes to bind for dependency injection | ||
* via `app.model()` API. | ||
*/ | ||
models?: Class<Model>[]; | ||
|
||
/** | ||
* An array of controller classes | ||
*/ | ||
controllers?: ControllerClass[]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import {PutObjectCommand, S3} from '@aws-sdk/client-s3'; | ||
import {BindingScope, Provider, inject, injectable} from '@loopback/core'; | ||
import {AnyObject} from '@loopback/repository'; | ||
import {HttpErrors} from '@loopback/rest'; | ||
import {AWSS3Bindings, AwsS3Config} from '../keys'; | ||
import {ExportDataExternalSystem} from '../types'; | ||
|
||
@injectable({scope: BindingScope.TRANSIENT}) | ||
export class ExportArchiveDataProvider | ||
implements Provider<ExportDataExternalSystem> | ||
{ | ||
constructor( | ||
@inject(AWSS3Bindings.Config, {optional: true}) | ||
private readonly config: AwsS3Config, | ||
) {} | ||
value(): ExportDataExternalSystem { | ||
return async (seletedEntries: AnyObject[]) => | ||
this.exportToCsv(seletedEntries); | ||
} | ||
async exportToCsv(seletedEntries: AnyObject[]): Promise<string> { | ||
const csvRows = []; | ||
const header = Object.keys(seletedEntries[0]); | ||
|
||
csvRows.push(header.join(',')); | ||
|
||
for (const entry of seletedEntries) { | ||
const values = []; | ||
|
||
for (const key of header) { | ||
let value = entry[key]; | ||
|
||
// Check if it is an object | ||
// convert object to string | ||
|
||
if (value instanceof Date) { | ||
value = new Date(value).toISOString(); | ||
} else if (value && typeof value === 'object') { | ||
value = JSON.stringify(entry[key]); | ||
// Escape existing quotation marks within the value | ||
value = value.replace(/"/g, '""'); | ||
// Surround the value with quotation marks | ||
value = `"${value}"`; | ||
} else { | ||
//this is intentional | ||
} | ||
|
||
values.push(value); | ||
} | ||
csvRows.push(values.join(',')); | ||
} | ||
const csvString = csvRows.join('\n'); | ||
const timestamp = new Date().toISOString(); | ||
//Example: PATH_TO_UPLOAD_FILES='/path' | ||
const fileName = `${process.env.PATH_TO_UPLOAD_FILES}/archive_${timestamp}.csv`; | ||
|
||
const s3Config: AwsS3Config = { | ||
credentials: { | ||
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? this.config.accessKeyId, | ||
secretAccessKey: | ||
process.env.AWS_SECRET_ACCESS_KEY ?? this.config.secretAccessKey, | ||
}, | ||
region: process.env.AWS_REGION ?? this.config.region, | ||
...this?.config, | ||
// Other properties... | ||
}; | ||
const s3Client: S3 = new S3(s3Config); | ||
const bucketName = process.env.AWS_S3_BUCKET_NAME; | ||
const params = { | ||
Body: csvString, | ||
Key: fileName, | ||
Bucket: bucketName as string, | ||
}; | ||
try { | ||
await s3Client.send(new PutObjectCommand(params)); | ||
return params.Key; | ||
} catch (error) { | ||
throw new HttpErrors.UnprocessableEntity(error.message); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { | ||
BindingScope, | ||
injectable, | ||
Provider, | ||
ValueOrPromise, | ||
} from '@loopback/core'; | ||
import {AnyObject} from '@loopback/repository'; | ||
import {HttpErrors} from '@loopback/rest'; | ||
import AWS from 'aws-sdk'; | ||
import {ImportDataExternalSystem} from '../types'; | ||
|
||
@injectable({scope: BindingScope.TRANSIENT}) | ||
export class ImportArchiveDataProvider | ||
implements Provider<ImportDataExternalSystem> | ||
{ | ||
value(): ValueOrPromise<ImportDataExternalSystem> { | ||
return async (fileName: string) => this.getFileContent(fileName); | ||
} | ||
|
||
async getFileContent(fileName: string): Promise<AnyObject[]> { | ||
const stream = require('stream'); | ||
const csv = require('csv-parser'); | ||
|
||
AWS.config = new AWS.Config({ | ||
accessKeyId: process.env.AWS_ACCESS_KEY_ID, | ||
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY, | ||
region: process.env.AWS_REGION, | ||
}); | ||
const s3 = new AWS.S3(); | ||
|
||
const params: AWS.S3.GetObjectRequest = { | ||
Bucket: process.env.AWS_S3_BUCKET_NAME as string, | ||
Key: fileName, | ||
}; | ||
let s3Response; | ||
try { | ||
s3Response = await s3.getObject(params).promise(); | ||
const csvData = s3Response.Body!.toString('utf-8'); | ||
|
||
const jsonArray: AnyObject[] = await new Promise((resolve, reject) => { | ||
const results: AnyObject[] = []; | ||
stream.Readable.from(csvData) | ||
.pipe(csv()) | ||
.on('data', (data: AnyObject) => results.push(data)) | ||
.on('end', () => resolve(results)) | ||
.on('error', reject); | ||
}); | ||
const headers = Object.keys(jsonArray[0]); | ||
for (const entry of jsonArray) { | ||
for (const key of headers) { | ||
const value = entry[key]; | ||
entry[key] = this.processEntryValue(key, value); | ||
} | ||
} | ||
return jsonArray; | ||
} catch (error) { | ||
throw new HttpErrors.UnprocessableEntity(error.message); | ||
} | ||
} | ||
|
||
private processEntryValue(key: string, value: string) { | ||
if (value === '') { | ||
return null; | ||
} | ||
if ( | ||
typeof value === 'string' && | ||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/.test(value) | ||
) { | ||
return new Date(value); | ||
} | ||
return value; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './export-archive-data.provider'; | ||
export * from './import-archive-data.provider'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {S3ClientConfig} from '@aws-sdk/client-s3'; | ||
import {BindingKey} from '@loopback/core'; | ||
import { | ||
ExportDataExternalSystem, | ||
ImportDataExternalSystem, | ||
ProcessRetrievedData, | ||
} from './types'; | ||
import {ArchivalComponent} from './archival-component'; | ||
|
||
/** | ||
* Binding keys used by this component. | ||
*/ | ||
|
||
const BINDING_PREFIX = 'sf.archival'; | ||
|
||
export namespace ArchivalComponentBindings { | ||
export const COMPONENT = BindingKey.create<ArchivalComponent>( | ||
`${BINDING_PREFIX}.ArchivalComponent`, | ||
); | ||
|
||
export const EXPORT_ARCHIVE_DATA = | ||
BindingKey.create<ExportDataExternalSystem | null>( | ||
`${BINDING_PREFIX}.entity.archive.export`, | ||
); | ||
|
||
export const IMPORT_ARCHIVE_DATA = | ||
BindingKey.create<ImportDataExternalSystem | null>( | ||
`${BINDING_PREFIX}.entity.archive.import`, | ||
); | ||
|
||
export const PROCESS_RETRIEVED_DATA = | ||
BindingKey.create<ProcessRetrievedData | null>( | ||
`${BINDING_PREFIX}.entity.import`, | ||
); | ||
} | ||
|
||
export namespace AWSS3Bindings { | ||
export const Config = BindingKey.create<AwsS3Config>( | ||
`${BINDING_PREFIX}.archival.s3.config`, | ||
); | ||
} | ||
|
||
export interface AwsS3Config extends S3ClientConfig { | ||
accessKeyId: string; | ||
secretAccessKey: string; | ||
region?: string; | ||
} |
Oops, something went wrong.