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