Archived
2

implemented first /sample methods

This commit is contained in:
VLE2FE 2020-05-06 14:39:04 +02:00
parent af071a9445
commit 20f57acd2a
24 changed files with 844 additions and 89 deletions

View File

@ -1,6 +1,6 @@
/samples:
get:
summary: TODO all samples in overview
summary: all samples in overview
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /sample
@ -10,7 +10,9 @@
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Samples'
type: array
items:
$ref: 'api.yaml#/components/schemas/SampleRefs'
401:
$ref: 'api.yaml#/components/responses/401'
500:
@ -39,7 +41,7 @@
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: TODO add/change sample
summary: TODO change sample
description: 'Auth: basic, levels: write, maintain, dev, admin'
tags:
- /sample
@ -88,10 +90,41 @@
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/new:
post:
summary: add sample
description: 'Auth: basic, levels: write, maintain, dev, admin'
tags:
- /sample
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Sample'
responses:
200:
description: samples details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/SampleRefs'
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'
/sample/notes/fields:
get:
summary: TODO list all existing field names for custom notes fields
description: 'Auth: all, levels: write, maintain, dev, admin'
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /sample
responses:

View File

@ -14,16 +14,17 @@ Color:
example: black
SampleProperties:
properties:
sample_number:
number:
type: string
example: Rng172
type:
type: string
example: granulate
batch:
type: string
validated:
type: boolean
example: 1560237365
Samples:
SampleRefs:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
- $ref: 'api.yaml#/components/schemas/Color'
@ -41,17 +42,23 @@ Sample:
- $ref: 'api.yaml#/components/schemas/Color'
- $ref: 'api.yaml#/components/schemas/SampleProperties'
properties:
material:
$ref: 'api.yaml#/components/schemas/Material'
material_id:
allOf:
- $ref: 'api.yaml#/components/schemas/Id'
notes:
type: object
properties:
comments:
comment:
type: string
sample_references:
type: array
items:
$ref: 'api.yaml#/components/schemas/Id'
properties:
id:
$ref: 'api.yaml#/components/schemas/Id'
relation:
type: string
example: part to this sample
SampleDetail:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
@ -63,7 +70,7 @@ SampleDetail:
notes:
type: object
properties:
comments:
comment:
type: string
sample_references:
type: array

View File

@ -37,7 +37,7 @@ export default class db {
}
// connect to db
mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true}, err => {
mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, connectTimeoutMS: 10000}, err => {
if (err) done(err);
});
mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
@ -92,7 +92,9 @@ export default class db {
Object.keys(json.collections).forEach(collectionName => { // create each collection
for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds
Object.keys(json.collections[collectionName][i]).forEach(key => {
json.collections[collectionName][i][key] = json.collections[collectionName][i][key].hasOwnProperty('$oid') ? mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid) : json.collections[collectionName][i][key];
if (json.collections[collectionName][i][key] !== null && json.collections[collectionName][i][key].hasOwnProperty('$oid')) {
json.collections[collectionName][i][key] = mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid);
}
})
}
this.state.db.createCollection(collectionName, (err, collection) => {

View File

@ -9,7 +9,7 @@ import UserModel from '../models/user';
module.exports = async (req, res, next) => {
let givenMethod = ''; // authorization method given by client, basic taken preferred
let user = {name: '', level: ''}; // user object
let user = {name: '', level: '', id: ''}; // user object
// test authentications
const userBasic = await basic(req, next);
@ -45,7 +45,8 @@ module.exports = async (req, res, next) => {
req.authDetails = {
method: givenMethod,
username: user.name,
level: user.level
level: user.level,
id: user.id
};
next();
@ -57,12 +58,12 @@ function basic (req, next): any { // checks basic auth and returns changed user
const auth = basicAuth(req);
if (auth !== undefined) { // basic auth available
UserModel.find({name: auth.name}).lean().exec( (err, data: any) => { // find user
if (err) next(err);
if (err) return next(err);
if (data.length === 1) { // one user found
bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password
if (err) next(err);
if (err) return next(err);
if (res === true) {
resolve({level: data[0].level, name: data[0].name});
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()});
}
else {
resolve(null);
@ -84,9 +85,9 @@ function key (req, next): any { // checks API key and returns changed user obje
return new Promise(resolve => {
if (req.query.key !== undefined) {
UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user
if (err) next(err);
if (err) return next(err);
if (data.length === 1) { // one user found
resolve({level: data[0].level, name: data[0].name});
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString()});
}
else {
resolve(null);

View File

@ -14,6 +14,7 @@ export default class TestHelper {
401: {status: 'Unauthorized'},
403: {status: 'Forbidden'},
404: {status: 'Not found'},
500: {status: 'Internal server error'}
}
static before (done) {

View File

@ -45,9 +45,10 @@ app.use(require('./helpers/authorize')); // handle authentication
// require routes
app.use('/', require('./routes/root'));
app.use('/', require('./routes/user'));
app.use('/', require('./routes/sample'));
app.use('/', require('./routes/material'));
app.use('/', require('./routes/template'));
app.use('/', require('./routes/user'));
// static files
app.use('/static', express.static('static'));

12
src/models/note.ts Normal file
View File

@ -0,0 +1,12 @@
import mongoose from 'mongoose';
const NoteSchema = new mongoose.Schema({
comment: String,
sample_references: [{
id: mongoose.Schema.Types.ObjectId,
relation: String
}],
custom_fields: mongoose.Schema.Types.Mixed
});
export default mongoose.model('note', NoteSchema);

8
src/models/note_field.ts Normal file
View File

@ -0,0 +1,8 @@
import mongoose from 'mongoose';
const NoteFieldSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
qty: Number
});
export default mongoose.model('note_field', NoteFieldSchema);

18
src/models/sample.ts Normal file
View File

@ -0,0 +1,18 @@
import mongoose from 'mongoose';
import MaterialModel from './material';
import NoteModel from './note';
import UserModel from './user';
const SampleSchema = new mongoose.Schema({
number: {type: String, index: {unique: true}},
type: String,
color: String,
batch: String,
validated: Boolean,
material_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialModel},
note_id: {type: mongoose.Schema.Types.ObjectId, ref: NoteModel},
user_id: {type: mongoose.Schema.Types.ObjectId, ref: UserModel}
});
export default mongoose.model('sample', SampleSchema);

View File

@ -19,7 +19,7 @@ describe('/material', () => {
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.users.length);
should(res.body).have.lengthOf(json.collections.materials.length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
should(material).have.property('_id').be.type('string');
@ -47,7 +47,7 @@ describe('/material', () => {
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.users.length);
should(res.body).have.lengthOf(json.collections.materials.length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
should(material).have.property('_id').be.type('string');
@ -82,7 +82,7 @@ describe('/material', () => {
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
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}, {color: 'natural', number: 5514263422}]}
});
});
it('returns the right material for an API key', done => {
@ -127,7 +127,7 @@ describe('/material', () => {
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {},
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}, {color: 'natural', number: 5514263422}]}
});
});
it('keeps unchanged properties', done => {
@ -296,7 +296,6 @@ describe('/material', () => {
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]}
}).end((err, res) => {
if (err) return done (err);
console.log(res.body);
should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('name', 'Crastin CE 2510');
@ -324,7 +323,7 @@ describe('/material', () => {
if (err) return done (err);
MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => {
if (err) return done (err);
console.log(data[0]);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', '__v');
should(data[0]).have.property('_id');
should(data[0]).have.property('name', 'Crastin CE 2510');

View File

@ -11,7 +11,7 @@ router.get('/materials', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
MaterialModel.find({}).lean().exec((err, data) => {
if(err) next(err);
if (err) return next(err);
res.json(data.map(e => MaterialValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors
});
});
@ -20,8 +20,7 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
MaterialModel.findById(req.params.id).lean().exec((err, data) => {
if(err) next(err);
console.log(data);
if (err) return next(err);
if (data) {
res.json(MaterialValidate.output(data));
}
@ -35,14 +34,14 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: material} = MaterialValidate.input(req.body, 'change');
if(error !== undefined) {
if (error) {
res.status(400).json({status: 'Invalid body format'});
return;
}
if (material.hasOwnProperty('name')) {
MaterialModel.find({name: material.name}).lean().exec((err, data) => {
if(err) next(err);
if (err) return next(err);
if (data.length > 0 && data[0]._id != req.params.id) {
res.status(400).json({status: 'Material name already taken'});
return;
@ -58,7 +57,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
function f() { // to resolve async
MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).lean().exec((err, data) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json(MaterialValidate.output(data));
}
@ -73,7 +72,7 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json({status: 'OK'})
}
@ -88,20 +87,20 @@ router.post('/material/new', (req, res, next) => {
// validate input
const {error, value: material} = MaterialValidate.input(req.body, 'new');
if(error !== undefined) {
if (error) {
res.status(400).json({status: 'Invalid body format'});
return;
}
MaterialModel.find({name: material.name}).lean().exec((err, data) => {
if(err) next(err);
if (err) return next(err);
if (data.length > 0) {
res.status(400).json({status: 'Material name already taken'});
return;
}
new MaterialModel(material).save((err, data) => {
if(err) next(err);
if (err) return next(err);
res.json(MaterialValidate.output(data.toObject()));
});
});

336
src/routes/sample.spec.ts Normal file
View File

@ -0,0 +1,336 @@
import should from 'should/as-function';
import SampleModel from '../models/sample';
import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field';
import TestHelper from "../helpers/test";
describe('/sample', () => {
let server;
before(done => TestHelper.before(done));
beforeEach(done => server = TestHelper.beforeEach(server, done));
afterEach(done => TestHelper.afterEach(server, done));
describe('GET /samples', () => {
it('returns all samples', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples',
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.length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(material).have.property('_id').be.type('string');
should(material).have.property('number').be.type('string');
should(material).have.property('type').be.type('string');
should(material).have.property('color').be.type('string');
should(material).have.property('batch').be.type('string');
should(material).have.property('material_id').be.type('string');
should(material).have.property('note_id');
should(material).have.property('user_id').be.type('string');
});
done();
});
});
it('works with an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples',
auth: {key: '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.length);
should(res.body).matchEach(material => {
should(material).have.only.keys('_id', 'number', 'type', 'color', 'batch', 'material_id', 'note_id', 'user_id');
should(material).have.property('_id').be.type('string');
should(material).have.property('number').be.type('string');
should(material).have.property('type').be.type('string');
should(material).have.property('color').be.type('string');
should(material).have.property('batch').be.type('string');
should(material).have.property('material_id').be.type('string');
should(material).have.property('note_id');
should(material).have.property('user_id').be.type('string');
});
done();
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples',
httpStatus: 401
});
});
});
describe('POST /sample/new', () => {
it('returns the right sample', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('number', 'Rng172');
should(res.body).have.property('color', 'black');
should(res.body).have.property('type', 'granulate');
should(res.body).have.property('batch', '1560237365');
should(res.body).have.property('material_id', '100000000000000000000001');
should(res.body).have.property('note_id').be.type('string');
should(res.body).have.property('user_id', '000000000000000000000002');
done();
});
});
it('stores the sample', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end(err => {
if (err) return done (err);
SampleModel.find({number: 'Rng172'}).lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'material_id', 'note_id', 'user_id', '__v');
should(data[0]).have.property('_id');
should(data[0]).have.property('number', 'Rng172');
should(data[0]).have.property('color', 'black');
should(data[0]).have.property('type', 'granulate');
should(data[0]).have.property('batch', '1560237365');
should(data[0].material_id.toString()).be.eql('100000000000000000000001');
should(data[0].user_id.toString()).be.eql('000000000000000000000002');
should(data[0]).have.property('note_id');
NoteModel.findById(data[0].note_id).lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.property('_id');
should(data).have.property('comment', 'Testcomment');
should(data).have.property('sample_references');
should(data.sample_references).have.lengthOf(1);
should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003');
should(data.sample_references[0]).have.property('relation', 'part to this sample');
done();
});
})
});
});
it('stores the custom fields', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'a', field2: 'b', 'not allowed for new applications': true}}}
}).end((err, res) => {
if (err) return done (err);
NoteModel.findById(res.body.note_id).lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('_id');
should(data).have.property('comment', 'Testcomment');
should(data).have.property('sample_references').have.lengthOf(0);
should(data).have.property('custom_fields');
should(data.custom_fields).have.property('field1', 'a');
should(data.custom_fields).have.property('field2', 'b');
should(data.custom_fields).have.property('not allowed for new applications', true);
NoteFieldModel.find({name: 'field1'}).lean().exec((err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.property('qty', 1);
NoteFieldModel.find({name: 'field2'}).lean().exec((err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.property('qty', 1);
NoteFieldModel.find({name: 'not allowed for new applications'}).lean().exec((err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.property('qty', 3);
done();
});
});
});
});
});
});
it('rejects a color not defined for the material', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Color not available for material'}
});
});
it('rejects an unknown material id', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '000000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Material not available'}
});
});
it('rejects a sample number in use', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '1', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Sample number already taken'}
});
});
it('rejects an invalid sample reference', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Sample reference not available'}
});
});
it('rejects a missing color', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'}
});
});
it('rejects a missing sample number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'}
});
});
it('rejects a missing type', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'}
});
});
it('rejects a missing batch', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'}
});
});
it('rejects a missing material id', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'}
});
});
it('rejects an invalid material id', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {key: 'janedoe'},
httpStatus: 401,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
});
});
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'user'},
httpStatus: 403,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
httpStatus: 401,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
});
});
});
describe('GET /sample/notes/fields', () => {
it('returns all fields', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/notes/fields',
auth: {basic: 'user'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.note_fields.length);
should(res.body).matchEach(material => {
should(material).have.only.keys('name', 'qty');
should(material).have.property('qty').be.type('number');
});
done();
});
});
it('works with an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/notes/fields',
auth: {key: 'user'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.note_fields.length);
should(res.body).matchEach(material => {
should(material).have.only.keys('name', 'qty');
should(material).have.property('qty').be.type('number');
});
done();
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/notes/fields',
httpStatus: 401
});
});
});
});

109
src/routes/sample.ts Normal file
View File

@ -0,0 +1,109 @@
import express from 'express';
import SampleValidate from './validate/sample';
import NoteFieldValidate from './validate/note_field';
import SampleModel from '../models/sample'
import MaterialModel from '../models/material';
import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field';
const router = express.Router();
router.get('/samples', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
SampleModel.find({}).lean().exec((err, data) => {
if (err) return next(err);
res.json(data.map(e => SampleValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors
})
});
router.post('/sample/new', (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: sample} = SampleValidate.input(req.body, 'new');
if (error) {
return res.status(400).json({status: 'Invalid body format'});
}
MaterialModel.findById(sample.material_id).lean().exec((err, data: any) => { // validate material_id
if (err) return next(err);
if (!data) { // could not find material_id
return res.status(400).json({status: 'Material not available'});
}
if (!data.numbers.find(e => e.color === sample.color)) { // color for material not specified
return res.status(400).json({status: 'Color not available for material'});
}
SampleModel.findOne({number: sample.number}).lean().exec((err, data) => { // validate sample number
if (err) return next(err);
if (data) { // found entry with sample number
return res.status(400).json({status: 'Sample number already taken'});
}
if (sample.notes.sample_references.length > 0) { // validate sample_references
let referencesCount = sample.notes.sample_references.length;
sample.notes.sample_references.forEach(reference => {
SampleModel.findById(reference.id).lean().exec((err, data) => {
if (err) return next(err);
if (!data) {
return res.status(400).json({status: 'Sample reference not available'});
}
referencesCount --;
if (referencesCount <= 0) {
f();
}
});
});
}
else {
f();
}
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
customFieldsAdd(Object.keys(sample.notes.custom_fields));
}
function f() { // to resolve async
new NoteModel(sample.notes).save((err, data) => {
if (err) return next(err);
delete sample.notes;
sample.note_id = data._id;
sample.user_id = req.authDetails.id;
new SampleModel(sample).save((err, data) => {
if (err) return next(err);
res.json(SampleValidate.output(data.toObject()));
});
});
}
});
})
});
router.get('/sample/notes/fields', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
NoteFieldModel.find({}).lean().exec((err, data) => {
if (err) return next(err);
res.json(data.map(e => NoteFieldValidate.output(e)).filter(e => e !== null)); // validate all and filter null values from validation errors
})
});
module.exports = router;
function customFieldsAdd (fields) {
fields.forEach(field => {
NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: 1}}).lean().exec((err, data) => { // check if field exists
if (err) return console.error(err);
if (!data) { // new field
new NoteFieldModel({name: field, qty: 1}).save(err => {
if (err) return console.error(err);
})
}
});
});
}

View File

@ -172,7 +172,6 @@ describe('/template', () => {
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');

View File

@ -41,7 +41,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next
if (err) next (err);
const templateState = data? 'change': 'new';
const {error, value: template} = TemplateValidate.input(req.body, templateState);
if(error !== undefined) {
if (error) {
res.status(400).json({status: 'Invalid body format'});
return;
}
@ -64,7 +64,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next
function f() { // to resolve async
collectionModel.findOneAndUpdate({name: req.params.name}, template, {new: true, upsert: true}).lean().exec((err, data) => {
if (err) next(err);
if (err) return next(err);
res.json(TemplateValidate.output(data));
});
}
@ -76,7 +76,7 @@ router.delete('/template/:collection(measurement|treatment)/:name', (req, res, n
(req.params.collection === 'treatment' ? TemplateTreatmentModel : TemplateMeasurementModel)
.findOneAndDelete({name: req.params.name}).lean().exec((err, data) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json({status: 'OK'})
}
@ -87,5 +87,4 @@ router.delete('/template/:collection(measurement|treatment)/:name', (req, res, n
});
module.exports = router;

View File

@ -27,7 +27,7 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
}
UserModel.findOne({name: username}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data)); // validate all and filter null values from validation errors
}
@ -46,7 +46,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
username = req.params.username;
}
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
if(error !== undefined) {
if (error) {
res.status(400).json({status: 'Invalid body format'});
return;
}
@ -58,14 +58,14 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
// check that user does not already exist if new name was specified
if (user.hasOwnProperty('name') && user.name !== username) {
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
}
UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data));
}
@ -77,7 +77,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
}
else {
UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data)); // validate all and filter null values from validation errors
}
@ -98,7 +98,7 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { //
}
UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
if (data) {
res.json({status: 'OK'})
}
@ -109,11 +109,10 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { //
});
router.get('/user/key', (req, res, next) => {
console.log('hmm');
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
res.json({key: data.key});
});
});
@ -123,14 +122,14 @@ router.post('/user/new', (req, res, next) => {
// validate input
const {error, value: user} = UserValidate.input(req.body, 'new');
if(error !== undefined) {
if (error) {
res.status(400).json({status: 'Invalid body format'});
return;
}
// check that user does not already exist
UserModel.find({name: user.name}).lean().exec( (err, data:any) => {
if (err) next(err);
if (err) return next(err);
if (data.length > 0 || UserValidate.isSpecialName(user.name)) {
res.status(400).json({status: 'Username already taken'});
return;
@ -140,7 +139,7 @@ router.post('/user/new', (req, res, next) => {
bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing
user.pass = hash;
new UserModel(user).save((err, data) => { // store user
if (err) next(err);
if (err) return next(err);
res.json(UserValidate.output(data.toObject()));
});
});
@ -150,15 +149,15 @@ router.post('/user/new', (req, res, next) => {
router.post('/user/passreset', (req, res, next) => {
// check if user/email combo exists
UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => {
if (err) next(err);
if (err) return next(err);
if (data.length === 1) { // it exists
const newPass = Math.random().toString(36).substring(2);
bcrypt.hash(newPass, 10, (err, hash) => { // password hashing
if (err) next(err);
if (err) return next(err);
UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password
if (err) next(err);
if (err) return next(err);
mail(data[0].email, 'Your new password for the DFOP database', '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>The DFOP team', err => {
if (err) next(err);
if (err) return next(err);
res.json({status: 'OK'});
});
});

View File

@ -11,7 +11,16 @@ export default class IdValidate {
return this.id.validate(id).error === undefined;
}
static parameter() { // :id url parameter
static parameter () { // :id url parameter
return ':id([0-9a-f]{24})';
}
static stringify (data) {
Object.keys(data).forEach(key => {
if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') {
data[key] = data[key].toString();
}
});
return data;
}
}

View File

@ -66,7 +66,7 @@ export default class MaterialValidate { // validate input for material
}
static output (data) { // validate output from database for needed properties, strip everything else
data._id = data._id.toString();
data = IdValidate.stringify(data);
const {value, error} = joi.object({
_id: IdValidate.get(),
name: this.material.name,

View File

@ -0,0 +1,18 @@
import joi from '@hapi/joi';
export default class NoteFieldValidate {
private static note_field = {
name: joi.string()
.max(128),
qty: joi.number()
};
static output (data) {
const {value, error} = joi.object({
name: this.note_field.name,
qty: this.note_field.qty
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
}

View File

@ -0,0 +1,77 @@
import joi from '@hapi/joi';
import IdValidate from './id';
export default class SampleValidate {
private static sample = {
number: joi.string()
.max(128),
color: joi.string()
.max(128),
type: joi.string()
.max(128),
batch: joi.string()
.max(128)
.allow(''),
notes: joi.object({
comment: joi.string()
.max(512),
sample_references: joi.array()
.items(joi.object({
id: IdValidate.get(),
relation: joi.string()
.max(128)
})),
custom_fields: joi.object()
.pattern(/.*/, joi.alternatives()
.try(
joi.string().max(128),
joi.number(),
joi.boolean(),
joi.date()
)
)
})
};
static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated)
if (param === 'new') {
return joi.object({
number: this.sample.number.required(),
color: this.sample.color.required(),
type: this.sample.type.required(),
batch: this.sample.batch.required(),
material_id: IdValidate.get().required(),
notes: this.sample.notes.required()
}).validate(data);
}
else if (param === 'change') {
return{error: 'Not implemented!', value: {}};
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data) {
data = IdValidate.stringify(data);
const {value, error} = joi.object({
_id: IdValidate.get(),
number: this.sample.number,
color: this.sample.color,
type: this.sample.type,
batch: this.sample.batch,
material_id: IdValidate.get(),
note_id: IdValidate.get().allow(null),
user_id: IdValidate.get()
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
}

View File

@ -48,7 +48,7 @@ export default class TemplateValidate {
}
static output (data) { // validate output from database for needed properties, strip everything else
data._id = data._id.toString();
data = IdValidate.stringify(data);
const {value, error} = joi.object({
_id: IdValidate.get(),
name: this.template.name,

View File

@ -69,7 +69,7 @@ export default class UserValidate { // validate input for user
}
static output (data) { // validate output from database for needed properties, strip everything else
data._id = data._id.toString();
data = IdValidate.stringify(data);
const {value, error} = joi.object({
_id: IdValidate.get(),
name: this.user.name,

View File

@ -1,38 +1,93 @@
{
"collections": {
"users": [
"samples": [
{
"_id": {"$oid":"000000000000000000000001"},
"email": "user@bosch.com",
"name": "user",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "read",
"location": "Rng",
"device_name": "Alpha I",
"key": "000000000000000000001001",
"_id": {"$oid":"400000000000000000000001"},
"number": "1",
"type": "granulate",
"color": "black",
"batch": "",
"validated": true,
"material_id": {"$oid":"100000000000000000000004"},
"note_id": null,
"user_id": {"$oid":"000000000000000000000002"},
"__v": 0
},
{
"_id": {"$oid":"000000000000000000000002"},
"email": "jane.doe@bosch.com",
"name": "janedoe",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "write",
"location": "Rng",
"device_name": "Alpha I",
"key": "000000000000000000001002",
"_id": {"$oid":"400000000000000000000002"},
"number": "21",
"type": "granulate",
"color": "natural",
"batch": "1560237365",
"validated": true,
"material_id": {"$oid":"100000000000000000000001"},
"note_id": {"$oid":"500000000000000000000001"},
"user_id": {"$oid":"000000000000000000000002"},
"__v": 0
},
{
"_id": {"$oid":"000000000000000000000003"},
"email": "a.d.m.i.n@bosch.com",
"name": "admin",
"pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K",
"level": "admin",
"location": "Rng",
"device_name": "",
"key": "000000000000000000001003",
"__v": "0"
"_id": {"$oid":"400000000000000000000003"},
"number": "33",
"type": "part",
"color": "black",
"batch": "1704-005",
"validated": false,
"material_id": {"$oid":"100000000000000000000005"},
"note_id": {"$oid":"500000000000000000000002"},
"user_id": {"$oid":"000000000000000000000003"},
"__v": 0
},
{
"_id": {"$oid":"400000000000000000000004"},
"number": "32",
"type": "granulate",
"color": "black",
"batch": "1653000308",
"validated": false,
"material_id": {"$oid":"100000000000000000000005"},
"note_id": {"$oid":"500000000000000000000003"},
"user_id": {"$oid":"000000000000000000000003"},
"__v": 0
}
],
"notes": [
{
"_id": {"$oid":"500000000000000000000001"},
"comment": "Stoff gesperrt",
"sample_references": [],
"__v": 0
},
{
"_id": {"$oid":"500000000000000000000002"},
"comment": "",
"sample_references": [{
"id": "400000000000000000000004",
"relation": "granulate to sample"
}],
"custom_fields": {
"not allowed for new applications": true
},
"__v": 0
},
{
"_id": {"$oid":"500000000000000000000003"},
"comment": "",
"sample_references": [{
"id": "400000000000000000000003",
"relation": "part to sample"
}],
"custom_fields": {
"not allowed for new applications": true
},
"__v": 0
}
],
"note_fields": [
{
"_id": {"$oid":"600000000000000000000001"},
"name": "not allowed for new applications",
"qty": 2,
"__v": 0
}
],
"materials": [
@ -48,6 +103,10 @@
{
"color": "black",
"number": 5514263423
},
{
"color": "natural",
"number": 5514263422
}
],
"__v": 0
@ -83,6 +142,38 @@
"numbers": [
],
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000004"},
"name": "Schulamid 66 GF 25 H",
"supplier": "Schulmann",
"group": "PA66",
"mineral": 0,
"glass_fiber": 25,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": 5513933405
}
],
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000005"},
"name": "Amodel A 1133 HS",
"supplier": "Solvay",
"group": "PPA",
"mineral": 0,
"glass_fiber": 33,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": 5514262406
}
],
"__v": 0
}
],
"treatment_templates": [
@ -150,6 +241,41 @@
}
]
}
],
"users": [
{
"_id": {"$oid":"000000000000000000000001"},
"email": "user@bosch.com",
"name": "user",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "read",
"location": "Rng",
"device_name": "Alpha I",
"key": "000000000000000000001001",
"__v": 0
},
{
"_id": {"$oid":"000000000000000000000002"},
"email": "jane.doe@bosch.com",
"name": "janedoe",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "write",
"location": "Rng",
"device_name": "Alpha I",
"key": "000000000000000000001002",
"__v": 0
},
{
"_id": {"$oid":"000000000000000000000003"},
"email": "a.d.m.i.n@bosch.com",
"name": "admin",
"pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K",
"level": "admin",
"location": "Rng",
"device_name": "",
"key": "000000000000000000001003",
"__v": "0"
}
]
}
}

View File

@ -6,6 +6,8 @@
"sourceMap": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"diagnostics": true,
"typeRoots": [
"src/customTypings",
"node_modules/@types"