diff --git a/api/parameters.yaml b/api/parameters.yaml index 3cbe49b..67ac778 100644 --- a/api/parameters.yaml +++ b/api/parameters.yaml @@ -6,6 +6,14 @@ Id: type: string example: 5ea0450ed851c30a90e70894 +Number: + name: number + in: path + required: true + schema: + type: string + example: Rng740 + Name: name: name description: has to be URL encoded @@ -15,10 +23,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/sample.yaml b/api/sample.yaml index 2b0ce31..82d6c7c 100644 --- a/api/sample.yaml +++ b/api/sample.yaml @@ -140,10 +140,10 @@ application/json: schema: $ref: 'api.yaml#/components/schemas/SampleDetail' - 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: @@ -201,6 +201,31 @@ 500: $ref: 'api.yaml#/components/responses/500' +/sample/number/{number}: + parameters: + - $ref: 'api.yaml#/components/parameters/Number' + get: + summary: sample details + description: 'Auth: all, levels: read, write, maintain, dev, admin
Returns validated as well as new measurements' + x-doc: deleted samples are available only for maintain/admin + tags: + - /sample + responses: + 200: + description: samples details + content: + application/json: + schema: + $ref: 'api.yaml#/components/schemas/SampleDetail' + 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' + /sample/restore/{id}: parameters: - $ref: 'api.yaml#/components/parameters/Id' diff --git a/api/schemas.yaml b/api/schemas.yaml index 99f7998..1c844bb 100644 --- a/api/schemas.yaml +++ b/api/schemas.yaml @@ -92,7 +92,12 @@ SampleDetail: sample_references: type: array items: - $ref: 'api.yaml#/components/schemas/Id' + properties: + sample_id: + $ref: 'api.yaml#/components/schemas/Id' + relation: + type: string + example: part to this sample measurements: type: array items: @@ -115,25 +120,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: + material_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/data_import/import.js b/data_import/import.js index dc8c8d8..8fac949 100644 --- a/data_import/import.js +++ b/data_import/import.js @@ -5,75 +5,131 @@ const {Builder} = require('selenium-webdriver'); // selenium and the chrome d const chrome = require('selenium-webdriver/chrome'); const pdfReader = require('pdfreader'); const iconv = require('iconv-lite'); +const _ = require('lodash'); -const metaDoc = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\metadata.csv'; // metadata files -const kfDoc = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\kf.csv'; -const vzDoc = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\vz.csv'; -const nmDocs = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\nmDocs'; // NormMaster Documents -const dptFiles = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\DPT'; // Spectrum files +const stages = { + materials: true, + samples: true, + dpt: true +} + +const docs = [ + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata__AnP2.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata__AnP2_A.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata__AnP2_B.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Ap.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Bj.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Eh.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Eh_B.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Eh_Duroplasten.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Rng_aktuell.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Rng_aktuell_A.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_Rng_aktuell_B.csv", + "C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\Metadata_WaP.csv", +]; +const errors = []; +const nmDocs = 'C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\nmDocs'; // NormMaster Documents +const dptFiles = 'C:\\Users\\vle2fe\\Documents\\Data\\All_200717\\DPT'; // Spectrum files const host = 'http://localhost:3000'; // const host = 'https://definma-api.apps.de1.bosch-iot-cloud.com'; +const requiredProperties = ['samplenumber','materialnumber','materialname','supplier','reinforcementmaterial','material','granulate/part','color','charge/batch','comments']; +dict = { // dictionary + 'Granulat': 'granulate', + 'Zugstab': 'tension rod', + 'Stecker': 'plug' +}; let data = []; // metadata contents let materials = {}; +let numberToColor = {}; let samples = []; let normMaster = {}; let sampleDevices = {}; +const sampleReferences = []; // references to other samples in format {sample, referencedSample, relation} +let commentsLog = []; +let customFieldsLog = []; +const vzValues = {}; // vz values from comments +const dptLog = []; -// TODO: BASF twice, BASF as color -// TODO: duplicate kf values // TODO: conditions -// TODO: comment and reference handling - - -// TODO: check last color errors (filter out already taken) use location and device for user, upload to BIC main(); async function main() { - if (0) { // materials + if (stages.materials) { // materials await getNormMaster(); - await importCsv(metaDoc); - await allMaterials(); - await saveMaterials(); - await importCsv(kfDoc); - await allMaterials(); - await saveMaterials(); - await importCsv(vzDoc); - await allMaterials(); - await saveMaterials(); + for (let i in docs) { + await importCsv(docs[i]); + await allMaterials(); + await saveMaterials(); + } + fs.writeFileSync('./data_import/numberToColor.json', JSON.stringify(numberToColor)); } - if (0) { // samples + if (stages.samples) { // samples sampleDeviceMap(); - if (1) { - console.log('-------- META ----------'); - await importCsv(metaDoc); - await allSamples(); - await saveSamples(); - } - if (1) { - console.log('-------- KF ----------'); - await importCsv(kfDoc); - await allSamples(); - await saveSamples(); - await allKfVz(); - } - if (1) { - console.log('-------- VZ ----------'); - await importCsv(vzDoc); + numberToColor = JSON.parse(fs.readFileSync('./data_import/numberToColor.json'), 'utf-8'); + for (let i in docs) { + await importCsv(docs[i]); await allSamples(); await saveSamples(); await allKfVz(); } + // write logs + fs.writeFileSync('./data_import/comments.txt', commentsLog.join('\r\n')); + fs.writeFileSync('./data_import/customFields.txt', customFieldsLog.join('\r\n')); + fs.writeFileSync('./data_import/sampleReferences.txt', sampleReferences.map(e => JSON.stringify(e)).join('\r\n')); + fs.writeFileSync('./data_import/sampleReferences.json', JSON.stringify(sampleReferences)); + + await sampleReferencesSave(); } - if (1) { // DPT + if (stages.dpt) { // DPT await allDpts(); + fs.writeFileSync('./data_import/sdptLog.txt', dptLog.join('\r\n')); } if (0) { // pdf test - console.log(await readPdf('N28_BN05-OX013_2016-03-11.pdf')); + console.log(await readPdf('N28_BN05-OX023_2019-07-16.pdf')); + } + if (errors.length) { + // console.log(errors); + fs.writeFileSync('./data_import/errors/errors_' + new Date().getTime() + '.txt', errors.join('\r\n')); } } async function importCsv(doc) { + // Uniform name samplenumber materialnumber materialname supplier material plastic reinforcingmaterial granulate/part color charge/batch comments vz(ml/g) kfingew% degradation(%) glassfibrecontent(%) stabwn + // Metadata__AnP2.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, granulate/Part,Color,Charge/ Batch, Comments + // Metadata__AnP2_A.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material, Granulate/Part, Comments, Humidity [ppm] + // Metadata__AnP2_B.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material, Granulate/Part, VZ [ml/g], glass fibre content + // Metadata_Ap.csv Sample number,Material number,Material name,Supplier, Plastic,Reinforcing material, Granulate/Part,Color,Charge/Batch, Comments + // Metadata_Bj.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, Granulate/Part,Color,Charge/batch granulate/part,Comments + // Metadata_Eh.csv Sample number,Material number,Material name,Supplier,Material, Reinforcing material, Granulate/Part,Color,Charge/Batch granulate/part,Comments, VZ [cm³/g], Spalte1 + // Metadata_Eh_B.csv Sample number, Material name,Supplier, Plastic,Reinforcing material, Granulate/Part,Color, Comments, VZ [cm³/g] + // Metadata_Eh_Duroplasten.csv Sample number,Material number,Material name,Supplier,Material, Reinforcing material, Granulate/Part,Color,Charge/Batch granulate/part,Comments + // Metadata_Rng_aktuell.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, Granulate/Part,Color,Charge/batch granulate/part,Comments, VZ (ml/g), Degradation(%),Glas fibre content (%) + // Metadata_Rng_aktuell_A.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, Granulate/Part,Farbe,Charge/batch granulate/part,Comments, KF in Gew%, Stabwn + // Metadata_Rng_aktuell_B.csv Sample number, Material name,Supplier, Plastic,Reinforcing material (content in %),Granulate/Part, Comments, VZ (ml/g), Degradation (%), Alterungszeit in h + // Metadata_WaP.csv Probennummer, Name, Firma, Material, Teil/Rohstoff, Charge, Anmerkung,VZ (ml/g), Abbau (%), Verstärkungsstoffgehalt (%), Versuchsnummer + const nameCorrection = { // map to right column names + 'probennummer': 'samplenumber', + 'name': 'materialname', + 'firma': 'supplier', + 'reinforcingmaterial(contentin%)': 'reinforcingmaterial', + 'teil/rohstoff': 'granulate/part', + 'charge/batchgranulate/part': 'charge/batch', + 'charge': 'charge/batch', + 'anmerkung': 'comments', + 'vz[ml/g]': 'vz(ml/g)', + 'vz[cm³/g]': 'vz(ml/g)', + 'abbau(%)': 'degradation(%)', + 'verstärkungsstoffgehalt(%)': 'glassfibrecontent(%)' + }; + const missingFieldsFill = [ // column names to fill if they do not exist + 'color', + 'charge/batch', + 'comments', + 'materialnumber', + 'reinforcementmaterial' + ] + console.log('importing ' + doc); data = []; await new Promise(resolve => { fs.createReadStream(doc) @@ -83,10 +139,47 @@ async function importCsv(doc) { data.push(row); }) .on('end', () => { + data = data.map(e => { + const newE = {}; + Object.keys(e).forEach(key => { + newE[key.toLowerCase().replace(/ /g, '')] = e[key]; + }); + // replace wrong column names + Object.keys(newE).forEach(key => { + if (nameCorrection.hasOwnProperty(key)) { + newE[nameCorrection[key]] = newE[key]; + delete newE[key]; + } + }); + + // add missing fields with empty values + missingFieldsFill.forEach(field => { + if (!newE.hasOwnProperty(field)) { + newE[field] = ''; + } + }); + if (newE['supplier'] === '') { // empty supplier fields + newE['supplier'] = 'unknown'; + } + if (!newE.hasOwnProperty('material')) { + newE['material'] = newE['plastic'].indexOf(' GF') >= 0 ? newE['plastic'].split(' ')[0] : newE['plastic']; + } + return newE; + }).filter(e => { + const missingProperties = requiredProperties.filter(el => !e.hasOwnProperty(el)); + if (e['materialname'] === '') { + missingProperties.push('materialname'); + } + if (e['samplenumber'] === '') { // empty row + return false; + } + else if (missingProperties.length > 0) { // incomplete sample + errors.push(`${doc}: ${JSON.stringify(e)}is missing the required properties ${missingProperties}`); + return false; + } + return true; + }); console.info('CSV file successfully processed'); - if (data[0]['Farbe']) { // fix German column names - data.map(e => {e['Color'] = e['Farbe']; return e; }); - } resolve(); }); }); @@ -114,18 +207,20 @@ async function allDpts() { res.data.forEach(sample => { sampleIds[sample.number] = sample._id; }); - const dptRegex = /.*?_(.*?)_(\d+|\d+_\d+).DPT/; + const dptRegex = /(.*?)_(.*?)_(\d+|\d+_\d+).DPT/; const dpts = fs.readdirSync(dptFiles); for (let i in dpts) { const regexRes = dptRegex.exec(dpts[i]) - if (regexRes && sampleIds[regexRes[1]]) { // found matching sample - console.log(dpts[i]); + if (regexRes && sampleIds[regexRes[2]]) { // found matching sample + console.log(`${dpts[i]} -> ${regexRes[2]}`); + dptLog.push(`${dpts[i]}, ${regexRes[2]}`); const f = fs.readFileSync(dptFiles + '\\' + dpts[i], 'utf-8'); const data = { - sample_id: sampleIds[regexRes[1]], + sample_id: sampleIds[regexRes[2]], values: {}, measurement_template }; + data.values.device = regexRes[1]; data.values.dpt = f.split('\r\n').map(e => e.split(',')); let rescale = false; for (let i in data.values.dpt) { @@ -147,11 +242,19 @@ async function allDpts() { data }).catch(err => { console.log(dpts[i]); - console.error(err.response.data); + if (err.response) { + console.error(err.response.data); + errors.push(`Could not upload ${dpts[i]} for sample ${regexRes[2]}: ${err.response.data}`); + } + else { + console.error(err); + errors.push(`Could not upload ${dpts[i]} for sample ${regexRes[2]}: ${JSON.stringify(err)}`); + } }); } else { - console.log(`Could not find sample for ${dpts[i]} !!!!!!`); + console.log(`Could not find sample for ${dpts[i]}`); + errors.push(`Could not find sample for ${dpts[i]}`); } } } @@ -180,54 +283,57 @@ async function allKfVz() { sampleIds[sample.number] = sample._id; }); for (let index in data) { - console.info(`${index}/${data.length}`); + console.info(`KF/VZ ${index}/${data.length}`); let sample = data[index]; - if (sample['Sample number'] !== '') { - let credentials = ['admin', 'Abc123!#']; - if (sampleDevices[sample['Sample number']]) { - credentials = [sampleDevices[sample['Sample number']], '2020DeFinMachen!'] - } - if (sample['KF in Gew%']) { - await axios({ - method: 'post', - url: host + '/measurement/new', - auth: { - username: credentials[0], - password: credentials[1] - }, - data: { - sample_id: sampleIds[sample['Sample number']], - measurement_template: kf_template, - values: { - 'weight %': sample['KF in Gew%'], - 'standard deviation': sample['Stabwn'] - } + let credentials = ['admin', 'Abc123!#']; + if (sampleDevices[sample['samplenumber']]) { + credentials = [sampleDevices[sample['samplenumber']], '2020DeFinMachen!'] + } + if (!sample['vz(ml/g)'] && vzValues[sample['samplenumber']]) { // fill in VZ values from comments + sample['vz(ml/g)'] = vzValues[sample['samplenumber']]; + } + if (sample['kfingew%']) { + await axios({ + method: 'post', + url: host + '/measurement/new', + auth: { + username: credentials[0], + password: credentials[1] + }, + data: { + sample_id: sampleIds[sample['samplenumber']], + measurement_template: kf_template, + values: { + 'weight %': sample['kfingew%'], + 'standard deviation': sample['stabwn'] } - }).catch(err => { - console.log(sample['Sample number']); - console.error(err.response.data); - }); - } - if (sample['VZ (ml/g)']) { - await axios({ - method: 'post', - url: host + '/measurement/new', - auth: { - username: credentials[0], - password: credentials[1] - }, - data: { - sample_id: sampleIds[sample['Sample number']], - measurement_template: vz_template, - values: { - vz: sample['VZ (ml/g)'] - } + } + }).catch(err => { + console.log(sample['samplenumber']); + console.error(err.response.data); + errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); + }); + } + if (sample['VZ (ml/g)']) { + await axios({ + method: 'post', + url: host + '/measurement/new', + auth: { + username: credentials[0], + password: credentials[1] + }, + data: { + sample_id: sampleIds[sample['samplenumber']], + measurement_template: vz_template, + values: { + vz: sample['vz(ml/g)'] } - }).catch(err => { - console.log(sample['Sample number']); - console.error(err.response.data); - }); - } + } + }).catch(err => { + console.log(sample['samplenumber']); + console.error(err.response.data); + errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); + }); } } } @@ -244,6 +350,7 @@ async function allSamples() { }); const dbMaterials = {} res.data.forEach(m => { + m.numbers = m.numbers.map(e => ({number: e, color: numberToColor[e]})); dbMaterials[m.name] = m; }) res = await axios({ @@ -261,59 +368,71 @@ async function allSamples() { for (let index in data) { - console.info(`${index}/${data.length}`); + console.info(`SAMPLE LOAD ${index}/${data.length}`); let sample = data[index]; - if (sample['Sample number'] !== '') { // TODO: what about samples without color - if (sample['Supplier'] === '') { // empty supplier fields - sample['Supplier'] = 'unknown'; + if (sample['granulate/Part'] === '') { // empty supplier fields + sample['granulate/Part'] = 'unknown'; + } + const material = dbMaterials[trim(sample['materialname'])]; + if (!material) { // could not find material, skipping sample + errors.push(`Could not find a material for ${JSON.stringify(sample)}`); + continue; + } + samples.push({ + number: sample['samplenumber'], + type: sampleType(sample['granulate/part']), + batch: sample['charge/batch'], + material_id: material._id, + notes: { + custom_fields: customFields(sample['comments'], sample['samplenumber']) } - if (sample['Granulate/Part'] === '') { // empty supplier fields - sample['Granulate/Part'] = 'unknown'; - } - const material = dbMaterials[trim(sample['Material name'])]; - if (!material) { // could not find material, skipping sample - continue; - } - console.log(sample['Material name']); - console.log(material._id); - samples.push({ - number: sample['Sample number'], - type: sample['Granulate/Part'], - batch: sample['Charge/batch granulate/part'] || '', - material_id: material._id, - notes: { - comment: sample['Comments'] + }); + // if (sample['comments']) { + // comments.push(sample['samplenumber'] + ' ' + sample['comments']); + // } + const si = samples.length - 1; // sample index + if (samples[si].notes.custom_fields.hasOwnProperty('xRest')) { // reroute xRest property to comment + samples[si].notes.comment = samples[si].notes.custom_fields.xRest; + commentsLog.push(sample['samplenumber'] + ' ' + samples[si].notes.comment); + delete samples[si].notes.custom_fields.xRest; + } + if (Object.keys(samples[si].notes.custom_fields).length === 0) { // delete empty custom fields + delete samples[si].notes.custom_fields; + } + else { + customFieldsLog.push(sample['samplenumber'] + ' ' + JSON.stringify(samples[si].notes.custom_fields)); + } + if (sample['materialnumber'] !== '' && material.numbers.find(e => e.number === sample['materialnumber'])) { + samples[si].color = material.numbers.find(e => e.number === sample['materialnumber']).color; + } + else if (sample['color'] !== '') { // find color with all edge cases + let number = material.numbers.find(e => e.color && e.color.indexOf(trim(sample['color'])) >= 0); + if (!number && /black/.test(sample['color'])) { // special case bk for black + console.log(material); + number = material.numbers.find(e => e.color && e.color.toLowerCase().indexOf('bk') >= 0); + if (!number) { // try German word + number = material.numbers.find(e => e.color && e.color.toLowerCase().indexOf('schwarz') >= 0); } - }); - const si = samples.length - 1; - if (sample['Material number'] !== '' && material.numbers.find(e => e.number === sample['Material number'])) { // TODO: fix because of false material/material number - samples[si].color = material.numbers.find(e => e.number === sample['Material number']).color; } - else if (sample['Color'] && sample['Color'] !== '') { - let number = material.numbers.find(e => e.color.indexOf(trim(sample['Color'])) >= 0); - if (!number && /black/.test(sample['Color'])) { // special case bk for black - number = material.numbers.find(e => e.color.toLowerCase().indexOf('bk') >= 0); - if (!number) { // try German word - number = material.numbers.find(e => e.color.toLowerCase().indexOf('schwarz') >= 0); - } - } + if (number) { samples[si].color = number.color; } - else if (sampleColors[sample['Sample number'].split('_')[0]]) { // derive color from main sample for kf/vz - samples[si].color = sampleColors[sample['Sample number'].split('_')[0]]; - } - else { - samples[si].color = ''; - } + } + else if (sampleColors[sample['samplenumber'].split('_')[0]]) { // derive color from main sample for kf/vz + samples[si].color = sampleColors[sample['samplenumber'].split('_')[0]]; + } + if (!samples[si].color) { + samples[si].color = sample['color']; } } } async function saveSamples() { for (let i in samples) { - console.info(`${i}/${samples.length}`); + console.info(`SAMPLE SAVE ${i}/${samples.length}`); let credentials = ['admin', 'Abc123!#']; if (sampleDevices[samples[i].number]) { + console.log(sampleDevices[samples[i].number]); credentials = [sampleDevices[samples[i].number], '2020DeFinMachen!'] } await axios({ @@ -328,68 +447,140 @@ async function saveSamples() { if (err.response.data.status && err.response.data.status !== 'Sample number already taken') { console.log(samples[i]); console.error(err.response.data); + errors.push(`Upload for ${JSON.stringify(samples[i])} failed: ${JSON.stringify(err.response.data)}`); } }); } console.info('saved all samples'); } +async function sampleReferencesSave() { + for (let i in sampleReferences) { + console.info(`SAMPLE REFERENCES ${i}/${sampleReferences.length}`); + let refRes = await axios({ + method: 'get', + url: host + '/sample/number/' + sampleReferences[i].referencedSample, + auth: { + username: 'admin', + password: 'Abc123!#' + } + }).catch(err => { + console.log(sampleReferences[i].referencedSample); + console.error(err.response.data); + errors.push(`Getting reference id for ${JSON.stringify(sampleReferences[i])} failed: ${JSON.stringify(err.response.data)}`); + }); + if (!refRes) continue; + let sampleRes = await axios({ + method: 'get', + url: host + '/sample/number/' + sampleReferences[i].sample, + auth: { + username: 'admin', + password: 'Abc123!#' + } + }).catch(err => { + console.log(sampleReferences[i].sample); + console.error(err.response.data); + errors.push(`Getting sample id for ${JSON.stringify(sampleReferences[i])} failed: ${JSON.stringify(err.response.data)}`); + }); + if (!sampleRes) continue; + sampleRes.data.notes.sample_references.push({sample_id: refRes.data._id, relation: sampleReferences[i].relation}) + await axios({ + method: 'put', + url: host + '/sample/' + sampleRes.data._id, + auth: { + username: 'admin', + password: 'Abc123!#' + }, + data: {notes: {sample_references: sampleRes.data.notes.sample_references}} + }).catch(err => { + console.log(sampleRes.data.notes.sample_references); + if (err.response.data) { + console.error(err.response.data); + errors.push(`Saving references for ${JSON.stringify(sampleRes.data)} failed: ${JSON.stringify(err.response.data)}`); + } + else { + console.error(err.response); + errors.push(`Saving references for ${JSON.stringify(sampleRes.data)} failed: ${JSON.stringify(err.response)}`); + } + }); + } +} + async function allMaterials() { - materials = {}; + // materials = {}; + let res = await axios({ + method: 'get', + url: host + '/template/materials', + auth: { + username: 'admin', + password: 'Abc123!#' + } + }); + const materialTemplate = res.data.find(e => e.name === 'plastic')._id; + + // process all samples for (let index in data) { let sample = data[index]; - if (sample['Sample number'] && sample['Sample number'] !== '') { - if (sample['Supplier'] === '') { // empty supplier fields - sample['Supplier'] = 'unknown'; - } - if (sample['Material name'] === '') { // empty name fields - sample['Material name'] = sample['Material']; - } - if (!sample['Material']) { // column Material is named Plastic in VZ metadata - sample['Material'] = sample['Plastic']; - } - sample['Material name'] = trim(sample['Material name']); - if (materials.hasOwnProperty(sample['Material name'])) { // material already found at least once - if (sample['Material number'] && sample['Material number'] !== '') { - if (materials[sample['Material name']].numbers.length === 0 || !materials[sample['Material name']].numbers.find(e => e.number === stripSpaces(sample['Material number']))) { // new material number - if (materials[sample['Material name']].numbers.find(e => e.color === sample['Color'] && e.number === '')) { // color already in list, only number missing - materials[sample['Material name']].numbers.find(e => e.color === sample['Color'] && e.number === '').number = stripSpaces(sample['Material number']); - } - else { - materials[sample['Material name']].numbers.push({color: trim(sample['Color']), number: stripSpaces(sample['Material number'])}); - } + if (sample['supplier'] === '') { // empty supplier fields + sample['supplier'] = 'unknown'; + } + if (sample['materialname'] === '') { // empty name fields + sample['materialname'] = sample['material']; + } + sample['materialname'] = trim(sample['materialname']); + if (materials.hasOwnProperty(sample['materialname'])) { // material already found at least once + if (sample['materialnumber'] !== '') { // material number given + if (materials[sample['materialname']].numbers.length === 0 || !materials[sample['materialname']].numbers.find(e => e.number === stripSpaces(sample['materialnumber']))) { // new material number + if (materials[sample['materialname']].numbers.find(e => e.color === sample['color'] && e.number === '')) { // color already in list, only number missing + materials[sample['materialname']].numbers.find(e => e.color === sample['color'] && e.number === '').number = stripSpaces(sample['materialnumber']); } - } - else if (sample['Color'] && sample['Color'] !== '') { - if (!materials[sample['Material name']].numbers.find(e => e.color === stripSpaces(sample['Color']))) { // new material color - materials[sample['Material name']].numbers.push({color: trim(sample['Color']), number: ''}); + else { // completely new number entry + materials[sample['materialname']].numbers.push({color: trim(sample['color']), number: stripSpaces(sample['materialnumber'])}); } } } - else { // new material - console.info(`${index}/${data.length} ${sample['Material name']}`); - materials[sample['Material name']] = { - name: sample['Material name'], - supplier: trim(sample['Supplier']), - group: trim(sample['Material']) - }; - let tmp = /M(\d+)/.exec(sample['Reinforcing material']); - materials[sample['Material name']].mineral = tmp ? tmp[1] : 0; - tmp = /GF(\d+)/.exec(sample['Reinforcing material']); - materials[sample['Material name']].glass_fiber = tmp ? tmp[1] : 0; - tmp = /CF(\d+)/.exec(sample['Reinforcing material']); - materials[sample['Material name']].carbon_fiber = tmp ? tmp[1] : 0; - materials[sample['Material name']].numbers = await numbersFetch(sample); - console.log(materials[sample['Material name']]); + else if (sample['color'] !== '') { // color given + if (!materials[sample['materialname']].numbers.find(e => e.color === stripSpaces(sample['color']))) { // new material color + materials[sample['materialname']].numbers.push({color: trim(sample['color']), number: ''}); + } } } + else { // new material + console.info(`MATERIAL LOAD ${index}/${data.length} ${sample['materialname']}`); + materials[sample['materialname']] = { + name: trim(sample['materialname']), + supplier: trim(sample['supplier']), + group: trim(sample['material']) + }; + materials[sample['materialname']].numbers = await numbersFetch(sample); + + // material properties + materials[sample['materialname']].properties = {material_template: materialTemplate}; + let tmp = /M(\d+)/.exec(sample['reinforcingmaterial']); + materials[sample['materialname']].properties.mineral = tmp ? tmp[1] : 0; + tmp = /GF(\d+)/.exec(sample['reinforcingmaterial']); + materials[sample['materialname']].properties.glass_fiber = tmp ? tmp[1] : 0; + tmp = /CF(\d+)/.exec(sample['reinforcingmaterial']); + materials[sample['materialname']].properties.carbon_fiber = tmp ? tmp[1] : 0; + } } + + // Fill numberToColor array + Object.keys(materials).forEach(mKey => { + materials[mKey].numbers.forEach(number => { + if (number.number && number.color) { + numberToColor[number.number] = number.color; + } + }) + }); } async function saveMaterials() { const mKeys = Object.keys(materials) for (let i in mKeys) { - console.info(`${i}/${mKeys.length}`); + console.info(`MATERIAL SAVE ${i}/${mKeys.length}`); + const material = _.cloneDeep(materials[mKeys[i]]); + material.numbers = material.numbers.map(e => e.number).filter(e => e !== '').map(e => e.replace(/ /g, '')); await axios({ method: 'post', url: host + '/material/new', @@ -397,11 +588,12 @@ async function saveMaterials() { username: 'admin', password: 'Abc123!#' }, - data: materials[mKeys[i]] + data: material }).catch(err => { if (err.response.data.status && err.response.data.status !== 'Material name already taken') { - console.info(materials[mKeys[i]]); + console.info(material); console.error(err.response.data); + errors.push(`Upload for ${JSON.stringify(material)} failed: ${JSON.stringify(err.response.data)}`) } }); } @@ -411,17 +603,17 @@ async function saveMaterials() { async function numbersFetch(sample) { let nm = []; let res = []; - if (sample['Material number']) { // sample has a material number - nm = normMaster[stripSpaces(sample['Material number'])]? [normMaster[stripSpaces(sample['Material number'])]] : []; + if (sample['materialnumber']) { // sample has a material number + nm = normMaster[stripSpaces(sample['materialnumber'])]? [normMaster[stripSpaces(sample['materialnumber'])]] : []; } else { // try finding via material name - nm = Object.keys(normMaster).filter(e => normMaster[e].nameSpaceless === stripSpaces(sample['Material name'])).map(e => normMaster[e]); + nm = Object.keys(normMaster).filter(e => normMaster[e].nameSpaceless === stripSpaces(sample['materialnumber'])).map(e => normMaster[e]); } if (nm.length > 0) { for (let i in nm) { - // if (!fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document not loaded - // await getNormMasterDoc(nm[i].url.replace(/ /g, '%20')); - // } + if (!fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document not loaded + await getNormMasterDoc(nm[i].url.replace(/ /g, '%20')); + } // if (!fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document not loaded // console.info('Retrying download...'); // await getNormMasterDoc(nm[i].url.replace(/ /g, '%20'), 2.2); @@ -437,21 +629,22 @@ async function numbersFetch(sample) { break; } else if (i + 1 >= nm.length) { - console.error('Download failed!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!'); + errors.push(`Download of ${nm[i].url.replace(/ /g, '%20')} for material number ${sample['materialnumber']} failed`); + errors.push(nm[i].doc.replace(/ /g, '_')); } } } if (res.length === 0) { // no results - if ((sample['Color'] && sample['Color'] !== '') || (sample['Material number'] &&sample['Material number'] !== '')) { - return [{color: trim(sample['Color']), number: sample['Material number']}]; + if (sample['color'] !== '' || sample['materialnumber'] !== '') { // information in data available + return [{color: trim(sample['color']), number: sample['materialnumber']}]; } else { return []; } } else { - if (sample['Material number'] && !res.find(e => e.number === sample['Material number'])) { // sometimes norm master does not include sample number even if listed - res.push({color: trim(sample['Color']), number: sample['Material number']}); + if (!res.find(e => e.number === sample['materialnumber'])) { // sometimes norm master does not include sample number even if listed + res.push({color: trim(sample['color']), number: sample['materialnumber']}); } return res; } @@ -521,7 +714,7 @@ function readPdf(file) { let lastLastText = ''; // text of last last item await new pdfReader.PdfReader().parseFileItems(nmDocs + '\\' + file, (err, item) => { if (item && item.text) { - if ((stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignationsupplier') >= 0) || (stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignatiomsupplier') >= 0)) { // table area starts + if ((stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignationsuppl') >= 0) || (stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignatiomsupplier') >= 0)) { // table area starts table = countdown; } if (table > 0) { @@ -570,6 +763,107 @@ function sampleDeviceMap() { } } +function customFields (comment, sampleNumber) { + const customFields = [ + {docKey: 'Versuchsreihe', dbKey: 'test series', regex: /Versuchsreihe (\d+),/, category: 'customField'}, + {docKey: 'Stillstand', dbKey: 'idle', regex: /Stillstand (\d+ min):/, category: 'customField'}, + {docKey: 'Serienzyklus', dbKey: 'cycle', regex: /(\d+.) Serienzyklus (\(.*?\))/, category: 'customField'}, + {docKey: 'Berstdruck', dbKey: 'bursting pressure', regex: /Berstdruck: (.*?bar);/, category: 'customField'}, + {docKey: 'gemessen am', dbKey: 'measured at', regex: /gemessen am (.*20\d\d)/, category: 'customField'}, + {docKey: 'used for', dbKey: 'used for', regex: /used for (.*)/, category: 'customField'}, + {docKey: 'Stabilized', dbKey: 'stabilized', regex: /Stabilized, (.*)/, category: 'customField'}, + {docKey: 'parts from field', dbKey: 'parts from field', regex: null, category: 'customField'}, + {docKey: 'side', dbKey: 'side', regex: /(\S*?) side/, category: 'customField'}, + {docKey: 'Creep test', dbKey: 'creep test', regex: null, category: 'customField'}, + {docKey: 'Variante', dbKey: 'variant', regex: /(.*)/, category: 'customField'}, + {docKey: 'Parameter', dbKey: 'parameter', regex: /Parameter (\d)/, category: 'customField'}, + {docKey: 'days without cooling', dbKey: 'days without cooling', regex: /(\d+) days without cooling/, category: 'customField'}, + {docKey: 'Zyklus', dbKey: 'cycle', regex: /Zyklus (\d+ s)/, category: 'customField'}, + {docKey: 'fast cure', dbKey: 'fast cure', regex: null, category: 'customField'}, + {docKey: 'Stoff gesperrt', dbKey: 'material blocked', regex: null, category: 'customField'}, + {docKey: 'anwendungsbeschränkt', dbKey: 'limited application', regex: null, category: 'customField'}, + {docKey: 'für Neuanwendungen gesperrt', dbKey: 'blocked for new applications', regex: null, category: 'customField'}, + {docKey: 'V', dbKey: 'test', regex: /V(\d+-\d+);/, category: 'customField'}, + {docKey: 'Twz', dbKey: 'twz', regex: /Twz \(°C\): (\d+);/, category: 'customField'}, + {docKey: 'Pnach', dbKey: 'pressure after', regex: /Pnach \(bar\): (\d+);/, category: 'customField'}, + {docKey: 'Vein', dbKey: 'volume in', regex: /Vein \(ccm\/s\): (\d+)/, category: 'customField'}, + {docKey: 'low emission', dbKey: 'low emission', regex: /low emission (\S+)[;]?/, category: 'customField'}, + {docKey: 'aus', dbKey: 'from', regex: /aus (.*)/, category: 'customField'}, + {docKey: 'Erprobung', dbKey: 'trial', regex: /Erprobung (.*?);/, category: 'customField'}, + {docKey: 'Auftragsnummer', dbKey: 'job number', regex: /Auftragsnummer: (\S+)[;]?/, category: 'customField'}, + {docKey: 'Wärmealterung', dbKey: 'heat aging', regex: /Wärmealterung: (.*)/, category: 'customField'}, + {docKey: 'A: Wandung außen / I: Wandung innen / S: Wandung Steg', dbKey: 'outer wall', regex: /Steg.*?A: (\d+)/, category: 'customField'}, + {docKey: 'A: Wandung außen / I: Wandung innen / S: Wandung Steg', dbKey: 'inner wall', regex: /Steg.*?I: (\d+)/, category: 'customField'}, + {docKey: 'A: Wandung außen / I: Wandung innen / S: Wandung Steg', dbKey: 'support wall', regex: /Steg.*?S: (\d+)/, category: 'customField'}, + {docKey: 'A: Wandung außen / I: Wandung innen / S: Wandung Steg', dbKey: 'outer wall degraded', regex: /Degradation:.*?A: (\d+)/, category: 'customField'}, + {docKey: 'A: Wandung außen / I: Wandung innen / S: Wandung Steg', dbKey: 'inner wall degraded', regex: /Degradation:.*?I: (\d+)/, category: 'customField'}, + {docKey: 'A: Wandung außen / I: Wandung innen / S: Wandung Steg', dbKey: 'support wall degraded', regex: /Degradation:.*?S: (\d+)/, category: 'customField'}, + {docKey: 'Reines Polymer', dbKey: 'pure polymer', regex: null, category: 'customField'}, + {docKey: 'Rücksendung erforderlich', dbKey: 'return needed', regex: /(.*?,) Rücksendung erforderlich, (.*)/, category: 'customField'}, + {docKey: 'Prio', dbKey: 'priority', regex: /Prio (\d+)/, category: 'customField'}, + {docKey: 'beanstandet', dbKey: 'faulty', regex: null, category: 'customField'}, + {docKey: 'aged', dbKey: 'aged', regex: /aged: (.*)/, category: 'customField'}, + {docKey: 'DOPPELT!!', dbKey: 'double', regex: null, category: 'customField'}, + {docKey: 'Bauteil', dbKey: 'construction part', regex: /Bauteil (\S+)/, category: 'customField'}, + {docKey: 'T =', dbKey: 'temperature', regex: /T = (\S+)/, category: 'customField'}, + {docKey: 'nicht vorgealtert', dbKey: 'not preaged', regex: /nicht vorgealtert (.*)/, category: 'customField'}, + {docKey: 'TS119', dbKey: 'TS119', regex: /TS119 (W\S+);/, category: 'customField'}, + {docKey: 'GF vom Datenblatt', dbKey: 'glass fibre from data sheet', regex: null, category: 'customField'}, + {docKey: 'nach Datensatz', dbKey: 'according to dataset', regex: null, category: 'customField'}, + {docKey: 'Dosiergeschw', dbKey: 'metering speed', regex: /Dosiergeschw.*? (.*?min)/, category: 'customField'}, + {docKey: 'Einspritzgeschw', dbKey: 'injection speed', regex: /Einspritzgeschw.*? (.*\/s)/, category: 'customField'}, + {docKey: 'Heizbänder', dbKey: 'heating lines', regex: /Heizbänder (.*)/, category: 'customField'}, + {docKey: 'Verweilzeit', dbKey: 'dwell time', regex: /Verweilzeit (.*?min)/, category: 'customField'}, + {docKey: 'Probe', dbKey: 'belongs to', regex: /Probe (\S*\d+)/, category: 'reference'}, + {docKey: 'zu', dbKey: 'belongs to', regex: /zu (\S*\d+)/, category: 'reference'}, + {docKey: 'granulate zu', dbKey: 'granulate to', regex: /granulate zu.* (\S*\d+)/, category: 'reference'}, + {docKey: 'construction part', dbKey: 'construction part', regex: /(? { + if (comment.indexOf(cField.docKey) >= 0) { // comment contains docKey + if (cField.regex !== null) { + const regexRes = cField.regex.exec(comment); + if (regexRes) { + usedParts.push(regexRes[0]); + if (cField.category === 'reference') { + sampleReferences.push({sample: sampleNumber, referencedSample: regexRes[1], relation: cField.dbKey}); + } + else if (cField.category === 'vz') { + vzValues[sampleNumber] = regexRes[1]; + } + else { + res[cField.dbKey] = regexRes.filter((e, i) => i > 0).join(' '); + } + } + } + else { + usedParts.push(cField.docKey); + res[cField.dbKey] = true; + } + } + }); + usedParts.forEach(part => { + const index = comment.indexOf(part); + if (index >= 0) { + comment = comment.slice(0, index) + comment.slice(index + part.length); + } + }); + if (/\w+/.test(comment)) { + res.xRest = comment; + } + return res; +} + +function sampleType (type) { + const allowedTypes = ['tension rod', 'part', 'granulate']; + return allowedTypes.indexOf(type) >= 0 ? type : (type === '' ? 'unknown' : 'other'); +} + function stripSpaces(s) { return s ? s.replace(/ /g,'') : ''; } diff --git a/src/helpers/csv.ts b/src/helpers/csv.ts index 38c487a..e6f07b2 100644 --- a/src/helpers/csv.ts +++ b/src/helpers/csv.ts @@ -13,11 +13,16 @@ function flatten (data) { // flatten object: {a: {b: true}} -> {a.b: true} result[prop] = cur; } else if (Array.isArray(cur)) { - let l = 0; - for(let i = 0, l = cur.length; i < l; i++) - recurse(cur[i], prop + "[" + i + "]"); - if (l == 0) - result[prop] = []; + 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; 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..54a49ab 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,37 @@ 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} = ParametersValidate.input(_.omit(properties, 'material_template'), materialData.parameters, param); + if (error) {res400(error, res); return false;} + Object.keys(value).forEach(key => { + properties[key] = value[key]; + }); + 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..ca62d16 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; @@ -201,7 +200,7 @@ describe('/sample', () => { }).end((err, res) => { if (err) return done(err); should(res.body[0]).have.property('number', 'Rng36'); - should(res.body[1]).have.property('number', '33'); + should(res.body[1]).have.property('number', '34'); should(res.body[res.body.length - 1]).have.property('number', '1'); done(); }); @@ -215,7 +214,7 @@ describe('/sample', () => { }).end((err, res) => { if (err) return done(err); should(res.body[0]).have.property('_id', '400000000000000000000006'); - should(res.body[1]).have.property('_id', '400000000000000000000002'); + should(res.body[1]).have.property('_id', '400000000000000000000007'); done(); }); }); @@ -227,7 +226,7 @@ describe('/sample', () => { httpStatus: 200 }).end((err, res) => { if (err) return done(err); - should(res.body[0]).have.property('_id', '400000000000000000000002'); + should(res.body[0]).have.property('_id', '400000000000000000000007'); should(res.body[1]).have.property('_id', '400000000000000000000006'); done(); }); @@ -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(); }); }); @@ -335,19 +334,51 @@ describe('/sample', () => { done(); }); }); - it('filters multiple properties', done => { + it('filters by a measurement properties property', 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[]=material.name&fields[]=material.properties.glass_fiber&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.properties.glass_fiber%22%2C%22values%22%3A%5B%2225%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(2); + should(res.body).matchEach(sample => { + should(sample.material.properties.glass_fiber).be.eql(25); + }); done(); }); - }); // TODO: do measurement pipeline, check if it works with UI + }); + it('filters and sorts by a measurement properties property', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&sort=material.properties.glass_fiber-desc&fields[]=number&fields[]=material.name&fields[]=material.properties.glass_fiber&filters[]=%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.properties.glass_fiber%22%2C%22values%22%3A%5B%2225%22%5D%7D', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + should(res.body).have.lengthOf(2); + should(res.body[0].number).be.eql('Rng36'); + should(res.body[1].number).be.eql('1'); + should(res.body).matchEach(sample => { + should(sample.material.properties.glass_fiber).be.eql(25); + }); + done(); + }); + }); + it('filters multiple properties', done => { + TestHelper.request(server, done, { + method: 'get', + 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(4); + should(res.body[0]).be.eql({number: '1', batch: ''}); + done(); + }); + }); it('rejects an invalid JSON string as a filters parameter', done => { TestHelper.request(server, done, { method: 'get', @@ -360,10 +391,10 @@ describe('/sample', () => { it('rejects an invalid filter mode', done => { TestHelper.request(server, done, { method: 'get', - url: '/samples?status=all&fields[]=number&fields[]=material.glass_fiber&fields[]=batch&filters[]=%7B%22mode%22%3A%22xx%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%22xx%22%2C%22field%22%3A%22batch%22%2C%22values%22%3A%5B%221704-005%22%5D%7D', auth: {basic: 'janedoe'}, httpStatus: 400, - res: {status: 'Invalid body format', details: '"filters[0].mode" must be one of [eq, ne, lt, lte, gt, gte, in, nin]'} + res: {status: 'Invalid body format', details: '"filters[0].mode" must be one of [eq, ne, lt, lte, gt, gte, in, nin, stringin]'} }); }); it('rejects an filter field not existing', done => { @@ -372,7 +403,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 +433,29 @@ 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('returns specified material properties fields', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/samples?status=all&fields[]=number&fields[]=material.properties.glass_fiber&fields[]=material.name', + auth: {basic: 'janedoe'}, + httpStatus: 200 + }).end((err, res) => { + if (err) return done(err); + const json = require('../test/db.json'); + should(res.body).matchEach(sample => { + const materialId = json.collections.samples.find(e => e.number === sample.number).material_id; + const material = json.collections.materials.find(e => e._id.toString() == materialId); + should(sample).have.only.keys('number', 'material'); + should(sample.material.name).be.eql(material.name); + should(sample.material.properties.glass_fiber).be.eql(material.properties.glass_fiber); + }); + done() }); }); it('rejects a from-id not in the database', done => { @@ -432,7 +482,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 +500,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 +669,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 +678,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 +687,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 +945,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 +982,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 +1084,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', @@ -1313,6 +1343,67 @@ describe('/sample', () => { }); }); + describe('GET /sample/number/{number}', () => { + it('returns the right sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/33', + 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', 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 => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/33', + 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', 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 => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/Rng33', + 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', 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 => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/Rng33', + auth: {basic: 'janedoe'}, + httpStatus: 403 + }); + }); + it('returns 404 for an unknown sample', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/Rng883', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects an invalid id', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/xx-xx', + auth: {basic: 'janedoe'}, + httpStatus: 404 + }); + }); + it('rejects unauthorized requests', done => { + TestHelper.request(server, done, { + method: 'get', + url: '/sample/number/33', + httpStatus: 401 + }); + }); + }); + describe('PUT /sample/restore/{id}', () => { it('sets the status', done => { TestHelper.request(server, done, { @@ -1642,16 +1733,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 +1934,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..e468a41 100644 --- a/src/routes/sample.ts +++ b/src/routes/sample.ts @@ -22,11 +22,13 @@ import csv from '../helpers/csv'; const router = express.Router(); // TODO: check added filter -// TODO: return total number of pages -> use facet // TODO: use query pointer // 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; @@ -240,6 +242,13 @@ router.get('/samples', async (req, res, next) => { && e !== filters.sort[0] // field was not in sort ); + if (fieldsToAdd.find(e => e === 'notes')) { // add notes + queryPtr.push( + {$lookup: {from: 'notes', localField: 'note_id', foreignField: '_id', as: 'notes'}}, + {$addFields: {notes: { $arrayElemAt: ['$notes', 0]}}} + ); + } + if (fieldsToAdd.find(e => /material\./.test(e)) && !materialAdded) { // add material, was not added already queryPtr.push( {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, @@ -311,7 +320,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; @@ -385,26 +394,7 @@ router.get('/sample/' + IdValidate.parameter(), (req, res, next) => { SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => { if (err) return next(err); - - if (sampleData) { - await sampleData.populate('material_id.group_id').populate('material_id.supplier_id').execPopulate().catch(err => next(err)); - if (sampleData instanceof Error) return; - sampleData = sampleData.toObject(); - - if (sampleData.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin - sampleData.material = sampleData.material_id; // map data to right keys - sampleData.material.group = sampleData.material.group_id.name; - sampleData.material.supplier = sampleData.material.supplier_id.name; - sampleData.user = sampleData.user_id.name; - sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; - MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id), status: {$ne: globals.status.deleted}}).lean().exec((err, data) => { - sampleData.measurements = data; - res.json(SampleValidate.output(sampleData, 'details')); - }); - } - else { - res.status(404).json({status: 'Not found'}); - } + await sampleReturn(sampleData, req, res, next); }); }); @@ -425,14 +415,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; } @@ -514,6 +502,15 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { }); }); +router.get('/sample/number/:number', (req, res, next) => { + if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; + + SampleModel.findOne({number: req.params.number}).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => { + if (err) return next(err); + await sampleReturn(sampleData, req, res, next); + }); +}); + router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => { if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; @@ -615,11 +612,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 +644,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,11 +754,13 @@ 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}; } + else if (e.mode === 'stringin') { + return {[e.field]: {['$in']: [new RegExp(e.values[0])]}}; + } else { return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}}; // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin } @@ -777,4 +769,27 @@ function filterQueries (filters) { function dateToOId (date) { // convert date to ObjectId return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000'); +} + +async function sampleReturn (sampleData, req, res, next) { + if (sampleData) { + console.log(sampleData); + await sampleData.populate('material_id.group_id').populate('material_id.supplier_id').execPopulate().catch(err => next(err)); + if (sampleData instanceof Error) return; + sampleData = sampleData.toObject(); + + if (sampleData.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin + sampleData.material = sampleData.material_id; // map data to right keys + sampleData.material.group = sampleData.material.group_id.name; + sampleData.material.supplier = sampleData.material.supplier_id.name; + sampleData.user = sampleData.user_id.name; + sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; + MeasurementModel.find({sample_id: sampleData._id, status: {$ne: globals.status.deleted}}).lean().exec((err, data) => { + sampleData.measurements = data; + res.json(SampleValidate.output(sampleData, 'details')); + }); + } + else { + res.status(404).json({status: 'Not found'}); + } } \ No newline at end of file diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts index cd90108..b07014b 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.material_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..20f1b3b 100644 --- a/src/routes/template.ts +++ b/src/routes/template.ts @@ -4,6 +4,7 @@ import _ from 'lodash'; import TemplateValidate from './validate/template'; import ConditionTemplateModel from '../models/condition_template'; import MeasurementTemplateModel from '../models/measurement_template'; +import MaterialTemplateModel from '../models/material_template'; import res400 from './validate/res400'; import IdValidate from './validate/id'; import mongoose from "mongoose"; @@ -13,7 +14,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 +24,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 +38,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 +63,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'); @@ -82,5 +83,9 @@ router.post('/template/:collection(measurement|condition)/new', async (req, res, module.exports = router; function model (req) { // return right template model - return req.params.collection === 'condition' ? ConditionTemplateModel : MeasurementTemplateModel; + switch (req.params.collection) { + case 'condition': return ConditionTemplateModel + case 'measurement': return MeasurementTemplateModel + case 'material': return MaterialTemplateModel + } } \ No newline at end of file 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..74214d5 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() + .max(64) + ) }; 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/parameters.ts b/src/routes/validate/parameters.ts index e6070b0..61b48d3 100644 --- a/src/routes/validate/parameters.ts +++ b/src/routes/validate/parameters.ts @@ -10,6 +10,7 @@ export default class ParametersValidate { .valid(...parameter.range.values); } else if (parameter.range.hasOwnProperty('min') && parameter.range.hasOwnProperty('max')) { + joiObject[parameter.name] = Joi.number() .min(parameter.range.min) .max(parameter.range.max); diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts index 3fb28d9..3e9aed3 100644 --- a/src/routes/validate/sample.ts +++ b/src/routes/validate/sample.ts @@ -62,23 +62,22 @@ export default class SampleValidate { 'material.name', 'material.supplier', 'material.group', - 'material.mineral', - 'material.glass_fiber', - 'material.carbon_fiber', 'material.number', + 'material.properties.*', 'measurements.(?!spectrum)*' ]; private static fieldKeys = [ ...SampleValidate.sortKeys, 'condition', + 'notes', 'material_id', 'material', 'note_id', 'user_id', 'material._id', 'material.numbers', - 'measurements.spectrum.dpt' + 'measurements.spectrum.dpt', ]; static input (data, param) { // validate input, set param to 'new' to make all attributes required @@ -136,6 +135,7 @@ export default class SampleValidate { material_id: IdValidate.get(), material: MaterialValidate.outputV().append({number: Joi.string().max(128).allow('')}), note_id: IdValidate.get().allow(null), + notes: this.sample.notes, user_id: IdValidate.get(), added: this.sample.added }; @@ -175,8 +175,8 @@ export default class SampleValidate { let validator; let field = data.filters[i].field if (/material\./.test(field)) { // select right validation model - validator = MaterialValidate.outputV().append({number: Joi.string().max(128).allow('')}); - field = field.replace('material.', ''); + validator = MaterialValidate.outputV().append({number: Joi.string().max(128).allow(''), properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128))}); + field = field.replace('material.', '').split('.')[0]; } else if (/measurements\./.test(field)) { validator = Joi.object({ @@ -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,11 +211,11 @@ 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')), - values: Joi.array().items(Joi.alternatives().try(Joi.string().max(128), Joi.number(), Joi.boolean(), Joi.date().iso())).min(1) + mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'stringin'), + 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(), Joi.object())).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..7b0fab9 100644 --- a/src/test/db.json +++ b/src/test/db.json @@ -95,6 +95,19 @@ "user_id": {"$oid":"000000000000000000000002"}, "status": 0, "__v": 0 + }, + { + "_id": {"$oid":"400000000000000000000007"}, + "number": "34", + "type": "liquid", + "color": "black", + "batch": "", + "condition": {}, + "material_id": {"$oid":"100000000000000000000009"}, + "note_id": null, + "user_id": {"$oid":"000000000000000000000002"}, + "status": 0, + "__v": 0 } ], "notes": [ @@ -150,18 +163,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 +181,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 +199,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 +215,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 +232,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 +249,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 +266,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 +282,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 +355,11 @@ "_id": {"$oid":"900000000000000000000006"}, "name": "PK", "__v": 0 + }, + { + "_id": {"$oid":"900000000000000000000007"}, + "name": "Fabric glue", + "__v": 0 } ], "material_suppliers": [ @@ -565,6 +609,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"},