Skip to content

Commit b72eba7

Browse files
committed
2 parents 9ef565e + a8a0129 commit b72eba7

9 files changed

Lines changed: 513 additions & 65 deletions

File tree

‎.github/workflows/update_others.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
updated_commit=$(git rev-parse HEAD)
4646
echo "Updated subtree last commit: $updated_commit"
4747
# Iterate MDDB repositores which use this code
48-
for repo in MDDB-REST-API
48+
for repo in MDDB-REST-API MDDB-loader
4949
do echo "Checking $repo"
5050
# Donwload the repository
5151
repo_url=https://x-access-token:$token@github.com/mmb-irb/$repo.git

‎index.js‎

Lines changed: 95 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@ const {
1313
STANDARD_STRUCTURE_FILENAME,
1414
} = require('./utils/constants');
1515

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

@@ -52,28 +58,102 @@ class Database {
5258
return this._bucket;
5359
}
5460

61+
// Setup the database by creating and indexing the configured collections
62+
// This function is shared by the loader and the monitor
63+
setup = async () => {
64+
// Check the collections already existing in the database
65+
const currentCollections = await this.db.listCollections().toArray()
66+
const currentCollectionNames = currentCollections.map(collection => collection.name);
67+
// Iterate over the configured collections
68+
for await (const [collectionKey, collectionConfig] of Object.entries(this.COLLECTIONS)) {
69+
// If the collection already exists then do nothing
70+
if (currentCollectionNames.includes(collectionConfig.name)) {
71+
// Get the configuration indexes for this collection
72+
const configIndexes = collectionConfig.indexes;
73+
// If there are no configuration indexes at all then we are done
74+
if (!configIndexes) continue;
75+
// Get the current collection indexes
76+
const currentIndexesData = await this[collectionKey].indexes();
77+
const currentIndexes = currentIndexesData.map(indexData => indexData.key);
78+
// Iterate the expected indexes
79+
for await (const configIndex of configIndexes) {
80+
// Make sure the index exists among the current indexes
81+
let found = false;
82+
for (const collectionIndex of currentIndexes) {
83+
// Compare indices
84+
if (areObjectsIdentical(collectionIndex, configIndex)) {
85+
found = true;
86+
break;
87+
}
88+
}
89+
// If the index does not exist then we create it
90+
if (!found) {
91+
console.log(`🛠️ Setting a missing index in "${collectionKey}" collection: ${JSON.stringify(configIndex)}`);
92+
await this[collectionKey].createIndex(configIndex);
93+
}
94+
}
95+
// Proceed to the next collection
96+
continue;
97+
}
98+
console.log(`🛠️ Setting up ${collectionKey} collection`);
99+
// Create the collection
100+
await this.db.createCollection(collectionConfig.name);
101+
// Set some indices if specified to accelerate specific queries
102+
if (collectionConfig.indexes) {
103+
for await (const index of collectionConfig.indexes) {
104+
await this[collectionKey].createIndex(index);
105+
}
106+
}
107+
}
108+
};
109+
110+
// Set some filters in the project query depending on the context in a single query object
111+
getProjectsFilter = (isGlobal, isProduction, hostCollection, withAccession) => {
112+
// Set the published filter according to the host configuration
113+
// If the environment is tagged as "production" only published projects are returned from mongo
114+
// Note that in the global API we target projects flagged as 'posited' instead of 'published'
115+
const productionTargetFlag = isGlobal ? 'posited' : 'published';
116+
const publishedFilter = Object.seal(isProduction ? { [productionTargetFlag]: true } : {});
117+
// Set a filter for the global API to not return unposited projects
118+
// Note that a non global API is not expected to have this field so it makes not sense applying the filter
119+
const positedFilter = Object.seal(isGlobal ? { unposited: { $exists: false } } : {});
120+
// Set the collection filter according to the request URL
121+
// This filter is applied over the project metadata 'collections', nothing to do with mongo collections
122+
// Note that unknown hosts (e.g. 'localhost:8000') will get all simulations, with no filter
123+
const collectionFilter = Object.seal(hostCollection ? { 'metadata.COLLECTIONS': hostCollection } : {});
124+
// Set the starting base filter
125+
// This is a strict filter and it is applied even when a specific project is requested
126+
const baseFilter = { ...publishedFilter, ...positedFilter, ...collectionFilter };
127+
// If a specific project was requested then the base filter is returned as it is
128+
// Note that the filter above are mandatory for "privacy" reasons
129+
// In the sense that they are not to be skipped even if the requests asks for a specific accession
130+
// The filters below, in the other hand, are to discard projects which are not important
131+
// And they could make the query fail if the user asks for a booked/deleted entry
132+
if (withAccession) return baseFilter;
133+
// If no specific project was requested then the filter is extended
134+
// We hide booked and deleted projects
135+
// Note that these are not hidden when asking specifically for them
136+
// Set the booked filter to remove booked projects from the query
137+
// These are projects which are not yet uploaded
138+
const bookedFilter = Object.seal({ booked: { $ne: true } });
139+
// Set the deleted filter to remove deleted projects from the query
140+
// These are projects which were deleted but the entry is kept to preserve the persistent id
141+
const deletedFilter = Object.seal({ deleted: { $ne: true } });
142+
// Return all filters together, including also the publsihed filter
143+
return { ...baseFilter, ...bookedFilter, ...deletedFilter };
144+
};
145+
146+
// Add additional functions
147+
countOptions = (query, fields, shouldCountMds, useSavedCounts) =>
148+
countOptions(this, query, fields, shouldCountMds, useSavedCounts);
149+
55150
// Close the connection to mongo and delete this handler
56151
close = () => {
57152
this.client.close();
58153
delete this;
59154
}
60155
}
61156

62-
// Connect to the database
63-
// Then construct and return the database handler
64-
const connectToDatabase = async isGlobal => {
65-
// Save the mongo database connection
66-
const client = await dbConnection;
67-
// Instantiate the database handler
68-
return new Database(client, isGlobal);
69-
};
70-
71-
// Get the database client
72-
// Note that this function is set separatedly and not integrated in the handler since it is async
73-
const getDatabaseClient = async () => {
74-
const client = await dbConnection;
75-
}
76-
77157
module.exports = {
78158
databaseConnection,
79159
Database,

‎utils/auxiliar/index.js‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Small generic functions used along the whole repository
2+
3+
// Compare if two objects are identical key by key and value by value
4+
// Note that this is a shallow comparator
5+
// If the object have objects or arrays inside the result may be unexpected
6+
const areObjectsIdentical = (object1, object2) => {
7+
// Iterate keys and values to in the first object
8+
for (const [key, value] of Object.entries(object1)) {
9+
if (!key in object2) return false;
10+
if (object1[key] !== value) return false;
11+
}
12+
// Iterate keys and values to in the second object
13+
for (const [key, value] of Object.entries(object2)) {
14+
if (!key in object1) return false;
15+
if (object1[key] !== value) return false;
16+
}
17+
// If no differentce was found then object must be identical
18+
return true;
19+
}
20+
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+
40+
module.exports = {
41+
areObjectsIdentical,
42+
getValueGetter,
43+
};

‎utils/connect-mongo/index.js‎

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,36 @@
11
const mongodb = require('mongodb');
22

3+
// Set the required environmental variables to connect to Mongo
4+
const REQUIRED_ENV = [ 'DB_SERVER', 'DB_PORT', 'DB_NAME',
5+
'DB_AUTH_USER', 'DB_AUTH_PASSWORD', 'DB_AUTHSOURCE'];
6+
37
// Try to connect with the mongo db
48
const establishConnection = async () => {
5-
let client;
6-
try {
7-
client = await mongodb.MongoClient.connect(
8-
`mongodb://${process.env.DB_SERVER}:${process.env.DB_PORT}`,
9-
{
10-
auth: {
11-
username: process.env.DB_AUTH_USER,
12-
password: process.env.DB_AUTH_PASSWORD,
13-
},
14-
authSource: process.env.DB_AUTHSOURCE,
15-
useNewUrlParser: true,
16-
useUnifiedTopology: true,
17-
},
18-
);
19-
return client;
20-
} catch (error) {
21-
console.error('mongodb connection error');
22-
console.error(error);
23-
if (client && 'close' in client) client.close();
24-
}
9+
// Make sure we have the required enviornmental variables
10+
const missingEnv = REQUIRED_ENV.filter(env => !process.env[env]);
11+
if (missingEnv.length > 0) throw new Error(
12+
'Missing enviornmental variables: ' + missingEnv.join(', ') + '.\n' +
13+
'Please define these variables in the ".env" file.');
14+
let client;
15+
try {
16+
client = await mongodb.MongoClient.connect(
17+
`mongodb://${process.env.DB_SERVER}:${process.env.DB_PORT}`,
18+
{
19+
auth: {
20+
username: process.env.DB_AUTH_USER,
21+
password: process.env.DB_AUTH_PASSWORD,
22+
},
23+
authSource: process.env.DB_AUTHSOURCE,
24+
useNewUrlParser: true,
25+
useUnifiedTopology: true,
26+
},
27+
);
28+
return client;
29+
} catch (error) {
30+
console.error('mongodb connection error');
31+
console.error(error);
32+
if (client && 'close' in client) client.close();
33+
}
2534
};
2635

2736
module.exports = establishConnection();

‎utils/constants/index.js‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,54 +3,66 @@
33
// name - Actual name of the collection inside the database
44
// indexes - Index configuration in the database, for the collections setup
55
// documentNames - Document names used for displaying only
6+
// parent - Collection and fields to determine if documents are orphan
67
const LOCAL_COLLECTIONS = {
78
projects: {
89
name: 'projects',
910
indexes: [{ published: 1 }],
1011
documentNames: { singular: 'project', plural: 'projects' },
12+
parent: null,
1113
},
1214
uniprot_refs: {
1315
name: 'references',
1416
documentNames: { singular: 'reference', plural: 'references' },
17+
parent: { collectionKey: 'projects', referenceField: 'metadata.REFERENCES', localField: 'uniprot' },
1518
},
1619
inchikey_refs: {
1720
name: 'inchikey_refs',
1821
documentNames: { singular: 'inchikey', plural: 'inchikeys' },
22+
parent: { collectionKey: 'projects', referenceField: 'metadata.INCHIKEYS', localField: 'inchikey' },
1923
},
2024
pdb_refs: {
2125
name: 'pdb_refs',
2226
documentNames: { singular: 'PDB', plural: 'PDBs' },
27+
parent: { collectionKey: 'projects', referenceField: 'metadata.PDBIDS', localField: 'id' },
2328
},
2429
chain_refs: {
2530
name: 'chain_refs',
2631
documentNames: { singular: 'chain', plural: 'chains' },
32+
parent: { collectionKey: 'projects', referenceField: 'metadata.PROTSEQ', localField: 'sequence' },
2733
},
2834
collection_refs: {
2935
name: 'collection_refs',
3036
documentNames: { singular: 'collection', plural: 'collections' },
37+
parent: { collectionKey: 'projects', referenceField: 'metadata.COLLECTIONS', localField: 'id' },
3138
},
3239
topologies: {
3340
name: 'topologies',
3441
indexes: [{ project: 1 }],
3542
documentNames: { singular: 'topology', plural: 'topologies' },
43+
parent: { collectionKey: 'projects', referenceField: '_id', localField: 'project' },
3644
},
3745
files: {
3846
name: 'fs.files',
3947
indexes: [{ 'metadata.project': 1 }],
4048
documentNames: { singular: 'file', plural: 'files' },
49+
parent: { collectionKey: 'projects', referenceField: '_id', localField: 'metadata.project' },
4150
},
4251
chunks: {
4352
name: 'fs.chunks',
4453
documentNames: { singular: 'chunk', plural: 'chunks' },
54+
parent: { collectionKey: 'files', referenceField: '_id', localField: 'files_id' },
4555
},
4656
analyses: {
4757
name: 'analyses',
4858
indexes: [{ project: 1 }],
4959
documentNames: { singular: 'analysis', plural: 'analyses' },
60+
parent: { collectionKey: 'projects', referenceField: '_id', localField: 'project' },
5061
},
5162
counters: {
5263
name: 'counters',
53-
documentNames: { singular: 'counter', plural: 'counters' }
64+
documentNames: { singular: 'counter', plural: 'counters' },
65+
parent: null,
5466
},
5567
};
5668
const GLOBAL_COLLECTIONS = {

0 commit comments

Comments
 (0)