-
Notifications
You must be signed in to change notification settings - Fork 0
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
API Endpoint to Download TSV Files for donors by Donor ID #1062
Changes from 4 commits
51f1376
f12255b
155bd74
fd25c45
53552af
da8f597
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,7 +36,10 @@ import { | |
ClinicalErrorsApiBody, | ||
ClinicalSearchApiBody, | ||
CompletionState, | ||
DonorDataApiBody, | ||
} from './types'; | ||
import { loggerFor } from '../../logger'; | ||
const L = loggerFor(__filename); | ||
|
||
// TODO: Update value type to match mongo schema | ||
export const completionFilters: Record<CompletionState, any> = { | ||
|
@@ -140,6 +143,40 @@ class ClinicalController { | |
res.send(zip.toBuffer()); | ||
} | ||
|
||
/** | ||
* Download Clinical Data selected by Donor ID. | ||
* Requires Full Read Access. | ||
* Used by API for a user to download clinical data for specific donors and/or files. | ||
* @param req | ||
* @param res | ||
* @returns | ||
*/ | ||
@HasFullReadAccess() | ||
async getDonorDataByIdAsTsvsInZip(req: Request, res: Response) { | ||
const bodyParseResult = DonorDataApiBody.safeParse(req.body); | ||
if (!bodyParseResult.success) { | ||
return ControllerUtils.badRequest( | ||
res, | ||
`Invalid data in request body: ${JSON.stringify(bodyParseResult.error)}`, | ||
); | ||
} | ||
const { donorIds } = bodyParseResult.data; | ||
|
||
const donorEntityData = await service.getDonorEntityData(donorIds); | ||
|
||
const date = currentDateFormatted(); | ||
const fileName = `filename=Donor_Clinical_Data_${date}.zip`; | ||
Comment on lines
+167
to
+168
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is sharing the naming convention used for for downloads requested by program submitters. I'm open to suggestions to improve the provided filename. Perhaps a timestamp is a good idea in addition to the current date. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. aside from the filename format itself, would it make sense to abstract this into an env variable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't see a reason to have different filenames per env, what's your reason? If you want filename constanta centralized into a constants file I can do that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tbh, I'm just used to abstracting constants like this one, because that makes it easier for these values to be updated, without having to rebuild the code "just for a string change". If you believe this doesn't need to be abstracted, I of course trust your insight. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good reason. will make the change. |
||
res | ||
.status(200) | ||
.contentType('application/zip') | ||
.attachment(fileName) | ||
.setHeader('content-disposition', fileName); | ||
|
||
const zip = createClinicalZipFile(donorEntityData); | ||
|
||
res.send(zip.toBuffer()); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. funny, I'd have imagined you'd get an error/warning here saying that you had already sent a response in line 170. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. previous use of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah... I was under the impression There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. follow up: https://expressjs.com/en/api.html#res.status |
||
} | ||
|
||
@HasProgramReadAccess((req: Request) => req.params.programId) | ||
async getProgramClinicalEntityData(req: Request, res: Response) { | ||
const programShortName: string = req.params.programId; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,6 +28,7 @@ import { | |
ClinicalErrorsResponseRecord, | ||
EntityAlias, | ||
aliasEntityNames, | ||
allEntityNames, | ||
} from '../common-model/entities'; | ||
import { Errors } from '../utils'; | ||
import { patchCoreCompletionWithOverride } from '../submission/submission-to-clinical/stat-calculator'; | ||
|
@@ -53,13 +54,14 @@ export type ClinicalDonorEntityQuery = { | |
completionState?: {}; | ||
}; | ||
|
||
// Returns paginated Donor + Entity Submission Data | ||
export type PaginatedClinicalQuery = ClinicalDonorEntityQuery & { | ||
export type PaginationQuery = { | ||
page: number; | ||
pageSize?: number; | ||
sort: string; | ||
}; | ||
|
||
export type PaginatedClinicalQuery = ClinicalDonorEntityQuery & PaginationQuery; | ||
Comment on lines
-57
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We had a need to separate the a worker thread task to accept Pagination parameters separate from the Clinical Donor Entity query data, so we can separate out that data for reference to its own type |
||
|
||
// GQL Query Arguments | ||
// Submitted Data Search Bar | ||
export type ClinicalSearchVariables = { | ||
|
@@ -122,6 +124,10 @@ export async function getDonors(programId: string) { | |
return await donorDao.findBy({}, 999); | ||
} | ||
|
||
export async function getDonorsByIds(donorIds: number[]) { | ||
return await donorDao.findByDonorIds(donorIds); | ||
joneubank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
export async function findDonorId(submitterId: string, programId: string) { | ||
const donor = await findDonorBySubmitterId(submitterId, programId); | ||
if (!donor) { | ||
|
@@ -219,27 +225,46 @@ export const getPaginatedClinicalData = async ( | |
query: PaginatedClinicalQuery, | ||
) => { | ||
if (!programId) throw new Error('Missing programId!'); | ||
const start = new Date().getTime() / 1000; | ||
|
||
const allSchemasWithFields = await dictionaryManager.instance().getSchemasWithFields(); | ||
// Get all donors + records for given entity | ||
const { donors, totalDonors } = await donorDao.findByPaginatedProgramId(programId, query); | ||
|
||
const taskToRun = WorkerTasks.ExtractEntityDataFromDonors; | ||
const taskArgs = [donors as Donor[], totalDonors, allSchemasWithFields, query]; | ||
const taskArgs = [donors as Donor[], totalDonors, allSchemasWithFields, query.entityTypes, query]; | ||
Comment on lines
233
to
+234
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see the type for query implies the property ( |
||
|
||
// Return paginated data | ||
const data = await runTaskInWorkerThread<{ clinicalEntities: ClinicalEntityData[] }>( | ||
taskToRun, | ||
taskArgs, | ||
); | ||
|
||
const end = new Date().getTime() / 1000; | ||
L.debug(`getPaginatedClinicalData took ${end - start}s`); | ||
|
||
return data; | ||
}; | ||
|
||
export const getDonorEntityData = async (donorIds: number[]) => { | ||
const allSchemasWithFields = await dictionaryManager.instance().getSchemasWithFields(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need to do anything about this one, just felt the need to acknowledge out loud how nervous it makes me to have these long chains without There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. long optional chaining makes me nervous too. that node pattern of throw/catch actual errors vs just retuning nulls/undefined is used a lot in this repo and I think it works well. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. re There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. welp! sounds like that requires a lot of "insider" knowledge on that try+catch sure does sound desirable, but I guess my non-evident point is that this line is a bit "code golf"ey, and could use being broken into separate lines 🤷♂️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't need insider knowledge... the type system informs us of what the output will be. if we can trust the type system then we're safe here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh fun! didn't realise TS (intellisense) was visible in PR reviews at Github.com must get myself that browser extension! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sarcasm acknowledged. agreed that the lack of introspection in the browser code review is limiting. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. to be fair, we could review PRs by testing them locally... like we used to back in the day. Modern tooling has become a crutch for what could be more declarative; which proves hard to keep up in the review process. How do you suggest we improve that? |
||
// Get all donors + records for given entity | ||
const donors = await getDonorsByIds(donorIds); | ||
const totalDonors = donors.length; | ||
|
||
const paginationQuery: PaginationQuery = { | ||
page: 0, | ||
sort: 'donorId', | ||
pageSize: totalDonors, | ||
}; | ||
|
||
const taskToRun = WorkerTasks.ExtractEntityDataFromDonors; | ||
const taskArgs = [donors, donors.length, allSchemasWithFields, allEntityNames, paginationQuery]; | ||
|
||
// Return paginated data | ||
const data = await runTaskInWorkerThread<{ clinicalEntities: ClinicalEntityData[] }>( | ||
taskToRun, | ||
taskArgs, | ||
); | ||
return data.clinicalEntities; | ||
joneubank marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}; | ||
|
||
export const getClinicalSearchResults = async ( | ||
programId: string, | ||
query: ClinicalDonorEntityQuery, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,6 +91,7 @@ export interface DonorRepository { | |
programId: string, | ||
query: ClinicalDonorEntityQuery, | ||
): Promise<DeepReadonly<{ donors: Donor[] }>>; | ||
findByDonorIds(donorIds: number[]): Promise<DeepReadonly<Donor[]>>; | ||
deleteByProgramId(programId: string): Promise<void>; | ||
deleteByProgramIdAndDonorIds(programId: string, donorIds: number[]): Promise<void>; | ||
findByProgramAndSubmitterId( | ||
|
@@ -392,7 +393,17 @@ export const donorDao: DonorRepository = { | |
submitterId: { $in: submitterIds }, | ||
programId: programId, | ||
}); | ||
const mapped = result.map((d: DonorDocument) => { | ||
const mapped = result.map(d => { | ||
return MongooseUtils.toPojo(d) as Donor; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you, using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Old code pattern in the db repo module, there are better TS ways to do this now using mongoose. Following convention of the file instead of investing time in a re-write of the DB management. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. screenshotted to use without context in the future (context provider is undefineed) :p |
||
}); | ||
return F(mapped); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what on earth is ...I know it's just the established pattern, and it's obviously a function, but what does it do!? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rewrites the type as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can't update/extend properties dynamically |
||
}, | ||
|
||
async findByDonorIds(donorIds: number[]): Promise<DeepReadonly<Donor[]>> { | ||
const result = await DonorModel.find({ | ||
donorId: { $in: donorIds }, | ||
}); | ||
const mapped = result.map(d => { | ||
return MongooseUtils.toPojo(d) as Donor; | ||
}); | ||
return F(mapped); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could
Log
, orLogger
do the same job, while being a bit more declarative than a single capital letterl
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes and it would be better. I'm being consistent with the existing pattern in this repo. I would - recommend a separate action to rename them all.