Merge pull request #47 in DEFINMA/definma-api from improved-comments to master
* commit 'edc68bd6d2724d134afa55f48243f18d347a324c': Fixed uppercase change for commented out code Change first character of all end-of-line comments to upper case
This commit is contained in:
		
							
								
								
									
										32
									
								
								src/api.ts
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								src/api.ts
									
									
									
									
									
								
							@@ -4,22 +4,22 @@ import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser';
 | 
				
			|||||||
import oasParser from '@apidevtools/swagger-parser';
 | 
					import oasParser from '@apidevtools/swagger-parser';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// modified from https://github.com/scottie1984/swagger-ui-express
 | 
					// Modified from https://github.com/scottie1984/swagger-ui-express
 | 
				
			||||||
// usage: app.use('/api-doc', api.serve(), api.setup());
 | 
					// Usage: app.use('/api-doc', api.serve(), api.setup());
 | 
				
			||||||
// the paths property can be split using allOf
 | 
					// The paths property can be split using allOf
 | 
				
			||||||
// further route documentation can be included in the x-doc property
 | 
					// Further route documentation can be included in the x-doc property
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function api () {
 | 
					export default function api () {
 | 
				
			||||||
  // generate apiDoc
 | 
					  // Generate apiDoc
 | 
				
			||||||
  let apiDoc: JSONSchema = {};
 | 
					  let apiDoc: JSONSchema = {};
 | 
				
			||||||
  jsonRefParser.bundle('api/api.yaml', (err, doc) => {  // parse yaml
 | 
					  jsonRefParser.bundle('api/api.yaml', (err, doc) => {  // Parse yaml
 | 
				
			||||||
    if (err) throw err;
 | 
					    if (err) throw err;
 | 
				
			||||||
    apiDoc = doc;
 | 
					    apiDoc = doc;
 | 
				
			||||||
    apiDoc.servers.splice(process.env.NODE_ENV === 'production', 1);
 | 
					    apiDoc.servers.splice(process.env.NODE_ENV === 'production', 1);
 | 
				
			||||||
    apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e));  // bundle routes
 | 
					    apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e));  // Bundle routes
 | 
				
			||||||
    apiDoc = resolveXDoc(apiDoc);
 | 
					    apiDoc = resolveXDoc(apiDoc);
 | 
				
			||||||
    oasParser.validate(apiDoc, (err, api) => {  // validate oas schema
 | 
					    oasParser.validate(apiDoc, (err, api) => {  // Validate oas schema
 | 
				
			||||||
      if (err) {
 | 
					      if (err) {
 | 
				
			||||||
        console.error(err);
 | 
					        console.error(err);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -30,7 +30,7 @@ export default function api () {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return [
 | 
					  return [
 | 
				
			||||||
    (req, res, next) => {  // serve init js and apiDoc file
 | 
					    (req, res, next) => {  // Serve init js and apiDoc file
 | 
				
			||||||
      switch (req.url) {
 | 
					      switch (req.url) {
 | 
				
			||||||
        case '/swagger-ui-init.js':
 | 
					        case '/swagger-ui-init.js':
 | 
				
			||||||
          res.set('Content-Type', 'application/javascript');
 | 
					          res.set('Content-Type', 'application/javascript');
 | 
				
			||||||
@@ -43,21 +43,21 @@ export default function api () {
 | 
				
			|||||||
        default:
 | 
					        default:
 | 
				
			||||||
          next();
 | 
					          next();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },  // serve swagger files
 | 
					    },  // Serve swagger files
 | 
				
			||||||
    express.static(swaggerUi.getAbsoluteFSPath(), {index: false}),
 | 
					    express.static(swaggerUi.getAbsoluteFSPath(), {index: false}),
 | 
				
			||||||
    (req, res) => {  // serve html file as default
 | 
					    (req, res) => {  // Serve html file as default
 | 
				
			||||||
      res.send(htmlTplString);
 | 
					      res.send(htmlTplString);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function resolveXDoc (doc) {  // resolve x-doc properties recursively
 | 
					function resolveXDoc (doc) {  // Resolve x-doc properties recursively
 | 
				
			||||||
  Object.keys(doc).forEach(key => {
 | 
					  Object.keys(doc).forEach(key => {
 | 
				
			||||||
    if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) {  // add x-doc to description, is styled via css
 | 
					    if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) {  // Add x-doc to description, is styled via css
 | 
				
			||||||
      doc[key].description += '<details class="docs"><summary>docs</summary>' + doc[key]['x-doc'] + '</details>';
 | 
					      doc[key].description += '<details class="docs"><summary>docs</summary>' + doc[key]['x-doc'] + '</details>';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (typeof doc[key] === 'object' && doc[key] !== null) {  // go deeper into recursion
 | 
					    else if (typeof doc[key] === 'object' && doc[key] !== null) {  // Go deeper into recursion
 | 
				
			||||||
      doc[key] = resolveXDoc(doc[key]);
 | 
					      doc[key] = resolveXDoc(doc[key]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -65,9 +65,9 @@ function resolveXDoc (doc) {  // resolve x-doc properties recursively
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// templates
 | 
					// Templates
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// noinspection LongLine
 | 
					// Noinspection LongLine
 | 
				
			||||||
const htmlTplString = `
 | 
					const htmlTplString = `
 | 
				
			||||||
<!DOCTYPE html>
 | 
					<!DOCTYPE html>
 | 
				
			||||||
<html lang="en">
 | 
					<html lang="en">
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										72
									
								
								src/db.ts
									
									
									
									
									
								
							
							
						
						
									
										72
									
								
								src/db.ts
									
									
									
									
									
								
							@@ -5,29 +5,29 @@ import ChangelogModel from './models/changelog';
 | 
				
			|||||||
import cron from 'node-cron';
 | 
					import cron from 'node-cron';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// database urls, prod db url is retrieved automatically
 | 
					// Database urls, prod db url is retrieved automatically
 | 
				
			||||||
const TESTING_URL = 'mongodb://localhost/dfopdb_test';
 | 
					const TESTING_URL = 'mongodb://localhost/dfopdb_test';
 | 
				
			||||||
const DEV_URL = 'mongodb://localhost/dfopdb';
 | 
					const DEV_URL = 'mongodb://localhost/dfopdb';
 | 
				
			||||||
const debugging = true;
 | 
					const debugging = true;
 | 
				
			||||||
const changelogKeepDays = 30;  // days to keep the changelog
 | 
					const changelogKeepDays = 30;  // Days to keep the changelog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (process.env.NODE_ENV !== 'production' && debugging) {
 | 
					if (process.env.NODE_ENV !== 'production' && debugging) {
 | 
				
			||||||
  mongoose.set('debug', true);  // enable mongoose debug
 | 
					  mongoose.set('debug', true);  // Enable mongoose debug
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class db {
 | 
					export default class db {
 | 
				
			||||||
  private static state = {  // db object and current mode (test, dev, prod)
 | 
					  private static state = {  // Db object and current mode (test, dev, prod)
 | 
				
			||||||
    db: null,
 | 
					    db: null,
 | 
				
			||||||
    mode: null,
 | 
					    mode: null,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // set mode to test for unit/integration tests, otherwise skip parameters. done is also only needed for testing
 | 
					  // Set mode to test for unit/integration tests, otherwise skip parameters. done is also only needed for testing
 | 
				
			||||||
  static connect (mode = '', done: Function = () => {}) {
 | 
					  static connect (mode = '', done: Function = () => {}) {
 | 
				
			||||||
    if (this.state.db) return done();  // db is already connected
 | 
					    if (this.state.db) return done();  // Db is already connected
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // find right connection url
 | 
					    // Find right connection url
 | 
				
			||||||
    let connectionString: string = "";
 | 
					    let connectionString: string = "";
 | 
				
			||||||
    if (mode === 'test') {  // testing
 | 
					    if (mode === 'test') {  // Testing
 | 
				
			||||||
      connectionString = TESTING_URL;
 | 
					      connectionString = TESTING_URL;
 | 
				
			||||||
      this.state.mode = 'test';
 | 
					      this.state.mode = 'test';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -45,7 +45,7 @@ export default class db {
 | 
				
			|||||||
      this.state.mode = 'dev';
 | 
					      this.state.mode = 'dev';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // connect to db
 | 
					    // Connect to db
 | 
				
			||||||
    mongoose.connect(connectionString, {
 | 
					    mongoose.connect(connectionString, {
 | 
				
			||||||
      useNewUrlParser: true,
 | 
					      useNewUrlParser: true,
 | 
				
			||||||
      useUnifiedTopology: true,
 | 
					      useUnifiedTopology: true,
 | 
				
			||||||
@@ -55,19 +55,19 @@ export default class db {
 | 
				
			|||||||
      if (err) done(err);
 | 
					      if (err) done(err);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
 | 
					    mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
 | 
				
			||||||
    mongoose.connection.on('connected', () => {  // evaluation connection behaviour on prod
 | 
					    mongoose.connection.on('connected', () => {  // Evaluation connection behaviour on prod
 | 
				
			||||||
      if (process.env.NODE_ENV !== 'test') {  // Do not interfere with testing
 | 
					      if (process.env.NODE_ENV !== 'test') {  // Do not interfere with testing
 | 
				
			||||||
        console.info('Database connected');
 | 
					        console.info('Database connected');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    mongoose.connection.on('disconnected', () => {  // reset state on disconnect
 | 
					    mongoose.connection.on('disconnected', () => {  // Reset state on disconnect
 | 
				
			||||||
      if (process.env.NODE_ENV !== 'test') {  // Do not interfere with testing
 | 
					      if (process.env.NODE_ENV !== 'test') {  // Do not interfere with testing
 | 
				
			||||||
        console.info('Database disconnected');
 | 
					        console.info('Database disconnected');
 | 
				
			||||||
        // this.state.db = 0;   // prod database connects and disconnects automatically
 | 
					        // This.state.db = 0;   // prod database connects and disconnects automatically
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    process.on('SIGINT', () => {  // close connection when app is terminated
 | 
					    process.on('SIGINT', () => {  // Close connection when app is terminated
 | 
				
			||||||
      if (!this.state.db) {  // database still connected
 | 
					      if (!this.state.db) {  // Database still connected
 | 
				
			||||||
        mongoose.connection.close(() => {
 | 
					        mongoose.connection.close(() => {
 | 
				
			||||||
          console.info('Mongoose default connection disconnected through app termination');
 | 
					          console.info('Mongoose default connection disconnected through app termination');
 | 
				
			||||||
          process.exit(0);
 | 
					          process.exit(0);
 | 
				
			||||||
@@ -81,9 +81,9 @@ export default class db {
 | 
				
			|||||||
      done();
 | 
					      done();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (mode !== 'test') {  // clear old changelog regularly
 | 
					    if (mode !== 'test') {  // Clear old changelog regularly
 | 
				
			||||||
      cron.schedule('0 0 * * *', () => {
 | 
					      cron.schedule('0 0 * * *', () => {
 | 
				
			||||||
        ChangelogModel.deleteMany({_id: {$lt:  // id from time
 | 
					        ChangelogModel.deleteMany({_id: {$lt:  // Id from time
 | 
				
			||||||
          Math.floor(new Date().getTime() / 1000 - changelogKeepDays * 24 * 60 * 60).toString(16) + '0000000000000000'
 | 
					          Math.floor(new Date().getTime() / 1000 - changelogKeepDays * 24 * 60 * 60).toString(16) + '0000000000000000'
 | 
				
			||||||
        }}).lean().exec(err => {
 | 
					        }}).lean().exec(err => {
 | 
				
			||||||
          if (err) console.error(err);
 | 
					          if (err) console.error(err);
 | 
				
			||||||
@@ -104,18 +104,18 @@ export default class db {
 | 
				
			|||||||
    return this.state;
 | 
					    return this.state;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // drop all collections of connected db (only dev and test for safety reasons)
 | 
					  // Drop all collections of connected db (only dev and test for safety reasons)
 | 
				
			||||||
  static drop (done: Function = () => {}) {
 | 
					  static drop (done: Function = () => {}) {
 | 
				
			||||||
    if (!this.state.db || this.state.mode === 'prod') return done();  // no db connection or prod db
 | 
					    if (!this.state.db || this.state.mode === 'prod') return done();  // No db connection or prod db
 | 
				
			||||||
    this.state.db.db.listCollections().toArray((err, collections) => {  // get list of all collections
 | 
					    this.state.db.db.listCollections().toArray((err, collections) => {  // Get list of all collections
 | 
				
			||||||
      if (collections.length === 0) {  // there are no collections to drop
 | 
					      if (collections.length === 0) {  // There are no collections to drop
 | 
				
			||||||
        return done();
 | 
					        return done();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      else {
 | 
					      else {
 | 
				
			||||||
        let dropCounter = 0;  // count number of dropped collections to know when to return done()
 | 
					        let dropCounter = 0;  // Count number of dropped collections to know when to return done()
 | 
				
			||||||
        collections.forEach(collection => {  // drop each collection
 | 
					        collections.forEach(collection => {  // Drop each collection
 | 
				
			||||||
          this.state.db.dropCollection(collection.name, () => {
 | 
					          this.state.db.dropCollection(collection.name, () => {
 | 
				
			||||||
            if (++ dropCounter >= collections.length) {  // all collections dropped
 | 
					            if (++ dropCounter >= collections.length) {  // All collections dropped
 | 
				
			||||||
              done();
 | 
					              done();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
@@ -124,21 +124,21 @@ export default class db {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static loadJson (json, done: Function = () => {}) {  // insert given JSON data into db, uses core mongodb methods
 | 
					  static loadJson (json, done: Function = () => {}) {  // Insert given JSON data into db, uses core mongodb methods
 | 
				
			||||||
    // no db connection or nothing to load
 | 
					    // No db connection or nothing to load
 | 
				
			||||||
    if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) {
 | 
					    if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) {
 | 
				
			||||||
      return done();
 | 
					      return done();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let loadCounter = 0;  // count number of loaded collections to know when to return done()
 | 
					    let loadCounter = 0;  // Count number of loaded collections to know when to return done()
 | 
				
			||||||
    Object.keys(json.collections).forEach(collectionName => {  // create each collection
 | 
					    Object.keys(json.collections).forEach(collectionName => {  // Create each collection
 | 
				
			||||||
      json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
 | 
					      json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
 | 
				
			||||||
      this.state.db.createCollection(collectionName, (err, collection) => {
 | 
					      this.state.db.createCollection(collectionName, (err, collection) => {
 | 
				
			||||||
        if (err) {
 | 
					        if (err) {
 | 
				
			||||||
          console.error(err);
 | 
					          console.error(err);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        collection.insertMany(json.collections[collectionName], () => {  // insert JSON data
 | 
					        collection.insertMany(json.collections[collectionName], () => {  // Insert JSON data
 | 
				
			||||||
          if (++ loadCounter >= Object.keys(json.collections).length) {  // all collections loaded
 | 
					          if (++ loadCounter >= Object.keys(json.collections).length) {  // All collections loaded
 | 
				
			||||||
            done();
 | 
					            done();
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@@ -146,11 +146,11 @@ export default class db {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // changelog entry, expects (req, this (from query helper)) or (req, collection, conditions, data)
 | 
					  // Changelog entry, expects (req, this (from query helper)) or (req, collection, conditions, data)
 | 
				
			||||||
  static log(req, thisOrCollection, conditions = null, data = null) {
 | 
					  static log(req, thisOrCollection, conditions = null, data = null) {
 | 
				
			||||||
    if (! (conditions || data)) {  // (req, this)
 | 
					    if (! (conditions || data)) {  // (req, this)
 | 
				
			||||||
      data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {};  // replace undefined with {}
 | 
					      data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {};  // Replace undefined with {}
 | 
				
			||||||
      // replace keys with a leading $
 | 
					      // Replace keys with a leading $
 | 
				
			||||||
      Object.keys(data).forEach(key => {
 | 
					      Object.keys(data).forEach(key => {
 | 
				
			||||||
        if (key[0] === '$') {
 | 
					        if (key[0] === '$') {
 | 
				
			||||||
          data[key.substr(1)] = data[key];
 | 
					          data[key.substr(1)] = data[key];
 | 
				
			||||||
@@ -180,19 +180,19 @@ export default class db {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static oidResolve (object: any) {  // resolve $oid fields to actual ObjectIds recursively
 | 
					  private static oidResolve (object: any) {  // Resolve $oid fields to actual ObjectIds recursively
 | 
				
			||||||
    Object.keys(object).forEach(key => {
 | 
					    Object.keys(object).forEach(key => {
 | 
				
			||||||
      if (object[key] !== null && object[key].hasOwnProperty('$oid')) {  // found oid, replace
 | 
					      if (object[key] !== null && object[key].hasOwnProperty('$oid')) {  // Found oid, replace
 | 
				
			||||||
        object[key] = mongoose.Types.ObjectId(object[key].$oid);
 | 
					        object[key] = mongoose.Types.ObjectId(object[key].$oid);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      else if (typeof object[key] === 'object' && object[key] !== null) {  // deeper into recursion
 | 
					      else if (typeof object[key] === 'object' && object[key] !== null) {  // Deeper into recursion
 | 
				
			||||||
        object[key] = this.oidResolve(object[key]);
 | 
					        object[key] = this.oidResolve(object[key]);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    return object;
 | 
					    return object;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private static logEscape(obj) {  // replace MongoDB control characters in keys
 | 
					  private static logEscape(obj) {  // Replace MongoDB control characters in keys
 | 
				
			||||||
    if (Object(obj) === obj && Object.keys(obj).length > 0) {
 | 
					    if (Object(obj) === obj && Object.keys(obj).length > 0) {
 | 
				
			||||||
      Object.keys(obj).forEach(key => {
 | 
					      Object.keys(obj).forEach(key => {
 | 
				
			||||||
        const safeKey = key.replace(/[$.]/g, '');
 | 
					        const safeKey = key.replace(/[$.]/g, '');
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
// globals for required names in the database. change values here to rename these properties
 | 
					// Globals for required names in the database. change values here to rename these properties
 | 
				
			||||||
// the keys are the terms used internally, the values can be changed to other terms used in database and output
 | 
					// The keys are the terms used internally, the values can be changed to other terms used in database and output
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const globals = {
 | 
					const globals = {
 | 
				
			||||||
  levels: {  // access levels, sorted asc by rights
 | 
					  levels: {  // Access levels, sorted asc by rights
 | 
				
			||||||
    predict: 'predict',
 | 
					    predict: 'predict',
 | 
				
			||||||
    read: 'read',
 | 
					    read: 'read',
 | 
				
			||||||
    write: 'write',
 | 
					    write: 'write',
 | 
				
			||||||
@@ -10,13 +10,13 @@ const globals = {
 | 
				
			|||||||
    admin: 'admin'
 | 
					    admin: 'admin'
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  status: {  // names of the document statuses
 | 
					  status: {  // Names of the document statuses
 | 
				
			||||||
    del: 'deleted',
 | 
					    del: 'deleted',
 | 
				
			||||||
    new: 'new',
 | 
					    new: 'new',
 | 
				
			||||||
    val: 'validated',
 | 
					    val: 'validated',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  spectrum: {  // names of required spectrum fields
 | 
					  spectrum: {  // Names of required spectrum fields
 | 
				
			||||||
    spectrum: 'spectrum',
 | 
					    spectrum: 'spectrum',
 | 
				
			||||||
    dpt: 'dpt'
 | 
					    dpt: 'dpt'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,22 +4,22 @@ import UserModel from '../models/user';
 | 
				
			|||||||
import globals from '../globals';
 | 
					import globals from '../globals';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// appends req.auth(res, ['levels'], method = 'all')
 | 
					// Appends req.auth(res, ['levels'], method = 'all')
 | 
				
			||||||
// which returns sends error message and returns false if unauthorized, otherwise true
 | 
					// Which returns sends error message and returns false if unauthorized, otherwise true
 | 
				
			||||||
// req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'}
 | 
					// Req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = async (req, res, next) => {
 | 
					module.exports = async (req, res, next) => {
 | 
				
			||||||
	let givenMethod = '';  // authorization method given by client, basic taken preferred
 | 
						let givenMethod = '';  // Authorization method given by client, basic taken preferred
 | 
				
			||||||
	let user = {name: '', level: '', id: '', location: '', models: []};               // user object
 | 
						let user = {name: '', level: '', id: '', location: '', models: []};               // User object
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// test authentications
 | 
						// Test authentications
 | 
				
			||||||
	const userBasic = await basic(req, next);
 | 
						const userBasic = await basic(req, next);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (userBasic) {  // basic available
 | 
						if (userBasic) {  // Basic available
 | 
				
			||||||
		givenMethod = 'basic';
 | 
							givenMethod = 'basic';
 | 
				
			||||||
		user = userBasic;
 | 
							user = userBasic;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	else {  // if basic not available, test key
 | 
						else {  // If basic not available, test key
 | 
				
			||||||
		const userKey = await key(req, next);
 | 
							const userKey = await key(req, next);
 | 
				
			||||||
	if (userKey) {
 | 
						if (userKey) {
 | 
				
			||||||
		givenMethod = 'key';
 | 
							givenMethod = 'key';
 | 
				
			||||||
@@ -28,8 +28,8 @@ module.exports = async (req, res, next) => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req.auth = (res, levels, method = 'all') => {
 | 
						req.auth = (res, levels, method = 'all') => {
 | 
				
			||||||
		if (givenMethod === method || (method === 'all' && givenMethod !== '')) {  // method is available
 | 
							if (givenMethod === method || (method === 'all' && givenMethod !== '')) {  // Method is available
 | 
				
			||||||
			if (levels.indexOf(user.level) > -1) {  // level is available
 | 
								if (levels.indexOf(user.level) > -1) {  // Level is available
 | 
				
			||||||
				return true;
 | 
									return true;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else {
 | 
								else {
 | 
				
			||||||
@@ -56,16 +56,16 @@ module.exports = async (req, res, next) => {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function basic (req, next): any {  // checks basic auth and returns changed user object
 | 
					function basic (req, next): any {  // Checks basic auth and returns changed user object
 | 
				
			||||||
	return new Promise(resolve => {
 | 
						return new Promise(resolve => {
 | 
				
			||||||
		const auth = basicAuth(req);
 | 
							const auth = basicAuth(req);
 | 
				
			||||||
		if (auth !== undefined) {  // basic auth available
 | 
							if (auth !== undefined) {  // Basic auth available
 | 
				
			||||||
			UserModel.find({name: auth.name, status: globals.status.new}).lean().exec( (err, data: any) => {  // find user
 | 
								UserModel.find({name: auth.name, status: globals.status.new}).lean().exec( (err, data: any) => {  // Find user
 | 
				
			||||||
				if (err) return next(err);
 | 
									if (err) return next(err);
 | 
				
			||||||
				if (data.length === 1) {  // one user found
 | 
									if (data.length === 1) {  // One user found
 | 
				
			||||||
					bcrypt.compare(auth.pass, data[0].pass, (err, res) => {  // check password
 | 
										bcrypt.compare(auth.pass, data[0].pass, (err, res) => {  // Check password
 | 
				
			||||||
						if (err) return next(err);
 | 
											if (err) return next(err);
 | 
				
			||||||
						if (res === true) {  // password correct
 | 
											if (res === true) {  // Password correct
 | 
				
			||||||
							resolve({
 | 
												resolve({
 | 
				
			||||||
								level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0],
 | 
													level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0],
 | 
				
			||||||
								name: data[0].name,
 | 
													name: data[0].name,
 | 
				
			||||||
@@ -90,12 +90,12 @@ function basic (req, next): any {  // checks basic auth and returns changed user
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function key (req, next): any {  // checks API key and returns changed user object
 | 
					function key (req, next): any {  // Checks API key and returns changed user object
 | 
				
			||||||
	return new Promise(resolve => {
 | 
						return new Promise(resolve => {
 | 
				
			||||||
		if (req.query.key !== undefined) {  // key available
 | 
							if (req.query.key !== undefined) {  // Key available
 | 
				
			||||||
			UserModel.find({key: req.query.key, status: globals.status.new}).lean().exec( (err, data: any) => {  // find user
 | 
								UserModel.find({key: req.query.key, status: globals.status.new}).lean().exec( (err, data: any) => {  // Find user
 | 
				
			||||||
				if (err) return next(err);
 | 
									if (err) return next(err);
 | 
				
			||||||
				if (data.length === 1) {  // one user found
 | 
									if (data.length === 1) {  // One user found
 | 
				
			||||||
					resolve({
 | 
										resolve({
 | 
				
			||||||
						level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0],
 | 
											level: Object.entries(globals.levels).find(e => e[1] === data[0].level)[0],
 | 
				
			||||||
						name: data[0].name,
 | 
											name: data[0].name,
 | 
				
			||||||
@@ -104,7 +104,7 @@ function key (req, next): any {  // checks API key and returns changed user obje
 | 
				
			|||||||
						models: data[0].models
 | 
											models: data[0].models
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
					if (!/^\/api/m.test(req.url)){
 | 
										if (!/^\/api/m.test(req.url)){
 | 
				
			||||||
						delete req.query.key;  // delete query parameter to avoid interference with later validation
 | 
											delete req.query.key;  // Delete query parameter to avoid interference with later validation
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else {
 | 
									else {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import {parseAsync} from 'json2csv';
 | 
					import {parseAsync} from 'json2csv';
 | 
				
			||||||
import flatten from './flatten';
 | 
					import flatten from './flatten';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function csv(input: any[], f: (err, data) => void) {  // parse JSON to CSV
 | 
					export default function csv(input: any[], f: (err, data) => void) {  // Parse JSON to CSV
 | 
				
			||||||
	parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true})
 | 
						parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true})
 | 
				
			||||||
	.then(csv => f(null, csv))
 | 
						.then(csv => f(null, csv))
 | 
				
			||||||
	.catch(err => f(err, null));
 | 
						.catch(err => f(err, null));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,12 @@
 | 
				
			|||||||
import globals from '../globals';
 | 
					import globals from '../globals';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function flatten (data, keepArray = false) {  // flatten object: {a: {b: true}} -> {a.b: true}
 | 
					export default function flatten (data, keepArray = false) {  // Flatten object: {a: {b: true}} -> {a.b: true}
 | 
				
			||||||
	const result = {};
 | 
						const result = {};
 | 
				
			||||||
	function recurse (cur, prop) {
 | 
						function recurse (cur, prop) {
 | 
				
			||||||
		if (Object(cur) !== cur || Object.keys(cur).length === 0) {  // simple value
 | 
							if (Object(cur) !== cur || Object.keys(cur).length === 0) {  // Simple value
 | 
				
			||||||
			result[prop] = cur;
 | 
								result[prop] = cur;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else if (prop === `${globals.spectrum.spectrum}.${globals.spectrum.dpt}`) {  // convert spectrum for ML
 | 
							else if (prop === `${globals.spectrum.spectrum}.${globals.spectrum.dpt}`) {  // Convert spectrum for ML
 | 
				
			||||||
			result[prop + '.labels'] = cur.map(e => parseFloat(e[0]));
 | 
								result[prop + '.labels'] = cur.map(e => parseFloat(e[0]));
 | 
				
			||||||
		result[prop + '.values'] = cur.map(e => parseFloat(e[1]));
 | 
							result[prop + '.values'] = cur.map(e => parseFloat(e[1]));
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -14,8 +14,8 @@ export default function flatten (data, keepArray = false) {  // flatten object:
 | 
				
			|||||||
			if (keepArray) {
 | 
								if (keepArray) {
 | 
				
			||||||
				result[prop] = cur;
 | 
									result[prop] = cur;
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else {  // array to string
 | 
								else {  // Array to string
 | 
				
			||||||
				if (cur.length && (Object(cur[0]) !== cur || Object.keys(cur[0]).length === 0)) {  // array of non-objects
 | 
									if (cur.length && (Object(cur[0]) !== cur || Object.keys(cur[0]).length === 0)) {  // Array of non-objects
 | 
				
			||||||
					result[prop] = '[' + cur.join(', ') + ']';
 | 
										result[prop] = '[' + cur.join(', ') + ']';
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
				else {
 | 
									else {
 | 
				
			||||||
@@ -27,7 +27,7 @@ export default function flatten (data, keepArray = false) {  // flatten object:
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {  // object
 | 
							else {  // Object
 | 
				
			||||||
			let isEmpty = true;
 | 
								let isEmpty = true;
 | 
				
			||||||
			for (let p in cur) {
 | 
								for (let p in cur) {
 | 
				
			||||||
				isEmpty = false;
 | 
									isEmpty = false;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,36 +1,36 @@
 | 
				
			|||||||
import axios from 'axios';
 | 
					import axios from 'axios';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// sends an email using the BIC service
 | 
					// Sends an email using the BIC service
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class Mail{
 | 
					export default class Mail{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static readonly address = 'definma@bosch-iot.com';  // email address
 | 
						static readonly address = 'definma@bosch-iot.com';  // Email address
 | 
				
			||||||
	static uri: string;                                 // mail API URI
 | 
						static uri: string;                                 // Mail API URI
 | 
				
			||||||
	static auth = {username: '', password: ''};         // mail API credentials
 | 
						static auth = {username: '', password: ''};         // Mail API credentials
 | 
				
			||||||
	static mailPass: string;                            // mail API generates password
 | 
						static mailPass: string;                            // Mail API generates password
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static init() {
 | 
						static init() {
 | 
				
			||||||
		if (process.env.NODE_ENV === 'production') {  // only send mails in production
 | 
							if (process.env.NODE_ENV === 'production') {  // Only send mails in production
 | 
				
			||||||
			this.mailPass = Array(64).fill(0).map(() => Math.floor(Math.random() * 10)).join('');
 | 
								this.mailPass = Array(64).fill(0).map(() => Math.floor(Math.random() * 10)).join('');
 | 
				
			||||||
			this.uri = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.uri;
 | 
								this.uri = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.uri;
 | 
				
			||||||
			this.auth.username = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.username;
 | 
								this.auth.username = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.username;
 | 
				
			||||||
			this.auth.password = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.password;
 | 
								this.auth.password = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.password;
 | 
				
			||||||
			axios({  // get registered mail addresses
 | 
								axios({  // Get registered mail addresses
 | 
				
			||||||
				method: 'get',
 | 
									method: 'get',
 | 
				
			||||||
				url: this.uri + '/management/userDomainMapping',
 | 
									url: this.uri + '/management/userDomainMapping',
 | 
				
			||||||
				auth: this.auth
 | 
									auth: this.auth
 | 
				
			||||||
			}).then(res => {
 | 
								}).then(res => {
 | 
				
			||||||
				return new Promise(async (resolve, reject) => {
 | 
									return new Promise(async (resolve, reject) => {
 | 
				
			||||||
					try {
 | 
										try {
 | 
				
			||||||
						if (res.data.addresses.indexOf(this.address) < 0) {  // mail address not registered
 | 
											if (res.data.addresses.indexOf(this.address) < 0) {  // Mail address not registered
 | 
				
			||||||
							if (res.data.addresses.length) {  // delete wrong registered mail address
 | 
												if (res.data.addresses.length) {  // Delete wrong registered mail address
 | 
				
			||||||
								await axios({
 | 
													await axios({
 | 
				
			||||||
									method: 'delete',
 | 
														method: 'delete',
 | 
				
			||||||
									url: this.uri + '/management/mailAddresses/' + res.data.addresses[0],
 | 
														url: this.uri + '/management/mailAddresses/' + res.data.addresses[0],
 | 
				
			||||||
									auth: this.auth
 | 
														auth: this.auth
 | 
				
			||||||
								});
 | 
													});
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
							await axios({  // register right mail address
 | 
												await axios({  // Register right mail address
 | 
				
			||||||
								method: 'post',
 | 
													method: 'post',
 | 
				
			||||||
								url: this.uri + '/management/mailAddresses/' + this.address,
 | 
													url: this.uri + '/management/mailAddresses/' + this.address,
 | 
				
			||||||
								auth: this.auth
 | 
													auth: this.auth
 | 
				
			||||||
@@ -43,22 +43,22 @@ export default class Mail{
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}).then(() => {
 | 
								}).then(() => {
 | 
				
			||||||
				return axios({  // set new mail password
 | 
									return axios({  // Set new mail password
 | 
				
			||||||
					method: 'put',
 | 
										method: 'put',
 | 
				
			||||||
					url: this.uri + '/management/mailAddresses/' + this.address + '/password/' + this.mailPass,
 | 
										url: this.uri + '/management/mailAddresses/' + this.address + '/password/' + this.mailPass,
 | 
				
			||||||
					auth: this.auth
 | 
										auth: this.auth
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
			}).then(() => {  // init done successfully
 | 
								}).then(() => {  // Init done successfully
 | 
				
			||||||
				console.info('Mail service established successfully');
 | 
									console.info('Mail service established successfully');
 | 
				
			||||||
			}).catch(err => {  // somewhere an error occurred
 | 
								}).catch(err => {  // Somewhere an error occurred
 | 
				
			||||||
				console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`,
 | 
									console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`,
 | 
				
			||||||
							  err.response.data);
 | 
												  err.response.data);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static send (mailAddress, subject, content, f: (x?) => void = () => {}) {  // callback executed empty or with error
 | 
						static send (mailAddress, subject, content, f: (x?) => void = () => {}) {  // Callback executed empty or with error
 | 
				
			||||||
		if (process.env.NODE_ENV === 'production') {  // only send mails in production
 | 
							if (process.env.NODE_ENV === 'production') {  // Only send mails in production
 | 
				
			||||||
			axios({
 | 
								axios({
 | 
				
			||||||
				method: 'post',
 | 
									method: 'post',
 | 
				
			||||||
				url: this.uri + '/email',
 | 
									url: this.uri + '/email',
 | 
				
			||||||
@@ -81,7 +81,7 @@ export default class Mail{
 | 
				
			|||||||
				f(err);
 | 
									f(err);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {  // dev dummy replacement
 | 
							else {  // Dev dummy replacement
 | 
				
			||||||
			console.info('Sending mail to ' + mailAddress + ':  -- ' + subject + ' -- ' + content);
 | 
								console.info('Sending mail to ' + mailAddress + ':  -- ' + subject + ' -- ' + content);
 | 
				
			||||||
			f();
 | 
								f();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										42
									
								
								src/index.ts
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								src/index.ts
									
									
									
									
									
								
							@@ -9,24 +9,24 @@ import db from './db';
 | 
				
			|||||||
import Mail from './helpers/mail';
 | 
					import Mail from './helpers/mail';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// tell if server is running in debug or production environment
 | 
					// Tell if server is running in debug or production environment
 | 
				
			||||||
console.info(process.env.NODE_ENV === 'production' ?
 | 
					console.info(process.env.NODE_ENV === 'production' ?
 | 
				
			||||||
  '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
 | 
					  '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// mongodb connection
 | 
					// Mongodb connection
 | 
				
			||||||
db.connect();
 | 
					db.connect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// mail service
 | 
					// Mail service
 | 
				
			||||||
Mail.init();
 | 
					Mail.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// create Express app
 | 
					// Create Express app
 | 
				
			||||||
const app = express();
 | 
					const app = express();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// get port from environment, defaults to 3000
 | 
					// Get port from environment, defaults to 3000
 | 
				
			||||||
const port = process.env.PORT || 3000;
 | 
					const port = process.env.PORT || 3000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// security headers
 | 
					// Security headers
 | 
				
			||||||
const defaultHeaderConfig = {
 | 
					const defaultHeaderConfig = {
 | 
				
			||||||
  contentSecurityPolicy: {
 | 
					  contentSecurityPolicy: {
 | 
				
			||||||
    directives: {
 | 
					    directives: {
 | 
				
			||||||
@@ -43,7 +43,7 @@ const defaultHeaderConfig = {
 | 
				
			|||||||
  refererPolicy: true
 | 
					  refererPolicy: true
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
app.use(helmet(defaultHeaderConfig));
 | 
					app.use(helmet(defaultHeaderConfig));
 | 
				
			||||||
// special CSP header for api-doc
 | 
					// Special CSP header for api-doc
 | 
				
			||||||
app.use('/api-doc', helmet.contentSecurityPolicy({
 | 
					app.use('/api-doc', helmet.contentSecurityPolicy({
 | 
				
			||||||
  ...defaultHeaderConfig,
 | 
					  ...defaultHeaderConfig,
 | 
				
			||||||
  directives: {
 | 
					  directives: {
 | 
				
			||||||
@@ -54,7 +54,7 @@ app.use('/api-doc', helmet.contentSecurityPolicy({
 | 
				
			|||||||
    imgSrc: [`'self'`, 'data:']
 | 
					    imgSrc: [`'self'`, 'data:']
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
// special CSP header for the intro-presentation
 | 
					// Special CSP header for the intro-presentation
 | 
				
			||||||
app.use(/\/static\/intro-presentation\/(index.html)?/, helmet.contentSecurityPolicy({
 | 
					app.use(/\/static\/intro-presentation\/(index.html)?/, helmet.contentSecurityPolicy({
 | 
				
			||||||
  ...defaultHeaderConfig,
 | 
					  ...defaultHeaderConfig,
 | 
				
			||||||
  directives: {
 | 
					  directives: {
 | 
				
			||||||
@@ -64,7 +64,7 @@ app.use(/\/static\/intro-presentation\/(index.html)?/, helmet.contentSecurityPol
 | 
				
			|||||||
    imgSrc: [`'self'`]
 | 
					    imgSrc: [`'self'`]
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
// special CSP header for the bosch-logo.svg
 | 
					// Special CSP header for the bosch-logo.svg
 | 
				
			||||||
app.use('/static/*.svg', helmet.contentSecurityPolicy({
 | 
					app.use('/static/*.svg', helmet.contentSecurityPolicy({
 | 
				
			||||||
  ...defaultHeaderConfig,
 | 
					  ...defaultHeaderConfig,
 | 
				
			||||||
  directives: {
 | 
					  directives: {
 | 
				
			||||||
@@ -72,8 +72,8 @@ app.use('/static/*.svg', helmet.contentSecurityPolicy({
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// middleware
 | 
					// Middleware
 | 
				
			||||||
app.use(compression());  // compress responses
 | 
					app.use(compression());  // Compress responses
 | 
				
			||||||
app.use(express.json({ limit: '5mb'}));
 | 
					app.use(express.json({ limit: '5mb'}));
 | 
				
			||||||
app.use(express.urlencoded({ extended: false, limit: '5mb' }));
 | 
					app.use(express.urlencoded({ extended: false, limit: '5mb' }));
 | 
				
			||||||
app.use(bodyParser.json());
 | 
					app.use(bodyParser.json());
 | 
				
			||||||
@@ -81,11 +81,11 @@ app.use(contentFilter({
 | 
				
			|||||||
  urlBlackList: ['$', '&&', '||'],
 | 
					  urlBlackList: ['$', '&&', '||'],
 | 
				
			||||||
  bodyBlackList: ['$', '{', '&&', '||'],
 | 
					  bodyBlackList: ['$', '{', '&&', '||'],
 | 
				
			||||||
  appendFound: true
 | 
					  appendFound: true
 | 
				
			||||||
}));  // filter URL query attacks
 | 
					}));  // Filter URL query attacks
 | 
				
			||||||
app.use((err, req, res, ignore) => {  // bodyParser error handling
 | 
					app.use((err, req, res, ignore) => {  // BodyParser error handling
 | 
				
			||||||
  res.status(400).send({status: 'Invalid JSON body'});
 | 
					  res.status(400).send({status: 'Invalid JSON body'});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
app.use((req, res, next) => {  // no database connection error
 | 
					app.use((req, res, next) => {  // No database connection error
 | 
				
			||||||
  if (db.getState().db) {
 | 
					  if (db.getState().db) {
 | 
				
			||||||
    next();
 | 
					    next();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -95,12 +95,12 @@ app.use((req, res, next) => {  // no database connection error
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
app.use(cors());  // CORS headers
 | 
					app.use(cors());  // CORS headers
 | 
				
			||||||
app.use(require('./helpers/authorize'));  // handle authentication
 | 
					app.use(require('./helpers/authorize'));  // Handle authentication
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// redirect /api routes for Angular proxy in development
 | 
					// Redirect /api routes for Angular proxy in development
 | 
				
			||||||
if (process.env.NODE_ENV !== 'production') {
 | 
					if (process.env.NODE_ENV !== 'production') {
 | 
				
			||||||
  app.use('/api/:url([^]+)', (req, res) => {
 | 
					  app.use('/api/:url([^]+)', (req, res) => {
 | 
				
			||||||
    if (/help\//.test(req.params.url)) { // encode URI again for help route
 | 
					    if (/help\//.test(req.params.url)) { // Encode URI again for help route
 | 
				
			||||||
      req.params.url = 'help/' + encodeURIComponent(req.params.url.replace('help/', ''));
 | 
					      req.params.url = 'help/' + encodeURIComponent(req.params.url.replace('help/', ''));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    req.url = '/' + req.params.url;
 | 
					    req.url = '/' + req.params.url;
 | 
				
			||||||
@@ -109,7 +109,7 @@ if (process.env.NODE_ENV !== 'production') {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// require routes
 | 
					// Require routes
 | 
				
			||||||
app.use('/', require('./routes/root'));
 | 
					app.use('/', require('./routes/root'));
 | 
				
			||||||
app.use('/', require('./routes/sample'));
 | 
					app.use('/', require('./routes/sample'));
 | 
				
			||||||
app.use('/', require('./routes/material'));
 | 
					app.use('/', require('./routes/material'));
 | 
				
			||||||
@@ -119,7 +119,7 @@ app.use('/', require('./routes/model'));
 | 
				
			|||||||
app.use('/', require('./routes/user'));
 | 
					app.use('/', require('./routes/user'));
 | 
				
			||||||
app.use('/', require('./routes/help'));
 | 
					app.use('/', require('./routes/help'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// static files
 | 
					// Static files
 | 
				
			||||||
app.use('/static', express.static('static'));
 | 
					app.use('/static', express.static('static'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Swagger UI
 | 
					// Swagger UI
 | 
				
			||||||
@@ -129,13 +129,13 @@ app.use((req, res) => {  // 404 error handling
 | 
				
			|||||||
  res.status(404).json({status: 'Not found'});
 | 
					  res.status(404).json({status: 'Not found'});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.use((err, req, res, ignore) => {  // internal server error handling
 | 
					app.use((err, req, res, ignore) => {  // Internal server error handling
 | 
				
			||||||
  console.error(err);
 | 
					  console.error(err);
 | 
				
			||||||
  res.status(500).json({status: 'Internal server error'});
 | 
					  res.status(500).json({status: 'Internal server error'});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// hook up server to port
 | 
					// Hook up server to port
 | 
				
			||||||
const server = app.listen(port, () => {
 | 
					const server = app.listen(port, () => {
 | 
				
			||||||
  console.info(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
 | 
					  console.info(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,9 +9,9 @@ const ConditionTemplateSchema = new mongoose.Schema({
 | 
				
			|||||||
		name: String,
 | 
							name: String,
 | 
				
			||||||
		range: mongoose.Schema.Types.Mixed
 | 
							range: mongoose.Schema.Types.Mixed
 | 
				
			||||||
	} ,{ _id : false })]
 | 
						} ,{ _id : false })]
 | 
				
			||||||
}, {minimize: false});  // to allow empty objects
 | 
					}, {minimize: false});  // To allow empty objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
ConditionTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					ConditionTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ const HelpSchema = new mongoose.Schema({
 | 
				
			|||||||
	text: String
 | 
						text: String
 | 
				
			||||||
}, {minimize: false});
 | 
					}, {minimize: false});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
HelpSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					HelpSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ const MaterialSchema = new mongoose.Schema({
 | 
				
			|||||||
	status: String
 | 
						status: String
 | 
				
			||||||
}, {minimize: false});
 | 
					}, {minimize: false});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
MaterialSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					MaterialSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ const MaterialGroupsSchema = new mongoose.Schema({
 | 
				
			|||||||
	name: {type: String, index: {unique: true}}
 | 
						name: {type: String, index: {unique: true}}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
MaterialGroupsSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					MaterialGroupsSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ const MaterialSuppliersSchema = new mongoose.Schema({
 | 
				
			|||||||
	name: {type: String, index: {unique: true}}
 | 
						name: {type: String, index: {unique: true}}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
MaterialSuppliersSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					MaterialSuppliersSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,9 +9,9 @@ const MaterialTemplateSchema = new mongoose.Schema({
 | 
				
			|||||||
		name: String,
 | 
							name: String,
 | 
				
			||||||
		range: mongoose.Schema.Types.Mixed
 | 
							range: mongoose.Schema.Types.Mixed
 | 
				
			||||||
	} ,{ _id : false })]
 | 
						} ,{ _id : false })]
 | 
				
			||||||
}, {minimize: false});  // to allow empty objects
 | 
					}, {minimize: false});  // To allow empty objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
MaterialTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					MaterialTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,7 +12,7 @@ const MeasurementSchema = new mongoose.Schema({
 | 
				
			|||||||
	status: String
 | 
						status: String
 | 
				
			||||||
}, {minimize: false});
 | 
					}, {minimize: false});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
MeasurementSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					MeasurementSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,9 +9,9 @@ const MeasurementTemplateSchema = new mongoose.Schema({
 | 
				
			|||||||
		name: String,
 | 
							name: String,
 | 
				
			||||||
		range: mongoose.Schema.Types.Mixed
 | 
							range: mongoose.Schema.Types.Mixed
 | 
				
			||||||
	} ,{ _id : false })]
 | 
						} ,{ _id : false })]
 | 
				
			||||||
}, {minimize: false});  // to allow empty objects
 | 
					}, {minimize: false});  // To allow empty objects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
MeasurementTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					MeasurementTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ const ModelSchema = new mongoose.Schema({
 | 
				
			|||||||
	} ,{ _id : true })]
 | 
						} ,{ _id : true })]
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
ModelSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					ModelSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ const NoteSchema = new mongoose.Schema({
 | 
				
			|||||||
	custom_fields: mongoose.Schema.Types.Mixed
 | 
						custom_fields: mongoose.Schema.Types.Mixed
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
NoteSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					NoteSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ const NoteFieldSchema = new mongoose.Schema({
 | 
				
			|||||||
	qty: Number
 | 
						qty: Number
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
NoteFieldSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					NoteFieldSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@ const SampleSchema = new mongoose.Schema({
 | 
				
			|||||||
	status: String
 | 
						status: String
 | 
				
			||||||
}, {minimize: false});
 | 
					}, {minimize: false});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
SampleSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					SampleSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ const UserSchema = new mongoose.Schema({
 | 
				
			|||||||
	status: String
 | 
						status: String
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// changelog query helper
 | 
					// Changelog query helper
 | 
				
			||||||
UserSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
					UserSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
				
			||||||
	db.log(req, this);
 | 
						db.log(req, this);
 | 
				
			||||||
	return this;
 | 
						return this;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ router.get('/help/:key', (req, res, next) => {
 | 
				
			|||||||
		if (!data) {
 | 
							if (!data) {
 | 
				
			||||||
			return res.status(404).json({status: 'Not found'});
 | 
								return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (data.level !== 'none') {  // check level
 | 
							if (data.level !== 'none') {  // Check level
 | 
				
			||||||
			if (!req.auth(res,
 | 
								if (!req.auth(res,
 | 
				
			||||||
						  Object.values(globals.levels).slice(Object.values(globals.levels).findIndex(e => e === data.level))
 | 
											  Object.values(globals.levels).slice(Object.values(globals.levels).findIndex(e => e === data.level))
 | 
				
			||||||
		, 'basic')) return;
 | 
							, 'basic')) return;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ router.get('/materials', (req, res, next) => {
 | 
				
			|||||||
	.lean().exec((err, data) => {
 | 
						.lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => MaterialValidate.output(e, true))));
 | 
							res.json(_.compact(data.map(e => MaterialValidate.output(e, true))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -41,7 +41,7 @@ router.get(`/materials/:state(${globals.status.new}|${globals.status.del})`, (re
 | 
				
			|||||||
	.lean().exec((err, data) => {
 | 
						.lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => MaterialValidate.output(e))));
 | 
							res.json(_.compact(data.map(e => MaterialValidate.output(e))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -56,7 +56,7 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
			return res.status(404).json({status: 'Not found'});
 | 
								return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// deleted materials only available for dev/admin
 | 
							// Deleted materials only available for dev/admin
 | 
				
			||||||
		if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
							if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
				
			||||||
	res.json(MaterialValidate.output(data));
 | 
						res.json(MaterialValidate.output(data));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
@@ -91,9 +91,9 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
									   materialData.properties.material_template.toString() !== material.properties.material_template)) return;
 | 
														   materialData.properties.material_template.toString() !== material.properties.material_template)) return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// check for changes
 | 
							// Check for changes
 | 
				
			||||||
		if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) {
 | 
							if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) {
 | 
				
			||||||
			material.status = globals.status.new;  // set status to new
 | 
								material.status = globals.status.new;  // Set status to new
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true})
 | 
							await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true})
 | 
				
			||||||
@@ -107,7 +107,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
					router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// check if there are still samples referencing this material
 | 
						// Check if there are still samples referencing this material
 | 
				
			||||||
	SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id), status: {$ne: globals.status.del}})
 | 
						SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id), status: {$ne: globals.status.del}})
 | 
				
			||||||
.lean().exec((err, data) => {
 | 
					.lean().exec((err, data) => {
 | 
				
			||||||
	if (err) return next(err);
 | 
						if (err) return next(err);
 | 
				
			||||||
@@ -152,7 +152,7 @@ router.post('/material/new', async (req, res, next) => {
 | 
				
			|||||||
	if (!material) return;
 | 
						if (!material) return;
 | 
				
			||||||
	if (!await propertiesCheck(material.properties, 'new', res, next)) return;
 | 
						if (!await propertiesCheck(material.properties, 'new', res, next)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	material.status = globals.status.new;  // set status to new
 | 
						material.status = globals.status.new;  // Set status to new
 | 
				
			||||||
	await new MaterialModel(material).save(async (err, data) => {
 | 
						await new MaterialModel(material).save(async (err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		db.log(req, 'materials', {_id: data._id}, data.toObject());
 | 
							db.log(req, 'materials', {_id: data._id}, data.toObject());
 | 
				
			||||||
@@ -168,7 +168,7 @@ router.get('/material/groups', (req, res, next) => {
 | 
				
			|||||||
	MaterialGroupModel.find().lean().exec((err, data: any) => {
 | 
						MaterialGroupModel.find().lean().exec((err, data: any) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name))));
 | 
							res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -179,7 +179,7 @@ router.get('/material/suppliers', (req, res, next) => {
 | 
				
			|||||||
	MaterialSupplierModel.find().lean().exec((err, data: any) => {
 | 
						MaterialSupplierModel.find().lean().exec((err, data: any) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name))));
 | 
							res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -188,10 +188,10 @@ router.get('/material/suppliers', (req, res, next) => {
 | 
				
			|||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function nameCheck (material, res, next) {  // check if name was already taken
 | 
					async function nameCheck (material, res, next) {  // Check if name was already taken
 | 
				
			||||||
	const materialData = await MaterialModel.findOne({name: material.name}).lean().exec().catch(err => next(err)) as any;
 | 
						const materialData = await MaterialModel.findOne({name: material.name}).lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
if (materialData instanceof Error) return false;
 | 
					if (materialData instanceof Error) return false;
 | 
				
			||||||
if (materialData) {  // could not find material_id
 | 
					if (materialData) {  // Could not find material_id
 | 
				
			||||||
	res.status(400).json({status: 'Material name already taken'});
 | 
						res.status(400).json({status: 'Material name already taken'});
 | 
				
			||||||
	return false;
 | 
						return false;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -222,32 +222,32 @@ async function supplierResolve (material, req, next) {
 | 
				
			|||||||
	return material;
 | 
						return material;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// validate material properties, returns false if invalid, otherwise template data
 | 
					// Validate material properties, returns false if invalid, otherwise template data
 | 
				
			||||||
async function propertiesCheck (properties, param, res, next, checkVersion = true) {
 | 
					async function propertiesCheck (properties, param, res, next, checkVersion = true) {
 | 
				
			||||||
	if (!properties.material_template || !IdValidate.valid(properties.material_template)) {  // template id not found
 | 
						if (!properties.material_template || !IdValidate.valid(properties.material_template)) {  // Template id not found
 | 
				
			||||||
		res.status(400).json({status: 'Material template not available'});
 | 
							res.status(400).json({status: 'Material template not available'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const materialData = await MaterialTemplateModel.findById(properties.material_template)
 | 
						const materialData = await MaterialTemplateModel.findById(properties.material_template)
 | 
				
			||||||
	.lean().exec().catch(err => next(err)) as any;
 | 
						.lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
	if (materialData instanceof Error) return false;
 | 
						if (materialData instanceof Error) return false;
 | 
				
			||||||
	if (!materialData) {  // template not found
 | 
						if (!materialData) {  // Template not found
 | 
				
			||||||
		res.status(400).json({status: 'Material template not available'});
 | 
							res.status(400).json({status: 'Material template not available'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (checkVersion) {
 | 
						if (checkVersion) {
 | 
				
			||||||
		// get all template versions and check if given is latest
 | 
							// Get all template versions and check if given is latest
 | 
				
			||||||
		const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1})
 | 
							const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1})
 | 
				
			||||||
	.lean().exec().catch(err => next(err)) as any;
 | 
						.lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
	if (materialVersions instanceof Error) return false;
 | 
						if (materialVersions instanceof Error) return false;
 | 
				
			||||||
	if (properties.material_template !== materialVersions[0]._id.toString()) {  // template not latest
 | 
						if (properties.material_template !== materialVersions[0]._id.toString()) {  // Template not latest
 | 
				
			||||||
		res.status(400).json({status: 'Old template version not allowed'});
 | 
							res.status(400).json({status: 'Old template version not allowed'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate parameters
 | 
						// Validate parameters
 | 
				
			||||||
	const {error, value} = ParametersValidate
 | 
						const {error, value} = ParametersValidate
 | 
				
			||||||
	.input(_.omit(properties, 'material_template'), materialData.parameters, param);
 | 
						.input(_.omit(properties, 'material_template'), materialData.parameters, param);
 | 
				
			||||||
	if (error) {res400(error, res); return false;}
 | 
						if (error) {res400(error, res); return false;}
 | 
				
			||||||
@@ -257,7 +257,7 @@ async function propertiesCheck (properties, param, res, next, checkVersion = tru
 | 
				
			|||||||
	return materialData;
 | 
						return materialData;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setStatus (status, req, res, next) {  // set measurement status
 | 
					function setStatus (status, req, res, next) {  // Set measurement status
 | 
				
			||||||
	MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
 | 
						MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,7 +23,7 @@ router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
		if (!data) {
 | 
							if (!data) {
 | 
				
			||||||
			return res.status(404).json({status: 'Not found'});
 | 
								return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// deleted measurements only available for dev/admin
 | 
							// Deleted measurements only available for dev/admin
 | 
				
			||||||
		if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
							if (data.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	res.json(MeasurementValidate.output(data, req));
 | 
						res.json(MeasurementValidate.output(data, req));
 | 
				
			||||||
@@ -45,16 +45,16 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
 | 
				
			|||||||
		return res.status(403).json({status: 'Forbidden'});
 | 
							return res.status(403).json({status: 'Forbidden'});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// add properties needed for sampleIdCheck
 | 
						// Add properties needed for sampleIdCheck
 | 
				
			||||||
	measurement.measurement_template = data.measurement_template;
 | 
						measurement.measurement_template = data.measurement_template;
 | 
				
			||||||
measurement.sample_id = data.sample_id;
 | 
					measurement.sample_id = data.sample_id;
 | 
				
			||||||
if (!await sampleIdCheck(measurement, req, res, next)) return;
 | 
					if (!await sampleIdCheck(measurement, req, res, next)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// check for changes
 | 
					// Check for changes
 | 
				
			||||||
if (measurement.values) {  // fill not changed values from database
 | 
					if (measurement.values) {  // Fill not changed values from database
 | 
				
			||||||
	measurement.values = _.assign({}, data.values, measurement.values);
 | 
						measurement.values = _.assign({}, data.values, measurement.values);
 | 
				
			||||||
	if (!_.isEqual(measurement.values, data.values)) {
 | 
						if (!_.isEqual(measurement.values, data.values)) {
 | 
				
			||||||
		measurement.status = globals.status.new;  // set status to new
 | 
							measurement.status = globals.status.new;  // Set status to new
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -130,35 +130,35 @@ router.post('/measurement/new', async (req, res, next) => {
 | 
				
			|||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// validate sample_id, returns false if invalid or user has no access for this sample
 | 
					// Validate sample_id, returns false if invalid or user has no access for this sample
 | 
				
			||||||
async function sampleIdCheck (measurement, req, res, next) {
 | 
					async function sampleIdCheck (measurement, req, res, next) {
 | 
				
			||||||
	const sampleData = await SampleModel.findById(measurement.sample_id)
 | 
						const sampleData = await SampleModel.findById(measurement.sample_id)
 | 
				
			||||||
	.lean().exec().catch(err => {next(err); return false;}) as any;
 | 
						.lean().exec().catch(err => {next(err); return false;}) as any;
 | 
				
			||||||
	if (!sampleData) {  // sample_id not found
 | 
						if (!sampleData) {  // Sample_id not found
 | 
				
			||||||
		res.status(400).json({status: 'Sample id not available'});
 | 
							res.status(400).json({status: 'Sample id not available'});
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// sample does not belong to user
 | 
						// Sample does not belong to user
 | 
				
			||||||
	return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic'));
 | 
						return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic'));
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// validate measurement_template and values, returns values, true if values are {} or false if invalid,
 | 
					// Validate measurement_template and values, returns values, true if values are {} or false if invalid,
 | 
				
			||||||
// param for 'new'/'change'
 | 
					// Param for 'new'/'change'
 | 
				
			||||||
async function templateCheck (measurement, param, res, next) {
 | 
					async function templateCheck (measurement, param, res, next) {
 | 
				
			||||||
	const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template)
 | 
						const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template)
 | 
				
			||||||
	.lean().exec().catch(err => {next(err); return false;}) as any;
 | 
						.lean().exec().catch(err => {next(err); return false;}) as any;
 | 
				
			||||||
	if (!templateData) {  // template not found
 | 
						if (!templateData) {  // Template not found
 | 
				
			||||||
		res.status(400).json({status: 'Measurement template not available'});
 | 
							res.status(400).json({status: 'Measurement template not available'});
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// fill not given values for new measurements
 | 
						// Fill not given values for new measurements
 | 
				
			||||||
	if (param === 'new') {
 | 
						if (param === 'new') {
 | 
				
			||||||
		// get all template versions and check if given is latest
 | 
							// Get all template versions and check if given is latest
 | 
				
			||||||
		const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1})
 | 
							const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1})
 | 
				
			||||||
	.lean().exec().catch(err => next(err)) as any;
 | 
						.lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
	if (templateVersions instanceof Error) return false;
 | 
						if (templateVersions instanceof Error) return false;
 | 
				
			||||||
	if (measurement.measurement_template !== templateVersions[0]._id.toString()) {  // template not latest
 | 
						if (measurement.measurement_template !== templateVersions[0]._id.toString()) {  // Template not latest
 | 
				
			||||||
		res.status(400).json({status: 'Old template version not allowed'});
 | 
							res.status(400).json({status: 'Old template version not allowed'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -167,20 +167,20 @@ async function templateCheck (measurement, param, res, next) {
 | 
				
			|||||||
		res.status(400).json({status: 'At least one value is required'});
 | 
							res.status(400).json({status: 'At least one value is required'});
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const fillValues = {};  // initialize not given values with null
 | 
						const fillValues = {};  // Initialize not given values with null
 | 
				
			||||||
	templateData.parameters.forEach(parameter => {
 | 
						templateData.parameters.forEach(parameter => {
 | 
				
			||||||
		fillValues[parameter.name] = null;
 | 
							fillValues[parameter.name] = null;
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	measurement.values = _.assign({}, fillValues, measurement.values);
 | 
						measurement.values = _.assign({}, fillValues, measurement.values);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate values
 | 
						// Validate values
 | 
				
			||||||
	const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null');
 | 
						const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null');
 | 
				
			||||||
	if (error) {res400(error, res); return false;}
 | 
						if (error) {res400(error, res); return false;}
 | 
				
			||||||
	return value || true;
 | 
						return value || true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function setStatus (status, req, res, next) {  // set measurement status
 | 
					function setStatus (status, req, res, next) {  // Set measurement status
 | 
				
			||||||
	MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
 | 
						MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ router.get('/model/groups', (req, res, next) => {
 | 
				
			|||||||
	if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let conditions: any = [{}, {}];
 | 
						let conditions: any = [{}, {}];
 | 
				
			||||||
	if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) {  // if not dev or admin, user has to possess model rights
 | 
						if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) {  // If not dev or admin, user has to possess model rights
 | 
				
			||||||
		conditions = [
 | 
							conditions = [
 | 
				
			||||||
			{'models._id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))}},
 | 
								{'models._id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))}},
 | 
				
			||||||
			{group: true, 'models.$': true}
 | 
								{group: true, 'models.$': true}
 | 
				
			||||||
@@ -25,7 +25,7 @@ router.get('/model/groups', (req, res, next) => {
 | 
				
			|||||||
	ModelModel.find(...conditions).lean().exec((err, data) => {
 | 
						ModelModel.find(...conditions).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => ModelValidate.output(e))));
 | 
							res.json(_.compact(data.map(e => ModelValidate.output(e))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -39,8 +39,8 @@ router.post('/model/:group', (req, res, next) => {
 | 
				
			|||||||
	ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => {
 | 
						ModelModel.findOne({group: req.params.group}).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (data) {  // group exists
 | 
							if (data) {  // Group exists
 | 
				
			||||||
			if (data.models.find(e => e.name === model.name)) {  // name exists, overwrite
 | 
								if (data.models.find(e => e.name === model.name)) {  // Name exists, overwrite
 | 
				
			||||||
				ModelModel.findOneAndUpdate(
 | 
									ModelModel.findOneAndUpdate(
 | 
				
			||||||
					{$and: [{group: req.params.group}, {'models.name': model.name}]},
 | 
										{$and: [{group: req.params.group}, {'models.name': model.name}]},
 | 
				
			||||||
					{'models.$': model},
 | 
										{'models.$': model},
 | 
				
			||||||
@@ -49,7 +49,7 @@ router.post('/model/:group', (req, res, next) => {
 | 
				
			|||||||
						res.json({status: 'OK'})
 | 
											res.json({status: 'OK'})
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else {  // create new
 | 
								else {  // Create new
 | 
				
			||||||
				ModelModel.findOneAndUpdate(
 | 
									ModelModel.findOneAndUpdate(
 | 
				
			||||||
					{group: req.params.group},
 | 
										{group: req.params.group},
 | 
				
			||||||
					{$push: {models: model as never}}
 | 
										{$push: {models: model as never}}
 | 
				
			||||||
@@ -59,7 +59,7 @@ router.post('/model/:group', (req, res, next) => {
 | 
				
			|||||||
				});
 | 
									});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {  // create new group
 | 
							else {  // Create new group
 | 
				
			||||||
			new ModelModel({group: req.params.group, models: [model]}).save((err, data) => {
 | 
								new ModelModel({group: req.params.group, models: [model]}).save((err, data) => {
 | 
				
			||||||
				if (err) return next(err);
 | 
									if (err) return next(err);
 | 
				
			||||||
				db.log(req, 'models', {_id: data._id}, data.toObject());
 | 
									db.log(req, 'models', {_id: data._id}, data.toObject());
 | 
				
			||||||
@@ -78,11 +78,11 @@ router.delete('/model/:group(((?!file)[^\\/]+?))/:name', (req, res, next) => {
 | 
				
			|||||||
		if (!data || !data.models.find(e => e.name === req.params.name)) {
 | 
							if (!data || !data.models.find(e => e.name === req.params.name)) {
 | 
				
			||||||
			return res.status(404).json({status: 'Not found'});
 | 
								return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// delete all references in user.models
 | 
							// Delete all references in user.models
 | 
				
			||||||
		UserModel.updateMany({}, {$pull: {models: data.models.find(e => e.name === req.params.name)._id as never}},
 | 
							UserModel.updateMany({}, {$pull: {models: data.models.find(e => e.name === req.params.name)._id as never}},
 | 
				
			||||||
							 { multi: true }).log(req).lean().exec(err => {
 | 
												 { multi: true }).log(req).lean().exec(err => {
 | 
				
			||||||
								 if (err) return next(err);
 | 
													 if (err) return next(err);
 | 
				
			||||||
								 if (data.models.length > 1) {  // only remove model
 | 
													 if (data.models.length > 1) {  // Only remove model
 | 
				
			||||||
									 ModelModel.findOneAndUpdate(
 | 
														 ModelModel.findOneAndUpdate(
 | 
				
			||||||
										 {group: req.params.group},
 | 
															 {group: req.params.group},
 | 
				
			||||||
										 {$pull: {models: data.models.find(e => e.name === req.params.name) as never}}
 | 
															 {$pull: {models: data.models.find(e => e.name === req.params.name) as never}}
 | 
				
			||||||
@@ -91,7 +91,7 @@ router.delete('/model/:group(((?!file)[^\\/]+?))/:name', (req, res, next) => {
 | 
				
			|||||||
										 res.json({status: 'OK'})
 | 
															 res.json({status: 'OK'})
 | 
				
			||||||
									 });
 | 
														 });
 | 
				
			||||||
								 }
 | 
													 }
 | 
				
			||||||
								 else {  // remove document
 | 
													 else {  // Remove document
 | 
				
			||||||
									 ModelModel.findOneAndDelete({group: req.params.group}).log(req).lean().exec(err => {
 | 
														 ModelModel.findOneAndDelete({group: req.params.group}).log(req).lean().exec(err => {
 | 
				
			||||||
										 if (err) return next(err);
 | 
															 if (err) return next(err);
 | 
				
			||||||
										 res.json({status: 'OK'})
 | 
															 res.json({status: 'OK'})
 | 
				
			||||||
@@ -152,7 +152,7 @@ router.delete('/model/file/:name', (req, res, next) => {
 | 
				
			|||||||
router.get('/model/authorized/:url', (req, res, next) => {
 | 
					router.get('/model/authorized/:url', (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) {  // if not dev or admin, user has to possess model rights
 | 
						if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) {  // If not dev or admin, user has to possess model rights
 | 
				
			||||||
		ModelModel.findOne({models: { $elemMatch: {
 | 
							ModelModel.findOne({models: { $elemMatch: {
 | 
				
			||||||
			url: decodeURIComponent(req.params.url),
 | 
								url: decodeURIComponent(req.params.url),
 | 
				
			||||||
			'_id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))}
 | 
								'_id': {$in: req.authDetails.models.map(e => mongoose.Types.ObjectId(e))}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -208,7 +208,7 @@ describe('/', () => {
 | 
				
			|||||||
		});
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// describe('A not connected database', () => {  // RUN AS LAST OR RECONNECT DATABASE!!
 | 
						// Describe('A not connected database', () => {  // RUN AS LAST OR RECONNECT DATABASE!!
 | 
				
			||||||
	//   it('resolves to an 500 error', done => {
 | 
						//   it('resolves to an 500 error', done => {
 | 
				
			||||||
	//     db.disconnect(() => {
 | 
						//     db.disconnect(() => {
 | 
				
			||||||
	//       TestHelper.request(server, done, {
 | 
						//       TestHelper.request(server, done, {
 | 
				
			||||||
@@ -223,7 +223,7 @@ describe('/', () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
describe('The /api/{url} redirect', () => {
 | 
					describe('The /api/{url} redirect', () => {
 | 
				
			||||||
	let server;
 | 
						let server;
 | 
				
			||||||
	let counter = 0;  // count number of current test method
 | 
						let counter = 0;  // Count number of current test method
 | 
				
			||||||
	before(done => {
 | 
						before(done => {
 | 
				
			||||||
		process.env.port = '2999';
 | 
							process.env.port = '2999';
 | 
				
			||||||
		db.connect('test', done);
 | 
							db.connect('test', done);
 | 
				
			||||||
@@ -246,7 +246,7 @@ describe('The /api/{url} redirect', () => {
 | 
				
			|||||||
			res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'}
 | 
								res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
	// it('is disabled in production', done => {
 | 
						// It('is disabled in production', done => {
 | 
				
			||||||
	//   TestHelper.request(server, done, {
 | 
						//   TestHelper.request(server, done, {
 | 
				
			||||||
	//     method: 'get',
 | 
						//     method: 'get',
 | 
				
			||||||
	//     url: '/api/authorized',
 | 
						//     url: '/api/authorized',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -37,7 +37,7 @@ router.get('/changelog/:id/:page?/:pagesize?', (req, res, next) => {
 | 
				
			|||||||
	.lean().exec((err, data) => {
 | 
						.lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => RootValidate.changelogOutput(e))));
 | 
							res.json(_.compact(data.map(e => RootValidate.changelogOutput(e))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1460,7 +1460,7 @@ describe('/sample', () => {
 | 
				
			|||||||
			}).end((err, res) => {
 | 
								}).end((err, res) => {
 | 
				
			||||||
				if (err) return done(err);
 | 
									if (err) return done(err);
 | 
				
			||||||
				should(res.body).be.eql({status: 'OK'});
 | 
									should(res.body).be.eql({status: 'OK'});
 | 
				
			||||||
				setTimeout(() => {  // background action takes some time before we can check
 | 
									setTimeout(() => {  // Background action takes some time before we can check
 | 
				
			||||||
					NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => {
 | 
										NoteModel.findById('500000000000000000000003').lean().exec((err, data: any) => {
 | 
				
			||||||
						if (err) return done(err);
 | 
											if (err) return done(err);
 | 
				
			||||||
						should(data).have.property('sample_references').with.lengthOf(1);
 | 
											should(data).have.property('sample_references').with.lengthOf(1);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,22 +29,22 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
	const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0);
 | 
						const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0);
 | 
				
			||||||
	if (error) return res400(error, res);
 | 
						if (error) return res400(error, res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// spectral data and csv not allowed for read/write users
 | 
						// Spectral data and csv not allowed for read/write users
 | 
				
			||||||
	if ((filters.fields.find(e => e.indexOf('.' + globals.spectrum.dpt) >= 0) || filters.output !== 'json') &&
 | 
						if ((filters.fields.find(e => e.indexOf('.' + globals.spectrum.dpt) >= 0) || filters.output !== 'json') &&
 | 
				
			||||||
		!req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
							!req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// evaluate sort parameter from 'color-asc' to ['color', 1]
 | 
							// Evaluate sort parameter from 'color-asc' to ['color', 1]
 | 
				
			||||||
		filters.sort = filters.sort.split('-');
 | 
							filters.sort = filters.sort.split('-');
 | 
				
			||||||
		filters.sort[0] = filters.sort[0] === 'added' ? '_id' : filters.sort[0];  // route added sorting criteria to _id
 | 
							filters.sort[0] = filters.sort[0] === 'added' ? '_id' : filters.sort[0];  // Route added sorting criteria to _id
 | 
				
			||||||
		filters.sort[1] = filters.sort[1] === 'desc' ? -1 : 1;
 | 
							filters.sort[1] = filters.sort[1] === 'desc' ? -1 : 1;
 | 
				
			||||||
		if (!filters['to-page']) {  // set to-page default
 | 
							if (!filters['to-page']) {  // Set to-page default
 | 
				
			||||||
			filters['to-page'] = 0;
 | 
								filters['to-page'] = 0;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		const addedFilter = filters.filters.find(e => e.field === 'added');
 | 
							const addedFilter = filters.filters.find(e => e.field === 'added');
 | 
				
			||||||
		if (addedFilter) {  // convert added filter to object id
 | 
							if (addedFilter) {  // Convert added filter to object id
 | 
				
			||||||
			filters.filters.splice(filters.filters.findIndex(e => e.field === 'added'), 1);
 | 
								filters.filters.splice(filters.filters.findIndex(e => e.field === 'added'), 1);
 | 
				
			||||||
			if (addedFilter.mode === 'in') {
 | 
								if (addedFilter.mode === 'in') {
 | 
				
			||||||
				const v = [];  // query value
 | 
									const v = [];  // Query value
 | 
				
			||||||
				addedFilter.values.forEach(value => {
 | 
									addedFilter.values.forEach(value => {
 | 
				
			||||||
					const date = [new Date(value).setHours(0,0,0,0), new Date(value).setHours(23,59,59,999)];
 | 
										const date = [new Date(value).setHours(0,0,0,0), new Date(value).setHours(23,59,59,999)];
 | 
				
			||||||
					v.push({$and: [{ _id: { '$gte': dateToOId(date[0])}}, { _id: { '$lte': dateToOId(date[1])}}]});
 | 
										v.push({$and: [{ _id: { '$gte': dateToOId(date[0])}}, { _id: { '$lte': dateToOId(date[1])}}]});
 | 
				
			||||||
@@ -53,7 +53,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			else if (addedFilter.mode === 'nin') {
 | 
								else if (addedFilter.mode === 'nin') {
 | 
				
			||||||
				addedFilter.values = addedFilter.values.sort();
 | 
									addedFilter.values = addedFilter.values.sort();
 | 
				
			||||||
				const v = [];  // query value
 | 
									const v = [];  // Query value
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				for (let i = 0; i <= addedFilter.values.length; i ++) {
 | 
									for (let i = 0; i <= addedFilter.values.length; i ++) {
 | 
				
			||||||
					v[i] = {$and: []};
 | 
										v[i] = {$and: []};
 | 
				
			||||||
@@ -69,19 +69,19 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
				filters.filters.push({mode: 'or', field: '_id', values: v});
 | 
									filters.filters.push({mode: 'or', field: '_id', values: v});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else {
 | 
								else {
 | 
				
			||||||
				// start and end of day
 | 
									// Start and end of day
 | 
				
			||||||
				const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0),
 | 
									const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0),
 | 
				
			||||||
					new Date(addedFilter.values[0]).setHours(23,59,59,999)];
 | 
										new Date(addedFilter.values[0]).setHours(23,59,59,999)];
 | 
				
			||||||
					if (addedFilter.mode === 'lt') {  // lt start
 | 
										if (addedFilter.mode === 'lt') {  // Lt start
 | 
				
			||||||
						filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]});
 | 
											filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if (addedFilter.mode === 'eq' || addedFilter.mode === 'lte') {  // lte end
 | 
										if (addedFilter.mode === 'eq' || addedFilter.mode === 'lte') {  // Lte end
 | 
				
			||||||
						filters.filters.push({mode: 'lte', field: '_id', values: [dateToOId(date[1])]});
 | 
											filters.filters.push({mode: 'lte', field: '_id', values: [dateToOId(date[1])]});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if (addedFilter.mode === 'gt') {  // gt end
 | 
										if (addedFilter.mode === 'gt') {  // Gt end
 | 
				
			||||||
						filters.filters.push({mode: 'gt', field: '_id', values: [dateToOId(date[1])]});
 | 
											filters.filters.push({mode: 'gt', field: '_id', values: [dateToOId(date[1])]});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if (addedFilter.mode === 'eq' || addedFilter.mode === 'gte') {  // gte start
 | 
										if (addedFilter.mode === 'eq' || addedFilter.mode === 'gte') {  // Gte start
 | 
				
			||||||
						filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]});
 | 
											filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]});
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					if (addedFilter.mode === 'ne') {
 | 
										if (addedFilter.mode === 'ne') {
 | 
				
			||||||
@@ -98,7 +98,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
		let queryPtr = query;
 | 
							let queryPtr = query;
 | 
				
			||||||
		queryPtr.push({$match: {$and: []}});
 | 
							queryPtr.push({$match: {$and: []}});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (filters.sort[0].indexOf('measurements.') >= 0) {  // sorting with measurements as starting collection
 | 
							if (filters.sort[0].indexOf('measurements.') >= 0) {  // Sorting with measurements as starting collection
 | 
				
			||||||
			collection = MeasurementModel;
 | 
								collection = MeasurementModel;
 | 
				
			||||||
			const [,measurementName, measurementParam] = filters.sort[0].split('.');
 | 
								const [,measurementName, measurementParam] = filters.sort[0].split('.');
 | 
				
			||||||
			const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName})
 | 
								const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName})
 | 
				
			||||||
@@ -108,7 +108,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
				return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'});
 | 
									return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'});
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			let sortStartValue = null;
 | 
								let sortStartValue = null;
 | 
				
			||||||
			if (filters['from-id']) {  // from-id specified, fetch values for sorting
 | 
								if (filters['from-id']) {  // From-id specified, fetch values for sorting
 | 
				
			||||||
				const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])})
 | 
									const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])})
 | 
				
			||||||
			.lean().exec().catch(err => {next(err);});
 | 
								.lean().exec().catch(err => {next(err);});
 | 
				
			||||||
			if (fromSample instanceof Error) return;
 | 
								if (fromSample instanceof Error) return;
 | 
				
			||||||
@@ -117,29 +117,29 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
			sortStartValue = fromSample.values[measurementParam];
 | 
								sortStartValue = fromSample.values[measurementParam];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			// find measurements to sort
 | 
								// Find measurements to sort
 | 
				
			||||||
			queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}});
 | 
								queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}});
 | 
				
			||||||
			if (filters.filters.find(e => e.field === filters.sort[0])) {  // sorted measurement should also be filtered
 | 
								if (filters.filters.find(e => e.field === filters.sort[0])) {  // Sorted measurement should also be filtered
 | 
				
			||||||
				queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0])
 | 
									queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0])
 | 
				
			||||||
															  .map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; })));
 | 
																				  .map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; })));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			queryPtr.push(
 | 
								queryPtr.push(
 | 
				
			||||||
				...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue),  // sort measurements
 | 
									...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue),  // Sort measurements
 | 
				
			||||||
				{$replaceRoot: {newRoot: {measurement: '$$ROOT'}}},  // fetch samples and restructure them to fit sample structure
 | 
									{$replaceRoot: {newRoot: {measurement: '$$ROOT'}}},  // Fetch samples and restructure them to fit sample structure
 | 
				
			||||||
				{$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}},
 | 
									{$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}},
 | 
				
			||||||
				{$match: statusQuery(filters, 'sample.status')},  // filter out wrong status once samples were added
 | 
									{$match: statusQuery(filters, 'sample.status')},  // Filter out wrong status once samples were added
 | 
				
			||||||
				{$addFields: {['sample.' + measurementName]: '$measurement.values'}},  // more restructuring
 | 
									{$addFields: {['sample.' + measurementName]: '$measurement.values'}},  // More restructuring
 | 
				
			||||||
				{$replaceRoot: {newRoot: {$mergeObjects: [{$arrayElemAt: ['$sample', 0]}, {}]}}}
 | 
									{$replaceRoot: {newRoot: {$mergeObjects: [{$arrayElemAt: ['$sample', 0]}, {}]}}}
 | 
				
			||||||
			);
 | 
								);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {  // sorting with samples as starting collection
 | 
							else {  // Sorting with samples as starting collection
 | 
				
			||||||
			collection = SampleModel;
 | 
								collection = SampleModel;
 | 
				
			||||||
			queryPtr[0].$match.$and.push(statusQuery(filters, 'status'));
 | 
								queryPtr[0].$match.$and.push(statusQuery(filters, 'status'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			// sorting for sample keys
 | 
								// Sorting for sample keys
 | 
				
			||||||
			if (SampleValidate.sampleKeys.indexOf(filters.sort[0]) >= 0 || /condition\./.test(filters.sort[0])) {
 | 
								if (SampleValidate.sampleKeys.indexOf(filters.sort[0]) >= 0 || /condition\./.test(filters.sort[0])) {
 | 
				
			||||||
				let sortStartValue = null;
 | 
									let sortStartValue = null;
 | 
				
			||||||
				if (filters['from-id']) {  // from-id specified
 | 
									if (filters['from-id']) {  // From-id specified
 | 
				
			||||||
					const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => {
 | 
										const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => {
 | 
				
			||||||
						next(err);
 | 
											next(err);
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
@@ -151,28 +151,28 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
				}
 | 
									}
 | 
				
			||||||
				queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue));
 | 
									queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue));
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else {  // add sort key to list to add field later
 | 
								else {  // Add sort key to list to add field later
 | 
				
			||||||
				sortFilterKeys.push(filters.sort[0]);
 | 
									sortFilterKeys.push(filters.sort[0]);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		addFilterQueries(queryPtr, filters.filters.filter(
 | 
							addFilterQueries(queryPtr, filters.filters.filter(
 | 
				
			||||||
			e => (SampleValidate.sampleKeys.indexOf(e.field) >= 0) || /condition\./.test(e.field))
 | 
								e => (SampleValidate.sampleKeys.indexOf(e.field) >= 0) || /condition\./.test(e.field))
 | 
				
			||||||
						);  // sample filters
 | 
											);  // Sample filters
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						let materialQuery = [];  // put material query together separate first to reuse for first-id
 | 
											let materialQuery = [];  // Put material query together separate first to reuse for first-id
 | 
				
			||||||
							let materialAdded = false;
 | 
												let materialAdded = false;
 | 
				
			||||||
						if (sortFilterKeys.find(e => /material\./.test(e))) {  //  add material fields
 | 
											if (sortFilterKeys.find(e => /material\./.test(e))) {  //  add material fields
 | 
				
			||||||
							materialAdded = true;
 | 
												materialAdded = true;
 | 
				
			||||||
							materialQuery.push(  // add material properties
 | 
												materialQuery.push(  // Add material properties
 | 
				
			||||||
											   {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
 | 
																   {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
 | 
				
			||||||
											   {$addFields: {material: {$arrayElemAt: ['$material', 0]}}}
 | 
																   {$addFields: {material: {$arrayElemAt: ['$material', 0]}}}
 | 
				
			||||||
											  );
 | 
																  );
 | 
				
			||||||
											  const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e))
 | 
																  const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e))
 | 
				
			||||||
											  .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0);
 | 
																  .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0);
 | 
				
			||||||
											  // base material filters
 | 
																  // Base material filters
 | 
				
			||||||
											  addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0));
 | 
																  addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0));
 | 
				
			||||||
											  if (sortFilterKeys.find(e => e === 'material.supplier')) {  // add supplier if needed
 | 
																  if (sortFilterKeys.find(e => e === 'material.supplier')) {  // Add supplier if needed
 | 
				
			||||||
												  materialQuery.push(
 | 
																	  materialQuery.push(
 | 
				
			||||||
													  {$lookup: {
 | 
																		  {$lookup: {
 | 
				
			||||||
														  from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}
 | 
																			  from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}
 | 
				
			||||||
@@ -180,7 +180,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
													  {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
 | 
																		  {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
 | 
				
			||||||
												  );
 | 
																	  );
 | 
				
			||||||
											  }
 | 
																  }
 | 
				
			||||||
											  if (sortFilterKeys.find(e => e === 'material.group')) {  // add group if needed
 | 
																  if (sortFilterKeys.find(e => e === 'material.group')) {  // Add group if needed
 | 
				
			||||||
												  materialQuery.push(
 | 
																	  materialQuery.push(
 | 
				
			||||||
													  {$lookup: {
 | 
																		  {$lookup: {
 | 
				
			||||||
														  from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }
 | 
																			  from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }
 | 
				
			||||||
@@ -190,12 +190,12 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
											  }
 | 
																  }
 | 
				
			||||||
											  const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e))
 | 
																  const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e))
 | 
				
			||||||
											  .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0);
 | 
																  .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0);
 | 
				
			||||||
											  // base material filters
 | 
																  // Base material filters
 | 
				
			||||||
											  addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0));
 | 
																  addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0));
 | 
				
			||||||
											  queryPtr.push(...materialQuery);
 | 
																  queryPtr.push(...materialQuery);
 | 
				
			||||||
											  if (/material\./.test(filters.sort[0])) {  // sort by material key
 | 
																  if (/material\./.test(filters.sort[0])) {  // Sort by material key
 | 
				
			||||||
												  let sortStartValue = null;
 | 
																	  let sortStartValue = null;
 | 
				
			||||||
												  if (filters['from-id']) {  // from-id specified
 | 
																	  if (filters['from-id']) {  // From-id specified
 | 
				
			||||||
													  const fromSample = await SampleModel.aggregate(
 | 
																		  const fromSample = await SampleModel.aggregate(
 | 
				
			||||||
														  [{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery]
 | 
																			  [{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery]
 | 
				
			||||||
													  ).exec().catch(err => {next(err);});
 | 
																		  ).exec().catch(err => {next(err);});
 | 
				
			||||||
@@ -215,7 +215,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
											  }
 | 
																  }
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
						if (sortFilterKeys.find(e => e === 'measurements')) {  // filter for samples without measurements
 | 
											if (sortFilterKeys.find(e => e === 'measurements')) {  // Filter for samples without measurements
 | 
				
			||||||
							queryPtr.push({$lookup: {
 | 
												queryPtr.push({$lookup: {
 | 
				
			||||||
								from: 'measurements', let: {sId: '$_id'},
 | 
													from: 'measurements', let: {sId: '$_id'},
 | 
				
			||||||
								pipeline: [{$match:{$expr:{$and:[{$eq:['$sample_id','$$sId']}]}}}, {$project: {_id: true}}],
 | 
													pipeline: [{$match:{$expr:{$and:[{$eq:['$sample_id','$$sId']}]}}}, {$project: {_id: true}}],
 | 
				
			||||||
@@ -225,7 +225,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
										 );
 | 
															 );
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
						const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e))
 | 
											const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e))
 | 
				
			||||||
															   .map(e => e.split('.')[1]));  // filter measurement names and remove duplicates from parameters
 | 
																				   .map(e => e.split('.')[1]));  // Filter measurement names and remove duplicates from parameters
 | 
				
			||||||
															   if (sortFilterKeys.find(e => /measurements\./.test(e))) {  //  add measurement fields
 | 
																				   if (sortFilterKeys.find(e => /measurements\./.test(e))) {  //  add measurement fields
 | 
				
			||||||
																   const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}})
 | 
																					   const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}})
 | 
				
			||||||
																   .lean().exec().catch(err => {next(err);});
 | 
																					   .lean().exec().catch(err => {next(err);});
 | 
				
			||||||
@@ -238,7 +238,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																	   {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}
 | 
																						   {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}
 | 
				
			||||||
																   ]}}}
 | 
																					   ]}}}
 | 
				
			||||||
																   ];
 | 
																					   ];
 | 
				
			||||||
																   if (measurementFilterFields.indexOf(globals.spectrum.spectrum) >= 0) {  // filter out dpts
 | 
																					   if (measurementFilterFields.indexOf(globals.spectrum.spectrum) >= 0) {  // Filter out dpts
 | 
				
			||||||
																	   pipeline.push(
 | 
																						   pipeline.push(
 | 
				
			||||||
																		   {$project: {['values.' + globals.spectrum.dpt]: false}},
 | 
																							   {$project: {['values.' + globals.spectrum.dpt]: false}},
 | 
				
			||||||
																		   {$addFields: {'values._id': '$_id'}}
 | 
																							   {$addFields: {'values._id': '$_id'}}
 | 
				
			||||||
@@ -264,7 +264,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																   addFilterQueries(queryPtr, filters.filters
 | 
																					   addFilterQueries(queryPtr, filters.filters
 | 
				
			||||||
																					.filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0)
 | 
																										.filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0)
 | 
				
			||||||
																					.map(e => {e.field = e.field.replace('measurements.', ''); return e; })
 | 
																										.map(e => {e.field = e.field.replace('measurements.', ''); return e; })
 | 
				
			||||||
																				   );  // measurement filters
 | 
																									   );  // Measurement filters
 | 
				
			||||||
															   }
 | 
																				   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
															   if (sortFilterKeys.find(e => e === 'notes.comment')) {
 | 
																				   if (sortFilterKeys.find(e => e === 'notes.comment')) {
 | 
				
			||||||
@@ -272,18 +272,18 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																   addFilterQueries(queryPtr, filters.filters.filter(e => e.field === 'notes.comment'));
 | 
																					   addFilterQueries(queryPtr, filters.filters.filter(e => e.field === 'notes.comment'));
 | 
				
			||||||
															   }
 | 
																				   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
															   // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not
 | 
																				   // Count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not
 | 
				
			||||||
															   // included
 | 
																				   // Included
 | 
				
			||||||
															   if (!filters.fields.find(e =>
 | 
																				   if (!filters.fields.find(e =>
 | 
				
			||||||
																						e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0) && !filters['from-id']
 | 
																											e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0) && !filters['from-id']
 | 
				
			||||||
																  ) {
 | 
																					  ) {
 | 
				
			||||||
																	  queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}});
 | 
																						  queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}});
 | 
				
			||||||
																	  queryPtr = queryPtr[queryPtr.length - 1].$facet.samples;  // add rest of aggregation pipeline into $facet
 | 
																						  queryPtr = queryPtr[queryPtr.length - 1].$facet.samples;  // Add rest of aggregation pipeline into $facet
 | 
				
			||||||
																  }
 | 
																					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
																  // paging
 | 
																					  // Paging
 | 
				
			||||||
																  if (filters['to-page']) {
 | 
																					  if (filters['to-page']) {
 | 
				
			||||||
																	  // number to skip, if going back pages, one page has to be skipped less but on sample more
 | 
																						  // Number to skip, if going back pages, one page has to be skipped less but on sample more
 | 
				
			||||||
																	  queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] +
 | 
																						  queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] +
 | 
				
			||||||
																					Number(filters['to-page'] < 0)})
 | 
																										Number(filters['to-page'] < 0)})
 | 
				
			||||||
																  }
 | 
																					  }
 | 
				
			||||||
@@ -291,22 +291,22 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																	  queryPtr.push({$limit: filters['page-size']});
 | 
																						  queryPtr.push({$limit: filters['page-size']});
 | 
				
			||||||
																  }
 | 
																					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
																  const fieldsToAdd = filters.fields.filter(e =>   // fields to add
 | 
																					  const fieldsToAdd = filters.fields.filter(e =>   // Fields to add
 | 
				
			||||||
																											sortFilterKeys.indexOf(e) < 0                      // field was not in filter
 | 
																																sortFilterKeys.indexOf(e) < 0                      // Field was not in filter
 | 
				
			||||||
																  && e !== filters.sort[0]                           // field was not in sort
 | 
																					  && e !== filters.sort[0]                           // Field was not in sort
 | 
				
			||||||
																										   );
 | 
																															   );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
																										   if (fieldsToAdd.find(e => /^notes(\..+|$)/m.test(e))) {  // add notes
 | 
																															   if (fieldsToAdd.find(e => /^notes(\..+|$)/m.test(e))) {  // Add notes
 | 
				
			||||||
																											   addNotes(queryPtr);
 | 
																																   addNotes(queryPtr);
 | 
				
			||||||
																										   }
 | 
																															   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
																										   if (fieldsToAdd.find(e => /material\./.test(e)) && !materialAdded) {  // add material, was not added already
 | 
																															   if (fieldsToAdd.find(e => /material\./.test(e)) && !materialAdded) {  // Add material, was not added already
 | 
				
			||||||
																											   queryPtr.push(
 | 
																																   queryPtr.push(
 | 
				
			||||||
																												   {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
 | 
																																	   {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
 | 
				
			||||||
																												   {$addFields: {material: { $arrayElemAt: ['$material', 0]}}}
 | 
																																	   {$addFields: {material: { $arrayElemAt: ['$material', 0]}}}
 | 
				
			||||||
																											   );
 | 
																																   );
 | 
				
			||||||
																										   }
 | 
																															   }
 | 
				
			||||||
																										   if (fieldsToAdd.indexOf('material.supplier') >= 0) {  // add supplier if needed
 | 
																															   if (fieldsToAdd.indexOf('material.supplier') >= 0) {  // Add supplier if needed
 | 
				
			||||||
																											   queryPtr.push(
 | 
																																   queryPtr.push(
 | 
				
			||||||
																												   {$lookup: {
 | 
																																	   {$lookup: {
 | 
				
			||||||
																													   from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'
 | 
																																		   from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'
 | 
				
			||||||
@@ -314,7 +314,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																												   {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
 | 
																																	   {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
 | 
				
			||||||
																											   );
 | 
																																   );
 | 
				
			||||||
																										   }
 | 
																															   }
 | 
				
			||||||
																										   if (fieldsToAdd.indexOf('material.group') >= 0) {  // add group if needed
 | 
																															   if (fieldsToAdd.indexOf('material.group') >= 0) {  // Add group if needed
 | 
				
			||||||
																											   queryPtr.push(
 | 
																																   queryPtr.push(
 | 
				
			||||||
																												   {$lookup: {
 | 
																																	   {$lookup: {
 | 
				
			||||||
																													   from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group'
 | 
																																		   from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group'
 | 
				
			||||||
@@ -325,17 +325,17 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
																										   let measurementFieldsFields: string[] = _.uniq(
 | 
																															   let measurementFieldsFields: string[] = _.uniq(
 | 
				
			||||||
																											   fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])
 | 
																																   fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])
 | 
				
			||||||
																										   );  // filter measurement names and remove duplicates from parameters
 | 
																															   );  // Filter measurement names and remove duplicates from parameters
 | 
				
			||||||
																										   if (fieldsToAdd.find(e => /measurements\./.test(e))) {  // add measurement fields
 | 
																															   if (fieldsToAdd.find(e => /measurements\./.test(e))) {  // Add measurement fields
 | 
				
			||||||
																											   const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}})
 | 
																																   const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}})
 | 
				
			||||||
																											   .lean().exec().catch(err => {next(err);});
 | 
																																   .lean().exec().catch(err => {next(err);});
 | 
				
			||||||
																											   if (measurementTemplates instanceof Error) return;
 | 
																																   if (measurementTemplates instanceof Error) return;
 | 
				
			||||||
																											   if (measurementTemplates.length < measurementFieldsFields.length) {
 | 
																																   if (measurementTemplates.length < measurementFieldsFields.length) {
 | 
				
			||||||
																												   return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'});
 | 
																																	   return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'});
 | 
				
			||||||
																											   }
 | 
																																   }
 | 
				
			||||||
																											   // use different lookup methods with and without dpt for the best performance
 | 
																																   // Use different lookup methods with and without dpt for the best performance
 | 
				
			||||||
																											   if (fieldsToAdd.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) {  // with dpt
 | 
																																   if (fieldsToAdd.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) {  // With dpt
 | 
				
			||||||
																												   // spectrum was already used for filters
 | 
																																	   // Spectrum was already used for filters
 | 
				
			||||||
																												   if (sortFilterKeys.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) {
 | 
																																	   if (sortFilterKeys.find(e => new RegExp('measurements\\.' + globals.spectrum.spectrum).test(e))) {
 | 
				
			||||||
																													   queryPtr.push(
 | 
																																		   queryPtr.push(
 | 
				
			||||||
																														   {$lookup: {from: 'measurements', localField: 'spectrum._id', foreignField: '_id', as: 'measurements'}}
 | 
																																			   {$lookup: {from: 'measurements', localField: 'spectrum._id', foreignField: '_id', as: 'measurements'}}
 | 
				
			||||||
@@ -380,11 +380,11 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
																										   const projection = filters.fields.map(e => e.replace('measurements.', ''))
 | 
																															   const projection = filters.fields.map(e => e.replace('measurements.', ''))
 | 
				
			||||||
																										   .reduce((s, e) => {s[e] = true; return s; }, {});
 | 
																															   .reduce((s, e) => {s[e] = true; return s; }, {});
 | 
				
			||||||
																										   if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) {  // disable _id explicitly
 | 
																															   if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) {  // Disable _id explicitly
 | 
				
			||||||
																											   projection._id = false;
 | 
																																   projection._id = false;
 | 
				
			||||||
																										   }
 | 
																															   }
 | 
				
			||||||
																										   queryPtr.push({$project: projection});
 | 
																															   queryPtr.push({$project: projection});
 | 
				
			||||||
																										   // use streaming when including spectrum files
 | 
																															   // Use streaming when including spectrum files
 | 
				
			||||||
																										   if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0)) {
 | 
																															   if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0)) {
 | 
				
			||||||
																											   collection.aggregate(query).allowDiskUse(true).exec((err, data) => {
 | 
																																   collection.aggregate(query).allowDiskUse(true).exec((err, data) => {
 | 
				
			||||||
																												   if (err) return next(err);
 | 
																																	   if (err) return next(err);
 | 
				
			||||||
@@ -393,7 +393,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																													   res.header('Access-Control-Expose-Headers', 'x-total-items');
 | 
																																		   res.header('Access-Control-Expose-Headers', 'x-total-items');
 | 
				
			||||||
																													   data = data[0].samples;
 | 
																																		   data = data[0].samples;
 | 
				
			||||||
																												   }
 | 
																																	   }
 | 
				
			||||||
																												   if (filters.fields.indexOf('added') >= 0) {  // add added date
 | 
																																	   if (filters.fields.indexOf('added') >= 0) {  // Add added date
 | 
				
			||||||
																													   data.map(e => {
 | 
																																		   data.map(e => {
 | 
				
			||||||
																														   e.added = e._id.getTimestamp();
 | 
																																			   e.added = e._id.getTimestamp();
 | 
				
			||||||
																														   if (filters.fields.indexOf('_id') < 0) {
 | 
																																			   if (filters.fields.indexOf('_id') < 0) {
 | 
				
			||||||
@@ -409,7 +409,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																													   [filters.sort[0].split('.')[1],
 | 
																																		   [filters.sort[0].split('.')[1],
 | 
				
			||||||
																														   ...measurementFilterFields, ...measurementFieldsFields]
 | 
																																			   ...measurementFilterFields, ...measurementFieldsFields]
 | 
				
			||||||
																												   );
 | 
																																	   );
 | 
				
			||||||
																												   if (filters.output === 'csv') {  // output as csv
 | 
																																	   if (filters.output === 'csv') {  // Output as csv
 | 
				
			||||||
																													   csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => {
 | 
																																		   csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => {
 | 
				
			||||||
																														   if (err) return next(err);
 | 
																																			   if (err) return next(err);
 | 
				
			||||||
																														   res.set('Content-Type', 'text/csv');
 | 
																																			   res.set('Content-Type', 'text/csv');
 | 
				
			||||||
@@ -420,7 +420,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																												   else if (filters.output === 'flatten') {
 | 
																																	   else if (filters.output === 'flatten') {
 | 
				
			||||||
																													   res.json(_.compact(data.map(e => flatten(SampleValidate.output(e, 'refs', measurementFields), true))));
 | 
																																		   res.json(_.compact(data.map(e => flatten(SampleValidate.output(e, 'refs', measurementFields), true))));
 | 
				
			||||||
																												   }
 | 
																																	   }
 | 
				
			||||||
																												   else {  // validate all and filter null values from validation errors
 | 
																																	   else {  // Validate all and filter null values from validation errors
 | 
				
			||||||
																													   res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))));
 | 
																																		   res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))));
 | 
				
			||||||
																												   }
 | 
																																	   }
 | 
				
			||||||
																											   });
 | 
																																   });
 | 
				
			||||||
@@ -431,7 +431,7 @@ router.get('/samples', async (req, res, next) => {
 | 
				
			|||||||
																												   let count = 0;
 | 
																																	   let count = 0;
 | 
				
			||||||
																												   const stream = collection.aggregate(query).allowDiskUse(true).cursor().exec();
 | 
																																	   const stream = collection.aggregate(query).allowDiskUse(true).cursor().exec();
 | 
				
			||||||
																												   stream.on('data', data => {
 | 
																																	   stream.on('data', data => {
 | 
				
			||||||
																													   if (filters.fields.indexOf('added') >= 0) {  // add added date
 | 
																																		   if (filters.fields.indexOf('added') >= 0) {  // Add added date
 | 
				
			||||||
																														   data.added = data._id.getTimestamp();
 | 
																																			   data.added = data._id.getTimestamp();
 | 
				
			||||||
																														   if (filters.fields.indexOf('_id') < 0) {
 | 
																																			   if (filters.fields.indexOf('_id') < 0) {
 | 
				
			||||||
																															   delete data._id;
 | 
																																				   delete data._id;
 | 
				
			||||||
@@ -457,7 +457,7 @@ router.get(`/samples/:state(${globals.status.new}|${globals.status.del})`, (req,
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	SampleModel.find({status: req.params.state}).lean().exec((err, data) => {
 | 
						SampleModel.find({status: req.params.state}).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => SampleValidate.output(e))));
 | 
							res.json(_.compact(data.map(e => SampleValidate.output(e))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -487,7 +487,7 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
	const {error, value: sample} = SampleValidate.input(req.body, 'change');
 | 
						const {error, value: sample} = SampleValidate.input(req.body, 'change');
 | 
				
			||||||
	if (error) return res400(error, res);
 | 
						if (error) return res400(error, res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => {  // check if id exists
 | 
						SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => {  // Check if id exists
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
	if (!sampleData) {
 | 
						if (!sampleData) {
 | 
				
			||||||
		return res.status(404).json({status: 'Not found'});
 | 
							return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
@@ -496,12 +496,12 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
		return res.status(403).json({status: 'Forbidden'});
 | 
							return res.status(403).json({status: 'Forbidden'});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// only dev and admin are allowed to edit other user's data
 | 
						// Only dev and admin are allowed to edit other user's data
 | 
				
			||||||
	if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return;
 | 
						if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return;
 | 
				
			||||||
	if (sample.hasOwnProperty('material_id')) {
 | 
						if (sample.hasOwnProperty('material_id')) {
 | 
				
			||||||
		if (!await materialCheck(sample, res, next)) return;
 | 
							if (!await materialCheck(sample, res, next)) return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// do not execute check if condition is and was empty
 | 
						// Do not execute check if condition is and was empty
 | 
				
			||||||
	if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) {
 | 
						if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) {
 | 
				
			||||||
		sample.condition = await conditionCheck(sample.condition, 'change', res, next,
 | 
							sample.condition = await conditionCheck(sample.condition, 'change', res, next,
 | 
				
			||||||
												!(sampleData.condition.condition_template &&
 | 
																	!(sampleData.condition.condition_template &&
 | 
				
			||||||
@@ -511,35 +511,35 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	if (sample.hasOwnProperty('notes')) {
 | 
						if (sample.hasOwnProperty('notes')) {
 | 
				
			||||||
		let newNotes = true;
 | 
							let newNotes = true;
 | 
				
			||||||
		if (sampleData.note_id !== null) {  // old notes data exists
 | 
							if (sampleData.note_id !== null) {  // Old notes data exists
 | 
				
			||||||
			const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any;
 | 
								const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any;
 | 
				
			||||||
			if (data instanceof Error) return;
 | 
								if (data instanceof Error) return;
 | 
				
			||||||
			// check if notes were changed
 | 
								// Check if notes were changed
 | 
				
			||||||
			newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes);
 | 
								newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes);
 | 
				
			||||||
		if (newNotes) {
 | 
							if (newNotes) {
 | 
				
			||||||
			if (data.hasOwnProperty('custom_fields')) {  // update note_fields
 | 
								if (data.hasOwnProperty('custom_fields')) {  // Update note_fields
 | 
				
			||||||
				customFieldsChange(Object.keys(data.custom_fields), -1, req);
 | 
									customFieldsChange(Object.keys(data.custom_fields), -1, req);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			await NoteModel.findByIdAndDelete(sampleData.note_id).log(req).lean().exec(err => {  // delete old notes
 | 
								await NoteModel.findByIdAndDelete(sampleData.note_id).log(req).lean().exec(err => {  // Delete old notes
 | 
				
			||||||
				if (err) return console.error(err);
 | 
									if (err) return console.error(err);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (_.keys(sample.notes).length > 0 && newNotes) {  // save new notes
 | 
							if (_.keys(sample.notes).length > 0 && newNotes) {  // Save new notes
 | 
				
			||||||
			if (!await sampleRefCheck(sample, res, next)) return;
 | 
								if (!await sampleRefCheck(sample, res, next)) return;
 | 
				
			||||||
			// new custom_fields
 | 
								// New custom_fields
 | 
				
			||||||
			if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
 | 
								if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
 | 
				
			||||||
				customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
 | 
									customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)});  // save new notes
 | 
								let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)});  // Save new notes
 | 
				
			||||||
			db.log(req, 'notes', {_id: data._id}, data.toObject());
 | 
								db.log(req, 'notes', {_id: data._id}, data.toObject());
 | 
				
			||||||
			delete sample.notes;
 | 
								delete sample.notes;
 | 
				
			||||||
			sample.note_id = data._id;
 | 
								sample.note_id = data._id;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// check for changes
 | 
						// Check for changes
 | 
				
			||||||
	if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) {
 | 
						if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) {
 | 
				
			||||||
		sample.status = globals.status.new;
 | 
							sample.status = globals.status.new;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -555,28 +555,28 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
 | 
					router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => {  // check if id exists
 | 
						SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => {  // Check if id exists
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
	if (!sampleData) {
 | 
						if (!sampleData) {
 | 
				
			||||||
		return res.status(404).json({status: 'Not found'});
 | 
							return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// only dev and admin are allowed to edit other user's data
 | 
						// Only dev and admin are allowed to edit other user's data
 | 
				
			||||||
	if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return;
 | 
						if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// set sample status
 | 
						// Set sample status
 | 
				
			||||||
	await SampleModel.findByIdAndUpdate(req.params.id, {status:'deleted'}).log(req).lean().exec(err => {
 | 
						await SampleModel.findByIdAndUpdate(req.params.id, {status:'deleted'}).log(req).lean().exec(err => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// set status of associated measurements also to deleted
 | 
							// Set status of associated measurements also to deleted
 | 
				
			||||||
		MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: globals.status.del})
 | 
							MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: globals.status.del})
 | 
				
			||||||
		.log(req).lean().exec(err => {
 | 
							.log(req).lean().exec(err => {
 | 
				
			||||||
			if (err) return next(err);
 | 
								if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (sampleData.note_id !== null) {  // handle notes
 | 
								if (sampleData.note_id !== null) {  // Handle notes
 | 
				
			||||||
				NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => {  // find notes to update note_fields
 | 
									NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => {  // Find notes to update note_fields
 | 
				
			||||||
					if (err) return next(err);
 | 
										if (err) return next(err);
 | 
				
			||||||
					if (data.hasOwnProperty('custom_fields')) {  // update note_fields
 | 
										if (data.hasOwnProperty('custom_fields')) {  // Update note_fields
 | 
				
			||||||
						customFieldsChange(Object.keys(data.custom_fields), -1, req);
 | 
											customFieldsChange(Object.keys(data.custom_fields), -1, req);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					res.json({status: 'OK'});
 | 
										res.json({status: 'OK'});
 | 
				
			||||||
@@ -615,7 +615,7 @@ router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => {
 | 
				
			|||||||
router.post('/sample/new', async (req, res, next) => {
 | 
					router.post('/sample/new', async (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!req.body.hasOwnProperty('condition')) {  // add empty condition if not specified
 | 
						if (!req.body.hasOwnProperty('condition')) {  // Add empty condition if not specified
 | 
				
			||||||
		req.body.condition = {};
 | 
							req.body.condition = {};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -626,17 +626,17 @@ router.post('/sample/new', async (req, res, next) => {
 | 
				
			|||||||
	if (!await materialCheck(sample, res, next)) return;
 | 
						if (!await materialCheck(sample, res, next)) return;
 | 
				
			||||||
	if (!await sampleRefCheck(sample, res, next)) return;
 | 
						if (!await sampleRefCheck(sample, res, next)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// new custom_fields
 | 
						// New custom_fields
 | 
				
			||||||
	if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
 | 
						if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
 | 
				
			||||||
		customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
 | 
							customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!_.isEmpty(sample.condition)) {  // do not execute check if condition is empty
 | 
						if (!_.isEmpty(sample.condition)) {  // Do not execute check if condition is empty
 | 
				
			||||||
		sample.condition = await conditionCheck(sample.condition, 'change', res, next);
 | 
							sample.condition = await conditionCheck(sample.condition, 'change', res, next);
 | 
				
			||||||
	if (!sample.condition) return;
 | 
						if (!sample.condition) return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	sample.status = globals.status.new;  // set status to new
 | 
						sample.status = globals.status.new;  // Set status to new
 | 
				
			||||||
	if (sample.hasOwnProperty('number')) {
 | 
						if (sample.hasOwnProperty('number')) {
 | 
				
			||||||
		if (!await numberCheck(sample, res, next)) return;
 | 
							if (!await numberCheck(sample, res, next)) return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -645,7 +645,7 @@ router.post('/sample/new', async (req, res, next) => {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	if (!sample.number) return;
 | 
						if (!sample.number) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await new NoteModel(sample.notes).save((err, data) => {  // save notes
 | 
						await new NoteModel(sample.notes).save((err, data) => {  // Save notes
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		db.log(req, 'notes', {_id: data._id}, data.toObject());
 | 
							db.log(req, 'notes', {_id: data._id}, data.toObject());
 | 
				
			||||||
		delete sample.notes;
 | 
							delete sample.notes;
 | 
				
			||||||
@@ -665,7 +665,7 @@ router.get('/sample/notes/fields', (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	NoteFieldModel.find({}).lean().exec((err, data) => {
 | 
						NoteFieldModel.find({}).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => NoteFieldValidate.output(e))));
 | 
							res.json(_.compact(data.map(e => NoteFieldValidate.output(e))));
 | 
				
			||||||
	})
 | 
						})
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -673,10 +673,10 @@ router.get('/sample/notes/fields', (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// store the highest generated number for each location to avoid duplicate numbers
 | 
					// Store the highest generated number for each location to avoid duplicate numbers
 | 
				
			||||||
const numberBuffer: {[location: string]: number} = {};
 | 
					const numberBuffer: {[location: string]: number} = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// generate number in format Location32, returns false on error
 | 
					// Generate number in format Location32, returns false on error
 | 
				
			||||||
async function numberGenerate (sample, req, res, next) {
 | 
					async function numberGenerate (sample, req, res, next) {
 | 
				
			||||||
	const sampleData = await SampleModel
 | 
						const sampleData = await SampleModel
 | 
				
			||||||
	.aggregate([
 | 
						.aggregate([
 | 
				
			||||||
@@ -705,50 +705,50 @@ async function numberGenerate (sample, req, res, next) {
 | 
				
			|||||||
async function numberCheck(sample, res, next) {
 | 
					async function numberCheck(sample, res, next) {
 | 
				
			||||||
	const sampleData = await SampleModel.findOne({number: sample.number})
 | 
						const sampleData = await SampleModel.findOne({number: sample.number})
 | 
				
			||||||
	.lean().exec().catch(err => {next(err); return false;});
 | 
						.lean().exec().catch(err => {next(err); return false;});
 | 
				
			||||||
	if (sampleData) {  // found entry with sample number
 | 
						if (sampleData) {  // Found entry with sample number
 | 
				
			||||||
		res.status(400).json({status: 'Sample number already taken'});
 | 
							res.status(400).json({status: 'Sample number already taken'});
 | 
				
			||||||
		return false
 | 
							return false
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return true;
 | 
						return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// validate material_id and color, returns false if invalid
 | 
					// Validate material_id and color, returns false if invalid
 | 
				
			||||||
async function materialCheck (sample, res, next) {
 | 
					async function materialCheck (sample, res, next) {
 | 
				
			||||||
	const materialData = await MaterialModel.findById(sample.material_id).lean().exec().catch(err => next(err)) as any;
 | 
						const materialData = await MaterialModel.findById(sample.material_id).lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
	if (materialData instanceof Error) return false;
 | 
						if (materialData instanceof Error) return false;
 | 
				
			||||||
	if (!materialData) {  // could not find material_id
 | 
						if (!materialData) {  // Could not find material_id
 | 
				
			||||||
		res.status(400).json({status: 'Material not available'});
 | 
							res.status(400).json({status: 'Material not available'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	return true;
 | 
						return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// validate treatment template, returns false if invalid, otherwise template data
 | 
					// Validate treatment template, returns false if invalid, otherwise template data
 | 
				
			||||||
async function conditionCheck (condition, param, res, next, checkVersion = true) {
 | 
					async function conditionCheck (condition, param, res, next, checkVersion = true) {
 | 
				
			||||||
	if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) {  // template id not found
 | 
						if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) {  // Template id not found
 | 
				
			||||||
		res.status(400).json({status: 'Condition template not available'});
 | 
							res.status(400).json({status: 'Condition template not available'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	const conditionData = await ConditionTemplateModel.findById(condition.condition_template)
 | 
						const conditionData = await ConditionTemplateModel.findById(condition.condition_template)
 | 
				
			||||||
	.lean().exec().catch(err => next(err)) as any;
 | 
						.lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
	if (conditionData instanceof Error) return false;
 | 
						if (conditionData instanceof Error) return false;
 | 
				
			||||||
	if (!conditionData) {  // template not found
 | 
						if (!conditionData) {  // Template not found
 | 
				
			||||||
		res.status(400).json({status: 'Condition template not available'});
 | 
							res.status(400).json({status: 'Condition template not available'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (checkVersion) {
 | 
						if (checkVersion) {
 | 
				
			||||||
		// get all template versions and check if given is latest
 | 
							// Get all template versions and check if given is latest
 | 
				
			||||||
		const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id})
 | 
							const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id})
 | 
				
			||||||
	.sort({version: -1}).lean().exec().catch(err => next(err)) as any;
 | 
						.sort({version: -1}).lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
	if (conditionVersions instanceof Error) return false;
 | 
						if (conditionVersions instanceof Error) return false;
 | 
				
			||||||
	if (condition.condition_template !== conditionVersions[0]._id.toString()) {  // template not latest
 | 
						if (condition.condition_template !== conditionVersions[0]._id.toString()) {  // Template not latest
 | 
				
			||||||
		res.status(400).json({status: 'Old template version not allowed'});
 | 
							res.status(400).json({status: 'Old template version not allowed'});
 | 
				
			||||||
		return false;
 | 
							return false;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate parameters
 | 
						// Validate parameters
 | 
				
			||||||
	const {error, value} =
 | 
						const {error, value} =
 | 
				
			||||||
		ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param);
 | 
							ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param);
 | 
				
			||||||
	if (error) {res400(error, res); return false;}
 | 
						if (error) {res400(error, res); return false;}
 | 
				
			||||||
@@ -756,11 +756,11 @@ async function conditionCheck (condition, param, res, next, checkVersion = true)
 | 
				
			|||||||
	return value;
 | 
						return value;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function sampleRefCheck (sample, res, next) {  // validate sample_references, resolves false for invalid reference
 | 
					function sampleRefCheck (sample, res, next) {  // Validate sample_references, resolves false for invalid reference
 | 
				
			||||||
	return new Promise(resolve => {
 | 
						return new Promise(resolve => {
 | 
				
			||||||
		// there are sample_references
 | 
							// There are sample_references
 | 
				
			||||||
		if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) {
 | 
							if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) {
 | 
				
			||||||
			let referencesCount = sample.notes.sample_references.length;  // count to keep track of running async operations
 | 
								let referencesCount = sample.notes.sample_references.length;  // Count to keep track of running async operations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			sample.notes.sample_references.forEach(reference => {
 | 
								sample.notes.sample_references.forEach(reference => {
 | 
				
			||||||
				SampleModel.findById(reference.sample_id).lean().exec((err, data) => {
 | 
									SampleModel.findById(reference.sample_id).lean().exec((err, data) => {
 | 
				
			||||||
@@ -770,7 +770,7 @@ function sampleRefCheck (sample, res, next) {  // validate sample_references, re
 | 
				
			|||||||
						return resolve(false);
 | 
											return resolve(false);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
					referencesCount --;
 | 
										referencesCount --;
 | 
				
			||||||
					if (referencesCount <= 0) {  // all async requests done
 | 
										if (referencesCount <= 0) {  // All async requests done
 | 
				
			||||||
						resolve(true);
 | 
											resolve(true);
 | 
				
			||||||
					}
 | 
										}
 | 
				
			||||||
				});
 | 
									});
 | 
				
			||||||
@@ -782,18 +782,18 @@ function sampleRefCheck (sample, res, next) {  // validate sample_references, re
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function customFieldsChange (fields, amount, req) {  // update custom_fields and respective quantities
 | 
					function customFieldsChange (fields, amount, req) {  // Update custom_fields and respective quantities
 | 
				
			||||||
	fields.forEach(field => {
 | 
						fields.forEach(field => {
 | 
				
			||||||
		NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true})
 | 
							NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true})
 | 
				
			||||||
		.log(req).lean().exec((err, data: any) => {  // check if field exists
 | 
							.log(req).lean().exec((err, data: any) => {  // Check if field exists
 | 
				
			||||||
			if (err) return console.error(err);
 | 
								if (err) return console.error(err);
 | 
				
			||||||
		if (!data) {  // new field
 | 
							if (!data) {  // New field
 | 
				
			||||||
			new NoteFieldModel({name: field, qty: 1}).save((err, data) => {
 | 
								new NoteFieldModel({name: field, qty: 1}).save((err, data) => {
 | 
				
			||||||
				if (err) return console.error(err);
 | 
									if (err) return console.error(err);
 | 
				
			||||||
				db.log(req, 'note_fields', {_id: data._id}, data.toObject());
 | 
									db.log(req, 'note_fields', {_id: data._id}, data.toObject());
 | 
				
			||||||
			})
 | 
								})
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else if (data.qty <= 0) {  // delete document if field is not used anymore
 | 
							else if (data.qty <= 0) {  // Delete document if field is not used anymore
 | 
				
			||||||
			NoteFieldModel.findOneAndDelete({name: field}).log(req).lean().exec(err => {
 | 
								NoteFieldModel.findOneAndDelete({name: field}).log(req).lean().exec(err => {
 | 
				
			||||||
				if (err) return console.error(err);
 | 
									if (err) return console.error(err);
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
@@ -803,9 +803,9 @@ function customFieldsChange (fields, amount, req) {  // update custom_fields and
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function sortQuery(filters, sortKeys, sortStartValue) {  // sortKeys = ['primary key', 'secondary key']
 | 
					function sortQuery(filters, sortKeys, sortStartValue) {  // sortKeys = ['primary key', 'secondary key']
 | 
				
			||||||
	if (filters['from-id']) {  // from-id specified
 | 
						if (filters['from-id']) {  // From-id specified
 | 
				
			||||||
		const ssv = sortStartValue !== undefined;  // if value is not given, match for existence
 | 
							const ssv = sortStartValue !== undefined;  // If value is not given, match for existence
 | 
				
			||||||
			if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) {  // asc
 | 
								if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) {  // Asc
 | 
				
			||||||
				return [
 | 
									return [
 | 
				
			||||||
					{$match: {$or: [
 | 
										{$match: {$or: [
 | 
				
			||||||
						{[sortKeys[0]]: ssv ? {$gt: sortStartValue} : {$exists: true}},
 | 
											{[sortKeys[0]]: ssv ? {$gt: sortStartValue} : {$exists: true}},
 | 
				
			||||||
@@ -828,8 +828,8 @@ function sortQuery(filters, sortKeys, sortStartValue) {  // sortKeys = ['primary
 | 
				
			|||||||
					{$sort: {[sortKeys[0]]: -1, _id: -1}}
 | 
										{$sort: {[sortKeys[0]]: -1, _id: -1}}
 | 
				
			||||||
				];
 | 
									];
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
	} else {  // sort from beginning
 | 
						} else {  // Sort from beginning
 | 
				
			||||||
		return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}];  // set _id as secondary sort
 | 
							return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}];  // Set _id as secondary sort
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -837,7 +837,7 @@ function statusQuery(filters, field) {
 | 
				
			|||||||
	return {$or: filters.status.map(e => ({[field]: e}))};
 | 
						return {$or: filters.status.map(e => ({[field]: e}))};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function addFilterQueries (queryPtr, filters) {  // returns array of match queries from given filters
 | 
					function addFilterQueries (queryPtr, filters) {  // Returns array of match queries from given filters
 | 
				
			||||||
	if (filters.length) {
 | 
						if (filters.length) {
 | 
				
			||||||
		queryPtr.push({$match: {$and: filterQueries(filters)}});
 | 
							queryPtr.push({$match: {$and: filterQueries(filters)}});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -845,21 +845,21 @@ function addFilterQueries (queryPtr, filters) {  // returns array of match queri
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function filterQueries (filters) {
 | 
					function filterQueries (filters) {
 | 
				
			||||||
	return filters.map(e => {
 | 
						return filters.map(e => {
 | 
				
			||||||
		if (e.mode === 'or') {  // allow or queries (needed for $ne added)
 | 
							if (e.mode === 'or') {  // Allow or queries (needed for $ne added)
 | 
				
			||||||
			return {['$' + e.mode]: e.values};
 | 
								return {['$' + e.mode]: e.values};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else if (e.mode === 'stringin') {
 | 
							else if (e.mode === 'stringin') {
 | 
				
			||||||
			return {[e.field]: {['$in']: [new RegExp(e.values[0])]}};
 | 
								return {[e.field]: {['$in']: [new RegExp(e.values[0])]}};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {
 | 
							else {
 | 
				
			||||||
			// add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin
 | 
								// Add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin
 | 
				
			||||||
			return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}};
 | 
								return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}};
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// add measurements as property [template.name], if one result, array is reduced to direct values. All given templates
 | 
					// Add measurements as property [template.name], if one result, array is reduced to direct values. All given templates
 | 
				
			||||||
// must have the same name
 | 
					// Must have the same name
 | 
				
			||||||
function addMeasurements(queryPtr, templates) {
 | 
					function addMeasurements(queryPtr, templates) {
 | 
				
			||||||
	queryPtr.push(
 | 
						queryPtr.push(
 | 
				
			||||||
		{$addFields: {[templates[0].name]: {$let: {vars: {
 | 
							{$addFields: {[templates[0].name]: {$let: {vars: {
 | 
				
			||||||
@@ -880,7 +880,7 @@ function addMeasurements(queryPtr, templates) {
 | 
				
			|||||||
	);
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function addNotes(queryPtr) {  // add note fields with default, if no notes are found
 | 
					function addNotes(queryPtr) {  // Add note fields with default, if no notes are found
 | 
				
			||||||
	queryPtr.push(
 | 
						queryPtr.push(
 | 
				
			||||||
		{$lookup: {from: 'notes', localField: 'note_id', foreignField: '_id', as: 'notes'}},
 | 
							{$lookup: {from: 'notes', localField: 'note_id', foreignField: '_id', as: 'notes'}},
 | 
				
			||||||
		{$addFields: {notes: {$cond: [
 | 
							{$addFields: {notes: {$cond: [
 | 
				
			||||||
@@ -891,7 +891,7 @@ function addNotes(queryPtr) {  // add note fields with default, if no notes are
 | 
				
			|||||||
	);
 | 
						);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function dateToOId (date) {  // convert date to ObjectId
 | 
					function dateToOId (date) {  // Convert date to ObjectId
 | 
				
			||||||
	return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000');
 | 
						return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -902,9 +902,9 @@ async function sampleReturn (sampleData, req, res, next) {
 | 
				
			|||||||
		if (sampleData instanceof Error) return;
 | 
							if (sampleData instanceof Error) return;
 | 
				
			||||||
		sampleData = sampleData.toObject();
 | 
							sampleData = sampleData.toObject();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// deleted samples only available for dev/admin
 | 
							// Deleted samples only available for dev/admin
 | 
				
			||||||
		if (sampleData.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
							if (sampleData.status === globals.status.del && !req.auth(res, ['dev', 'admin'], 'all')) return;
 | 
				
			||||||
	sampleData.material = sampleData.material_id;  // map data to right keys
 | 
						sampleData.material = sampleData.material_id;  // Map data to right keys
 | 
				
			||||||
	sampleData.material.group = sampleData.material.group_id.name;
 | 
						sampleData.material.group = sampleData.material.group_id.name;
 | 
				
			||||||
	sampleData.material.supplier = sampleData.material.supplier_id.name;
 | 
						sampleData.material.supplier = sampleData.material.supplier_id.name;
 | 
				
			||||||
	sampleData.user = sampleData.user_id.name;
 | 
						sampleData.user = sampleData.user_id.name;
 | 
				
			||||||
@@ -912,7 +912,7 @@ async function sampleReturn (sampleData, req, res, next) {
 | 
				
			|||||||
	MeasurementModel.find({sample_id: sampleData._id, status: {$ne: 'deleted'}})
 | 
						MeasurementModel.find({sample_id: sampleData._id, status: {$ne: 'deleted'}})
 | 
				
			||||||
	.lean().exec((err, data) => {
 | 
						.lean().exec((err, data) => {
 | 
				
			||||||
		sampleData.measurements = data;
 | 
							sampleData.measurements = data;
 | 
				
			||||||
		if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) {  // strip dpt values if not dev or admin
 | 
							if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) {  // Strip dpt values if not dev or admin
 | 
				
			||||||
			sampleData.measurements.forEach(measurement => {
 | 
								sampleData.measurements.forEach(measurement => {
 | 
				
			||||||
				if (measurement.values[globals.spectrum.dpt]) {
 | 
									if (measurement.values[globals.spectrum.dpt]) {
 | 
				
			||||||
					delete measurement.values[globals.spectrum.dpt];
 | 
										delete measurement.values[globals.spectrum.dpt];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -590,7 +590,7 @@ describe('/template', () => {
 | 
				
			|||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		// other methods should be covered by condition tests
 | 
							// Other methods should be covered by condition tests
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	describe('/template/material', () => {
 | 
						describe('/template/material', () => {
 | 
				
			||||||
@@ -656,6 +656,6 @@ describe('/template', () => {
 | 
				
			|||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		// other methods should be covered by condition tests
 | 
							// Other methods should be covered by condition tests
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,10 +20,10 @@ const router = express.Router();
 | 
				
			|||||||
router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => {
 | 
					router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req.params.collection = req.params.collection.replace(/s$/g, '');  // remove trailing s
 | 
						req.params.collection = req.params.collection.replace(/s$/g, '');  // Remove trailing s
 | 
				
			||||||
	model(req).find({}).lean().exec((err, data) => {
 | 
						model(req).find({}).lean().exec((err, data) => {
 | 
				
			||||||
		if (err) next (err);
 | 
							if (err) next (err);
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => TemplateValidate.output(e))));
 | 
							res.json(_.compact(data.map(e => TemplateValidate.output(e))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
@@ -49,13 +49,13 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
				
			|||||||
			   const {error, value: template} = TemplateValidate.input(req.body, 'change');
 | 
								   const {error, value: template} = TemplateValidate.input(req.body, 'change');
 | 
				
			||||||
			   if (error) return res400(error, res);
 | 
								   if (error) return res400(error, res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			   // find given template
 | 
								   // Find given template
 | 
				
			||||||
			   const templateRef = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
 | 
								   const templateRef = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
 | 
				
			||||||
			   if (templateRef instanceof Error) return;
 | 
								   if (templateRef instanceof Error) return;
 | 
				
			||||||
			   if (!templateRef) {
 | 
								   if (!templateRef) {
 | 
				
			||||||
				   return res.status(404).json({status: 'Not found'});
 | 
									   return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
			   }
 | 
								   }
 | 
				
			||||||
			   // find latest version
 | 
								   // Find latest version
 | 
				
			||||||
			   const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1})
 | 
								   const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1})
 | 
				
			||||||
			   .lean().exec().catch(err => {next(err);}) as any;
 | 
								   .lean().exec().catch(err => {next(err);}) as any;
 | 
				
			||||||
			   if (templateData instanceof Error) return;
 | 
								   if (templateData instanceof Error) return;
 | 
				
			||||||
@@ -63,8 +63,8 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
				
			|||||||
				   return res.status(404).json({status: 'Not found'});
 | 
									   return res.status(404).json({status: 'Not found'});
 | 
				
			||||||
			   }
 | 
								   }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			   if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) {  // data was changed
 | 
								   if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) {  // Data was changed
 | 
				
			||||||
				   if (!template.parameters || _.isEqual(templateData.parameters, template.parameters)) {  // only name was changed
 | 
									   if (!template.parameters || _.isEqual(templateData.parameters, template.parameters)) {  // Only name was changed
 | 
				
			||||||
					   model(req).findByIdAndUpdate(req.params.id, {name: template.name}, {new: true})
 | 
										   model(req).findByIdAndUpdate(req.params.id, {name: template.name}, {new: true})
 | 
				
			||||||
					   .log(req).lean().exec((err, data) => {
 | 
										   .log(req).lean().exec((err, data) => {
 | 
				
			||||||
						   if (err) next (err);
 | 
											   if (err) next (err);
 | 
				
			||||||
@@ -72,15 +72,15 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
				
			|||||||
					   });
 | 
										   });
 | 
				
			||||||
				   }
 | 
									   }
 | 
				
			||||||
				   else if (template.parameters.filter((e, i) => _.isEqual(e.range, templateData.parameters[i].range)).length
 | 
									   else if (template.parameters.filter((e, i) => _.isEqual(e.range, templateData.parameters[i].range)).length
 | 
				
			||||||
							=== templateData.parameters.length) {  // only names changed
 | 
												=== templateData.parameters.length) {  // Only names changed
 | 
				
			||||||
								const changedParameterNames = template.parameters.map((e, i) => (  // list of new names
 | 
													const changedParameterNames = template.parameters.map((e, i) => (  // List of new names
 | 
				
			||||||
																								 {name: e.name, index: i, oldName: templateData.parameters[i].name}
 | 
																													 {name: e.name, index: i, oldName: templateData.parameters[i].name}
 | 
				
			||||||
																								)).filter(e => e.name !== e.oldName);
 | 
																													)).filter(e => e.name !== e.oldName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
																								// custom mappings for different collections
 | 
																													// Custom mappings for different collections
 | 
				
			||||||
																								let targetModel;  // model of the collection where the template is used
 | 
																													let targetModel;  // Model of the collection where the template is used
 | 
				
			||||||
																							let pathPrefix;   // path to the parameters in use
 | 
																												let pathPrefix;   // Path to the parameters in use
 | 
				
			||||||
																							let templatePath; // complete path of the template property
 | 
																												let templatePath; // Complete path of the template property
 | 
				
			||||||
																							switch (req.params.collection) {
 | 
																												switch (req.params.collection) {
 | 
				
			||||||
																								case 'condition':
 | 
																													case 'condition':
 | 
				
			||||||
																									targetModel = SampleModel;
 | 
																														targetModel = SampleModel;
 | 
				
			||||||
@@ -116,8 +116,8 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
				
			|||||||
																												   });
 | 
																																	   });
 | 
				
			||||||
							}
 | 
												}
 | 
				
			||||||
							else {
 | 
												else {
 | 
				
			||||||
								template.version = templateData.version + 1;  // increase version
 | 
													template.version = templateData.version + 1;  // Increase version
 | 
				
			||||||
								// save new template, fill with old properties
 | 
													// Save new template, fill with old properties
 | 
				
			||||||
								await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => {
 | 
													await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => {
 | 
				
			||||||
									if (err) next (err);
 | 
														if (err) next (err);
 | 
				
			||||||
									db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
 | 
														db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
 | 
				
			||||||
@@ -136,9 +136,9 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
				
			|||||||
			   const {error, value: template} = TemplateValidate.input(req.body, 'new');
 | 
								   const {error, value: template} = TemplateValidate.input(req.body, 'new');
 | 
				
			||||||
			   if (error) return res400(error, res);
 | 
								   if (error) return res400(error, res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			   template._id = mongoose.Types.ObjectId();  // set reference to itself for first version of template
 | 
								   template._id = mongoose.Types.ObjectId();  // Set reference to itself for first version of template
 | 
				
			||||||
				   template.first_id = template._id;
 | 
									   template.first_id = template._id;
 | 
				
			||||||
			   template.version = 1;  // set template version
 | 
								   template.version = 1;  // Set template version
 | 
				
			||||||
			   await new (model(req))(template).save((err, data) => {
 | 
								   await new (model(req))(template).save((err, data) => {
 | 
				
			||||||
				   if (err) next (err);
 | 
									   if (err) next (err);
 | 
				
			||||||
				   db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
 | 
									   db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
 | 
				
			||||||
@@ -149,7 +149,7 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
		   module.exports = router;
 | 
							   module.exports = router;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		   function model (req) {  // return right template model
 | 
							   function model (req) {  // Return right template model
 | 
				
			||||||
			   switch (req.params.collection) {
 | 
								   switch (req.params.collection) {
 | 
				
			||||||
				   case 'condition': return ConditionTemplateModel
 | 
									   case 'condition': return ConditionTemplateModel
 | 
				
			||||||
				   case 'measurement': return MeasurementTemplateModel
 | 
									   case 'measurement': return MeasurementTemplateModel
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,12 +18,12 @@ router.get('/users', (req, res) => {
 | 
				
			|||||||
	if (!req.auth(res, ['admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	UserModel.find({}).lean().exec(  (err, data:any) => {
 | 
						UserModel.find({}).lean().exec(  (err, data:any) => {
 | 
				
			||||||
		// validate all and filter null values from validation errors
 | 
							// Validate all and filter null values from validation errors
 | 
				
			||||||
		res.json(_.compact(data.map(e => UserValidate.output(e, 'admin'))));
 | 
							res.json(_.compact(data.map(e => UserValidate.output(e, 'admin'))));
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this path matches /user, /user/ and /user/xxx, but not /user/key or user/new.
 | 
					// This path matches /user, /user/ and /user/xxx, but not /user/key or user/new.
 | 
				
			||||||
// See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
 | 
					// See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
 | 
				
			||||||
router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
 | 
					router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
@@ -33,7 +33,7 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
 | 
				
			|||||||
	UserModel.findOne({name: username}).lean().exec(  (err, data:any) => {
 | 
						UserModel.findOne({name: username}).lean().exec(  (err, data:any) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		if (data) {
 | 
							if (data) {
 | 
				
			||||||
			res.json(UserValidate.output(data));  // validate all and filter null values from validation errors
 | 
								res.json(UserValidate.output(data));  // Validate all and filter null values from validation errors
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {
 | 
							else {
 | 
				
			||||||
			res.status(404).json({status: 'Not found'});
 | 
								res.status(404).json({status: 'Not found'});
 | 
				
			||||||
@@ -41,7 +41,7 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this path matches /user, /user/ and /user/xxx, but not /user/key or user/new
 | 
					// This path matches /user, /user/ and /user/xxx, but not /user/key or user/new
 | 
				
			||||||
router.put('/user:username([/](?!key|new|restore).?*|/?)', async (req, res, next) => {
 | 
					router.put('/user:username([/](?!key|new|restore).?*|/?)', async (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,7 +56,7 @@ router.put('/user:username([/](?!key|new|restore).?*|/?)', async (req, res, next
 | 
				
			|||||||
		user.pass = bcrypt.hashSync(user.pass, 10);
 | 
							user.pass = bcrypt.hashSync(user.pass, 10);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// check that user does not already exist if new name was specified
 | 
						// Check that user does not already exist if new name was specified
 | 
				
			||||||
	if (user.hasOwnProperty('name') && user.name !== username) {
 | 
						if (user.hasOwnProperty('name') && user.name !== username) {
 | 
				
			||||||
		if (!await usernameCheck(user.name, res, next)) return;
 | 
							if (!await usernameCheck(user.name, res, next)) return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
@@ -65,13 +65,13 @@ router.put('/user:username([/](?!key|new|restore).?*|/?)', async (req, res, next
 | 
				
			|||||||
		if (!await modelsCheck(user.models, res, next)) return;
 | 
							if (!await modelsCheck(user.models, res, next)) return;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// get current mail address to compare to given address
 | 
						// Get current mail address to compare to given address
 | 
				
			||||||
	const oldUserData = await UserModel.findOne({name: username}).lean().exec().catch(err => next(err));
 | 
						const oldUserData = await UserModel.findOne({name: username}).lean().exec().catch(err => next(err));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec(  (err, data:any) => {
 | 
						await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec(  (err, data:any) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		if (data) {
 | 
							if (data) {
 | 
				
			||||||
			if (data.email !== oldUserData.email) {  // mail address was changed, send notice to old address
 | 
								if (data.email !== oldUserData.email) {  // Mail address was changed, send notice to old address
 | 
				
			||||||
				Mail.send(oldUserData.email, 'Email change in your DeFinMa database account',
 | 
									Mail.send(oldUserData.email, 'Email change in your DeFinMa database account',
 | 
				
			||||||
						  'Hi, <br><br> Your email address of your DeFinMa account was changed to ' + data.mail +
 | 
											  'Hi, <br><br> Your email address of your DeFinMa account was changed to ' + data.mail +
 | 
				
			||||||
							  '<br><br>If you actually did this, just delete this email.' +
 | 
												  '<br><br>If you actually did this, just delete this email.' +
 | 
				
			||||||
@@ -87,7 +87,7 @@ router.put('/user:username([/](?!key|new|restore).?*|/?)', async (req, res, next
 | 
				
			|||||||
	});
 | 
						});
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// this path matches /user, /user/ and /user/xxx, but not /user/key or user/new.
 | 
					// This path matches /user, /user/ and /user/xxx, but not /user/key or user/new.
 | 
				
			||||||
// See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
 | 
					// See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
 | 
				
			||||||
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
 | 
					router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['predict', 'read', 'write', 'dev', 'admin'], 'basic')) return;
 | 
				
			||||||
@@ -132,19 +132,19 @@ router.get('/user/key', (req, res, next) => {
 | 
				
			|||||||
router.post('/user/new', async (req, res, next) => {
 | 
					router.post('/user/new', async (req, res, next) => {
 | 
				
			||||||
	if (!req.auth(res, ['admin'], 'basic')) return;
 | 
						if (!req.auth(res, ['admin'], 'basic')) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate input
 | 
						// Validate input
 | 
				
			||||||
	const {error, value: user} = UserValidate.input(req.body, 'new');
 | 
						const {error, value: user} = UserValidate.input(req.body, 'new');
 | 
				
			||||||
	if (error) return res400(error, res);
 | 
						if (error) return res400(error, res);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// check that user does not already exist
 | 
						// Check that user does not already exist
 | 
				
			||||||
	if (!await usernameCheck(user.name, res, next)) return;
 | 
						if (!await usernameCheck(user.name, res, next)) return;
 | 
				
			||||||
	if (!await modelsCheck(user.models, res, next)) return;
 | 
						if (!await modelsCheck(user.models, res, next)) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	user.key = mongoose.Types.ObjectId();  // use object id as unique API key
 | 
						user.key = mongoose.Types.ObjectId();  // Use object id as unique API key
 | 
				
			||||||
	user.status = globals.status.new;
 | 
						user.status = globals.status.new;
 | 
				
			||||||
	bcrypt.hash(user.pass, 10, (err, hash) => {  // password hashing
 | 
						bcrypt.hash(user.pass, 10, (err, hash) => {  // Password hashing
 | 
				
			||||||
		user.pass = hash;
 | 
							user.pass = hash;
 | 
				
			||||||
		new UserModel(user).save((err, data) => {  // store user
 | 
							new UserModel(user).save((err, data) => {  // Store user
 | 
				
			||||||
			if (err) return next(err);
 | 
								if (err) return next(err);
 | 
				
			||||||
			db.log(req, 'users', {_id: data._id}, data.toObject());
 | 
								db.log(req, 'users', {_id: data._id}, data.toObject());
 | 
				
			||||||
			res.json(UserValidate.output(data.toObject()));
 | 
								res.json(UserValidate.output(data.toObject()));
 | 
				
			||||||
@@ -153,18 +153,18 @@ router.post('/user/new', async (req, res, next) => {
 | 
				
			|||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.post('/user/passreset', (req, res, next) => {
 | 
					router.post('/user/passreset', (req, res, next) => {
 | 
				
			||||||
	// check if user/email combo exists
 | 
						// Check if user/email combo exists
 | 
				
			||||||
	UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => {
 | 
						UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => {
 | 
				
			||||||
		if (err) return next(err);
 | 
							if (err) return next(err);
 | 
				
			||||||
		if (data.length === 1) {  // it exists
 | 
							if (data.length === 1) {  // It exists
 | 
				
			||||||
			const newPass = Math.random().toString(36).substring(2);  // generate temporary password
 | 
								const newPass = Math.random().toString(36).substring(2);  // Generate temporary password
 | 
				
			||||||
			bcrypt.hash(newPass, 10, (err, hash) => {  // password hashing
 | 
								bcrypt.hash(newPass, 10, (err, hash) => {  // Password hashing
 | 
				
			||||||
				if (err) return next(err);
 | 
									if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}).log(req).exec(err => {  // write new password
 | 
									UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}).log(req).exec(err => {  // Write new password
 | 
				
			||||||
					if (err) return next(err);
 | 
										if (err) return next(err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
					// send email
 | 
										// Send email
 | 
				
			||||||
					Mail.send(data[0].email, 'Your new password for the DeFinMa database',
 | 
										Mail.send(data[0].email, 'Your new password for the DeFinMa database',
 | 
				
			||||||
							  'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '' +
 | 
												  'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '' +
 | 
				
			||||||
							  '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.' +
 | 
												  '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.' +
 | 
				
			||||||
@@ -184,9 +184,9 @@ router.post('/user/passreset', (req, res, next) => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
module.exports = router;
 | 
					module.exports = router;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getUsername (req, res) {  // returns username or false if action is not allowed
 | 
					function getUsername (req, res) {  // Returns username or false if action is not allowed
 | 
				
			||||||
	req.params.username = req.params[0];  // because of path regex
 | 
						req.params.username = req.params[0];  // Because of path regex
 | 
				
			||||||
if (req.params.username !== undefined) {  // different username than request user
 | 
					if (req.params.username !== undefined) {  // Different username than request user
 | 
				
			||||||
	if (!req.auth(res, ['admin'], 'basic')) return false;
 | 
						if (!req.auth(res, ['admin'], 'basic')) return false;
 | 
				
			||||||
	return req.params.username;
 | 
						return req.params.username;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -195,7 +195,7 @@ else {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function usernameCheck (name, res, next) {  // check if username is already taken
 | 
					async function usernameCheck (name, res, next) {  // Check if username is already taken
 | 
				
			||||||
	const userData = await UserModel.findOne({name: name}).lean().exec().catch(err => next(err)) as any;
 | 
						const userData = await UserModel.findOne({name: name}).lean().exec().catch(err => next(err)) as any;
 | 
				
			||||||
if (userData instanceof Error) return false;
 | 
					if (userData instanceof Error) return false;
 | 
				
			||||||
if (userData || UserValidate.isSpecialName(name)) {
 | 
					if (userData || UserValidate.isSpecialName(name)) {
 | 
				
			||||||
@@ -205,7 +205,7 @@ if (userData || UserValidate.isSpecialName(name)) {
 | 
				
			|||||||
return true;
 | 
					return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function modelsCheck (models, res, next) {  // check if model ids exist, returns false on error
 | 
					async function modelsCheck (models, res, next) {  // Check if model ids exist, returns false on error
 | 
				
			||||||
	let result = true;
 | 
						let result = true;
 | 
				
			||||||
for (let i in models) {
 | 
					for (let i in models) {
 | 
				
			||||||
	const model = await ModelModel.findOne({'models._id': mongoose.Types.ObjectId(models[i])})
 | 
						const model = await ModelModel.findOne({'models._id': mongoose.Types.ObjectId(models[i])})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,11 +6,11 @@ export default class IdValidate {
 | 
				
			|||||||
	.length(24)
 | 
						.length(24)
 | 
				
			||||||
	.messages({'string.pattern.base': 'Invalid object id'});
 | 
						.messages({'string.pattern.base': 'Invalid object id'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static get () {  // return joi validation
 | 
						static get () {  // Return joi validation
 | 
				
			||||||
		return this.id;
 | 
							return this.id;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static valid (id) {  // validate id
 | 
						static valid (id) {  // Validate id
 | 
				
			||||||
		return this.id.validate(id).error === undefined;
 | 
							return this.id.validate(id).error === undefined;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -18,13 +18,13 @@ export default class IdValidate {
 | 
				
			|||||||
		return ':id([0-9a-f]{24})';
 | 
							return ':id([0-9a-f]{24})';
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static stringify (data) {  // convert all ObjectID objects to plain strings
 | 
						static stringify (data) {  // Convert all ObjectID objects to plain strings
 | 
				
			||||||
		Object.keys(data).forEach(key => {
 | 
							Object.keys(data).forEach(key => {
 | 
				
			||||||
			// stringify id
 | 
								// Stringify id
 | 
				
			||||||
			if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') {
 | 
								if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') {
 | 
				
			||||||
				data[key] = data[key].toString();
 | 
									data[key] = data[key].toString();
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			else if (typeof data[key] === 'object' && data[key] !== null) {  // deeper into recursion
 | 
								else if (typeof data[key] === 'object' && data[key] !== null) {  // Deeper into recursion
 | 
				
			||||||
				data[key] = this.stringify(data[key]);
 | 
									data[key] = this.stringify(data[key]);
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ import Joi from 'joi';
 | 
				
			|||||||
import IdValidate from './id';
 | 
					import IdValidate from './id';
 | 
				
			||||||
import globals from '../../globals';
 | 
					import globals from '../../globals';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class MaterialValidate {  // validate input for material
 | 
					export default class MaterialValidate {  // Validate input for material
 | 
				
			||||||
	private static material = {
 | 
						private static material = {
 | 
				
			||||||
		name: Joi.string()
 | 
							name: Joi.string()
 | 
				
			||||||
		.max(128),
 | 
							.max(128),
 | 
				
			||||||
@@ -26,7 +26,7 @@ export default class MaterialValidate {  // validate input for material
 | 
				
			|||||||
		.valid(...Object.values(globals.status))
 | 
							.valid(...Object.values(globals.status))
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static input (data, param) {  // validate input, set param to 'new' to make all attributes required
 | 
						static input (data, param) {  // Validate input, set param to 'new' to make all attributes required
 | 
				
			||||||
		if (param === 'new') {
 | 
							if (param === 'new') {
 | 
				
			||||||
			return Joi.object({
 | 
								return Joi.object({
 | 
				
			||||||
				name: this.material.name.required(),
 | 
									name: this.material.name.required(),
 | 
				
			||||||
@@ -50,7 +50,7 @@ export default class MaterialValidate {  // validate input for material
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static output (data, status = false) {  // validate output and strip unwanted properties, returns null if not valid
 | 
						static output (data, status = false) {  // Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		data = IdValidate.stringify(data);
 | 
							data = IdValidate.stringify(data);
 | 
				
			||||||
	data.group = data.group_id.name;
 | 
						data.group = data.group_id.name;
 | 
				
			||||||
	data.supplier = data.supplier_id.name;
 | 
						data.supplier = data.supplier_id.name;
 | 
				
			||||||
@@ -69,17 +69,17 @@ export default class MaterialValidate {  // validate input for material
 | 
				
			|||||||
	return error !== undefined? null : value;
 | 
						return error !== undefined? null : value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static outputGroups (data)  {// validate groups output and strip unwanted properties, returns null if not valid
 | 
						static outputGroups (data)  {// Validate groups output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		const {value, error} = this.material.group.validate(data, {stripUnknown: true});
 | 
							const {value, error} = this.material.group.validate(data, {stripUnknown: true});
 | 
				
			||||||
	return error !== undefined? null : value;
 | 
						return error !== undefined? null : value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static outputSuppliers (data)  {// validate suppliers output and strip unwanted properties, returns null if not valid
 | 
						static outputSuppliers (data)  {// Validate suppliers output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		const {value, error} = this.material.supplier.validate(data, {stripUnknown: true});
 | 
							const {value, error} = this.material.supplier.validate(data, {stripUnknown: true});
 | 
				
			||||||
	return error !== undefined? null : value;
 | 
						return error !== undefined? null : value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static outputV() {  // return output validator
 | 
						static outputV() {  // Return output validator
 | 
				
			||||||
		return Joi.object({
 | 
							return Joi.object({
 | 
				
			||||||
			_id: IdValidate.get(),
 | 
								_id: IdValidate.get(),
 | 
				
			||||||
			name: this.material.name,
 | 
								name: this.material.name,
 | 
				
			||||||
@@ -92,7 +92,7 @@ export default class MaterialValidate {  // validate input for material
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	static query (data, dev = false) {
 | 
						static query (data, dev = false) {
 | 
				
			||||||
		const acceptedStatuses = [globals.status.val, globals.status.new];
 | 
							const acceptedStatuses = [globals.status.val, globals.status.new];
 | 
				
			||||||
		if (dev) {  // dev and admin can also access deleted samples
 | 
							if (dev) {  // Dev and admin can also access deleted samples
 | 
				
			||||||
			acceptedStatuses.push(globals.status.del)
 | 
								acceptedStatuses.push(globals.status.del)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return Joi.object({
 | 
							return Joi.object({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,14 +11,14 @@ export default class MeasurementValidate {
 | 
				
			|||||||
	Joi.string().max(128),
 | 
						Joi.string().max(128),
 | 
				
			||||||
	Joi.number(),
 | 
						Joi.number(),
 | 
				
			||||||
	Joi.boolean(),
 | 
						Joi.boolean(),
 | 
				
			||||||
	Joi.array().items(Joi.array().items(Joi.number())),  // for spectra
 | 
						Joi.array().items(Joi.array().items(Joi.number())),  // For spectra
 | 
				
			||||||
		Joi.array()
 | 
							Joi.array()
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
.allow(null)
 | 
					.allow(null)
 | 
				
			||||||
				)
 | 
									)
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static input (data, param) {  // validate input, set param to 'new' to make all attributes required
 | 
						static input (data, param) {  // Validate input, set param to 'new' to make all attributes required
 | 
				
			||||||
		if (param === 'new') {
 | 
							if (param === 'new') {
 | 
				
			||||||
			return Joi.object({
 | 
								return Joi.object({
 | 
				
			||||||
				sample_id: IdValidate.get().required(),
 | 
									sample_id: IdValidate.get().required(),
 | 
				
			||||||
@@ -36,10 +36,10 @@ export default class MeasurementValidate {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate output and strip unwanted properties, returns null if not valid
 | 
						// Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
	static output (data, req, status = false) {
 | 
						static output (data, req, status = false) {
 | 
				
			||||||
		data = IdValidate.stringify(data);
 | 
							data = IdValidate.stringify(data);
 | 
				
			||||||
		// spectral data not allowed for read/write users
 | 
							// Spectral data not allowed for read/write users
 | 
				
			||||||
		if (['dev', 'admin'].indexOf(req.authDetails.level) < 0 && data.values[globals.spectrum.dpt]) {
 | 
							if (['dev', 'admin'].indexOf(req.authDetails.level) < 0 && data.values[globals.spectrum.dpt]) {
 | 
				
			||||||
			delete data.values[globals.spectrum.dpt];
 | 
								delete data.values[globals.spectrum.dpt];
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
@@ -56,7 +56,7 @@ export default class MeasurementValidate {
 | 
				
			|||||||
		return error !== undefined? null : value;
 | 
							return error !== undefined? null : value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static outputV() {  // return output validator
 | 
						static outputV() {  // Return output validator
 | 
				
			||||||
		return Joi.object({
 | 
							return Joi.object({
 | 
				
			||||||
			_id: IdValidate.get(),
 | 
								_id: IdValidate.get(),
 | 
				
			||||||
			sample_id: IdValidate.get(),
 | 
								sample_id: IdValidate.get(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,7 +2,7 @@ import Joi from 'joi';
 | 
				
			|||||||
import IdValidate from './id';
 | 
					import IdValidate from './id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ModelValidate {  // validate input for model
 | 
					export default class ModelValidate {  // Validate input for model
 | 
				
			||||||
	private static model = {
 | 
						private static model = {
 | 
				
			||||||
		group: Joi.string()
 | 
							group: Joi.string()
 | 
				
			||||||
		.disallow('file')
 | 
							.disallow('file')
 | 
				
			||||||
@@ -20,11 +20,11 @@ export default class ModelValidate {  // validate input for model
 | 
				
			|||||||
		})
 | 
							})
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static input (data) {  // validate input
 | 
						static input (data) {  // Validate input
 | 
				
			||||||
		return this.model.model.required().validate(data);
 | 
							return this.model.model.required().validate(data);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static output (data) {  // validate output and strip unwanted properties, returns null if not valid
 | 
						static output (data) {  // Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		data = IdValidate.stringify(data);
 | 
							data = IdValidate.stringify(data);
 | 
				
			||||||
	const {value, error} = Joi.object({
 | 
						const {value, error} = Joi.object({
 | 
				
			||||||
		group: this.model.group,
 | 
							group: this.model.group,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ export default class NoteFieldValidate {
 | 
				
			|||||||
		qty: Joi.number()
 | 
							qty: Joi.number()
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static output (data) {  // validate output and strip unwanted properties, returns null if not valid
 | 
						static output (data) {  // Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		const {value, error} = Joi.object({
 | 
							const {value, error} = Joi.object({
 | 
				
			||||||
			name: this.note_field.name,
 | 
								name: this.note_field.name,
 | 
				
			||||||
			qty: this.note_field.qty
 | 
								qty: this.note_field.qty
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import Joi from 'joi';
 | 
					import Joi from 'joi';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class ParametersValidate {
 | 
					export default class ParametersValidate {
 | 
				
			||||||
	// data to validate, parameters from template, param: 'new', 'change', 'null'(null values are allowed)
 | 
						// Data to validate, parameters from template, param: 'new', 'change', 'null'(null values are allowed)
 | 
				
			||||||
	static input (data, parameters, param) {
 | 
						static input (data, parameters, param) {
 | 
				
			||||||
		let joiObject = {};
 | 
							let joiObject = {};
 | 
				
			||||||
		parameters.forEach(parameter => {
 | 
							parameters.forEach(parameter => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
// respond with 400 and include error details from the joi validation
 | 
					// Respond with 400 and include error details from the joi validation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function res400 (error, res) {
 | 
					export default function res400 (error, res) {
 | 
				
			||||||
	res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
 | 
						res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import Joi from 'joi';
 | 
					import Joi from 'joi';
 | 
				
			||||||
import IdValidate from './id';
 | 
					import IdValidate from './id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class RootValidate {  // validate input for root methods
 | 
					export default class RootValidate {  // Validate input for root methods
 | 
				
			||||||
	private static changelog = {
 | 
						private static changelog = {
 | 
				
			||||||
		timestamp: Joi.date()
 | 
							timestamp: Joi.date()
 | 
				
			||||||
		.iso()
 | 
							.iso()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -58,7 +58,7 @@ export default class SampleValidate {
 | 
				
			|||||||
		.valid(...Object.values(globals.status))
 | 
							.valid(...Object.values(globals.status))
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static readonly sampleKeys = [  // keys which can be found in the sample directly
 | 
						static readonly sampleKeys = [  // Keys which can be found in the sample directly
 | 
				
			||||||
		'_id',
 | 
							'_id',
 | 
				
			||||||
		'color',
 | 
							'color',
 | 
				
			||||||
		'number',
 | 
							'number',
 | 
				
			||||||
@@ -102,7 +102,7 @@ export default class SampleValidate {
 | 
				
			|||||||
		`measurements.${globals.spectrum.spectrum}.${globals.spectrum.dpt}`,
 | 
							`measurements.${globals.spectrum.spectrum}.${globals.spectrum.dpt}`,
 | 
				
			||||||
	];
 | 
						];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static input (data, param) {  // validate input, set param to 'new' to make all attributes required
 | 
						static input (data, param) {  // Validate input, set param to 'new' to make all attributes required
 | 
				
			||||||
		if (param === 'new') {
 | 
							if (param === 'new') {
 | 
				
			||||||
			return Joi.object({
 | 
								return Joi.object({
 | 
				
			||||||
				color: this.sample.color.required(),
 | 
									color: this.sample.color.required(),
 | 
				
			||||||
@@ -139,7 +139,7 @@ export default class SampleValidate {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// validate output and strip unwanted properties, returns null if not valid
 | 
						// Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
	static output (data, param = 'refs+added', additionalParams = []) {
 | 
						static output (data, param = 'refs+added', additionalParams = []) {
 | 
				
			||||||
		if (param === 'refs+added') {
 | 
							if (param === 'refs+added') {
 | 
				
			||||||
			param = 'refs';
 | 
								param = 'refs';
 | 
				
			||||||
@@ -200,13 +200,13 @@ export default class SampleValidate {
 | 
				
			|||||||
					}
 | 
										}
 | 
				
			||||||
					catch (ignore) {}
 | 
										catch (ignore) {}
 | 
				
			||||||
					data.filters[i] = JSON.parse(data.filters[i]);
 | 
										data.filters[i] = JSON.parse(data.filters[i]);
 | 
				
			||||||
					data.filters[i].values = data.filters[i].values.map(e => {  // validate filter values
 | 
										data.filters[i].values = data.filters[i].values.map(e => {  // Validate filter values
 | 
				
			||||||
						if (e === null) {  // null values are always allowed
 | 
											if (e === null) {  // Null values are always allowed
 | 
				
			||||||
							return null;
 | 
												return null;
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
						let validator;
 | 
											let validator;
 | 
				
			||||||
						let field = data.filters[i].field;
 | 
											let field = data.filters[i].field;
 | 
				
			||||||
						if (/material\./.test(field)) {  // select right validation model
 | 
											if (/material\./.test(field)) {  // Select right validation model
 | 
				
			||||||
							validator = MaterialValidate.outputV().append({
 | 
												validator = MaterialValidate.outputV().append({
 | 
				
			||||||
								number: Joi.string().max(128).allow(''),
 | 
													number: Joi.string().max(128).allow(''),
 | 
				
			||||||
								properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128).allow(''))
 | 
													properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128).allow(''))
 | 
				
			||||||
@@ -240,7 +240,7 @@ export default class SampleValidate {
 | 
				
			|||||||
							validator = Joi.object(this.sample);
 | 
												validator = Joi.object(this.sample);
 | 
				
			||||||
						}
 | 
											}
 | 
				
			||||||
						const {value, error} = validator.validate({[field]: e});
 | 
											const {value, error} = validator.validate({[field]: e});
 | 
				
			||||||
						if (error) throw error;  // reject invalid values
 | 
											if (error) throw error;  // Reject invalid values
 | 
				
			||||||
						return value[field];
 | 
											return value[field];
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
@@ -250,7 +250,7 @@ export default class SampleValidate {
 | 
				
			|||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		const acceptedStatuses = [globals.status.val, globals.status.new];
 | 
							const acceptedStatuses = [globals.status.val, globals.status.new];
 | 
				
			||||||
		if (dev) {  // dev and admin can also access deleted samples
 | 
							if (dev) {  // Dev and admin can also access deleted samples
 | 
				
			||||||
			acceptedStatuses.push(globals.status.del)
 | 
								acceptedStatuses.push(globals.status.del)
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		return Joi.object({
 | 
							return Joi.object({
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,7 +39,7 @@ export default class TemplateValidate {
 | 
				
			|||||||
		)
 | 
							)
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static input (data, param) {  // validate input, set param to 'new' to make all attributes required
 | 
						static input (data, param) {  // Validate input, set param to 'new' to make all attributes required
 | 
				
			||||||
		if (param === 'new') {
 | 
							if (param === 'new') {
 | 
				
			||||||
			return Joi.object({
 | 
								return Joi.object({
 | 
				
			||||||
				name: this.template.name.required(),
 | 
									name: this.template.name.required(),
 | 
				
			||||||
@@ -57,7 +57,7 @@ export default class TemplateValidate {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static output (data) {  // validate output and strip unwanted properties, returns null if not valid
 | 
						static output (data) {  // Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		data = IdValidate.stringify(data);
 | 
							data = IdValidate.stringify(data);
 | 
				
			||||||
	const {value, error} = Joi.object({
 | 
						const {value, error} = Joi.object({
 | 
				
			||||||
		_id: IdValidate.get(),
 | 
							_id: IdValidate.get(),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@ import globals from '../../globals';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import IdValidate from './id';
 | 
					import IdValidate from './id';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class UserValidate {  // validate input for user
 | 
					export default class UserValidate {  // Validate input for user
 | 
				
			||||||
	private static user = {
 | 
						private static user = {
 | 
				
			||||||
		name: Joi.string()
 | 
							name: Joi.string()
 | 
				
			||||||
		.lowercase()
 | 
							.lowercase()
 | 
				
			||||||
@@ -40,9 +40,9 @@ export default class UserValidate {  // validate input for user
 | 
				
			|||||||
			  .valid(...Object.values(globals.status))
 | 
								  .valid(...Object.values(globals.status))
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	private static specialUsernames: string[] = ['admin', 'user', 'key', 'new', 'passreset'];  // names a user cannot take
 | 
						private static specialUsernames: string[] = ['admin', 'user', 'key', 'new', 'passreset'];  // Names a user cannot take
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static input (data, param) {  // validate input, set param to 'new' to make all attributes required
 | 
						static input (data, param) {  // Validate input, set param to 'new' to make all attributes required
 | 
				
			||||||
		if (param === 'new') {
 | 
							if (param === 'new') {
 | 
				
			||||||
			return Joi.object({
 | 
								return Joi.object({
 | 
				
			||||||
				name: this.user.name.required(),
 | 
									name: this.user.name.required(),
 | 
				
			||||||
@@ -79,7 +79,7 @@ export default class UserValidate {  // validate input for user
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static output (data, param = '') {  // validate output and strip unwanted properties, returns null if not valid
 | 
						static output (data, param = '') {  // Validate output and strip unwanted properties, returns null if not valid
 | 
				
			||||||
		data = IdValidate.stringify(data);
 | 
							data = IdValidate.stringify(data);
 | 
				
			||||||
	const validate: {[key: string]: object} = {
 | 
						const validate: {[key: string]: object} = {
 | 
				
			||||||
		_id: IdValidate.get(),
 | 
							_id: IdValidate.get(),
 | 
				
			||||||
@@ -97,7 +97,7 @@ export default class UserValidate {  // validate input for user
 | 
				
			|||||||
	return error !== undefined? null : value;
 | 
						return error !== undefined? null : value;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static isSpecialName (name) {  // true if name belongs to special names
 | 
						static isSpecialName (name) {  // True if name belongs to special names
 | 
				
			||||||
		return this.specialUsernames.indexOf(name) > -1;
 | 
							return this.specialUsernames.indexOf(name) > -1;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ import IdValidate from '../routes/validate/id';
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class TestHelper {
 | 
					export default class TestHelper {
 | 
				
			||||||
	public static auth = {  // test user credentials
 | 
						public static auth = {  // Test user credentials
 | 
				
			||||||
		admin: {pass: 'Abc123!#', key: '000000000000000000001003', id: '000000000000000000000003'},
 | 
							admin: {pass: 'Abc123!#', key: '000000000000000000001003', id: '000000000000000000000003'},
 | 
				
			||||||
		janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002', id: '000000000000000000000002'},
 | 
							janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002', id: '000000000000000000000002'},
 | 
				
			||||||
		user: {pass: 'Xyz890*)', key: '000000000000000000001001', id: '000000000000000000000001'},
 | 
							user: {pass: 'Xyz890*)', key: '000000000000000000001001', id: '000000000000000000000001'},
 | 
				
			||||||
@@ -15,7 +15,7 @@ export default class TestHelper {
 | 
				
			|||||||
		customer: {pass: 'Xyz890*)', key: '000000000000000000001005', id: '000000000000000000000005'}
 | 
							customer: {pass: 'Xyz890*)', key: '000000000000000000001005', id: '000000000000000000000005'}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	public static res = {  // default responses
 | 
						public static res = {  // Default responses
 | 
				
			||||||
		400: {status: 'Bad request'},
 | 
							400: {status: 'Bad request'},
 | 
				
			||||||
		401: {status: 'Unauthorized'},
 | 
							401: {status: 'Unauthorized'},
 | 
				
			||||||
		403: {status: 'Forbidden'},
 | 
							403: {status: 'Forbidden'},
 | 
				
			||||||
@@ -30,27 +30,27 @@ export default class TestHelper {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	static beforeEach (server, done) {
 | 
						static beforeEach (server, done) {
 | 
				
			||||||
		// delete cached server code except models as these are needed in the testing files as well
 | 
							// Delete cached server code except models as these are needed in the testing files as well
 | 
				
			||||||
		Object.keys(require.cache).filter(e => /API\\dist\\(?!(models|db|test))/.test(e)).forEach(key => {
 | 
							Object.keys(require.cache).filter(e => /API\\dist\\(?!(models|db|test))/.test(e)).forEach(key => {
 | 
				
			||||||
			delete require.cache[key];  // prevent loading from cache
 | 
								delete require.cache[key];  // Prevent loading from cache
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		server = require('../index');
 | 
							server = require('../index');
 | 
				
			||||||
		db.drop(err => {  // reset database
 | 
							db.drop(err => {  // Reset database
 | 
				
			||||||
			if (err) return done(err);
 | 
								if (err) return done(err);
 | 
				
			||||||
			db.loadJson(require('./db.json'), done);
 | 
								db.loadJson(require('./db.json'), done);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
		return server
 | 
							return server
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res,
 | 
						// Options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res,
 | 
				
			||||||
	// default (set to false if you want to dismiss default .end handling)}
 | 
						// Default (set to false if you want to dismiss default .end handling)}
 | 
				
			||||||
	static request (server, done, options) {
 | 
						static request (server, done, options) {
 | 
				
			||||||
		let st = supertest(server);
 | 
							let st = supertest(server);
 | 
				
			||||||
		if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) {  // resolve API key
 | 
							if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) {  // Resolve API key
 | 
				
			||||||
			options.url += '?key=' +
 | 
								options.url += '?key=' +
 | 
				
			||||||
				(this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key);
 | 
									(this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		switch (options.method) {  // http method
 | 
							switch (options.method) {  // Http method
 | 
				
			||||||
			case 'get':
 | 
								case 'get':
 | 
				
			||||||
				st = st.get(options.url)
 | 
									st = st.get(options.url)
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
@@ -64,16 +64,16 @@ export default class TestHelper {
 | 
				
			|||||||
				st = st.delete(options.url)
 | 
									st = st.delete(options.url)
 | 
				
			||||||
			break;
 | 
								break;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (options.hasOwnProperty('reqType')) {  // request body
 | 
							if (options.hasOwnProperty('reqType')) {  // Request body
 | 
				
			||||||
			st = st.type(options.reqType);
 | 
								st = st.type(options.reqType);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (options.hasOwnProperty('req')) {  // request body
 | 
							if (options.hasOwnProperty('req')) {  // Request body
 | 
				
			||||||
			st = st.send(options.req);
 | 
								st = st.send(options.req);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (options.hasOwnProperty('reqContentType')) {  // request body
 | 
							if (options.hasOwnProperty('reqContentType')) {  // Request body
 | 
				
			||||||
			st = st.set('Content-Type', options.reqContentType);
 | 
								st = st.set('Content-Type', options.reqContentType);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) {  // resolve basic auth
 | 
							if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) {  // Resolve basic auth
 | 
				
			||||||
			if (this.auth.hasOwnProperty(options.auth.basic)) {
 | 
								if (this.auth.hasOwnProperty(options.auth.basic)) {
 | 
				
			||||||
				st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass)
 | 
									st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
@@ -87,26 +87,26 @@ export default class TestHelper {
 | 
				
			|||||||
		else {
 | 
							else {
 | 
				
			||||||
			st = st.expect('Content-type', /json/).expect(options.httpStatus);
 | 
								st = st.expect('Content-type', /json/).expect(options.httpStatus);
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		if (options.hasOwnProperty('res')) {  // evaluate result
 | 
							if (options.hasOwnProperty('res')) {  // Evaluate result
 | 
				
			||||||
			return st.end((err, res) => {
 | 
								return st.end((err, res) => {
 | 
				
			||||||
				if (err) return done (err);
 | 
									if (err) return done (err);
 | 
				
			||||||
				should(res.body).be.eql(options.res);
 | 
									should(res.body).be.eql(options.res);
 | 
				
			||||||
				done();
 | 
									done();
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) {  // evaluate default results
 | 
							else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) {  // Evaluate default results
 | 
				
			||||||
			return st.end((err, res) => {
 | 
								return st.end((err, res) => {
 | 
				
			||||||
				if (err) return done (err);
 | 
									if (err) return done (err);
 | 
				
			||||||
				should(res.body).be.eql(this.res[options.httpStatus]);
 | 
									should(res.body).be.eql(this.res[options.httpStatus]);
 | 
				
			||||||
				done();
 | 
									done();
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)}
 | 
							// Check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)}
 | 
				
			||||||
		else if (options.hasOwnProperty('log')) {
 | 
							else if (options.hasOwnProperty('log')) {
 | 
				
			||||||
			return st.end(err => {
 | 
								return st.end(err => {
 | 
				
			||||||
				if (err) return done (err);
 | 
									if (err) return done (err);
 | 
				
			||||||
				ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0)
 | 
									ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0)
 | 
				
			||||||
				.lean().exec((err, data) => {  // latest entry
 | 
									.lean().exec((err, data) => {  // Latest entry
 | 
				
			||||||
					if (err) return done(err);
 | 
										if (err) return done(err);
 | 
				
			||||||
					should(data).have.only.keys('_id', 'action', 'collection_name', 'conditions', 'data', 'user_id', '__v');
 | 
										should(data).have.only.keys('_id', 'action', 'collection_name', 'conditions', 'data', 'user_id', '__v');
 | 
				
			||||||
					should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url);
 | 
										should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url);
 | 
				
			||||||
@@ -132,7 +132,7 @@ export default class TestHelper {
 | 
				
			|||||||
				});
 | 
									});
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else {  // return object to do .end() manually
 | 
							else {  // Return object to do .end() manually
 | 
				
			||||||
			return st;
 | 
								return st;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
import db from '../db';
 | 
					import db from '../db';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// script to load test db into dev db for a clean start
 | 
					// Script to load test db into dev db for a clean start
 | 
				
			||||||
 | 
					
 | 
				
			||||||
db.connect('dev', () => {
 | 
					db.connect('dev', () => {
 | 
				
			||||||
	console.info('dropping data...');
 | 
						console.info('dropping data...');
 | 
				
			||||||
	db.drop(() => {  // reset database
 | 
						db.drop(() => {  // Reset database
 | 
				
			||||||
		console.info('loading data...');
 | 
							console.info('loading data...');
 | 
				
			||||||
		db.loadJson(require('./db.json'), () => {
 | 
							db.loadJson(require('./db.json'), () => {
 | 
				
			||||||
			console.info('done');
 | 
								console.info('done');
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user