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

API Endpoint to Download TSV Files for donors by Donor ID #1062

Merged
merged 6 commits into from
Oct 6, 2023

Conversation

joneubank
Copy link
Member

Description of changes

This adds a new API endpoint at POST /clinical/donors/tsv which accepts in the request body an array of donor IDs. It will collect all entity data for the requested donors and return that data in a Zip file with TSVs of each clinical entity.

This endpoint is restricted to Full Read Access only, requiring service level read access.

This will be used by the UI/API for users to request Clinical Data for files/donors that they have embargo level access to. Additional authorization restrictions for this request are managed by the Platform API Gateway.

Type of Change

  • Bug
  • Refactor
  • New Feature
  • Release Candidate

Checklist before requesting review:

  • Check branch (code change PRs go to develop not master)
  • Check copyrights for new files
  • Manual testing
  • Regression tests completed and passing (double check number of tests).
  • Spelling has been checked.
  • Updated swagger docs accordingly (check it's still valid)
  • Set validationDependency in meta tag for Argo Dictionary fields used in code

Comment on lines -57 to +63
export type PaginatedClinicalQuery = ClinicalDonorEntityQuery & {
export type PaginationQuery = {
page: number;
pageSize?: number;
sort: string;
};

export type PaginatedClinicalQuery = ClinicalDonorEntityQuery & PaginationQuery;
Copy link
Member Author

Choose a reason for hiding this comment

The 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 PaginationQuery.

Comment on lines 233 to +234
const taskToRun = WorkerTasks.ExtractEntityDataFromDonors;
const taskArgs = [donors as Donor[], totalDonors, allSchemasWithFields, query];
const taskArgs = [donors as Donor[], totalDonors, allSchemasWithFields, query.entityTypes, query];
Copy link
Member Author

@joneubank joneubank Sep 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ExtractEntityDataFromDonors task was updated to require the PaginatedQuery in place of the ClinicalDonorEntityQuery since the new request by DonorId doesn't include much of that request data. The only additional information used from that query type was the list of entities to include in the request, which is now passed as a separate argument query.entityTypes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the type for query implies the property (entityTypes) is guaranteed to be present.
is it safe to assume its actual presence is validated on runtime elsewhere?

Comment on lines +167 to +168
const date = currentDateFormatted();
const fileName = `filename=Donor_Clinical_Data_${date}.zip`;
Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

@justincorrigible justincorrigible Oct 2, 2023

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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?

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.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good reason. will make the change.

} from './types';
import { loggerFor } from '../../logger';
const L = loggerFor(__filename);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could Log, or Logger do the same job, while being a bit more declarative than a single capital letter l?

Copy link
Member Author

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.


const zip = createClinicalZipFile(donorEntityData);

res.send(zip.toBuffer());
Copy link
Member

Choose a reason for hiding this comment

The 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.
obviously I must be mistaken.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

previous use of res didn't send data, just set some headers and status.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah... I was under the impression res.status sends a response.
time for me to do some reading and update my understanding.

Copy link
Member

@justincorrigible justincorrigible Oct 2, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow up: https://expressjs.com/en/api.html#res.status
looks like I've been away from coding for too long, starting to forget how to 😢
...I miss creating things

return data;
};

export const getDonorEntityData = async (donorIds: number[]) => {
const allSchemasWithFields = await dictionaryManager.instance().getSchemasWithFields();
Copy link
Member

Choose a reason for hiding this comment

The 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 ?. 😆

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

long optional chaining makes me nervous too.
I actually really like how dictionaryManager does this - throws an error if there's no instance - so you could actually just catch the error and return ... empty array of donorIds in that case?

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re ? part of this.... getSchemaWIthFields will never actually be run on undefined, an error will be thrown instead

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

welp! sounds like that requires a lot of "insider" knowledge on that dictionaryManager.

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 🤷‍♂️

Copy link
Member Author

@joneubank joneubank Oct 4, 2023

Choose a reason for hiding this comment

The 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.

Copy link
Member

@justincorrigible justincorrigible Oct 4, 2023

Choose a reason for hiding this comment

The 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!

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The 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?

@@ -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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you, using as? le wut!

Copy link
Member Author

@joneubank joneubank Oct 2, 2023

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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

Co-authored-by: Anders Richardsson <[email protected]>
const mapped = result.map(d => {
return MongooseUtils.toPojo(d) as Donor;
});
return F(mapped);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what on earth is F!?

...I know it's just the established pattern, and it's obviously a function, but what does it do!?
without having to change the whole app, at least the import could be renamed in this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrites the type as DeepReadonly<T>. Again, following the service convention, I do not recommend adopting this pattern elsewhere.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { F as Freeze } should help... but even then, what's freezing? 😮‍💨

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't update/extend properties dynamically
if I remember correctly it's a very light lib around the native Object.freeze() https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze

Copy link
Member

@justincorrigible justincorrigible left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (except for the deeper "pattern" issues pointed out in the comments, which are beyond the scope of this PR)

@joneubank joneubank merged commit 64bcd34 into develop Oct 6, 2023
@joneubank joneubank deleted the feat/donor-download-mixed-programs branch October 6, 2023 18:20
@demariadaniel demariadaniel mentioned this pull request Oct 13, 2023
11 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants