-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add program exception model * add route with controller * add exception servicen and typings file * exception db funcs * 422 status code util * exception util file * clean up exception repo file * fix req error handling * fix record error handling for promise behaviour * add swagger * remove unused util file * use enum for exception values * fix up types * add no emit typecheck command * add no program exception record type guard * rename req body check func * change HTTP status code Co-authored-by: Ciaran Schutte <[email protected]>
- Loading branch information
1 parent
1986ad2
commit d52aa28
Showing
9 changed files
with
406 additions
and
54 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
* Copyright (c) 2022 The Ontario Institute for Cancer Research. All rights reserved | ||
* | ||
* This program and the accompanying materials are made available under the terms of | ||
* the GNU Affero General Public License v3.0. You should have received a copy of the | ||
* GNU Affero General Public License along with this program. | ||
* If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY | ||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | ||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT | ||
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | ||
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; | ||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | ||
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | ||
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
|
||
import { Request, Response } from 'express'; | ||
import { HasFullWriteAccess } from '../decorators'; | ||
import { loggerFor } from '../logger'; | ||
import { ControllerUtils, TsvUtils } from '../utils'; | ||
import * as exceptionService from './exception-service'; | ||
import { isProgramExceptionRecord, isReadonlyArrayOf } from './types'; | ||
|
||
const L = loggerFor(__filename); | ||
|
||
enum ProgramExceptionErrorMessage { | ||
TSV_PARSING_FAILED = `This file is formatted incorrectly`, | ||
} | ||
|
||
class ExceptionController { | ||
@HasFullWriteAccess() | ||
async createProgramException(req: Request, res: Response) { | ||
if (!requestContainsFile(req, res)) { | ||
return false; | ||
} | ||
|
||
const programId = req.params.programId; | ||
const file = req.file; | ||
|
||
try { | ||
const records = await TsvUtils.tsvToJson(file.path); | ||
if (records.length === 0) { | ||
throw new Error('TSV has no records!'); | ||
} | ||
|
||
if (!isReadonlyArrayOf(records, isProgramExceptionRecord)) { | ||
throw new Error('TSV is incorrectly structured'); | ||
} | ||
|
||
const result = await exceptionService.operations.createProgramException({ | ||
programId, | ||
records, | ||
}); | ||
|
||
if (!result.successful) { | ||
return res.status(422).send(result); | ||
} | ||
return res.status(201).send(result); | ||
} catch (err) { | ||
L.error(`Program Exception TSV_PARSING_FAILED`, err); | ||
return ControllerUtils.unableToProcess(res, ProgramExceptionErrorMessage.TSV_PARSING_FAILED); | ||
} | ||
} | ||
} | ||
|
||
const requestContainsFile = (req: Request, res: Response): boolean => { | ||
if (req.file === undefined || req.file.size <= 0) { | ||
L.debug(`File missing`); | ||
ControllerUtils.badRequest(res, `Program exception file upload required`); | ||
return false; | ||
} | ||
return true; | ||
}; | ||
|
||
export default new ExceptionController(); |
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,158 @@ | ||
/* | ||
* Copyright (c) 2022 The Ontario Institute for Cancer Research. All rights reserved | ||
* | ||
* This program and the accompanying materials are made available under the terms of | ||
* the GNU Affero General Public License v3.0. You should have received a copy of the | ||
* GNU Affero General Public License along with this program. | ||
* If not, see <http://www.gnu.org/licenses/>. | ||
* | ||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY | ||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES | ||
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT | ||
* SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, | ||
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED | ||
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; | ||
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER | ||
* IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN | ||
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
*/ | ||
|
||
import { DeepReadonly } from 'deep-freeze'; | ||
import * as dictionaryManager from '../dictionary/manager'; | ||
import { SchemaWithFields } from '../dictionary/manager'; | ||
import { programExceptionRepository } from './exception-repo'; | ||
import { ExceptionValue, ProgramException, ProgramExceptionRecord } from './types'; | ||
|
||
const recordsToException = ( | ||
programId: string, | ||
records: ReadonlyArray<ProgramExceptionRecord>, | ||
): ProgramException => ({ | ||
programId, | ||
exceptions: records.map(r => ({ | ||
schema: r.schema, | ||
coreField: r.requested_core_field, | ||
exceptionValue: r.requested_exception_value, | ||
})), | ||
}); | ||
|
||
interface ProgramExceptionResult { | ||
programException: undefined | DeepReadonly<ProgramException>; | ||
errors: ValidationError[]; | ||
successful: boolean; | ||
} | ||
export namespace operations { | ||
export const createProgramException = async ({ | ||
programId, | ||
records, | ||
}: { | ||
programId: string; | ||
records: ReadonlyArray<ProgramExceptionRecord>; | ||
}): Promise<ProgramExceptionResult> => { | ||
const errors = await validateExceptionRecords(programId, records); | ||
|
||
if (errors.length > 0) { | ||
return { | ||
programException: undefined, | ||
errors, | ||
successful: false, | ||
}; | ||
} else { | ||
const exception = recordsToException(programId, records); | ||
|
||
const result = await programExceptionRepository.create(exception); | ||
return { | ||
programException: result, | ||
errors: [], | ||
successful: true, | ||
}; | ||
} | ||
}; | ||
} | ||
|
||
interface ValidationError { | ||
message: string; | ||
row: number; | ||
} | ||
|
||
const validateExceptionRecords = async ( | ||
programId: string, | ||
records: ReadonlyArray<ProgramExceptionRecord>, | ||
): Promise<ValidationError[]> => { | ||
let errors: ValidationError[] = []; | ||
|
||
for (const [idx, record] of records.entries()) { | ||
const programErrors = checkProgramId(programId, record, idx); | ||
const coreFieldErrors = await checkCoreField(record, idx); | ||
const requestedValErrors = checkRequestedValue(record, idx); | ||
errors = errors.concat(programErrors, coreFieldErrors, requestedValErrors); | ||
} | ||
|
||
return errors; | ||
}; | ||
|
||
const createValidationError = (row: number, message: string) => ({ | ||
row: row + 1, // account for tsc header row | ||
message, | ||
}); | ||
|
||
const checkProgramId = (programId: string, record: ProgramExceptionRecord, idx: number) => { | ||
if (programId !== record.program_name) { | ||
return [ | ||
createValidationError( | ||
idx, | ||
`submitted program id of ${programId} does not match record program id of ${record.program_name}`, | ||
), | ||
]; | ||
} | ||
return []; | ||
}; | ||
|
||
const checkCoreField = async (record: ProgramExceptionRecord, idx: number) => { | ||
const currentDictionary = await dictionaryManager.instance(); | ||
|
||
const requestedCoreField = record.requested_core_field; | ||
|
||
if (requestedCoreField === undefined) { | ||
return [createValidationError(idx, `requested_core_field field is not defined`)]; | ||
} | ||
|
||
const fieldFilter = (field: { name: string; meta?: { core: boolean } }): boolean => { | ||
return field.name === requestedCoreField && !!field.meta?.core; | ||
}; | ||
|
||
const schemaFilter = (schema: SchemaWithFields): boolean => { | ||
return schema.name === record.schema; | ||
}; | ||
|
||
const existingDictionarySchema = await currentDictionary.getSchemasWithFields( | ||
schemaFilter, | ||
fieldFilter, | ||
); | ||
|
||
if (existingDictionarySchema[0] && existingDictionarySchema[0].fields.length === 0) { | ||
return [ | ||
createValidationError(idx, `core field of ${record.requested_core_field} is not valid`), | ||
]; | ||
} | ||
|
||
return []; | ||
}; | ||
|
||
const checkRequestedValue = (record: ProgramExceptionRecord, idx: number) => { | ||
const validRequests = Object.values(ExceptionValue); | ||
const requestedExceptionValue = record.requested_exception_value; | ||
|
||
if (requestedExceptionValue === undefined) { | ||
return [createValidationError(idx, `requested_exception_value field is not defined`)]; | ||
} else if (typeof requestedExceptionValue !== 'string') { | ||
return [createValidationError(idx, `requested_exception_value is not a string`)]; | ||
} else if (!validRequests.includes(requestedExceptionValue)) { | ||
return [ | ||
createValidationError( | ||
idx, | ||
`requested_exception_value is not valid. must be one of ${validRequests.join(', ')}`, | ||
), | ||
]; | ||
} | ||
return []; | ||
}; |
Oops, something went wrong.