Skip to content

Commit 1e350c9

Browse files
automatic subtree merge
2 parents 1f702b1 + 7716ab2 commit 1e350c9

3 files changed

Lines changed: 280 additions & 1 deletion

File tree

‎src/mddb-database/index.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ const {
1616
// Import auxiliar functions
1717
const { areObjectsIdentical } = require('./utils/auxiliar');
1818

19+
// Import additional functions
20+
const countOptions = require('./utils/count-options');
21+
1922
// GridFSBucket manages the saving of files bigger than 16 Mb, splitting them into 4 Mb fragments (chunks)
2023
const { GridFSBucket } = require('mongodb');
2124

@@ -104,6 +107,9 @@ class Database {
104107
}
105108
};
106109

110+
// Add additional functions
111+
countOptions = (query, fields, shouldCountMds) => countOptions(this, query, fields, shouldCountMds);
112+
107113
// Close the connection to mongo and delete this handler
108114
close = () => {
109115
this.client.close();

‎src/mddb-database/utils/auxiliar/index.js‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,26 @@ const areObjectsIdentical = (object1, object2) => {
1818
return true;
1919
}
2020

21+
// Set a function to build value getters with specific nesting paths
22+
// Each nested step is separated by a dot
23+
// e.g. 'metadata.INCHIKEYS' -> { metadata: { INCHIKEYS: <target value> } }
24+
const getValueGetter = path => {
25+
if (!path) throw new Error('Value getter has no path');
26+
// Split the path in its nested steps
27+
const steps = path.split('.');
28+
// Build the getter function
29+
const valueGetter = object => {
30+
let lastObject = object;
31+
for (const step of steps) {
32+
lastObject = lastObject[step]
33+
if (lastObject === undefined) return;
34+
}
35+
return lastObject;
36+
}
37+
return valueGetter;
38+
};
39+
2140
module.exports = {
22-
areObjectsIdentical
41+
areObjectsIdentical,
42+
getValueGetter,
2343
};
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// Import some auxiliar functions
2+
const { getValueGetter } = require('../auxiliar');
3+
4+
// Set a header present in all
5+
const REFERENCE_HEADER = 'references.';
6+
7+
// Count the number of projects with the same values for every field
8+
// WARNING: This function is used by both the API and the loader
9+
// It is important that they use the same code so they are coordinated
10+
const countOptions = async (database, query, fields, shouldCountMds) => {
11+
// Set the options object to be returned
12+
// Then all mined data will be written into it
13+
const options = {};
14+
// Options may be fields from projects or references collections
15+
// Fields in references are headed with the 'references.' label and they are handled separately
16+
// Start with options from references
17+
// In case there is any reference we must query the projects collections first
18+
const requestedProjections = { projects: [] };
19+
const availableReferences = Object.keys(database.REFERENCES);
20+
availableReferences.forEach(referenceName => { requestedProjections[referenceName] = [] });
21+
// Keep a set with the references included in the projection
22+
const requestedReferences = new Set();
23+
// First separate reference fields from project fields
24+
for (const field of fields) {
25+
// If this field has not the reference header either then it is a project field
26+
if (!field.startsWith(REFERENCE_HEADER)) {
27+
requestedProjections.projects.push(field);
28+
continue;
29+
}
30+
// Otherwise it is a reference field
31+
// Find the reference it belongs to
32+
const requestedReference = field.split('.')[1];
33+
// Make sure the reference exists
34+
if (!availableReferences.includes(requestedReference)) return {
35+
code: '400',
36+
error: `Unknown reference "${requestedReference}". Available references: ${availableReferences.join(', ')}`
37+
};
38+
// Make sure there is something after the reference name or we will have a mongo error later
39+
if (!field.split('.')[2]) return {
40+
code: '400',
41+
error: `Empty reference field in "${field}". Please provide a field name after "${requestedReference}"`
42+
}
43+
// Add the requetsed reference to the set
44+
requestedReferences.add(requestedReference);
45+
// Add the requested field to its corresponding reference
46+
requestedProjections[requestedReference].push(field);
47+
}
48+
// First of all make the projects request
49+
// This requests has 2 goals
50+
// First, we get the project requested fields
51+
// Second, we get reference id fields to further count the number of matches per reference requested field
52+
// Set the projector according to the two previously explained goals
53+
// We will need internal ids if we have to request any reference field
54+
const projector = { _id: true };
55+
// If MDs are to be counted then return the MD count as well
56+
if (shouldCountMds) projector.mdcount = true;
57+
// Add requested project fields
58+
requestedProjections.projects.forEach(field => {
59+
projector[field] = true
60+
});
61+
// Add requested references id fields
62+
requestedReferences.forEach(referenceName => {
63+
const reference = database.REFERENCES[referenceName];
64+
projector[reference.projectIdsField] = true;
65+
});
66+
// Set the projects cursor
67+
const projectsCursor = await database.projects.find(query, { projection: projector });
68+
// Consume the projects cursor
69+
const projectsData = await projectsCursor.toArray();
70+
// If projects data is empty then stop here
71+
if (projectsData.length === 0) return {
72+
code: '404',
73+
error: `The result of ${(query && `query "${query}"`) || ''}${(query && search && ' and ') || ''}${(search && `search "${search}"`) || ''} is empty`
74+
}
75+
// Set an object with project md counts
76+
const projectMdCounts = {};
77+
if (shouldCountMds) projectsData.forEach(project => {
78+
projectMdCounts[project._id] = project.mdcount || 1;
79+
});
80+
// Start handling references options
81+
// First of all, make sure there was at least one reference projection request
82+
const anyReferenceProjectionRequest = requestedReferences.size > 0;
83+
if (anyReferenceProjectionRequest) {
84+
// Now iterate along the different references
85+
for await (const referenceName of requestedReferences) {
86+
// Get the reference configuration
87+
const reference = database.REFERENCES[referenceName];
88+
// Set a getter function for the project reference ids field
89+
const projectIdsGetter = getValueGetter(reference.projectIdsField);
90+
// Set a list of projects including every reference id
91+
const referenceIdProjects = {};
92+
for (const projectData of projectsData) {
93+
const projectReferenceIds = projectIdsGetter(projectData);
94+
if (!projectReferenceIds) continue;
95+
projectReferenceIds.forEach(referenceId => {
96+
if (referenceId in referenceIdProjects)
97+
referenceIdProjects[referenceId].push(projectData._id);
98+
else referenceIdProjects[referenceId] = [projectData._id];
99+
});
100+
}
101+
// Get the requested projection fields for the current reference
102+
// Remove both the reference header and the reference name from every field to get the actual fields
103+
// e.g. 'references.proteins.name' -> 'name'
104+
const referenceRequestedProjections = requestedProjections[referenceName].map(
105+
field => field.split('.').slice(2).join('.')
106+
);
107+
// Set the references projector
108+
const referencesProjector = { _id: false };
109+
// Get reference ids to associate values further
110+
referencesProjector[reference.idField] = true;
111+
// Get every requested projection field
112+
referenceRequestedProjections.forEach(field => {
113+
referencesProjector[field] = true;
114+
});
115+
// Get all references using the custom projector
116+
const collection = database[reference.collectionName];
117+
const referencesCursor = await collection.find(
118+
{}, // Get all references, independently from the request origin
119+
// Discard the heaviest fields we do not need anyway
120+
{ projection: referencesProjector },
121+
);
122+
// Consume the references cursor
123+
const referencesData = await referencesCursor.toArray();
124+
// Now for each field, get the different available values and the reference ids on each value
125+
// Then count how many times any of those reference ids is in the project references list
126+
referenceRequestedProjections.forEach(field => {
127+
const referenceIdsPerValue = {};
128+
// Set a function to mine values
129+
const getValues = (object, steps, referenceId) => {
130+
let value = object;
131+
for (const [index, step] of steps.entries()) {
132+
// Get the actual value
133+
value = value[step];
134+
if (value === undefined) return;
135+
// In case it is an array search for the remaining steps on each element
136+
if (Array.isArray(value)) {
137+
const remainingSteps = steps.slice(index + 1);
138+
value.forEach(element =>
139+
getValues(element, remainingSteps, referenceId),
140+
);
141+
return;
142+
}
143+
}
144+
// If the value is a string then make it lower caps
145+
// This way we avoid having duplicated values because of different capitalizatioin
146+
// This is quiet common in PDB annotations (organism, method, etc.)
147+
if (typeof value === 'string') value = value.toLowerCase();
148+
// If the value exists and it is not an array then add it to the list
149+
// First create an empty list in case this is the first time we find this value
150+
if (!referenceIdsPerValue[value]) referenceIdsPerValue[value] = [];
151+
// Then add the reference id to the list
152+
referenceIdsPerValue[value].push(referenceId);
153+
};
154+
// Run the actual values mining
155+
const fieldSteps = field.split('.');
156+
referencesData.forEach(referenceData => {
157+
const referenceId = referenceData[reference.idField];
158+
// If the reference id is not among the projects recap then skip it
159+
// There will be no matches from it anyway
160+
// This may happen when we do not target the whole database
161+
if (!(referenceId in referenceIdProjects)) return;
162+
getValues(referenceData, fieldSteps, referenceData[reference.idField])
163+
});
164+
// Convert every reference ids list in the count of projects including any of these reference ids
165+
const valueCounts = {};
166+
Object.entries(referenceIdsPerValue).forEach(([value, referenceIds]) => {
167+
// Now for each value get all associated project ids
168+
let valueProjects = [];
169+
referenceIds.forEach(referenceId => {
170+
valueProjects = valueProjects.concat(referenceIdProjects[referenceId]);
171+
});
172+
// Get unique project ids
173+
const uniqueValueProjects = new Set(valueProjects);
174+
const projectCount = uniqueValueProjects.size;
175+
// If the count is 0 then skip this value entirely
176+
// This may happen when a reference is orphan (i.e. its associated projects were deleted)
177+
if (projectCount === 0) return;
178+
// Count MDs if requested
179+
if (shouldCountMds) {
180+
const mdCount = Array.from(uniqueValueProjects).reduce(
181+
(accumulated, projectId) => accumulated + projectMdCounts[projectId], 0);
182+
valueCounts[value] = [projectCount, mdCount];
183+
}
184+
else {
185+
valueCounts[value] = projectCount;
186+
}
187+
});
188+
// Add current value counts to the options object to be returned
189+
const originalFieldName = `${REFERENCE_HEADER}${referenceName}.${field}`;
190+
options[originalFieldName] = valueCounts;
191+
});
192+
}
193+
}
194+
// Now handle project options
195+
if (requestedProjections.projects.length !== 0) {
196+
// For each projected field, get the counts
197+
requestedProjections.projects.forEach(field => {
198+
// For each different value, save all project ids from projects including it
199+
// This allows us to not count the same project twice
200+
const values = {};
201+
// Set a recursive function to reach indented values
202+
const getValues = (object, steps, projectId) => {
203+
let value = object;
204+
for (const [index, step] of steps.entries()) {
205+
value = value[step];
206+
if (value === undefined) return;
207+
// In case it is an array search for the remaining steps on each element
208+
if (Array.isArray(value)) {
209+
const remainingSteps = steps.slice(index + 1);
210+
value.forEach(element => getValues(element, remainingSteps, projectId));
211+
return;
212+
}
213+
}
214+
// Get the set of projects with the current value and update it
215+
const currentValueProjects = values[value];
216+
if (currentValueProjects) currentValueProjects.add(projectId);
217+
else values[value] = new Set([ projectId ]);
218+
};
219+
// Start the recursive function here
220+
const fieldSteps = field.split('.');
221+
projectsData.forEach(projectData => getValues(projectData, fieldSteps, projectData._id));
222+
// Count how many times is repeated each value and save the number with the fieldname key
223+
const counts = {};
224+
if (shouldCountMds) Object.entries(values).forEach(([value, projectIds]) => {
225+
const projectCount = projectIds.size;
226+
const mdCount = Array.from(projectIds).reduce(
227+
(accumulated, projectId) => accumulated + projectMdCounts[projectId], 0);
228+
counts[value] = [projectCount, mdCount];
229+
});
230+
else Object.entries(values).forEach(([value, projectIds]) => { counts[value] = projectIds.size });
231+
// Add current field counts to the overall options object to be returned
232+
options[field] = counts;
233+
});
234+
}
235+
// This is the moment to do any last changes to the results
236+
// Here are the hardcodes to solve silly issues or not "good looking" numbers
237+
// Remove MD interaction-analyses with the numerated name
238+
const mdAnalysesKey = 'mds.analyses.name';
239+
const numeratedAnalysisPattern = /-[0-9]*$/;
240+
if (mdAnalysesKey in options) {
241+
const mdAnalyses = options[mdAnalysesKey];
242+
Object.keys(mdAnalyses).forEach(key => {
243+
// If it ends in '-xx' where xx is any number of numeric characters
244+
if (key.match(numeratedAnalysisPattern)) delete mdAnalyses[key];
245+
});
246+
}
247+
// LORE: We no longer sort the final response, this is now done by the client
248+
// LORE: You can not rely in objects order and returning everything as arrays is not efficient
249+
// Send all mined data
250+
return options;
251+
}
252+
253+
module.exports = countOptions;

0 commit comments

Comments
 (0)