-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathserver.js
473 lines (423 loc) · 17.8 KB
/
server.js
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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
require('dotenv').config()
const express = require('express')
const app = express()
// enable support for Cross-Origin Resource Sharing
const cors = require('cors')
app.use( cors() )
// interpret all body data in the incoming HTTP request as if it were JSON,
// regardless of whether the HTTP request header was set correctly as: Content-Type: application/json
app.use(express.json({ type: '*/*' }))
const testData = require('./testdata.json')
const statusTexts = [
'OK',
'Database connection failed',
'No match found in database',
'Invalid object type - allowed characters are: A-Z, a-z, 0-9, - and _',
'Please provide a valid id in the querystring, consisting of 24 characters',
'The API received invalid JSON in the request body. Please check your JSON syntax'
]
// function to check if a variable contains valid data
const validateParameter = (txt, type) => {
let valid
switch(type) {
case 'objectType':
// allowed characters for objectType are: A-Z, a-z, 0-9, - and _
valid = /^[0-9a-zA-Z\-_]+$/
break
case 'id':
// a MongoDB id should have 24 characters (numbers or lowercase letters)
valid = /^[0-9a-z]{24}$/
break
case 'number':
// a number can only have 0-9
valid = /^[0-9]+$/
break
default:
// other types cannot be checked,so just return false
return false
}
return valid.test(txt)
}
const { MongoClient, ServerApiVersion, ObjectId } = require('mongodb')
// Construct the URL used to connect to the database from the information in the .env file
const uri = `mongodb+srv://${process.env.DB_USERNAME}:${process.env.DB_PASS}@${process.env.DB_HOST}/${process.env.DB_NAME}?retryWrites=true&w=majority`
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
const client = new MongoClient(uri, {
serverApi: {
version: ServerApiVersion.v1,
strict: true,
deprecationErrors: true,
}
})
let dbStatus = false
// try to open a database connection
client.connect()
.then(() => {
console.log('Database connection established')
dbStatus = true
})
.catch((err) => {
console.log(`Database connection error - ${err}`)
console.log(`For uri - ${uri}`)
})
// this route responds with a status that indicates if the API has a working database connection
app.get('/maintenance/status', (req, res) => {
res.set('Content-Type', 'application/json')
if (dbStatus) {
// we're up and running
const response = `{
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// there is no working database connection
const response = `{
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
})
// this route fills the database with random test data of objectType test
app.get('/maintenance/generatetestdata', async(req, res) => {
res.set('Content-Type', 'application/json')
if (dbStatus) {
const collection = client.db(process.env.DB_NAME).collection(process.env.DB_COLLECTION)
// check if testdata already exists. If so, delete it and replace with new testdata
const data = await collection.find({ objectType: 'test' }).toArray()
if (data[0]) {
collection.deleteMany( {objectType: 'test'} )
}
for (let i = 0; i < 10; i++) {
let objectData = {
objectType: 'test',
data: {
name: testData.names[Math.floor(Math.random() * 49)],
age: Math.floor(Math.random() * 60 + 15),
profession: testData.professions[Math.floor(Math.random() * 49)]
}
}
collection.insertOne(objectData)
}
const response = `{
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// there is no working database connection
const response = `{
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
})
// this route empties the database
app.get('/maintenance/cleardatabase', async(req, res) => {
res.set('Content-Type', 'application/json')
if (dbStatus) {
const collection = client.db(process.env.DB_NAME).collection(process.env.DB_COLLECTION)
collection.deleteMany( {} )
const response = `{
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// there is no working database connection
const response = `{
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
})
// this route returns data of the requested objectType from the database.
// It is possible to search for a specific ID or a certain field in the data by adding a search parameter to the querystring
// If the querystring is empty, all existing objects are returned
app.get('/:objectType', async(req, res) => {
res.set('Content-Type', 'application/json')
if (validateParameter(req.params.objectType, 'objectType')) {
if (dbStatus) {
const collection = client.db(process.env.DB_NAME).collection(process.env.DB_COLLECTION)
let data, dbQuery
// if the user requests a specific id in the querystring, see if it is valid
// if so, try to find this id in the database. If the id is invalid, no data is returned by default
if (req.query.id ) {
if ( validateParameter(req.query.id, 'id') ) {
let id = req.query.id
data = await collection.find({ objectType: req.params.objectType, _id: new ObjectId(id) }).toArray()
} else {
data = []
}
} else {
// if no id was specified, see if there was another search parameter in the querystring. If so, construct a db query to search for requested data
// if multiple parameters are provided, the first is used and the rest is ignored
if ( Object.keys(req.query).length ) {
// TODO: sanitize searchField and searchString
const searchField = Object.keys(req.query)[0]
const searchString = req.query[searchField ]
// searchString is taken from the querystring in the URL of the HTTP request, which always has datatype string
// so, we cannot really tell if the user wanted to search for a string or another specifc datatype
// when in doubt, we'll search for both the string and the alternative datatype
if ( validateParameter(searchString, 'number') ) {
// if the searchString contains an (integer) number, search for both an integer or a string representing this number
dbQuery = JSON.parse(`{
"objectType": "${req.params.objectType}",
"$or": [
{"data.${searchField}": ${searchString}},
{"data.${searchField}": "${searchString}"}
]
}`)
} else if (searchString == 'true' || searchString == 'false' || searchString == 'null') {
// if the searchString is a JavaScript literal (true, false or null), search for both the literal or a string representing this literal
dbQuery = JSON.parse(`{
"objectType": "${req.params.objectType}",
"$or": [
{"data.${searchField}": ${searchString}},
{"data.${searchField}": "${searchString}"}
]
}`)
} else {
// otherwise just do a regular text search
dbQuery = JSON.parse(`{
"objectType": "${req.params.objectType}",
"data.${searchField}": "${searchString}"
}`)
}
data = await collection.find(dbQuery).toArray()
} else {
// no search paramaters were specified, so just find all instances of this objectType
data = await collection.find({ objectType: req.params.objectType }).toArray()
}
}
if (data[0]) {
// send the results back in JSON format
const response = `{
"records": ${JSON.stringify(data)},
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// no matching results were found
const response = `{
"records": [],
"statusCode": 2,
"statusText": "${statusTexts[2]}"
}`
res.send(response)
}
} else {
// there is no working database connection
const response = `{
"records": [],
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
} else {
// the requested objectType had invalid characters
const response = `{
"records": [],
"statusCode": 3,
"statusText": "${statusTexts[3]}"
}`
res.send(response)
}
})
// Update a record of the specified objectType with new data.
// The MongoDB id of the record needs to be specified in the querystring
app.patch('/:objectType', async(req, res) => {
res.set('Content-Type', 'application/json')
if (validateParameter(req.params.objectType, 'objectType')) {
if (dbStatus) {
const collection = client.db(process.env.DB_NAME).collection(process.env.DB_COLLECTION)
// The user needs to requests a specific id in the querystring, see if it is valid
if ( req.query.id && validateParameter(req.query.id, 'id') ) {
const id = req.query.id
let updateFields = '{'
let i = 0
for(let key in req.body) {
// if the user wants to update multiple fields at once, seperate them with a ,
if (i != 0) {
updateFields += ', '
}
i++
// if the value is a number or a literal (true, false or null), don't add quotes around the value. For a string, do add quotes
if ( validateParameter(req.body[key], 'number') ) {
updateFields += `"data.${key}": ${req.body[key]}`
} else if (req.body[key] == true || req.body[key] == false || req.body[key] == null) {
updateFields += `"data.${key}": ${req.body[key]}`
} else {
updateFields += `"data.${key}": "${req.body[key]}"`
}
}
updateFields += '}'
const result = await collection.updateOne({ objectType: req.params.objectType, _id: new ObjectId(id) }, {$set: JSON.parse(updateFields) })
if (result.matchedCount) {
// send the result back in JSON format
const response = `{
"itemsModified": ${result.modifiedCount},
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// no matching results were found
const response = `{
"itemsModified": 0,
"statusCode": 2,
"statusText": "${statusTexts[2]}"
}`
res.send(response)
}
} else {
// No id was provided or the id was invalid
const response = `{
"itemsModified": 0,
"statusCode": 4,
"statusText": "${statusTexts[4]}"
}`
res.send(response)
}
} else {
// there is no working database connection
const response = `{
"itemsModified": 0,
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
} else {
// the requested objectType had invalid characters
const response = `{
"itemsModified": 0,
"statusCode": 3,
"statusText": "${statusTexts[3]}"
}`
res.send(response)
}
})
// Add a record of the specified objectType with new data.
// The MongoDB id of the new record is returned
app.post('/:objectType', async(req, res) => {
res.set('Content-Type', 'application/json')
if (validateParameter(req.params.objectType, 'objectType')) {
if (dbStatus) {
const collection = client.db(process.env.DB_NAME).collection(process.env.DB_COLLECTION)
const result = await collection.insertOne({ objectType: req.params.objectType, data: req.body })
const response = `{
"_id": "${result['insertedId']}",
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// there is no working database connection
const response = `{
"_id": null,
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
} else {
// the requested objectType had invalid characters
const response = `{
"_id": null,
"statusCode": 3,
"statusText": "${statusTexts[3]}"
}`
res.send(response)
}
})
// Delete a record of the specified objectType with new data.
// The MongoDB id of the record needs to be specified in the querystring
app.delete('/:objectType', async(req, res) => {
res.set('Content-Type', 'application/json')
if (validateParameter(req.params.objectType, 'objectType')) {
if (dbStatus) {
const collection = client.db(process.env.DB_NAME).collection(process.env.DB_COLLECTION)
// if the user requests a specific id in the querystring, see if it is valid
// if so, try to find this id in the database.
if (req.query.id && validateParameter(req.query.id, 'id') ) {
const id = req.query.id
const result = await collection.deleteOne({ objectType: req.params.objectType, _id: new ObjectId(id) })
if (result['deletedCount']) {
// an item was deleted
const response = `{
"itemsDeleted": ${result['deletedCount']},
"statusCode": 0,
"statusText": "${statusTexts[0]}"
}`
res.send(response)
} else {
// no item was deleted, no match was found for the provided id and objectType
const response = `{
"itemsDeleted": 0,
"statusCode": 2,
"statusText": "${statusTexts[2]}"
}`
res.send(response)
}
} else {
// no id found in querystring or invalid id provided
const response = `{
"itemsDeleted": 0,
"statusCode": 4,
"statusText": "${statusTexts[4]}"
}`
res.send(response)
}
} else {
// there is no working database connection
const response = `{
"itemsDeleted": 0,
"statusCode": 1,
"statusText": "${statusTexts[1]}"
}`
res.send(response)
}
} else {
// the requested objectType had invalid characters
const response = `{
"itemsDeleted": 0,
"statusCode": 3,
"statusText": "${statusTexts[3]}"
}`
res.send(response)
}
})
// middleware to handle 404 errors
app.use((req, res, next) => {
console.log('404 error at URL: ' + req.url)
res.status(404).sendFile('error_pages/404.html', {root: __dirname})
})
// middleware to handle server errors
app.use((err, req, res, next) => {
if (err.type == 'entity.parse.failed') {
// if the user send invalid JSON to the API, send back an error message
res.set('Content-Type', 'application/json')
const response = `{
"errorMessage": "${err.message}",
"statusCode": 5,
"statusText": "${statusTexts[5]}"
}`
res.send(response)
} else {
// something else has gone wrong
console.log('500 error at URL: ' + req.url)
res.status(500).sendFile('error_pages/500.html', {root: __dirname})
}
// log the error details to the console
console.error(err.stack)
})
// start the webserver
app.listen(process.env.PORT, () => {
console.log(`Project Tech Data API listening on port ${process.env.PORT}`)
})