Archived
2

finished /template methods

This commit is contained in:
VLE2FE 2020-05-04 15:48:07 +02:00
parent 5f20afcf04
commit af071a9445
16 changed files with 898 additions and 32 deletions

View File

@ -48,7 +48,7 @@ tags:
- name: /material - name: /material
- name: /condition - name: /condition
- name: /measurement - name: /measurement
- name: /templates - name: /template
- name: /model - name: /model
- name: /user - name: /user

View File

@ -70,7 +70,7 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: TODO delete material summary: delete material
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, maintain, dev, admin'
tags: tags:
- /material - /material
@ -90,7 +90,7 @@
/material/new: /material/new:
post: post:
summary: TODO add material summary: add material
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, maintain, dev, admin'
tags: tags:
- /material - /material

View File

@ -7,6 +7,7 @@ Id:
example: 5ea0450ed851c30a90e70894 example: 5ea0450ed851c30a90e70894
Name: Name:
name: name name: name
description: has to be URL encoded
in: path in: path
required: true required: true
schema: schema:

View File

@ -1,9 +1,9 @@
/template/treatments: /template/treatments:
get: get:
summary: TODO all available treatment methods summary: all available treatment methods
description: 'Auth: basic, levels: read, write, maintain, dev, admin' description: 'Auth: basic, levels: read, write, maintain, dev, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
responses: responses:
@ -16,23 +16,26 @@
items: items:
$ref: 'api.yaml#/components/schemas/Template' $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: heat aging name: heat aging
parameters: parameters:
- name: method - name: method
range: range:
values:
- copper - copper
- hot air
401: 401:
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
/templates/treatment/{name}: /template/treatment/{name}:
parameters: parameters:
- $ref: 'api.yaml#/components/parameters/Name' - $ref: 'api.yaml#/components/parameters/Name'
get: get:
summary: TODO treatment method details summary: treatment method details
description: 'Auth: basic, levels: read, write, maintain, admin' description: 'Auth: basic, levels: read, write, maintain, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
responses: responses:
@ -44,13 +47,14 @@
allOf: allOf:
- $ref: 'api.yaml#/components/schemas/Template' - $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: heat aging name: heat aging
parameters: parameters:
- name: method - name: method
range: range:
values:
- copper - copper
400: - hot air
$ref: 'api.yaml#/components/responses/400'
401: 401:
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
404: 404:
@ -58,10 +62,10 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: TODO add/change treatment method summary: add/change treatment method
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: maintain, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
requestBody: requestBody:
@ -76,7 +80,9 @@
parameters: parameters:
- name: method - name: method
range: range:
values:
- copper - copper
- hot air
responses: responses:
200: 200:
description: treatment details description: treatment details
@ -86,11 +92,14 @@
allOf: allOf:
- $ref: 'api.yaml#/components/schemas/Template' - $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: heat aging name: heat aging
parameters: parameters:
- name: method - name: method
range: range:
values:
- copper - copper
- hot air
400: 400:
$ref: 'api.yaml#/components/responses/400' $ref: 'api.yaml#/components/responses/400'
401: 401:
@ -102,10 +111,10 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: TODO delete treatment method summary: delete treatment method
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: maintain, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
responses: responses:
@ -123,10 +132,10 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
/template/measurements: /template/measurements:
get: get:
summary: TODO all available measurement methods summary: all available measurement methods
description: 'Auth: basic, levels: read, write, maintain, dev, admin' description: 'Auth: basic, levels: read, write, maintain, dev, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
responses: responses:
@ -139,6 +148,7 @@
items: items:
$ref: 'api.yaml#/components/schemas/Template' $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: humidity name: humidity
parameters: parameters:
- name: kf - name: kf
@ -149,14 +159,14 @@
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
/templates/measurement/{name}: /template/measurement/{name}:
parameters: parameters:
- $ref: 'api.yaml#/components/parameters/Name' - $ref: 'api.yaml#/components/parameters/Name'
get: get:
summary: TODO measurement method details summary: measurement method details
description: 'Auth: basic, levels: read, write, maintain, admin' description: 'Auth: basic, levels: read, write, maintain, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
responses: responses:
@ -168,6 +178,7 @@
allOf: allOf:
- $ref: 'api.yaml#/components/schemas/Template' - $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: humidity name: humidity
parameters: parameters:
- name: kf - name: kf
@ -183,10 +194,10 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: TODO add/change measurement method summary: add/change measurement method
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: maintain, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
requestBody: requestBody:
@ -197,6 +208,7 @@
allOf: allOf:
- $ref: 'api.yaml#/components/schemas/Template' - $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: humidity name: humidity
parameters: parameters:
- name: kf - name: kf
@ -212,6 +224,7 @@
allOf: allOf:
- $ref: 'api.yaml#/components/schemas/Template' - $ref: 'api.yaml#/components/schemas/Template'
example: example:
_id: 5ea0450ed851c30a90e70894
name: humidity name: humidity
parameters: parameters:
- name: kf - name: kf
@ -229,10 +242,10 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: TODO delete measurement method summary: delete measurement method
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: maintain, admin'
tags: tags:
- /templates - /template
security: security:
- BasicAuth: [] - BasicAuth: []
responses: responses:

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"tsc": "tsc", "tsc": "tsc",
"test": "mocha dist/**/**.spec.js", "test": "mocha dist/**/**.spec.js",
"start": "tsc && node dist/index.js", "start": "tsc && node dist/index.js || exit 1",
"dev": "nodemon -e ts,yaml --exec \"npm run start\"" "dev": "nodemon -e ts,yaml --exec \"npm run start\""
}, },
"keywords": [], "keywords": [],

View File

@ -1,6 +1,8 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import cfenv from 'cfenv'; import cfenv from 'cfenv';
// mongoose.set('debug', true); // enable mongoose debug
// 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';

View File

@ -47,6 +47,7 @@ app.use(require('./helpers/authorize')); // handle authentication
app.use('/', require('./routes/root')); app.use('/', require('./routes/root'));
app.use('/', require('./routes/user')); app.use('/', require('./routes/user'));
app.use('/', require('./routes/material')); app.use('/', require('./routes/material'));
app.use('/', require('./routes/template'));
// static files // static files
app.use('/static', express.static('static')); app.use('/static', express.static('static'));

View File

@ -0,0 +1,11 @@
import mongoose from 'mongoose';
const MeasurementTemplateSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
parameters: [{
name: String,
range: mongoose.Schema.Types.Mixed
}]
}, {minimize: false}); // to allow empty objects
export default mongoose.model('measurement_template', MeasurementTemplateSchema);

View File

@ -0,0 +1,11 @@
import mongoose from 'mongoose';
const TreatmentTemplateSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
parameters: [{
name: String,
range: mongoose.Schema.Types.Mixed
}]
}, {minimize: false}); // to allow empty objects
export default mongoose.model('treatment_template', TreatmentTemplateSchema);

View File

@ -130,7 +130,7 @@ describe('/material', () => {
res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]} res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: 5514263423}]}
}); });
}); });
it('returns keeps unchanged properties', done => { it('keeps unchanged properties', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/material/100000000000000000000001', url: '/material/100000000000000000000001',
@ -148,11 +148,12 @@ describe('/material', () => {
httpStatus: 200, httpStatus: 200,
req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]} req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]}
, ,
}).end(err => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]});
MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => {
if (err) return done(err); if (err) return done(err);
data._id = data._id.toString(); data._id = data._id.toString({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}]});
data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}});
should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0} should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: 5514212901}, {color: 'signalviolet', number: 5514612901}], __v: 0}
); );

View File

@ -57,7 +57,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
} }
function f() { // to resolve async function f() { // to resolve async
MaterialModel.findByIdAndUpdate(req.params.id, material).lean().exec((err, data) => { MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => {
if (err) next(err); if (err) next(err);
if (data) { if (data) {
res.json(MaterialValidate.output(data)); res.json(MaterialValidate.output(data));

579
src/routes/template.spec.ts Normal file
View File

@ -0,0 +1,579 @@
import should from 'should/as-function';
import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template';
import TestHelper from "../helpers/test";
describe('/template', () => {
let server;
before(done => TestHelper.before(done));
beforeEach(done => server = TestHelper.beforeEach(server, done));
afterEach(done => TestHelper.afterEach(server, done));
describe('/template/treatment', () => {
describe('GET /template/treatments', () => {
it('returns all treatment templates', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatments',
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.treatment_templates.length);
should(res.body).matchEach(treatment => {
should(treatment).have.only.keys('_id', 'name', 'parameters');
should(treatment).have.property('_id').be.type('string');
should(treatment).have.property('name').be.type('string');
should(treatment.parameters).matchEach(number => {
should(number).have.only.keys('name', 'range');
should(number).have.property('name').be.type('string');
should(number).have.property('range').be.type('object');
});
});
done();
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatments',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatments',
httpStatus: 401
});
});
});
describe('GET /template/treatment/{name}', () => {
it('returns the right treatment template', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatment/heat%20treatment',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects an unknown name', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatment/xxx',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/treatment/heat%20treatment',
httpStatus: 401
});
});
});
describe('PUT /template/treatment/{name}', () => {
it('returns the right treatment template', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('keeps unchanged properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]},
res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('changes the given properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({_id: '200000000000000000000001', name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]});
TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'parameters');
should(data[0]).have.property('name', 'heat aging');
should(data[0]).have.property('parameters').have.lengthOf(1);
should(data[0].parameters[0]).have.property('name', 'time');
should(data[0].parameters[0]).have.property('range');
should(data[0].parameters[0].range).have.property('min', 1);
done();
});
});
});
it('supports values ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]},
res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}
});
});
it('supports min max ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]},
res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {min: 1, max: 11}}]}
});
});
it('supports empty ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {}}]},
res: {_id: '200000000000000000000001', name: 'heat treatment', parameters: [{name: 'time', range: {}}]}
});
});
it('adds a new template for an unknown name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
}).end(err => {
if (err) return done(err);
TemplateTreatmentModel.find({name: 'heat aging'}).lean().exec((err, data:any) => {
if (err) return done(err);
console.log(data);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v');
should(data[0]).have.property('name', 'heat aging');
should(data[0].parameters[0]).have.property('name', 'time');
should(data[0].parameters[0]).have.property('range');
should(data[0].parameters[0].range).have.property('min', 1);
done();
});
});
});
it('rejects an incomplete template for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'time'}]},
res: {status: 'Invalid body format'}
});
});
it('rejects already existing names', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat treatment 2', parameters: [{name: 'time', range: {min: 1}}]},
res: {status: 'Template name already taken'}
});
});
it('rejects wrong properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'time'}], xx: 33},
res: {status: 'Invalid body format'}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20treatment',
httpStatus: 401,
req: {}
});
});
});
describe('DELETE /template/treatment/{name}', () => {
it('deletes the template', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
TemplateTreatmentModel.find({name: 'heat treatment'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(0);
done();
});
});
});
it('rejects deleting a template still in use');
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/treatment/heat%20treatment',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/treatment/heat%20treatment',
auth: {basic: 'janedoe'},
httpStatus: 403
})
});
it('returns 404 for an unknown name', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/treatment/xxx',
auth: {basic: 'admin'},
httpStatus: 404
})
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/treatment/heat%20treatment',
httpStatus: 401
})
});
});
});
describe('/template/measurement', () => {
describe('GET /template/measurements', () => {
it('returns all measurement templates', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurements',
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.measurement_templates.length);
should(res.body).matchEach(measurement => {
should(measurement).have.only.keys('_id', 'name', 'parameters');
should(measurement).have.property('_id').be.type('string');
should(measurement).have.property('name').be.type('string');
should(measurement.parameters).matchEach(number => {
should(number).have.only.keys('name', 'range');
should(number).have.property('name').be.type('string');
should(number).have.property('range').be.type('object');
});
});
done();
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurements',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurements',
httpStatus: 401
});
});
});
describe('GET /template/measurement/{name}', () => {
it('returns the right measurement template', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/spectrum',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/spectrum',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects an unknown name', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/xxx',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/spectrum',
httpStatus: 401
});
});
});
describe('PUT /template/measurement/{name}', () => {
it('returns the right measurement template', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]}
});
});
it('keeps unchanged properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'spectrum', parameters: [{name: 'dpt', range: {}}]},
res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {}}]}
});
});
it('changes the given properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({_id: '300000000000000000000001', name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]});
TemplateMeasurementModel.find({name: 'IR spectrum'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'parameters');
should(data[0]).have.property('name', 'IR spectrum');
should(data[0]).have.property('parameters').have.lengthOf(1);
should(data[0].parameters[0]).have.property('name', 'data point table');
should(data[0].parameters[0]).have.property('range');
should(data[0].parameters[0].range).have.property('min', 0);
should(data[0].parameters[0].range).have.property('max', 1000);
done();
});
});
});
it('supports values ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]},
res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]}
});
});
it('supports min max ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]},
res: {_id: '300000000000000000000001', name: 'spectrum', parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]}
});
});
it('supports empty ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/kf',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'weight %', range: {}}]},
res: {_id: '300000000000000000000002', name: 'kf', parameters: [{name: 'weight %', range: {}}]}
});
});
it('adds a new template for an unknown name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/vz',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
}).end(err => {
if (err) return done(err);
TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'parameters', '__v');
should(data[0]).have.property('name', 'vz');
should(data[0]).have.property('parameters').have.lengthOf(1);
should(data[0].parameters[0]).have.property('name', 'vz');
should(data[0].parameters[0]).have.property('range');
should(data[0].parameters[0].range).have.property('min', 1);
done();
});
});
});
it('rejects an incomplete template for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/vz',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'vz'}]},
res: {status: 'Invalid body format'}
});
});
it('rejects already existing names', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'kf', parameters: [{name: 'dpt', range: {min: 1}}]},
res: {status: 'Template name already taken'}
});
});
it('rejects wrong properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'dpt'}], xx: 33},
res: {status: 'Invalid body format'}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum',
httpStatus: 401,
req: {}
});
});
});
describe('DELETE /template/measurement/{name}', () => {
it('deletes the template', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/measurement/spectrum',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
TemplateMeasurementModel.find({name: 'spectrum'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(0);
done();
});
});
});
it('rejects deleting a template still in use');
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/measurement/spectrum',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/measurement/spectrum',
auth: {basic: 'janedoe'},
httpStatus: 403
})
});
it('returns 404 for an unknown name', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/measurement/xxx',
auth: {basic: 'admin'},
httpStatus: 404
})
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/template/measurement/spectrum',
httpStatus: 401
})
});
});
});
});

91
src/routes/template.ts Normal file
View File

@ -0,0 +1,91 @@
import express from 'express';
import TemplateValidate from './validate/template';
import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template';
const router = express.Router();
router.get('/template/:collection(measurements|treatments)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
(req.params.collection === 'treatments' ? TemplateTreatmentModel : TemplateMeasurementModel)
.find({}).lean().exec((err, data) => {
if (err) next (err);
res.json(data.map(e => TemplateValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors
});
});
router.get('/template/:collection(measurement|treatment)/:name', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
(req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel)
.findOne({name: req.params.name}).lean().exec((err, data) => {
if (err) next (err);
if (data) {
res.json(TemplateValidate.output(data));
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
router.put('/template/:collection(measurement|treatment)/:name', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
const collectionModel = req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel;
collectionModel.findOne({name: req.params.name}).lean().exec((err, data) => {
if (err) next (err);
const templateState = data? 'change': 'new';
const {error, value: template} = TemplateValidate.input(req.body, templateState);
if(error !== undefined) {
res.status(400).json({status: 'Invalid body format'});
return;
}
if (template.hasOwnProperty('name') && template.name !== req.params.name) {
collectionModel.find({name: template.name}).lean().exec((err, data) => {
if (err) next (err);
if (data.length > 0) {
res.status(400).json({status: 'Template name already taken'});
return;
}
else {
f();
}
});
}
else {
f();
}
function f() { // to resolve async
collectionModel.findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => {
if (err) next(err);
res.json(TemplateValidate.output(data));
});
}
});
});
router.delete('/template/:collection(measurement|treatment)/:name', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
(req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel)
.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => {
if (err) next(err);
if (data) {
res.json({status: 'OK'})
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
module.exports = router;

View File

@ -0,0 +1,59 @@
import joi from '@hapi/joi';
import IdValidate from './id';
export default class TemplateValidate {
private static template = {
name: joi.string()
.max(128),
parameters: joi.array()
.min(1)
.items(
joi.object({
name: joi.string()
.max(128)
.required(),
range: joi.object({
values: joi.array()
.min(1),
min: joi.number(),
max: joi.number()
})
.oxor('values', 'min')
.oxor('values', 'max')
.required()
})
)
};
static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated)
if (param === 'new') {
return joi.object({
name: this.template.name.required(),
parameters: this.template.parameters.required()
}).validate(data);
}
else if (param === 'change') {
return joi.object({
name: this.template.name,
parameters: this.template.parameters
}).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data) { // validate output from database for needed properties, strip everything else
data._id = data._id.toString();
const {value, error} = joi.object({
_id: IdValidate.get(),
name: this.template.name,
parameters: this.template.parameters
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
}

View File

@ -84,6 +84,72 @@
], ],
"__v": 0 "__v": 0
} }
],
"treatment_templates": [
{
"_id": {"$oid":"200000000000000000000001"},
"name": "heat treatment",
"parameters": [
{
"name": "material",
"range": {
"values": [
"copper",
"hot air"
]
}
},
{
"name": "weeks",
"range": {
"min": 1,
"max": 10
}
}
]
},
{
"_id": {"$oid":"200000000000000000000002"},
"name": "heat treatment 2",
"parameters": [
{
"name": "material",
"range": {}
}
]
}
],
"measurement_templates": [
{
"_id": {"$oid":"300000000000000000000001"},
"name": "spectrum",
"parameters": [
{
"name": "dpt",
"range": {}
}
]
},
{
"_id": {"$oid":"300000000000000000000002"},
"name": "kf",
"parameters": [
{
"name": "weight %",
"range": {
"min": 0,
"max": 1.5
}
},
{
"name": "standard deviation",
"range": {
"min": 0,
"max": 0.5
}
}
]
}
] ]
} }
} }

View File

@ -144,6 +144,13 @@ body:after {
border-color: var(--red); border-color: var(--red);
} }
/*download button*/
.swagger-ui .download-contents {
border-radius: 0;
height: 28px;
width: 80px;
}
/*model*/ /*model*/
.swagger-ui .model-box { .swagger-ui .model-box {
border-radius: 0; border-radius: 0;
@ -153,6 +160,22 @@ body:after {
.swagger-ui .btn.execute { .swagger-ui .btn.execute {
background-color: var(--dark-blue); background-color: var(--dark-blue);
border-color: var(--dark-blue); border-color: var(--dark-blue);
height: 30px;
line-height: 0.7;
}
.swagger-ui .btn-group .btn:last-child {
border-radius: 0;
height: 30px;
border-color: var(--dark-blue);
}
.swagger-ui .btn-group .btn:first-child {
border-radius: 0;
}
.swagger-ui .btn-group {
padding: 0 20px;
} }
/*parameter input*/ /*parameter input*/
@ -160,6 +183,14 @@ body:after {
border-radius: 0; border-radius: 0;
} }
/*required label*/
.swagger-ui .parameter__name.required > span {
color: var(--red) !important;
}
.swagger-ui .parameter__name.required::after {
color: var(--red);
}
/*Remove colored parameters bar*/ /*Remove colored parameters bar*/
.swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after {
background: none; background: none;