diff --git a/.idea/dictionaries/VLE2FE.xml b/.idea/dictionaries/VLE2FE.xml
index 1dd7309..5337928 100644
--- a/.idea/dictionaries/VLE2FE.xml
+++ b/.idea/dictionaries/VLE2FE.xml
@@ -5,6 +5,7 @@
cfenv
dfopdb
janedoe
+ pagesize
testcomment
diff --git a/api/api.yaml b/api/api.yaml
index 9090378..d281206 100644
--- a/api/api.yaml
+++ b/api/api.yaml
@@ -62,7 +62,7 @@ tags:
paths:
allOf:
- - $ref: 'others.yaml'
+ - $ref: 'root.yaml'
- $ref: 'sample.yaml'
- $ref: 'material.yaml'
- $ref: 'measurement.yaml'
diff --git a/api/material.yaml b/api/material.yaml
index 967071c..378628d 100644
--- a/api/material.yaml
+++ b/api/material.yaml
@@ -19,9 +19,9 @@
500:
$ref: 'api.yaml#/components/responses/500'
-/materials/{group}:
+/materials/{state}:
parameters:
- - $ref: 'api.yaml#/components/parameters/Group'
+ - $ref: 'api.yaml#/components/parameters/State'
get:
summary: lists all new/deleted materials
description: 'Auth: basic, levels: maintain, admin'
@@ -140,6 +140,29 @@
500:
$ref: 'api.yaml#/components/responses/500'
+/material/validate/{id}:
+ parameters:
+ - $ref: 'api.yaml#/components/parameters/Id'
+ put:
+ summary: restore material
+ description: 'Auth: basic, levels: maintain, admin'
+ x-doc: status is set to 10
+ tags:
+ - /material
+ security:
+ - BasicAuth: []
+ responses:
+ 200:
+ $ref: 'api.yaml#/components/responses/Ok'
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
+ 404:
+ $ref: 'api.yaml#/components/responses/404'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
/material/new:
post:
summary: add material
@@ -168,5 +191,51 @@
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
+/material/groups:
+ get:
+ summary: list all existing material groups
+ description: 'Auth: all, levels: read, write, maintain, dev, admin'
+ tags:
+ - /material
+ responses:
+ 200:
+ description: all material groups
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+ example: PA66
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
+/material/suppliers:
+ get:
+ summary: list all existing material suppliers
+ description: 'Auth: all, levels: read, write, maintain, dev, admin'
+ tags:
+ - /material
+ responses:
+ 200:
+ description: all material suppliers
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: string
+ example: BASF
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
\ No newline at end of file
diff --git a/api/measurement.yaml b/api/measurement.yaml
index 3068d97..0c29e77 100644
--- a/api/measurement.yaml
+++ b/api/measurement.yaml
@@ -100,6 +100,29 @@
500:
$ref: 'api.yaml#/components/responses/500'
+/measurement/validate/{id}:
+ parameters:
+ - $ref: 'api.yaml#/components/parameters/Id'
+ put:
+ summary: set measurement status to validated
+ description: 'Auth: basic, levels: maintain, admin'
+ x-doc: status is set to 10
+ tags:
+ - /measurement
+ security:
+ - BasicAuth: []
+ responses:
+ 200:
+ $ref: 'api.yaml#/components/responses/Ok'
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
+ 404:
+ $ref: 'api.yaml#/components/responses/404'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
/measurement/new:
post:
summary: add measurement
@@ -129,4 +152,4 @@
403:
$ref: 'api.yaml#/components/responses/403'
500:
- $ref: 'api.yaml#/components/responses/500'
\ No newline at end of file
+ $ref: 'api.yaml#/components/responses/500'
diff --git a/api/others.yaml b/api/others.yaml
deleted file mode 100644
index a953bf8..0000000
--- a/api/others.yaml
+++ /dev/null
@@ -1,43 +0,0 @@
-/:
- get:
- summary: Root method
- description: 'Auth: none'
- tags:
- - /
- security: []
- responses:
- 200:
- description: Server is working
- content:
- application/json:
- schema:
- properties:
- status:
- type: string
- example: 'API server up and running!'
- 500:
- $ref: 'api.yaml#/components/responses/500'
-
-/authorized:
- get:
- summary: Checks authorization
- description: 'Auth: all, levels: read, write, maintain, dev, admin'
- tags:
- - /
- responses:
- 200:
- description: Authorized
- content:
- application/json:
- schema:
- properties:
- status:
- type: string
- example: 'Authorization successful'
- method:
- type: string
- example: 'basic'
- 401:
- $ref: 'api.yaml#/components/responses/401'
- 500:
- $ref: 'api.yaml#/components/responses/500'
\ No newline at end of file
diff --git a/api/parameters.yaml b/api/parameters.yaml
index b4586f7..3cbe49b 100644
--- a/api/parameters.yaml
+++ b/api/parameters.yaml
@@ -14,7 +14,7 @@ Name:
schema:
type: string
-Group:
+State:
name: group
description: 'possible values: new, deleted'
in: path
diff --git a/api/root.yaml b/api/root.yaml
new file mode 100644
index 0000000..3070412
--- /dev/null
+++ b/api/root.yaml
@@ -0,0 +1,102 @@
+/:
+ get:
+ summary: Root method
+ description: 'Auth: none'
+ tags:
+ - /
+ security: []
+ responses:
+ 200:
+ description: Server is working
+ content:
+ application/json:
+ schema:
+ properties:
+ status:
+ type: string
+ example: 'API server up and running!'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
+/authorized:
+ get:
+ summary: Checks authorization
+ description: 'Auth: all, levels: read, write, maintain, dev, admin'
+ tags:
+ - /
+ responses:
+ 200:
+ description: Authorized
+ content:
+ application/json:
+ schema:
+ properties:
+ status:
+ type: string
+ example: 'Authorization successful'
+ method:
+ type: string
+ example: 'basic'
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
+/changelog/{timestamp}/{page}/{pagesize}:
+ parameters:
+ - name: timestamp
+ in: path
+ required: true
+ schema:
+ type: string
+ example: 1970-01-01T00:00:00.000Z
+ - name: page
+ in: path
+ required: true
+ schema:
+ type: number
+ example: 3
+ - name: pagesize
+ in: path
+ required: true
+ schema:
+ type: number
+ example: 30
+ get:
+ summary: get changelog
+ description: 'Auth: basic, levels: maintain, admin
Displays all logs older than timestamp, sorted by date descending, page defaults to 0, pagesize defaults to 25
Avoid using high page numbers for older logs, better use an older timestamp'
+ tags:
+ - /
+ responses:
+ 200:
+ description: Changelog
+ content:
+ application/json:
+ schema:
+ properties:
+ date:
+ type: string
+ example: 1970-01-01T00:00:00.000Z
+ action:
+ type: string
+ example: PUT /sample/400000000000000000000001
+ collection:
+ type: string
+ example: samples
+ conditions:
+ type: object
+ example:
+ _id: '400000000000000000000001'
+ data:
+ type: object
+ example:
+ type: part
+ status: 0
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
+ 404:
+ $ref: 'api.yaml#/components/responses/404'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
\ No newline at end of file
diff --git a/api/sample.yaml b/api/sample.yaml
index 67f25ac..eae0ddc 100644
--- a/api/sample.yaml
+++ b/api/sample.yaml
@@ -19,9 +19,9 @@
500:
$ref: 'api.yaml#/components/responses/500'
-/samples/{group}:
+/samples/{state}:
parameters:
- - $ref: 'api.yaml#/components/parameters/Group'
+ - $ref: 'api.yaml#/components/parameters/State'
get:
summary: all new/deleted samples in overview
description: 'Auth: basic, levels: maintain, admin'
@@ -142,10 +142,35 @@
500:
$ref: 'api.yaml#/components/responses/500'
+/sample/validate/{id}:
+ parameters:
+ - $ref: 'api.yaml#/components/parameters/Id'
+ put:
+ summary: set sample status to validated
+ description: 'Auth: basic, levels: maintain, admin'
+ x-doc: status is set to 10
+ tags:
+ - /sample
+ security:
+ - BasicAuth: []
+ responses:
+ 200:
+ $ref: 'api.yaml#/components/responses/Ok'
+ 400:
+ $ref: 'api.yaml#/components/responses/400'
+ 401:
+ $ref: 'api.yaml#/components/responses/401'
+ 403:
+ $ref: 'api.yaml#/components/responses/403'
+ 404:
+ $ref: 'api.yaml#/components/responses/404'
+ 500:
+ $ref: 'api.yaml#/components/responses/500'
+
/sample/new:
post:
summary: add sample
- description: 'Auth: basic, levels: write, maintain, dev, admin'
+ description: 'Auth: basic, levels: write, maintain, dev, admin. Number property is only for admin when adding existing samples'
x-doc: 'Adds status: 0 automatically'
tags:
- /sample
@@ -156,7 +181,12 @@
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/Sample'
+ allOf:
+ - $ref: 'api.yaml#/components/schemas/Sample'
+ properties:
+ number:
+ type: string
+ readOnly: false
responses:
200:
description: samples details
diff --git a/api/schemas.yaml b/api/schemas.yaml
index e76cfb0..21ceddf 100644
--- a/api/schemas.yaml
+++ b/api/schemas.yaml
@@ -69,6 +69,7 @@ Sample:
relation:
type: string
example: part to this sample
+
SampleDetail:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
@@ -165,14 +166,6 @@ Template:
min: 0
max: 2
-ConditionTemplate:
- allOf:
- - $ref: 'api.yaml#/components/schemas/Template'
- properties:
- number_prefix:
- type: string
- example: B
-
Email:
properties:
email:
diff --git a/api/template.yaml b/api/template.yaml
index 71a282f..4fa938d 100644
--- a/api/template.yaml
+++ b/api/template.yaml
@@ -14,7 +14,7 @@
schema:
type: array
items:
- $ref: 'api.yaml#/components/schemas/ConditionTemplate'
+ $ref: 'api.yaml#/components/schemas/Template'
401:
$ref: 'api.yaml#/components/responses/401'
500:
@@ -36,7 +36,7 @@
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/ConditionTemplate'
+ $ref: 'api.yaml#/components/schemas/Template'
401:
$ref: 'api.yaml#/components/responses/401'
404:
@@ -56,14 +56,14 @@
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/ConditionTemplate'
+ $ref: 'api.yaml#/components/schemas/Template'
responses:
200:
description: condition details
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/ConditionTemplate'
+ $ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
@@ -88,14 +88,14 @@
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/ConditionTemplate'
+ $ref: 'api.yaml#/components/schemas/Template'
responses:
200:
description: condition details
content:
application/json:
schema:
- $ref: 'api.yaml#/components/schemas/ConditionTemplate'
+ $ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
diff --git a/build.bat b/build.bat
new file mode 100644
index 0000000..d632b14
--- /dev/null
+++ b/build.bat
@@ -0,0 +1,4 @@
+call npm run tsc-full
+copy package.json dist\package.json
+Xcopy /E /I api dist\api
+Xcopy /E /I static dist\static
\ No newline at end of file
diff --git a/data_import/import.js b/data_import/import.js
new file mode 100644
index 0000000..e69de29
diff --git a/package-lock.json b/package-lock.json
index 6d935ee..d3b646e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -678,6 +678,11 @@
"type-is": "~1.6.17"
}
},
+ "bowser": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.9.0.tgz",
+ "integrity": "sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA=="
+ },
"boxen": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz",
@@ -861,6 +866,11 @@
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
"integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
},
+ "camelize": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.0.tgz",
+ "integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
+ },
"cfenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfenv/-/cfenv-1.2.2.tgz",
@@ -998,6 +1008,35 @@
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
"dev": true
},
+ "compressible": {
+ "version": "2.0.18",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
+ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==",
+ "requires": {
+ "mime-db": ">= 1.43.0 < 2"
+ }
+ },
+ "compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ }
+ }
+ },
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1029,6 +1068,11 @@
"resolved": "https://registry.npmjs.org/content-filter/-/content-filter-1.1.2.tgz",
"integrity": "sha512-VaZ4Y7h776r0v2WxWqu3iatjYI6/N0msXK8O1ymtkFWbSvaFoCePksS8U60BS6dUMZeAlqhN09SuM7ghdzRP1Q=="
},
+ "content-security-policy-builder": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz",
+ "integrity": "sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ=="
+ },
"content-type": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
@@ -1092,6 +1136,11 @@
"resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz",
"integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA=="
},
+ "dasherize": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dasherize/-/dasherize-2.0.0.tgz",
+ "integrity": "sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg="
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -1163,6 +1212,16 @@
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A=="
},
+ "dns-prefetch-control": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/dns-prefetch-control/-/dns-prefetch-control-0.2.0.tgz",
+ "integrity": "sha512-hvSnros73+qyZXhHFjx2CMLwoj3Fe7eR9EJsFsqmcI1bB2OBWL/+0YzaEaKssCHnj/6crawNnUyw74Gm2EKe+Q=="
+ },
+ "dont-sniff-mimetype": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz",
+ "integrity": "sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug=="
+ },
"dot-prop": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.2.0.tgz",
@@ -1265,6 +1324,11 @@
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
},
+ "expect-ct": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/expect-ct/-/expect-ct-0.2.0.tgz",
+ "integrity": "sha512-6SK3MG/Bbhm8MsgyJAylg+ucIOU71/FzyFalcfu5nY19dH8y/z0tBJU0wrNBXD4B27EoQtqPF/9wqH0iYAd04g=="
+ },
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
@@ -1308,6 +1372,11 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
+ "feature-policy": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/feature-policy/-/feature-policy-0.3.0.tgz",
+ "integrity": "sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ=="
+ },
"fill-range": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
@@ -1409,6 +1478,11 @@
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
"integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
},
+ "frameguard": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/frameguard/-/frameguard-3.1.0.tgz",
+ "integrity": "sha512-TxgSKM+7LTA6sidjOiSZK9wxY0ffMPY3Wta//MqwmX0nZuEHc8QrkV8Fh3ZhMJeiH+Uyh/tcaarImRy8u77O7g=="
+ },
"fresh": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
@@ -1568,6 +1642,76 @@
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true
},
+ "helmet": {
+ "version": "3.22.0",
+ "resolved": "https://registry.npmjs.org/helmet/-/helmet-3.22.0.tgz",
+ "integrity": "sha512-Xrqicn2nm1ZIUxP3YGuTBmbDL04neKsIT583Sjh0FkiwKDXYCMUqGqC88w3NUvVXtA75JyR2Jn6jw6ZEMOD+ZA==",
+ "requires": {
+ "depd": "2.0.0",
+ "dns-prefetch-control": "0.2.0",
+ "dont-sniff-mimetype": "1.1.0",
+ "expect-ct": "0.2.0",
+ "feature-policy": "0.3.0",
+ "frameguard": "3.1.0",
+ "helmet-crossdomain": "0.4.0",
+ "helmet-csp": "2.10.0",
+ "hide-powered-by": "1.1.0",
+ "hpkp": "2.0.0",
+ "hsts": "2.2.0",
+ "ienoopen": "1.1.0",
+ "nocache": "2.1.0",
+ "referrer-policy": "1.2.0",
+ "x-xss-protection": "1.3.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ }
+ }
+ },
+ "helmet-crossdomain": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz",
+ "integrity": "sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA=="
+ },
+ "helmet-csp": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/helmet-csp/-/helmet-csp-2.10.0.tgz",
+ "integrity": "sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w==",
+ "requires": {
+ "bowser": "2.9.0",
+ "camelize": "1.0.0",
+ "content-security-policy-builder": "2.1.0",
+ "dasherize": "2.0.0"
+ }
+ },
+ "hide-powered-by": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/hide-powered-by/-/hide-powered-by-1.1.0.tgz",
+ "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg=="
+ },
+ "hpkp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/hpkp/-/hpkp-2.0.0.tgz",
+ "integrity": "sha1-EOFCJk52IVpdMMROxD3mTe5tFnI="
+ },
+ "hsts": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/hsts/-/hsts-2.2.0.tgz",
+ "integrity": "sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ==",
+ "requires": {
+ "depd": "2.0.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="
+ }
+ }
+ },
"html-escaper": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
@@ -1599,6 +1743,11 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
+ "ienoopen": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/ienoopen/-/ienoopen-1.1.0.tgz",
+ "integrity": "sha512-MFs36e/ca6ohEKtinTJ5VvAJ6oDRAYFdYXweUnGY9L9vcoqFOU4n2ZhmJ0C4z/cwGZ3YIQRSB3XZ1+ghZkY5NQ=="
+ },
"ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
@@ -2270,6 +2419,11 @@
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
"integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
},
+ "nocache": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/nocache/-/nocache-2.1.0.tgz",
+ "integrity": "sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q=="
+ },
"node-environment-flags": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.6.tgz",
@@ -2571,6 +2725,11 @@
"ee-first": "1.1.1"
}
},
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ },
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -2845,6 +3004,11 @@
"picomatch": "^2.0.7"
}
},
+ "referrer-policy": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.2.0.tgz",
+ "integrity": "sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA=="
+ },
"regexp-clone": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz",
@@ -3652,6 +3816,11 @@
"typedarray-to-buffer": "^3.1.5"
}
},
+ "x-xss-protection": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/x-xss-protection/-/x-xss-protection-1.3.0.tgz",
+ "integrity": "sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg=="
+ },
"xdg-basedir": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",
diff --git a/package.json b/package.json
index 6e7f289..777e274 100644
--- a/package.json
+++ b/package.json
@@ -6,9 +6,10 @@
"scripts": {
"tsc": "tsc",
"tsc-full": "del /q dist\\* & (for /d %x in (dist\\*) do @rd /s /q \"%x\") & tsc",
+ "build": "build.bat",
"test": "mocha dist/**/**.spec.js",
- "start": "tsc && node dist/index.js || exit 1",
- "dev": "nodemon -e ts,yaml --exec \"npm run start\"",
+ "start": "node index.js",
+ "dev": "nodemon -e ts,yaml --exec \"tsc && node dist/index.js || exit 1\"",
"loadDev": "node dist/test/loadDev.js",
"coverage": "tsc && nyc --reporter=html --reporter=text mocha dist/**/**.spec.js --timeout 5000"
},
@@ -19,35 +20,37 @@
"@apidevtools/json-schema-ref-parser": "^8.0.0",
"@apidevtools/swagger-parser": "^9.0.1",
"@hapi/joi": "^17.1.1",
- "@types/bcrypt": "^3.0.0",
- "@types/body-parser": "^1.19.0",
- "@types/express-serve-static-core": "^4.17.5",
- "@types/mocha": "^5.2.7",
- "@types/mongoose": "^5.7.12",
- "@types/node": "^13.1.6",
- "@types/qs": "^6.9.1",
- "@types/serve-static": "^1.13.3",
"axios": "^0.19.2",
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"cfenv": "^1.2.2",
+ "compression": "^1.7.4",
"content-filter": "^1.1.2",
"express": "^4.17.1",
+ "helmet": "^3.22.0",
"json-schema": "^0.2.5",
"lodash": "^4.17.15",
"mongo-sanitize": "^1.1.0",
"mongoose": "^5.8.7",
- "nodemon": "^2.0.3",
- "swagger-ui-express": "^4.1.2",
- "tslint": "^5.20.1",
- "typescript": "^3.7.4"
+ "swagger-ui-express": "^4.1.2"
},
"devDependencies": {
+ "@types/bcrypt": "^3.0.0",
+ "@types/body-parser": "^1.19.0",
+ "@types/express-serve-static-core": "^4.17.5",
"@types/lodash": "^4.14.150",
+ "@types/mocha": "^5.2.7",
+ "@types/mongoose": "^5.7.12",
+ "@types/node": "^13.1.6",
+ "@types/qs": "^6.9.1",
+ "@types/serve-static": "^1.13.3",
"mocha": "^7.1.2",
+ "nodemon": "^2.0.3",
"nyc": "^15.0.1",
"should": "^13.2.3",
- "supertest": "^4.0.2"
+ "supertest": "^4.0.2",
+ "tslint": "^5.20.1",
+ "typescript": "^3.7.4"
}
}
diff --git a/src/api.ts b/src/api.ts
index 59ce0b3..0867bc1 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -16,7 +16,7 @@ export default class api {
static setup () {
let apiDoc: JSONSchema = {};
jsonRefParser.bundle('api/api.yaml', (err, doc) => { // parse yaml
- if(err) throw err;
+ if (err) throw err;
apiDoc = doc;
apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
apiDoc = this.resolveXDoc(apiDoc);
diff --git a/src/db.ts b/src/db.ts
index fb5d424..60dadf9 100644
--- a/src/db.ts
+++ b/src/db.ts
@@ -1,5 +1,7 @@
import mongoose from 'mongoose';
import cfenv from 'cfenv';
+import _ from 'lodash';
+import ChangelogModel from './models/changelog';
// mongoose.set('debug', true); // enable mongoose debug
@@ -112,6 +114,27 @@ export default class db {
});
}
+ // changelog entry
+ static log(req, thisOrCollection, conditions = null, data = null) { // expects (req, this (from query helper)) or (req, collection, conditions, data)
+ if (! (conditions || data)) { // (req, this)
+ data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {}; // replace undefined with {}
+ Object.keys(data).forEach(key => {
+ if (key[0] === '$') {
+ data[key.substr(1)] = data[key];
+ delete data[key];
+ }
+ });
+ new ChangelogModel({action: req.method + ' ' + req.url, collectionName: thisOrCollection._collection.collectionName, conditions: thisOrCollection._conditions, data: data, user_id: req.authDetails.id ? req.authDetails.id : null}).save(err => {
+ if (err) console.error(err);
+ });
+ }
+ else { // (req, collection, conditions, data)
+ new ChangelogModel({action: req.method + ' ' + req.url, collectionName: thisOrCollection, conditions: conditions, data: data, user_id: req.authDetails.id ? req.authDetails.id : null}).save(err => {
+ if (err) console.error(err);
+ });
+ }
+ }
+
private static oidResolve (object: any) { // resolve $oid fields to actual ObjectIds recursively
Object.keys(object).forEach(key => {
if (object[key] !== null && object[key].hasOwnProperty('$oid')) { // found oid, replace
@@ -123,4 +146,4 @@ export default class db {
});
return object;
}
-};
\ No newline at end of file
+};
diff --git a/src/index.ts b/src/index.ts
index 7dda199..d274b89 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,20 +1,12 @@
import express from 'express';
import bodyParser from 'body-parser';
+import compression from 'compression';
import contentFilter from 'content-filter';
import mongoSanitize from 'mongo-sanitize';
+import helmet from 'helmet';
import api from './api';
import db from './db';
-// TODO: changelog
-// TODO: check executing index.js/move everything needed into dist
-// TODO: One condition per sample
-// TODO: validation: VZ, Humidity: min/max value, DPT: filename
-// TODO: condition values not needed on initial add
-// TODO: add multiple samples at once
-// TODO: coverage
-// TODO: think about the display of deleted/new samples and validation in data and UI
-// TODO: improve error coverage
-// TODO: guess properties from material name in UI
// tell if server is running in debug or production environment
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
@@ -31,8 +23,10 @@ app.disable('x-powered-by');
const port = process.env.PORT || 3000;
//middleware
+app.use(helmet());
app.use(express.json({ limit: '5mb'}));
app.use(express.urlencoded({ extended: false, limit: '5mb' }));
+app.use(compression()); // compress responses
app.use(bodyParser.json());
app.use(contentFilter()); // filter URL query attacks
app.use((req, res, next) => { // filter body query attacks
diff --git a/src/models/changelog.ts b/src/models/changelog.ts
new file mode 100644
index 0000000..75600c4
--- /dev/null
+++ b/src/models/changelog.ts
@@ -0,0 +1,11 @@
+import mongoose from 'mongoose';
+
+const ChangelogSchema = new mongoose.Schema({
+ action: String,
+ collectionName: String,
+ conditions: Object,
+ data: Object,
+ user_id: mongoose.Schema.Types.ObjectId
+}, {minimize: false});
+
+export default mongoose.model>('changelog', ChangelogSchema);
\ No newline at end of file
diff --git a/src/models/condition_template.ts b/src/models/condition_template.ts
index 20c7234..ca61da2 100644
--- a/src/models/condition_template.ts
+++ b/src/models/condition_template.ts
@@ -1,12 +1,20 @@
import mongoose from 'mongoose';
+import db from '../db';
const ConditionTemplateSchema = new mongoose.Schema({
+ first_id: mongoose.Schema.Types.ObjectId,
name: String,
version: Number,
- parameters: [{
+ parameters: [new mongoose.Schema({
name: String,
range: mongoose.Schema.Types.Mixed
- }]
+ } ,{ _id : false })]
}, {minimize: false}); // to allow empty objects
-export default mongoose.model('condition_template', ConditionTemplateSchema);
\ No newline at end of file
+// changelog query helper
+ConditionTemplateSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('condition_template', ConditionTemplateSchema);
\ No newline at end of file
diff --git a/src/models/material.ts b/src/models/material.ts
index 71d6b34..bcebb83 100644
--- a/src/models/material.ts
+++ b/src/models/material.ts
@@ -1,12 +1,15 @@
import mongoose from 'mongoose';
+import MaterialSupplierModel from '../models/material_suppliers';
+import MaterialGroupsModel from '../models/material_groups';
+import db from '../db';
const MaterialSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
- supplier: String,
- group: String,
- mineral: String,
- glass_fiber: String,
- carbon_fiber: String,
+ supplier_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialSupplierModel},
+ group_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialGroupsModel},
+ mineral: Number,
+ glass_fiber: Number,
+ carbon_fiber: Number,
numbers: [{
color: String,
number: String
@@ -14,4 +17,10 @@ const MaterialSchema = new mongoose.Schema({
status: Number
}, {minimize: false});
-export default mongoose.model('material', MaterialSchema);
\ No newline at end of file
+// changelog query helper
+MaterialSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('material', MaterialSchema);
\ No newline at end of file
diff --git a/src/models/material_groups.ts b/src/models/material_groups.ts
new file mode 100644
index 0000000..00be706
--- /dev/null
+++ b/src/models/material_groups.ts
@@ -0,0 +1,14 @@
+import mongoose from 'mongoose';
+import db from '../db';
+
+const MaterialGroupsSchema = new mongoose.Schema({
+ name: {type: String, index: {unique: true}}
+});
+
+// changelog query helper
+MaterialGroupsSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('material_groups', MaterialGroupsSchema);
\ No newline at end of file
diff --git a/src/models/material_suppliers.ts b/src/models/material_suppliers.ts
new file mode 100644
index 0000000..5c47e3b
--- /dev/null
+++ b/src/models/material_suppliers.ts
@@ -0,0 +1,14 @@
+import mongoose from 'mongoose';
+import db from '../db';
+
+const MaterialSuppliersSchema = new mongoose.Schema({
+ name: {type: String, index: {unique: true}}
+});
+
+// changelog query helper
+MaterialSuppliersSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('material_suppliers', MaterialSuppliersSchema);
\ No newline at end of file
diff --git a/src/models/measurement.ts b/src/models/measurement.ts
index d003ea5..1136e6b 100644
--- a/src/models/measurement.ts
+++ b/src/models/measurement.ts
@@ -1,6 +1,7 @@
import mongoose from 'mongoose';
import SampleModel from './sample';
import MeasurementTemplateModel from './measurement_template';
+import db from '../db';
@@ -11,4 +12,10 @@ const MeasurementSchema = new mongoose.Schema({
status: Number
}, {minimize: false});
-export default mongoose.model('measurement', MeasurementSchema);
\ No newline at end of file
+// changelog query helper
+MeasurementSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('measurement', MeasurementSchema);
\ No newline at end of file
diff --git a/src/models/measurement_template.ts b/src/models/measurement_template.ts
index 080f42b..b34e847 100644
--- a/src/models/measurement_template.ts
+++ b/src/models/measurement_template.ts
@@ -1,12 +1,20 @@
import mongoose from 'mongoose';
+import db from '../db';
const MeasurementTemplateSchema = new mongoose.Schema({
+ first_id: mongoose.Schema.Types.ObjectId,
name: String,
version: Number,
- parameters: [{
+ parameters: [new mongoose.Schema({
name: String,
range: mongoose.Schema.Types.Mixed
- }]
+ } ,{ _id : false })]
}, {minimize: false}); // to allow empty objects
-export default mongoose.model('measurement_template', MeasurementTemplateSchema);
\ No newline at end of file
+// changelog query helper
+MeasurementTemplateSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('measurement_template', MeasurementTemplateSchema);
\ No newline at end of file
diff --git a/src/models/note.ts b/src/models/note.ts
index cd0847b..5d02502 100644
--- a/src/models/note.ts
+++ b/src/models/note.ts
@@ -1,4 +1,5 @@
import mongoose from 'mongoose';
+import db from '../db';
const NoteSchema = new mongoose.Schema({
comment: String,
@@ -9,4 +10,10 @@ const NoteSchema = new mongoose.Schema({
custom_fields: mongoose.Schema.Types.Mixed
});
-export default mongoose.model('note', NoteSchema);
\ No newline at end of file
+// changelog query helper
+NoteSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('note', NoteSchema);
\ No newline at end of file
diff --git a/src/models/note_field.ts b/src/models/note_field.ts
index 86158e3..733ba02 100644
--- a/src/models/note_field.ts
+++ b/src/models/note_field.ts
@@ -1,8 +1,15 @@
import mongoose from 'mongoose';
+import db from '../db';
const NoteFieldSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
qty: Number
});
-export default mongoose.model('note_field', NoteFieldSchema);
\ No newline at end of file
+// changelog query helper
+NoteFieldSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('note_field', NoteFieldSchema);
\ No newline at end of file
diff --git a/src/models/sample.ts b/src/models/sample.ts
index 1338728..0e457d8 100644
--- a/src/models/sample.ts
+++ b/src/models/sample.ts
@@ -3,6 +3,7 @@ import mongoose from 'mongoose';
import MaterialModel from './material';
import NoteModel from './note';
import UserModel from './user';
+import db from '../db';
const SampleSchema = new mongoose.Schema({
number: {type: String, index: {unique: true}},
@@ -16,4 +17,10 @@ const SampleSchema = new mongoose.Schema({
status: Number
}, {minimize: false});
-export default mongoose.model('sample', SampleSchema);
\ No newline at end of file
+// changelog query helper
+SampleSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('sample', SampleSchema);
\ No newline at end of file
diff --git a/src/models/user.ts b/src/models/user.ts
index 50178a6..1e50d0c 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -1,4 +1,5 @@
import mongoose from 'mongoose';
+import db from '../db';
const UserSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
@@ -10,4 +11,10 @@ const UserSchema = new mongoose.Schema({
device_name: String
});
-export default mongoose.model('user', UserSchema);
\ No newline at end of file
+// changelog query helper
+UserSchema.query.log = function > (req) {
+ db.log(req, this);
+ return this;
+}
+
+export default mongoose.model>('user', UserSchema);
\ No newline at end of file
diff --git a/src/routes/material.spec.ts b/src/routes/material.spec.ts
index 56f094e..e91e87e 100644
--- a/src/routes/material.spec.ts
+++ b/src/routes/material.spec.ts
@@ -1,11 +1,12 @@
import should from 'should/as-function';
import _ from 'lodash';
import MaterialModel from '../models/material';
+import MaterialGroupModel from '../models/material_groups';
+import MaterialSupplierModel from '../models/material_suppliers';
import TestHelper from "../test/helper";
import globals from '../globals';
-// TODO: color name must be unique to get color number
-// TODO: separate supplier/ material name into own collections
+
describe('/material', () => {
let server;
@@ -80,7 +81,7 @@ describe('/material', () => {
});
});
- describe('GET /materials/{group}', () => {
+ describe('GET /materials/{state}', () => {
it('returns all new materials', done => {
TestHelper.request(server, done, {
method: 'get',
@@ -268,7 +269,17 @@ describe('/material', () => {
MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
- done();
+ MaterialGroupModel.find({name: 'PA46'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(1);
+ should(data[0]._id.toString()).be.eql('900000000000000000000001');
+ MaterialSupplierModel.find({name: 'DSM'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(1);
+ should(data[0]._id.toString()).be.eql('110000000000000000000001');
+ done();
+ });
+ });
});
});
});
@@ -296,19 +307,48 @@ describe('/material', () => {
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}]}
- ,
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}]});
MaterialModel.findById('100000000000000000000001').lean().exec((err, data:any) => {
if (err) return done(err);
data._id = data._id.toString();
+ data.group_id = data.group_id.toString();
+ data.supplier_id = data.supplier_id.toString();
data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}});
- should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: '0', glass_fiber: '35', carbon_fiber: '0', numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: 0, __v: 0});
- done();
+ should(data).be.eql({_id: '100000000000000000000001', name: 'UltramidTKR4355G7_2', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: 0, __v: 0});
+ MaterialGroupModel.find({name: 'PA6/6T'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(1);
+ should(data[0]._id.toString()).be.eql('900000000000000000000002');
+ MaterialSupplierModel.find({name: 'BASF'}).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.lengthOf(1);
+ should(data[0]._id.toString()).be.eql('110000000000000000000002');
+ done();
+ });
+ });
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/100000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {name: 'UltramidTKR4355G7_2', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}]},
+ log: {
+ collection: 'materials',
+ dataAdd: {
+ group_id: '900000000000000000000002',
+ supplier_id: '110000000000000000000002',
+ status: 0
+ },
+ dataIgn: ['supplier', 'group']
+ }
+ });
+ });
it('accepts a color without number', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -437,13 +477,27 @@ describe('/material', () => {
MaterialModel.findById('100000000000000000000002').lean().exec((err, data: any) => {
if (err) return done(err);
data._id = data._id.toString();
+ data.group_id = data.group_id.toString();
+ data.supplier_id = data.supplier_id.toString();
data.numbers = data.numbers.map(e => {return {color: e.color, number: e.number}});
- should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier: 'BASF', group: 'PA6/6T', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: -1, __v: 0}
+ should(data).be.eql({_id: '100000000000000000000002', name: 'Ultramid T KR 4355 G7', supplier_id: '110000000000000000000002', group_id: '900000000000000000000002', mineral: 0, glass_fiber: 35, carbon_fiber: 0, numbers: [{color: 'black', number: '5514212901'}, {color: 'signalviolet', number: '5514612901'}], status: -1, __v: 0}
);
done();
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/material/100000000000000000000002',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ log: {
+ collection: 'materials',
+ dataAdd: { status: -1}
+ }
+ });
+ });
it('rejects deleting a material referenced by samples', done => {
TestHelper.request(server, done, {
method: 'delete',
@@ -512,6 +566,21 @@ describe('/material', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/restore/100000000000000000000008',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ log: {
+ collection: 'materials',
+ dataAdd: {
+ status: 0
+ }
+ }
+ });
+ });
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -549,6 +618,76 @@ describe('/material', () => {
});
});
+ describe('PUT /material/validate/{id}', () => {
+ it('sets the status', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/validate/100000000000000000000007',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {}
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).be.eql({status: 'OK'});
+ MaterialModel.findById('100000000000000000000007').lean().exec((err, data: any) => {
+ if (err) return done(err);
+ should(data).have.property('status',globals.status.validated);
+ done();
+ });
+ });
+ });
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/validate/100000000000000000000007',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ log: {
+ collection: 'materials',
+ dataAdd: {
+ status: 10
+ }
+ }
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/validate/100000000000000000000007',
+ auth: {key: 'admin'},
+ httpStatus: 401,
+ req: {}
+ });
+ });
+ it('rejects a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/validate/100000000000000000000007',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403,
+ req: {}
+ });
+ });
+ it('returns 404 for an unknown sample', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/validate/000000000000000000000007',
+ auth: {basic: 'admin'},
+ httpStatus: 404,
+ req: {}
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/material/validate/100000000000000000000007',
+ httpStatus: 401,
+ req: {}
+ });
+ });
+ });
+
describe('POST /material/new', () => {
it('returns the right material', done => {
TestHelper.request(server, done, {
@@ -584,23 +723,42 @@ describe('/material', () => {
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []}
}).end(err => {
if (err) return done (err);
- MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => {
+ MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, materialData: any) => {
if (err) return done (err);
- should(data).have.lengthOf(1);
- should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v');
- should(data[0]).have.property('_id');
- should(data[0]).have.property('name', 'Crastin CE 2510');
- should(data[0]).have.property('supplier', 'Du Pont');
- should(data[0]).have.property('group', 'PBT');
- should(data[0]).have.property('mineral', '0');
- should(data[0]).have.property('glass_fiber', '30');
- should(data[0]).have.property('carbon_fiber', '0');
- should(data[0]).have.property('status',globals.status.new);
- should(data[0].numbers).have.lengthOf(0);
- done();
+ should(materialData).have.lengthOf(1);
+ should(materialData[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v');
+ should(materialData[0]).have.property('name', 'Crastin CE 2510');
+ should(materialData[0]).have.property('mineral', 0);
+ should(materialData[0]).have.property('glass_fiber', 30);
+ should(materialData[0]).have.property('carbon_fiber', 0);
+ should(materialData[0]).have.property('status',globals.status.new);
+ should(materialData[0].numbers).have.lengthOf(0);
+ MaterialGroupModel.findById(materialData[0].group_id).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.property('name', 'PBT')
+ MaterialSupplierModel.findById(materialData[0].supplier_id).lean().exec((err, data) => {
+ if (err) return done(err);
+ should(data).have.property('name', 'Du Pont');
+ done();
+ });
+ });
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/material/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: []},
+ log: {
+ collection: 'materials',
+ dataAdd: {status: 0},
+ dataIgn: ['group_id', 'supplier_id', 'group', 'supplier']
+ }
+ });
+ });
it('accepts a color without number', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -626,14 +784,12 @@ describe('/material', () => {
MaterialModel.find({name: 'Crastin CE 2510'}).lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.lengthOf(1);
- should(data[0]).have.only.keys('_id', 'name', 'supplier', 'group', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v');
+ should(data[0]).have.only.keys('_id', 'name', 'supplier_id', 'group_id', 'mineral', 'glass_fiber', 'carbon_fiber', 'numbers', 'status', '__v');
should(data[0]).have.property('_id');
should(data[0]).have.property('name', 'Crastin CE 2510');
- should(data[0]).have.property('supplier', 'Du Pont');
- should(data[0]).have.property('group', 'PBT');
- should(data[0]).have.property('mineral', '0');
- should(data[0]).have.property('glass_fiber', '30');
- should(data[0]).have.property('carbon_fiber', '0');
+ should(data[0]).have.property('mineral', 0);
+ should(data[0]).have.property('glass_fiber', 30);
+ should(data[0]).have.property('carbon_fiber', 0);
should(data[0]).have.property('status',globals.status.new);
should(_.omit(data[0].numbers[0], '_id')).be.eql({color: 'black', number: ''});
done();
@@ -767,4 +923,92 @@ describe('/material', () => {
});
});
});
+
+ describe('GET /material/groups', () => {
+ it('returns all groups', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/material/groups',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ const json = require('../test/db.json');
+ should(res.body).have.lengthOf(json.collections.material_groups.length);
+ should(res.body[0]).be.eql(json.collections.material_groups[0].name);
+ should(res.body).matchEach(group => {
+ should(group).be.type('string');
+ });
+ done();
+ });
+ });
+ it('works with an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/material/groups',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ const json = require('../test/db.json');
+ should(res.body).have.lengthOf(json.collections.material_groups.length);
+ should(res.body[0]).be.eql(json.collections.material_groups[0].name);
+ should(res.body).matchEach(group => {
+ should(group).be.type('string');
+ });
+ done();
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/material/groups',
+ httpStatus: 401
+ });
+ });
+ });
+
+ describe('GET /material/suppliers', () => {
+ it('returns all suppliers', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/material/suppliers',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ const json = require('../test/db.json');
+ should(res.body).have.lengthOf(json.collections.material_suppliers.length);
+ should(res.body[0]).be.eql(json.collections.material_suppliers[0].name);
+ should(res.body).matchEach(supplier => {
+ should(supplier).be.type('string');
+ });
+ done();
+ });
+ });
+ it('works with an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/material/suppliers',
+ auth: {key: 'janedoe'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ const json = require('../test/db.json');
+ should(res.body).have.lengthOf(json.collections.material_suppliers.length);
+ should(res.body[0]).be.eql(json.collections.material_suppliers[0].name);
+ should(res.body).matchEach(supplier => {
+ should(supplier).be.type('string');
+ });
+ done();
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/material/suppliers',
+ httpStatus: 401
+ });
+ });
+ });
});
\ No newline at end of file
diff --git a/src/routes/material.ts b/src/routes/material.ts
index 1711eb5..8373c9d 100644
--- a/src/routes/material.ts
+++ b/src/routes/material.ts
@@ -4,10 +4,13 @@ import _ from 'lodash';
import MaterialValidate from './validate/material';
import MaterialModel from '../models/material'
import SampleModel from '../models/sample';
+import MaterialGroupModel from '../models/material_groups';
+import MaterialSupplierModel from '../models/material_suppliers';
import IdValidate from './validate/id';
import res400 from './validate/res400';
import mongoose from 'mongoose';
import globals from '../globals';
+import db from '../db';
@@ -16,17 +19,19 @@ const router = express.Router();
router.get('/materials', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
- MaterialModel.find({status:globals.status.validated}).lean().exec((err, data) => {
+ MaterialModel.find({status:globals.status.validated}).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err);
+
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors
});
});
-router.get('/materials/:group(new|deleted)', (req, res, next) => {
+router.get('/materials/:state(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
- MaterialModel.find({status: globals.status[req.params.group]}).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);
+
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors
});
});
@@ -34,12 +39,13 @@ router.get('/materials/:group(new|deleted)', (req, res, next) => {
router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
- MaterialModel.findById(req.params.id).lean().exec((err, data: any) => {
+ MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => {
if (err) return next(err);
if (!data) {
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
res.json(MaterialValidate.output(data));
});
@@ -48,7 +54,7 @@ router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
- const {error, value: material} = MaterialValidate.input(req.body, 'change');
+ let {error, value: material} = MaterialValidate.input(req.body, 'change');
if (error) return res400(error, res);
MaterialModel.findById(req.params.id).lean().exec(async (err, materialData: any) => {
@@ -61,13 +67,21 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (material.hasOwnProperty('name') && material.name !== materialData.name) {
if (!await nameCheck(material, res, next)) return;
}
+ if (material.hasOwnProperty('group')) {
+ material = await groupResolve(material, req, next);
+ if (!material) return;
+ }
+ if (material.hasOwnProperty('supplier')) {
+ material = await supplierResolve(material, req, next);
+ if (!material) return;
+ }
// check for changes
- if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), material)) {
+ if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) {
material.status = globals.status.new; // set status to new
}
- await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).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);
res.json(MaterialValidate.output(data));
});
@@ -83,7 +97,7 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (data.length) {
return res.status(400).json({status: 'Material still in use'});
}
- MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).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 (data) {
res.json({status: 'OK'});
@@ -98,31 +112,58 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
router.put('/material/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
- MaterialModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).lean().exec((err, data) => {
- if (err) return next(err);
+ setStatus(globals.status.new, req, res, next);
+});
- if (!data) {
- return res.status(404).json({status: 'Not found'});
- }
- res.json({status: 'OK'});
- });
+router.put('/material/validate/' + IdValidate.parameter(), (req, res, next) => {
+ if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
+
+ setStatus(globals.status.validated, req, res, next);
});
router.post('/material/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
- const {error, value: material} = MaterialValidate.input(req.body, 'new');
+ let {error, value: material} = MaterialValidate.input(req.body, 'new');
if (error) return res400(error, res);
if (!await nameCheck(material, res, next)) return;
+ material = await groupResolve(material, req, next);
+ if (!material) return;
+ material = await supplierResolve(material, req, next);
+ if (!material) return;
+
material.status = globals.status.new; // set status to new
- await new MaterialModel(material).save((err, data) => {
+ await new MaterialModel(material).save(async (err, data) => {
if (err) return next(err);
+ db.log(req, 'materials', {_id: data._id}, data.toObject());
+ await data.populate('group_id').populate('supplier_id').execPopulate().catch(err => next(err));
+ if (data instanceof Error) return;
res.json(MaterialValidate.output(data.toObject()));
});
});
+router.get('/material/groups', (req, res, next) => {
+ if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
+
+ MaterialGroupModel.find().lean().exec((err, data: any) => {
+ if (err) return next(err);
+
+ res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); // validate all and filter null values from validation errors
+ });
+});
+
+router.get('/material/suppliers', (req, res, next) => {
+ if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
+
+ MaterialSupplierModel.find().lean().exec((err, data: any) => {
+ if (err) return next(err);
+
+ res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); // validate all and filter null values from validation errors
+ });
+});
+
module.exports = router;
@@ -135,4 +176,31 @@ async function nameCheck (material, res, next) { // check if name was already t
return false;
}
return true;
+}
+
+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;
+ if (groupData instanceof Error) return false;
+ material.group_id = groupData._id;
+ delete material.group;
+ return material;
+}
+
+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;
+ if (supplierData instanceof Error) return false;
+ material.supplier_id = supplierData._id;
+ delete material.supplier;
+ return material;
+}
+
+function setStatus (status, req, res, next) { // set measurement status
+ MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
+ if (err) return next(err);
+
+ if (!data) {
+ return res.status(404).json({status: 'Not found'});
+ }
+ res.json({status: 'OK'});
+ });
}
\ No newline at end of file
diff --git a/src/routes/measurement.spec.ts b/src/routes/measurement.spec.ts
index c27bf63..dd43520 100644
--- a/src/routes/measurement.spec.ts
+++ b/src/routes/measurement.spec.ts
@@ -138,6 +138,23 @@ describe('/measurement', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/800000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {values: {dpt: [[1,2],[3,4],[5,6]]}},
+ log: {
+ collection: 'measurements',
+ dataAdd: {
+ measurement_template: '300000000000000000000001',
+ sample_id: '400000000000000000000001',
+ status: 0
+ }
+ }
+ });
+ });
it('allows changing only one value', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -296,7 +313,7 @@ describe('/measurement', () => {
method: 'delete',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
- httpStatus: 200,
+ httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
@@ -307,6 +324,20 @@ describe('/measurement', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/measurement/800000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ log: {
+ collection: 'measurements',
+ dataAdd: {
+ status: -1
+ }
+ }
+ });
+ });
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'delete',
@@ -383,6 +414,21 @@ describe('/measurement', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/restore/800000000000000000000004',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ log: {
+ collection: 'measurements',
+ dataAdd: {
+ status: 0
+ }
+ }
+ });
+ });
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -420,6 +466,76 @@ describe('/measurement', () => {
});
});
+ describe('PUT /measurement/validate/{id}', () => {
+ it('sets the status', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/validate/800000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {}
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).be.eql({status: 'OK'});
+ MeasurementModel.findById('800000000000000000000003').lean().exec((err, data: any) => {
+ if (err) return done(err);
+ should(data).have.property('status',globals.status.validated);
+ done();
+ });
+ });
+ });
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/validate/800000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ log: {
+ collection: 'measurements',
+ dataAdd: {
+ status: 10
+ }
+ }
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/validate/800000000000000000000003',
+ auth: {key: 'admin'},
+ httpStatus: 401,
+ req: {}
+ });
+ });
+ it('rejects a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/validate/800000000000000000000003',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403,
+ req: {}
+ });
+ });
+ it('returns 404 for an unknown sample', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/validate/000000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 404,
+ req: {}
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/measurement/validate/800000000000000000000003',
+ httpStatus: 401,
+ req: {}
+ });
+ });
+ });
+
describe('POST /measurement/new', () => {
it('returns the right measurement', done => {
TestHelper.request(server, done, {
@@ -462,6 +578,21 @@ describe('/measurement', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/measurement/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'},
+ log: {
+ collection: 'measurements',
+ dataAdd: {
+ status: 0
+ }
+ }
+ });
+ });
it('rejects an invalid sample id', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -531,14 +662,24 @@ describe('/measurement', () => {
done();
});
});
+ it('rejects no values', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/measurement/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {sample_id: '400000000000000000000001', values: {}, measurement_template: '300000000000000000000002'},
+ res: {status: 'At least one value is required'}
+ });
+ });
it('rejects a value not in the value range', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
- req: {sample_id: '400000000000000000000001', values: {val1: 4}, measurement_template: '300000000000000000000003'},
- res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'}
+ req: {sample_id: '400000000000000000000001', values: {val2: 5}, measurement_template: '300000000000000000000004'},
+ res: {status: 'Invalid body format', details: '"val2" must be one of [1, 2, 3, 4, null]'}
});
});
it('rejects a value below minimum range', done => {
@@ -609,6 +750,16 @@ describe('/measurement', () => {
done();
});
});
+ it('rejects an old version of a measurement template', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/measurement/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {sample_id: '400000000000000000000001', values: {val1: 2}, measurement_template: '300000000000000000000003'},
+ res: {status: 'Old template version not allowed'}
+ });
+ });
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'post',
diff --git a/src/routes/measurement.ts b/src/routes/measurement.ts
index e7f6271..47af305 100644
--- a/src/routes/measurement.ts
+++ b/src/routes/measurement.ts
@@ -9,6 +9,7 @@ import IdValidate from './validate/id';
import res400 from './validate/res400';
import ParametersValidate from './validate/parameters';
import globals from '../globals';
+import db from '../db';
const router = express.Router();
@@ -56,7 +57,7 @@ router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
}
if (!await templateCheck(measurement, 'change', res, next)) return;
- await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).lean().exec((err, data) => {
+ await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).log(req).lean().exec((err, data) => {
if (err) return next(err);
res.json(MeasurementValidate.output(data));
});
@@ -71,7 +72,7 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
return res.status(404).json({status: 'Not found'});
}
if (!await sampleIdCheck(data, req, res, next)) return;
- await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).lean().exec(err => {
+ await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => {
if (err) return next(err);
return res.json({status: 'OK'});
});
@@ -81,14 +82,13 @@ router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
- MeasurementModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).lean().exec((err, data) => {
- if (err) return next(err);
+ setStatus(globals.status.new, req, res, next);
+});
- if (!data) {
- return res.status(404).json({status: 'Not found'});
- }
- res.json({status: 'OK'});
- });
+router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => {
+ if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
+
+ setStatus(globals.status.validated, req, res, next);
});
router.post('/measurement/new', async (req, res, next) => {
@@ -104,6 +104,7 @@ router.post('/measurement/new', async (req, res, next) => {
measurement.status = 0;
await new MeasurementModel(measurement).save((err, data) => {
if (err) return next(err);
+ db.log(req, 'measurements', {_id: data._id}, data.toObject());
res.json(MeasurementValidate.output(data.toObject()));
});
});
@@ -131,6 +132,14 @@ async function templateCheck (measurement, param, res, next) { // validate meas
// fill not given values for new measurements
if (param === 'new') {
+ // 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;
+ if (templateVersions instanceof Error) return false;
+ if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest
+ res.status(400).json({status: 'Old template version not allowed'});
+ return false;
+ }
+
if (Object.keys(measurement.values).length === 0) {
res.status(400).json({status: 'At least one value is required'});
return false
@@ -146,4 +155,15 @@ async function templateCheck (measurement, param, res, next) { // validate meas
const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null');
if (error) {res400(error, res); return false;}
return value || true;
+}
+
+function setStatus (status, req, res, next) { // set measurement status
+ MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
+ if (err) return next(err);
+
+ if (!data) {
+ return res.status(404).json({status: 'Not found'});
+ }
+ res.json({status: 'OK'});
+ });
}
\ No newline at end of file
diff --git a/src/routes/root.spec.ts b/src/routes/root.spec.ts
index 569af8b..68531a5 100644
--- a/src/routes/root.spec.ts
+++ b/src/routes/root.spec.ts
@@ -1,4 +1,5 @@
import TestHelper from "../test/helper";
+import should from 'should/as-function';
import db from '../db';
@@ -20,6 +21,121 @@ describe('/', () => {
});
});
+ describe('GET /changelog/{timestamp}/{page}/{pagesize}', () => {
+ it('returns the first page', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/0/2',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).have.lengthOf(2);
+ should(res.body[0].date).be.eql('1979-07-28T06:04:51.000Z');
+ should(res.body[1].date).be.eql('1979-07-28T06:04:50.000Z');
+ should(res.body).matchEach(log => {
+ should(log).have.only.keys('date', 'action', 'collection', 'conditions', 'data');
+ should(log).have.property('action', 'PUT /sample/400000000000000000000001');
+ should(log).have.property('collection', 'samples');
+ should(log).have.property('conditions', {_id: '400000000000000000000001'});
+ should(log).have.property('data', {type: 'part', status: 0});
+ });
+ done();
+ });
+ });
+ it('returns another page', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/1/2',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).have.lengthOf(1);
+ should(res.body[0].date).be.eql('1979-07-28T06:04:49.000Z');
+ should(res.body).matchEach(log => {
+ should(log).have.only.keys('date', 'action', 'collection', 'conditions', 'data');
+ should(log).have.property('action', 'PUT /sample/400000000000000000000001');
+ should(log).have.property('collection', 'samples');
+ should(log).have.property('conditions', {_id: '400000000000000000000001'});
+ should(log).have.property('data', {type: 'part', status: 0});
+ done();
+ });
+ });
+ });
+ it('returns an empty array for a page with no results', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
+ auth: {basic: 'admin'},
+ httpStatus: 200
+ }).end((err, res) => {
+ if (err) return done(err);
+ should(res.body).have.lengthOf(0);
+ done();
+ });
+ });
+ it('rejects timestamps pre unix epoch', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1879-07-28T06:04:51.000Z/10/2',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ res: {status: 'Invalid body format', details: '"timestamp" must be larger than or equal to "1970-01-01T00:00:00.000Z"'}
+ });
+ });
+ it('rejects invalid timestamps', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-14-28T06:04:51.000Z/10/2',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ res: {status: 'Invalid body format', details: '"timestamp" must be in ISO 8601 date format'}
+ });
+ });
+ it('rejects negative page numbers', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/-10/2',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ res: {status: 'Invalid body format', details: '"page" must be larger than or equal to 0'}
+ });
+ });
+ it('rejects negative pagesizes', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/10/-2',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ res: {status: 'Invalid body format', details: '"pagesize" must be larger than or equal to 0'}
+ });
+ });
+ it('rejects request from a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403
+ });
+ });
+ it('rejects requests from an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
+ auth: {key: 'admin'},
+ httpStatus: 401
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'get',
+ url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
+ httpStatus: 401
+ });
+ });
+ });
+
describe('Unknown routes', () => {
it('return a 404 message', done => {
TestHelper.request(server, done, {
diff --git a/src/routes/root.ts b/src/routes/root.ts
index 2705280..946948f 100644
--- a/src/routes/root.ts
+++ b/src/routes/root.ts
@@ -1,5 +1,10 @@
import express from 'express';
import globals from '../globals';
+import RootValidate from './validate/root';
+import res400 from './validate/res400';
+import ChangelogModel from '../models/changelog';
+import mongoose from 'mongoose';
+import _ from 'lodash';
const router = express.Router();
@@ -12,4 +17,18 @@ router.get('/authorized', (req, res) => {
res.json({status: 'Authorization successful', method: req.authDetails.method});
});
+router.get('/changelog/:timestamp/:page?/:pagesize?', (req, res, next) => {
+ if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
+
+ const {error, value: options} = RootValidate.changelogParams({timestamp: req.params.timestamp, page: req.params.page, pagesize: req.params.pagesize});
+ if (error) return res400(error, res);
+
+ const id = new mongoose.Types.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);
+
+ res.json(_.compact(data.map(e => RootValidate.changelogOutput(e)))); // validate all and filter null values from validation errors
+ });
+});
+
module.exports = router;
diff --git a/src/routes/sample.spec.ts b/src/routes/sample.spec.ts
index b90a722..97b9eb3 100644
--- a/src/routes/sample.spec.ts
+++ b/src/routes/sample.spec.ts
@@ -9,11 +9,7 @@ import mongoose from 'mongoose';
// TODO: generate output for ML in format DPT -> data, implement filtering, field selection
// TODO: generate csv
-// TODO: filter by not completely filled/no measurements
// TODO: write script for data import
-// TODO: allow adding sample numbers for existing samples
-// TODO: Do not allow validation or measurement entry without condition
-
describe('/sample', () => {
let server;
@@ -84,7 +80,7 @@ describe('/sample', () => {
});
});
- describe('GET /samples/{group}', () => {
+ describe('GET /samples/{state}', () => {
it('returns all new samples', done => {
TestHelper.request(server, done, {
method: 'get',
@@ -375,6 +371,22 @@ describe('/sample', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {type: 'part', color: 'signalviolet', batch: '114531', condition: {condition_template: '200000000000000000000003'}, material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ log: {
+ collection: 'samples',
+ dataAdd: {
+ status: 0
+ },
+ dataIgn: ['notes', 'note_id']
+ }
+ });
+ });
it('adjusts the note_fields correctly', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -455,6 +467,16 @@ describe('/sample', () => {
res: {status: 'Color not available for material'}
});
});
+ it('rejects an undefined color for the same material', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {type: 'part', color: 'signalviolet', batch: '114531', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ res: {status: 'Color not available for material'}
+ });
+ });
it('rejects an unknown material id', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -574,6 +596,26 @@ describe('/sample', () => {
res: {_id: '400000000000000000000006', number: 'Rng36', type: 'granulate', color: 'black', batch: '', condition: {}, material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
});
});
+ it('rejects an old version of a condition template', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {condition: {p1: 36, condition_template: '200000000000000000000004'}},
+ res: {status: 'Old template version not allowed'}
+ });
+ });
+ it('allows keeping an old version of a condition template', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/400000000000000000000004',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {condition: {p1: 36, condition_template: '200000000000000000000004'}},
+ res: {_id: '400000000000000000000004', number: '32', type: 'granulate', color: 'black', batch: '1653000308', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000005', note_id: '500000000000000000000003', user_id: '000000000000000000000003'}
+ });
+ });
it('rejects an changing back to an empty condition', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -678,6 +720,19 @@ describe('/sample', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/sample/400000000000000000000001',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ log: {
+ collection: 'samples',
+ skip: 1,
+ dataAdd: {status: -1}
+ }
+ });
+ });
it('keeps the notes of the sample', done => {
TestHelper.request(server, done, {
method: 'delete',
@@ -838,6 +893,24 @@ describe('/sample', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/restore/400000000000000000000005',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ log: {
+ collection: 'samples',
+ dataAdd: {
+ group_id: '900000000000000000000002',
+ supplier_id: '110000000000000000000002',
+ status: 0
+ },
+ dataIgn: ['group_id', 'supplier_id']
+ }
+ });
+ });
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -875,6 +948,99 @@ describe('/sample', () => {
});
});
+ describe('PUT /sample/validate/{id}', () => {
+ it('sets the status', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {}
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).be.eql({status: 'OK'});
+ SampleModel.findById('400000000000000000000003').lean().exec((err, data: any) => {
+ if (err) return done(err);
+ should(data).have.property('status',globals.status.validated);
+ done();
+ });
+ });
+ });
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {},
+ log: {
+ collection: 'samples',
+ dataAdd: {
+ group_id: '900000000000000000000002',
+ supplier_id: '110000000000000000000002',
+ status: 10
+ },
+ dataIgn: ['group_id', 'supplier_id']
+ }
+ });
+ });
+ it('rejects validating a sample without condition', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000006',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {},
+ res: {status: 'Sample without condition cannot be valid'}
+ });
+ });
+ it('rejects validating a sample without measurements', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000004',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {},
+ res: {status: 'Sample without measurements cannot be valid'}
+ });
+ });
+ it('rejects an API key', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000003',
+ auth: {key: 'admin'},
+ httpStatus: 401,
+ req: {}
+ });
+ });
+ it('rejects a write user', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000003',
+ auth: {basic: 'janedoe'},
+ httpStatus: 403,
+ req: {}
+ });
+ });
+ it('returns 404 for an unknown sample', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/000000000000000000000003',
+ auth: {basic: 'admin'},
+ httpStatus: 404,
+ req: {}
+ });
+ });
+ it('rejects unauthorized requests', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/sample/validate/400000000000000000000003',
+ httpStatus: 401,
+ req: {}
+ });
+ });
+ });
+
describe('POST /sample/new', () => {
it('returns the right sample', done => {
TestHelper.request(server, done, {
@@ -934,6 +1100,24 @@ describe('/sample', () => {
})
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/sample/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ req: {color: 'black', type: 'granulate', batch: '1560237365', condition: {material: 'copper', weeks: 3, condition_template: '200000000000000000000001'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ log: {
+ collection: 'samples',
+ dataAdd: {
+ number: 'Rng37',
+ user_id: '000000000000000000000002',
+ status: 0
+ },
+ dataIgn: ['notes', 'note_id']
+ }
+ });
+ });
it('stores the custom fields', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -1034,7 +1218,7 @@ describe('/sample', () => {
res: {status: 'Material not available'}
});
});
- it('rejects a sample number', done => {
+ it('rejects a sample number for a write user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/sample/new',
@@ -1044,6 +1228,38 @@ describe('/sample', () => {
res: {status: 'Invalid body format', details: '"number" is not allowed'}
});
});
+ it('allows a sample number for an admin user', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/sample/new',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {number: 'Rng34', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ }).end((err, res) => {
+ if (err) return done (err);
+ should(res.body).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'condition', 'material_id', 'note_id', 'user_id');
+ should(res.body).have.property('_id').be.type('string');
+ should(res.body).have.property('number', 'Rng34');
+ should(res.body).have.property('color', 'black');
+ should(res.body).have.property('type', 'granulate');
+ should(res.body).have.property('batch', '1560237365');
+ should(res.body).have.property('condition', {});
+ should(res.body).have.property('material_id', '100000000000000000000001');
+ should(res.body).have.property('note_id').be.type('string');
+ should(res.body).have.property('user_id', '000000000000000000000003');
+ done();
+ });
+ });
+ it('rejects an existing sample number for an admin user', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/sample/new',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {number: 'Rng33', color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ res: {status: 'Sample number already taken'}
+ });
+ });
it('rejects an invalid sample reference', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -1134,6 +1350,16 @@ describe('/sample', () => {
res: {status: 'Condition template not available'}
});
});
+ it('rejects an old version of a condition template', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/sample/new',
+ auth: {basic: 'janedoe'},
+ httpStatus: 400,
+ req: {color: 'black', type: 'granulate', batch: '1560237365', condition: {p1: 36, condition_template: '200000000000000000000004'}, material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{sample_id: '400000000000000000000003', relation: 'part to this sample'}]}},
+ res: {status: 'Old template version not allowed'}
+ });
+ });
it('rejects a missing color', done => {
TestHelper.request(server, done, {
method: 'post',
diff --git a/src/routes/sample.ts b/src/routes/sample.ts
index e8ed1f7..3966c9b 100644
--- a/src/routes/sample.ts
+++ b/src/routes/sample.ts
@@ -14,6 +14,7 @@ import mongoose from 'mongoose';
import ConditionTemplateModel from '../models/condition_template';
import ParametersValidate from './validate/parameters';
import globals from '../globals';
+import db from '../db';
const router = express.Router();
@@ -27,10 +28,10 @@ router.get('/samples', (req, res, next) => {
})
});
-router.get('/samples/:group(new|deleted)', (req, res, next) => {
+router.get('/samples/:state(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
- SampleModel.find({status: globals.status[req.params.group]}).lean().exec((err, data) => {
+ SampleModel.find({status: globals.status[req.params.state]}).lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors
});
@@ -39,12 +40,18 @@ router.get('/samples/:group(new|deleted)', (req, res, next) => {
router.get('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
- SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').lean().exec((err, sampleData: any) => {
+ SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => {
if (err) return next(err);
if (sampleData) {
+ await sampleData.populate('material_id.group_id').populate('material_id.supplier_id').execPopulate().catch(err => next(err));
+ if (sampleData instanceof Error) return;
+ sampleData = sampleData.toObject();
+
if (sampleData.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin
sampleData.material = sampleData.material_id; // map data to right keys
+ sampleData.material.group = sampleData.material.group_id.name;
+ sampleData.material.supplier = sampleData.material.supplier_id.name;
sampleData.user = sampleData.user_id.name;
sampleData.notes = sampleData.note_id ? sampleData.note_id : {};
MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => {
@@ -84,7 +91,7 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
}
if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { // do not execute check if condition is and was empty
- if (!await conditionCheck(sample.condition, 'change', res, next)) return;
+ if (!await conditionCheck(sample.condition, 'change', res, next, sampleData.condition.condition_template.toString() !== sample.condition.condition_template)) return;
}
if (sample.hasOwnProperty('notes')) {
@@ -95,9 +102,9 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); // check if notes were changed
if (newNotes) {
if (data.hasOwnProperty('custom_fields')) { // update note_fields
- customFieldsChange(Object.keys(data.custom_fields), -1);
+ customFieldsChange(Object.keys(data.custom_fields), -1, req);
}
- await NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes
+ await NoteModel.findByIdAndDelete(sampleData.note_id).log(req).lean().exec(err => { // delete old notes
if (err) return console.error(err);
});
}
@@ -106,9 +113,10 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes
if (!await sampleRefCheck(sample, res, next)) return;
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields
- customFieldsChange(Object.keys(sample.notes.custom_fields), 1);
+ 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
+ db.log(req, 'notes', {_id: data._id}, data.toObject());
delete sample.notes;
sample.note_id = data._id;
}
@@ -119,7 +127,7 @@ router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
sample.status = globals.status.new;
}
- await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data: any) => {
+ await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).log(req).lean().exec((err, data: any) => {
if (err) return next(err);
res.json(SampleValidate.output(data));
});
@@ -139,18 +147,18 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
// only maintain and admin are allowed to edit other user's data
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
- await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).lean().exec(err => { // set sample status
+ await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { // set sample status
if (err) return next(err);
// set status of associated measurements also to deleted
- MeasurementModel.update({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: -1}).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 (sampleData.note_id !== null) { // handle notes
NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => { // find notes to update note_fields
if (err) return next(err);
if (data.hasOwnProperty('custom_fields')) { // update note_fields
- customFieldsChange(Object.keys(data.custom_fields), -1);
+ customFieldsChange(Object.keys(data.custom_fields), -1, req);
}
res.json({status: 'OK'});
});
@@ -166,7 +174,7 @@ router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
- SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).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 (!data) {
@@ -176,6 +184,34 @@ router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => {
});
});
+router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => {
+ if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
+
+ SampleModel.findById(req.params.id).lean().exec((err, data: any) => {
+ if (err) return next(err);
+
+ if (!data) {
+ 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) => {
+ 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) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
@@ -183,14 +219,14 @@ router.post('/sample/new', async (req, res, next) => {
req.body.condition = {};
}
- const {error, value: sample} = SampleValidate.input(req.body, 'new');
+ const {error, value: sample} = SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : ''));
if (error) return res400(error, res);
if (!await materialCheck(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
- customFieldsChange(Object.keys(sample.notes.custom_fields), 1);
+ customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
}
if (!_.isEmpty(sample.condition)) { // do not execute check if condition is empty
@@ -198,17 +234,24 @@ router.post('/sample/new', async (req, res, next) => {
}
sample.status = globals.status.new; // set status to new
- sample.number = await numberGenerate(sample, req, res, next);
+ if (sample.hasOwnProperty('number')) {
+ if (!await numberCheck(sample, res, next)) return;
+ }
+ else {
+ sample.number = await numberGenerate(sample, req, res, next);
+ }
if (!sample.number) return;
await new NoteModel(sample.notes).save((err, data) => { // save notes
if (err) return next(err);
+ db.log(req, 'notes', {_id: data._id}, data.toObject());
delete sample.notes;
sample.note_id = data._id;
sample.user_id = req.authDetails.id;
new SampleModel(sample).save((err, data) => {
if (err) return next(err);
+ db.log(req, 'samples', {_id: data._id}, data.toObject());
res.json(SampleValidate.output(data.toObject()));
});
});
@@ -238,6 +281,15 @@ async function numberGenerate (sample, req, res, next) { // generate number in
return req.authDetails.location + (sampleData ? Number(sampleData.number.replace(/[^0-9]+/g, '')) + 1 : 1);
}
+async function numberCheck(sample, res, next) {
+ const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;});
+ if (sampleData) { // found entry with sample number
+ res.status(400).json({status: 'Sample number already taken'});
+ return false
+ }
+ return true;
+}
+
async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid
const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any;
if (materialData instanceof Error) return false;
@@ -252,7 +304,7 @@ async function materialCheck (sample, res, next, id = sample.material_id) { //
return true;
}
-async function conditionCheck (condition, param, res, next) { // validate treatment template, returns false if invalid, otherwise template data
+async function conditionCheck (condition, param, res, next, checkVersion = true) { // validate treatment template, returns false if invalid, otherwise template data
if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found
res.status(400).json({status: 'Condition template not available'});
return false;
@@ -264,6 +316,16 @@ async function conditionCheck (condition, param, res, next) { // validate treat
return false;
}
+ if (checkVersion) {
+ // 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;
+ if (conditionVersions instanceof Error) return false;
+ if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest
+ res.status(400).json({status: 'Old template version not allowed'});
+ return false;
+ }
+ }
+
// validate parameters
const {error, value: ignore} = ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param);
if (error) {res400(error, res); return false;}
@@ -272,7 +334,7 @@ async function conditionCheck (condition, param, res, next) { // validate treat
function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference
return new Promise(resolve => {
- if (sample.notes.sample_references.length > 0) { // there are sample_references
+ if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { // there are sample_references
let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations
sample.notes.sample_references.forEach(reference => {
@@ -295,17 +357,18 @@ function sampleRefCheck (sample, res, next) { // validate sample_references, re
});
}
-function customFieldsChange (fields, amount) { // update custom_fields and respective quantities
+function customFieldsChange (fields, amount, req) { // update custom_fields and respective quantities
fields.forEach(field => {
- NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}}, {new: true}).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 (!data) { // new field
- new NoteFieldModel({name: field, qty: 1}).save(err => {
+ new NoteFieldModel({name: field, qty: 1}).save((err, data) => {
if (err) return console.error(err);
+ db.log(req, 'note_fields', {_id: data._id}, data.toObject());
})
}
else if (data.qty <= 0) { // delete document if field is not used anymore
- NoteFieldModel.findOneAndDelete({name: field}).lean().exec(err => {
+ NoteFieldModel.findOneAndDelete({name: field}).log(req).lean().exec(err => {
if (err) return console.error(err);
});
}
diff --git a/src/routes/template.spec.ts b/src/routes/template.spec.ts
index 54adfcb..cd90108 100644
--- a/src/routes/template.spec.ts
+++ b/src/routes/template.spec.ts
@@ -4,9 +4,6 @@ import TemplateConditionModel from '../models/condition_template';
import TemplateMeasurementModel from '../models/measurement_template';
import TestHelper from "../test/helper";
-// TODO: do not allow usage of old templates for new samples
-// TODO: remove number_prefix
-// TODO: template parameters are not allowed to be condition_template
describe('/template', () => {
let server;
@@ -135,7 +132,8 @@ describe('/template', () => {
if (err) return done(err);
TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
- should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v');
+ should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
+ should(data.first_id.toString()).be.eql('200000000000000000000001');
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(1);
@@ -146,6 +144,22 @@ describe('/template', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/condition/200000000000000000000001',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]},
+ log: {
+ collection: 'condition_templates',
+ dataAdd: {
+ first_id: '200000000000000000000001',
+ version: 2
+ }
+ }
+ });
+ });
it('allows changing only one property', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -157,7 +171,8 @@ describe('/template', () => {
if (err) return done(err);
TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
- should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v');
+ should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
+ should(data.first_id.toString()).be.eql('200000000000000000000001');
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(2);
@@ -219,6 +234,16 @@ describe('/template', () => {
done();
});
});
+ it('rejects `condition_template` as parameter name', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/condition/200000000000000000000001',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {parameters: [{name: 'condition_template', range: {}}]},
+ res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'}
+ });
+ });
it('rejects not specified parameters', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -228,7 +253,7 @@ describe('/template', () => {
req: {name: 'heat treatment', parameters: [{name: 'material', range: {xx: 5}}]},
res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'}
});
- })
+ });
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -307,7 +332,8 @@ describe('/template', () => {
if (err) return done(err);
TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
- should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v');
+ should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
+ should(data.first_id.toString()).be.eql(data._id.toString());
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 1);
should(data).have.property('parameters').have.lengthOf(1);
@@ -318,6 +344,20 @@ describe('/template', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/template/condition/new',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]},
+ log: {
+ collection: 'condition_templates',
+ dataAdd: {version: 1},
+ dataIgn: ['first_id']
+ }
+ });
+ });
it('rejects a missing name', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -328,6 +368,16 @@ describe('/template', () => {
res: {status: 'Invalid body format', details: '"name" is required'}
});
});
+ it('rejects `condition_template` as parameter name', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/template/condition/new',
+ auth: {basic: 'admin'},
+ httpStatus: 400,
+ req: {name: 'heat aging', parameters: [{name: 'condition_template', range: {min: 1}}]},
+ res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'}
+ });
+ });
it('rejects a number prefix', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -532,13 +582,14 @@ describe('/template', () => {
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
- req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
+ req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]});
TemplateMeasurementModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
- should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v');
+ should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
+ should(data.first_id.toString()).be.eql('300000000000000000000001');
should(data).have.property('name', 'IR spectrum');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(1);
@@ -550,6 +601,22 @@ describe('/template', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/template/measurement/300000000000000000000001',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
+ log: {
+ collection: 'measurement_templates',
+ dataAdd: {
+ first_id: '300000000000000000000001',
+ version: 2
+ }
+ }
+ });
+ });
it('allows changing only one property', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -562,7 +629,8 @@ describe('/template', () => {
should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, parameters: [{name: 'dpt', range: {type: 'array'}}]});
TemplateMeasurementModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
- should(data).have.only.keys('_id', 'name', 'version', 'parameters', '__v');
+ should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
+ should(data.first_id.toString()).be.eql('300000000000000000000001');
should(data).have.property('name', 'IR spectrum');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(1);
@@ -621,7 +689,7 @@ describe('/template', () => {
req: {parameters: [{name: 'weight %', range: {}}]}
}).end((err, res) => {
if (err) return done(err);
- should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 3, parameters: [{name: 'weight %', range: {}}]});
+ should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 2, parameters: [{name: 'weight %', range: {}}]});
done();
});
});
@@ -713,7 +781,8 @@ describe('/template', () => {
TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(1);
- should(data[0]).have.only.keys('_id', 'name', 'version', 'parameters', '__v');
+ should(data[0]).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
+ should(data[0].first_id.toString()).be.eql(data[0]._id.toString());
should(data[0]).have.property('name', 'vz');
should(data[0]).have.property('version', 1);
should(data[0]).have.property('parameters').have.lengthOf(1);
@@ -724,6 +793,20 @@ describe('/template', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/template/measurement/new',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]},
+ log: {
+ collection: 'measurement_templates',
+ dataAdd: {version: 1},
+ dataIgn: ['first_id']
+ }
+ });
+ });
it('rejects a missing name', done => {
TestHelper.request(server, done, {
method: 'post',
diff --git a/src/routes/template.ts b/src/routes/template.ts
index 849cf59..c3bd14b 100644
--- a/src/routes/template.ts
+++ b/src/routes/template.ts
@@ -6,6 +6,8 @@ import ConditionTemplateModel from '../models/condition_template';
import MeasurementTemplateModel from '../models/measurement_template';
import res400 from './validate/res400';
import IdValidate from './validate/id';
+import mongoose from "mongoose";
+import db from '../db';
@@ -51,6 +53,7 @@ router.put('/template/:collection(measurement|condition)/' + IdValidate.paramete
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
if (err) next (err);
+ db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
res.json(TemplateValidate.output(data.toObject()));
});
}
@@ -65,9 +68,12 @@ router.post('/template/:collection(measurement|condition)/new', async (req, res,
const {error, value: template} = TemplateValidate.input(req.body, 'new');
if (error) return res400(error, res);
+ template._id = mongoose.Types.ObjectId(); // set reference to itself for first version of template
+ template.first_id = template._id;
template.version = 1; // set template version
await new (model(req))(template).save((err, data) => {
if (err) next (err);
+ db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
res.json(TemplateValidate.output(data.toObject()));
});
});
diff --git a/src/routes/user.spec.ts b/src/routes/user.spec.ts
index a0d67a5..79c0769 100644
--- a/src/routes/user.spec.ts
+++ b/src/routes/user.spec.ts
@@ -2,7 +2,7 @@ import should from 'should/as-function';
import UserModel from '../models/user';
import TestHelper from "../test/helper";
-// TODO: reject usernames containing admin, etc.
+
describe('/user', () => {
let server;
@@ -200,6 +200,19 @@ describe('/user', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'put',
+ url: '/user',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'},
+ log: {
+ collection: 'users',
+ dataIgn: ['pass']
+ }
+ });
+ });
it('lets the admin change a user level', done => {
TestHelper.request(server, done, {
method: 'put',
@@ -371,6 +384,17 @@ describe('/user', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'delete',
+ url: '/user',
+ auth: {basic: 'janedoe'},
+ httpStatus: 200,
+ log: {
+ collection: 'users'
+ }
+ });
+ });
it('rejects requests from non-admins for another user', done => {
TestHelper.request(server, done, {
method: 'delete',
@@ -483,6 +507,19 @@ describe('/user', () => {
});
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/user/new',
+ auth: {basic: 'admin'},
+ httpStatus: 200,
+ req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
+ log: {
+ collection: 'users',
+ dataIgn: ['pass', 'key']
+ }
+ });
+ });
it('rejects a username already in use', done => {
TestHelper.request(server, done, {
method: 'post',
@@ -588,6 +625,18 @@ describe('/user', () => {
res: {status: 'OK'}
});
});
+ it('creates a changelog', done => {
+ TestHelper.request(server, done, {
+ method: 'post',
+ url: '/user/passreset',
+ httpStatus: 200,
+ req: {email: 'jane.doe@bosch.com', name: 'janedoe'},
+ log: {
+ collection: 'users',
+ dataIgn: ['email', 'name', 'pass']
+ }
+ });
+ });
it('returns 404 for wrong username/email combo', done => {
TestHelper.request(server, done, {
method: 'post',
diff --git a/src/routes/user.ts b/src/routes/user.ts
index 6ebed4b..65c41d5 100644
--- a/src/routes/user.ts
+++ b/src/routes/user.ts
@@ -7,6 +7,7 @@ import UserValidate from './validate/user';
import UserModel from '../models/user';
import mail from '../helpers/mail';
import res400 from './validate/res400';
+import db from '../db';
const router = express.Router();
@@ -53,7 +54,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => {
if (!await usernameCheck(user.name, res, next)) return;
}
- await UserModel.findOneAndUpdate({name: username}, user, {new: true}).lean().exec( (err, data:any) => {
+ await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data));
@@ -70,7 +71,7 @@ router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { //
const username = getUsername(req, res);
if (!username) return;
- UserModel.findOneAndDelete({name: username}).lean().exec( (err, data:any) => {
+ UserModel.findOneAndDelete({name: username}).log(req).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json({status: 'OK'})
@@ -105,6 +106,7 @@ router.post('/user/new', async (req, res, next) => {
user.pass = hash;
new UserModel(user).save((err, data) => { // store user
if (err) return next(err);
+ db.log(req, 'users', {_id: data._id}, data.toObject());
res.json(UserValidate.output(data.toObject()));
});
});
@@ -119,7 +121,7 @@ router.post('/user/passreset', (req, res, next) => {
bcrypt.hash(newPass, 10, (err, hash) => { // password hashing
if (err) return next(err);
- UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}, err => { // write new password
+ UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}).log(req).exec(err => { // write new password
if (err) return next(err);
// send email
diff --git a/src/routes/validate/material.ts b/src/routes/validate/material.ts
index 805ccd2..7a2c3fb 100644
--- a/src/routes/validate/material.ts
+++ b/src/routes/validate/material.ts
@@ -70,6 +70,8 @@ export default class MaterialValidate { // validate input for material
static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data);
+ data.group = data.group_id.name;
+ data.supplier = data.supplier_id.name;
const {value, error} = Joi.object({
_id: IdValidate.get(),
name: this.material.name,
@@ -83,6 +85,16 @@ export default class MaterialValidate { // validate input for material
return error !== undefined? null : value;
}
+ static outputGroups (data) {// validate groups output and strip unwanted properties, returns null if not valid
+ const {value, error} = this.material.group.validate(data, {stripUnknown: true});
+ return error !== undefined? null : value;
+ }
+
+ static outputSuppliers (data) {// validate suppliers output and strip unwanted properties, returns null if not valid
+ const {value, error} = this.material.supplier.validate(data, {stripUnknown: true});
+ return error !== undefined? null : value;
+ }
+
static outputV() { // return output validator
return Joi.object({
_id: IdValidate.get(),
diff --git a/src/routes/validate/root.ts b/src/routes/validate/root.ts
new file mode 100644
index 0000000..3d05f9b
--- /dev/null
+++ b/src/routes/validate/root.ts
@@ -0,0 +1,50 @@
+import Joi from '@hapi/joi';
+import IdValidate from './id';
+
+export default class RootValidate { // validate input for root methods
+ private static changelog = {
+ timestamp: Joi.date()
+ .iso()
+ .min('1970-01-01T00:00:00.000Z'),
+
+ page: Joi.number()
+ .integer()
+ .min(0)
+ .default(0),
+
+ pagesize: Joi.number()
+ .integer()
+ .min(0)
+ .default(25),
+
+ action: Joi.string(),
+
+ collection: Joi.string(),
+
+ conditions: Joi.object(),
+
+ data: Joi.object()
+ };
+
+ static changelogParams (data) {
+ return Joi.object({
+ timestamp: this.changelog.timestamp.required(),
+ page: this.changelog.page,
+ pagesize: this.changelog.pagesize
+ }).validate(data);
+ }
+
+ static changelogOutput (data) {
+ data.date = data._id.getTimestamp();
+ data.collection = data.collectionName;
+ data = IdValidate.stringify(data);
+ const {value, error} = Joi.object({
+ date: this.changelog.timestamp,
+ action: this.changelog.action,
+ collection: this.changelog.collection,
+ conditions: this.changelog.conditions,
+ data: this.changelog.data,
+ }).validate(data, {stripUnknown: true});
+ return error !== undefined? null : value;
+ }
+}
\ No newline at end of file
diff --git a/src/routes/validate/sample.ts b/src/routes/validate/sample.ts
index 93b86b1..58c33ba 100644
--- a/src/routes/validate/sample.ts
+++ b/src/routes/validate/sample.ts
@@ -67,6 +67,17 @@ export default class SampleValidate {
notes: this.sample.notes,
}).validate(data);
}
+ else if (param === 'new-admin') {
+ return Joi.object({
+ number: this.sample.number,
+ color: this.sample.color.required(),
+ type: this.sample.type.required(),
+ batch: this.sample.batch.required(),
+ condition: this.sample.condition.required(),
+ material_id: IdValidate.get().required(),
+ notes: this.sample.notes.required()
+ }).validate(data);
+ }
else {
return{error: 'No parameter specified!', value: {}};
}
diff --git a/src/routes/validate/template.ts b/src/routes/validate/template.ts
index 111951e..7a63d1d 100644
--- a/src/routes/validate/template.ts
+++ b/src/routes/validate/template.ts
@@ -14,6 +14,7 @@ export default class TemplateValidate {
Joi.object({
name: Joi.string()
.max(128)
+ .invalid('condition_template')
.required(),
range: Joi.object({
diff --git a/src/test/db.json b/src/test/db.json
index de4070f..ef26a63 100644
--- a/src/test/db.json
+++ b/src/test/db.json
@@ -59,9 +59,8 @@
"color": "black",
"batch": "1653000308",
"condition": {
- "material": "hot air",
- "weeks": 5,
- "condition_template": {"$oid":"200000000000000000000001"}
+ "p1": 44,
+ "condition_template": {"$oid":"200000000000000000000004"}
},
"material_id": {"$oid":"100000000000000000000005"},
"note_id": {"$oid":"500000000000000000000003"},
@@ -149,8 +148,8 @@
{
"_id": {"$oid":"100000000000000000000001"},
"name": "Stanyl TW 200 F8",
- "supplier": "DSM",
- "group": "PA46",
+ "supplier_id": {"$oid":"110000000000000000000001"},
+ "group_id": {"$oid":"900000000000000000000001"},
"mineral": 0,
"glass_fiber": 40,
"carbon_fiber": 0,
@@ -170,8 +169,8 @@
{
"_id": {"$oid":"100000000000000000000002"},
"name": "Ultramid T KR 4355 G7",
- "supplier": "BASF",
- "group": "PA6/6T",
+ "supplier_id": {"$oid":"110000000000000000000002"},
+ "group_id": {"$oid":"900000000000000000000002"},
"mineral": 0,
"glass_fiber": 35,
"carbon_fiber": 0,
@@ -191,8 +190,8 @@
{
"_id": {"$oid":"100000000000000000000003"},
"name": "PA GF 50 black (2706)",
- "supplier": "Akro-Plastic",
- "group": "PA66+PA6I/6T",
+ "supplier_id": {"$oid":"110000000000000000000003"},
+ "group_id": {"$oid":"900000000000000000000003"},
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
@@ -204,8 +203,8 @@
{
"_id": {"$oid":"100000000000000000000004"},
"name": "Schulamid 66 GF 25 H",
- "supplier": "Schulmann",
- "group": "PA66",
+ "supplier_id": {"$oid":"110000000000000000000004"},
+ "group_id": {"$oid":"900000000000000000000004"},
"mineral": 0,
"glass_fiber": 25,
"carbon_fiber": 0,
@@ -221,8 +220,8 @@
{
"_id": {"$oid":"100000000000000000000005"},
"name": "Amodel A 1133 HS",
- "supplier": "Solvay",
- "group": "PPA",
+ "supplier_id": {"$oid":"110000000000000000000005"},
+ "group_id": {"$oid":"900000000000000000000005"},
"mineral": 0,
"glass_fiber": 33,
"carbon_fiber": 0,
@@ -238,8 +237,8 @@
{
"_id": {"$oid":"100000000000000000000006"},
"name": "PK-HM natural (4773)",
- "supplier": "Akro-Plastic",
- "group": "PK",
+ "supplier_id": {"$oid":"110000000000000000000003"},
+ "group_id": {"$oid":"900000000000000000000006"},
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
@@ -255,8 +254,8 @@
{
"_id": {"$oid":"100000000000000000000007"},
"name": "Ultramid A4H",
- "supplier": "BASF",
- "group": "PA66",
+ "supplier_id": {"$oid":"110000000000000000000002"},
+ "group_id": {"$oid":"900000000000000000000004"},
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
@@ -272,8 +271,8 @@
{
"_id": {"$oid":"100000000000000000000008"},
"name": "Latamid 66 H 2 G 30",
- "supplier": "LATI",
- "group": "PA66",
+ "supplier_id": {"$oid":"110000000000000000000006"},
+ "group_id": {"$oid":"900000000000000000000004"},
"mineral": 0,
"glass_fiber": 30,
"carbon_fiber": 0,
@@ -287,6 +286,70 @@
"__v": 0
}
],
+ "material_groups": [
+ {
+ "_id": {"$oid":"900000000000000000000001"},
+ "name": "PA46",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"900000000000000000000002"},
+ "name": "PA6/6T",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"900000000000000000000003"},
+ "name": "PA66+PA6I/6T",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"900000000000000000000004"},
+ "name": "PA66",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"900000000000000000000005"},
+ "name": "PPA",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"900000000000000000000006"},
+ "name": "PK",
+ "__v": 0
+ }
+ ],
+ "material_suppliers": [
+ {
+ "_id": {"$oid":"110000000000000000000001"},
+ "name": "DSM",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"110000000000000000000002"},
+ "name": "BASF",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"110000000000000000000003"},
+ "name": "Akro-Plastic",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"110000000000000000000004"},
+ "name": "Schulmann",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"110000000000000000000005"},
+ "name": "Solvay",
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"110000000000000000000006"},
+ "name": "LATI",
+ "__v": 0
+ }
+ ],
"measurements": [
{
"_id": {"$oid":"800000000000000000000001"},
@@ -343,11 +406,23 @@
"status": 10,
"measurement_template": {"$oid":"300000000000000000000002"},
"__v": 0
+ },
+ {
+ "_id": {"$oid":"800000000000000000000006"},
+ "sample_id": {"$oid":"400000000000000000000006"},
+ "values": {
+ "weight %": 0.5,
+ "standard deviation":null
+ },
+ "status": 0,
+ "measurement_template": {"$oid":"300000000000000000000002"},
+ "__v": 0
}
],
"condition_templates": [
{
"_id": {"$oid":"200000000000000000000001"},
+ "first_id": {"$oid":"200000000000000000000001"},
"name": "heat treatment",
"version": 1,
"parameters": [
@@ -371,22 +446,37 @@
"__v": 0
},
{
- "_id": {"$oid":"200000000000000000000002"},
- "name": "heat treatment 2",
- "version": 2,
+ "_id": {"$oid":"200000000000000000000003"},
+ "first_id": {"$oid":"200000000000000000000003"},
+ "name": "raw material",
+ "version": 1,
+ "parameters": [
+ ],
+ "__v": 0
+ },
+ {
+ "_id": {"$oid":"200000000000000000000004"},
+ "first_id": {"$oid":"200000000000000000000004"},
+ "name": "old condition",
+ "version": 1,
"parameters": [
{
- "name": "material",
+ "name": "p1",
"range": {}
}
],
"__v": 0
},
{
- "_id": {"$oid":"200000000000000000000003"},
- "name": "raw material",
- "version": 1,
+ "_id": {"$oid":"200000000000000000000005"},
+ "first_id": {"$oid":"200000000000000000000004"},
+ "name": "new condition",
+ "version": 2,
"parameters": [
+ {
+ "name": "p11",
+ "range": {}
+ }
],
"__v": 0
}
@@ -394,6 +484,7 @@
"measurement_templates": [
{
"_id": {"$oid":"300000000000000000000001"},
+ "first_id": {"$oid":"300000000000000000000001"},
"name": "spectrum",
"version": 1,
"parameters": [
@@ -408,8 +499,9 @@
},
{
"_id": {"$oid":"300000000000000000000002"},
+ "first_id": {"$oid":"300000000000000000000002"},
"name": "kf",
- "version": 2,
+ "version": 1,
"parameters": [
{
"name": "weight %",
@@ -430,6 +522,7 @@
},
{
"_id": {"$oid":"300000000000000000000003"},
+ "first_id": {"$oid":"300000000000000000000003"},
"name": "mt 3",
"version": 1,
"parameters": [
@@ -441,6 +534,21 @@
}
],
"__v": 0
+ },
+ {
+ "_id": {"$oid":"300000000000000000000004"},
+ "first_id": {"$oid":"300000000000000000000003"},
+ "name": "mt 31",
+ "version": 2,
+ "parameters": [
+ {
+ "name": "val2",
+ "range": {
+ "values": [1,2,3,4]
+ }
+ }
+ ],
+ "__v": 0
}
],
"users": [
@@ -488,6 +596,64 @@
"key": "000000000000000000001004",
"__v": 0
}
+ ],
+ "changelogs": [
+ {
+ "_id" : {"$oid": "120000010000000000000000"},
+ "action" : "PUT /sample/400000000000000000000001",
+ "collectionName" : "samples",
+ "conditions" : {
+ "_id" : {"$oid": "400000000000000000000001"}
+ },
+ "data" : {
+ "type" : "part",
+ "status" : 0
+ },
+ "user_id" : {"$oid": "000000000000000000000003"},
+ "__v" : 0
+ },
+ {
+ "_id" : {"$oid": "120000020000000000000000"},
+ "action" : "PUT /sample/400000000000000000000001",
+ "collectionName" : "samples",
+ "conditions" : {
+ "_id" : {"$oid": "400000000000000000000001"}
+ },
+ "data" : {
+ "type" : "part",
+ "status" : 0
+ },
+ "user_id" : {"$oid": "000000000000000000000003"},
+ "__v" : 0
+ },
+ {
+ "_id" : {"$oid": "120000030000000000000000"},
+ "action" : "PUT /sample/400000000000000000000001",
+ "collectionName" : "samples",
+ "conditions" : {
+ "_id" : {"$oid": "400000000000000000000001"}
+ },
+ "data" : {
+ "type" : "part",
+ "status" : 0
+ },
+ "user_id" : {"$oid": "000000000000000000000003"},
+ "__v" : 0
+ },
+ {
+ "_id" : {"$oid": "120000040000000000000000"},
+ "action" : "PUT /sample/400000000000000000000001",
+ "collectionName" : "samples",
+ "conditions" : {
+ "_id" : {"$oid": "400000000000000000000001"}
+ },
+ "data" : {
+ "type" : "part",
+ "status" : 0
+ },
+ "user_id" : {"$oid": "000000000000000000000003"},
+ "__v" : 0
+ }
]
}
}
\ No newline at end of file
diff --git a/src/test/helper.ts b/src/test/helper.ts
index fbb45ff..e1e8eec 100644
--- a/src/test/helper.ts
+++ b/src/test/helper.ts
@@ -1,14 +1,17 @@
import supertest from 'supertest';
import should from 'should/as-function';
-import db from "../db";
+import _ from 'lodash';
+import db from '../db';
+import ChangelogModel from '../models/changelog';
+import IdValidate from '../routes/validate/id';
export default class TestHelper {
public static auth = { // test user credentials
- admin: {pass: 'Abc123!#', key: '000000000000000000001003'},
- janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002'},
- user: {pass: 'Xyz890*)', key: '000000000000000000001001'},
- johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004'}
+ admin: {pass: 'Abc123!#', key: '000000000000000000001003', id: '000000000000000000000003'},
+ janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002', id: '000000000000000000000002'},
+ user: {pass: 'Xyz890*)', key: '000000000000000000001001', id: '000000000000000000000001'},
+ johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004', id: '000000000000000000000004'}
}
public static res = { // default responses
@@ -92,6 +95,35 @@ export default class TestHelper {
done();
});
}
+ else if (options.hasOwnProperty('log')) { // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)}
+ return st.end(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
+ if (err) return done(err);
+ 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('collectionName', options.log.collection);
+ if (options.log.hasOwnProperty('data')) {
+ should(data).have.property('data', options.log.data);
+ }
+ else {
+ const ignore = ['_id', '__v'];
+ if (options.log.hasOwnProperty('dataIgn')) {
+ ignore.push(...options.log.dataIgn);
+ }
+ let tmp = options.req ? options.req : {};
+ if (options.log.hasOwnProperty('dataAdd')) {
+ _.assign(tmp, options.log.dataAdd)
+ }
+ should(IdValidate.stringify(_.omit(data.data, ignore))).be.eql(_.omit(tmp, ignore));
+ }
+ if (data.user_id) {
+ should(data.user_id.toString()).be.eql(this.auth[options.auth.basic].id);
+ }
+ done();
+ });
+ });
+ }
else { // return object to do .end() manually
return st;
}