Skip to content

Commit

Permalink
Convert age range to age buckets on case creation #2670
Browse files Browse the repository at this point in the history
  • Loading branch information
iamleeg committed Apr 21, 2022
1 parent 59d963f commit 9b98106
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 10 deletions.
24 changes: 22 additions & 2 deletions data-serving/data-service/src/controllers/case.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
Case,
CaseDocument,
CaseDTO,
caseWithDenormalisedConfirmationDate,
RestrictedCase,
} from '../model/case';
import { EventDocument } from '../model/event';
import { GenomeSequenceDocument } from '../model/genome-sequence';
import caseFields from '../model/fields.json';
import { Source } from '../model/source';
import {
Expand All @@ -30,13 +30,32 @@ import {
import { logger } from '../util/logger';
import stringify from 'csv-stringify/lib/sync';
import _ from 'lodash';
import { AgeBucket } from '../model/age-bucket';

class GeocodeNotFoundError extends Error {}

class InvalidParamError extends Error {}

type BatchValidationErrors = { index: number; message: string }[];

const caseFromDTO = async (receivedCase: CaseDTO) => {
const aCase = new Case(receivedCase);
if (receivedCase.demographics?.ageRange) {
// won't be many age buckets, so fetch all of them.
const allBuckets = await AgeBucket.find({});
const caseStart = receivedCase.demographics?.ageRange.start;
const caseEnd = receivedCase.demographics?.ageRange.end;
const matchingBucketIDs = allBuckets.filter(b => {
const bucketContainsStart = (b.start <= caseStart && b.end >= caseStart);
const bucketContainsEnd = (b.start <= caseEnd && b.end >= caseEnd);
const bucketWithinCaseRange = (b.start > caseStart && b.end < caseEnd);
return bucketContainsStart || bucketContainsEnd || bucketWithinCaseRange;
}).map((b) => (b._id));
aCase.demographics.ageBuckets = matchingBucketIDs;
}
return aCase;
}

export class CasesController {
private csvHeaders: string[];
constructor(private readonly geocoders: Geocoder[]) {
Expand Down Expand Up @@ -336,7 +355,8 @@ export class CasesController {
const numCases = Number(req.query.num_cases) || 1;
try {
await this.geocode(req);
let c = new Case(req.body);
const receivedCase = req.body as CaseDTO;
let c = await caseFromDTO(receivedCase);
let restrictedCases = false;
if (c.caseReference.sourceId) {
const s = await Source.find({ _id: c.caseReference.sourceId });
Expand Down
2 changes: 1 addition & 1 deletion data-serving/data-service/src/model/age-bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const ageBucketSchema = new mongoose.Schema({

export type AgeBucketDocument = mongoose.Document & Range<number>;

export const AgeBuckets = mongoose.model<AgeBucketDocument>(
export const AgeBucket = mongoose.model<AgeBucketDocument>(
'AgeBuckets',
ageBucketSchema,
);
23 changes: 19 additions & 4 deletions data-serving/data-service/src/model/case.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { CaseReferenceDocument, caseReferenceSchema } from './case-reference';
import { DemographicsDocument, demographicsSchema } from './demographics';
import { DemographicsDocument, DemographicsDTO, demographicsSchema } from './demographics';
import { EventDocument, eventSchema } from './event';
import {
GenomeSequenceDocument,
Expand Down Expand Up @@ -27,6 +27,15 @@ import mongoose from 'mongoose';
import { ExclusionDataDocument, exclusionDataSchema } from './exclusion-data';
import { dateFieldInfo } from './date';

/*
* There are separate types for case for data storage (the mongoose document) and
* for data transfer (CaseDTO). The DTO only has an age range, and is what the cases
* controller receives and transmits over the network. The mongoose document has both an age
* range and age buckets, and is what gets written to the database. The end goal is that the
* mongoose document only has age buckets, and that the cases controller converts between the
* two so that outside you only see a single age range.
*/

const requiredDateField = {
...dateFieldInfo,
required: true,
Expand Down Expand Up @@ -127,11 +136,9 @@ caseSchema.methods.equalsJSON = function (jsonCase: any): boolean {
);
};

export type CaseDocument = mongoose.Document & {
_id: ObjectId;
export type ICase = {
caseReference: CaseReferenceDocument;
confirmationDate: Date;
demographics: DemographicsDocument;
events: [EventDocument];
exclusionData: ExclusionDataDocument;
genomeSequences: [GenomeSequenceDocument];
Expand All @@ -149,7 +156,15 @@ export type CaseDocument = mongoose.Document & {
travelHistory: TravelHistoryDocument;
vaccines: [VaccineDocument];
variant: VariantDocument;
};

export type CaseDTO = ICase & {
demographics?: DemographicsDTO;
};

export type CaseDocument = mongoose.Document & ICase & {
_id: ObjectId;
demographics: DemographicsDocument;
// TODO: Type request Cases.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
equalsJSON(jsonCase: any): boolean;
Expand Down
18 changes: 15 additions & 3 deletions data-serving/data-service/src/model/demographics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ import { Range } from './range';
import mongoose from 'mongoose';
import { ObjectId } from 'mongodb';

/*
* There are separate types for demographics for data storage (the mongoose document) and
* for data transfer (DemographicsDTO). The DTO only has an age range, and is what the cases
* controller receives and transmits over the network. The mongoose document has both an age
* range and age buckets, and is what gets written to the database. The end goal is that the
* mongoose document only has age buckets, and that the cases controller converts between the
* two so that outside you only see a single age range.
*/

export const demographicsSchema = new mongoose.Schema(
{
/*
Expand Down Expand Up @@ -35,13 +44,16 @@ export const demographicsSchema = new mongoose.Schema(
{ _id: false },
);

export type DemographicsDocument = mongoose.Document & {
ageBuckets: ObjectId[];
ageRange: Range<number>;
export type DemographicsDTO = {
ageRange?: Range<number>;
gender: string;
occupation: string;
nationalities: [string];
ethnicity: string;
}

export type DemographicsDocument = mongoose.Document & DemographicsDTO & {
ageBuckets: ObjectId[];
};

export const Demographics = mongoose.model<DemographicsDocument>(
Expand Down
27 changes: 27 additions & 0 deletions data-serving/data-service/test/controllers/case.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
handlers,
} from '../mocks/handlers';
import fs from 'fs';
import { AgeBucket } from '../../src/model/age-bucket';

let mongoServer: MongoMemoryServer;

Expand Down Expand Up @@ -43,9 +44,24 @@ function stringParser(res: request.Response) {
});
}

async function createAgeBuckets() {
await new AgeBucket({
start: 0,
end: 0,
}).save();
for (let start = 1; start <= 116; start += 5) {
const end = start + 4;
await new AgeBucket({
start,
end,
}).save();
}
}

beforeAll(async () => {
mockLocationServer.listen();
mongoServer = new MongoMemoryServer();
await createAgeBuckets();
global.Date.now = jest.fn(() => new Date('2020-12-12T12:12:37Z').getTime());
});

Expand All @@ -61,6 +77,7 @@ afterEach(() => {
});

afterAll(async () => {
await AgeBucket.deleteMany({});
mockLocationServer.close();
global.Date.now = realDate;
return mongoServer.stop();
Expand Down Expand Up @@ -456,6 +473,16 @@ describe('POST', () => {
.expect(201);
expect(await Case.collection.countDocuments()).toEqual(1);
});
it('create with valid input should bucket the age range', async () => {
await request(app)
.post('/api/cases')
.send(minimalRequest)
.expect('Content-Type', /json/)
.expect(201);
const theCase = await Case.findOne({});
// case has range 40-50, should be bucketed into 36-40, 41-45, 46-50
expect(theCase!.demographics.ageBuckets).toHaveLength(3);
})
it('create many cases with valid input should return 201 OK', async () => {
const res = await request(app)
.post('/api/cases?num_cases=3')
Expand Down

0 comments on commit 9b98106

Please sign in to comment.