Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upload summary results #549

Merged
merged 22 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c76ad41
can get a record in the db and get a sensible response
anissa-agahchen Sep 20, 2021
40ac169
Merge branch 'dev' of https://github.com/bcgov/biohubbc into uploadSu…
anissa-agahchen Sep 21, 2021
8ac38b4
Merge branch 'dev' of https://github.com/bcgov/biohubbc into uploadSu…
anissa-agahchen Sep 22, 2021
e11c0d9
WIP
anissa-agahchen Sep 22, 2021
e317299
Merge branch 'dev' of https://github.com/bcgov/biohubbc into uploadSu…
anissa-agahchen Sep 23, 2021
7de57e3
Merge branch 'dev' of https://github.com/bcgov/biohubbc into uploadSu…
anissa-agahchen Sep 23, 2021
b983387
with error
anissa-agahchen Sep 24, 2021
2366226
upload and display of summary results is showing
anissa-agahchen Sep 24, 2021
7d8f80c
clean up
anissa-agahchen Sep 24, 2021
b109065
Merge branch 'dev' of https://github.com/bcgov/biohubbc into uploadSu…
anissa-agahchen Sep 24, 2021
013d0e0
fixes and tests
anissa-agahchen Sep 27, 2021
74fc97b
clean up
anissa-agahchen Sep 27, 2021
1559e36
fix ObservationCSV test to work with props
anissa-agahchen Sep 27, 2021
fca66b1
clean up
anissa-agahchen Sep 27, 2021
46088cd
lint-fix
anissa-agahchen Sep 27, 2021
8841ca5
update dependencies
anissa-agahchen Sep 27, 2021
b0b49f9
added tests
anissa-agahchen Sep 27, 2021
ff500ff
addressed feedback
anissa-agahchen Sep 27, 2021
69cd8df
clean up
anissa-agahchen Sep 27, 2021
fbdaacc
clean up
anissa-agahchen Sep 27, 2021
79ac585
code smell
anissa-agahchen Sep 27, 2021
930c110
Merge branch 'dev' of https://github.com/bcgov/biohubbc into uploadSu…
anissa-agahchen Sep 27, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/models/survey-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export class GetViewSurveyDetailsData {
completion_status: string;
publish_date: string;
occurrence_submission_id: number;
summary_results_submission_id: number;

constructor(surveyDetailsData?: any) {
defaultLog.debug({ label: 'GetViewSurveyDetailsData', message: 'params', surveyDetailsData });
Expand Down Expand Up @@ -72,6 +73,7 @@ export class GetViewSurveyDetailsData {

this.id = surveyDataItem?.id ?? null;
this.occurrence_submission_id = surveyDataItem?.occurrence_submission_id ?? null;
this.summary_results_submission_id = surveyDataItem?.summary_results_submission_id ?? null;
this.survey_name = surveyDataItem?.name || '';
this.survey_purpose = surveyDataItem?.objectives || '';
this.focal_species = (focalSpeciesList.length && focalSpeciesList.filter((item: string | number) => !!item)) || [];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
'use strict';

import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SYSTEM_ROLE } from '../../../../../../constants/roles';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
'use strict';

import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SYSTEM_ROLE } from '../../../../../../../constants/roles';
import { getDBConnection } from '../../../../../../../database/db';
import { HTTP400 } from '../../../../../../../errors/CustomError';
import { getLatestSurveySummarySubmissionSQL } from '../../../../../../../queries/survey/survey-summary-queries';
import { getLogger } from '../../../../../../../utils/logger';

const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/submission/get');

export const GET: Operation = [getSurveySummarySubmission()];

GET.apiDoc = {
description: 'Fetches an observation occurrence submission for a survey.',
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
tags: ['observation_submission'],
security: [
{
Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN]
}
],
parameters: [
{
in: 'path',
name: 'projectId',
schema: {
type: 'number'
},
required: true
},
{
in: 'path',
name: 'surveyId',
schema: {
type: 'number'
},
required: true
}
],
responses: {
200: {
description: 'Observation submission get response.',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
id: {
type: 'number'
},
fileName: {
description: 'The file name of the submission',
type: 'string'
},
status: {
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
description: 'The validation status of the submission',
type: 'string'
},
messages: {
description: 'The validation status messages of the submission',
type: 'array',
items: {
type: 'object',
description: 'A validation status message of the submission'
}
}
}
}
}
}
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/401'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

export function getSurveySummarySubmission(): RequestHandler {
return async (req, res) => {
defaultLog.debug({ label: 'Get a survey summary result', message: 'params', req_params: req.params });

if (!req.params.surveyId) {
throw new HTTP400('Missing required path param `surveyId`');
}

const connection = getDBConnection(req['keycloak_token']);

try {
const getSurveySummarySubmissionSQLStatement = getLatestSurveySummarySubmissionSQL(Number(req.params.surveyId));

if (!getSurveySummarySubmissionSQLStatement) {
throw new HTTP400('Failed to build SQL getLatestSurveySummarySubmissionSQLStatement statement');
}

await connection.open();

const summarySubmissionData = await connection.query(
getSurveySummarySubmissionSQLStatement.text,
getSurveySummarySubmissionSQLStatement.values
);

if (!summarySubmissionData || !summarySubmissionData.rows || !summarySubmissionData.rows[0]) {
return res.status(200).json(null);
}

await connection.commit();

const getSummarySubmissionData =
(summarySubmissionData &&
summarySubmissionData.rows &&
summarySubmissionData.rows[0] && {
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
id: summarySubmissionData.rows[0].survey_summary_submission_id,
fileName: summarySubmissionData.rows[0].file_name
}) ||
null;

return res.status(200).json(getSummarySubmissionData);
} catch (error) {
defaultLog.debug({ label: 'getSummarySubmissionData', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
'use strict';
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SYSTEM_ROLE } from '../../../../../../../constants/roles';
import { getDBConnection, IDBConnection } from '../../../../../../../database/db';
import { HTTP400 } from '../../../../../../../errors/CustomError';
import {
insertSurveySummarySubmissionSQL,
updateSurveySummarySubmissionWithKeySQL
} from '../../../../../../../queries/survey/survey-summary-queries';
import { generateS3FileKey, scanFileForVirus, uploadFileToS3 } from '../../../../../../../utils/file-utils';
import { getLogger } from '../../../../../../../utils/logger';
import { logRequest } from '../../../../../../../utils/path-utils';

const defaultLog = getLogger('/api/project/{projectId}/survey/{surveyId}/summary/upload');

export const POST: Operation = [
logRequest('paths/project/{projectId}/survey/{surveyId}/summary/upload', 'POST'),
uploadMedia()
];

POST.apiDoc = {
description: 'Upload survey results file.',
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
tags: ['results'],
security: [
{
Bearer: [SYSTEM_ROLE.SYSTEM_ADMIN, SYSTEM_ROLE.PROJECT_ADMIN]
}
],
parameters: [
{
in: 'path',
name: 'projectId',
required: true
},
{
in: 'path',
name: 'surveyId',
required: true
}
],
requestBody: {
description: 'Survey summary results file to upload',
content: {
'multipart/form-data': {
schema: {
type: 'object',
properties: {
media: {
description: 'A survey summary file.',
type: 'string',
format: 'binary'
}
}
}
}
}
},
responses: {
200: {
description: 'Upload OK'
},
400: {
$ref: '#/components/responses/400'
},
401: {
$ref: '#/components/responses/401'
},
403: {
$ref: '#/components/responses/401'
},
500: {
$ref: '#/components/responses/500'
},
default: {
$ref: '#/components/responses/default'
}
}
};

/**
* Uploads a media file to S3 and inserts a matching record in the `occurrence_submission` table.
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
*
* @return {*} {RequestHandler}
*/
export function uploadMedia(): RequestHandler {
return async (req, res) => {
const rawMediaArray: Express.Multer.File[] = req.files as Express.Multer.File[];

if (!rawMediaArray || !rawMediaArray.length) {
// no media objects included, skipping media upload step
throw new HTTP400('Missing upload data');
}

defaultLog.debug({
label: 'uploadMedia',
message: 'files',
files: rawMediaArray.map((item) => {
return { ...item, buffer: 'Too big to print' };
})
});

if (rawMediaArray.length !== 1) {
// no media objects included
throw new HTTP400('Too many files uploaded, expected 1');
}

if (!req.params.projectId) {
throw new HTTP400('Missing required path param: projectId');
}

if (!req.params.surveyId) {
throw new HTTP400('Missing required path param: surveyId');
}

const connection = getDBConnection(req['keycloak_token']);

try {
const rawMediaFile = rawMediaArray[0];

await connection.open();

// Scan file for viruses using ClamAV
const virusScanResult = await scanFileForVirus(rawMediaFile);

if (!virusScanResult) {
throw new HTTP400('Malicious content detected, upload cancelled');
}

const response = await insertSurveySummarySubmission(
Number(req.params.surveyId),
'BioHub',
rawMediaFile.originalname,
connection
);

const summarySubmissionId = response.rows[0].id;

const key = generateS3FileKey({
projectId: Number(req.params.projectId),
surveyId: Number(req.params.surveyId),
folder: `summaryresults/${summarySubmissionId}`,
fileName: rawMediaFile.originalname
});

await updateSurveySummarySubmissionWithKey(summarySubmissionId, key, connection);

await connection.commit();

const metadata = {
filename: rawMediaFile.originalname,
username: (req['auth_payload'] && req['auth_payload'].preferred_username) || '',
email: (req['auth_payload'] && req['auth_payload'].email) || ''
};

await uploadFileToS3(rawMediaFile, key, metadata);

return res.status(200).send({ summarySubmissionId });
} catch (error) {
defaultLog.debug({ label: 'uploadMedia', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}

/**
* Inserts a new record into the `survey_summary_submission` table.
*
* @param {number} surveyId
* @param {string} source
* @param {string} key
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
* @param {IDBConnection} connection
* @return {*} {Promise<void>}
*/
export const insertSurveySummarySubmission = async (
surveyId: number,
source: string,
file_name: string,
connection: IDBConnection
): Promise<any> => {
const insertSqlStatement = insertSurveySummarySubmissionSQL(surveyId, source, file_name);

if (!insertSqlStatement) {
throw new HTTP400('Failed to build SQL insert statement');
}

const insertResponse = await connection.query(insertSqlStatement.text, insertSqlStatement.values);

if (!insertResponse || !insertResponse.rowCount) {
throw new HTTP400('Failed to insert survey summary submission record');
}

return insertResponse;
};

/**
* Update existing `survey_summary_submission` record with key.
*
* @param {number} surveyId
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
* @param {string} source
anissa-agahchen marked this conversation as resolved.
Show resolved Hide resolved
* @param {string} key
* @param {IDBConnection} connection
* @return {*} {Promise<void>}
*/
export const updateSurveySummarySubmissionWithKey = async (
submissionId: number,
key: string,
connection: IDBConnection
): Promise<any> => {
const updateSqlStatement = updateSurveySummarySubmissionWithKeySQL(submissionId, key);

if (!updateSqlStatement) {
throw new HTTP400('Failed to build SQL update statement');
}

const updateResponse = await connection.query(updateSqlStatement.text, updateSqlStatement.values);

if (!updateResponse || !updateResponse.rowCount) {
throw new HTTP400('Failed to update survey summary submission record');
}

return updateResponse;
};
Loading