From 78d35c520e7177318e4b785213360e835eeb2889 Mon Sep 17 00:00:00 2001 From: VLE2FE Date: Wed, 15 Jul 2020 13:11:33 +0200 Subject: [PATCH] restructured material --- api/parameters.yaml | 13 +- api/schemas.yaml | 28 +-- api/template.yaml | 134 ++--------- src/models/material.ts | 9 +- src/models/material_template.ts | 20 ++ src/routes/material.spec.ts | 383 +++++++++++++++++--------------- src/routes/material.ts | 35 ++- src/routes/measurement.spec.ts | 4 +- src/routes/sample.spec.ts | 71 ++---- src/routes/sample.ts | 17 +- src/routes/template.spec.ts | 45 ++++ src/routes/template.ts | 8 +- src/routes/user.spec.ts | 4 +- src/routes/validate/id.ts | 2 +- src/routes/validate/material.ts | 44 +--- src/routes/validate/sample.ts | 5 +- src/routes/validate/template.ts | 6 +- src/routes/validate/user.ts | 6 +- src/test/db.json | 214 +++++++++++++----- 19 files changed, 556 insertions(+), 492 deletions(-) create mode 100644 src/models/material_template.ts diff --git a/api/parameters.yaml b/api/parameters.yaml index 3cbe49b..066fc3a 100644 --- a/api/parameters.yaml +++ b/api/parameters.yaml @@ -15,10 +15,19 @@ Name: type: string State: - name: group + name: state description: 'possible values: new, deleted' in: path required: true schema: type: string - example: deleted \ No newline at end of file + example: deleted + +Collection: + name: collection + description: 'possible values: condition, measurement, material' + in: path + required: true + schema: + type: string + example: condition \ No newline at end of file diff --git a/api/schemas.yaml b/api/schemas.yaml index 99f7998..1f41d46 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -115,25 +115,21 @@ Material: group: type: string example: PA46 - mineral: - type: number - example: 0 - glass_fiber: - type: number - example: 40 - carbon_fiber: - type: number - example: 0 + properties: + type: object + properties: + material_template: + $ref: 'api.yaml#/components/schemas/Id' + example: + condition_template: 5ea0450ed851c30a90e70894 + mineral: 0 + glass_fiber: 40 + carbon_fiber: 0 numbers: type: array items: - type: object - allOf: - - $ref: 'api.yaml#/components/schemas/Color' - properties: - number: - type: string - example: 5514263423 + type: string + example: 5514263423 Measurement: allOf: diff --git a/api/template.yaml b/api/template.yaml index 4fa938d..6af1294 100644 --- a/api/template.yaml +++ b/api/template.yaml @@ -1,6 +1,8 @@ -/template/conditions: +/template/{collection}s: + parameters: + - $ref: 'api.yaml#/components/parameters/Collection' get: - summary: all available condition methods + summary: all available templates description: 'Auth: basic, levels: read, write, maintain, dev, admin' tags: - /template @@ -8,7 +10,7 @@ - BasicAuth: [] responses: 200: - description: list of conditions + description: list of templates content: application/json: schema: @@ -20,11 +22,12 @@ 500: $ref: 'api.yaml#/components/responses/500' -/template/condition/{id}: +/template/{collection}/{id}: parameters: + - $ref: 'api.yaml#/components/parameters/Collection' - $ref: 'api.yaml#/components/parameters/Id' get: - summary: condition method details + summary: template details description: 'Auth: basic, levels: read, write, maintain, admin' tags: - /template @@ -32,7 +35,7 @@ - BasicAuth: [] responses: 200: - description: condition details + description: template details content: application/json: schema: @@ -44,7 +47,7 @@ 500: $ref: 'api.yaml#/components/responses/500' put: - summary: change condition method + summary: change template description: 'Auth: basic, levels: maintain, admin' x-doc: With a change a new version is set, resulting in a new template with a new id tags: @@ -59,7 +62,7 @@ $ref: 'api.yaml#/components/schemas/Template' responses: 200: - description: condition details + description: template details content: application/json: schema: @@ -75,116 +78,11 @@ 500: $ref: 'api.yaml#/components/responses/500' -/template/condition/new: - post: - summary: add condition method - description: 'Auth: basic, levels: maintain, admin' - tags: - - /template - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Template' - responses: - 200: - description: condition details - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Template' - 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' - -/template/measurements: - get: - summary: all available measurement methods - description: 'Auth: basic, levels: read, write, maintain, dev, admin' - tags: - - /template - security: - - BasicAuth: [] - responses: - 200: - description: list of measurement methods - content: - application/json: - schema: - type: array - items: - $ref: 'api.yaml#/components/schemas/Template' - 401: - $ref: 'api.yaml#/components/responses/401' - 500: - $ref: 'api.yaml#/components/responses/500' -/template/measurement/{id}: +/template/{collection}/new: parameters: - - $ref: 'api.yaml#/components/parameters/Id' - get: - summary: measurement method details - description: 'Auth: basic, levels: read, write, maintain, admin' - tags: - - /template - security: - - BasicAuth: [] - responses: - 200: - description: measurement details - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Template' - 400: - $ref: 'api.yaml#/components/responses/400' - 401: - $ref: 'api.yaml#/components/responses/401' - 404: - $ref: 'api.yaml#/components/responses/404' - 500: - $ref: 'api.yaml#/components/responses/500' - put: - summary: change measurement method - description: 'Auth: basic, levels: maintain, admin' - tags: - - /template - security: - - BasicAuth: [] - requestBody: - required: true - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Template' - responses: - 200: - description: measurement details - content: - application/json: - schema: - $ref: 'api.yaml#/components/schemas/Template' - 400: - $ref: 'api.yaml#/components/responses/400' - 401: - $ref: 'api.yaml#/components/responses/401' - 403: - $ref: 'api.yaml#/components/responses/403' - 404: - $ref: 'api.yaml#/components/responses/404' - 500: - $ref: 'api.yaml#/components/responses/500' - -/template/measurement/new: + - $ref: 'api.yaml#/components/parameters/Collection' post: - summary: add measurement method + summary: add template description: 'Auth: basic, levels: maintain, admin' tags: - /template @@ -198,7 +96,7 @@ $ref: 'api.yaml#/components/schemas/Template' responses: 200: - description: measurement details + description: template details content: application/json: schema: @@ -210,4 +108,4 @@ 403: $ref: 'api.yaml#/components/responses/403' 500: - $ref: 'api.yaml#/components/responses/500' \ No newline at end of file + $ref: 'api.yaml#/components/responses/500' diff --git a/src/models/material.ts b/src/models/material.ts index d7d5eb9..0c1629a 100644 --- a/src/models/material.ts +++ b/src/models/material.ts @@ -7,13 +7,8 @@ const MaterialSchema = new mongoose.Schema({ name: {type: String, index: {unique: true}}, supplier_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialSupplierModel}, group_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialGroupsModel}, - mineral: Number, - glass_fiber: Number, - carbon_fiber: Number, - numbers: [{ - color: String, - number: String - }], + properties: mongoose.Schema.Types.Mixed, + numbers: [String], status: Number }, {minimize: false}); diff --git a/src/models/material_template.ts b/src/models/material_template.ts new file mode 100644 index 0000000..5e06819 --- /dev/null +++ b/src/models/material_template.ts @@ -0,0 +1,20 @@ +import mongoose from 'mongoose'; +import db from '../db'; + +const MaterialTemplateSchema = new mongoose.Schema({ + first_id: mongoose.Schema.Types.ObjectId, + name: String, + version: Number, + parameters: [new mongoose.Schema({ + name: String, + range: mongoose.Schema.Types.Mixed + } ,{ _id : false })] +}, {minimize: false}); // to allow empty objects + +// changelog query helper +MaterialTemplateSchema.query.log = function > (req) { + db.log(req, this); + return this; +} + +export default mongoose.model>('material_template', MaterialTemplateSchema); \ No newline at end of file diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts index e412615..789f6e5 100644 --- a/src/routes/material.spec.ts +++ b/src/routes/material.spec.ts @@ -1,5 +1,4 @@ import should from 'should/as-function'; -import _ from 'lodash'; import MaterialModel from '../models/material'; import MaterialGroupModel from '../models/material_groups'; import MaterialSupplierModel from '../models/material_suppliers'; @@ -7,7 +6,6 @@ import TestHelper from "../test/helper"; import globals from '../globals'; - describe('/material', () => { let server; before(done => TestHelper.before(done)); @@ -27,19 +25,13 @@ describe('/material', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === globals.status.validated).length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); - should(material).have.property('mineral').be.type('number'); - should(material).have.property('glass_fiber').be.type('number'); - should(material).have.property('carbon_fiber').be.type('number'); - should(material.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color').be.type('string'); - should(number).have.property('number').be.type('string'); - }); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); }); done(); }); @@ -55,19 +47,13 @@ describe('/material', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === globals.status.validated).length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); - should(material).have.property('mineral').be.type('number'); - should(material).have.property('glass_fiber').be.type('number'); - should(material).have.property('carbon_fiber').be.type('number'); - should(material.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color').be.type('string'); - should(number).have.property('number').be.type('string'); - }); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); }); done(); }); @@ -83,19 +69,13 @@ describe('/material', () => { const json = require('../test/db.json'); should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status === globals.status.new).length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); - should(material).have.property('mineral').be.type('number'); - should(material).have.property('glass_fiber').be.type('number'); - should(material).have.property('carbon_fiber').be.type('number'); - should(material.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color').be.type('string'); - should(number).have.property('number').be.type('string'); - }); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); }); done(); }); @@ -131,19 +111,13 @@ describe('/material', () => { let asyncCounter = res.body.length; should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status ===globals.status.new).length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); - should(material).have.property('mineral').be.type('number'); - should(material).have.property('glass_fiber').be.type('number'); - should(material).have.property('carbon_fiber').be.type('number'); - should(material.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color').be.type('string'); - should(number).have.property('number').be.type('string'); - }); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); MaterialModel.findById(material._id).lean().exec((err, data) => { should(data).have.property('status',globals.status.new); if (--asyncCounter === 0) { @@ -165,19 +139,13 @@ describe('/material', () => { let asyncCounter = res.body.length; should(res.body).have.lengthOf(json.collections.materials.filter(e => e.status ===globals.status.deleted).length); should(res.body).matchEach(material => { - should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(material).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); should(material).have.property('_id').be.type('string'); should(material).have.property('name').be.type('string'); should(material).have.property('supplier').be.type('string'); should(material).have.property('group').be.type('string'); - should(material).have.property('mineral').be.type('number'); - should(material).have.property('glass_fiber').be.type('number'); - should(material).have.property('carbon_fiber').be.type('number'); - should(material.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color').be.type('string'); - should(number).have.property('number').be.type('string'); - }); + should(material.properties).have.property('material_template').be.type('string'); + should(material.numbers).be.instanceof(Array); MaterialModel.findById(material._id).lean().exec((err, data) => { should(data).have.property('status',globals.status.deleted); if (--asyncCounter === 0) { @@ -219,7 +187,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'}, {color: 'natural', number: '5514263422'}]} + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} }); }); it('returns the right material for an API key', done => { @@ -228,7 +196,7 @@ describe('/material', () => { url: '/material/100000000000000000000003', auth: {key: 'admin'}, httpStatus: 200, - res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: []} + res: {_id: '100000000000000000000003', name: 'PA GF 50 black (2706)', supplier: 'Akro-Plastic', group: 'PA66+PA6I/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0}, numbers: []} }); }); it('returns a material with a color without number', done => { @@ -237,7 +205,7 @@ describe('/material', () => { url: '/material/100000000000000000000007', auth: {basic: 'janedoe'}, httpStatus: 200, - res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: [{color: 'black', number: ''}]} + res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0}, numbers: []} }); }); it('returns a deleted material for a maintain/admin user', done => { @@ -246,7 +214,7 @@ describe('/material', () => { url: '/material/100000000000000000000008', auth: {basic: 'admin'}, httpStatus: 200, - res: {_id: '100000000000000000000008', name: 'Latamid 66 H 2 G 30', supplier: 'LATI', group: 'PA66', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'blue', number: '5513943509'}]} + res: {_id: '100000000000000000000008', name: 'Latamid 66 H 2 G 30', supplier: 'LATI', group: 'PA66', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5513943509']} }); }); it('returns 403 for a write user when requesting a deleted material', done => { @@ -290,7 +258,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'}, {color: 'natural', number: '5514263422'}]} + res: {_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} }); }); it('keeps unchanged properties', done => { @@ -299,10 +267,10 @@ describe('/material', () => { url: '/material/100000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {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'}]} + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_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'}]}); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { if (err) return done(err); should(data).have.property('status',globals.status.validated); @@ -329,7 +297,24 @@ describe('/material', () => { req: {name: 'Stanyl TW 200 F8'} }).end((err, res) => { if (err) return done(err); - should(res.body).be.eql({_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'}]}); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); + MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { + if (err) return done(err); + should(data).have.property('status',globals.status.validated); + done(); + }); + }); + }); + it('keeps unchanged properties', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}} + }).end((err, res) => { + if (err) return done(err); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423', '5514263422']}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { if (err) return done(err); should(data).have.property('status',globals.status.validated); @@ -343,17 +328,16 @@ describe('/material', () => { url: '/material/100000000000000000000001', auth: {basic: 'janedoe'}, 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']} }).end((err, res) => { 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'}]}); + should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']}); MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => { if (err) return done(err); data._id = data._id.toString(); data.group_id = data.group_id.toString(); data.supplier_id = data.supplier_id.toString(); - data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); - should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: 0, __v: 0}); + should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901'], status: 0, __v: 0}); MaterialGroupModel.find({name: 'PA6/6T'}).lean().exec((err, data) => { if (err) return done(err); should(data).have.lengthOf(1); @@ -374,7 +358,7 @@ describe('/material', () => { url: '/material/100000000000000000000001', auth: {basic: 'janedoe'}, 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', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901']}, log: { collection: 'materials', dataAdd: { @@ -386,16 +370,6 @@ describe('/material', () => { } }); }); - it('accepts a color without number', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000007', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {numbers: [{color: 'black', number: ''}, {color: 'natural', number: ''}]}, - res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', mineral: 0, glass_fiber: 0, carbon_fiber: 0, numbers: [{color: 'black', number: ''}, {color: 'natural', number: ''}]} - }); - }) it('rejects already existing material names', done => { TestHelper.request(server, done, { method: 'put', @@ -406,46 +380,16 @@ describe('/material', () => { res: {status: 'Material name already taken'} }); }); - it('rejects a wrong mineral property', done => { + it('rejects wrong material properties', done => { TestHelper.request(server, done, { method: 'put', url: '/material/100000000000000000000001', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {mineral: 'x'}, + req: {properties: {material_template: '130000000000000000000003', mineral: 'x', glass_fiber: 0, carbon_fiber: 0}}, res: {status: 'Invalid body format', details: '"mineral" must be a number'} }); }); - it('rejects a wrong glass_fiber property', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {glass_fiber: 'x'}, - res: {status: 'Invalid body format', details: '"glass_fiber" must be a number'} - }); - }); - it('rejects a wrong carbon_fiber property', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {carbon_fiber: 'x'}, - res: {status: 'Invalid body format', details: '"carbon_fiber" must be a number'} - }); - }); - it('rejects a wrong color name property', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/material/100000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {numbers: [{colorxx: 'black', number: '55'}]}, - res: {status: 'Invalid body format', details: '"numbers[0].color" is required'} - }); - }); it('rejects an invalid id', done => { TestHelper.request(server, done, { method: 'put', @@ -455,6 +399,86 @@ describe('/material', () => { req: {}, }); }); + it('rejects not specified properties parameters', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0, x: 55}}, + res: {status: 'Invalid body format', details: '"x" is not allowed'} + }); + }); + it('rejects a properties parameter not in the value range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000009', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000002', stickiness: 'xx'}}, + res: {status: 'Invalid body format', details: '"stickiness" must be one of [not so sticky, medium, very sticky]'} + }); + }); + it('rejects a properties parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: -5, carbon_fiber: 0}}, + res: {status: 'Invalid body format', details: '"glass_fiber" must be larger than or equal to 0'} + }); + }); + it('rejects a properties parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 105}}, + res: {status: 'Invalid body format', details: '"carbon_fiber" must be less than or equal to 100'} + }); + }); + it('rejects an invalid material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '1300000000000h0000000001', mineral: 0, glass_fiber: 0, carbon_fiber: 0}}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an unknown material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '100000000000000000000001', mineral: 0, glass_fiber: 0, carbon_fiber: 0}}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an old version of a material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000001', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {properties: {material_template: '130000000000000000000001', glass_fiber: 0}}, + res: {status: 'Old template version not allowed'} + }); + }); + it('allows keeping an old version of a material template', done => { + TestHelper.request(server, done, { + method: 'put', + url: '/material/100000000000000000000010', + auth: {basic: 'janedoe'}, + httpStatus: 200, + req: {properties: {material_template: '130000000000000000000001', glass_fiber: 5}}, + res: {_id: '100000000000000000000010', name: 'Latamid 66 G 40', numbers: ['5513943509'], supplier: 'LATI', group: 'PA66', properties: {material_template: '130000000000000000000001', glass_fiber: 5}} + }); + }); it('rejects editing a deleted material', done => { TestHelper.request(server, done, { method: 'put', @@ -516,8 +540,8 @@ describe('/material', () => { data._id = data._id.toString(); data.group_id = data.group_id.toString(); data.supplier_id = data.supplier_id.toString(); - data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}}); - should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: -1, __v: 0} + data.properties.material_template = data.properties.material_template.toString(); + should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 35, carbon_fiber: 0}, numbers: ['5514212901', '5514612901'], status: -1, __v: 0} ); done(); }); @@ -732,22 +756,19 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: '05515798402'}]} + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']} }).end((err, res) => { if (err) return done (err); - should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers'); + should(res.body).have.only.keys('_id', 'name', 'supplier', 'group', 'properties', 'numbers'); should(res.body).have.property('_id').be.type('string'); should(res.body).have.property('name', 'Crastin CE 2510'); should(res.body).have.property('supplier', 'Du Pont'); should(res.body).have.property('group', 'PBT'); - should(res.body).have.property('mineral', 0); - should(res.body).have.property('glass_fiber', 30); - should(res.body).have.property('carbon_fiber', 0); - should(res.body.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color', 'black'); - should(number).have.property('number', '05515798402'); - }); + should(res.body.properties).have.property('material_template', '130000000000000000000003'); + should(res.body.properties).have.property('mineral', 0); + should(res.body.properties).have.property('glass_fiber', 30); + should(res.body.properties).have.property('carbon_fiber', 0); + should(res.body).have.property('numbers', ['5515798402']); done(); }); }); @@ -757,17 +778,18 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} }).end(err => { if (err) return done (err); MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, materialData: any) => { if (err) return done (err); should(materialData).have.lengthOf(1); - should(materialData[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v'); + should(materialData[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'properties', 'numbers', 'status', '__v'); should(materialData[0]).have.property('name', 'Crastin CE 2510'); - should(materialData[0]).have.property('mineral', 0); - should(materialData[0]).have.property('glass_fiber', 30); - should(materialData[0]).have.property('carbon_fiber', 0); + should(materialData[0].properties).have.property('material_template', '130000000000000000000003'); + should(materialData[0].properties).have.property('mineral', 0); + should(materialData[0].properties).have.property('glass_fiber', 30); + should(materialData[0].properties).have.property('carbon_fiber', 0); should(materialData[0]).have.property('status',globals.status.new); should(materialData[0].numbers).have.lengthOf(0); MaterialGroupModel.findById(materialData[0].group_id).lean().exec((err, data) => { @@ -788,7 +810,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 200, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []}, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []}, log: { collection: 'materials', dataAdd: {status: 0}, @@ -796,50 +818,13 @@ describe('/material', () => { } }); }); - it('accepts a color without number', done => { - TestHelper.request(server, done, { - method: 'post', - url: '/material/new', - auth: {basic: 'janedoe'}, - httpStatus: 200, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: ''}]} - }).end((err, res) => { - if (err) return done (err); - 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'); - should(res.body).have.property('supplier', 'Du Pont'); - should(res.body).have.property('group', 'PBT'); - should(res.body).have.property('mineral', 0); - should(res.body).have.property('glass_fiber', 30); - should(res.body).have.property('carbon_fiber', 0); - should(res.body.numbers).matchEach(number => { - should(number).have.only.keys('color', 'number'); - should(number).have.property('color', 'black'); - should(number).have.property('number', ''); - }); - MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => { - if (err) return done (err); - should(data).have.lengthOf(1); - should(data[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v'); - should(data[0]).have.property('_id'); - should(data[0]).have.property('name', 'Crastin CE 2510'); - should(data[0]).have.property('mineral', 0); - should(data[0]).have.property('glass_fiber', 30); - should(data[0]).have.property('carbon_fiber', 0); - should(data[0]).have.property('status',globals.status.new); - should(_.omit(data[0].numbers[0], '_id')).be.eql({color: 'black', number: ''}); - done(); - }); - }); - }); it('rejects already existing material names', done => { TestHelper.request(server, done, { method: 'post', url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', mineral: 0, glass_fiber: 40, carbon_fiber: 0, numbers: [{color: 'black', number: '5514263423'}]}, + req: {name: 'Stanyl TW 200 F8', supplier: 'DSM', group: 'PA46', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 40, carbon_fiber: 0}, numbers: ['5514263423']}, res: {status: 'Material name already taken'} }); }); @@ -849,7 +834,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: '5515798402'}]}, + req: {supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, res: {status: 'Invalid body format', details: '"name" is required'} }); }); @@ -859,7 +844,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: '5515798402'}]}, + req: {name: 'Crastin CE 2510', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, res: {status: 'Invalid body format', details: '"supplier" is required'} }); }); @@ -869,7 +854,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: '5515798402'}]}, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, res: {status: 'Invalid body format', details: '"group" is required'} }); }); @@ -879,7 +864,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: '5515798402'}]}, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: 30, carbon_fiber: 0}, numbers: ['5515798402']}, res: {status: 'Invalid body format', details: '"mineral" is required'} }); }); @@ -889,7 +874,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, carbon_fiber: 0, numbers: [{color: 'black', number: '5515798402'}]}, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, carbon_fiber: 0}, numbers: ['5515798402']}, res: {status: 'Invalid body format', details: '"glass_fiber" is required'} }); }); @@ -899,7 +884,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, numbers: [{color: 'black', number: '5515798402'}]}, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30}, numbers: ['5515798402']}, res: {status: 'Invalid body format', details: '"carbon_fiber" is required'} }); }); @@ -909,28 +894,78 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}}, res: {status: 'Invalid body format', details: '"numbers" is required'} }); }); - it('rejects a missing color name', done => { + it('rejects not specified properties parameters', done => { TestHelper.request(server, done, { method: 'post', url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{number: '5515798402'}]}, - res: {status: 'Invalid body format', details: '"numbers[0].color" is required'} + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, carbon_fiber: 0, glass_fiber: 30, x: 47}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"x" is not allowed'} }); }); - it('rejects a missing color number', done => { + it('rejects a properties parameter not in the value range', done => { TestHelper.request(server, done, { method: 'post', url: '/material/new', auth: {basic: 'janedoe'}, httpStatus: 400, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black'}]}, - res: {status: 'Invalid body format', details: '"numbers[0].number" is required'} + req: {name: 'Glue2', supplier: 'BASF', group: 'Glue', properties: {material_template: '130000000000000000000002', stickiness: 'not so much'}, numbers: []}, + res: {status: 'Invalid body format', details: '"stickiness" must be one of [not so sticky, medium, very sticky]'} + }); + }); + it('rejects a properties parameter below minimum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: -0.3}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"glass_fiber" must be larger than or equal to 0'} + }); + }); + it('rejects a properties parameter above maximum range', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', glass_fiber: 100.001}, numbers: ['5515798402']}, + res: {status: 'Invalid body format', details: '"glass_fiber" must be less than or equal to 100'} + }); + }); + it('rejects an invalid material template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000h00000000000003', glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an unknown material template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '100000000000000000000003', glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Material template not available'} + }); + }); + it('rejects an old version of a material template', done => { + TestHelper.request(server, done, { + method: 'post', + url: '/material/new', + auth: {basic: 'janedoe'}, + httpStatus: 400, + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000001', glass_fiber: 30}, numbers: ['5515798402']}, + res: {status: 'Old template version not allowed'} }); }); it('rejects an API key', done => { @@ -939,7 +974,7 @@ describe('/material', () => { url: '/material/new', auth: {key: 'janedoe'}, httpStatus: 401, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} }); }); it('rejects requests from a read user', done => { @@ -948,7 +983,7 @@ describe('/material', () => { url: '/material/new', auth: {basic: 'user'}, httpStatus: 403, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} }); }); it('rejects unauthorized requests', done => { @@ -956,7 +991,7 @@ describe('/material', () => { method: 'post', url: '/material/new', httpStatus: 401, - req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []} + req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 30, carbon_fiber: 0}, numbers: []} }); }); }); diff --git a/src/routes/material.ts b/src/routes/material.ts index 3f34e3a..43c818b 100644 --- a/src/routes/material.ts +++ b/src/routes/material.ts @@ -11,6 +11,8 @@ import res400 from './validate/res400'; import mongoose from 'mongoose'; import globals from '../globals'; import db from '../db'; +import MaterialTemplateModel from '../models/material_template'; +import ParametersValidate from './validate/parameters'; @@ -92,6 +94,9 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => { material = await supplierResolve(material, req, next); if (!material) return; } + if (material.hasOwnProperty('properties')) { + if (!await propertiesCheck(material.properties, 'change', res, next, materialData.properties.material_template.toString() !== material.properties.material_template)) return; + } // check for changes if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) { @@ -149,7 +154,7 @@ router.post('/material/new', async (req, res, next) => { if (!material) return; material = await supplierResolve(material, req, next); if (!material) return; - + if (!await propertiesCheck(material.properties, 'new', res, next)) return; material.status = globals.status.new; // set status to new await new MaterialModel(material).save(async (err, data) => { @@ -211,6 +216,34 @@ async function supplierResolve (material, req, next) { return material; } +async function propertiesCheck (properties, param, res, next, checkVersion = true) { // validate material properties, returns false if invalid, otherwise template data + if (!properties.material_template || !IdValidate.valid(properties.material_template)) { // template id not found + res.status(400).json({status: 'Material template not available'}); + return false; + } + const materialData = await MaterialTemplateModel.findById(properties.material_template).lean().exec().catch(err => next(err)) as any; + if (materialData instanceof Error) return false; + if (!materialData) { // template not found + res.status(400).json({status: 'Material template not available'}); + return false; + } + + if (checkVersion) { + // get all template versions and check if given is latest + const materialVersions = await MaterialTemplateModel.find({first_id: materialData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any; + if (materialVersions instanceof Error) return false; + if (properties.material_template !== materialVersions[0]._id.toString()) { // template not latest + res.status(400).json({status: 'Old template version not allowed'}); + return false; + } + } + + // validate parameters + const {error, value: ignore} = ParametersValidate.input(_.omit(properties, 'material_template'), materialData.parameters, param); + if (error) {res400(error, res); return false;} + return materialData; +} + function setStatus (status, req, res, next) { // set measurement status MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => { if (err) return next(err); diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts index dd43520..d33bfdc 100644 --- a/src/routes/measurement.spec.ts +++ b/src/routes/measurement.spec.ts @@ -600,7 +600,7 @@ describe('/measurement', () => { auth: {basic: 'janedoe'}, httpStatus: 400, req: {sample_id: '400000000000h00000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}, - res: {status: 'Invalid body format', details: '"sample_id" with value "400000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'} + res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); it('rejects a sample id not available', done => { @@ -620,7 +620,7 @@ describe('/measurement', () => { auth: {basic: 'janedoe'}, httpStatus: 400, req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '30000000000h000000000002'}, - res: {status: 'Invalid body format', details: '"measurement_template" with value "30000000000h000000000002" fails to match the required pattern: /[0-9a-f]{24}/'} + res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); it('rejects a measurement_template not available', done => { diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts index 7dc5f24..25289c0 100644 --- a/src/routes/sample.spec.ts +++ b/src/routes/sample.spec.ts @@ -7,10 +7,9 @@ import TestHelper from "../test/helper"; import globals from '../globals'; import mongoose from 'mongoose'; -// TODO: generate output for ML in format DPT -> data, implement filtering, field selection -// TODO: generate csv -// TODO: write script for data import + // TODO: allowed types: tension rod, part, granulate, other +// TODO: filter by conditions and material properties describe('/sample', () => { let server; @@ -262,14 +261,14 @@ describe('/sample', () => { it('multiplies the sample information for each spectrum', done => { TestHelper.request(server, done, { method: 'get', - url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum', + url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum.dpt', auth: {basic: 'janedoe'}, httpStatus: 200 }).end((err, res) => { if (err) return done(err); should(res.body).have.lengthOf(2); - should(res.body[0]).have.property('spectrum', [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); - should(res.body[1]).have.property('spectrum', [[3996.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); + should(res.body[0].spectrum).have.property('dpt', [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); + should(res.body[1].spectrum).have.property('dpt', [[3996.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]); done(); }); }); @@ -338,16 +337,16 @@ describe('/sample', () => { it('filters multiple properties', done => { TestHelper.request(server, done, { method: 'get', - url: '/samples?status=all&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.glass_fiber%22%2C%22values%22%3A%5B33%5D%7D&filters[]=%7B%22mode%22%3A%22lte%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22Rng33%22%5D%7D&filters[]=%7B%22mode%22%3A%22nin%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', + url: '/samples?status=all&fields[]=number&fields[]=batch&filters[]=%7B%22mode%22%3A%22lte%22%2C%22field%22%3A%22number%22%2C%22values%22%3A%5B%22Rng33%22%5D%7D&filters[]=%7B%22mode%22%3A%22nin%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', auth: {basic: 'janedoe'}, httpStatus: 200 }).end((err, res) => { if (err) return done(err); - should(res.body).have.lengthOf(1); - should(res.body[0]).be.eql({number: '32', material: {glass_fiber: 33}, batch: '1653000308'}); + should(res.body).have.lengthOf(3); + should(res.body[0]).be.eql({number: '1', batch: ''}); done(); }); - }); // TODO: do measurement pipeline, check if it works with UI + }); it('rejects an invalid JSON string as a filters parameter', done => { TestHelper.request(server, done, { method: 'get', @@ -372,7 +371,7 @@ describe('/sample', () => { url: '/samples?status=all&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22xx%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', auth: {basic: 'janedoe'}, httpStatus: 400, - res: {status: 'Invalid body format', details: '"filters[0].field" with value "xx" fails to match the required pattern: /^(_id|color|number|type|batch|added|material\\.name|material\\.supplier|material\\.group|material\\.mineral|material\\.glass_fiber|material\\.carbon_fiber|material\\.number|measurements\\.(?!spectrum).+|condition|material_id|material|note_id|user_id|material\\._id|material\\.numbers|measurements\\.spectrum)$/m'} + res: {status: 'Invalid body format', details: 'Invalid JSON string for filter parameter'} }); }); it('rejects unknown measurement names', done => { @@ -402,10 +401,10 @@ describe('/sample', () => { it('returns only the fields specified', done => { TestHelper.request(server, done, { method: 'get', - url: '/samples?status=all&page-size=1&fields[]=number&fields[]=condition&fields[]=color&fields[]=material.name&fields[]=material.mineral', + url: '/samples?status=all&page-size=1&fields[]=number&fields[]=condition&fields[]=color&fields[]=material.name&fields[]=material.supplier', auth: {basic: 'janedoe'}, httpStatus: 200, - res: [{number: '1', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, color: 'black', material: {name: 'Schulamid 66 GF 25 H', mineral: 0}}] + res: [{number: '1', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, color: 'black', material: {name: 'Schulamid 66 GF 25 H', supplier: 'Schulmann'}}] }); }); it('rejects a from-id not in the database', done => { @@ -432,7 +431,7 @@ describe('/sample', () => { url: '/samples?status=all&page-size=1&fields[]=xx', auth: {basic: 'janedoe'}, httpStatus: 400, - res: {status: 'Invalid body format', details: '"fields[0]" with value "xx" fails to match the required pattern: /^(_id|color|number|type|batch|added|material\\.name|material\\.supplier|material\\.group|material\\.mineral|material\\.glass_fiber|material\\.carbon_fiber|material\\.number|measurements\\.(?!spectrum).+|condition|material_id|material|note_id|user_id|material\\._id|material\\.numbers|measurements\\.spectrum)$/m'} + res: {status: 'Invalid body format', details: 'Invalid field name'} }); }); it('rejects a negative page size', done => { @@ -450,7 +449,7 @@ describe('/sample', () => { url: '/samples?from-id=40000000000h000000000002', auth: {basic: 'janedoe'}, httpStatus: 400, - res: {status: 'Invalid body format', details: '"from-id" with value "40000000000h000000000002" fails to match the required pattern: /[0-9a-f]{24}/'} + res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); it('rejects a to-page without page-size', done => { @@ -619,7 +618,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000003', auth: {basic: 'janedoe'}, httpStatus: 200, - res: {_id: '400000000000000000000003', number: '33', type: 'part', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', mineral: 0, glass_fiber: 33, carbon_fiber: 0, numbers: [{color: 'black', number: '5514262406'}]}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], user: 'admin'} + res: {_id: '400000000000000000000003', number: '33', type: 'part', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], user: 'admin'} }); }); it('works with an API key', done => { @@ -628,7 +627,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000003', auth: {key: 'janedoe'}, httpStatus: 200, - res: {_id: '400000000000000000000003', number: '33', type: 'part', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', mineral: 0, glass_fiber: 33, carbon_fiber: 0, numbers: [{color: 'black', number: '5514262406'}]}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], user: 'admin'} + res: {_id: '400000000000000000000003', number: '33', type: 'part', color: 'black', batch: '1704-005', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {comment: '', sample_references: [{sample_id: '400000000000000000000004', relation: 'granulate to sample'}], custom_fields: {'not allowed for new applications': true}}, measurements: [{_id: '800000000000000000000003', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}], user: 'admin'} }); }); it('returns a deleted sample for a maintain/admin user', done => { @@ -637,7 +636,7 @@ describe('/sample', () => { url: '/sample/400000000000000000000005', auth: {basic: 'admin'}, httpStatus: 200, - res: {_id: '400000000000000000000005', number: 'Rng33', type: 'granulate', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', mineral: 0, glass_fiber: 33, carbon_fiber: 0, numbers: [{color: 'black', number: '5514262406'}]}, notes: {}, measurements: [], user: 'admin'} + res: {_id: '400000000000000000000005', number: 'Rng33', type: 'granulate', color: 'black', batch: '1653000308', condition: {condition_template: '200000000000000000000003'}, material: {_id: '100000000000000000000005', name: 'Amodel A 1133 HS', supplier: 'Solvay', group: 'PPA', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 33, carbon_fiber: 0}, numbers: ['5514262406']}, notes: {}, measurements: [], user: 'admin'} }); }); it('returns 403 for a write user when requesting a deleted sample', done => { @@ -895,26 +894,6 @@ describe('/sample', () => { }); }); }); - it('rejects a color not defined for the material', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Color not available for material'} - }); - }); - it('rejects an undefined color for the same material', done => { - TestHelper.request(server, done, { - method: 'put', - url: '/sample/400000000000000000000001', - auth: {basic: 'janedoe'}, - httpStatus: 400, - req: {type: 'part', color: 'signalviolet', batch: '114531', notes: {comment: 'Testcomment', sample_references: [{sample_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: 'put', @@ -952,7 +931,7 @@ describe('/sample', () => { auth: {basic: 'janedoe'}, httpStatus: 400, req: {type: 'part', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'} + res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); it('rejects an invalid id', done => { @@ -1054,7 +1033,7 @@ describe('/sample', () => { res: {_id: '400000000000000000000004', number: '32', type: 'granulate', color: 'black', batch: '1653000308', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000005', note_id: '500000000000000000000003', user_id: '000000000000000000000003', added: '2004-01-10T13:37:04.000Z'} }); }); - it('rejects an changing back to an empty condition', done => { + it('rejects changing back to an empty condition', done => { TestHelper.request(server, done, { method: 'put', url: '/sample/400000000000000000000001', @@ -1642,16 +1621,6 @@ describe('/sample', () => { 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: {color: 'green', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_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', @@ -1853,7 +1822,7 @@ describe('/sample', () => { auth: {basic: 'janedoe'}, httpStatus: 400, req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}}, - res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'} + res: {status: 'Invalid body format', details: 'Invalid object id'} }); }); it('rejects an API key', done => { diff --git a/src/routes/sample.ts b/src/routes/sample.ts index 91ada86..474a04e 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -27,6 +27,9 @@ const router = express.Router(); // TODO: convert filter value to number according to table model // TODO: validation for filter parameters // TODO: location/device sort/filter + +// TODO: think about material numbers + router.get('/samples', async (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; @@ -311,7 +314,7 @@ router.get('/samples', async (req, res, next) => { if (!fieldsToAdd.find(e => /spectrum\./.test(e))) { // use streaming when including spectrum files collection.aggregate(query).exec((err, data) => { if (err) return next(err); - if (data[0].count) { + if (data[0] && data[0].count) { res.header('x-total-items', data[0].count.length > 0 ? data[0].count[0].count : 0); res.header('Access-Control-Expose-Headers', 'x-total-items'); data = data[0].samples; @@ -425,14 +428,12 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { // only maintain and admin are allowed to edit other user's data if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return; - if (sample.hasOwnProperty('material_id')) { if (!await materialCheck(sample, res, next)) return; } else if (sample.hasOwnProperty('color')) { if (!await materialCheck(sample, res, next, sampleData.material_id)) return; } - if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { // do not execute check if condition is and was empty if (!await conditionCheck(sample.condition, 'change', res, next, sampleData.condition.condition_template.toString() !== sample.condition.condition_template)) return; } @@ -615,11 +616,8 @@ module.exports = router; async function numberGenerate (sample, req, res, next) { // generate number in format Location32, returns false on error const sampleData = await SampleModel - // .findOne({number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}) - // .sort({number: -1}) - // .lean() .aggregate([ - {$match: {number: new RegExp('^' + 'Rng' + '[0-9]+$', 'm')}}, + {$match: {number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')}}, // {$addFields: {number2: {$toDecimal: {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}}}}, // not working with MongoDb 3.6 {$addFields: {sortNumber: {$let: { vars: {tmp: {$concat: ['000000000000000000000000000000', {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}}, @@ -650,10 +648,6 @@ async function materialCheck (sample, res, next, id = sample.material_id) { // res.status(400).json({status: 'Material not available'}); return false; } - if (sample.hasOwnProperty('color') && sample.color !== '' && !materialData.numbers.find(e => e.color === sample.color)) { // color for material not specified - res.status(400).json({status: 'Color not available for material'}); - return false; - } return true; } @@ -764,7 +758,6 @@ function addFilterQueries (queryPtr, filters) { // returns array of match queri } function filterQueries (filters) { - console.log(filters); return filters.map(e => { if (e.mode === 'or') { // allow or queries (needed for $ne added) return {['$' + e.mode]: e.values}; diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index cd90108..ffb0ff4 100644 --- a/src/routes/template.spec.ts +++ b/src/routes/template.spec.ts @@ -895,4 +895,49 @@ describe('/template', () => { }); }); }); + + describe('/template/material', () => { + describe('GET /template/materials', () => { + it('returns all material templates', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/materials', + 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', 'version', 'parameters'); + should(measurement).have.property('_id').be.type('string'); + should(measurement).have.property('name').be.type('string'); + should(measurement).have.property('version').be.type('number'); + 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/materials', + auth: {key: 'janedoe'}, + httpStatus: 401 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/template/materials', + httpStatus: 401 + }); + }); + }); + // other methods should be covered by measurement and condition tests + }); }); \ No newline at end of file diff --git a/src/routes/template.ts b/src/routes/template.ts index c3bd14b..f19587f 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -13,7 +13,7 @@ import db from '../db'; const router = express.Router(); -router.get('/template/:collection(measurements|conditions)', (req, res, next) => { +router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s @@ -23,7 +23,7 @@ router.get('/template/:collection(measurements|conditions)', (req, res, next) => }); }); -router.get('/template/:collection(measurement|condition)/' + IdValidate.parameter(), (req, res, next) => { +router.get('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; model(req).findById(req.params.id).lean().exec((err, data) => { @@ -37,7 +37,7 @@ router.get('/template/:collection(measurement|condition)/' + IdValidate.paramete }); }); -router.put('/template/:collection(measurement|condition)/' + IdValidate.parameter(), async (req, res, next) => { +router.put('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), async (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; const {error, value: template} = TemplateValidate.input(req.body, 'change'); @@ -62,7 +62,7 @@ router.put('/template/:collection(measurement|condition)/' + IdValidate.paramete } }); -router.post('/template/:collection(measurement|condition)/new', async (req, res, next) => { +router.post('/template/:collection(measurement|condition|material)/new', async (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; const {error, value: template} = TemplateValidate.input(req.body, 'new'); diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts index 79c0769..a39bc50 100644 --- a/src/routes/user.spec.ts +++ b/src/routes/user.spec.ts @@ -302,7 +302,7 @@ describe('/user', () => { auth: {basic: 'admin'}, httpStatus: 400, req: {pass: 'password'}, - res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$)[a-zA-Z0-9!"#%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{|}~]{8,}$/'} + res: {status: 'Invalid body format', details: 'password must have at least 8 characters, one uppercase and one lowercase character, one number and at least one of the following characters: !\"\\#%&\'()*+,-.\\/:;<=>?@[]^_`\u0000|}~'} }); }); it('rejects requests from non-admins for another user', done => { @@ -584,7 +584,7 @@ describe('/user', () => { auth: {basic: 'admin'}, httpStatus: 400, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}, - res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$)[a-zA-Z0-9!"#%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{|}~]{8,}$/'} + res: {status: 'Invalid body format', details: 'password must have at least 8 characters, one uppercase and one lowercase character, one number and at least one of the following characters: !\"\\#%&\'()*+,-.\\/:;<=>?@[]^_`\u0000|}~'} }); }); it('rejects requests from non-admins', done => { diff --git a/src/routes/validate/id.ts b/src/routes/validate/id.ts index 6b7b677..f640ccf 100644 --- a/src/routes/validate/id.ts +++ b/src/routes/validate/id.ts @@ -1,7 +1,7 @@ import Joi from '@hapi/joi'; export default class IdValidate { - private static id = Joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24); + private static id = Joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24).messages({'string.pattern.base': 'Invalid object id'}); static get () { // return joi validation return this.id; diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts index 969ac43..215ca90 100644 --- a/src/routes/validate/material.ts +++ b/src/routes/validate/material.ts @@ -13,31 +13,13 @@ export default class MaterialValidate { // validate input for material group: Joi.string() .max(128), - mineral: Joi.number() - .integer() - .min(0) - .max(100), - - glass_fiber: Joi.number() - .integer() - .min(0) - .max(100), - - carbon_fiber: Joi.number() - .integer() - .min(0) - .max(100), + properties: Joi.object(), numbers: Joi.array() - .items(Joi.object({ - color: Joi.string() - .max(128) - .required(), - number: Joi.string() - .max(128) - .allow('') - .required() - })) + .items( + Joi.string() + .length(10) + ) }; static input (data, param) { // validate input, set param to 'new' to make all attributes required @@ -46,9 +28,7 @@ export default class MaterialValidate { // validate input for material name: this.material.name.required(), supplier: this.material.supplier.required(), group: this.material.group.required(), - mineral: this.material.mineral.required(), - glass_fiber: this.material.glass_fiber.required(), - carbon_fiber: this.material.carbon_fiber.required(), + properties: this.material.properties.required(), numbers: this.material.numbers.required() }).validate(data); } @@ -57,9 +37,7 @@ export default class MaterialValidate { // validate input for material name: this.material.name, supplier: this.material.supplier, group: this.material.group, - mineral: this.material.mineral, - glass_fiber: this.material.glass_fiber, - carbon_fiber: this.material.carbon_fiber, + properties: this.material.properties, numbers: this.material.numbers }).validate(data); } @@ -77,9 +55,7 @@ export default class MaterialValidate { // validate input for material name: this.material.name, supplier: this.material.supplier, group: this.material.group, - mineral: this.material.mineral, - glass_fiber: this.material.glass_fiber, - carbon_fiber: this.material.carbon_fiber, + properties: this.material.properties, numbers: this.material.numbers }).validate(data, {stripUnknown: true}); return error !== undefined? null : value; @@ -101,9 +77,7 @@ export default class MaterialValidate { // validate input for material name: this.material.name, supplier: this.material.supplier, group: this.material.group, - mineral: this.material.mineral, - glass_fiber: this.material.glass_fiber, - carbon_fiber: this.material.carbon_fiber, + properties: this.material.properties, numbers: this.material.numbers }); } diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index 3fb28d9..b420e08 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -195,7 +195,6 @@ export default class SampleValidate { validator = Joi.object(this.sample); } const {value, error} = validator.validate({[field]: e}); - console.log(value); if (error) throw error; // reject invalid values // TODO: return exact error description, handle in frontend filters return value[field]; }); @@ -212,10 +211,10 @@ export default class SampleValidate { 'page-size': Joi.number().integer().min(1), sort: Joi.string().pattern(new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm')).default('_id-asc'), csv: Joi.boolean().default(false), - fields: Joi.array().items(Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm'))).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']), + fields: Joi.array().items(Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm'))).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']).messages({'string.pattern.base': 'Invalid field name'}), filters: Joi.array().items(Joi.object({ mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin'), - field: Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm')), + field: Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm')).messages({'string.pattern.base': 'Invalid filter field name'}), values: Joi.array().items(Joi.alternatives().try(Joi.string().max(128), Joi.number(), Joi.boolean(), Joi.date().iso())).min(1) })).default([]) }).with('to-page', 'page-size').validate(data); diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts index ae9426a..0721bd7 100644 --- a/src/routes/validate/template.ts +++ b/src/routes/validate/template.ts @@ -15,8 +15,10 @@ export default class TemplateValidate { Joi.object({ name: Joi.string() .max(128) - .invalid('condition_template') - .required(), + .invalid('condition_template', 'material_template') + .pattern(/^[^.]+$/) + .required() + .messages({'string.pattern.base': 'name must not contain a dot'}), range: Joi.object({ values: Joi.array() diff --git a/src/routes/validate/user.ts b/src/routes/validate/user.ts index 9c0c7d1..639132f 100644 --- a/src/routes/validate/user.ts +++ b/src/routes/validate/user.ts @@ -8,7 +8,8 @@ export default class UserValidate { // validate input for user name: Joi.string() .lowercase() .pattern(new RegExp('^[a-z0-9-_.]+$')) - .max(128), + .max(128) + .messages({'string.pattern.base': 'name must only contain a-z0-9_.'}), email: Joi.string() .email({minDomainSegments: 2}) @@ -17,7 +18,8 @@ export default class UserValidate { // validate input for user pass: Joi.string() .pattern(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&'()*+,-.\/:;<=>?@[\]^_`{|}~])(?=\S+$)[a-zA-Z0-9!"#%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]{8,}$/) - .max(128), + .max(128) + .messages({'string.pattern.base': 'password must have at least 8 characters, one uppercase and one lowercase character, one number and at least one of the following characters: !"\\#%&\'()*+,-.\\/:;<=>?@[]^_`\\{|}~'}), level: Joi.string() .valid(...globals.levels), diff --git a/src/test/db.json b/src/test/db.json index 99ae417..5c8a626 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -150,18 +150,15 @@ "name": "Stanyl TW 200 F8", "supplier_id": {"$oid":"110000000000000000000001"}, "group_id": {"$oid":"900000000000000000000001"}, - "mineral": 0, - "glass_fiber": 40, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 40, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "black", - "number": "5514263423" - }, - { - "color": "natural", - "number": "5514263422" - } + "5514263423", + "5514263422" ], "status": 10, "__v": 0 @@ -171,18 +168,15 @@ "name": "Ultramid T KR 4355 G7", "supplier_id": {"$oid":"110000000000000000000002"}, "group_id": {"$oid":"900000000000000000000002"}, - "mineral": 0, - "glass_fiber": 35, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 35, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "black", - "number": "5514212901" - }, - { - "color": "signalviolet", - "number": "5514612901" - } + "5514212901", + "5514612901" ], "status": 10, "__v": 0 @@ -192,9 +186,12 @@ "name": "PA GF 50 black (2706)", "supplier_id": {"$oid":"110000000000000000000003"}, "group_id": {"$oid":"900000000000000000000003"}, - "mineral": 0, - "glass_fiber": 0, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 0, + "carbon_fiber": 0 + }, "numbers": [ ], "status": 10, @@ -205,14 +202,14 @@ "name": "Schulamid 66 GF 25 H", "supplier_id": {"$oid":"110000000000000000000004"}, "group_id": {"$oid":"900000000000000000000004"}, - "mineral": 0, - "glass_fiber": 25, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 25, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "black", - "number": "5513933405" - } + "5513933405" ], "status": 10, "__v": 0 @@ -222,14 +219,14 @@ "name": "Amodel A 1133 HS", "supplier_id": {"$oid":"110000000000000000000005"}, "group_id": {"$oid":"900000000000000000000005"}, - "mineral": 0, - "glass_fiber": 33, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 33, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "black", - "number": "5514262406" - } + "5514262406" ], "status": 10, "__v": 0 @@ -239,14 +236,14 @@ "name": "PK-HM natural (4773)", "supplier_id": {"$oid":"110000000000000000000003"}, "group_id": {"$oid":"900000000000000000000006"}, - "mineral": 0, - "glass_fiber": 0, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 0, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "natural", - "number": "10000000" - } + "1000000000" ], "status": -1, "__v": 0 @@ -256,14 +253,13 @@ "name": "Ultramid A4H", "supplier_id": {"$oid":"110000000000000000000002"}, "group_id": {"$oid":"900000000000000000000004"}, - "mineral": 0, - "glass_fiber": 0, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 0, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "black", - "number": "" - } ], "status": 0, "__v": 0 @@ -273,17 +269,47 @@ "name": "Latamid 66 H 2 G 30", "supplier_id": {"$oid":"110000000000000000000006"}, "group_id": {"$oid":"900000000000000000000004"}, - "mineral": 0, - "glass_fiber": 30, - "carbon_fiber": 0, + "properties": { + "material_template": {"$oid": "130000000000000000000003"}, + "mineral": 0, + "glass_fiber": 30, + "carbon_fiber": 0 + }, "numbers": [ - { - "color": "blue", - "number": "5513943509" - } + "5513943509" ], "status": -1, "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000009"}, + "name": "Glue 1", + "supplier_id": {"$oid":"110000000000000000000002"}, + "group_id": {"$oid":"900000000000000000000007"}, + "properties": { + "material_template": {"$oid": "130000000000000000000002"}, + "stickiness": "medium" + }, + "numbers": [ + "5513943509" + ], + "status": 0, + "__v": 0 + }, + { + "_id": {"$oid":"100000000000000000000010"}, + "name": "Latamid 66 G 40", + "supplier_id": {"$oid":"110000000000000000000006"}, + "group_id": {"$oid":"900000000000000000000004"}, + "properties": { + "material_template": {"$oid": "130000000000000000000001"}, + "glass_fiber": 40 + }, + "numbers": [ + "5513943509" + ], + "status": 0, + "__v": 0 } ], "material_groups": [ @@ -316,6 +342,11 @@ "_id": {"$oid":"900000000000000000000006"}, "name": "PK", "__v": 0 + }, + { + "_id": {"$oid":"900000000000000000000007"}, + "name": "Fabric glue", + "__v": 0 } ], "material_suppliers": [ @@ -565,6 +596,69 @@ "__v": 0 } ], + "material_templates": [ + { + "_id": {"$oid":"130000000000000000000001"}, + "first_id": {"$oid":"130000000000000000000001"}, + "name": "plastic", + "version": 1, + "parameters": [ + { + "name": "glass_fiber", + "range": { + "min": 0, + "max": 100 + } + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"130000000000000000000002"}, + "first_id": {"$oid":"130000000000000000000002"}, + "name": "glue", + "version": 1, + "parameters": [ + { + "name": "stickiness", + "range": { + "values": ["not so sticky", "medium", "very sticky"] + } + } + ], + "__v": 0 + }, + { + "_id": {"$oid":"130000000000000000000003"}, + "first_id": {"$oid":"130000000000000000000001"}, + "name": "plastic", + "version": 2, + "parameters": [ + { + "name": "glass_fiber", + "range": { + "min": 0, + "max": 100 + } + }, + { + "name": "carbon_fiber", + "range": { + "min": 0, + "max": 100 + } + }, + { + "name": "mineral", + "range": { + "min": 0, + "max": 100 + } + } + ], + "__v": 0 + } + ], "users": [ { "_id": {"$oid":"000000000000000000000001"},