Repository navigation
Expand file tree
/
Copy pathindex.js
More file actions
186 lines (175 loc) · 9.38 KB
/
Copy pathindex.js
File metadata and controls
186 lines (175 loc) · 9.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// Connect to the mongo database and return the connection
// Alternatively, in 'test' context, connect to a local fake mongo database and return the connection
const databaseConnection = process.env.NODE_ENV === 'test'
? require('./utils/fake-mongo')
: require('./utils/connect-mongo');
// Import collections configuration
const {
LOCAL_COLLECTIONS,
GLOBAL_COLLECTIONS,
REFERENCES,
QUERY_FIELDS,
PATH_QUERY_FIELDS,
OPTIONS_QUERY_FIELDS,
STANDARD_TRAJECTORY_FILENAME,
STANDARD_STRUCTURE_FILENAME,
FIRST_ACCESSION_CODE,
ACCESSION_CHARACTERS_LIMIT,
ALPHANUMERIC,
} = require('./utils/constants');
// Import auxiliar functions
const { areObjectsIdentical } = require('./utils/auxiliar');
// Import additional functions
const countOptions = require('./utils/count-options');
// GridFSBucket manages the saving of files bigger than 16 Mb, splitting them into 4 Mb fragments (chunks)
const { GridFSBucket, ObjectId } = require('mongodb');
// Set the database handler class
class Database {
constructor (client, isGlobal) {
if (!client) throw new Error('No client');
if (isGlobal !== false && isGlobal !== true)
throw new Error('The "isGlobal" argument must be either true or false.');
// Store inputs
this.client = client;
this.isGlobal = isGlobal;
// Get the mongo specific database
this.db = this.client.db(process.env.DB_NAME);
// Set the mongo collections depending of it we are aiming for the global database
this.COLLECTIONS = this.isGlobal ? GLOBAL_COLLECTIONS : LOCAL_COLLECTIONS;
// Set all collections as values of the database itself
for (const [ collectionKey, collectionConfig ] of Object.entries(this.COLLECTIONS)) {
this[collectionKey] = this.db.collection(collectionConfig.name);
}
// Save additional constants just to have them available more easily
this.QUERY_FIELDS = QUERY_FIELDS;
this.PATH_QUERY_FIELDS = PATH_QUERY_FIELDS;
this.OPTIONS_QUERY_FIELDS = OPTIONS_QUERY_FIELDS;
this.STANDARD_TRAJECTORY_FILENAME = STANDARD_TRAJECTORY_FILENAME;
this.STANDARD_STRUCTURE_FILENAME = STANDARD_STRUCTURE_FILENAME;
this.FIRST_ACCESSION_CODE = FIRST_ACCESSION_CODE;
this.ACCESSION_CHARACTERS_LIMIT = ACCESSION_CHARACTERS_LIMIT;
this.ALPHANUMERIC = ALPHANUMERIC;
// Get the available references in a single string, which may be used for logs
this.REFERENCES = REFERENCES;
this.AVAILABLE_REFERENCES = Object.keys(this.REFERENCES).join(', ');
// Save some internal values
this._bucket = undefined;
};
// Get the grid fs bucket
get bucket () {
// Return the internal value if it is already declared
if (this._bucket !== undefined) return this._bucket;
// Instantiate the bucket otherwise
this._bucket = new GridFSBucket(this.db);
return this._bucket;
}
// Setup the database by creating and indexing the configured collections
// This function is shared by the loader and the monitor
setup = async () => {
// Check the collections already existing in the database
const currentCollections = await this.db.listCollections().toArray()
const currentCollectionNames = currentCollections.map(collection => collection.name);
// Iterate over the configured collections
for await (const [collectionKey, collectionConfig] of Object.entries(this.COLLECTIONS)) {
// If the collection already exists then do nothing
if (currentCollectionNames.includes(collectionConfig.name)) {
// Get the configuration indexes for this collection
const configIndexes = collectionConfig.indexes;
// If there are no configuration indexes at all then we are done
if (!configIndexes) continue;
// Get the current collection indexes
const currentIndexesData = await this[collectionKey].indexes();
const currentIndexes = currentIndexesData.map(indexData => indexData.key);
// Iterate the expected indexes
for await (const configIndex of configIndexes) {
// Make sure the index exists among the current indexes
let found = false;
for (const collectionIndex of currentIndexes) {
// Compare indices
if (areObjectsIdentical(collectionIndex, configIndex)) {
found = true;
break;
}
}
// If the index does not exist then we create it
if (!found) {
console.log(`🛠️ Setting a missing index in "${collectionKey}" collection: ${JSON.stringify(configIndex)}`);
await this[collectionKey].createIndex(configIndex);
}
}
// Proceed to the next collection
continue;
}
console.log(`🛠️ Setting up ${collectionKey} collection`);
// Create the collection
await this.db.createCollection(collectionConfig.name);
// Set some indices if specified to accelerate specific queries
if (collectionConfig.indexes) {
for await (const index of collectionConfig.indexes) {
await this[collectionKey].createIndex(index);
}
}
}
// Make sure there is a counter document to track the last issued accession
const accessionsCounter = await this.counters.findOne({ accessions: true });
// If the counter does not exist yet then create it
if (!accessionsCounter) {
// Set the "zero" count for the counter
// Note that this is not zero since we want the first issued accession to star with 'A'
const zeroCount = parseInt(FIRST_ACCESSION_CODE, ALPHANUMERIC) - 1;
// Set the counter document
const counter = { accessions: true, last: zeroCount };
// Insert the new document
console.log(`🛠️ Creating new accession counter`);
const result = await this.counters.insertOne(counter);
if (!result.acknowledged) console.log(`🛠️ Failed to create new accession counter`);
console.log('🛠️ Created new accession counter');
}
};
// Set some filters in the project query depending on the context in a single query object
getProjectsFilter = (isGlobal, isProduction, hostCollection, withAccession) => {
// Set the published filter according to the host configuration
// If the environment is tagged as "production" only published projects are returned from mongo
// Note that in the global API we target projects flagged as 'posited' instead of 'published'
const productionTargetFlag = isGlobal ? 'posited' : 'published';
const publishedFilter = Object.seal(isProduction ? { [productionTargetFlag]: true } : {});
// Set the collection filter according to the request URL
// This filter is applied over the project metadata 'collections', nothing to do with mongo collections
// Note that unknown hosts (e.g. 'localhost:8000') will get all simulations, with no filter
const collectionFilter = Object.seal(hostCollection ? { 'metadata.COLLECTIONS': hostCollection } : {});
// Set the starting base filter
// This is a strict filter and it is applied even when a specific project is requested
const baseFilter = { ...publishedFilter, ...collectionFilter };
// If a specific project was requested then the base filter is returned as it is
// Note that the filter above are mandatory for "privacy" reasons
// In the sense that they are not to be skipped even if the requests asks for a specific accession
// The filters below, in the other hand, are to discard projects which are not important
// And they could make the query fail if the user asks for a booked/deleted entry
if (withAccession) return baseFilter;
// If no specific project was requested then the filter is extended
// We hide booked and deleted projects
// Note that these are not hidden when asking specifically for them
// Set the booked filter to remove booked projects from the query
// These are projects which are not yet uploaded
const bookedFilter = Object.seal({ booked: { $ne: true } });
// Set the deleted filter to remove deleted projects from the query
// These are projects which were deleted but the entry is kept to preserve the persistent id
const deletedFilter = Object.seal({ deleted: { $ne: true } });
// Return all filters together, including also the publsihed filter
return { ...baseFilter, ...bookedFilter, ...deletedFilter };
};
// Add additional functions
countOptions = (query, fields, shouldCountMds, useSavedCounts) =>
countOptions(this, query, fields, shouldCountMds, useSavedCounts);
// Close the connection to mongo and delete this handler
close = () => {
this.client.close();
delete this;
}
// Expose the ObjectId class to be used by other modules
static ObjectId = ObjectId
}
module.exports = {
databaseConnection,
Database,
}