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

#758 Mark submitted file errors #762

Merged
merged 57 commits into from May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
33dab55
Stub out Clinical Data route
demariadaniel Apr 6, 2022
b05006d
Align Routes
demariadaniel Apr 6, 2022
b980fa4
Typo Fixes
demariadaniel Apr 8, 2022
409a7fc
Update .json method
demariadaniel Apr 8, 2022
6b761a0
Refactor clinicalInfoRecords
demariadaniel Apr 8, 2022
62b8c76
Pagination Update
demariadaniel Apr 12, 2022
bc305a3
Update Swagger
demariadaniel Apr 13, 2022
517598c
Match Response to Clinical API
demariadaniel Apr 19, 2022
6d014cf
Simplify res.json
demariadaniel Apr 20, 2022
85e06d1
Update Swagger
demariadaniel Apr 22, 2022
209ab5d
Typo Fix
demariadaniel Apr 22, 2022
91098c1
Add Schema + Completion Stats
demariadaniel Apr 22, 2022
ef7aa59
Revert "Update Swagger"
demariadaniel Apr 22, 2022
b17d2cd
Update CompletionStats & SchemaMeta Data
demariadaniel Apr 25, 2022
9373bb9
Remove SchemaMetadata, Add Notes
demariadaniel Apr 26, 2022
702602d
Move Comment (Fixes TS Lint)
demariadaniel Apr 27, 2022
8088a6a
Stub Out New Error Service Method
demariadaniel Apr 27, 2022
4993f43
Update Service & API
demariadaniel Apr 28, 2022
daa64a8
Remove leftover 'query' object
demariadaniel Apr 28, 2022
1bfa554
Working String Sort
demariadaniel Apr 28, 2022
98b5ffa
Working EntityTypes Projection
demariadaniel Apr 28, 2022
fe96e2a
Updated + Working Pagination
demariadaniel Apr 28, 2022
6816618
Basic Service Setup
demariadaniel Apr 29, 2022
366eb40
Map DonorID onto records
demariadaniel Apr 29, 2022
937b796
Minor Updates
demariadaniel Apr 29, 2022
31df4cc
Type Additions
demariadaniel Apr 29, 2022
f8500e6
Working Latest Migration
demariadaniel Apr 29, 2022
8a95e47
Working Migration Errors
demariadaniel Apr 29, 2022
f5d0c2a
Update with working Clinical Errors
demariadaniel May 2, 2022
33c7ef8
Fix TS Lint
demariadaniel May 2, 2022
1627cc1
Merge branch 'develop' of https://github.com/icgc-argo/argo-clinical …
demariadaniel May 2, 2022
3d05eee
Swagger Update
demariadaniel May 2, 2022
99ae09f
Revert changes to findByProgramId
demariadaniel May 2, 2022
8e58047
A few reversions
demariadaniel May 3, 2022
6aa7af3
Change Route Handler Name, Clean Up Errors
demariadaniel May 3, 2022
67a7f57
Fix findByProgramId
demariadaniel May 3, 2022
81c493b
Fix -- Down to 1 integration error left
demariadaniel May 3, 2022
a85ec46
Working Refactor
demariadaniel May 3, 2022
d2b6cb1
New Endpoint for Clinical Errors
demariadaniel May 4, 2022
bab114c
Remove console statement
demariadaniel May 4, 2022
30ea5cc
Updated Query
demariadaniel May 4, 2022
5ad5faa
Working Filters for DonorID / SubmitterID
demariadaniel May 5, 2022
a92cf06
Working Completion Filter
demariadaniel May 6, 2022
47a17da
Fix Enum, Filters actually working now
demariadaniel May 6, 2022
2f6a0f2
Working DonorId Filter
demariadaniel May 9, 2022
3ccab19
Updated Swagger for Clinical Data
demariadaniel May 10, 2022
dbad2a5
Added Swagger for Errors Endpoint
demariadaniel May 10, 2022
4f8398c
Use DonorIdField key
demariadaniel May 10, 2022
c6cf16f
Updated Constants
demariadaniel May 10, 2022
b700f59
Rename to getPaginatedClinicalData
demariadaniel May 10, 2022
3ddc43b
Remove Extract function from Worker Threads
demariadaniel May 10, 2022
26c0301
Fix Logger TS Errors
demariadaniel May 10, 2022
e17d42a
Fix Integration Tests - Move SampleReg Back
demariadaniel May 11, 2022
b7db022
Type ClinicalQuery, Small Fixes
demariadaniel May 11, 2022
d53d85b
Remove Logging
demariadaniel May 11, 2022
54ec646
First New Integration Test
demariadaniel May 11, 2022
37f4cd9
Basic integration test for Errors
demariadaniel May 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7,814 changes: 7,696 additions & 118 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -91,6 +91,7 @@
},
"dependencies": {
"@overturebio-stack/lectern-client": "1.3.0",
"@types/mongoose-paginate-v2": "^1.3.11",
"adm-zip": "^0.4.16",
"async": "^3.0.1",
"bcrypt-nodejs": "^0.0.3",
Expand All @@ -107,14 +108,15 @@
"express-flash": "0.0.2",
"express-session": "^1.16.2",
"express-validator": "^6.14.0",
"migrate-mongo": "^8.2.3",
"ini": ">=1.3.6",
"jsonwebtoken": "^8.5.1",
"kafkajs": "^1.12.0",
"kind-of": "^6.0.3",
"lodash": "^4.17.21",
"migrate-mongo": "^8.2.3",
"mock-http-server": "^1.4.1",
"mongoose": "^5.13.14",
"mongoose-paginate-v2": "^1.6.3",
"mongoose-sequence": "^5.2.2",
"morgan": "^1.10.0",
"mquery": ">=3.2.3",
Expand Down
82 changes: 79 additions & 3 deletions src/clinical/clinical-api.ts
Expand Up @@ -31,6 +31,31 @@ import { Donor } from './clinical-entities';
import { omit } from 'lodash';
import { DeepReadonly } from 'deep-freeze';

export type ClinicalQuery = {
demariadaniel marked this conversation as resolved.
Show resolved Hide resolved
programShortName: string;
page: number;
limit: number;
demariadaniel marked this conversation as resolved.
Show resolved Hide resolved
entityTypes: string[];
sort?: string;
donorIds?: number[];
submitterDonorIds?: string[];
completionState?: {};
};

enum CompletionStates {
all = 'all',
invalid = 'invalid',
complete = 'complete',
incomplete = 'incomplete',
}

const completionFilters = {
invalid: { 'schemaMetadata.isValid': false },
complete: { 'completionStats.coreCompletionPercentage': 100 },
incomplete: { 'completionStats.coreCompletionPercentage': 0 },
all: {},
};

class ClinicalController {
@HasFullReadAccess()
async findDonors(req: Request, res: Response) {
Expand Down Expand Up @@ -61,6 +86,57 @@ class ClinicalController {
res.send(zip.toBuffer());
}

@HasProgramReadAccess((req: Request) => req.params.programId)
demariadaniel marked this conversation as resolved.
Show resolved Hide resolved
async getProgramClinicalEntityData(req: Request, res: Response) {
const programId: string = req.params.programId;
const sort: string = req.query.sort || 'donorId';
const state: CompletionStates = req.query.completionState || CompletionStates.all;
const entityTypes: string[] =
req.query.entityTypes && req.query.entityTypes.length > 0
? req.query.entityTypes.split(',')
: [''];
const completionState: {} = completionFilters[state] || {};
const donorIds =
req.query.donorIds && req.query.donorIds.length > 0
? { donorId: { $in: req.query.donorIds.split(',') } }
: '';

const submitterDonorIds =
req.query.submitterDonorIds && req.query.submitterDonorIds.length > 0
? { submitterId: { $in: req.query.submitterDonorIds.split(',') } }
: '';

const query: ClinicalQuery = {
...req.query,
sort,
entityTypes,
donorIds,
submitterDonorIds,
completionState,
};

if (!programId) {
return ControllerUtils.badRequest(res, 'Invalid programId provided');
demariadaniel marked this conversation as resolved.
Show resolved Hide resolved
}

const entityData = await service.getPaginatedClinicalData(programId, query);

res.status(200).json(entityData);
}

@HasProgramReadAccess((req: Request) => req.params.programId)
async getProgramClinicalErrors(req: Request, res: Response) {
const programId = req.params.programId;
const query = req.query.donorIds && req.query.donorIds.split(',');
if (!programId) {
return ControllerUtils.badRequest(res, 'Invalid programId provided');
}

const clinicalErrors = await service.getClinicalEntityMigrationErrors(programId, query);

res.status(200).json(clinicalErrors);
}

/**
* Fetches data for a single clinical entity type and returns the values as a TSV. This is returned in the body of the request, not as a downloadable file.
* @param req
Expand Down Expand Up @@ -131,13 +207,13 @@ class ClinicalController {

const coreCompletionOverride = req.body.coreCompletionOverride || {};

const upadtedDonor = await service.updateDonorStats(donorId, coreCompletionOverride);
const updatedDonor = await service.updateDonorStats(donorId, coreCompletionOverride);

if (!upadtedDonor) {
if (!updatedDonor) {
return ControllerUtils.notFound(res, `Donor with donorId:${donorId} not found`);
}

return res.status(200).send(upadtedDonor);
return res.status(200).send(updatedDonor);
}

@HasProgramReadAccess((req: Request) => req.params.programId)
Expand Down
6 changes: 6 additions & 0 deletions src/clinical/clinical-entities.ts
Expand Up @@ -113,6 +113,12 @@ export interface ClinicalInfo {
[field: string]: string | number | boolean | string[] | number[] | boolean[] | undefined;
}

export type ClinicalEntityData = {
entityName: string;
records: ClinicalEntity[];
entityFields: any;
};

export type DonorMap = Readonly<{ [submitterId: string]: Donor }>;

export type DonorBySubmitterIdMap = { [k: string]: DeepReadonly<Donor> };
Expand Down
69 changes: 64 additions & 5 deletions src/clinical/clinical-service.ts
Expand Up @@ -19,13 +19,20 @@

import { donorDao, DONOR_DOCUMENT_FIELDS } from './donor-repo';
import { Errors } from '../utils';
import { Sample, Donor } from './clinical-entities';
import { Sample, Donor, ClinicalEntityData } from './clinical-entities';
import { ClinicalQuery } from './clinical-api';
import { DeepReadonly } from 'deep-freeze';
import _ from 'lodash';
import { forceRecalcDonorCoreEntityStats } from '../submission/submission-to-clinical/stat-calculator';
import { migrationRepo } from '../submission/migration/migration-repo';
import {
DictionaryMigration,
DonorMigrationError,
DonorMigrationErrorRecord,
} from '../submission/migration/migration-entities';
import * as dictionaryManager from '../dictionary/manager';
import { loggerFor } from '../logger';
import { WorkerTasks } from './service-worker-thread/tasks';
import { WorkerTasks, extractEntityDataFromDonors } from './service-worker-thread/tasks';
import { runTaskInWorkerThread } from './service-worker-thread/runner';

const L = loggerFor(__filename);
Expand Down Expand Up @@ -163,12 +170,64 @@ export const getClinicalData = async (programId: string) => {

const taskToRun = WorkerTasks.ExtractDataFromDonors;
const taskArgs = [donors, allSchemasWithFields];
const data = await runTaskInWorkerThread<
{ entityName: string; records: unknown; entityFields: any }[]
>(taskToRun, taskArgs);
const data = await runTaskInWorkerThread<ClinicalEntityData[]>(taskToRun, taskArgs);

const end = new Date().getTime() / 1000;
L.debug(`getClinicalData took ${end - start}s`);

return data;
};

export const getPaginatedClinicalData = async (programId: string, query: ClinicalQuery) => {
if (!programId) throw new Error('Missing programId!');
const start = new Date().getTime() / 1000;

const allSchemasWithFields = await dictionaryManager.instance().getSchemasWithFields();

const donors = await donorDao.findByPaginatedProgramId(programId, query);

const data = extractEntityDataFromDonors(donors as Donor[], allSchemasWithFields);

const end = new Date().getTime() / 1000;
L.debug(`getPaginatedClinicalData took ${end - start}s`);

return data;
};

interface DonorMigration extends Omit<DictionaryMigration, 'invalidDonorsErrors '> {
invalidDonorsErrors: DonorMigrationError[];
}

export const getClinicalEntityMigrationErrors = async (programId: string, query: string[]) => {
if (!programId) throw new Error('Missing programId!');
const start = new Date().getTime() / 1000;

const migration: DeepReadonly<
DonorMigration | undefined
> = await migrationRepo.getLatestSuccessful();
const clinicalErrors: any[] = [];

if (migration) {
const { invalidDonorsErrors }: DeepReadonly<DonorMigration> = migration;
invalidDonorsErrors
.filter(
donor =>
donor.programId.toString() === programId && query.includes(donor.donorId.toString()),
)
.forEach(donor => {
const { donorId, submitterDonorId } = donor;
// Overwrite donor.errors + flatten entityName to simplify query
let errors: DonorMigrationErrorRecord[] = [];
donor.errors.forEach(entity => {
const entityName = Object.keys(entity)[0];
errors = errors.concat(entity[entityName].map(error => ({ ...error, entityName })));
});
clinicalErrors.push({ donorId, submitterDonorId, errors });
});
}

const end = new Date().getTime() / 1000;
L.debug(`getClinicalEntityMigrationErrors took ${end - start}s`);

return clinicalErrors;
};
62 changes: 59 additions & 3 deletions src/clinical/donor-repo.ts
Expand Up @@ -18,9 +18,12 @@
*/

import { Donor } from './clinical-entities';
import mongoose from 'mongoose';
import { ClinicalQuery } from './clinical-api';
import mongoose, { PaginateModel } from 'mongoose';
import mongoosePaginate from 'mongoose-paginate-v2';
import { DeepReadonly } from 'deep-freeze';
import { F, MongooseUtils, notEmpty } from '../utils';

export const SUBMITTER_ID = 'submitterId';
export const SPECIMEN_SUBMITTER_ID = 'specimen.submitterId';
export const SPECIMEN_SAMPLE_SUBMITTER_ID = 'specimen.sample.submitterId';
Expand All @@ -44,11 +47,27 @@ export enum DONOR_DOCUMENT_FIELDS {
FAMILY_HISTORY_ID = 'familyHistory.clinicalInfo.family_relative_id',
}

const DONOR_ENTITY_CORE_FIELDS = [
'donorId',
'submitterId',
'programId',
'gender',
'clinicalInfo',
'completionStats',
'specimens',
];

export type FindByProgramAndSubmitterFilter = DeepReadonly<{
programId: string;
submitterId: string;
}>;

export type FindPaginatedProgramFilter = {
programId: string;
submitterId?: { $in: '' };
donorId?: { $in: '' };
};

export interface DonorRepository {
findByClinicalEntitySubmitterIdAndProgramId(
filters: DeepReadonly<FindByProgramAndSubmitterFilter>,
Expand All @@ -63,6 +82,7 @@ export interface DonorRepository {
projections?: Partial<Record<DONOR_DOCUMENT_FIELDS, number>>,
omitMongoDocIds?: boolean,
): Promise<DeepReadonly<Donor[]>>;
findByPaginatedProgramId(programId: string, query: ClinicalQuery): Promise<DeepReadonly<Donor[]>>;
deleteByProgramId(programId: string): Promise<void>;
findByProgramAndSubmitterId(
filters: DeepReadonly<FindByProgramAndSubmitterFilter[]>,
Expand Down Expand Up @@ -134,7 +154,6 @@ export const donorDao: DonorRepository = {
if (omitMongoDocIds) {
return findByProgramIdOmitMongoDocId(programId, projection);
}

const result = await DonorModel.find(
{
[DONOR_DOCUMENT_FIELDS.PROGRAM_ID]: programId,
Expand All @@ -152,6 +171,39 @@ export const donorDao: DonorRepository = {
return F(mapped);
},

async findByPaginatedProgramId(
programId: string,
query: ClinicalQuery,
): Promise<DeepReadonly<Donor[]>> {
const { page, limit, sort, entityTypes, donorIds, submitterDonorIds, completionState } = query;

const projection = [...DONOR_ENTITY_CORE_FIELDS, ...entityTypes].join(' ');

const result = await DonorModel.paginate(
{
[DONOR_DOCUMENT_FIELDS.PROGRAM_ID]: programId,
...donorIds,
...submitterDonorIds,
...completionState,
},
{
limit,
page,
projection,
sort,
select: '-_id',
},
);

const mapped: Donor[] = result.docs
.map((d: DonorDocument) => {
return MongooseUtils.toPojo(d) as Donor;
})
.filter(notEmpty);

return F(mapped);
},

async findBySpecimenSubmitterIdAndProgramId(
filter: FindByProgramAndSubmitterFilter,
): Promise<DeepReadonly<Donor> | undefined> {
Expand Down Expand Up @@ -492,6 +544,8 @@ DonorSchema.plugin(AutoIncrement, {
start_seq: process.env.DONOR_ID_SEED || 250000,
});

DonorSchema.plugin(mongoosePaginate);

SpecimenSchema.plugin(AutoIncrement, {
inc_field: 'specimenId',
start_seq: process.env.SPECIMEN_ID_SEED || 210000,
Expand Down Expand Up @@ -537,4 +591,6 @@ TreatmentSchema.plugin(AutoIncrement, {
start_seq: 1,
});

export let DonorModel = mongoose.model<DonorDocument>('Donor', DonorSchema);
export let DonorModel = mongoose.model<DonorDocument>('Donor', DonorSchema) as PaginateModel<
DonorDocument
>;