Skip to content

Commit

Permalink
COMS file upload and PCNS Document create
Browse files Browse the repository at this point in the history
  • Loading branch information
kyle1morel committed Jan 12, 2024
1 parent 757a5c9 commit 9378815
Show file tree
Hide file tree
Showing 14 changed files with 205 additions and 84 deletions.
4 changes: 4 additions & 0 deletions app/config/custom-environment-variables.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"frontend": {
"apiPath": "FRONTEND_APIPATH",
"coms": {
"apiPath": "FRONTEND_COMS_APIPATH",
"bucketId": "FRONTEND_COMS_BUCKETID"
},
"notificationBanner": "FRONTEND_NOTIFICATION_BANNER",
"oidc": {
"authority": "FRONTEND_OIDC_AUTHORITY",
Expand Down
33 changes: 20 additions & 13 deletions app/src/controllers/document.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,36 @@
import { addDashesToUuid, mixedQueryToArray, isTruthy } from '../components/utils';
import { documentService } from '../services';

import type { NextFunction, Request, Response } from 'express';
import type { NextFunction, Request, Response } from '../interfaces/IExpress';

const controller = {
async searchDocuments(req: Request, res: Response, next: NextFunction) {
async createDocument(
req: Request<
never,
never,
{ documentId: string; submissionId: string; filename: string; mimeType: string; length: number }
>,
res: Response,
next: NextFunction
) {
try {
const response = await documentService.getDocuments(req.query.submissionId as string);
const response = await documentService.createDocument(
req.body.documentId,
req.body.submissionId,
req.body.filename,
req.body.mimeType,
req.body.length
);
res.status(200).send(response);
} catch (e: unknown) {
next(e);
}
},

async linkDocument(req: Request, res: Response, next: NextFunction) {
async listDocuments(req: Request<{ submissionId: string }>, res: Response, next: NextFunction) {
try {
await documentService.createDocumentEntry(
req.params.submissionId,
req.params.comsId,
req.header('filename') as string,
req.header('Content-Type') as string,
parseInt(req.header('Content-Length'))
);
const response = await documentService.listDocuments(req.params.submissionId);
res.status(200).send(response);
} catch (e: unknown) {
// if fail, return error (client will rollback/hard-delete uploaded COMS file)
next(e);
}
}
Expand Down
1 change: 1 addition & 0 deletions app/src/db/migrations/20231212000000_init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ export async function up(knex: Knex): Promise<void> {
table.text('mimeType');
table.bigInteger('filesize');
stamps(knex, table);
table.unique(['documentId', 'submissionId']);
})
)

Expand Down
14 changes: 4 additions & 10 deletions app/src/db/models/document.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
import { Prisma } from '@prisma/client';
import { default as submission } from './submission';
import disconnectRelation from '../utils/disconnectRelation';

import type { IStamps } from '../../interfaces/IStamps';
import type { Document } from '../../types';

// Define types
const _document = Prisma.validator<Prisma.documentDefaultArgs>()({});
const _documentWithGraph = Prisma.validator<Prisma.documentDefaultArgs>()({
include: { submission: { include: { user: true } } }
});
const _documentWithGraph = Prisma.validator<Prisma.documentDefaultArgs>()({});

type SubmissionRelation = {
submission:
Expand All @@ -34,10 +31,8 @@ export default {
documentId: input.documentId as string,
filename: input.filename,
mimeType: input.mimeType,
filesize: input.filesize,
submission: input.submission?.submissionId
? { connect: { submissionId: input.submission.submissionId } }
: disconnectRelation
filesize: input.filesize ? BigInt(input.filesize) : null,
submission: input.submissionId ? { connect: { submissionId: input.submissionId } } : disconnectRelation
};
},

Expand All @@ -48,8 +43,7 @@ export default {
documentId: input.documentId,
filename: input.filename,
mimeType: input.mimeType,
filesize: input.filesize,
submission: submission.fromPrismaModel(input.submission),
filesize: Number(input.filesize),
submissionId: input.submissionId as string
};
}
Expand Down
2 changes: 2 additions & 0 deletions app/src/db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,6 @@ model document {
updatedBy String?
updatedAt DateTime? @db.Timestamptz(6)
submission submission? @relation(fields: [submissionId], references: [submissionId], onDelete: Cascade, map: "document_submissionid_foreign")
@@unique([documentId, submissionId], map: "document_documentid_submissionid_unique")
}
21 changes: 7 additions & 14 deletions app/src/routes/v1/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,17 @@ import express from 'express';
import { documentController } from '../../controllers';
import { requireSomeAuth } from '../../middleware/requireSomeAuth';

import type { NextFunction, Request, Response } from 'express';
import type { NextFunction, Request, Response } from '../../interfaces/IExpress';

const router = express.Router();
router.use(requireSomeAuth);

// proposed endpoints

// GET /documents/list/:submissionId
// (get list of docs for given submission)
/* response:
<array of document table responses>
*/
router.get('/documents/list/:submissionId', (req: Request, res: Response, next: NextFunction): void => {
documentController.searchDocuments(req, res, next);
router.put('/', (req: Request, res: Response, next: NextFunction): void => {
documentController.createDocument(req, res, next);
});

// PUT documents?submissionId=:submissionId&comsId=:comsId
// headers: filesize, mimetype, filename - if missing any, reject (future: use joi for validation, but not for now)
router.put('/documents', (req: Request, res: Response, next: NextFunction): void => {
documentController.linkDocument(req, res, next);
router.get('/list/:submissionId', (req: Request, res: Response, next: NextFunction): void => {
documentController.listDocuments(req, res, next);
});

export default router;
3 changes: 2 additions & 1 deletion app/src/routes/v1/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { currentUser } from '../../middleware/authentication';
import express from 'express';
import chefs from './chefs';
import document from './document';
import user from './user';

const router = express.Router();
Expand All @@ -13,8 +14,8 @@ router.get('/', (_req, res) => {
});
});

/** CHEFS Router */
router.use('/chefs', chefs);
router.use('/document', document);
router.use('/user', user);

export default router;
56 changes: 15 additions & 41 deletions app/src/services/document.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,26 @@
import axios from 'axios';
import config from 'config';

import { Prisma } from '@prisma/client';
import { v4, NIL } from 'uuid';

import prisma from '../db/dataConnection';
import { document } from '../db/models';

import type { AxiosInstance, AxiosRequestConfig } from 'axios';

// /**
// * @function comsAxios
// * Returns an Axios instance for the COMS API
// * @param {AxiosRequestConfig} options Axios request config options
// * @returns {AxiosInstance} An axios instance
// */
// // TODO: swap out formId
// function comsAxios(formId: string, options: AxiosRequestConfig = {}): AxiosInstance {
// return axios.create({
// baseURL: config.get('server.chefs.apiPath'),
// timeout: 10000,
// // TODO: swap out CHEFS API key for Pathfinder SSO user JWT
// auth: { username: formId, password: getChefsApiKey(formId) ?? '' },
// ...options
// });
// }

const service = {
/**
* @function createDocumentEntry
* "Adds" a document to a submission by linking a COMS object to the coresponding submission.
* @param comsId coms-id for an existing COMS object
* @param submissionId submissionId for the submission to be added to
* @function createDocument
* Creates a link between a submission and a previously existing object in COMS
* @param documentId COMS ID of an existing object
* @param submissionId Submission ID the document is associated with
* @param filename Original filename of the document
* @param mimeType Type of document
* @param filesize Size of document
*/
createDocumentEntry: async (
createDocument: async (
documentId: string,
submissionId: string,
comsId: string,
filename: string,
mimeType: string,
filesize: bigint
filesize: number
) => {
// client uploads file to COMS first
// the client then gives us the coms-id (that + JWT is all we need to access the file)

// Create record in PCNS DB
await prisma.document.create({
data: {
documentId: comsId,
documentId: documentId,
submissionId: submissionId,
filename: filename,
mimeType: mimeType,
Expand All @@ -56,12 +30,12 @@ const service = {
},

/**
* @function getDocuments
* @function listDocuments
* Retrieve a list of documents associated with a given submission
* @param submissionId PCNS submissionId
* @returns list of documents associated associated with the submission
* @param submissionId PCNS Submission ID
* @returns Array of documents associated with the submission
*/
getDocuments: async (submissionId: string) => {
listDocuments: async (submissionId: string) => {
const response = await prisma.document.findMany({
where: {
submissionId: submissionId
Expand Down
6 changes: 2 additions & 4 deletions app/src/types/Document.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { IStamps } from '../interfaces/IStamps';
import { ChefsSubmissionForm } from './ChefsSubmissionForm';

export type Document = {
documentId?: string; // Primary Key
documentId: string; // Primary Key
submissionId: string;
filename: string | null;
mimeType: string | null;
filesize: bigint | null;
submission: ChefsSubmissionForm | null;
filesize: number | null;
} & Partial<IStamps>;
58 changes: 58 additions & 0 deletions frontend/src/services/comsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { comsAxios } from './interceptors';
import { setDispositionHeader } from '@/utils/utils';

import type { AxiosRequestConfig } from 'axios';

const PATH = '/object';

export default {
/**
* @function createObject
* Post an object
* @param {any} object Object to be created
* @param {string} bucketId Bucket id containing the object
* @param {AxiosRequestConfig} axiosOptions Axios request config options
* @returns {Promise} An axios response
*/
async createObject(
object: any,
headers: {
metadata?: Array<{ key: string; value: string }>;
},
params: {
bucketId?: string;
tagset?: Array<{ key: string; value: string }>;
},
axiosOptions?: AxiosRequestConfig
) {
// setDispositionHeader constructs header based on file name
// Content-Type defaults octet-stream if MIME type unavailable
const config = {
headers: {
'Content-Disposition': setDispositionHeader(object.name),
'Content-Type': object?.type ?? 'application/octet-stream'
},
params: {
bucketId: params.bucketId,
tagset: {}
}
};

// Map the metadata if required
if (headers.metadata) {
config.headers = {
...config.headers,
...Object.fromEntries(headers.metadata.map((x: { key: string; value: string }) => [x.key, x.value]))
};
}

// Map the tagset if required
if (params.tagset) {
config.params.tagset = Object.fromEntries(
params.tagset.map((x: { key: string; value: string }) => [x.key, x.value])
);
}

return comsAxios(axiosOptions).put(PATH, object, config);
}
};
40 changes: 40 additions & 0 deletions frontend/src/services/documentService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import comsService from './comsService';
import { appAxios } from './interceptors';

const PATH = '/document';

export default {
/**
* @function createDocument
* @returns {Promise} An axios response
*/
async createDocument(file: File, submissionId: string, bucketId: string) {
let comsResponse;
try {
comsResponse = await comsService.createObject(
file,
{},
{ bucketId },
{ timeout: 0 } // Infinite timeout for big files upload to avoid timeout error
);

return appAxios().put(PATH, {
submissionId: submissionId,
documentId: comsResponse.data.id,
filename: comsResponse.data.name,
mimeType: comsResponse.data.mimeType,
length: comsResponse.data.length
});
} catch (e) {
// TODO: Delete object if Prisma write fails
}
},

async listDocuments(submissionId: string) {
try {
return appAxios().get(`${PATH}/list/${submissionId}`);
} catch (e) {
console.log(e);

Check warning on line 37 in frontend/src/services/documentService.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (16.x)

Unexpected console statement

Check warning on line 37 in frontend/src/services/documentService.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (18.x)

Unexpected console statement

Check warning on line 37 in frontend/src/services/documentService.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (20.x)

Unexpected console statement

Check warning on line 37 in frontend/src/services/documentService.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (16.x)

Unexpected console statement

Check warning on line 37 in frontend/src/services/documentService.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (18.x)

Unexpected console statement

Check warning on line 37 in frontend/src/services/documentService.ts

View workflow job for this annotation

GitHub Actions / Unit Tests (Frontend) (20.x)

Unexpected console statement
}
}
};
2 changes: 2 additions & 0 deletions frontend/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export { default as AuthService } from './authService';
export { default as ConfigService } from './configService';
export { default as chefsService } from './chefsService';
export { default as comsService } from './comsService';
export { default as documentService } from './documentService';
export { default as userService } from './userService';
Loading

0 comments on commit 9378815

Please sign in to comment.