Merge pull request #44 in ~VLE2FE/definma-api from develop to master
* commit '5744162220b9eb1a421c71515c92012d19e00828': code improvements implemented /help route Implemented filters for no condition or measurement Implemented new template change behaviour
This commit is contained in:
		@@ -16,7 +16,7 @@ Testing is done with mocha and can be executed using `npm test`.
 | 
			
		||||
 | 
			
		||||
## General structure
 | 
			
		||||
 | 
			
		||||
[index.ts](./src/index.ts) is exectued when starting the server. It includes all setup tasks, registers middleware,
 | 
			
		||||
[index.ts](./src/index.ts) is executed when starting the server. It includes all setup tasks, registers middleware,
 | 
			
		||||
routes and error handlers. Setting the `NODE_ENV` environment variable allows starting the server either in 
 | 
			
		||||
`production`, `development` or `test` mode.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,7 @@ tags:
 | 
			
		||||
  - name: /template
 | 
			
		||||
  - name: /model
 | 
			
		||||
  - name: /user
 | 
			
		||||
  - name: /help
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
paths:
 | 
			
		||||
@@ -76,6 +77,7 @@ paths:
 | 
			
		||||
    - $ref: 'template.yaml'
 | 
			
		||||
    - $ref: 'model.yaml'
 | 
			
		||||
    - $ref: 'user.yaml'
 | 
			
		||||
    - $ref: 'help.yaml'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
components:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								api/help.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								api/help.yaml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
/help/{key}:
 | 
			
		||||
  parameters:
 | 
			
		||||
    - $ref: 'api.yaml#/components/parameters/Key'
 | 
			
		||||
  get:
 | 
			
		||||
    summary: get help text for key
 | 
			
		||||
    description: 'Auth: basic, levels: predict, read, write, dev, admin, depending on the set document level'
 | 
			
		||||
    tags:
 | 
			
		||||
      - /help
 | 
			
		||||
    responses:
 | 
			
		||||
      200:
 | 
			
		||||
        description: the required text
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: 'api.yaml#/components/schemas/Help'
 | 
			
		||||
      403:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/403'
 | 
			
		||||
      404:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/404'
 | 
			
		||||
      500:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/500'
 | 
			
		||||
  post:
 | 
			
		||||
    summary: add/replace help text
 | 
			
		||||
    description: 'Auth: basic, levels: dev, admin <br> If the given key exists, the item is replaced,
 | 
			
		||||
      otherwise it is newly created'
 | 
			
		||||
    tags:
 | 
			
		||||
      - /help
 | 
			
		||||
    requestBody:
 | 
			
		||||
      required: true
 | 
			
		||||
      content:
 | 
			
		||||
        application/json:
 | 
			
		||||
          schema:
 | 
			
		||||
            $ref: 'api.yaml#/components/schemas/Help'
 | 
			
		||||
    responses:
 | 
			
		||||
      200:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/Ok'
 | 
			
		||||
      400:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/400'
 | 
			
		||||
      401:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/401'
 | 
			
		||||
      403:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/403'
 | 
			
		||||
      500:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/500'
 | 
			
		||||
  delete:
 | 
			
		||||
    summary: remove help text
 | 
			
		||||
    description: 'Auth: basic, levels: dev, admin'
 | 
			
		||||
    tags:
 | 
			
		||||
      - /help
 | 
			
		||||
    responses:
 | 
			
		||||
      200:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/Ok'
 | 
			
		||||
      401:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/401'
 | 
			
		||||
      403:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/403'
 | 
			
		||||
      404:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/404'
 | 
			
		||||
      500:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/500'
 | 
			
		||||
@@ -47,3 +47,12 @@ Group:
 | 
			
		||||
  schema:
 | 
			
		||||
    type: string
 | 
			
		||||
  example: vn
 | 
			
		||||
 | 
			
		||||
Key:
 | 
			
		||||
  name: key
 | 
			
		||||
  description: URIComponent encoded string
 | 
			
		||||
  in: path
 | 
			
		||||
  required: true
 | 
			
		||||
  schema:
 | 
			
		||||
    type: string
 | 
			
		||||
  example: '%2Fdocumentation%2Fdatabase'
 | 
			
		||||
@@ -52,7 +52,8 @@
 | 
			
		||||
      - name: filters[]
 | 
			
		||||
        description: "the filters to apply as an array of URIComponent encoded objects in the form {mode:
 | 
			
		||||
        'eq/ne/lt/lte/gt/gte/in/nin/stringin', field: 'material.m', values: ['15']} using
 | 
			
		||||
        encodeURIComponent(JSON.stringify({}))"
 | 
			
		||||
        encodeURIComponent(JSON.stringify({})) <br>Use {mode: 'eq', field: 'condition', values: [{}]} and
 | 
			
		||||
        {mode: 'eq', field: 'measurements', values: [null]} to filter for samples without condition or measurements"
 | 
			
		||||
        in: query
 | 
			
		||||
        schema:
 | 
			
		||||
         type: array
 | 
			
		||||
 
 | 
			
		||||
@@ -233,3 +233,13 @@ ModelItem:
 | 
			
		||||
    label:
 | 
			
		||||
      type: string
 | 
			
		||||
      example: 'ml/g'
 | 
			
		||||
 | 
			
		||||
Help:
 | 
			
		||||
  properties:
 | 
			
		||||
    text:
 | 
			
		||||
      type: string
 | 
			
		||||
      example: This page documents the database.
 | 
			
		||||
    level:
 | 
			
		||||
      type: string
 | 
			
		||||
      description: can be also null to allow access without authorization
 | 
			
		||||
      example: read
 | 
			
		||||
@@ -85,7 +85,7 @@ export default class db {
 | 
			
		||||
      cron.schedule('0 0 * * *', () => {
 | 
			
		||||
        ChangelogModel.deleteMany({_id: {$lt:  // id from time
 | 
			
		||||
          Math.floor(new Date().getTime() / 1000 - changelogKeepDays * 24 * 60 * 60).toString(16) + '0000000000000000'
 | 
			
		||||
        }}).log({method: 'scheduled changelog delete', url: '', authDetails: {}}).lean().exec(err => {
 | 
			
		||||
        }}).lean().exec(err => {
 | 
			
		||||
          if (err) console.error(err);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
@@ -149,7 +149,6 @@ export default class db {
 | 
			
		||||
  // changelog entry, expects (req, this (from query helper)) or (req, collection, conditions, data)
 | 
			
		||||
  static log(req, thisOrCollection, conditions = null, data = null) {
 | 
			
		||||
    if (! (conditions || data)) {  // (req, this)
 | 
			
		||||
      console.log(11);
 | 
			
		||||
      data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {};  // replace undefined with {}
 | 
			
		||||
      // replace keys with a leading $
 | 
			
		||||
      Object.keys(data).forEach(key => {
 | 
			
		||||
@@ -158,7 +157,6 @@ export default class db {
 | 
			
		||||
          delete data[key];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      console.log(thisOrCollection._conditions);
 | 
			
		||||
      new ChangelogModel(this.logEscape(_.cloneDeep({
 | 
			
		||||
        action: req.method + ' ' + req.url,
 | 
			
		||||
        collection_name: thisOrCollection._collection.collectionName,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import {parseAsync} from 'json2csv';
 | 
			
		||||
import flatten from './flatten';
 | 
			
		||||
 | 
			
		||||
export default function csv(input: any[], f: (err, data) => void) {
 | 
			
		||||
export default function csv(input: any[], f: (err, data) => void) {  // parse JSON to CSV
 | 
			
		||||
  parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true})
 | 
			
		||||
    .then(csv => f(null, csv))
 | 
			
		||||
    .catch(err => f(err, null));
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,10 @@ import globals from '../globals';
 | 
			
		||||
export default function flatten (data, keepArray = false) {  // flatten object: {a: {b: true}} -> {a.b: true}
 | 
			
		||||
  const result = {};
 | 
			
		||||
  function recurse (cur, prop) {
 | 
			
		||||
    if (Object(cur) !== cur || Object.keys(cur).length === 0) {
 | 
			
		||||
    if (Object(cur) !== cur || Object.keys(cur).length === 0) {  // simple value
 | 
			
		||||
      result[prop] = cur;
 | 
			
		||||
    }
 | 
			
		||||
    else if (prop === `${globals.spectrum.spectrum}.${globals.spectrum.dpt}`) {
 | 
			
		||||
    else if (prop === `${globals.spectrum.spectrum}.${globals.spectrum.dpt}`) {  // convert spectrum for ML
 | 
			
		||||
      result[prop + '.labels'] = cur.map(e => parseFloat(e[0]));
 | 
			
		||||
      result[prop + '.values'] = cur.map(e => parseFloat(e[1]));
 | 
			
		||||
    }
 | 
			
		||||
@@ -27,7 +27,7 @@ export default function flatten (data, keepArray = false) {  // flatten object:
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
    else {  // object
 | 
			
		||||
      let isEmpty = true;
 | 
			
		||||
      for (let p in cur) {
 | 
			
		||||
        isEmpty = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,10 @@ import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
export default class Mail{
 | 
			
		||||
 | 
			
		||||
  static readonly address = 'definma@bosch-iot.com';
 | 
			
		||||
  static uri: string;
 | 
			
		||||
  static auth = {username: '', password: ''};
 | 
			
		||||
  static mailPass: string;
 | 
			
		||||
  static readonly address = 'definma@bosch-iot.com';  // email address
 | 
			
		||||
  static uri: string;                                 // mail API URI
 | 
			
		||||
  static auth = {username: '', password: ''};         // mail API credentials
 | 
			
		||||
  static mailPass: string;                            // mail API generates password
 | 
			
		||||
 | 
			
		||||
  static init() {
 | 
			
		||||
    if (process.env.NODE_ENV === 'production') {  // only send mails in production
 | 
			
		||||
@@ -51,14 +51,14 @@ export default class Mail{
 | 
			
		||||
      }).then(() => {  // init done successfully
 | 
			
		||||
        console.info('Mail service established successfully');
 | 
			
		||||
        this.send('lukas.veit@bosch.com', 'Mail Service started', new Date().toString());
 | 
			
		||||
      }).catch(err => {  // anywhere an error occurred
 | 
			
		||||
      }).catch(err => {  // somewhere an error occurred
 | 
			
		||||
        console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`,
 | 
			
		||||
          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
 | 
			
		||||
      axios({
 | 
			
		||||
        method: 'post',
 | 
			
		||||
 
 | 
			
		||||
@@ -100,6 +100,9 @@ app.use(require('./helpers/authorize'));  // handle authentication
 | 
			
		||||
// redirect /api routes for Angular proxy in development
 | 
			
		||||
if (process.env.NODE_ENV !== 'production') {
 | 
			
		||||
  app.use('/api/:url([^]+)', (req, res) => {
 | 
			
		||||
    if (/help\//.test(req.params.url)) { // encode URI again for help route
 | 
			
		||||
      req.params.url = 'help/' + encodeURIComponent(req.params.url.replace('help/', ''));
 | 
			
		||||
    }
 | 
			
		||||
    req.url = '/' + req.params.url;
 | 
			
		||||
    app.handle(req, res);
 | 
			
		||||
  });
 | 
			
		||||
@@ -114,6 +117,7 @@ app.use('/', require('./routes/measurement'));
 | 
			
		||||
app.use('/', require('./routes/template'));
 | 
			
		||||
app.use('/', require('./routes/model'));
 | 
			
		||||
app.use('/', require('./routes/user'));
 | 
			
		||||
app.use('/', require('./routes/help'));
 | 
			
		||||
 | 
			
		||||
// static files
 | 
			
		||||
app.use('/static', express.static('static'));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								src/models/help.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								src/models/help.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import db from '../db';
 | 
			
		||||
import mongoose from 'mongoose';
 | 
			
		||||
 | 
			
		||||
const HelpSchema = new mongoose.Schema({
 | 
			
		||||
  key: {type: String, index: {unique: true}},
 | 
			
		||||
  level: String,
 | 
			
		||||
  text: String
 | 
			
		||||
}, {minimize: false});
 | 
			
		||||
 | 
			
		||||
// changelog query helper
 | 
			
		||||
HelpSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
 | 
			
		||||
  db.log(req, this);
 | 
			
		||||
  return this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default mongoose.model<any, mongoose.Model<any, any>>('help', HelpSchema);
 | 
			
		||||
							
								
								
									
										184
									
								
								src/routes/help.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/routes/help.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,184 @@
 | 
			
		||||
import should from 'should/as-function';
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
import HelpModel from '../models/help';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('/help', () => {
 | 
			
		||||
  let server;
 | 
			
		||||
  before(done => TestHelper.before(done));
 | 
			
		||||
  beforeEach(done => server = TestHelper.beforeEach(server, done));
 | 
			
		||||
  afterEach(done => TestHelper.afterEach(server, done));
 | 
			
		||||
  after(done => TestHelper.after(done));
 | 
			
		||||
 | 
			
		||||
  describe('GET /help/{key}', () => {
 | 
			
		||||
    it('returns the required text', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 200,
 | 
			
		||||
        res: {text: 'Samples help', level: 'read'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('returns the required text without authorization if allowed', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/%2Fdocumentation',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 200,
 | 
			
		||||
        res: {text: 'Documentation help', level: 'none'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('returns 404 for an invalid key', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/documentation/database',
 | 
			
		||||
        httpStatus: 404
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('returns 404 for an unknown key', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/xxx',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 404
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('returns 403 for a text with a higher level than given', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/%2Fmodels',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 403
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects an API key', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {api: 'janedoe'},
 | 
			
		||||
        httpStatus: 401,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects an unauthorized request if a level is given', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        httpStatus: 401
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('POST /help/{key}', () => {
 | 
			
		||||
    it('changes the required text', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 200,
 | 
			
		||||
        req: {text: 'New samples help', level: 'write'}
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
        should(res.body).be.eql({status: 'OK'});
 | 
			
		||||
        HelpModel.find({key: '/samples'}).lean().exec((err, data) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(data).have.lengthOf(1);
 | 
			
		||||
          should(data[0]).have.only.keys('_id', 'key', 'text', 'level');
 | 
			
		||||
          should(data[0]).property('key', '/samples');
 | 
			
		||||
          should(data[0]).property('text', 'New samples help');
 | 
			
		||||
          should(data[0]).property('level', 'write');
 | 
			
		||||
          done();
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('saves a new text', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        url: '/help/%2Fmaterials',
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 200,
 | 
			
		||||
        req: {text: 'Materials help', level: 'dev'}
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
        should(res.body).be.eql({status: 'OK'});
 | 
			
		||||
        HelpModel.find({key: '/materials'}).lean().exec((err, data) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(data).have.lengthOf(1);
 | 
			
		||||
          should(data[0]).have.only.keys('_id', 'key', 'text', 'level', '__v');
 | 
			
		||||
          should(data[0]).property('key', '/materials');
 | 
			
		||||
          should(data[0]).property('text', 'Materials help');
 | 
			
		||||
          should(data[0]).property('level', 'dev');
 | 
			
		||||
          done();
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects an API key', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {key: 'admin'},
 | 
			
		||||
        httpStatus: 401,
 | 
			
		||||
        req: {text: 'New samples help', level: 'write'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects a write user', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 403,
 | 
			
		||||
        req: {text: 'New samples help', level: 'write'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects an unauthorized request', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        httpStatus: 401,
 | 
			
		||||
        req: {text: 'New samples help', level: 'write'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('DELETE /help/{key}', () => {
 | 
			
		||||
    it('deletes the required entry', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 200
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
        should(res.body).be.eql({status: 'OK'});
 | 
			
		||||
        HelpModel.find({key: '/materials'}).lean().exec((err, data) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(data).have.lengthOf(0);
 | 
			
		||||
          done();
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects an API key', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {key: 'admin'},
 | 
			
		||||
        httpStatus: 401
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects a write user', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 403
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects an unauthorized request', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
        url: '/help/%2Fsamples',
 | 
			
		||||
        httpStatus: 401
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										55
									
								
								src/routes/help.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/routes/help.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import express from 'express';
 | 
			
		||||
import HelpModel from '../models/help';
 | 
			
		||||
import HelpValidate from './validate/help';
 | 
			
		||||
import res400 from './validate/res400';
 | 
			
		||||
import globals from '../globals';
 | 
			
		||||
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
 | 
			
		||||
router.get('/help/:key', (req, res, next) => {
 | 
			
		||||
  const {error: paramError, value: key} = HelpValidate.params(req.params);
 | 
			
		||||
  if (paramError) return res400(paramError, res);
 | 
			
		||||
 | 
			
		||||
  HelpModel.findOne(key).lean().exec((err, data) => {
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
 | 
			
		||||
    if (!data) {
 | 
			
		||||
      return res.status(404).json({status: 'Not found'});
 | 
			
		||||
    }
 | 
			
		||||
    if (data.level !== 'none') {  // check level
 | 
			
		||||
      if (!req.auth(res,
 | 
			
		||||
        Object.values(globals.levels).slice(Object.values(globals.levels).findIndex(e => e === data.level))
 | 
			
		||||
        , 'basic')) return;
 | 
			
		||||
    }
 | 
			
		||||
    res.json(HelpValidate.output(data));
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.post('/help/:key', (req, res, next) => {
 | 
			
		||||
  if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
 | 
			
		||||
  const {error: paramError, value: key} = HelpValidate.params(req.params);
 | 
			
		||||
  if (paramError) return res400(paramError, res);
 | 
			
		||||
  const {error, value: help} = HelpValidate.input(req.body);
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  HelpModel.findOneAndUpdate(key, help, {upsert: true}).log(req).lean().exec(err => {
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
    res.json({status: 'OK'});
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.delete('/help/:key', (req, res, next) => {
 | 
			
		||||
  if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
 | 
			
		||||
  const {error: paramError, value: key} = HelpValidate.params(req.params);
 | 
			
		||||
  if (paramError) return res400(paramError, res);
 | 
			
		||||
 | 
			
		||||
  HelpModel.findOneAndDelete(key).log(req).lean().exec((err, data) => {
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
    if (!data) {
 | 
			
		||||
      return res.status(404).json({status: 'Not found'});
 | 
			
		||||
    }
 | 
			
		||||
    res.json({status: 'OK'});
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
module.exports = router;
 | 
			
		||||
@@ -25,21 +25,7 @@ router.get('/materials', (req, res, next) => {
 | 
			
		||||
    MaterialValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0);
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  let conditions;
 | 
			
		||||
 | 
			
		||||
  if (filters.hasOwnProperty('status')) {
 | 
			
		||||
    if(filters.status === 'all') {
 | 
			
		||||
      conditions = {$or: [{status: globals.status.val}, {status: globals.status.new}]}
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      conditions = {status: filters.status};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  else {  // default
 | 
			
		||||
    conditions = {status: globals.status.val};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  MaterialModel.find(conditions).sort({name: 1}).populate('group_id').populate('supplier_id')
 | 
			
		||||
  MaterialModel.find(filters).sort({name: 1}).populate('group_id').populate('supplier_id')
 | 
			
		||||
    .lean().exec((err, data) => {
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -440,6 +440,48 @@ describe('/sample', () => {
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('filters for empty conditions', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=condition&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22condition%22%2C%22values%22%3A%5B%7B%7D%5D%7D',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 200
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
        const json = require('../test/db.json');
 | 
			
		||||
        should(res.body).have.lengthOf(
 | 
			
		||||
          json.collections.samples
 | 
			
		||||
            .filter(e => e.status !== 'deleted')
 | 
			
		||||
            .filter(e => Object.keys(e.condition).length === 0)
 | 
			
		||||
            .length
 | 
			
		||||
        );
 | 
			
		||||
        should(res.body).matchEach(sample => {
 | 
			
		||||
          should(sample.condition).be.eql({});
 | 
			
		||||
        });
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('filters for samples without measurements', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
        url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=_id&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22measurements%22%2C%22values%22%3A%5Bnull%5D%7D',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 200
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
        const json = require('../test/db.json');
 | 
			
		||||
        should(res.body).have.lengthOf(
 | 
			
		||||
          json.collections.samples
 | 
			
		||||
            .filter(e => e.status !== 'deleted')
 | 
			
		||||
            .filter(e => !json.collections.measurements.find(el => el.sample_id.toString() === e._id.toString()))
 | 
			
		||||
            .length
 | 
			
		||||
        );
 | 
			
		||||
        should(res.body).matchEach(sample => {
 | 
			
		||||
          should(json.collections.measurements.find(el => el.sample_id.toString() === sample._id)).be.eql(undefined);
 | 
			
		||||
        });
 | 
			
		||||
        done();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('returns comment fields', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'get',
 | 
			
		||||
 
 | 
			
		||||
@@ -22,16 +22,12 @@ import globals from '../globals';
 | 
			
		||||
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
 | 
			
		||||
// TODO: do not use streaming for spectrum filenames
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
router.get('/samples', async (req, res, next) => {
 | 
			
		||||
  if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
 | 
			
		||||
 | 
			
		||||
  const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0);
 | 
			
		||||
  console.log(error);
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
  console.log(filters.filters);
 | 
			
		||||
 | 
			
		||||
  // spectral data and csv not allowed for read/write users
 | 
			
		||||
  if ((filters.fields.find(e => e.indexOf('.' + globals.spectrum.dpt) >= 0) || filters.output !== 'json') &&
 | 
			
		||||
@@ -219,6 +215,15 @@ router.get('/samples', async (req, res, next) => {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (sortFilterKeys.find(e => e === 'measurements')) {  // filter for samples without measurements
 | 
			
		||||
    queryPtr.push({$lookup: {
 | 
			
		||||
        from: 'measurements', let: {sId: '$_id'},
 | 
			
		||||
        pipeline: [{$match:{$expr:{$and:[{$eq:['$sample_id','$$sId']}]}}}, {$project: {_id: true}}],
 | 
			
		||||
        as: 'measurementCount'
 | 
			
		||||
      }},
 | 
			
		||||
      {$match: {measurementCount: {$size: 0}}}
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
  const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e))
 | 
			
		||||
    .map(e => e.split('.')[1]));  // filter measurement names and remove duplicates from parameters
 | 
			
		||||
  if (sortFilterKeys.find(e => /measurements\./.test(e))) {  //  add measurement fields
 | 
			
		||||
@@ -235,7 +240,7 @@ router.get('/samples', async (req, res, next) => {
 | 
			
		||||
    ];
 | 
			
		||||
    if (measurementFilterFields.indexOf(globals.spectrum.spectrum) >= 0) {  // filter out dpts
 | 
			
		||||
      pipeline.push(
 | 
			
		||||
        {$project: {'values.device': true, measurement_template: true}},
 | 
			
		||||
        {$project: {['values.' + globals.spectrum.dpt]: false}},
 | 
			
		||||
        {$addFields: {'values._id': '$_id'}}
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
@@ -379,7 +384,6 @@ router.get('/samples', async (req, res, next) => {
 | 
			
		||||
    projection._id = false;
 | 
			
		||||
  }
 | 
			
		||||
  queryPtr.push({$project: projection});
 | 
			
		||||
  console.log(JSON.stringify(query));
 | 
			
		||||
  // use streaming when including spectrum files
 | 
			
		||||
  if (!fieldsToAdd.find(e => e.indexOf(globals.spectrum.spectrum + '.' + globals.spectrum.dpt) >= 0)) {
 | 
			
		||||
    collection.aggregate(query).allowDiskUse(true).exec((err, data) => {
 | 
			
		||||
@@ -678,11 +682,10 @@ async function numberGenerate (sample, req, res, next) {
 | 
			
		||||
  const sampleData = await SampleModel
 | 
			
		||||
    .aggregate([
 | 
			
		||||
      {$match: {number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}},
 | 
			
		||||
      // {$addFields: {number2: {$toDecimal: {$arrayElemAt: [{$split: [{$arrayElemAt:
 | 
			
		||||
      //   [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}}}},  // not working with MongoDb 3.6
 | 
			
		||||
      {$addFields: {sortNumber: {$let: {
 | 
			
		||||
        vars: {tmp: {$concat: ['000000000000000000000000000000',
 | 
			
		||||
              {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}},
 | 
			
		||||
              {$arrayElemAt: [{$split:
 | 
			
		||||
                    [{$arrayElemAt: [{$split: ['$number', req.authDetails.location]}, 1]}, '_']}, 0]}]}},
 | 
			
		||||
        in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]}
 | 
			
		||||
      }}}},
 | 
			
		||||
      {$sort: {sortNumber: -1}},
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,11 @@
 | 
			
		||||
import should from 'should/as-function';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import TemplateConditionModel from '../models/condition_template';
 | 
			
		||||
import SampleModel from '../models/sample';
 | 
			
		||||
import MeasurementModel from '../models/measurement';
 | 
			
		||||
import MaterialModel from '../models/material';
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
import mongoose from 'mongoose';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('/template', () => {
 | 
			
		||||
@@ -90,7 +94,7 @@ describe('/template', () => {
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('PUT /template/condition/{name}', () => {
 | 
			
		||||
    describe('PUT /template/condition/{id}', () => {
 | 
			
		||||
      it('returns the right condition template', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
@@ -145,6 +149,24 @@ describe('/template', () => {
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
      });
 | 
			
		||||
      it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/condition/200000000000000000000001',
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 200, req: {name: 'heat treatment', parameters: [{name: 'treatmentMaterial', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]}
 | 
			
		||||
        }).end((err, res) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat treatment', version: 1, first_id: '200000000000000000000001', parameters: [{name: 'treatmentMaterial', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10, required: true}}]});
 | 
			
		||||
          SampleModel.find({'condition.condition_template': mongoose.Types.ObjectId('200000000000000000000001')}).lean().exec((err, data:any) => {
 | 
			
		||||
            if (err) return done(err);
 | 
			
		||||
            should(data).matchEach(sample => {
 | 
			
		||||
              should(sample.condition).have.only.keys('treatmentMaterial', 'weeks', 'condition_template');
 | 
			
		||||
            });
 | 
			
		||||
            done();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      it('creates a changelog', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
@@ -161,7 +183,7 @@ describe('/template', () => {
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      it('allows changing only one property', done => {
 | 
			
		||||
      it('does not increase the version on name change', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/condition/200000000000000000000001',
 | 
			
		||||
@@ -175,7 +197,7 @@ describe('/template', () => {
 | 
			
		||||
            should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
 | 
			
		||||
            should(data.first_id.toString()).be.eql('200000000000000000000001');
 | 
			
		||||
            should(data).have.property('name', 'heat aging');
 | 
			
		||||
            should(data).have.property('version', 2);
 | 
			
		||||
            should(data).have.property('version', 1);
 | 
			
		||||
            should(data).have.property('parameters').have.lengthOf(2);
 | 
			
		||||
            should(data.parameters[0]).have.property('name', 'material');
 | 
			
		||||
            should(data.parameters[1]).have.property('name', 'weeks');
 | 
			
		||||
@@ -183,6 +205,28 @@ describe('/template', () => {
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      it('does not increase the version on name change when property ranges stayed the same', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/condition/200000000000000000000001',
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 200,
 | 
			
		||||
          req: {name: 'heat aging', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'duration', range: {min: 1, max: 10, required: true}}]}
 | 
			
		||||
        }).end((err, res) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
 | 
			
		||||
            if (err) return done(err);
 | 
			
		||||
            should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
 | 
			
		||||
            should(data.first_id.toString()).be.eql('200000000000000000000001');
 | 
			
		||||
            should(data).have.property('name', 'heat aging');
 | 
			
		||||
            should(data).have.property('version', 1);
 | 
			
		||||
            should(data).have.property('parameters').have.lengthOf(2);
 | 
			
		||||
            should(data.parameters[0]).have.property('name', 'material');
 | 
			
		||||
            should(data.parameters[1]).have.property('name', 'duration');
 | 
			
		||||
            done();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      it('supports values ranges', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
@@ -526,6 +570,26 @@ describe('/template', () => {
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    describe('PUT /template/measurement/{id}', () => {
 | 
			
		||||
      it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/measurement/300000000000000000000001',
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 200, req: {name: 'spectrum', parameters: [{name: 'spectrumValues', range: {type: 'array'}}, {name: 'device', range: {}}, {name: 'filename', range: {}}]}
 | 
			
		||||
        }).end((err, res) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(res.body).be.eql({_id: '300000000000000000000001', name: 'spectrum', version: 1, first_id: '300000000000000000000001', parameters: [{name: 'spectrumValues', range: {type: 'array'}}, {name: 'device', range: {}}, {name: 'filename', range: {}}]});
 | 
			
		||||
          MeasurementModel.find({'measurement_template': mongoose.Types.ObjectId('300000000000000000000001')}).lean().exec((err, data:any) => {
 | 
			
		||||
            if (err) return done(err);
 | 
			
		||||
            should(data).matchEach(measurement => {
 | 
			
		||||
              should(measurement.values).have.only.keys('spectrumValues', 'device', 'filename');
 | 
			
		||||
            });
 | 
			
		||||
            done();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    // other methods should be covered by condition tests
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -571,6 +635,27 @@ describe('/template', () => {
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    describe('PUT /template/material/{id}', () => {
 | 
			
		||||
      it('renames all occurrences instead of creating a new version when only the parameter name is changed', done => {
 | 
			
		||||
        TestHelper.request(server, done, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/material/130000000000000000000003',
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 200,
 | 
			
		||||
          req: {name: 'plastic', parameters: [ {name: 'glassfiber', range: {min: 0, max: 100, required: true}}, {name: 'carbonfiber', range: {min: 0, max: 100, required: true}}, {name: 'mineral', range: {min: 0, max: 100, required: true}}]}
 | 
			
		||||
        }).end((err, res) => {
 | 
			
		||||
          if (err) return done(err);
 | 
			
		||||
          should(res.body).be.eql({_id: '130000000000000000000003', name: 'plastic', version: 2, first_id: '130000000000000000000001', parameters: [ {name: 'glassfiber', range: {min: 0, max: 100, required: true}}, {name: 'carbonfiber', range: {min: 0, max: 100, required: true}}, {name: 'mineral', range: {min: 0, max: 100, required: true}}]});
 | 
			
		||||
          MaterialModel.find({'properties': mongoose.Types.ObjectId('130000000000000000000003')}).lean().exec((err, data:any) => {
 | 
			
		||||
            if (err) return done(err);
 | 
			
		||||
            should(data).matchEach(material => {
 | 
			
		||||
              should(material.parameters).have.only.keys('glassfiber', 'carbonfiber', 'mineral', 'material_template');
 | 
			
		||||
            });
 | 
			
		||||
            done();
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    // other methods should be covered by condition tests
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -5,6 +5,9 @@ import TemplateValidate from './validate/template';
 | 
			
		||||
import ConditionTemplateModel from '../models/condition_template';
 | 
			
		||||
import MeasurementTemplateModel from '../models/measurement_template';
 | 
			
		||||
import MaterialTemplateModel from '../models/material_template';
 | 
			
		||||
import SampleModel from '../models/sample';
 | 
			
		||||
import MaterialModel from '../models/material';
 | 
			
		||||
import MeasurementModel from '../models/measurement';
 | 
			
		||||
import res400 from './validate/res400';
 | 
			
		||||
import IdValidate from './validate/id';
 | 
			
		||||
import mongoose from "mongoose";
 | 
			
		||||
@@ -61,13 +64,66 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) {  // data was changed
 | 
			
		||||
    template.version = templateData.version + 1;  // increase version
 | 
			
		||||
    // save new template, fill with old properties
 | 
			
		||||
    await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => {
 | 
			
		||||
      if (err) next (err);
 | 
			
		||||
      db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
 | 
			
		||||
      res.json(TemplateValidate.output(data.toObject()));
 | 
			
		||||
    });
 | 
			
		||||
    if (!template.parameters || _.isEqual(templateData.parameters, template.parameters)) {  // only name was changed
 | 
			
		||||
      model(req).findByIdAndUpdate(req.params.id, {name: template.name}, {new: true})
 | 
			
		||||
        .log(req).lean().exec((err, data) => {
 | 
			
		||||
        if (err) next (err);
 | 
			
		||||
        res.json(TemplateValidate.output(data));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else if (template.parameters.filter((e, i) => _.isEqual(e.range, templateData.parameters[i].range)).length
 | 
			
		||||
      === templateData.parameters.length) {  // only names changed
 | 
			
		||||
      const changedParameterNames = template.parameters.map((e, i) => (  // list of new names
 | 
			
		||||
          {name: e.name, index: i, oldName: templateData.parameters[i].name}
 | 
			
		||||
        )).filter(e => e.name !== e.oldName);
 | 
			
		||||
 | 
			
		||||
      // custom mappings for different collections
 | 
			
		||||
      let targetModel;  // model of the collection where the template is used
 | 
			
		||||
      let pathPrefix;   // path to the parameters in use
 | 
			
		||||
      let templatePath; // complete path of the template property
 | 
			
		||||
      switch (req.params.collection) {
 | 
			
		||||
        case 'condition':
 | 
			
		||||
          targetModel = SampleModel;
 | 
			
		||||
          pathPrefix = 'condition.';
 | 
			
		||||
          templatePath = 'condition.condition_template';
 | 
			
		||||
          break;
 | 
			
		||||
        case 'measurement':
 | 
			
		||||
          targetModel = MeasurementModel;
 | 
			
		||||
          pathPrefix = 'values.';
 | 
			
		||||
          templatePath = 'measurement_template';
 | 
			
		||||
          break;
 | 
			
		||||
        case 'material':
 | 
			
		||||
          targetModel = MaterialModel;
 | 
			
		||||
          pathPrefix = 'properties.';
 | 
			
		||||
          templatePath = 'properties.material_template';
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      targetModel.updateMany({[templatePath]: mongoose.Types.ObjectId(templateData._id)},
 | 
			
		||||
        {$rename:
 | 
			
		||||
          changedParameterNames.reduce((s, e) => {s[pathPrefix + e.oldName] = pathPrefix + e.name; return s;}, {})
 | 
			
		||||
        }) .log(req).lean().exec(err => {
 | 
			
		||||
        if (err) return next(err);
 | 
			
		||||
        model(req).findByIdAndUpdate(req.params.id,
 | 
			
		||||
          {$set:
 | 
			
		||||
            changedParameterNames.reduce(
 | 
			
		||||
              (s, e) => {s[`parameters.${e.index}.name`] = e.name; return s;}, {name: template.name}
 | 
			
		||||
            ),
 | 
			
		||||
          },{new: true}).log(req).lean().exec((err, data) => {
 | 
			
		||||
          if (err) next (err);
 | 
			
		||||
          res.json(TemplateValidate.output(data));
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      template.version = templateData.version + 1;  // increase version
 | 
			
		||||
      // save new template, fill with old properties
 | 
			
		||||
      await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => {
 | 
			
		||||
        if (err) next (err);
 | 
			
		||||
        db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
 | 
			
		||||
        res.json(TemplateValidate.output(data.toObject()));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  else {
 | 
			
		||||
    res.json(TemplateValidate.output(templateData));
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										34
									
								
								src/routes/validate/help.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/routes/validate/help.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
import Joi from 'joi';
 | 
			
		||||
import globals from '../../globals';
 | 
			
		||||
 | 
			
		||||
export default class HelpValidate {
 | 
			
		||||
  private static help = {
 | 
			
		||||
    text: Joi.string()
 | 
			
		||||
      .allow('')
 | 
			
		||||
      .max(8192),
 | 
			
		||||
 | 
			
		||||
    level: Joi.string()
 | 
			
		||||
      .valid('none', ...Object.values(globals.levels))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static input (data) {
 | 
			
		||||
    return Joi.object({
 | 
			
		||||
      text: this.help.text.required(),
 | 
			
		||||
      level: this.help.level.required()
 | 
			
		||||
    }).validate(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static output (data) {
 | 
			
		||||
    const {value, error} = Joi.object({
 | 
			
		||||
      text: this.help.text,
 | 
			
		||||
      level: this.help.level
 | 
			
		||||
    }).validate(data, {stripUnknown: true});
 | 
			
		||||
    return error !== undefined? null : value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static params(data) {
 | 
			
		||||
    return Joi.object({
 | 
			
		||||
      key: Joi.string().min(1).max(128)
 | 
			
		||||
    }).validate(data);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -98,6 +98,7 @@ export default class SampleValidate {
 | 
			
		||||
    'user_id',
 | 
			
		||||
    'material._id',
 | 
			
		||||
    'material.numbers',
 | 
			
		||||
    'measurements',
 | 
			
		||||
    `measurements.${globals.spectrum.spectrum}.${globals.spectrum.dpt}`,
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
@@ -198,7 +199,6 @@ export default class SampleValidate {
 | 
			
		||||
            data.filters[i] = decodeURIComponent(data.filters[i]);
 | 
			
		||||
          }
 | 
			
		||||
          catch (ignore) {}
 | 
			
		||||
          console.log(data.filters[i]);
 | 
			
		||||
          data.filters[i] = JSON.parse(data.filters[i]);
 | 
			
		||||
          data.filters[i].values = data.filters[i].values.map(e => {  // validate filter values
 | 
			
		||||
            if (e === null) {  // null values are always allowed
 | 
			
		||||
@@ -226,6 +226,12 @@ export default class SampleValidate {
 | 
			
		||||
              });
 | 
			
		||||
              field = 'value';
 | 
			
		||||
            }
 | 
			
		||||
            else if (field === 'measurements') {
 | 
			
		||||
              validator = Joi.object({
 | 
			
		||||
                value: Joi.object({}).allow(null).disallow({})
 | 
			
		||||
              });
 | 
			
		||||
              field = 'value';
 | 
			
		||||
            }
 | 
			
		||||
            else if (field === 'notes.comment') {
 | 
			
		||||
              field = 'comment';
 | 
			
		||||
              validator = this.sample.notes
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ export default class UserValidate {  // validate input for user
 | 
			
		||||
      }).validate(data);
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      return{error: 'No parameter specified!', value: {}};
 | 
			
		||||
      return {error: 'No parameter specified!', value: {}};
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -893,6 +893,26 @@
 | 
			
		||||
        "user_id" : {"$oid": "000000000000000000000003"},
 | 
			
		||||
        "__v" : 0
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "helps": [
 | 
			
		||||
      {
 | 
			
		||||
        "_id": {"$oid":"150000000000000000000001"},
 | 
			
		||||
        "key": "/documentation",
 | 
			
		||||
        "text": "Documentation help",
 | 
			
		||||
        "level": "none"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "_id": {"$oid":"150000000000000000000002"},
 | 
			
		||||
        "key": "/samples",
 | 
			
		||||
        "text": "Samples help",
 | 
			
		||||
        "level": "read"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "_id": {"$oid":"150000000000000000000003"},
 | 
			
		||||
        "key": "/models",
 | 
			
		||||
        "text": "Models help",
 | 
			
		||||
        "level": "dev"
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user