Skip to content

Commit fd33782

Browse files
authored
🐛 Fix family sqon (family-clinical-data) (#114)
1 parent 1480d00 commit fd33782

File tree

4 files changed

+183
-88
lines changed

4 files changed

+183
-88
lines changed

src/reports/family-clinical-data/generateFamilySqon.ts

-76
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {
2+
extractFieldAggregationIds,
3+
mergeParticipantsWithoutDuplicates,
4+
xIsSubsetOfY,
5+
} from './generatePtSqonWithRelativesIfExist';
6+
7+
describe('Sqon generator for selected participants and their relatives', () => {
8+
test('checks if X is a subset of Y', () => {
9+
//
10+
const x = ['p1', 'p2'];
11+
const y = ['p1', 'p2', 'p4'];
12+
expect(xIsSubsetOfY(x, y)).toBeTruthy();
13+
});
14+
test('merges participants adequately', () => {
15+
const x = ['p1', 'p2', 'p3'];
16+
const y = ['p2', 'p3', 'p4'];
17+
expect(mergeParticipantsWithoutDuplicates(x, y).every(p => ['p1', 'p2', 'p3', 'p4'].includes(p))).toBeTruthy();
18+
});
19+
test('extracts correctly all participants from initial ES response', async () => {
20+
const query = {
21+
bool: {
22+
must: [
23+
{
24+
terms: {
25+
down_syndrome_status: ['T21'],
26+
boost: 0,
27+
},
28+
},
29+
],
30+
},
31+
};
32+
const searchExecutor = async (_: object) =>
33+
Promise.resolve({
34+
query: _,
35+
body: {
36+
aggregations: {
37+
ids: {
38+
buckets: [
39+
{ key: 'p1', count: 1 },
40+
{ key: 'p2', count: 1 },
41+
{ key: 'p2', count: 1 },
42+
],
43+
},
44+
},
45+
},
46+
});
47+
const ps = await extractFieldAggregationIds(query, 'participant_id', searchExecutor);
48+
expect(ps).toEqual(['p1', 'p2']);
49+
});
50+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { buildQuery } from '@arranger/middleware';
2+
3+
import { getExtendedConfigs, getNestedFields } from '../../utils/arrangerUtils';
4+
import { executeSearch } from '../../utils/esUtils';
5+
import { Client } from '@elastic/elasticsearch';
6+
import { resolveSetsInSqon } from '../../utils/sqonUtils';
7+
8+
import { Sqon } from '../../utils/setsTypes';
9+
import { ES_QUERY_MAX_SIZE } from '../../env';
10+
11+
type Bucket = { key: string; count: number };
12+
type AggregationIdsRequest = {
13+
[index: string]: any;
14+
query: object;
15+
body: {
16+
aggregations: {
17+
ids: {
18+
buckets: Bucket[];
19+
};
20+
};
21+
};
22+
};
23+
export const extractFieldAggregationIds = async (
24+
query: object,
25+
field: string,
26+
searchExecutor: (q: object) => Promise<AggregationIdsRequest>,
27+
): Promise<string[]> => {
28+
const r = await searchExecutor({
29+
query,
30+
aggs: {
31+
ids: {
32+
terms: { field: field, size: ES_QUERY_MAX_SIZE },
33+
},
34+
},
35+
});
36+
const rawIds: string[] = (r.body?.aggregations?.ids?.buckets || []).map((bucket: Bucket) => bucket.key);
37+
return [...new Set(rawIds)];
38+
};
39+
40+
export const mergeParticipantsWithoutDuplicates = (x: string[], y: string[]) => [...new Set([...x, ...y])];
41+
42+
// extract in a more general file when and if needed.
43+
export const xIsSubsetOfY = (x: string[], y: string[]) => x.every((e: string) => y.includes(e));
44+
/**
45+
* Generate a sqon from the family_id of all the participants in the given `sqon`.
46+
* @param {object} es - an `elasticsearch.Client` instance.
47+
* @param {string} projectId - the id of the arranger project.
48+
* @param {object} sqon - the sqon used to filter the results.
49+
* @param {object} normalizedConfigs - the normalized report configuration.
50+
* @param {string} userId - the user id.
51+
* @param {string} accessToken - the user access token.
52+
* @returns {object} - A sqon of all the `family_id`.
53+
*/
54+
const generatePtSqonWithRelativesIfExist = async (
55+
es: Client,
56+
projectId: string,
57+
sqon: Sqon,
58+
normalizedConfigs: { indexName: string; alias: string; [index: string]: any },
59+
userId: string,
60+
accessToken: string,
61+
): Promise<Sqon> => {
62+
const extendedConfig = await getExtendedConfigs(es, projectId, normalizedConfigs.indexName);
63+
const nestedFields = getNestedFields(extendedConfig);
64+
const newSqon = await resolveSetsInSqon(sqon, userId, accessToken);
65+
66+
const query = buildQuery({ nestedFields, filters: newSqon });
67+
const searchExecutor = async (q: object) => await executeSearch(es, normalizedConfigs.alias, q);
68+
69+
const allSelectedParticipantsIds: string[] = await extractFieldAggregationIds(
70+
query,
71+
'participant_id',
72+
searchExecutor,
73+
);
74+
const allFamiliesIdsOfSelectedParticipants: string[] = await extractFieldAggregationIds(
75+
{
76+
bool: {
77+
must: [
78+
{
79+
terms: {
80+
participant_id: allSelectedParticipantsIds,
81+
},
82+
},
83+
],
84+
},
85+
},
86+
'families_id',
87+
searchExecutor,
88+
);
89+
const allRelativesIds: string[] = await extractFieldAggregationIds(
90+
{
91+
bool: {
92+
must: [
93+
{
94+
terms: {
95+
families_id: allFamiliesIdsOfSelectedParticipants,
96+
},
97+
},
98+
],
99+
},
100+
},
101+
'participant_id',
102+
searchExecutor,
103+
);
104+
const selectedParticipantsIdsPlusRelatives = mergeParticipantsWithoutDuplicates(
105+
allSelectedParticipantsIds,
106+
allRelativesIds,
107+
);
108+
109+
console.assert(
110+
selectedParticipantsIdsPlusRelatives.length >= allSelectedParticipantsIds.length &&
111+
xIsSubsetOfY(allSelectedParticipantsIds, selectedParticipantsIdsPlusRelatives),
112+
`Family Report (sqon enhancer): The participants ids computed must be equal or greater than the selected participants.
113+
Moreover, selected participants must a subset of the computed ids.`,
114+
);
115+
116+
return {
117+
op: 'and',
118+
content: [
119+
{
120+
op: 'in',
121+
content: {
122+
field: 'participant_id',
123+
value: selectedParticipantsIdsPlusRelatives,
124+
},
125+
},
126+
],
127+
};
128+
};
129+
130+
export default generatePtSqonWithRelativesIfExist;

src/reports/family-clinical-data/index.ts

+3-12
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import configKf from './configKf';
66
import configInclude from './configInclude';
77
import { normalizeConfigs } from '../../utils/configUtils';
88

9-
import generateFamilySqon from './generateFamilySqon';
9+
import generatePtSqonWithRelativesIfExist from './generatePtSqonWithRelativesIfExist';
1010
import { reportGenerationErrorHandler } from '../../errors';
1111
import { PROJECT } from '../../env';
1212
import { ProjectType, ReportConfig } from '../types';
@@ -38,19 +38,10 @@ const clinicalDataReport = async (req: Request, res: Response): Promise<void> =>
3838
const normalizedConfigs = await normalizeConfigs(esClient, projectId, reportConfig);
3939

4040
// generate a new sqon containing the id of all family members for the current sqon
41-
const familySqon = await generateFamilySqon(
42-
esClient,
43-
projectId,
44-
sqon,
45-
normalizedConfigs,
46-
userId,
47-
accessToken,
48-
PROJECT,
49-
isKfNext,
50-
);
41+
const participantsSqonWithRelatives = await generatePtSqonWithRelativesIfExist(esClient, projectId, sqon, normalizedConfigs, userId, accessToken);
5142

5243
// Generate the report
53-
await generateReport(esClient, res, projectId, familySqon, filename, normalizedConfigs, userId, accessToken);
44+
await generateReport(esClient, res, projectId, participantsSqonWithRelatives, filename, normalizedConfigs, userId, accessToken);
5445
} catch (err) {
5546
reportGenerationErrorHandler(err);
5647
}

0 commit comments

Comments
 (0)