Skip to content

Commit

Permalink
SIMSBIOHUB-606: Upload Capture Attachments (#1388)
Browse files Browse the repository at this point in the history
- Upload capture attachments
- Upload capture attachments and remove during edit
- Download attachments
  • Loading branch information
MacQSL authored Oct 7, 2024
1 parent 9748a1d commit 90de834
Show file tree
Hide file tree
Showing 33 changed files with 2,281 additions and 129 deletions.
17 changes: 17 additions & 0 deletions api/src/constants/attachments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,27 @@ export enum ATTACHMENT_TYPE {
export enum TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE {
/**
* Lotek API key file type.
*
* @export
* @enum {string}
*/
KEYX = 'KeyX',
/**
* Vectronic API key file type.
*/
CFG = 'Cfg'
}

export enum CRITTER_CAPTURE_ATTACHMENT_TYPE {
/**
* Critter Capture Attachment file type.
*
* Note: This will not be used as the attachment type on the record.
* But used to identify which service to get the S3 key from in the endpoint.
*
* @export
* @enum {string}
*/
CAPTURE = 'Capture',
MORTALITY = 'Mortality'
}
48 changes: 48 additions & 0 deletions api/src/database-models/critter_capture_attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from 'zod';
/**
* Note: These files should only contain the `Data Models` and `Data Records` with equivalent inferred types.
*
* Data Models contain a 1 to 1 mapping of the database table.
*
* Data Records contain a 1 to 1 mapping of the database table, minus the audit columns.
*/

/**
* Critter Capture Attachment Model.
*
* @description Data model for `critter_capture_attachment`.
*/
export const CritterCaptureAttachmentModel = z.object({
critter_capture_attachment_id: z.number(),
uuid: z.string().nullable(),
critter_id: z.number(),
critterbase_capture_id: z.string(),
file_type: z.string(),
file_name: z.string().nullable(),
file_size: z.number().nullable(),
title: z.string().nullable(),
description: z.string().nullable(),
key: z.string(),
create_date: z.string(),
create_user: z.number(),
update_date: z.string().nullable(),
update_user: z.number().nullable(),
revision_count: z.number()
});

export type CritterCaptureAttachmentModel = z.infer<typeof CritterCaptureAttachmentModel>;

/**
* Critter Capture Attachment Record.
*
* @description Data record for `critter_capture_attachment`.
*/
export const CritterCaptureAttachmentRecord = CritterCaptureAttachmentModel.omit({
create_date: true,
create_user: true,
update_date: true,
update_user: true,
revision_count: true
});

export type CritterCaptureAttachmentRecord = z.infer<typeof CritterCaptureAttachmentRecord>;
48 changes: 48 additions & 0 deletions api/src/database-models/critter_mortality_attachment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { z } from 'zod';
/**
* Note: These files should only contain the `Data Models` and `Data Records` with equivalent inferred types.
*
* Data Models contain a 1 to 1 mapping of the database table.
*
* Data Records contain a 1 to 1 mapping of the database table, minus the audit columns.
*/

/**
* Critter Mortality Attachment Model.
*
* @description Data model for `critter_mortality_attachment`.
*/
export const CritterMortalityAttachmentModel = z.object({
critter_mortality_attachment_id: z.number(),
uuid: z.string().nullable(),
critter_id: z.string(),
critterbase_mortality_id: z.string(),
file_type: z.string(),
file_name: z.string().nullable(),
file_size: z.number().nullable(),
title: z.string().nullable(),
description: z.string().nullable(),
key: z.string(),
create_date: z.string(),
create_user: z.number(),
update_date: z.string().nullable(),
update_user: z.number().nullable(),
revision_count: z.number()
});

export type CritterMortalityAttachmentModel = z.infer<typeof CritterMortalityAttachmentModel>;

/**
* Critter Mortality Attachment Record.
*
* @description Data record for `critter_mortality_attachment`.
*/
export const CritterMortalityAttachmentRecord = CritterMortalityAttachmentModel.omit({
create_date: true,
create_user: true,
update_date: true,
update_user: true,
revision_count: true
});

export type CritterMortalityAttachmentRecord = z.infer<typeof CritterMortalityAttachmentRecord>;
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { RequestHandler } from 'express';
import { Operation } from 'express-openapi';
import { ATTACHMENT_TYPE, TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE } from '../../../../../../../constants/attachments';
import {
ATTACHMENT_TYPE,
CRITTER_CAPTURE_ATTACHMENT_TYPE,
TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE
} from '../../../../../../../constants/attachments';
import { PROJECT_PERMISSION, SYSTEM_ROLE } from '../../../../../../../constants/roles';
import { getDBConnection } from '../../../../../../../database/db';
import { authorizeRequestHandler } from '../../../../../../../request-handlers/security/authorization';
import { AttachmentService } from '../../../../../../../services/attachment-service';
import { CritterAttachmentService } from '../../../../../../../services/critter-attachment-service';
import { getS3SignedURL } from '../../../../../../../utils/file-utils';
import { getLogger } from '../../../../../../../utils/logger';

Expand Down Expand Up @@ -74,7 +79,14 @@ GET.apiDoc = {
name: 'attachmentType',
schema: {
type: 'string',
enum: ['Report', 'KeyX', 'Cfg', 'Other']
enum: [
ATTACHMENT_TYPE.REPORT,
ATTACHMENT_TYPE.OTHER,
TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.CFG,
TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.KEYX,
CRITTER_CAPTURE_ATTACHMENT_TYPE.CAPTURE,
CRITTER_CAPTURE_ATTACHMENT_TYPE.MORTALITY
]
},
required: true
}
Expand Down Expand Up @@ -118,6 +130,10 @@ export function getSurveyAttachmentSignedURL(): RequestHandler {
req_body: req.body
});

const surveyId = Number(req.params.surveyId);
const attachmentId = Number(req.params.attachmentId);
const attachmentType = req.query.attachmentType;

const connection = getDBConnection(req.keycloak_token);

try {
Expand All @@ -126,26 +142,25 @@ export function getSurveyAttachmentSignedURL(): RequestHandler {
let s3Key;

const attachmentService = new AttachmentService(connection);
const critterAttachmentService = new CritterAttachmentService(connection);

if (req.query.attachmentType === ATTACHMENT_TYPE.REPORT) {
s3Key = await attachmentService.getSurveyReportAttachmentS3Key(
Number(req.params.surveyId),
Number(req.params.attachmentId)
);
} else if (
req.query.attachmentType === TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.KEYX ||
req.query.attachmentType === TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.CFG
) {
s3Key = await attachmentService.getSurveyTelemetryCredentialAttachmentS3Key(
Number(req.params.surveyId),
Number(req.params.attachmentId)
);
} else {
s3Key = await attachmentService.getSurveyAttachmentS3Key(
Number(req.params.surveyId),
Number(req.params.attachmentId)
);
switch (attachmentType) {
case CRITTER_CAPTURE_ATTACHMENT_TYPE.CAPTURE:
s3Key = await critterAttachmentService.getCritterCaptureAttachmentS3Key(surveyId, attachmentId);
break;
case ATTACHMENT_TYPE.REPORT:
s3Key = await attachmentService.getSurveyReportAttachmentS3Key(surveyId, attachmentId);
break;
case TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.KEYX:
s3Key = await attachmentService.getSurveyTelemetryCredentialAttachmentS3Key(surveyId, attachmentId);
break;
case TELEMETRY_CREDENTIAL_ATTACHMENT_TYPE.CFG:
s3Key = await attachmentService.getSurveyTelemetryCredentialAttachmentS3Key(surveyId, attachmentId);
break;
default:
s3Key = await attachmentService.getSurveyAttachmentS3Key(surveyId, attachmentId);
}

await connection.commit();

const s3SignedUrl = await getS3SignedURL(s3Key);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { expect } from 'chai';
import sinon from 'sinon';
import { deleteCritterCaptureAttachments } from '.';
import * as db from '../../../../../../../../../../database/db';
import { CritterAttachmentService } from '../../../../../../../../../../services/critter-attachment-service';
import * as S3 from '../../../../../../../../../../utils/file-utils';
import { getMockDBConnection, getRequestHandlerMocks } from '../../../../../../../../../../__mocks__/db';

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

it('deletes all attachments for a critter capture', async () => {
const mockDBConnection = getMockDBConnection({
open: sinon.stub(),
commit: sinon.stub(),
release: sinon.stub(),
rollback: sinon.stub()
});

const getDBConnectionStub = sinon.stub(db, 'getDBConnection').returns(mockDBConnection);
const mockFindAttachments = sinon.stub(CritterAttachmentService.prototype, 'findAllCritterCaptureAttachments');
const mockDeleteAttachments = sinon.stub(CritterAttachmentService.prototype, 'deleteCritterCaptureAttachments');
const mockBulkDeleteFilesFromS3 = sinon.stub(S3, 'bulkDeleteFilesFromS3');

mockFindAttachments.resolves([{ critter_capture_attachment_id: 1, key: 'DELETE_S3_KEY' }] as any[]);
mockDeleteAttachments.resolves(['DELETE_S3_KEY']);

const { mockReq, mockRes, mockNext } = getRequestHandlerMocks();

mockReq.params = {
projectId: '1',
surveyId: '2',
critterId: '3',
critterbaseCaptureId: '123e4567-e89b-12d3-a456-426614174000'
};

const requestHandler = deleteCritterCaptureAttachments();

await requestHandler(mockReq, mockRes, mockNext);

expect(getDBConnectionStub).to.have.been.calledOnce;
expect(mockDBConnection.open).to.have.been.calledOnce;

expect(mockFindAttachments).to.have.been.calledOnceWithExactly(2, '123e4567-e89b-12d3-a456-426614174000');
expect(mockDeleteAttachments).to.have.been.calledOnceWithExactly(2, [1]);

expect(mockBulkDeleteFilesFromS3).to.have.been.calledOnceWithExactly(['DELETE_S3_KEY']);

expect(mockRes.status).to.have.been.calledWith(200);
expect(mockRes.send).to.have.been.calledWith();

expect(mockDBConnection.commit).to.have.been.calledOnce;
expect(mockDBConnection.release).to.have.been.calledOnce;
expect(mockDBConnection.rollback).to.not.have.been.called;
});
});
Loading

0 comments on commit 90de834

Please sign in to comment.