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

BHBC-1868: Push Observation Data to BioHub #805

Merged
merged 10 commits into from
Jul 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12,568 changes: 12,554 additions & 14 deletions api/package-lock.json

Large diffs are not rendered by default.

6 changes: 0 additions & 6 deletions api/src/models/occurrence-create.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { getLogger } from '../utils/logger';

const defaultLog = getLogger('models/occurrence-create');

/**
* Pre-processes POST occurrences data
*
Expand All @@ -21,8 +17,6 @@ export class PostOccurrence {
eventDate: string;

constructor(obj?: any) {
defaultLog.debug({ label: 'PostOccurrence', message: 'params', obj });

this.associatedTaxa = obj?.associatedTaxa || null;
this.lifeStage = obj?.lifeStage || null;
this.sex = obj?.sex || null;
Expand Down
95 changes: 95 additions & 0 deletions api/src/paths/project/{projectId}/survey/{surveyId}/upload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { SYSTEM_ROLE } from '../../../../../constants/roles';
import { getDBConnection } from '../../../../../database/db';
import { HTTP400 } from '../../../../../errors/custom-error';
import { authorizeRequestHandler } from '../../../../../request-handlers/security/authorization';
import { PlatformService } from '../../../../../services/platform-service';
import { getLogger } from '../../../../../utils/logger';

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

export const POST: Operation = [
authorizeRequestHandler(() => {
return {
and: [
{
validSystemRoles: [SYSTEM_ROLE.SYSTEM_ADMIN],
discriminator: 'SystemRole'
}
]
};
}),
uploadSurveyDataToBioHub()
];

POST.apiDoc = {
description: 'Upload survey/observation data to BioHub.',
tags: ['survey'],
security: [
{
Bearer: []
}
],
parameters: [
{
in: 'path',
name: 'projectId',
schema: {
type: 'integer',
minimum: 1
},
required: true
},
{
in: 'path',
name: 'surveyId',
schema: {
type: 'integer',
minimum: 1
},
required: true
}
],
responses: {
200: {
description: 'Upload survey/observation data to BioHub OK.'
},
401: {
$ref: '#/components/responses/401'
},
default: {
$ref: '#/components/responses/default'
}
}
};

export function uploadSurveyDataToBioHub(): RequestHandler {
return async (req, res) => {
const projectId = Number(req.params.projectId);
const surveyId = Number(req.params.surveyId);

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

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

try {
await connection.open();

const platformService = new PlatformService(connection);
await platformService.uploadSurveyDataToBioHub(projectId, surveyId);

await connection.commit();

return res.status(200).send();
} catch (error) {
defaultLog.error({ label: 'uploadSurveyDataToBioHub', message: 'error', error });
await connection.rollback();
throw error;
} finally {
connection.release();
}
};
}
2 changes: 2 additions & 0 deletions api/src/queries/survey/survey-occurrence-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export const getLatestSurveyOccurrenceSubmissionSQL = (surveyId: number): SQLSta
os.event_timestamp,
os.input_key,
os.input_file_name,
os.output_key,
os.output_file_name,
ss.submission_status_id,
ss.submission_status_type_id,
sst.name as submission_status_type_name,
Expand Down
50 changes: 50 additions & 0 deletions api/src/services/platform-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import AdmZip from 'adm-zip';
import axios from 'axios';
import FormData from 'form-data';
import { URL } from 'url';
import { HTTP400 } from '../errors/custom-error';
import { getFileFromS3 } from '../utils/file-utils';
import { EmlService } from './eml-service';
import { KeycloakService } from './keycloak-service';
import { DBService } from './service';
import { SurveyService } from './survey-service';

export interface IDwCADataset {
archiveFile: {
Expand Down Expand Up @@ -137,4 +140,51 @@ export class PlatformService extends DBService {

return data;
}

/**
* Upload Survey/Project/Observation data to Backbone
*
* @param {number} projectId
* @param {number} surveyId
* @return {*}
* @memberof PlatformService
*/
async uploadSurveyDataToBioHub(projectId: number, surveyId: number) {
if (!this.BACKBONE_INTAKE_ENABLED) {
return;
}

const surveyService = new SurveyService(this.connection);
const surveyData = await surveyService.getLatestSurveyOccurrenceSubmission(surveyId);

if (!surveyData.output_key) {
throw new HTTP400('no s3Key found');
}
const s3File = await getFileFromS3(surveyData.output_key);

if (!s3File) {
throw new HTTP400('no s3File found');
}
const dwcArchiveZip = new AdmZip(s3File.Body as Buffer);

const emlService = new EmlService({ projectId: projectId }, this.connection);
const emlString = await emlService.buildProjectEml();

if (!emlString) {
throw new HTTP400('emlString failed to build');
}

dwcArchiveZip.addFile('eml.xml', Buffer.from(emlString));

const dwCADataset = {
archiveFile: {
data: dwcArchiveZip.toBuffer(),
fileName: 'DwCA.zip',
mimeType: 'application/zip'
},
dataPackageId: emlService.packageId
};

return this._submitDwCADatasetToBioHubBackbone(dwCADataset);
}
}
22 changes: 22 additions & 0 deletions api/src/services/survey-service.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import chai, { expect } from 'chai';
import { describe } from 'mocha';
import { QueryResult } from 'pg';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import { PutSurveyObject } from '../models/survey-update';
Expand Down Expand Up @@ -82,4 +83,25 @@ describe('SurveyService', () => {
expect(updateSurveyProprietorDataStub).to.have.been.calledOnce;
});
});

describe('getLatestSurveyOccurrenceSubmission', () => {
afterEach(() => {
sinon.restore();
});

it('Gets latest survey submission', async () => {
const mockRowObj = { id: 123 };
const mockQueryResponse = ({ rows: [mockRowObj] } as unknown) as QueryResult<any>;

const mockDBConnection = getMockDBConnection({ query: async () => mockQueryResponse });

const surveyId = 1;

const surveyService = new SurveyService(mockDBConnection);

const response = await surveyService.getLatestSurveyOccurrenceSubmission(surveyId);

expect(response).to.eql({ id: 123 });
});
});
});
19 changes: 19 additions & 0 deletions api/src/services/survey-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,25 @@ export class SurveyService extends DBService {
return (response && response.rows?.[0]) || null;
}

/**
* Get latest survey data submission from id
*
* @param {number} surveyId
* @return {*}
* @memberof SurveyService
*/
async getLatestSurveyOccurrenceSubmission(surveyId: number) {
const sqlStatement = queries.survey.getLatestSurveyOccurrenceSubmissionSQL(surveyId);

if (!sqlStatement) {
throw new ApiGeneralError('Failed to build SQL get statement');
}

const response = await this.connection.query(sqlStatement.text, sqlStatement.values);

return (response && response.rows?.[0]) || null;
}

async getSummaryResultId(surveyId: number) {
const sqlStatement = queries.survey.getLatestSummaryResultIdSQL(surveyId);

Expand Down
Loading