Archived
2

Merge pull request #21 in ~VLE2FE/dfop-api from develop to master

* commit '5abad59a0e80cf8f543d9ee2af10922c35ea5d10':
  improved mail service
  removed maintain user, constrained spctra access
  fixed testing cache
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-08-06 11:28:55 +02:00
commit 93a7a92667
33 changed files with 877 additions and 425 deletions

View File

@ -1,12 +1,48 @@
<component name="ProjectDictionaryState"> <component name="ProjectDictionaryState">
<dictionary name="VLE2FE"> <dictionary name="VLE2FE">
<words> <words>
<w>akro</w>
<w>amodel</w>
<w>anwendungsbeschränkt</w>
<w>batchgranulate</w>
<w>bcrypt</w> <w>bcrypt</w>
<w>bnpd</w>
<w>cfenv</w> <w>cfenv</w>
<w>colordesignatiomsuppl</w>
<w>colordesignationsuppl</w>
<w>contentin</w>
<w>definma</w>
<w>dfopdb</w> <w>dfopdb</w>
<w>dosiergeschw</w>
<w>dpts</w>
<w>einspritzgeschw</w>
<w>frameguard</w>
<w>functionlink</w>
<w>glassfibrecontent</w>
<w>isin</w>
<w>janedoe</w> <w>janedoe</w>
<w>johnnydoe</w>
<w>kfingew</w>
<w>latamid</w>
<w>lati</w>
<w>lyucy</w>
<w>materialnumber</w>
<w>pagesize</w> <w>pagesize</w>
<w>pnach</w>
<w>preaged</w>
<w>reinforcementmaterial</w>
<w>reinforcingmaterial</w>
<w>samplenumber</w>
<w>sdpt</w>
<w>signalviolet</w>
<w>solvay</w>
<w>spaceless</w>
<w>stabwn</w>
<w>stanyl</w>
<w>stringin</w>
<w>testcomment</w> <w>testcomment</w>
<w>ultramid</w>
<w>vorgealtert</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>

View File

@ -2,6 +2,7 @@
<profile version="1.0"> <profile version="1.0">
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="LongLine" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="ReservedWordUsedAsNameJS" enabled="false" level="WARNING" enabled_by_default="false" /> <inspection_tool class="ReservedWordUsedAsNameJS" enabled="false" level="WARNING" enabled_by_default="false" />
</profile> </profile>
</component> </component>

View File

@ -5,28 +5,35 @@ info:
title: Digital fingerprint of plastics - API title: Digital fingerprint of plastics - API
version: 1.0.0 version: 1.0.0
description: | description: |
This API gives access to the project database.<br> This **API** gives access to the project database.
Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password. Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password.
Data access methods can also be accessed using an API key at the URL ending like ?key=xxx<br> Data access methods can also be accessed using an API key at the URL ending like ?key=xxx
The description lists available authentication methods, also the locks of each method close correspondingly The description lists available authentication methods, also the locks of each method close correspondingly
if the entered authentication is allowed.<br><br> if the entered authentication is allowed.
There are a number of different user levels: <br>
<ul>
<li>read: read access to the samples database</li> There are a number of different user levels:
<li>write: write access to the samples database, users can change only the values they created</li>
<li>maintain: functions like changing templates, validating data, changing values of others</li> | | read sample data | add samples/edit own | read spectral data | edit other's data | maintain templates | edit users |
<li>dev: handling machine learning models</li> |:-----:|:----------------:|:--------------------:|:------------------:|:-----------------:|:------------------:|:----------:|
<li>admin: user administration</li> | read | yes | no | no | no | no | no |
</ul> | write | yes | yes | no | no | no | no |
| dev | yes | yes | yes | yes | yes | no |
| admin | yes | yes | yes | yes | yes | yes |
Password policy: Password policy:
<ul>
<li>at least one digit</li> - at least one digit
<li>at least one lower case letter</li> - at least one lower case letter
<li>at least one upper case letter</li> - at least one upper case letter
<li>at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~</li> - at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~
<li>no whitespace</li> - no whitespace
<li>at least 8 characters</li> - at least 8 characters
</ul>
<br>
x-doc: | x-doc: |
status: status:
<ul> <ul>
@ -34,8 +41,9 @@ info:
<li>0: newly added/changed</li> <li>0: newly added/changed</li>
<li>10: validated</li> <li>10: validated</li>
</ul> </ul>
<a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/dfop-api/">Bitbucket repository</a> <a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/dfop-api/">Bitbucket repository API</a>
# TODO: Link to new documentation page <a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/dfop-ui/">Bitbucket repository UI</a>
<a href="https://definma.apps.de1.bosch-iot-cloud.com/documentation">Documentation page</a>
servers: servers:

View File

@ -1,7 +1,7 @@
/materials: /materials:
get: get:
summary: lists all materials summary: lists all materials
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
x-doc: returns only materials with status 10 x-doc: returns only materials with status 10
tags: tags:
- /material - /material
@ -31,7 +31,7 @@
- $ref: 'api.yaml#/components/parameters/State' - $ref: 'api.yaml#/components/parameters/State'
get: get:
summary: lists all new/deleted materials summary: lists all new/deleted materials
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: returns materials with status 0/-1 x-doc: returns materials with status 0/-1
tags: tags:
- /material - /material
@ -54,8 +54,8 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
get: get:
summary: get material details summary: get material details
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
x-doc: deleted samples are available only for maintain/admin x-doc: deleted samples are available only for dev/admin
tags: tags:
- /material - /material
responses: responses:
@ -73,7 +73,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: change material summary: change material
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, dev, admin'
x-doc: status is reset to 0 on any changes, deleted samples cannot be changed x-doc: status is reset to 0 on any changes, deleted samples cannot be changed
tags: tags:
- /material - /material
@ -104,7 +104,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: delete material summary: delete material
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, dev, admin'
x-doc: sets status to -1 x-doc: sets status to -1
tags: tags:
- /material - /material
@ -129,7 +129,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
put: put:
summary: restore material summary: restore material
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: status is set to 0 x-doc: status is set to 0
tags: tags:
- /material - /material
@ -152,7 +152,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
put: put:
summary: restore material summary: restore material
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: status is set to 10 x-doc: status is set to 10
tags: tags:
- /material - /material
@ -173,7 +173,7 @@
/material/new: /material/new:
post: post:
summary: add material summary: add material
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, dev, admin'
x-doc: 'Adds status: 0 automatically' x-doc: 'Adds status: 0 automatically'
tags: tags:
- /material - /material
@ -204,7 +204,7 @@
/material/groups: /material/groups:
get: get:
summary: list all existing material groups summary: list all existing material groups
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
tags: tags:
- /material - /material
responses: responses:
@ -227,7 +227,7 @@
/material/suppliers: /material/suppliers:
get: get:
summary: list all existing material suppliers summary: list all existing material suppliers
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
tags: tags:
- /material - /material
responses: responses:

View File

@ -3,8 +3,8 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
get: get:
summary: measurement values by id summary: measurement values by id
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin'
x-doc: deleted samples are available only for maintain/admin x-doc: deleted samples are available only for dev/admin
tags: tags:
- /measurement - /measurement
responses: responses:
@ -24,7 +24,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: change measurement summary: change measurement
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, dev, admin'
x-doc: status is reset to 0 on any changes, deleted measurements cannot be edited x-doc: status is reset to 0 on any changes, deleted measurements cannot be edited
tags: tags:
- /measurement - /measurement
@ -57,7 +57,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: delete measurement summary: delete measurement
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, dev, admin'
x-doc: sets status to -1 x-doc: sets status to -1
tags: tags:
- /measurement - /measurement
@ -82,7 +82,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
put: put:
summary: restore measurement summary: restore measurement
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: status is set to 0 x-doc: status is set to 0
tags: tags:
- /measurement - /measurement
@ -105,7 +105,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
put: put:
summary: set measurement status to validated summary: set measurement status to validated
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: status is set to 10 x-doc: status is set to 10
tags: tags:
- /measurement - /measurement
@ -126,7 +126,7 @@
/measurement/new: /measurement/new:
post: post:
summary: add measurement summary: add measurement
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, dev, admin'
x-doc: 'Adds status: 0 automatically' x-doc: 'Adds status: 0 automatically'
tags: tags:
- /measurement - /measurement

View File

@ -21,7 +21,7 @@
/authorized: /authorized:
get: get:
summary: Checks authorization summary: Checks authorization
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
tags: tags:
- / - /
responses: responses:
@ -40,6 +40,8 @@
level: level:
type: string type: string
example: read example: read
user_id:
$ref: 'api.yaml#/components/schemas/Id'
401: 401:
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
500: 500:
@ -67,7 +69,9 @@
example: 30 example: 30
get: get:
summary: get changelog summary: get changelog
description: 'Auth: basic, levels: maintain, admin<br>Displays all logs older than timestamp, sorted by date descending, page defaults to 0, pagesize defaults to 25<br>Avoid using high page numbers for older logs, better use an older timestamp' description: 'Auth: basic, levels: dev, admin<br>Displays all logs older than timestamp, sorted by date descending,
page defaults to 0, pagesize defaults to 25<br>Avoid using high page numbers for older logs, better use an older
timestamp'
tags: tags:
- / - /
responses: responses:

View File

@ -1,8 +1,9 @@
/samples: /samples:
get: get:
summary: all samples in overview summary: all samples in overview
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin'
x-doc: 'Limitations: paging and csv output does not work when including the spectrum measurement fields as well as the returned number of total samples' x-doc: 'Limitations: paging and csv output does not work when including the spectrum measurement fields as well as
the returned number of total samples'
tags: tags:
- /sample - /sample
parameters: parameters:
@ -19,7 +20,8 @@
type: string type: string
example: 5ea0450ed851c30a90e70894 example: 5ea0450ed851c30a90e70894
- name: to-page - name: to-page
description: relative change of pages, use negative values to get back, defaults to 0, works only together with page-size description: 'relative change of pages, use negative values to get back, defaults to 0, works only together with
page-size'
in: query in: query
schema: schema:
type: string type: string
@ -43,7 +45,8 @@
type: boolean type: boolean
example: false example: false
- name: fields[] - name: fields[]
description: the fields to include in the output as array, defaults to ['_id','number','type','batch','material_id','color','condition','note_id','user_id','added'] description: "the fields to include in the output as array, defaults to ['_id', 'number', 'type',
'batch', 'material_id', 'color', 'condition', 'note_id', 'user_id', 'added']"
in: query in: query
schema: schema:
type: array type: array
@ -51,19 +54,23 @@
type: string type: string
example: ['number', 'batch'] example: ['number', 'batch']
- name: filters[] - name: filters[]
description: "the filters to apply as an array of URIComponent encoded objects in the form {mode: 'eq/ne/lt/lte/gt/gte/in/nin', field: 'material.m', values: ['15']} using encodeURIComponent(JSON.stringify({}))" description: "the filters to apply as an array of URIComponent encoded objects in the form {mode:
'eq/ne/lt/lte/gt/gte/in/nin', field: 'material.m', values: ['15']} using encodeURIComponent(JSON.stringify({}))"
in: query in: query
schema: schema:
type: array type: array
items: items:
type: string type: string
example: ["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D", "%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22%5D%7D"] example: '["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D",
"%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22
%5D%7D"]'
responses: responses:
200: 200:
description: samples overview (if the csv parameter is set, this is in CSV instead of JSON format) description: samples overview (if the csv parameter is set, this is in CSV instead of JSON format)
headers: headers:
x-total-items: x-total-items:
description: Total number of available items when from-id is not specified and spectrum field is not included description: Total number of available items when from-id is not specified and spectrum field is not
included
schema: schema:
type: integer type: integer
example: 243 example: 243
@ -87,7 +94,7 @@
- $ref: 'api.yaml#/components/parameters/State' - $ref: 'api.yaml#/components/parameters/State'
get: get:
summary: all new/deleted samples in overview summary: all new/deleted samples in overview
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: admin'
x-doc: returns only samples with status 0/-1 x-doc: returns only samples with status 0/-1
tags: tags:
- /sample - /sample
@ -108,7 +115,7 @@
/samples/count: /samples/count:
get: get:
summary: total number of samples summary: total number of samples
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
tags: tags:
- /sample - /sample
responses: responses:
@ -129,8 +136,9 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
get: get:
summary: sample details summary: sample details
description: 'Auth: all, levels: read, write, maintain, dev, admin<br>Returns validated as well as new measurements' description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin<br>
x-doc: deleted samples are available only for maintain/admin Returns validated as well as new measurements'
x-doc: deleted samples are available only for dev/admin
tags: tags:
- /sample - /sample
responses: responses:
@ -150,7 +158,8 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: change sample summary: change sample
description: 'Auth: basic, levels: write, maintain, dev, admin <br>Only maintain and admin are allowed to edit samples created by another user' description: 'Auth: basic, levels: write, dev, admin <br>
Only dev and admin are allowed to edit samples created by another user'
x-doc: status is reset to 0 on any changes, deleted samples cannot be changed x-doc: status is reset to 0 on any changes, deleted samples cannot be changed
tags: tags:
- /sample - /sample
@ -181,8 +190,10 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: delete sample summary: delete sample
description: 'Auth: basic, levels: write, maintain, dev, admin <br>Only maintain and admin are allowed to edit samples created by another user' description: 'Auth: basic, levels: write, dev, admin <br>
x-doc: sets status to -1, notes and references to this sample are also kept, only note_fields are updated accordingly Only dev and admin are allowed to edit samples created by another user'
x-doc: sets status to -1, notes and references to this sample are also kept, only note_fields are updated
accordingly
tags: tags:
- /sample - /sample
security: security:
@ -206,8 +217,9 @@
- $ref: 'api.yaml#/components/parameters/Number' - $ref: 'api.yaml#/components/parameters/Number'
get: get:
summary: sample details summary: sample details
description: 'Auth: all, levels: read, write, maintain, dev, admin<br>Returns validated as well as new measurements' description: 'Auth: all, levels: read, write, dev, admin, spectral data can only be accessed by dev and admin<br>
x-doc: deleted samples are available only for maintain/admin Returns validated as well as new measurements'
x-doc: deleted samples are available only for dev/admin
tags: tags:
- /sample - /sample
responses: responses:
@ -231,7 +243,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
put: put:
summary: restore sample summary: restore sample
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: status is set to 0 x-doc: status is set to 0
tags: tags:
- /sample - /sample
@ -254,7 +266,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
put: put:
summary: set sample status to validated summary: set sample status to validated
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: status is set to 10 x-doc: status is set to 10
tags: tags:
- /sample - /sample
@ -277,7 +289,8 @@
/sample/new: /sample/new:
post: post:
summary: add sample summary: add sample
description: 'Auth: basic, levels: write, maintain, dev, admin. Number property is only for admin when adding existing samples' description: 'Auth: basic, levels: write, dev, admin. Number property is only for admin when adding existing
samples'
x-doc: 'Adds status: 0 automatically' x-doc: 'Adds status: 0 automatically'
tags: tags:
- /sample - /sample
@ -313,7 +326,7 @@
/sample/notes/fields: /sample/notes/fields:
get: get:
summary: list all existing field names for custom notes fields summary: list all existing field names for custom notes fields
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, dev, admin'
x-doc: integrity has to be ensured x-doc: integrity has to be ensured
tags: tags:
- /sample - /sample

View File

@ -3,7 +3,7 @@
- $ref: 'api.yaml#/components/parameters/Collection' - $ref: 'api.yaml#/components/parameters/Collection'
get: get:
summary: all available templates summary: all available templates
description: 'Auth: basic, levels: read, write, maintain, dev, admin' description: 'Auth: basic, levels: read, write, dev, admin'
tags: tags:
- /template - /template
security: security:
@ -28,7 +28,7 @@
- $ref: 'api.yaml#/components/parameters/Id' - $ref: 'api.yaml#/components/parameters/Id'
get: get:
summary: template details summary: template details
description: 'Auth: basic, levels: read, write, maintain, admin' description: 'Auth: basic, levels: read, write, dev, admin'
tags: tags:
- /template - /template
security: security:
@ -48,7 +48,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: change template summary: change template
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
x-doc: With a change a new version is set, resulting in a new template with a new id x-doc: With a change a new version is set, resulting in a new template with a new id
tags: tags:
- /template - /template
@ -83,7 +83,7 @@
- $ref: 'api.yaml#/components/parameters/Collection' - $ref: 'api.yaml#/components/parameters/Collection'
post: post:
summary: add template summary: add template
description: 'Auth: basic, levels: maintain, admin' description: 'Auth: basic, levels: dev, admin'
tags: tags:
- /template - /template
security: security:

View File

@ -24,7 +24,7 @@
/user: /user:
get: get:
summary: list own user details summary: list own user details
description: 'Auth: basic, levels: read, write, maintain, admin' description: 'Auth: basic, levels: read, write, dev, admin'
tags: tags:
- /user - /user
security: security:
@ -44,7 +44,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: change user details summary: change user details
description: 'Auth: basic, levels: read, write, maintain, admin' description: 'Auth: basic, levels: read, write, dev, admin'
tags: tags:
- /user - /user
security: security:
@ -86,7 +86,7 @@
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: delete user summary: delete user
description: 'Auth: basic, levels: read, write, maintain, admin' description: 'Auth: basic, levels: read, write, dev, admin'
tags: tags:
- /user - /user
security: security:
@ -174,7 +174,7 @@
/user/key: /user/key:
get: get:
summary: get API key for the user summary: get API key for the user
description: 'Auth: basic, levels: read, write, maintain, dev, admin' description: 'Auth: basic, levels: read, write, dev, admin'
tags: tags:
- /user - /user
security: security:

View File

@ -47,8 +47,10 @@ let sampleDevices = {};
const sampleReferences = []; // references to other samples in format {sample, referencedSample, relation} const sampleReferences = []; // references to other samples in format {sample, referencedSample, relation}
let commentsLog = []; let commentsLog = [];
let customFieldsLog = []; let customFieldsLog = [];
const vzValues = {}; // vz values from comments const vnValues = {}; // vn values from comments
const dptLog = []; const dptLog = [];
const dptSampleAddLog = []; // log samples created during dpt insertion
const typeLog = [];
// TODO: conditions // TODO: conditions
@ -71,10 +73,11 @@ async function main() {
await importCsv(docs[i]); await importCsv(docs[i]);
await allSamples(); await allSamples();
await saveSamples(); await saveSamples();
await allKfVz(); await allMcVn();
} }
// write logs // write logs
fs.writeFileSync('./data_import/comments.txt', commentsLog.join('\r\n')); fs.writeFileSync('./data_import/comments.txt', commentsLog.join('\r\n'));
fs.writeFileSync('./data_import/typeLog.txt', typeLog.join('\r\n'));
fs.writeFileSync('./data_import/customFields.txt', customFieldsLog.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.txt', sampleReferences.map(e => JSON.stringify(e)).join('\r\n'));
fs.writeFileSync('./data_import/sampleReferences.json', JSON.stringify(sampleReferences)); fs.writeFileSync('./data_import/sampleReferences.json', JSON.stringify(sampleReferences));
@ -84,6 +87,7 @@ async function main() {
if (stages.dpt) { // DPT if (stages.dpt) { // DPT
await allDpts(); await allDpts();
fs.writeFileSync('./data_import/sdptLog.txt', dptLog.join('\r\n')); fs.writeFileSync('./data_import/sdptLog.txt', dptLog.join('\r\n'));
fs.writeFileSync('./data_import/dptSampleAddLog.txt', dptSampleAddLog.join('\r\n'));
} }
if (0) { // pdf test if (0) { // pdf test
console.log(await readPdf('N28_BN05-OX023_2019-07-16.pdf')); console.log(await readPdf('N28_BN05-OX023_2019-07-16.pdf'));
@ -95,24 +99,23 @@ async function main() {
} }
async function importCsv(doc) { 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 // Uniform name samplenumber materialnumber materialname supplier material plastic reinforcingmaterial granulate/part color charge/batch comments vz(ml/g) kfingew% degradation(%) reinforcingmaterialcontent stabwn
// Metadata__AnP2.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material, granulate/Part,Color,Charge/ Batch, Comments // 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_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__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_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_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.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_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_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.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_A.csv Sample number,Material number,Material name,Supplier,Material,Plastic,Reinforcing material,Granulate/Part,Farbe,Charge/batch granulate/part,Comments, KF in Gew%, Reinforcing material (content in %),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_Rng_aktuell_B.csv Sample number, Material name,Supplier, Plastic, 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 // 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 const nameCorrection = { // map to right column names
'probennummer': 'samplenumber', 'probennummer': 'samplenumber',
'name': 'materialname', 'name': 'materialname',
'firma': 'supplier', 'firma': 'supplier',
'reinforcingmaterial(contentin%)': 'reinforcingmaterial',
'teil/rohstoff': 'granulate/part', 'teil/rohstoff': 'granulate/part',
'charge/batchgranulate/part': 'charge/batch', 'charge/batchgranulate/part': 'charge/batch',
'charge': 'charge/batch', 'charge': 'charge/batch',
@ -120,7 +123,10 @@ async function importCsv(doc) {
'vz[ml/g]': 'vz(ml/g)', 'vz[ml/g]': 'vz(ml/g)',
'vz[cm³/g]': 'vz(ml/g)', 'vz[cm³/g]': 'vz(ml/g)',
'abbau(%)': 'degradation(%)', 'abbau(%)': 'degradation(%)',
'verstärkungsstoffgehalt(%)': 'glassfibrecontent(%)' 'glassfibrecontent': 'reinforcingmaterialcontent',
'glasfibrecontent(%)': 'reinforcingmaterialcontent',
'reinforcingmaterial(contentin%)': 'reinforcingmaterialcontent',
'verstärkungsstoffgehalt(%)': 'reinforcingmaterialcontent'
}; };
const missingFieldsFill = [ // column names to fill if they do not exist const missingFieldsFill = [ // column names to fill if they do not exist
'color', 'color',
@ -129,7 +135,7 @@ async function importCsv(doc) {
'materialnumber', 'materialnumber',
'reinforcementmaterial' 'reinforcementmaterial'
] ]
console.log('importing ' + doc); console.info('importing ' + doc);
data = []; data = [];
await new Promise(resolve => { await new Promise(resolve => {
fs.createReadStream(doc) fs.createReadStream(doc)
@ -158,9 +164,9 @@ async function importCsv(doc) {
newE[field] = ''; newE[field] = '';
} }
}); });
// if(newE['materialname'] === '') { // TODO: is this replacement okay? if(newE['materialname'] === '') {
// newE['materialname'] = newE['material']; newE['materialname'] = newE['material'];
// } }
if (newE['supplier'] === '') { // empty supplier fields if (newE['supplier'] === '') { // empty supplier fields
newE['supplier'] = 'unknown'; newE['supplier'] = 'unknown';
} }
@ -211,12 +217,69 @@ async function allDpts() {
res.data.forEach(sample => { res.data.forEach(sample => {
sampleIds[sample.number] = sample._id; sampleIds[sample.number] = sample._id;
}); });
const dptRegex = /(.*?)_(.*?)_(\d+|[a-zA-Z0-9]+_\d+).DPT/; const dptRegex = /(.*?)_(.*?)_(\d+|[a-zA-Z0-9]+[_.]\d+)(_JDX)?[.]{1,2}(DPT|csv|CSV|JDX)/;
const dpts = fs.readdirSync(dptFiles); const dpts = fs.readdirSync(dptFiles);
for (let i in dpts) { for (let i in dpts) {
const regexRes = dptRegex.exec(dpts[i]) let regexInput;
const bjRes = /^(Bj[FT]?)\s?([a-z0-9_]*)_JDX.DPT/.exec(dpts[i]);
if (bjRes) { // correct Bj numbers with space
regexInput = `Bj01_${bjRes[1]}${bjRes[2]}_0.DPT`;
}
else { // remove _JDX from name
regexInput = dpts[i].replace(/_JDX.*\./, '.');
}
const regexRes = dptRegex.exec(regexInput);
if (regexRes && !sampleIds[regexRes[2]]) { // when sample number includes an additional _x instead of having _x_x for spectrum description
regexRes[2] = `${regexRes[2]}_${regexRes[3].split('_')[0]}`;
}
let baseSample = null;
if (regexRes) {
baseSample = regexRes[2].split('_')[0];
if (baseSample === 'Wa11') { // as Wa11 samples use all the same material
baseSample = 'Wa11_B0_1';
}
}
if (regexRes && !sampleIds[regexRes[2]] && sampleIds[baseSample]) { // when number_abx does not exist but number
dptSampleAddLog.push(`Trying to find ${baseSample}`);
dptSampleAddLog.push(host + '/sample/' + sampleIds[baseSample]);
res = await axios({ // get base sample
method: 'get',
url: host + '/sample/' + stripSpaces(sampleIds[baseSample]),
auth: {
username: 'admin',
password: 'Abc123!#'
}
}).catch(err => {
if (err.response) {
console.error(err.response.data);
errors.push(`DPT Could not fetch sample ${baseSample}: ${JSON.stringify(err.response.data)}`);
}
});
if (res) {
const data = _.merge(_.pick(res.data, ['color', 'type', 'batch']),
{number: regexRes[2], condition: {}, notes: {}, material_id: res.data.material._id});
res = await axios({
method: 'post',
url: host + '/sample/new',
auth: {
username: res.data.user,
password: res.data.user === 'admin' ? 'Abc123!#' : '2020DeFinMachen!'
},
data
}).catch(err => {
if (err.response) {
console.error(err.response.data);
errors.push(`DPT Could not save sample ${data}: ${err.response.data}`);
}
});
if (res.data) {
dptSampleAddLog.push(`${regexRes[2]} from ${baseSample}`)
sampleIds[regexRes[2]] = res.data._id;
}
}
}
if (regexRes && sampleIds[regexRes[2]]) { // found matching sample if (regexRes && sampleIds[regexRes[2]]) { // found matching sample
console.log(`${dpts[i]} -> ${regexRes[2]}`); console.log(`${i}/${dpts.length} ${dpts[i]} -> ${regexRes[2]}`);
dptLog.push(`${dpts[i]}, ${regexRes[2]}`); dptLog.push(`${dpts[i]}, ${regexRes[2]}`);
const f = fs.readFileSync(dptFiles + '\\' + dpts[i], 'utf-8'); const f = fs.readFileSync(dptFiles + '\\' + dpts[i], 'utf-8');
const data = { const data = {
@ -225,10 +288,11 @@ async function allDpts() {
measurement_template measurement_template
}; };
data.values.device = regexRes[1]; data.values.device = regexRes[1];
data.values.dpt = f.split('\r\n').map(e => e.split(',')); data.values.filename = dpts[i];
data.values.dpt = f.split('\r\n').map(e => e.split(',').map(e => parseFloat(e)));
let rescale = false; let rescale = false;
for (let i in data.values.dpt) { for (let i in data.values.dpt) {
if (data.values.dpt[i][1] > 2) { if (data.values.dpt[i][1] > 10) {
rescale = true; rescale = true;
break; break;
} }
@ -258,12 +322,17 @@ async function allDpts() {
} }
else { 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]}`); if (regexRes) {
errors.push(`Could not find sample for ${dpts[i]}; [DEBUG] ${regexRes[2]}, ${!sampleIds[regexRes[2]]}, ${sampleIds[baseSample]}`);
}
else {
errors.push(`Could not find sample for ${dpts[i]} (did not match RegEx)`);
}
} }
} }
} }
async function allKfVz() { async function allMcVn() {
let res = await axios({ let res = await axios({
method: 'get', method: 'get',
url: host + '/template/measurements', url: host + '/template/measurements',
@ -272,8 +341,9 @@ async function allKfVz() {
password: 'Abc123!#' password: 'Abc123!#'
} }
}); });
const kf_template = res.data.filter(e => e.name === 'kf').sort((a, b) => b.version - a.version)[0]._id; const mc_template = res.data.filter(e => e.name === 'moisture content').sort((a, b) => b.version - a.version)[0]._id;
const vz_template = res.data.filter(e => e.name === 'vz').sort((a, b) => b.version - a.version)[0]._id; const vn_template = res.data.filter(e => e.name === 'vn').sort((a, b) => b.version - a.version)[0]._id;
const rmc_template = res.data.filter(e => e.name === 'reinforcement material content').sort((a, b) => b.version - a.version)[0]._id;
res = await axios({ res = await axios({
method: 'get', method: 'get',
url: host + '/samples?status=all', url: host + '/samples?status=all',
@ -287,14 +357,15 @@ async function allKfVz() {
sampleIds[sample.number] = sample._id; sampleIds[sample.number] = sample._id;
}); });
for (let index in data) { for (let index in data) {
console.info(`KF/VZ ${index}/${data.length}`); console.info(`MC/VN ${index}/${data.length}`);
let sample = data[index]; let sample = data[index];
sample['samplenumber'] = sample['samplenumber'].replace(/[A-Z][a-z]0\d_/, '');
let credentials = ['admin', 'Abc123!#']; let credentials = ['admin', 'Abc123!#'];
if (sampleDevices[sample['samplenumber']]) { if (sampleDevices[sample['samplenumber']]) {
credentials = [sampleDevices[sample['samplenumber']], '2020DeFinMachen!'] credentials = [sampleDevices[sample['samplenumber']], '2020DeFinMachen!']
} }
if (!sample['vz(ml/g)'] && vzValues[sample['samplenumber']]) { // fill in VZ values from comments if (!sample['vz(ml/g)'] && vnValues[sample['samplenumber']]) { // fill in VN values from comments
sample['vz(ml/g)'] = vzValues[sample['samplenumber']]; sample['vz(ml/g)'] = vnValues[sample['samplenumber']];
} }
if (sample['kfingew%']) { if (sample['kfingew%']) {
await axios({ await axios({
@ -306,7 +377,7 @@ async function allKfVz() {
}, },
data: { data: {
sample_id: sampleIds[sample['samplenumber']], sample_id: sampleIds[sample['samplenumber']],
measurement_template: kf_template, measurement_template: mc_template,
values: { values: {
'weight %': sample['kfingew%'], 'weight %': sample['kfingew%'],
'standard deviation': sample['stabwn'] 'standard deviation': sample['stabwn']
@ -315,7 +386,7 @@ async function allKfVz() {
}).catch(err => { }).catch(err => {
console.log(sample['samplenumber']); console.log(sample['samplenumber']);
console.error(err.response.data); console.error(err.response.data);
errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); errors.push(`MC/VN upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`);
}); });
} }
if (sample['vz(ml/g)']) { if (sample['vz(ml/g)']) {
@ -328,15 +399,36 @@ async function allKfVz() {
}, },
data: { data: {
sample_id: sampleIds[sample['samplenumber']], sample_id: sampleIds[sample['samplenumber']],
measurement_template: vz_template, measurement_template: vn_template,
values: { values: {
vz: sample['vz(ml/g)'] vn: sample['vz(ml/g)']
} }
} }
}).catch(err => { }).catch(err => {
console.log(sample['samplenumber']); console.log(sample['samplenumber']);
console.error(err.response.data); console.error(err.response.data);
errors.push(`KF/VZ upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`); errors.push(`MC/VN upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`);
});
}
if (sample['reinforcingmaterialcontent']) {
await axios({
method: 'post',
url: host + '/measurement/new',
auth: {
username: credentials[0],
password: credentials[1]
},
data: {
sample_id: sampleIds[sample['samplenumber']],
measurement_template: rmc_template,
values: {
percentage: Number(sample['reinforcingmaterialcontent'].replace('%', '').replace(',', '.'))
}
}
}).catch(err => {
console.log(sample['samplenumber']);
console.error(err.response.data);
errors.push(`MC/VN upload for ${JSON.stringify(sample)} failed: ${JSON.stringify(err.response.data)}`);
}); });
} }
} }
@ -422,7 +514,7 @@ async function allSamples() {
samples[si].color = number.color; samples[si].color = number.color;
} }
} }
else if (sampleColors[sample['samplenumber'].split('_')[0]]) { // derive color from main sample for kf/vz else if (sampleColors[sample['samplenumber'].split('_')[0]]) { // derive color from main sample for mc/vn
samples[si].color = sampleColors[sample['samplenumber'].split('_')[0]]; samples[si].color = sampleColors[sample['samplenumber'].split('_')[0]];
} }
if (!samples[si].color) { if (!samples[si].color) {
@ -436,7 +528,6 @@ async function saveSamples() {
console.info(`SAMPLE SAVE ${i}/${samples.length}`); console.info(`SAMPLE SAVE ${i}/${samples.length}`);
let credentials = ['admin', 'Abc123!#']; let credentials = ['admin', 'Abc123!#'];
if (sampleDevices[samples[i].number]) { if (sampleDevices[samples[i].number]) {
console.log(sampleDevices[samples[i].number]);
credentials = [sampleDevices[samples[i].number], '2020DeFinMachen!'] credentials = [sampleDevices[samples[i].number], '2020DeFinMachen!']
} }
await axios({ await axios({
@ -520,7 +611,7 @@ async function allMaterials() {
password: 'Abc123!#' password: 'Abc123!#'
} }
}); });
const materialTemplate = res.data.find(e => e.name === 'plastic')._id; const materialTemplate = res.data.filter(e => e.name === 'plastic').sort((a, b) => b.version - a.version)[0]._id;
// process all samples // process all samples
for (let index in data) { for (let index in data) {
@ -718,7 +809,7 @@ function readPdf(file) {
let lastLastText = ''; // text of last last item let lastLastText = ''; // text of last last item
await new pdfReader.PdfReader().parseFileItems(nmDocs + '\\' + file, (err, item) => { await new pdfReader.PdfReader().parseFileItems(nmDocs + '\\' + file, (err, item) => {
if (item && item.text) { if (item && item.text) {
if ((stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignationsuppl') >= 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('colordesignatiomsuppl') >= 0)) { // table area starts
table = countdown; table = countdown;
} }
if (table > 0) { if (table > 0) {
@ -822,9 +913,9 @@ function customFields (comment, sampleNumber) {
{docKey: 'zu', dbKey: 'belongs to', regex: /zu (\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: 'granulate zu', dbKey: 'granulate to', regex: /granulate zu.* (\S*\d+)/, category: 'reference'},
{docKey: 'construction part', dbKey: 'construction part', regex: /(?<!granulate)construction part.* (\S*\d+)/, category: 'reference'}, {docKey: 'construction part', dbKey: 'construction part', regex: /(?<!granulate)construction part.* (\S*\d+)/, category: 'reference'},
{docKey: 'VZ =', dbKey: 'vz', regex: /VZ = (\d+) cm³\/g/, category: 'vz'}, {docKey: 'VZ =', dbKey: 'vn', regex: /VZ = (\d+) cm³\/g/, category: 'vn'},
{docKey: 'VWZ', dbKey: 'vwz', regex: /(\d+ min) VWZ \//, category: 'customField'}, {docKey: 'VWZ', dbKey: 'vwz', regex: /(\d+ min) VWZ \//, category: 'customField'},
{docKey: 'VZ:', dbKey: 'vz', regex: /VZ: ([0-9.,]+) mL\/g[;]?/, category: 'vz'} {docKey: 'VZ:', dbKey: 'vn', regex: /VZ: ([0-9.,]+) mL\/g[;]?/, category: 'vn'}
]; ];
const res = {}; // returned result const res = {}; // returned result
const usedParts = []; // all substrings used for custom fields, subtract at the end, as some parts are used multiple times const usedParts = []; // all substrings used for custom fields, subtract at the end, as some parts are used multiple times
@ -837,8 +928,8 @@ function customFields (comment, sampleNumber) {
if (cField.category === 'reference') { if (cField.category === 'reference') {
sampleReferences.push({sample: sampleNumber, referencedSample: regexRes[1], relation: cField.dbKey}); sampleReferences.push({sample: sampleNumber, referencedSample: regexRes[1], relation: cField.dbKey});
} }
else if (cField.category === 'vz') { else if (cField.category === 'vn') {
vzValues[sampleNumber] = regexRes[1]; vnValues[sampleNumber] = regexRes[1];
} }
else { else {
res[cField.dbKey] = regexRes.filter((e, i) => i > 0).join(' '); res[cField.dbKey] = regexRes.filter((e, i) => i > 0).join(' ');
@ -864,8 +955,12 @@ function customFields (comment, sampleNumber) {
} }
function sampleType (type) { function sampleType (type) {
const allowedTypes = ['tension rod', 'part', 'granulate']; type = stripSpaces(type).toLowerCase();
return allowedTypes.indexOf(type) >= 0 ? type : (type === '' ? 'unknown' : 'other'); const allowedTypes = {'tension rod': 'tension rod', 'Zugstab': 'tension rod', 'part': 'part', 'granulate': 'granulate'};
if (!allowedTypes[type]) {
typeLog.push(type);
}
return allowedTypes[type] ? allowedTypes[type] : 'part';
} }
function stripSpaces(s) { function stripSpaces(s) {

5
package-lock.json generated
View File

@ -2522,11 +2522,6 @@
} }
} }
}, },
"mongo-sanitize": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/mongo-sanitize/-/mongo-sanitize-1.1.0.tgz",
"integrity": "sha512-6gB9AiJD+om2eZLxaPKIP5Q8P3Fr+s+17rVWso7hU0+MAzmIvIMlgTYuyvalDLTtE/p0gczcvJ8A3pbN1XmQ/A=="
},
"mongodb": { "mongodb": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.4.1.tgz",

View File

@ -11,6 +11,7 @@
"test": "mocha dist/**/**.spec.js", "test": "mocha dist/**/**.spec.js",
"start": "node index.js", "start": "node index.js",
"dev": "nodemon -e ts,yaml --exec \"tsc && node dist/index.js || exit 1\"", "dev": "nodemon -e ts,yaml --exec \"tsc && node dist/index.js || exit 1\"",
"start-local": "node dist/index.js",
"loadDev": "node dist/test/loadDev.js", "loadDev": "node dist/test/loadDev.js",
"coverage": "tsc && nyc --reporter=html --reporter=text mocha dist/**/**.spec.js --timeout 5000", "coverage": "tsc && nyc --reporter=html --reporter=text mocha dist/**/**.spec.js --timeout 5000",
"import": "node data_import/import.js" "import": "node data_import/import.js"
@ -35,7 +36,6 @@
"json-schema": "^0.2.5", "json-schema": "^0.2.5",
"json2csv": "^5.0.1", "json2csv": "^5.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"mongo-sanitize": "^1.1.0",
"mongoose": "^5.8.7", "mongoose": "^5.8.7",
"swagger-ui-dist": "^3.30.2" "swagger-ui-dist": "^3.30.2"
}, },

View File

@ -7,7 +7,7 @@ import ChangelogModel from './models/changelog';
// database urls, prod db url is retrieved automatically // database urls, prod db url is retrieved automatically
const TESTING_URL = 'mongodb://localhost/dfopdb_test'; const TESTING_URL = 'mongodb://localhost/dfopdb_test';
const DEV_URL = 'mongodb://localhost/dfopdb'; const DEV_URL = 'mongodb://localhost/dfopdb';
const debugging = true; const debugging = false;
if (process.env.NODE_ENV !== 'production' && debugging) { if (process.env.NODE_ENV !== 'production' && debugging) {
mongoose.set('debug', true); // enable mongoose debug mongoose.set('debug', true); // enable mongoose debug
@ -114,6 +114,9 @@ export default class db {
Object.keys(json.collections).forEach(collectionName => { // create each collection Object.keys(json.collections).forEach(collectionName => { // create each collection
json.collections[collectionName] = this.oidResolve(json.collections[collectionName]); json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
this.state.db.createCollection(collectionName, (err, collection) => { this.state.db.createCollection(collectionName, (err, collection) => {
if (err) {
console.error(err);
}
collection.insertMany(json.collections[collectionName], () => { // insert JSON data collection.insertMany(json.collections[collectionName], () => { // insert JSON data
if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded
done(); done();

View File

@ -1,8 +1,7 @@
const globals = { const globals = {
levels: [ // access levels levels: [ // access levels, sorted asc by rights
'read', 'read',
'write', 'write',
'maintain',
'dev', 'dev',
'admin' 'admin'
], ],

View File

@ -2,44 +2,65 @@ import axios from 'axios';
// sends an email using the BIC service // sends an email using the BIC service
export default (mailAddress, subject, content, f) => { // callback, executed empty or with error export default class Mail{
if (process.env.NODE_ENV === 'production') {
const mailService = JSON.parse(process.env.VCAP_SERVICES).Mail[0]; static readonly address = 'definma@bosch-iot.com';
axios({ static uri: string;
method: 'post', static auth = {username: '', password: ''};
url: mailService.credentials.uri + '/email', static mailPass: string;
auth: {username: mailService.credentials.username, password: mailService.credentials.password},
data: { static init() {
recipients: [{to: mailAddress}], this.mailPass = Array(64).map(() => Math.floor(Math.random() * 10)).join('');
subject: {content: subject}, this.uri = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.uri;
body: { this.auth.username = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.username;
content: content, this.auth.password = JSON.parse(process.env.VCAP_SERVICES).Mail[0].credentials.password;
contentType: "text/html" axios({ // get registered mail addresses
},
from: {
eMail: "definma@bosch-iot.com",
password: "PlasticsOfFingerprintDigital"
}
}
})
.then(() => {
f();
})
.catch((err) => {
f(err);
});
}
else if (process.env.NODE_ENV === 'test') {
console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content);
f();
}
else { // dev
axios({
method: 'get', method: 'get',
url: 'https://digital-fingerprint-of-plastics-mail-test.apps.de1.bosch-iot-cloud.com/api', url: this.uri + '/management/userDomainMapping',
data: { auth: this.auth
}).then(res => {
return new Promise(async (resolve, reject) => {
try {
if (res.data.addresses.indexOf(this.address) < 0) { // mail address not registered
if (res.data.addresses.length) { // delete wrong registered mail address
await axios({
method: 'delete',
url: this.uri + '/management/mailAddresses/' + res.data.addresses[0],
auth: this.auth
});
}
await axios({ // register right mail address
method: 'post',
url: this.uri + '/management/mailAddresses/' + this.address,
auth: this.auth
});
}
resolve();
}
catch (e) {
reject(e);
}
});
}).then(() => {
return axios({ // set new mail password
method: 'put',
url: this.uri + '/management/mailAddresses/' + this.address + '/password/' + this.mailPass,
auth: this.auth
});
}).then(() => { // init done successfully
this.send('lukas.veit@bosch.com', 'Mail Service started', new Date().toString());
}).catch(err => { // anywhere an error occurred
console.error(`Mail init error: ${err.request.method} ${err.request.path}: ${err.response.status}`,
err.response.data);
});
}
static send (mailAddress, subject, content, f = () => {}) { // callback, executed empty or with error
if (process.env.NODE_ENV === 'production') { // only send mails in production
axios({
method: 'post', method: 'post',
url: '/email', url: this.uri + '/email',
auth: this.auth,
data: { data: {
recipients: [{to: mailAddress}], recipients: [{to: mailAddress}],
subject: {content: subject}, subject: {content: subject},
@ -48,17 +69,19 @@ export default (mailAddress, subject, content, f) => { // callback, executed em
contentType: "text/html" contentType: "text/html"
}, },
from: { from: {
eMail: "dfop-test@bosch-iot.com", eMail: this.address,
password: "PlasticsOfFingerprintDigital" password: this.mailPass
} }
} }
} }).then(() => {
})
.then(() => {
f(); f();
}) }).catch((err) => {
.catch((err) => {
f(err); f(err);
}); });
}
else { // dev dummy replacement
console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content);
f();
}
} }
} }

View File

@ -2,7 +2,6 @@ import express from 'express';
import bodyParser from 'body-parser'; import bodyParser from 'body-parser';
import compression from 'compression'; import compression from 'compression';
import contentFilter from 'content-filter'; import contentFilter from 'content-filter';
import mongoSanitize from 'mongo-sanitize';
import helmet from 'helmet'; import helmet from 'helmet';
import cors from 'cors'; import cors from 'cors';
import api from './api'; import api from './api';
@ -11,7 +10,8 @@ import db from './db';
// TODO: check header, also in UI // TODO: check header, also in UI
// tell if server is running in debug or production environment // tell if server is running in debug or production environment
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); console.info(process.env.NODE_ENV === 'production' ?
'===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
// mongodb connection // mongodb connection
@ -61,15 +61,15 @@ app.use('/static/img/bosch-logo.svg', helmet.contentSecurityPolicy({
})); }));
// middleware // middleware
app.use(contentFilter()); // filter URL query attacks app.use(compression()); // compress responses
app.use(express.json({ limit: '5mb'})); app.use(express.json({ limit: '5mb'}));
app.use(express.urlencoded({ extended: false, limit: '5mb' })); app.use(express.urlencoded({ extended: false, limit: '5mb' }));
app.use(compression()); // compress responses
app.use(bodyParser.json()); app.use(bodyParser.json());
app.use((req, res, next) => { // filter body query attacks const injectionBlackList = ['$', '{', '&&', '||'];
req.body = mongoSanitize(req.body); app.use(contentFilter({
next(); urlBlackList: injectionBlackList,
}); bodyBlackList: injectionBlackList
})); // filter URL query attacks
app.use((err, req, res, ignore) => { // bodyParser error handling app.use((err, req, res, ignore) => { // bodyParser error handling
res.status(400).send({status: 'Invalid JSON body'}); res.status(400).send({status: 'Invalid JSON body'});
}); });

View File

@ -208,7 +208,7 @@ describe('/material', () => {
res: {_id: '100000000000000000000007', name: 'Ultramid A4H', supplier: 'BASF', group: 'PA66', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 0, carbon_fiber: 0}, numbers: []} 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 => { it('returns a deleted material for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/material/100000000000000000000008', url: '/material/100000000000000000000008',

View File

@ -19,7 +19,7 @@ import ParametersValidate from './validate/parameters';
const router = express.Router(); const router = express.Router();
router.get('/materials', (req, res, next) => { router.get('/materials', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
const {error, value: filters} = MaterialValidate.query(req.query); const {error, value: filters} = MaterialValidate.query(req.query);
if (error) return res400(error, res); if (error) return res400(error, res);
@ -41,22 +41,25 @@ router.get('/materials', (req, res, next) => {
MaterialModel.find(conditions).populate('group_id').populate('supplier_id').lean().exec((err, data) => { MaterialModel.find(conditions).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => MaterialValidate.output(e))));
}); });
}); });
router.get('/materials/:state(new|deleted)', (req, res, next) => { router.get('/materials/:state(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
MaterialModel.find({status: globals.status[req.params.state]}).populate('group_id').populate('supplier_id').lean().exec((err, data) => { MaterialModel.find({status: globals.status[req.params.state]}).populate('group_id').populate('supplier_id')
.lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => MaterialValidate.output(e))));
}); });
}); });
router.get('/material/' + IdValidate.parameter(), (req, res, next) => { router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => { MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => {
if (err) return next(err); if (err) return next(err);
@ -65,13 +68,14 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
if (data.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted materials only available for maintain/admin // deleted materials only available for dev/admin
if (data.status === globals.status.deleted && !req.auth(res, ['dev', 'admin'], 'all')) return;
res.json(MaterialValidate.output(data)); res.json(MaterialValidate.output(data));
}); });
}); });
router.put('/material/' + IdValidate.parameter(), (req, res, next) => { router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
let {error, value: material} = MaterialValidate.input(req.body, 'change'); let {error, value: material} = MaterialValidate.input(req.body, 'change');
if (error) return res400(error, res); if (error) return res400(error, res);
@ -95,7 +99,8 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!material) return; if (!material) return;
} }
if (material.hasOwnProperty('properties')) { if (material.hasOwnProperty('properties')) {
if (!await propertiesCheck(material.properties, 'change', res, next, materialData.properties.material_template.toString() !== material.properties.material_template)) return; if (!await propertiesCheck(material.properties, 'change', res, next,
materialData.properties.material_template.toString() !== material.properties.material_template)) return;
} }
// check for changes // check for changes
@ -103,7 +108,8 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
material.status = globals.status.new; // set status to new material.status = globals.status.new; // set status to new
} }
await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true})
.log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(MaterialValidate.output(data)); res.json(MaterialValidate.output(data));
}); });
@ -111,7 +117,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
}); });
router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
// check if there are still samples referencing this material // check if there are still samples referencing this material
SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => { SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => {
@ -119,7 +125,8 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (data.length) { if (data.length) {
return res.status(400).json({status: 'Material still in use'}); return res.status(400).json({status: 'Material still in use'});
} }
MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => { MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted})
.log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
if (data) { if (data) {
res.json({status: 'OK'}); res.json({status: 'OK'});
@ -132,19 +139,19 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
}); });
router.put('/material/restore/' + IdValidate.parameter(), (req, res, next) => { router.put('/material/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
setStatus(globals.status.new, req, res, next); setStatus(globals.status.new, req, res, next);
}); });
router.put('/material/validate/' + IdValidate.parameter(), (req, res, next) => { router.put('/material/validate/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
setStatus(globals.status.validated, req, res, next); setStatus(globals.status.validated, req, res, next);
}); });
router.post('/material/new', async (req, res, next) => { router.post('/material/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
let {error, value: material} = MaterialValidate.input(req.body, 'new'); let {error, value: material} = MaterialValidate.input(req.body, 'new');
if (error) return res400(error, res); if (error) return res400(error, res);
@ -167,22 +174,24 @@ router.post('/material/new', async (req, res, next) => {
}); });
router.get('/material/groups', (req, res, next) => { router.get('/material/groups', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
MaterialGroupModel.find().lean().exec((err, data: any) => { MaterialGroupModel.find().lean().exec((err, data: any) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name))));
}); });
}); });
router.get('/material/suppliers', (req, res, next) => { router.get('/material/suppliers', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
MaterialSupplierModel.find().lean().exec((err, data: any) => { MaterialSupplierModel.find().lean().exec((err, data: any) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name))));
}); });
}); });
@ -201,7 +210,11 @@ async function nameCheck (material, res, next) { // check if name was already t
} }
async function groupResolve (material, req, next) { async function groupResolve (material, req, next) {
const groupData = await MaterialGroupModel.findOneAndUpdate({name: material.group}, {name: material.group}, {upsert: true, new: true}).log(req).lean().exec().catch(err => next(err)) as any; const groupData = await MaterialGroupModel.findOneAndUpdate(
{name: material.group},
{name: material.group},
{upsert: true, new: true}
).log(req).lean().exec().catch(err => next(err)) as any;
if (groupData instanceof Error) return false; if (groupData instanceof Error) return false;
material.group_id = groupData._id; material.group_id = groupData._id;
delete material.group; delete material.group;
@ -209,19 +222,25 @@ async function groupResolve (material, req, next) {
} }
async function supplierResolve (material, req, next) { async function supplierResolve (material, req, next) {
const supplierData = await MaterialSupplierModel.findOneAndUpdate({name: material.supplier}, {name: material.supplier}, {upsert: true, new: true}).log(req).lean().exec().catch(err => next(err)) as any; const supplierData = await MaterialSupplierModel.findOneAndUpdate(
{name: material.supplier},
{name: material.supplier},
{upsert: true, new: true}
).log(req).lean().exec().catch(err => next(err)) as any;
if (supplierData instanceof Error) return false; if (supplierData instanceof Error) return false;
material.supplier_id = supplierData._id; material.supplier_id = supplierData._id;
delete material.supplier; delete material.supplier;
return material; return material;
} }
async function propertiesCheck (properties, param, res, next, checkVersion = true) { // validate material properties, returns false if invalid, otherwise template data // validate material properties, returns false if invalid, otherwise template data
async function propertiesCheck (properties, param, res, next, checkVersion = true) {
if (!properties.material_template || !IdValidate.valid(properties.material_template)) { // template id not found if (!properties.material_template || !IdValidate.valid(properties.material_template)) { // template id not found
res.status(400).json({status: 'Material template not available'}); res.status(400).json({status: 'Material template not available'});
return false; return false;
} }
const materialData = await MaterialTemplateModel.findById(properties.material_template).lean().exec().catch(err => next(err)) as any; const materialData = await MaterialTemplateModel.findById(properties.material_template)
.lean().exec().catch(err => next(err)) as any;
if (materialData instanceof Error) return false; if (materialData instanceof Error) return false;
if (!materialData) { // template not found if (!materialData) { // template not found
res.status(400).json({status: 'Material template not available'}); res.status(400).json({status: 'Material template not available'});
@ -230,7 +249,8 @@ async function propertiesCheck (properties, param, res, next, checkVersion = tru
if (checkVersion) { if (checkVersion) {
// get all template versions and check if given is latest // 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; 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 (materialVersions instanceof Error) return false;
if (properties.material_template !== materialVersions[0]._id.toString()) { // template not latest if (properties.material_template !== materialVersions[0]._id.toString()) { // template not latest
res.status(400).json({status: 'Old template version not allowed'}); res.status(400).json({status: 'Old template version not allowed'});
@ -239,7 +259,8 @@ async function propertiesCheck (properties, param, res, next, checkVersion = tru
} }
// validate parameters // validate parameters
const {error, value} = ParametersValidate.input(_.omit(properties, 'material_template'), materialData.parameters, param); const {error, value} = ParametersValidate
.input(_.omit(properties, 'material_template'), materialData.parameters, param);
if (error) {res400(error, res); return false;} if (error) {res400(error, res); return false;}
Object.keys(value).forEach(key => { Object.keys(value).forEach(key => {
properties[key] = value[key]; properties[key] = value[key];

View File

@ -16,7 +16,7 @@ describe('/measurement', () => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/measurement/800000000000000000000001', url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'} res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
}); });
@ -25,12 +25,21 @@ describe('/measurement', () => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/measurement/800000000000000000000001', url: '/measurement/800000000000000000000001',
auth: {key: 'janedoe'}, auth: {key: 'admin'},
httpStatus: 200, httpStatus: 200,
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'} res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
}); });
}); });
it('returns deleted measurements for a maintain/admin user', done => { it('filters out spectral data for a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
});
});
it('returns deleted measurements for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/measurement/800000000000000000000004', url: '/measurement/800000000000000000000004',
@ -77,7 +86,7 @@ describe('/measurement', () => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/measurement/800000000000000000000001', url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
req: {}, req: {},
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'} res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}, measurement_template: '300000000000000000000001'}
@ -87,7 +96,7 @@ describe('/measurement', () => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/measurement/800000000000000000000001', url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}} req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]], device: 'Alpha I'}}
}).end((err, res) => { }).end((err, res) => {
@ -121,7 +130,7 @@ describe('/measurement', () => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/measurement/800000000000000000000001', url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
req: {values: {dpt: [[1,2],[3,4],[5,6]]}} req: {values: {dpt: [[1,2],[3,4],[5,6]]}}
}).end((err, res) => { }).end((err, res) => {
@ -244,7 +253,7 @@ describe('/measurement', () => {
req: {values: {val1: 2}} req: {values: {val1: 2}}
}); });
}); });
it('accepts editing a measurement of another user for a maintain/admin user', done => { it('accepts editing a measurement of another user for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/measurement/800000000000000000000002', url: '/measurement/800000000000000000000002',
@ -362,7 +371,7 @@ describe('/measurement', () => {
httpStatus: 403, httpStatus: 403,
}); });
}); });
it('accepts deleting a measurement of another user for a maintain/admin user', done => { it('accepts deleting a measurement of another user for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/measurement/800000000000000000000001', url: '/measurement/800000000000000000000001',
@ -731,7 +740,7 @@ describe('/measurement', () => {
req: {sample_id: '400000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'} req: {sample_id: '400000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
}); });
}); });
it('accepts adding a measurement to the sample of another user for a maintain/admin user', done => { it('accepts adding a measurement to the sample of another user for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/measurement/new', url: '/measurement/new',

View File

@ -15,21 +15,22 @@ import db from '../db';
const router = express.Router(); const router = express.Router();
router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => { router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => { MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => {
if (err) return next(err); if (err) return next(err);
if (!data) { if (!data) {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
if (data.status ===globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted measurements only available for maintain/admin // deleted measurements only available for dev/admin
if (data.status === globals.status.deleted && !req.auth(res, ['dev', 'admin'], 'all')) return;
res.json(MeasurementValidate.output(data)); res.json(MeasurementValidate.output(data, req));
}); });
}); });
router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => { router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
const {error, value: measurement} = MeasurementValidate.input(req.body, 'change'); const {error, value: measurement} = MeasurementValidate.input(req.body, 'change');
if (error) return res400(error, res); if (error) return res400(error, res);
@ -57,14 +58,15 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
} }
if (!await templateCheck(measurement, 'change', res, next)) return; if (!await templateCheck(measurement, 'change', res, next)) return;
await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).log(req).lean().exec((err, data) => { await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true})
.log(req).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(MeasurementValidate.output(data)); res.json(MeasurementValidate.output(data, req));
}); });
}); });
router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => { router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => { MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => {
if (err) return next(err); if (err) return next(err);
@ -72,7 +74,8 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
if (!await sampleIdCheck(data, req, res, next)) return; if (!await sampleIdCheck(data, req, res, next)) return;
await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted})
.log(req).lean().exec(err => {
if (err) return next(err); if (err) return next(err);
return res.json({status: 'OK'}); return res.json({status: 'OK'});
}); });
@ -80,19 +83,19 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
}); });
router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => { router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
setStatus(globals.status.new, req, res, next); setStatus(globals.status.new, req, res, next);
}); });
router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => { router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
setStatus(globals.status.validated, req, res, next); setStatus(globals.status.validated, req, res, next);
}); });
router.post('/measurement/new', async (req, res, next) => { router.post('/measurement/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
const {error, value: measurement} = MeasurementValidate.input(req.body, 'new'); const {error, value: measurement} = MeasurementValidate.input(req.body, 'new');
if (error) return res400(error, res); if (error) return res400(error, res);
@ -105,7 +108,7 @@ router.post('/measurement/new', async (req, res, next) => {
await new MeasurementModel(measurement).save((err, data) => { await new MeasurementModel(measurement).save((err, data) => {
if (err) return next(err); if (err) return next(err);
db.log(req, 'measurements', {_id: data._id}, data.toObject()); db.log(req, 'measurements', {_id: data._id}, data.toObject());
res.json(MeasurementValidate.output(data.toObject())); res.json(MeasurementValidate.output(data.toObject(), req));
}); });
}); });
@ -113,18 +116,23 @@ router.post('/measurement/new', async (req, res, next) => {
module.exports = router; module.exports = router;
async function sampleIdCheck (measurement, req, res, next) { // validate sample_id, returns false if invalid or user has no access for this sample // validate sample_id, returns false if invalid or user has no access for this sample
const sampleData = await SampleModel.findById(measurement.sample_id).lean().exec().catch(err => {next(err); return false;}) as any; async function sampleIdCheck (measurement, req, res, next) {
const sampleData = await SampleModel.findById(measurement.sample_id)
.lean().exec().catch(err => {next(err); return false;}) as any;
if (!sampleData) { // sample_id not found if (!sampleData) { // sample_id not found
res.status(400).json({status: 'Sample id not available'}); res.status(400).json({status: 'Sample id not available'});
return false return false
} }
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user // sample does not belong to user
return true; return !(sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic'));
} }
async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, returns values, true if values are {} or false if invalid, param for 'new'/'change' // validate measurement_template and values, returns values, true if values are {} or false if invalid,
const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template).lean().exec().catch(err => {next(err); return false;}) as any; // param for 'new'/'change'
async function templateCheck (measurement, param, res, next) {
const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template)
.lean().exec().catch(err => {next(err); return false;}) as any;
if (!templateData) { // template not found if (!templateData) { // template not found
res.status(400).json({status: 'Measurement template not available'}); res.status(400).json({status: 'Measurement template not available'});
return false return false
@ -133,7 +141,8 @@ async function templateCheck (measurement, param, res, next) { // validate meas
// fill not given values for new measurements // fill not given values for new measurements
if (param === 'new') { if (param === 'new') {
// get all template versions and check if given is latest // get all template versions and check if given is latest
const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any; const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1})
.lean().exec().catch(err => next(err)) as any;
if (templateVersions instanceof Error) return false; if (templateVersions instanceof Error) return false;
if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest
res.status(400).json({status: 'Old template version not allowed'}); res.status(400).json({status: 'Old template version not allowed'});

View File

@ -179,7 +179,7 @@ describe('/', () => {
url: '/authorized', url: '/authorized',
auth: {key: 'admin'}, auth: {key: 'admin'},
httpStatus: 200, httpStatus: 200,
res: {status: 'Authorization successful', method: 'key', level: 'admin'} res: {status: 'Authorization successful', method: 'key', level: 'admin', user_id: '000000000000000000000003'}
}); });
}); });
it('works with basic auth', done => { it('works with basic auth', done => {
@ -188,7 +188,7 @@ describe('/', () => {
url: '/authorized', url: '/authorized',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
res: {status: 'Authorization successful', method: 'basic', level: 'admin'} res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'}
}); });
}); });
}); });
@ -207,17 +207,17 @@ describe('/', () => {
}); });
}); });
describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!! // describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!!
it('resolves to an 500 error', done => { // it('resolves to an 500 error', done => {
db.disconnect(() => { // db.disconnect(() => {
TestHelper.request(server, done, { // TestHelper.request(server, done, {
method: 'get', // method: 'get',
url: '/', // url: '/',
httpStatus: 500 // httpStatus: 500
}); // });
}); // });
}); // });
}); // });
}); });
describe('The /api/{url} redirect', () => { describe('The /api/{url} redirect', () => {
@ -242,15 +242,15 @@ describe('The /api/{url} redirect', () => {
url: '/api/authorized', url: '/api/authorized',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 200, httpStatus: 200,
res: {status: 'Authorization successful', method: 'basic', level: 'admin'} res: {status: 'Authorization successful', method: 'basic', level: 'admin', user_id: '000000000000000000000003'}
});
});
it('is disabled in production', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/api/authorized',
auth: {basic: 'admin'},
httpStatus: 404
}); });
}); });
// it('is disabled in production', done => {
// TestHelper.request(server, done, {
// method: 'get',
// url: '/api/authorized',
// auth: {basic: 'admin'},
// httpStatus: 404
// });
// });
}); });

View File

@ -14,21 +14,33 @@ router.get('/', (req, res) => {
router.get('/authorized', (req, res) => { router.get('/authorized', (req, res) => {
if (!req.auth(res, globals.levels)) return; if (!req.auth(res, globals.levels)) return;
res.json({status: 'Authorization successful', method: req.authDetails.method, level: req.authDetails.level}); res.json({
status: 'Authorization successful',
method: req.authDetails.method,
level: req.authDetails.level,
user_id: req.authDetails.id
});
}); });
// TODO: evaluate exact changelog functionality (restoring, delting after time, etc.) // TODO: evaluate exact changelog functionality (restoring, deleting after time, etc.)
router.get('/changelog/:timestamp/:page?/:pagesize?', (req, res, next) => { router.get('/changelog/:timestamp/:page?/:pagesize?', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
const {error, value: options} = RootValidate.changelogParams({timestamp: req.params.timestamp, page: req.params.page, pagesize: req.params.pagesize}); const {error, value: options} = RootValidate.changelogParams({
timestamp: req.params.timestamp,
page: req.params.page,
pagesize: req.params.pagesize
});
if (error) return res400(error, res); if (error) return res400(error, res);
const id = new mongoose.Types.ObjectId(Math.floor(new Date(options.timestamp).getTime() / 1000).toString(16) + '0000000000000000'); const id = new mongoose.Types
ChangelogModel.find({_id: {$lte: id}}).sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize).lean().exec((err, data) => { .ObjectId(Math.floor(new Date(options.timestamp).getTime() / 1000).toString(16) + '0000000000000000');
ChangelogModel.find({_id: {$lte: id}}).sort({_id: -1}).skip(options.page * options.pagesize).limit(options.pagesize)
.lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => RootValidate.changelogOutput(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => RootValidate.changelogOutput(e))));
}); });
}); });

View File

@ -262,7 +262,7 @@ describe('/sample', () => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum.dpt', url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum.dpt',
auth: {basic: 'janedoe'}, auth: {basic: 'admin'},
httpStatus: 200 httpStatus: 200
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
@ -379,6 +379,14 @@ describe('/sample', () => {
done(); done();
}); });
}); });
it('rejects returning spectral data for a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/samples?status=all&fields[]=number&fields[]=measurements.spectrum.dpt',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects an invalid JSON string as a filters parameter', done => { it('rejects an invalid JSON string as a filters parameter', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
@ -681,7 +689,25 @@ describe('/sample', () => {
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'} 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 => { it ('filters out spectral data for a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]}
});
});
it ('returns spectral data for an admin user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/400000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]}
});
});
it('returns a deleted sample for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/sample/400000000000000000000005', url: '/sample/400000000000000000000005',
@ -1054,6 +1080,16 @@ describe('/sample', () => {
res: {status: 'Condition template not available'} res: {status: 'Condition template not available'}
}); });
}); });
it('rejects a not accepted type', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {type: 'xx'},
res: {status: 'Invalid body format', details: '"type" must be one of [granulate, part, tension rod]'}
});
});
it('allows keeping an empty condition empty', done => { it('allows keeping an empty condition empty', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
@ -1121,7 +1157,7 @@ describe('/sample', () => {
req: {} req: {}
}); });
}); });
it('accepts changes for samples from another user for a maintain/admin user', done => { it('accepts changes for samples from another user for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
@ -1260,7 +1296,7 @@ describe('/sample', () => {
}); });
}); });
it('lets admin/maintain users delete samples of other users', done => { it('lets admin/dev users delete samples of other users', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/sample/400000000000000000000001', url: '/sample/400000000000000000000001',
@ -1362,7 +1398,7 @@ describe('/sample', () => {
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'} 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 => { it('returns a deleted sample for a dev/admin user', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
url: '/sample/number/Rng33', url: '/sample/number/Rng33',
@ -1371,6 +1407,24 @@ describe('/sample', () => {
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'} 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 ('filters out spectral data for a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/number/1',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]}
});
});
it ('returns spectral data for an admin user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/sample/number/1',
auth: {basic: 'admin'},
httpStatus: 200,
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material: {numbers: ['5513933405'], _id: '100000000000000000000004', name: 'Schulamid 66 GF 25 H', properties: {material_template: '130000000000000000000003', mineral: 0, glass_fiber: 25, carbon_fiber: 0}, group: 'PA66', supplier: 'Schulmann'}, user: 'janedoe', notes: {}, measurements: [{_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[ 3997.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]],device: 'Alpha I'}, measurement_template: '300000000000000000000001'}, {_id: '800000000000000000000007', sample_id: '400000000000000000000001', values: {dpt: [[ 3996.12558, 98.00555 ], [ 3995.08519, 98.03253 ], [ 3993.0448, 98.02657 ]], device: 'Alpha II'}, measurement_template: '300000000000000000000001'}]}
});
});
it('returns 403 for a write user when requesting a deleted sample', done => { it('returns 403 for a write user when requesting a deleted sample', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'get', method: 'get',
@ -1513,24 +1567,38 @@ describe('/sample', () => {
} }
}); });
}); });
it('rejects validating a sample without condition', done => { it('allows validating a sample without condition', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/sample/validate/400000000000000000000006', url: '/sample/validate/400000000000000000000006',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 200,
req: {}, req: {}
res: {status: 'Sample without condition cannot be valid'} }).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
SampleModel.findById('400000000000000000000006').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
done();
});
}); });
}); });
it('rejects validating a sample without measurements', done => { it('allows validating a sample without measurements', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/sample/validate/400000000000000000000004', url: '/sample/validate/400000000000000000000004',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 200,
req: {}, req: {}
res: {status: 'Sample without measurements cannot be valid'} }).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
SampleModel.findById('400000000000000000000004').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
done();
});
}); });
}); });
it('rejects an API key', done => { it('rejects an API key', done => {
@ -1937,6 +2005,16 @@ describe('/sample', () => {
res: {status: 'Invalid body format', details: 'Invalid object id'} res: {status: 'Invalid body format', details: 'Invalid object id'}
}); });
}); });
it('rejects a not accepted type', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {color: 'black', type: 'xx', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment'}},
res: {status: 'Invalid body format', details: '"type" must be one of [granulate, part, tension rod]'}
});
});
it('rejects an API key', done => { it('rejects an API key', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',

View File

@ -28,14 +28,19 @@ const router = express.Router();
// TODO: think about filter keys with measurement template versions // TODO: think about filter keys with measurement template versions
router.get('/samples', async (req, res, next) => { router.get('/samples', async (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
const {error, value: filters} = SampleValidate.query(req.query); const {error, value: filters} = SampleValidate.query(req.query);
if (error) return res400(error, res); if (error) return res400(error, res);
// spectral data not allowed for read/write users
if (filters.fields.find(e => /\.dpt$/.test(e)) && !req.auth(res, ['dev', 'admin'], 'all')) return;
// TODO: find a better place for these // TODO: find a better place for these
const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id', 'user_id']; const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id',
'user_id'];
// evaluate sort parameter from 'color-asc' to ['color', 1] // evaluate sort parameter from 'color-asc' to ['color', 1]
filters.sort = filters.sort.split('-'); filters.sort = filters.sort.split('-');
@ -74,7 +79,8 @@ router.get('/samples', async (req, res, next) => {
} }
else { else {
// start and end of day // start and end of day
const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0), new Date(addedFilter.values[0]).setHours(23,59,59,999)]; const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0),
new Date(addedFilter.values[0]).setHours(23,59,59,999)];
if (addedFilter.mode === 'lt') { // lt start if (addedFilter.mode === 'lt') { // lt start
filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]}); filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]});
} }
@ -88,7 +94,8 @@ router.get('/samples', async (req, res, next) => {
filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]}); filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]});
} }
if (addedFilter.mode === 'ne') { if (addedFilter.mode === 'ne') {
filters.filters.push({mode: 'or', field: '_id', values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]}); filters.filters.push({mode: 'or', field: '_id',
values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]});
} }
} }
} }
@ -103,27 +110,31 @@ router.get('/samples', async (req, res, next) => {
if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection
collection = MeasurementModel; collection = MeasurementModel;
const [,measurementName, measurementParam] = filters.sort[0].split('.'); const [,measurementName, measurementParam] = filters.sort[0].split('.');
const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName}).lean().exec().catch(err => {next(err);}); const measurementTemplates = await MeasurementTemplateModel.find({name: measurementName})
.lean().exec().catch(err => {next(err);});
if (measurementTemplates instanceof Error) return; if (measurementTemplates instanceof Error) return;
if (!measurementTemplates) { if (!measurementTemplates) {
return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'}); return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'});
} }
let sortStartValue = null; let sortStartValue = null;
if (filters['from-id']) { // from-id specified, fetch values for sorting if (filters['from-id']) { // from-id specified, fetch values for sorting
const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])}).lean().exec().catch(err => {next(err);}); // TODO: what if more than one measurement for sample? const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])})
.lean().exec().catch(err => {next(err);}); // TODO: what if more than one measurement for sample?
if (fromSample instanceof Error) return; if (fromSample instanceof Error) return;
if (!fromSample) { if (!fromSample) {
return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'});
} }
sortStartValue = fromSample.values[measurementParam]; sortStartValue = fromSample.values[measurementParam];
} }
queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}}); // find measurements to sort // find measurements to sort
queryPtr[0].$match.$and.push({measurement_template: {$in: measurementTemplates.map(e => e._id)}});
if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered
queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]).map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; }))); queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0])
.map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; })));
} }
queryPtr.push( queryPtr.push(
...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue), // sort measurements ...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue), // sort measurements
{$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure {$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure
{$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}}, {$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}},
{$match: statusQuery(filters, 'sample.status')}, // filter out wrong status once samples were added {$match: statusQuery(filters, 'sample.status')}, // filter out wrong status once samples were added
{$addFields: {['sample.' + measurementName]: '$measurement.values'}}, // more restructuring {$addFields: {['sample.' + measurementName]: '$measurement.values'}}, // more restructuring
@ -159,43 +170,52 @@ router.get('/samples', async (req, res, next) => {
let materialAdded = false; let materialAdded = false;
if (sortFilterKeys.find(e => /material\./.test(e))) { // add material fields if (sortFilterKeys.find(e => /material\./.test(e))) { // add material fields
materialAdded = true; materialAdded = true;
materialQuery.push( // add material properties materialQuery.push( // add material properties // TODO: project out unnecessary fields
{$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, // TODO: project out unnecessary fields {$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
{$addFields: {material: {$arrayElemAt: ['$material', 0]}}} {$addFields: {material: {$arrayElemAt: ['$material', 0]}}}
); );
const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e)).filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0); const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e))
addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0)); // base material filters .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0);
// base material filters
addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0));
if (sortFilterKeys.find(e => e === 'material.supplier')) { // add supplier if needed if (sortFilterKeys.find(e => e === 'material.supplier')) { // add supplier if needed
materialQuery.push( materialQuery.push(
{$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}}, {$lookup: {
from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}
},
{$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
); );
} }
if (sortFilterKeys.find(e => e === 'material.group')) { // add group if needed if (sortFilterKeys.find(e => e === 'material.group')) { // add group if needed
materialQuery.push( materialQuery.push(
{$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }}, {$lookup: {
from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }
},
{$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}
); );
} }
if (sortFilterKeys.find(e => e === 'material.number')) { // add material number if needed if (sortFilterKeys.find(e => e === 'material.number')) { // add material number if needed
materialQuery.push( materialQuery.push(
{$addFields: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]}}} {$addFields: {'material.number': { $arrayElemAt: [
'$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}
]}}}
); );
} }
const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e)).filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0); const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e))
addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0)); // base material filters .filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0);
// base material filters
addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0));
queryPtr.push(...materialQuery); queryPtr.push(...materialQuery);
if (/material\./.test(filters.sort[0])) { // sort by material key if (/material\./.test(filters.sort[0])) { // sort by material key
let sortStartValue = null; let sortStartValue = null;
if (filters['from-id']) { // from-id specified if (filters['from-id']) { // from-id specified
const fromSample = await SampleModel.aggregate([{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery]).exec().catch(err => {next(err);}); const fromSample = await SampleModel.aggregate(
[{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery]
).exec().catch(err => {next(err);});
if (fromSample instanceof Error) return; if (fromSample instanceof Error) return;
if (!fromSample) { if (!fromSample) {
return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'}); return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'});
} }
console.log(fromSample);
console.log(filters.sort[0]);
console.log(fromSample[filters.sort[0]]);
const filterKey = filters.sort[0].split('.'); const filterKey = filters.sort[0].split('.');
if (filterKey.length === 2) { if (filterKey.length === 2) {
sortStartValue = fromSample[0][filterKey[0]][filterKey[1]]; sortStartValue = fromSample[0][filterKey[0]][filterKey[1]];
@ -208,23 +228,25 @@ router.get('/samples', async (req, res, next) => {
} }
} }
const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e))
.map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters
if (sortFilterKeys.find(e => /measurements\./.test(e))) { // add measurement fields if (sortFilterKeys.find(e => /measurements\./.test(e))) { // add measurement fields
const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}}).lean().exec().catch(err => {next(err);}); const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}})
.lean().exec().catch(err => {next(err);});
if (measurementTemplates instanceof Error) return; if (measurementTemplates instanceof Error) return;
if (measurementTemplates.length < measurementFilterFields.length) { if (measurementTemplates.length < measurementFilterFields.length) {
return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'});
} }
queryPtr.push({$lookup: { queryPtr.push({$lookup: {
from: 'measurements', let: {sId: '$_id'}, from: 'measurements', let: {sId: '$_id'},
pipeline: [{$match: {$expr: {$and: [{$eq: ['$sample_id', '$$sId']}, {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}]}}}], pipeline: [{$match: {$expr: {$and: [
{$eq: ['$sample_id', '$$sId']},
{$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}
]}}}],
as: 'measurements' as: 'measurements'
}}); }});
measurementTemplates.forEach(template => { measurementTemplates.forEach(template => {
queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values addMeasurements(queryPtr, template);
vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}},
in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']}
}}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}});
}); });
addFilterQueries(queryPtr, filters.filters addFilterQueries(queryPtr, filters.filters
.filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0) .filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0)
@ -232,14 +254,18 @@ router.get('/samples', async (req, res, next) => {
); // measurement filters ); // measurement filters
} }
if (!filters.fields.find(e => /spectrum\./.test(e)) && !filters['from-id']) { // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not included // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not
// included
if (!filters.fields.find(e => /spectrum\./.test(e)) && !filters['from-id']) {
queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}}); queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}});
queryPtr = queryPtr[queryPtr.length - 1].$facet.samples; // add rest of aggregation pipeline into $facet queryPtr = queryPtr[queryPtr.length - 1].$facet.samples; // add rest of aggregation pipeline into $facet
} }
// paging // paging
if (filters['to-page']) { if (filters['to-page']) {
queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + Number(filters['to-page'] < 0)}) // number to skip, if going back pages, one page has to be skipped less but on sample more // number to skip, if going back pages, one page has to be skipped less but on sample more
queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] +
Number(filters['to-page'] < 0)})
} }
if (filters['page-size']) { if (filters['page-size']) {
queryPtr.push({$limit: filters['page-size']}); queryPtr.push({$limit: filters['page-size']});
@ -265,51 +291,65 @@ router.get('/samples', async (req, res, next) => {
} }
if (fieldsToAdd.indexOf('material.supplier') >= 0) { // add supplier if needed if (fieldsToAdd.indexOf('material.supplier') >= 0) { // add supplier if needed
queryPtr.push( queryPtr.push(
{$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}}, {$lookup: {
from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'
}},
{$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}} {$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
); );
} }
if (fieldsToAdd.indexOf('material.group') >= 0) { // add group if needed if (fieldsToAdd.indexOf('material.group') >= 0) { // add group if needed
queryPtr.push( queryPtr.push(
{$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }}, {$lookup: {
from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group'
}},
{$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}} {$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}
); );
} }
if (fieldsToAdd.indexOf('material.number') >= 0) { // add material number if needed if (fieldsToAdd.indexOf('material.number') >= 0) { // add material number if needed
queryPtr.push( queryPtr.push(
{$addFields: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]}}} {$addFields: {'material.number': {
$arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]
}}}
); );
} }
let measurementFieldsFields: string[] = _.uniq(fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters let measurementFieldsFields: string[] = _.uniq(
fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])
); // filter measurement names and remove duplicates from parameters
if (fieldsToAdd.find(e => /measurements\./.test(e))) { // add measurement fields if (fieldsToAdd.find(e => /measurements\./.test(e))) { // add measurement fields
const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}}).lean().exec().catch(err => {next(err);}); const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}})
.lean().exec().catch(err => {next(err);});
if (measurementTemplates instanceof Error) return; if (measurementTemplates instanceof Error) return;
if (measurementTemplates.length < measurementFieldsFields.length) { if (measurementTemplates.length < measurementFieldsFields.length) {
return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'}); return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'});
} }
if (fieldsToAdd.find(e => /spectrum\./.test(e))) { // use different lookup methods with and without spectrum for the best performance // use different lookup methods with and without spectrum for the best performance
queryPtr.push({$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}}); if (fieldsToAdd.find(e => /spectrum\./.test(e))) {
queryPtr.push(
{$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}}
);
} }
else { else {
queryPtr.push({$lookup: { queryPtr.push({$lookup: {
from: 'measurements', let: {sId: '$_id'}, from: 'measurements', let: {sId: '$_id'},
pipeline: [{$match: {$expr: {$and: [{$eq: ['$sample_id', '$$sId']}, {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}]}}}], pipeline: [{$match: {$expr: {$and: [
{$eq: ['$sample_id', '$$sId']},
{$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}
]}}}],
as: 'measurements' as: 'measurements'
}}); }});
} }
measurementTemplates.forEach(template => { // TODO: hard coded dpt for special treatment, change later measurementTemplates.forEach(template => { // TODO: hard coded dpt for special treatment, change later
queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values addMeasurements(queryPtr, template);
vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}},
in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']}
}}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}});
if (measurementFieldsFields.find(e => e === 'spectrum')) { if (measurementFieldsFields.find(e => e === 'spectrum')) {
queryPtr.push({$unwind: '$spectrum'}); queryPtr.push({$unwind: '$spectrum'});
} }
}); });
// if (measurementFieldsFields.find(e => e === 'spectrum')) { // TODO: remove hardcoded as well // if (measurementFieldsFields.find(e => e === 'spectrum')) { // TODO: remove hardcoded as well
// queryPtr.push( // queryPtr.push(
// {$addFields: {spectrum: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id]}}}}}, // {$addFields: {spectrum: {$filter: {input: '$measurements', cond: {
// $eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id]
// }}}}},
// {$addFields: {spectrum: '$spectrum.values'}}, // {$addFields: {spectrum: '$spectrum.values'}},
// {$unwind: '$spectrum'} // {$unwind: '$spectrum'}
// ); // );
@ -318,10 +358,11 @@ router.get('/samples', async (req, res, next) => {
queryPtr.push({$project: {measurements: 0}}); queryPtr.push({$project: {measurements: 0}});
} }
const projection = filters.fields.map(e => e.replace('measurements.', '')).reduce((s, e) => {s[e] = true; return s; }, {}); const projection = filters.fields.map(e => e.replace('measurements.', ''))
if (filters.fields.indexOf('added') >= 0) { // add added date .reduce((s, e) => {s[e] = true; return s; }, {});
if (filters.fields.indexOf('added') >= 0) { // add added date // TODO: upgrade MongoDB version or find alternative
// projection.added = {$toDate: '$_id'}; // projection.added = {$toDate: '$_id'};
// projection.added = { $convert: { input: '$_id', to: "date" } } // TODO: upgrade MongoDB version or find alternative // projection.added = { $convert: { input: '$_id', to: "date" } }
} }
if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) { // disable _id explicitly if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) { // disable _id explicitly
projection._id = false; projection._id = false;
@ -347,7 +388,10 @@ router.get('/samples', async (req, res, next) => {
if (filters['to-page'] < 0) { if (filters['to-page'] < 0) {
data.reverse(); data.reverse();
} }
const measurementFields = _.uniq([filters.sort[0].split('.')[1], ...measurementFilterFields, ...measurementFieldsFields]); const measurementFields = _.uniq(
[filters.sort[0].split('.')[1],
...measurementFilterFields, ...measurementFieldsFields]
);
if (filters.csv) { // output as csv if (filters.csv) { // output as csv
csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => { csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => {
if (err) return next(err); if (err) return next(err);
@ -355,8 +399,8 @@ router.get('/samples', async (req, res, next) => {
res.send(data); res.send(data);
}); });
} }
else { else { // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields)))); // validate all and filter null values from validation errors res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))));
} }
}); });
} }
@ -385,16 +429,17 @@ router.get('/samples', async (req, res, next) => {
}); });
router.get('/samples/:state(new|deleted)', (req, res, next) => { router.get('/samples/:state(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
SampleModel.find({status: globals.status[req.params.state]}).lean().exec((err, data) => { SampleModel.find({status: globals.status[req.params.state]}).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => SampleValidate.output(e))));
}); });
}); });
router.get('/samples/count', (req, res, next) => { router.get('/samples/count', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
SampleModel.estimatedDocumentCount((err, data) => { SampleModel.estimatedDocumentCount((err, data) => {
if (err) return next(err); if (err) return next(err);
@ -403,18 +448,20 @@ router.get('/samples/count', (req, res, next) => {
}); });
router.get('/sample/' + IdValidate.parameter(), (req, res, next) => { router.get('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => { SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id')
if (err) return next(err); .exec(async (err, sampleData: any) => {
if (err) return next(err);
await sampleReturn(sampleData, req, res, next); await sampleReturn(sampleData, req, res, next);
}); });
}); });
router.put('/sample/' + IdValidate.parameter(), (req, res, next) => { router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
const {error, value: sample} = SampleValidate.input(req.body, 'change'); const {error, value: sample} = SampleValidate.input(req.body, 'change');
console.log(error);
if (error) return res400(error, res); if (error) return res400(error, res);
SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists
@ -426,16 +473,19 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
return res.status(403).json({status: 'Forbidden'}); return res.status(403).json({status: 'Forbidden'});
} }
// only maintain and admin are allowed to edit other user's data // only dev 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 (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return;
if (sample.hasOwnProperty('material_id')) { if (sample.hasOwnProperty('material_id')) {
if (!await materialCheck(sample, res, next)) return; if (!await materialCheck(sample, res, next)) return;
} }
else if (sample.hasOwnProperty('color')) { else if (sample.hasOwnProperty('color')) {
if (!await materialCheck(sample, res, next, sampleData.material_id)) return; 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 // 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; if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) {
if (!await conditionCheck(sample.condition, 'change', res, next,
!(sampleData.condition.condition_template &&
sampleData.condition.condition_template.toString() === sample.condition.condition_template))) return;
} }
if (sample.hasOwnProperty('notes')) { if (sample.hasOwnProperty('notes')) {
@ -443,7 +493,8 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (sampleData.note_id !== null) { // old notes data exists if (sampleData.note_id !== null) { // old notes data exists
const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any; const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any;
if (data instanceof Error) return; if (data instanceof Error) return;
newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); // check if notes were changed // check if notes were changed
newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes);
if (newNotes) { if (newNotes) {
if (data.hasOwnProperty('custom_fields')) { // update note_fields if (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1, req); customFieldsChange(Object.keys(data.custom_fields), -1, req);
@ -456,7 +507,8 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes
if (!await sampleRefCheck(sample, res, next)) return; if (!await sampleRefCheck(sample, res, next)) return;
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields // new custom_fields
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
} }
let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes
@ -480,7 +532,7 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
}); });
router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => { router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists
if (err) return next(err); if (err) return next(err);
@ -488,14 +540,16 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
// only maintain and admin are allowed to edit other user's data // only dev 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 (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['dev', 'admin'], 'basic')) return;
await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { // set sample status // set sample status
await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => {
if (err) return next(err); if (err) return next(err);
// set status of associated measurements also to deleted // set status of associated measurements also to deleted
MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: -1}).log(req).lean().exec(err => { MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: -1})
.log(req).lean().exec(err => {
if (err) return next(err); if (err) return next(err);
if (sampleData.note_id !== null) { // handle notes if (sampleData.note_id !== null) { // handle notes
@ -516,16 +570,17 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
}); });
router.get('/sample/number/:number', (req, res, next) => { router.get('/sample/number/:number', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', '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) => { 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); if (err) return next(err);
await sampleReturn(sampleData, req, res, next); await sampleReturn(sampleData, req, res, next);
}); });
}); });
router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => { router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).log(req).lean().exec((err, data) => { SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).log(req).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
@ -538,47 +593,34 @@ router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => {
}); });
router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => { router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
SampleModel.findById(req.params.id).lean().exec((err, data: any) => { SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.validated}).log(req).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
if (!data) { if (!data) {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
if (Object.keys(data.condition).length === 0) {
return res.status(400).json({status: 'Sample without condition cannot be valid'});
}
MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => { res.json({status: 'OK'});
if (err) return next(err);
if (data.length === 0) {
return res.status(400).json({status: 'Sample without measurements cannot be valid'});
}
SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.validated}).log(req).lean().exec(err => {
if (err) return next(err);
res.json({status: 'OK'});
});
});
}); });
}); });
router.post('/sample/new', async (req, res, next) => { router.post('/sample/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'dev', 'admin'], 'basic')) return;
if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified
req.body.condition = {}; req.body.condition = {};
} }
const {error, value: sample} = SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : '')); const {error, value: sample} =
SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : ''));
if (error) return res400(error, res); if (error) return res400(error, res);
if (!await materialCheck(sample, res, next)) return; if (!await materialCheck(sample, res, next)) return;
if (!await sampleRefCheck(sample, res, next)) return; if (!await sampleRefCheck(sample, res, next)) return;
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields // new custom_fields
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req); customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
} }
@ -611,25 +653,31 @@ router.post('/sample/new', async (req, res, next) => {
}); });
router.get('/sample/notes/fields', (req, res, next) => { router.get('/sample/notes/fields', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'all')) return;
NoteFieldModel.find({}).lean().exec((err, data) => { NoteFieldModel.find({}).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(_.compact(data.map(e => NoteFieldValidate.output(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => NoteFieldValidate.output(e))));
}) })
}); });
module.exports = router; module.exports = router;
// store the highest generated number for each location to avoid duplicate numbers
const numberBuffer: {[location: string]: number} = {};
async function numberGenerate (sample, req, res, next) { // generate number in format Location32, returns false on error // generate number in format Location32, returns false on error
async function numberGenerate (sample, req, res, next) {
const sampleData = await SampleModel const sampleData = await SampleModel
.aggregate([ .aggregate([
{$match: {number: new RegExp('^' + req.authDetails.location + '[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: {number2: {$toDecimal: {$arrayElemAt: [{$split: [{$arrayElemAt:
// [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}}}}, // not working with MongoDb 3.6
{$addFields: {sortNumber: {$let: { {$addFields: {sortNumber: {$let: {
vars: {tmp: {$concat: ['000000000000000000000000000000', {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}}, vars: {tmp: {$concat: ['000000000000000000000000000000',
{$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}},
in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]} in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]}
}}}}, }}}},
{$sort: {sortNumber: -1}}, {$sort: {sortNumber: -1}},
@ -638,11 +686,18 @@ async function numberGenerate (sample, req, res, next) { // generate number in
.exec() .exec()
.catch(err => next(err)); .catch(err => next(err));
if (sampleData instanceof Error) return false; if (sampleData instanceof Error) return false;
return req.authDetails.location + (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1); let number = (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) : 0);
if (numberBuffer[req.authDetails.location] && numberBuffer[req.authDetails.location] >= number) {
number = numberBuffer[req.authDetails.location];
}
number ++;
numberBuffer[req.authDetails.location] = number;
return req.authDetails.location + number;
} }
async function numberCheck(sample, res, next) { async function numberCheck(sample, res, next) {
const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;}); const sampleData = await SampleModel.findOne({number: sample.number})
.lean().exec().catch(err => {next(err); return false;});
if (sampleData) { // found entry with sample number if (sampleData) { // found entry with sample number
res.status(400).json({status: 'Sample number already taken'}); res.status(400).json({status: 'Sample number already taken'});
return false return false
@ -650,7 +705,8 @@ async function numberCheck(sample, res, next) {
return true; return true;
} }
async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid // validate material_id and color, returns false if invalid
async function materialCheck (sample, res, next, id = sample.material_id) {
const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any; const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any;
if (materialData instanceof Error) return false; if (materialData instanceof Error) return false;
if (!materialData) { // could not find material_id if (!materialData) { // could not find material_id
@ -660,12 +716,14 @@ async function materialCheck (sample, res, next, id = sample.material_id) { //
return true; return true;
} }
async function conditionCheck (condition, param, res, next, checkVersion = true) { // validate treatment template, returns false if invalid, otherwise template data // validate treatment template, returns false if invalid, otherwise template data
async function conditionCheck (condition, param, res, next, checkVersion = true) {
if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found
res.status(400).json({status: 'Condition template not available'}); res.status(400).json({status: 'Condition template not available'});
return false; return false;
} }
const conditionData = await ConditionTemplateModel.findById(condition.condition_template).lean().exec().catch(err => next(err)) as any; const conditionData = await ConditionTemplateModel.findById(condition.condition_template)
.lean().exec().catch(err => next(err)) as any;
if (conditionData instanceof Error) return false; if (conditionData instanceof Error) return false;
if (!conditionData) { // template not found if (!conditionData) { // template not found
res.status(400).json({status: 'Condition template not available'}); res.status(400).json({status: 'Condition template not available'});
@ -674,7 +732,8 @@ async function conditionCheck (condition, param, res, next, checkVersion = true)
if (checkVersion) { if (checkVersion) {
// get all template versions and check if given is latest // get all template versions and check if given is latest
const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any; const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id})
.sort({version: -1}).lean().exec().catch(err => next(err)) as any;
if (conditionVersions instanceof Error) return false; if (conditionVersions instanceof Error) return false;
if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest
res.status(400).json({status: 'Old template version not allowed'}); res.status(400).json({status: 'Old template version not allowed'});
@ -683,14 +742,16 @@ async function conditionCheck (condition, param, res, next, checkVersion = true)
} }
// validate parameters // validate parameters
const {error, value: ignore} = ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param); const {error, value: ignore} =
ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param);
if (error) {res400(error, res); return false;} if (error) {res400(error, res); return false;}
return conditionData; return conditionData;
} }
function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference
return new Promise(resolve => { return new Promise(resolve => {
if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { // there are sample_references // there are sample_references
if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) {
let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations
sample.notes.sample_references.forEach(reference => { sample.notes.sample_references.forEach(reference => {
@ -715,7 +776,8 @@ function sampleRefCheck (sample, res, next) { // validate sample_references, re
function customFieldsChange (fields, amount, req) { // update custom_fields and respective quantities function customFieldsChange (fields, amount, req) { // update custom_fields and respective quantities
fields.forEach(field => { fields.forEach(field => {
NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true}).log(req).lean().exec((err, data: any) => { // check if field exists NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true})
.log(req).lean().exec((err, data: any) => { // check if field exists
if (err) return console.error(err); if (err) return console.error(err);
if (!data) { // new field if (!data) { // new field
new NoteFieldModel({name: field, qty: 1}).save((err, data) => { new NoteFieldModel({name: field, qty: 1}).save((err, data) => {
@ -735,11 +797,27 @@ function customFieldsChange (fields, amount, req) { // update custom_fields and
function sortQuery(filters, sortKeys, sortStartValue) { // sortKeys = ['primary key', 'secondary key'] function sortQuery(filters, sortKeys, sortStartValue) { // sortKeys = ['primary key', 'secondary key']
if (filters['from-id']) { // from-id specified if (filters['from-id']) { // from-id specified
if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc
return [{$match: {$or: [{[sortKeys[0]]: {$gt: sortStartValue}}, {$and: [{[sortKeys[0]]: sortStartValue}, {[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}}]}]}}, return [
{$sort: {[sortKeys[0]]: 1, _id: 1}}]; {$match: {$or: [
{[sortKeys[0]]: {$gt: sortStartValue}},
{$and: [
{[sortKeys[0]]: sortStartValue},
{[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}}
]}
]}},
{$sort: {[sortKeys[0]]: 1, _id: 1}}
];
} else { } else {
return [{$match: {$or: [{[sortKeys[0]]: {$lt: sortStartValue}}, {$and: [{[sortKeys[0]]: sortStartValue}, {[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}}]}]}}, return [
{$sort: {[sortKeys[0]]: -1, _id: -1}}]; {$match: {$or: [
{[sortKeys[0]]: {$lt: sortStartValue}},
{$and: [
{[sortKeys[0]]: sortStartValue},
{[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}}
]}
]}},
{$sort: {[sortKeys[0]]: -1, _id: -1}}
];
} }
} else { // sort from beginning } else { // sort from beginning
return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}]; // set _id as secondary sort return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}]; // set _id as secondary sort
@ -775,30 +853,57 @@ function filterQueries (filters) {
return {[e.field]: {['$in']: [new RegExp(e.values[0])]}}; return {[e.field]: {['$in']: [new RegExp(e.values[0])]}};
} }
else { 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 // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin
return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}};
} }
}); });
} }
// add measurements as property [template.name], if one result, array is reduced to direct values
function addMeasurements(queryPtr, template) {
queryPtr.push(
{$addFields: {[template.name]: {$let: {vars: {
arr: {$filter: {
input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}
}}},
in: {$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']}
}}}},
{$addFields: {[template.name]: {$cond: [
'$' + template.name + '.values',
'$' + template.name + '.values',
template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})
]}}}
);
}
function dateToOId (date) { // convert date to ObjectId function dateToOId (date) { // convert date to ObjectId
return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000'); return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000');
} }
async function sampleReturn (sampleData, req, res, next) { async function sampleReturn (sampleData, req, res, next) {
if (sampleData) { if (sampleData) {
console.log(sampleData); await sampleData.populate('material_id.group_id').populate('material_id.supplier_id')
await sampleData.populate('material_id.group_id').populate('material_id.supplier_id').execPopulate().catch(err => next(err)); .execPopulate().catch(err => next(err));
if (sampleData instanceof Error) return; if (sampleData instanceof Error) return;
sampleData = sampleData.toObject(); sampleData = sampleData.toObject();
if (sampleData.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin // deleted samples only available for dev/admin
if (sampleData.status === globals.status.deleted && !req.auth(res, ['dev', 'admin'], 'all')) return;
sampleData.material = sampleData.material_id; // map data to right keys sampleData.material = sampleData.material_id; // map data to right keys
sampleData.material.group = sampleData.material.group_id.name; sampleData.material.group = sampleData.material.group_id.name;
sampleData.material.supplier = sampleData.material.supplier_id.name; sampleData.material.supplier = sampleData.material.supplier_id.name;
sampleData.user = sampleData.user_id.name; sampleData.user = sampleData.user_id.name;
sampleData.notes = sampleData.note_id ? sampleData.note_id : {}; sampleData.notes = sampleData.note_id ? sampleData.note_id : {};
MeasurementModel.find({sample_id: sampleData._id, status: {$ne: globals.status.deleted}}).lean().exec((err, data) => { MeasurementModel.find({sample_id: sampleData._id, status: {$ne: globals.status.deleted}})
.lean().exec((err, data) => {
sampleData.measurements = data; sampleData.measurements = data;
if (['dev', 'admin'].indexOf(req.authDetails.level) < 0) { // strip dpt values if not dev or admin
sampleData.measurements.forEach(measurement => {
if (measurement.values.dpt) {
delete measurement.values.dpt;
}
});
}
res.json(SampleValidate.output(sampleData, 'details')); res.json(SampleValidate.output(sampleData, 'details'));
}); });
} }

View File

@ -4,6 +4,7 @@ import TemplateConditionModel from '../models/condition_template';
import TemplateMeasurementModel from '../models/measurement_template'; import TemplateMeasurementModel from '../models/measurement_template';
import TestHelper from "../test/helper"; import TestHelper from "../test/helper";
// TODO: method to return only latest template versions -> rework frontend accordingly
describe('/template', () => { describe('/template', () => {
let server; let server;

View File

@ -15,17 +15,18 @@ import db from '../db';
const router = express.Router(); const router = express.Router();
router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => { router.get('/template/:collection(measurements|conditions|materials)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s
model(req).find({}).lean().exec((err, data) => { model(req).find({}).lean().exec((err, data) => {
if (err) next (err); if (err) next (err);
res.json(_.compact(data.map(e => TemplateValidate.output(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => TemplateValidate.output(e))));
}); });
}); });
router.get('/template/:collection(measurement|condition|material)/' + 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; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
model(req).findById(req.params.id).lean().exec((err, data) => { model(req).findById(req.params.id).lean().exec((err, data) => {
if (err) next (err); if (err) next (err);
@ -38,8 +39,9 @@ router.get('/template/:collection(measurement|condition|material)/' + IdValidate
}); });
}); });
router.put('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(), async (req, res, next) => { router.put('/template/:collection(measurement|condition|material)/' + IdValidate.parameter(),
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return; async (req, res, next) => {
if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
const {error, value: template} = TemplateValidate.input(req.body, 'change'); const {error, value: template} = TemplateValidate.input(req.body, 'change');
if (error) return res400(error, res); if (error) return res400(error, res);
@ -51,7 +53,8 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
} }
// find latest version // find latest version
const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1}).lean().exec().catch(err => {next(err);}) as any; const templateData = await model(req).findOne({first_id: templateRef.first_id}).sort({version: -1})
.lean().exec().catch(err => {next(err);}) as any;
if (templateData instanceof Error) return; if (templateData instanceof Error) return;
if (!templateData) { if (!templateData) {
return res.status(404).json({status: 'Not found'}); return res.status(404).json({status: 'Not found'});
@ -59,7 +62,8 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed
template.version = templateData.version + 1; // increase version template.version = templateData.version + 1; // increase version
await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { // save new template, fill with old properties // save new template, fill with old properties
await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => {
if (err) next (err); if (err) next (err);
db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject()); db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
res.json(TemplateValidate.output(data.toObject())); res.json(TemplateValidate.output(data.toObject()));
@ -71,7 +75,7 @@ router.put('/template/:collection(measurement|condition|material)/' + IdValidate
}); });
router.post('/template/:collection(measurement|condition|material)/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; if (!req.auth(res, ['dev', 'admin'], 'basic')) return;
const {error, value: template} = TemplateValidate.input(req.body, 'new'); const {error, value: template} = TemplateValidate.input(req.body, 'new');
if (error) return res400(error, res); if (error) return res400(error, res);

View File

@ -564,7 +564,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Invalid body format', details: '"level" must be one of [read, write, maintain, dev, admin]'} res: {status: 'Invalid body format', details: '"level" must be one of [read, write, dev, admin]'}
}); });
}); });
it('rejects an invalid email address', done => { it('rejects an invalid email address', done => {

View File

@ -16,12 +16,15 @@ router.get('/users', (req, res) => {
if (!req.auth(res, ['admin'], 'basic')) return; if (!req.auth(res, ['admin'], 'basic')) return;
UserModel.find({}).lean().exec( (err, data:any) => { UserModel.find({}).lean().exec( (err, data:any) => {
res.json(_.compact(data.map(e => UserValidate.output(e)))); // validate all and filter null values from validation errors // validate all and filter null values from validation errors
res.json(_.compact(data.map(e => UserValidate.output(e))));
}); });
}); });
router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new.
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; // See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
const username = getUsername(req, res); const username = getUsername(req, res);
if (!username) return; if (!username) return;
@ -36,13 +39,15 @@ router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
}); });
}); });
router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
const username = getUsername(req, res); const username = getUsername(req, res);
if (!username) return; if (!username) return;
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); const {error, value: user} = UserValidate.input(req.body, 'change' +
(req.authDetails.level === 'admin'? 'admin' : ''));
if (error) return res400(error, res); if (error) return res400(error, res);
if (user.hasOwnProperty('pass')) { if (user.hasOwnProperty('pass')) {
@ -66,8 +71,10 @@ router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => {
}); });
// TODO: only possible if no data is linked to user, otherwise change status, etc. // TODO: only possible if no data is linked to user, otherwise change status, etc.
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new.
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; // See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
const username = getUsername(req, res); const username = getUsername(req, res);
if (!username) return; if (!username) return;
@ -84,7 +91,7 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { //
}); });
router.get('/user/key', (req, res, next) => { router.get('/user/key', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['read', 'write', 'dev', 'admin'], 'basic')) return;
UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => { UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => {
if (err) return next(err); if (err) return next(err);
@ -126,7 +133,10 @@ router.post('/user/passreset', (req, res, next) => {
if (err) return next(err); if (err) return next(err);
// send email // send email
mail(data[0].email, 'Your new password for the DFOP database', 'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.<br><br>The DFOP team', err => { mail(data[0].email, 'Your new password for the DeFinMa database',
'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '' +
'<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.' +
'<br><br>The DeFinMa team', err => {
if (err) return next(err); if (err) return next(err);
res.json({status: 'OK'}); res.json({status: 'OK'});
}); });

View File

@ -34,8 +34,12 @@ export default class MeasurementValidate {
} }
} }
static output (data) { // validate output and strip unwanted properties, returns null if not valid static output (data, req) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
// spectral data not allowed for read/write users
if (['dev', 'admin'].indexOf(req.authDetails.level) < 0 && data.values.dpt) {
delete data.values.dpt;
}
const {value, error} = Joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),
sample_id: IdValidate.get(), sample_id: IdValidate.get(),

View File

@ -15,7 +15,7 @@ export default class SampleValidate {
.allow(''), .allow(''),
type: Joi.string() type: Joi.string()
.max(128), .valid('granulate', 'part', 'tension rod'),
batch: Joi.string() batch: Joi.string()
.max(128) .max(128)
@ -116,7 +116,8 @@ export default class SampleValidate {
} }
} }
static output (data, param = 'refs+added', additionalParams = []) { // validate output and strip unwanted properties, returns null if not valid // validate output and strip unwanted properties, returns null if not valid
static output (data, param = 'refs+added', additionalParams = []) {
if (param === 'refs+added') { if (param === 'refs+added') {
param = 'refs'; param = 'refs';
data.added = data._id.getTimestamp(); data.added = data._id.getTimestamp();
@ -169,12 +170,16 @@ export default class SampleValidate {
if (filterValidation.error) return filterValidation; if (filterValidation.error) return filterValidation;
try { try {
for (let i in data.filters) { for (let i in data.filters) {
// data.filters[i] = JSON.parse(decodeURIComponent(data.filters[i]));
data.filters[i] = JSON.parse(data.filters[i]); data.filters[i] = JSON.parse(data.filters[i]);
data.filters[i].values = data.filters[i].values.map(e => { // validate filter values data.filters[i].values = data.filters[i].values.map(e => { // validate filter values
let validator; let validator;
let field = data.filters[i].field let field = data.filters[i].field
if (/material\./.test(field)) { // select right validation model if (/material\./.test(field)) { // select right validation model
validator = MaterialValidate.outputV().append({number: Joi.string().max(128).allow(''), properties: Joi.alternatives().try(Joi.number(), Joi.string().max(128))}); 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]; field = field.replace('material.', '').split('.')[0];
} }
else if (/measurements\./.test(field)) { else if (/measurements\./.test(field)) {
@ -194,12 +199,12 @@ export default class SampleValidate {
validator = Joi.object(this.sample); validator = Joi.object(this.sample);
} }
const {value, error} = validator.validate({[field]: e}); const {value, error} = validator.validate({[field]: e});
if (error) throw error; // reject invalid values // TODO: return exact error description, handle in frontend filters if (error) throw error; // reject invalid values
return value[field]; return value[field];
}); });
} }
} }
catch { catch (err) {
return {error: {details: [{message: 'Invalid JSON string for filter parameter'}]}, value: null} return {error: {details: [{message: 'Invalid JSON string for filter parameter'}]}, value: null}
} }
} }
@ -208,13 +213,22 @@ export default class SampleValidate {
'from-id': IdValidate.get(), 'from-id': IdValidate.get(),
'to-page': Joi.number().integer(), 'to-page': Joi.number().integer(),
'page-size': Joi.number().integer().min(1), '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'), sort: Joi.string().pattern(
new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm')
).default('_id-asc'),
csv: Joi.boolean().default(false), 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']).messages({'string.pattern.base': 'Invalid field name'}), 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({ filters: Joi.array().items(Joi.object({
mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin', 'stringin'), 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'}), field: Joi.string().pattern(
values: Joi.array().items(Joi.alternatives().try(Joi.string().max(128), Joi.number(), Joi.boolean(), Joi.date().iso(), Joi.object())).min(1) 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([]) })).default([])
}).with('to-page', 'page-size').validate(data); }).with('to-page', 'page-size').validate(data);
} }

View File

@ -1,7 +1,7 @@
import Joi from '@hapi/joi'; import Joi from '@hapi/joi';
import IdValidate from './id'; import IdValidate from './id';
// TODO: do not allow a . in the name // TODO: do not allow a . in the name !!!
export default class TemplateValidate { export default class TemplateValidate {
private static template = { private static template = {
name: Joi.string() name: Joi.string()

View File

@ -99,7 +99,7 @@
{ {
"_id": {"$oid":"400000000000000000000007"}, "_id": {"$oid":"400000000000000000000007"},
"number": "34", "number": "34",
"type": "liquid", "type": "part",
"color": "black", "color": "black",
"batch": "", "batch": "",
"condition": {}, "condition": {},

View File

@ -29,7 +29,10 @@ export default class TestHelper {
} }
static beforeEach (server, done) { static beforeEach (server, done) {
delete require.cache[require.resolve('../index')]; // prevent loading from cache // delete cached server code except models as these are needed in the testing files as well
Object.keys(require.cache).filter(e => /API\\dist\\(?!(models|db|test))/.test(e)).forEach(key => {
delete require.cache[key]; // prevent loading from cache
});
server = require('../index'); server = require('../index');
db.drop(err => { // reset database db.drop(err => { // reset database
if (err) return done(err); if (err) return done(err);
@ -38,10 +41,13 @@ export default class TestHelper {
return server return server
} }
static request (server, done, options) { // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, default (set to false if you want to dismiss default .end handling)} // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res,
// default (set to false if you want to dismiss default .end handling)}
static request (server, done, options) {
let st = supertest(server); let st = supertest(server);
if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key
options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key); options.url += '?key=' +
(this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key);
} }
switch (options.method) { // http method switch (options.method) { // http method
case 'get': case 'get':
@ -91,10 +97,12 @@ export default class TestHelper {
done(); done();
}); });
} }
else if (options.hasOwnProperty('log')) { // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)} // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)}
else if (options.hasOwnProperty('log')) {
return st.end(err => { return st.end(err => {
if (err) return done (err); if (err) return done (err);
ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0).lean().exec((err, data) => { // latest entry ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0)
.lean().exec((err, data) => { // latest entry
if (err) return done(err); if (err) return done(err);
should(data).have.only.keys('_id', 'action', 'collectionName', 'conditions', 'data', 'user_id', '__v'); should(data).have.only.keys('_id', 'action', 'collectionName', 'conditions', 'data', 'user_id', '__v');
should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url); should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url);