-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
404 lines (304 loc) · 11.5 KB
/
index.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
/**
* @overview NodeJS webserver for server-side ccm data management via HTTP using MongoDB
* @author André Kless <andre.kless@web.de> 2018-2019
* @license MIT License
*/
// webserver configurations
const configs = require (`${__dirname}/configs.json`);
// used webserver configuration
const config = configs.local;
// load required npm modules
let mongodb = require( 'mongodb' );
const http = require( 'http' );
const deparam = require( 'node-jquery-deparam' );
const moment = require( 'moment' );
// create connection to MongoDB
connectMongoDB( () => { if ( !mongodb || !config.mongo ) console.log( 'No MongoDB found => Server runs without MongoDB.' );
// start webserver
startWebserver();
/** starts a HTTP webserver with websocket support */
function startWebserver() {
// create HTTP webserver
const http_server = http.createServer( handleRequest );
// start HTTP webserver
http_server.listen( config.http.port );
console.log( 'Server is running. Now you can use this URLs on client-side:' );
console.log( '- http://' + config.domain + ':' + config.http.port + ' (using HTTP protocol)' );
}
/**
* handles incoming HTTP requests
* @param request
* @param response
*/
function handleRequest( request, response ) {
// handle 'OPTION' requests
if ( request.method === 'OPTIONS' ) {
response.setHeader( 'Access-Control-Allow-Origin', '*' );
response.setHeader( 'Access-Control-Allow-Headers', 'Content-Type' );
response.statusCode = 200;
response.end();
return;
}
// receive HTTP parameter data
if ( request.method === 'POST' ) {
let body = '';
request.on( 'data', data => {
body += data;
if ( body.length > config.max_data_size )
request.shouldKeepAlive = false;
} );
request.on( 'end', () => {
if ( body.length > config.max_data_size ) {
response.statusCode = 413;
response.end();
}
else {
try {
proceed( JSON.parse( body ) );
} catch ( e ) {
response.statusCode = 403;
response.end();
}
}
} );
}
else
proceed( deparam( request.url.substr( 2 ) ) );
/** @param {*} data - received data */
function proceed( data ) {
// support cross domain requests via CORS
response.setHeader( 'Access-Control-Allow-Origin', '*' );
// received invalid data? => abort and send 'Forbidden'
if ( !checkReceivedData( data ) ) return sendForbidden();
// no database operation? => abort and send 'Forbidden'
if ( !data.get && !data.set && !data.del ) return sendForbidden();
// perform database operation
performDatabaseOperation( data, result => {
// send result to client
result === undefined ? sendForbidden() : send( data.get ? result : ( data.set ? result.key : true ) );
} );
/**
* sends response to client
* @param {*} response_data
*/
function send( response_data ) {
// response is not a string? => transform data to JSON string
response_data = typeof response_data !== 'string' ? JSON.stringify( response_data ) : response_data;
// set response HTTP header
response.writeHead( 200, { 'content-type': 'application/json; charset=utf-8' } );
// send response data to client
response.end( response_data );
}
/** sends 'Forbidden' status code */
function sendForbidden() {
response.statusCode = 403;
response.end();
}
}
}
/**
* checks if received data is valid
* @returns {boolean} false in case of invalid data
*/
function checkReceivedData( data ) {
if ( data.store && typeof data.store !== 'string' ) return false;
if ( data.get && !isKey( data.get ) && !isObject( data.get ) ) return false;
if ( data.set ) {
if ( !isObject( data.set ) ) return false;
if ( !data.set.key || !isKey( data.set.key ) ) return false;
}
if ( data.del && !isKey( data.del ) ) return false;
// received data is valid
return true;
}
/**
* performs database operation
* @param {Object} data - received data
* @param {function} callback - callback (first parameter is/are result(s))
*/
function performDatabaseOperation( data, callback ) {
// select kind of database
useMongoDB();
/** performs database operation in MongoDB */
function useMongoDB() {
// get collection
mongodb.collection( data.store, ( err, collection ) => {
// determine and perform correct database operation
if ( data.get ) get(); // read
else if ( data.set ) set(); // create or update
else if ( data.del ) del(); // delete
/** reads dataset(s) and performs callback with read dataset(s) */
function get() {
// perform read operation
getDataset( data.get, results => {
// finish operation
finish( results );
} );
}
/** creates or updates dataset and perform callback with created/updated dataset */
function set() {
// read existing dataset
getDataset( data.set.key, existing_dataset => {
/**
* priority data
* @type {ccm.types.dataset}
*/
const priodata = convertDataset( data.set );
// set 'updated_at' timestamp
priodata.updated_at = moment().format();
// dataset exists? (then it's an update operation)
if ( existing_dataset ) {
/**
* attributes that have to be unset
* @type {Object}
*/
const unset_data = {};
for ( const key in priodata )
if ( priodata[ key ] === '' ) {
unset_data[ key ] = priodata[ key ];
delete priodata[ key ];
}
// update dataset
if ( Object.keys( unset_data ).length > 0 )
collection.updateOne( { _id: priodata._id }, { $set: priodata, $unset: unset_data }, success );
else
collection.updateOne( { _id: priodata._id }, { $set: priodata }, success );
}
// create operation => add 'created_at' timestamp and perform create operation
else {
priodata.created_at = priodata.updated_at;
collection.insertOne( priodata, success );
}
/** when dataset is created/updated */
function success() {
// perform callback with created/updated dataset
getDataset( data.set.key, finish );
}
} );
}
/** deletes dataset and performs callback with deleted dataset */
function del() {
// read existing dataset
getDataset( data.del, existing_dataset => {
// delete dataset and perform callback with deleted dataset
collection.deleteOne( { _id: convertKey( data.del ) }, () => finish( existing_dataset ) );
} );
}
/**
* reads dataset(s)
* @param {ccm.types.key|Object} key_or_query - dataset key or MongoDB query
* @param {function} callback - callback (first parameter is/are read dataset(s))
*/
function getDataset( key_or_query, callback ) {
// read dataset(s)
collection.find( isObject( key_or_query ) ? key_or_query : { _id: convertKey( key_or_query ) } ).toArray( ( err, res ) => {
// when result is null
if ( !res ) return callback( res );
// convert MongoDB dataset(s) in ccm dataset(s)
for ( let i = 0; i < res.length; i++ )
res[ i ] = reconvertDataset( res[ i ] );
// read dataset by key? => result is dataset or NULL
if ( !isObject( key_or_query ) ) res = res.length ? res[ 0 ] : null;
// perform callback with reconverted result(s)
callback( res );
} );
}
/**
* converts ccm dataset key to MongoDB dataset key
* @param {ccm.types.key} key - ccm dataset key
* @returns {string} MongoDB dataset key
*/
function convertKey( key ) {
return Array.isArray( key ) ? key.join() : key;
}
/**
* converts MongoDB key to ccm dataset key
* @param {string} key - MongoDB dataset key
* @returns {ccm.types.key} ccm dataset key
*/
function reconvertKey( key ) {
return typeof key === 'string' && key.indexOf( ',' ) !== -1 ? key.split( ',' ) : key;
}
/**
* converts ccm dataset to MongoDB dataset
* @param {Object} ccm_dataset - ccm dataset
* @returns {ccm.types.dataset} MongoDB dataset
*/
function convertDataset( ccm_dataset ) {
const mongodb_dataset = clone( ccm_dataset );
mongodb_dataset._id = convertKey( mongodb_dataset.key );
delete mongodb_dataset.key;
return mongodb_dataset;
}
/**
* reconverts MongoDB dataset to ccm dataset
* @param {Object} mongodb_dataset - MongoDB dataset
* @returns {ccm.types.dataset} ccm dataset
*/
function reconvertDataset( mongodb_dataset ) {
const ccm_dataset = clone( mongodb_dataset );
ccm_dataset.key = reconvertKey( ccm_dataset._id );
delete ccm_dataset._id;
return ccm_dataset;
}
/**
* makes a deep copy of an object
* @param {Object} obj - object
* @returns {Object} deep copy of object
*/
function clone( obj ) {
return JSON.parse( JSON.stringify( obj ) );
}
} );
}
/** finishes database operation */
function finish( results ) {
// perform callback with result(s)
callback( results );
}
}
/**
* checks if a value is a valid ccm dataset key
* @param {*} value - value to check
* @returns {boolean}
*/
function isKey( value ) {
/**
* definition of a valid dataset key
* @type {RegExp}
*/
const regex = /^[a-zA-Z0-9_\-]+$/;
// value is a string? => check if it is an valid key
if ( typeof value === 'string' ) return regex.test( value );
// value is an array? => check if it is an valid array key
if ( Array.isArray( value ) ) {
for ( let i = 0; i < value.length; i++ )
if ( !regex.test( value[ i ] ) )
return false;
return true;
}
// value is not a dataset key? => not valid
return false;
}
/**
* checks value if it is an object (including not null and not array)
* @param {*} value - value to check
* @returns {boolean}
*/
function isObject( value ) {
return typeof value === 'object' && value !== null && !Array.isArray( value );
}
} );
/**
* creates a connection to MongoDB
* @param {function} callback
* @param {boolean} waited
*/
function connectMongoDB( callback, waited ) {
if ( !mongodb || !config.mongo ) return callback();
mongodb.MongoClient.connect( `mongodb://${config.mongo.host}:${config.mongo.port}`, { useNewUrlParser: true }, ( err, client ) => {
if ( !err ) { mongodb = client.db( config.mongo.db ); return callback(); }
if ( !waited ) setTimeout( () => connectMongoDB( callback, true ), 3000 );
else { mongodb = null; callback(); }
} );
}