Merge pull request #27 in ~VLE2FE/definma-api from develop to master
* commit 'ed8b549752cbfbeba497adda8427f34c8b8497a4': flattened samples result csv only for dev/admin, mail change notice
This commit is contained in:
commit
917b9044c7
@ -40,12 +40,6 @@
|
|||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: color-asc
|
example: color-asc
|
||||||
- name: csv
|
|
||||||
description: output as csv
|
|
||||||
in: query
|
|
||||||
schema:
|
|
||||||
type: boolean
|
|
||||||
example: false
|
|
||||||
- name: fields[]
|
- name: fields[]
|
||||||
description: "the fields to include in the output as array, defaults to ['_id', 'number', 'type',
|
description: "the fields to include in the output as array, defaults to ['_id', 'number', 'type',
|
||||||
'batch', 'material_id', 'color', 'condition', 'note_id', 'user_id', 'added']"
|
'batch', 'material_id', 'color', 'condition', 'note_id', 'user_id', 'added']"
|
||||||
@ -57,7 +51,8 @@
|
|||||||
example: ['number', 'batch']
|
example: ['number', 'batch']
|
||||||
- name: filters[]
|
- name: filters[]
|
||||||
description: "the filters to apply as an array of URIComponent encoded objects in the form {mode:
|
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({}))"
|
'eq/ne/lt/lte/gt/gte/in/nin/stringin', field: 'material.m', values: ['15']} using
|
||||||
|
encodeURIComponent(JSON.stringify({}))"
|
||||||
in: query
|
in: query
|
||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
@ -66,6 +61,13 @@
|
|||||||
example: '["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D",
|
example: '["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D",
|
||||||
"%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22
|
"%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22
|
||||||
%5D%7D"]'
|
%5D%7D"]'
|
||||||
|
- name: output
|
||||||
|
description: 'output format, available values are csv, json, flatten (converts material: {number: x} to
|
||||||
|
material.number: x), defaults to json'
|
||||||
|
in: query
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
example: csv
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: samples overview (output depends on the fields specified)<br>
|
description: samples overview (output depends on the fields specified)<br>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {parseAsync} from 'json2csv';
|
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) {
|
||||||
parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true})
|
parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true})
|
||||||
@ -6,34 +7,3 @@ export default function csv(input: any[], f: (err, data) => void) {
|
|||||||
.catch(err => f(err, null));
|
.catch(err => f(err, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
function flatten (data) { // flatten object: {a: {b: true}} -> {a.b: true}
|
|
||||||
const result = {};
|
|
||||||
function recurse (cur, prop) {
|
|
||||||
if (Object(cur) !== cur || Object.keys(cur).length === 0) {
|
|
||||||
result[prop] = cur;
|
|
||||||
}
|
|
||||||
else if (Array.isArray(cur)) {
|
|
||||||
if (cur.length && (Object(cur[0]) !== cur || Object.keys(cur[0]).length === 0)) { // array of non-objects
|
|
||||||
result[prop] = '[' + cur.join(', ') + ']';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let l = 0;
|
|
||||||
for(let i = 0, l = cur.length; i < l; i++)
|
|
||||||
recurse(cur[i], prop + "[" + i + "]");
|
|
||||||
if (l == 0)
|
|
||||||
result[prop] = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let isEmpty = true;
|
|
||||||
for (let p in cur) {
|
|
||||||
isEmpty = false;
|
|
||||||
recurse(cur[p], prop ? prop+"."+p : p);
|
|
||||||
}
|
|
||||||
if (isEmpty && prop)
|
|
||||||
result[prop] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recurse(data, '');
|
|
||||||
return result;
|
|
||||||
}
|
|
41
src/helpers/flatten.ts
Normal file
41
src/helpers/flatten.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
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) {
|
||||||
|
result[prop] = cur;
|
||||||
|
}
|
||||||
|
else if (prop === 'spectrum.dpt') {
|
||||||
|
console.log('dpt');
|
||||||
|
result[prop + '.labels'] = cur.map(e => e[0]);
|
||||||
|
result[prop + '.values'] = cur.map(e => e[1]);
|
||||||
|
}
|
||||||
|
else if (Array.isArray(cur)) {
|
||||||
|
if (keepArray) {
|
||||||
|
result[prop] = cur;
|
||||||
|
}
|
||||||
|
else { // array to string
|
||||||
|
if (cur.length && (Object(cur[0]) !== cur || Object.keys(cur[0]).length === 0)) { // array of non-objects
|
||||||
|
result[prop] = '[' + cur.join(', ') + ']';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let l = 0;
|
||||||
|
for(let i = 0, l = cur.length; i < l; i++)
|
||||||
|
recurse(cur[i], prop + "[" + i + "]");
|
||||||
|
if (l == 0)
|
||||||
|
result[prop] = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let isEmpty = true;
|
||||||
|
for (let p in cur) {
|
||||||
|
isEmpty = false;
|
||||||
|
recurse(cur[p], prop ? prop+"."+p : p);
|
||||||
|
}
|
||||||
|
if (isEmpty && prop)
|
||||||
|
result[prop] = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recurse(data, '');
|
||||||
|
return result;
|
||||||
|
}
|
@ -7,8 +7,6 @@ import TestHelper from "../test/helper";
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
|
|
||||||
|
|
||||||
// TODO: allowed types: tension rod, part, granulate, other
|
|
||||||
// TODO: filter by conditions and material properties
|
|
||||||
|
|
||||||
describe('/sample', () => {
|
describe('/sample', () => {
|
||||||
let server;
|
let server;
|
||||||
@ -451,12 +449,12 @@ describe('/sample', () => {
|
|||||||
res: {status: 'Invalid body format', details: 'Measurement key not found'}
|
res: {status: 'Invalid body format', details: 'Measurement key not found'}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('returns a correct csv file if specified', done => {
|
it('returns a correct csv file for admins if specified', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
url: '/samples?status[]=new&status[]=validated&page-size=2&csv=true',
|
url: '/samples?status[]=new&status[]=validated&page-size=2&output=csv',
|
||||||
contentType: /text\/csv/,
|
contentType: /text\/csv/,
|
||||||
auth: {basic: 'janedoe'},
|
auth: {basic: 'admin'},
|
||||||
httpStatus: 200
|
httpStatus: 200
|
||||||
}).end((err, res) => {
|
}).end((err, res) => {
|
||||||
if (err) return done(err);
|
if (err) return done(err);
|
||||||
@ -466,6 +464,30 @@ describe('/sample', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
it('rejects returning a csv file for a write user', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'get',
|
||||||
|
url: '/samples?status[]=new&status[]=validated&page-size=2&output=csv',
|
||||||
|
auth: {basic: 'janedoe'},
|
||||||
|
httpStatus: 403
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('returns the object flattened if specified', done => {
|
||||||
|
TestHelper.request(server, done, {
|
||||||
|
method: 'get',
|
||||||
|
url: '/samples?status[]=new&status[]=validated&fields[]=number&fields[]=measurements.spectrum.device&fields[]=measurements.spectrum.dpt&page-size=1&output=flatten',
|
||||||
|
auth: {basic: 'admin'},
|
||||||
|
httpStatus: 200
|
||||||
|
}).end((err, res) => {
|
||||||
|
if (err) return done(err);
|
||||||
|
should(res.body[0]).have.only.keys('number', 'spectrum.device', 'spectrum.dpt.labels', 'spectrum.dpt.values');
|
||||||
|
should(res.body[0]).have.property('number', '1');
|
||||||
|
should(res.body[0]).have.property('spectrum.device', 'Alpha I');
|
||||||
|
should(res.body[0]).have.property('spectrum.dpt.labels', [3997.12558, 3995.08519, 3993.0448]);
|
||||||
|
should(res.body[0]).have.property('spectrum.dpt.values', [98.00555, 98.03253, 98.02657]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
it('returns only the fields specified', done => {
|
it('returns only the fields specified', done => {
|
||||||
TestHelper.request(server, done, {
|
TestHelper.request(server, done, {
|
||||||
method: 'get',
|
method: 'get',
|
||||||
|
@ -16,6 +16,7 @@ import ConditionTemplateModel from '../models/condition_template';
|
|||||||
import ParametersValidate from './validate/parameters';
|
import ParametersValidate from './validate/parameters';
|
||||||
import db from '../db';
|
import db from '../db';
|
||||||
import csv from '../helpers/csv';
|
import csv from '../helpers/csv';
|
||||||
|
import flatten from '../helpers/flatten';
|
||||||
|
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@ -34,8 +35,9 @@ router.get('/samples', async (req, res, next) => {
|
|||||||
const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0);
|
const {error, value: filters} = SampleValidate.query(req.query, ['dev', 'admin'].indexOf(req.authDetails.level) >= 0);
|
||||||
if (error) return res400(error, res);
|
if (error) return res400(error, res);
|
||||||
|
|
||||||
// spectral data not allowed for read/write users
|
// spectral data and csv not allowed for read/write users
|
||||||
if (filters.fields.find(e => /\.dpt$/.test(e)) && !req.auth(res, ['dev', 'admin'], 'all')) return;
|
if ((filters.fields.find(e => /\.dpt$/.test(e)) || filters.output !== 'json') &&
|
||||||
|
!req.auth(res, ['dev', 'admin'], 'all')) return;
|
||||||
|
|
||||||
// TODO: find a better place for these
|
// TODO: find a better place for these
|
||||||
const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id',
|
const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id',
|
||||||
@ -195,7 +197,8 @@ router.get('/samples', async (req, res, next) => {
|
|||||||
{$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}
|
{$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// if (sortFilterKeys.find(e => e === 'material.number')) { // add material number if needed // TODO: adapt code to new numbers format
|
// TODO: adapt code to new numbers format
|
||||||
|
// if (sortFilterKeys.find(e => e === 'material.number')) { // add material number if needed
|
||||||
// materialQuery.push(
|
// materialQuery.push(
|
||||||
// {$addFields: {'material.number': { $arrayElemAt: [
|
// {$addFields: {'material.number': { $arrayElemAt: [
|
||||||
// '$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}
|
// '$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}
|
||||||
@ -393,13 +396,16 @@ router.get('/samples', async (req, res, next) => {
|
|||||||
[filters.sort[0].split('.')[1],
|
[filters.sort[0].split('.')[1],
|
||||||
...measurementFilterFields, ...measurementFieldsFields]
|
...measurementFilterFields, ...measurementFieldsFields]
|
||||||
);
|
);
|
||||||
if (filters.csv) { // output as csv
|
if (filters.output === 'csv') { // output as csv
|
||||||
csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => {
|
csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
res.set('Content-Type', 'text/csv');
|
res.set('Content-Type', 'text/csv');
|
||||||
res.send(data);
|
res.send(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
else if (filters.output === 'flatten') {
|
||||||
|
res.json(_.compact(data.map(e => flatten(SampleValidate.output(e, 'refs', measurementFields), true))));
|
||||||
|
}
|
||||||
else { // validate all and filter null values from validation errors
|
else { // validate all and filter null values from validation errors
|
||||||
res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))));
|
res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))));
|
||||||
}
|
}
|
||||||
@ -417,6 +423,9 @@ router.get('/samples', async (req, res, next) => {
|
|||||||
delete data._id;
|
delete data._id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (filters.output === 'flatten') {
|
||||||
|
data = flatten(data, true);
|
||||||
|
}
|
||||||
res.write((count === 0 ? '' : ',\n') + JSON.stringify(data)); count ++;
|
res.write((count === 0 ? '' : ',\n') + JSON.stringify(data)); count ++;
|
||||||
});
|
});
|
||||||
stream.on('error', err => {
|
stream.on('error', err => {
|
||||||
@ -478,9 +487,6 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
|
|||||||
if (sample.hasOwnProperty('material_id')) {
|
if (sample.hasOwnProperty('material_id')) {
|
||||||
if (!await materialCheck(sample, res, next)) return;
|
if (!await materialCheck(sample, res, next)) return;
|
||||||
}
|
}
|
||||||
else if (sample.hasOwnProperty('color')) {
|
|
||||||
if (!await materialCheck(sample, res, next, sampleData.material_id)) return;
|
|
||||||
}
|
|
||||||
// do not execute check if condition is and was empty
|
// do not execute check if condition is and was empty
|
||||||
if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) {
|
if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) {
|
||||||
if (!await conditionCheck(sample.condition, 'change', res, next,
|
if (!await conditionCheck(sample.condition, 'change', res, next,
|
||||||
@ -706,8 +712,8 @@ async function numberCheck(sample, res, next) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// validate material_id and color, returns false if invalid
|
// validate material_id and color, returns false if invalid
|
||||||
async function materialCheck (sample, res, next, id = sample.material_id) {
|
async function materialCheck (sample, res, next) {
|
||||||
const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any;
|
const materialData = await MaterialModel.findById(sample.material_id).lean().exec().catch(err => next(err)) as any;
|
||||||
if (materialData instanceof Error) return false;
|
if (materialData instanceof Error) return false;
|
||||||
if (!materialData) { // could not find material_id
|
if (!materialData) { // could not find material_id
|
||||||
res.status(400).json({status: 'Material not available'});
|
res.status(400).json({status: 'Material not available'});
|
||||||
|
@ -59,9 +59,20 @@ router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => {
|
|||||||
if (!await usernameCheck(user.name, res, next)) return;
|
if (!await usernameCheck(user.name, res, next)) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get current mail address to compare to given address
|
||||||
|
const {email: oldMail} = await UserModel.findOne({name: username}).lean().exec().catch(err => next(err));
|
||||||
|
|
||||||
await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec( (err, data:any) => {
|
await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec( (err, data:any) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
if (data) {
|
if (data) {
|
||||||
|
if (data.mail !== oldMail) { // mail address was changed, send notice to old address
|
||||||
|
Mail.send(oldMail, 'Email change in your DeFinMa database account',
|
||||||
|
'Hi, <br><br> Your email address of your DeFinMa account was changed to ' + data.mail +
|
||||||
|
'<br><br>If you actually did this, just delete this email.' +
|
||||||
|
'<br><br>If you did not change your email, someone might be messing around with your account, ' +
|
||||||
|
'so talk to the sysadmin quickly!<br><br>Have a nice day.' +
|
||||||
|
'<br><br>The DeFinMa team');
|
||||||
|
}
|
||||||
res.json(UserValidate.output(data));
|
res.json(UserValidate.output(data));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -227,7 +227,7 @@ export default class SampleValidate {
|
|||||||
sort: Joi.string().pattern(
|
sort: Joi.string().pattern(
|
||||||
new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm')
|
new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm')
|
||||||
).default('_id-asc'),
|
).default('_id-asc'),
|
||||||
csv: Joi.boolean().default(false),
|
output: Joi.string().valid('json', 'flatten', 'csv').default('json'),
|
||||||
fields: Joi.array().items(Joi.string().pattern(
|
fields: Joi.array().items(Joi.string().pattern(
|
||||||
new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm')
|
new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm')
|
||||||
)).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added'])
|
)).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added'])
|
||||||
|
Reference in New Issue
Block a user