-
Notifications
You must be signed in to change notification settings - Fork 39
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #15 from knovator/review
Review API
- Loading branch information
Showing
25 changed files
with
359 additions
and
34 deletions.
There are no files selected for viewing
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
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
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
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
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,20 @@ | ||
import { Controller, Get, Param, UseGuards } from '@nestjs/common'; | ||
import { ApiOperation, ApiTags, ApiSecurity } from '@nestjs/swagger'; | ||
import { APIKeyGuard } from '../shared/framework/auth.gaurd'; | ||
import { DoReview } from './usecases/do-review/do-review.usecase'; | ||
|
||
@Controller('/review') | ||
@ApiTags('Review') | ||
@ApiSecurity('ACCESS_KEY') | ||
@UseGuards(APIKeyGuard) | ||
export class ReviewController { | ||
constructor(private doReview: DoReview) {} | ||
|
||
@Get(':uploadId') | ||
@ApiOperation({ | ||
summary: 'Get Review data for uploaded file', | ||
}) | ||
async getReview(@Param('uploadId') uploadId: string) { | ||
return await this.doReview.execute(uploadId); | ||
} | ||
} |
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,12 @@ | ||
import { Module } from '@nestjs/common'; | ||
import { USE_CASES } from './usecases'; | ||
import { ReviewController } from './review.controller'; | ||
import { SharedModule } from '../shared/shared.module'; | ||
import { AJVService } from './service/AJV.service'; | ||
|
||
@Module({ | ||
imports: [SharedModule], | ||
providers: [...USE_CASES, AJVService], | ||
controllers: [ReviewController], | ||
}) | ||
export class ReviewModule {} |
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,166 @@ | ||
import { Injectable } from '@nestjs/common'; | ||
import { ColumnTypesEnum } from '@impler/shared'; | ||
import { ColumnEntity, MappingEntity } from '@impler/dal'; | ||
import Ajv, { ErrorObject } from 'ajv'; | ||
import addFormats from 'ajv-formats'; | ||
import addKeywords from 'ajv-keywords'; | ||
|
||
const ajv = new Ajv({ | ||
allErrors: true, | ||
coerceTypes: true, | ||
allowUnionTypes: true, | ||
removeAdditional: true, | ||
verbose: true, | ||
}); | ||
addFormats(ajv, ['email']); | ||
addKeywords(ajv); | ||
ajv.addFormat('custom-date-time', function (dateTimeString) { | ||
if (typeof dateTimeString === 'object') { | ||
dateTimeString = (dateTimeString as Date).toISOString(); | ||
} | ||
|
||
return !isNaN(Date.parse(dateTimeString)); // any test that returns true/false | ||
}); | ||
|
||
@Injectable() | ||
export class AJVService { | ||
async validate(columns: ColumnEntity[], mappings: MappingEntity[], data: any) { | ||
const schema = this.buildAJVSchema(columns, mappings); | ||
const validator = ajv.compile(schema); | ||
|
||
const valid = validator(data); | ||
const returnData = { | ||
invalid: [], | ||
valid: [], | ||
}; | ||
if (!valid) { | ||
const errors: Record<number, any> = this.buildErrorRecords(validator.errors, data); | ||
|
||
returnData.invalid = Object.values(errors); | ||
Object.keys(errors).forEach((index) => (data as any).splice(index as unknown as number, 1)); | ||
} | ||
returnData.valid = data as any; | ||
|
||
return returnData; | ||
} | ||
private buildAJVSchema(columns: ColumnEntity[], mappings: MappingEntity[]) { | ||
const formattedColumns: Record<string, ColumnEntity> = columns.reduce((acc, column) => { | ||
acc[column._id] = { ...column }; | ||
|
||
return acc; | ||
}, {}); | ||
const properties: Record<string, unknown> = mappings.reduce((acc, mapping) => { | ||
acc[mapping.columnHeading] = this.getProperty(formattedColumns[mapping._columnId]); | ||
|
||
return acc; | ||
}, {}); | ||
const requiredProperties: string[] = mappings.reduce((acc, mapping) => { | ||
if (formattedColumns[mapping._columnId].isRequired) acc.push(mapping.columnHeading); | ||
|
||
return acc; | ||
}, []); | ||
const uniqueItemProperties = mappings.reduce((acc, mapping) => { | ||
if (formattedColumns[mapping._columnId].isUnique) acc.push(mapping.columnHeading); | ||
|
||
return acc; | ||
}, []); | ||
const objectSchema = { | ||
type: 'object', | ||
properties, | ||
required: requiredProperties, | ||
additionalProperties: false, | ||
}; | ||
|
||
return { | ||
type: 'array', | ||
items: objectSchema, | ||
uniqueItemProperties, | ||
}; | ||
} | ||
private getProperty(column: ColumnEntity) { | ||
/* | ||
* ${1} is record object | ||
* ${1#} is record index | ||
* ${0#} is key | ||
* ${0} is value | ||
*/ | ||
switch (column.type) { | ||
case ColumnTypesEnum.STRING: | ||
return { | ||
type: 'string', | ||
// errorMessage: { type: `\${1#}${this.indexSeperator}${heading} is not valid string` }, | ||
}; | ||
case ColumnTypesEnum.NUMBER: | ||
return { | ||
type: 'number', | ||
// errorMessage: { type: `\${1#}${this.indexSeperator}${heading} is not valid number` }, | ||
}; | ||
case ColumnTypesEnum.SELECT: | ||
return { | ||
type: 'string', | ||
enum: column.selectValues || [], | ||
/* | ||
* errorMessage: { | ||
* enum: `\${1#}${this.indexSeperator}${heading} value should be from [${column.selectValues.join(',')}]`, | ||
* type: `\${1#}${this.indexSeperator}${heading} is not valid string value`, | ||
* }, | ||
*/ | ||
}; | ||
case ColumnTypesEnum.REGEX: | ||
const [full, pattern, flags] = column.regex.match(/\/(.*)\/(.*)|(.*)/); | ||
|
||
return { type: 'string', regexp: { pattern: pattern || full, flags: flags || '' } }; | ||
case ColumnTypesEnum.EMAIL: | ||
return { type: 'string', format: 'email' }; | ||
case ColumnTypesEnum.DATE: | ||
return { type: 'string', format: 'custom-date-time' }; | ||
case ColumnTypesEnum.ANY: | ||
return { type: ['string', 'number', 'object'] }; | ||
default: | ||
throw new Error(`Column type ${column.type} is not supported`); | ||
} | ||
} | ||
private buildErrorRecords(errors: ErrorObject[], data?: any[]) { | ||
let index: string, field: string, message: string; | ||
|
||
return errors.reduce((acc, error) => { | ||
[, index, field] = error.instancePath.split('/'); | ||
message = this.getMessage(error, field); | ||
|
||
if (acc[index]) { | ||
acc[index].message += `${message}`; | ||
} else | ||
acc[index] = { | ||
index, | ||
message, | ||
...data[index], | ||
}; | ||
|
||
return acc; | ||
}, {}); | ||
} | ||
private getMessage(error: ErrorObject, field: string): string { | ||
let message = ''; | ||
switch (error.keyword) { | ||
case 'type': | ||
message = `${field} ${error.message}`; | ||
break; | ||
case 'enum': | ||
message = `${field} must be from [${error.params.allowedValues}]`; | ||
break; | ||
case 'regexp': | ||
message = `${field} must match pattern ${new RegExp( | ||
error.parentSchema?.regexp?.pattern, | ||
error.parentSchema?.regexp?.flags || '' | ||
).toString()}`; | ||
break; | ||
case 'pattern': | ||
message = `${field} must match pattern ${error.params.pattern}`; | ||
break; | ||
default: | ||
return `${field} contains invalid data`; | ||
} | ||
|
||
return `<li>${message}</li>`; | ||
} | ||
} |
50 changes: 50 additions & 0 deletions
50
apps/api/src/app/review/usecases/do-review/do-review.usecase.ts
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,50 @@ | ||
import { UploadStatusEnum } from '@impler/shared'; | ||
import { Injectable, BadRequestException } from '@nestjs/common'; | ||
import { ColumnRepository, UploadRepository, MappingRepository, FileEntity } from '@impler/dal'; | ||
import { StorageService } from '../../../shared/storage/storage.service'; | ||
import { AJVService } from '../../service/AJV.service'; | ||
import { APIMessages } from '../../../shared/constants'; | ||
import { FileNotExistError } from '../../../shared/errors/file-not-exist.error'; | ||
|
||
@Injectable() | ||
export class DoReview { | ||
constructor( | ||
private uploadRepository: UploadRepository, | ||
private storageService: StorageService, | ||
private columnRepository: ColumnRepository, | ||
private mappingRepository: MappingRepository, | ||
private ajvService: AJVService | ||
) {} | ||
|
||
async execute(uploadId: string) { | ||
const uploadInfo = await this.uploadRepository.getUploadInformation(uploadId); | ||
if (!uploadInfo) { | ||
throw new BadRequestException(APIMessages.UPLOAD_NOT_FOUND); | ||
} | ||
if (uploadInfo.status !== UploadStatusEnum.MAPPED) { | ||
throw new BadRequestException(APIMessages.FILE_MAPPING_REMAINING); | ||
} | ||
const allDataFileInfo = uploadInfo._allDataFileId as unknown as FileEntity; | ||
const dataContent = await this.getFileContent(allDataFileInfo.path); | ||
const mappings = await this.mappingRepository.find({ _uploadId: uploadId }, '_columnId columnHeading'); | ||
const columns = await this.columnRepository.find( | ||
{ _templateId: uploadInfo._templateId }, | ||
'isRequired isUnique selectValues type regex' | ||
); | ||
|
||
return this.ajvService.validate(columns, mappings, dataContent); | ||
} | ||
|
||
async getFileContent(path): Promise<string> { | ||
try { | ||
const dataContent = await this.storageService.getFileContent(path, 'utf8'); | ||
|
||
return JSON.parse(dataContent); | ||
} catch (error) { | ||
if (error instanceof FileNotExistError) { | ||
throw new BadRequestException(APIMessages.FILE_NOT_FOUND_IN_STORAGE); | ||
} | ||
throw error; | ||
} | ||
} | ||
} |
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,6 @@ | ||
import { DoReview } from './do-review/do-review.usecase'; | ||
|
||
export const USE_CASES = [ | ||
DoReview, | ||
// | ||
]; |
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
Oops, something went wrong.