Archived
2

Merge pull request #3 in ~VLE2FE/dfop-api from sample to develop

* commit '16a1cf5ba8255e7537eb1fdb20ee42951bea38af':
  deleting a material is rejected if it is referenced by a sample
  implemented more /sample methods
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-05-08 09:59:12 +02:00
commit 9580af9dd3
26 changed files with 1372 additions and 179 deletions

11
.idea/dataSources.xml Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="@localhost" uuid="46f112fc-d60d-4217-873f-f5ffea06180c">
<driver-ref>mongo</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.dbschema.MongoJdbcDriver</jdbc-driver>
<jdbc-url>mongodb://localhost:27017</jdbc-url>
</data-source>
</component>
</project>

458
.idea/dbnavigator.xml Normal file
View File

@ -0,0 +1,458 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DBNavigator.Project.DataEditorManager">
<record-view-column-sorting-type value="BY_INDEX" />
<value-preview-text-wrapping value="true" />
<value-preview-pinned value="false" />
</component>
<component name="DBNavigator.Project.DataExportManager">
<export-instructions>
<create-header value="true" />
<quote-values-containing-separator value="true" />
<quote-all-values value="false" />
<value-separator value="" />
<file-name value="" />
<file-location value="" />
<scope value="GLOBAL" />
<destination value="FILE" />
<format value="EXCEL" />
<charset value="windows-1252" />
</export-instructions>
</component>
<component name="DBNavigator.Project.DatabaseBrowserManager">
<autoscroll-to-editor value="false" />
<autoscroll-from-editor value="true" />
<show-object-properties value="true" />
<loaded-nodes />
</component>
<component name="DBNavigator.Project.DatabaseFileManager">
<open-files />
</component>
<component name="DBNavigator.Project.EditorStateManager">
<last-used-providers />
</component>
<component name="DBNavigator.Project.MethodExecutionManager">
<method-browser />
<execution-history>
<group-entries value="true" />
<execution-inputs />
</execution-history>
<argument-values-cache />
</component>
<component name="DBNavigator.Project.ObjectDependencyManager">
<last-used-dependency-type value="INCOMING" />
</component>
<component name="DBNavigator.Project.ObjectQuickFilterManager">
<last-used-operator value="EQUAL" />
<filters />
</component>
<component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true">
<recently-used-interfaces />
</component>
<component name="DBNavigator.Project.Settings">
<connections />
<browser-settings>
<general>
<display-mode value="TABBED" />
<navigation-history-size value="100" />
<show-object-details value="false" />
</general>
<filters>
<object-type-filter>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="true" />
<object-type name="ROLE" enabled="true" />
<object-type name="PRIVILEGE" enabled="true" />
<object-type name="CHARSET" enabled="true" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED_VIEW" enabled="true" />
<object-type name="NESTED_TABLE" enabled="true" />
<object-type name="COLUMN" enabled="true" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET_TRIGGER" enabled="true" />
<object-type name="DATABASE_TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="true" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
<object-type name="ARGUMENT" enabled="true" />
<object-type name="DIMENSION" enabled="true" />
<object-type name="CLUSTER" enabled="true" />
<object-type name="DBLINK" enabled="true" />
</object-type-filter>
</filters>
<sorting>
<object-type name="COLUMN" sorting-type="NAME" />
<object-type name="FUNCTION" sorting-type="NAME" />
<object-type name="PROCEDURE" sorting-type="NAME" />
<object-type name="ARGUMENT" sorting-type="POSITION" />
</sorting>
<default-editors>
<object-type name="VIEW" editor-type="SELECTION" />
<object-type name="PACKAGE" editor-type="SELECTION" />
<object-type name="TYPE" editor-type="SELECTION" />
</default-editors>
</browser-settings>
<navigation-settings>
<lookup-filters>
<lookup-objects>
<object-type name="SCHEMA" enabled="true" />
<object-type name="USER" enabled="false" />
<object-type name="ROLE" enabled="false" />
<object-type name="PRIVILEGE" enabled="false" />
<object-type name="CHARSET" enabled="false" />
<object-type name="TABLE" enabled="true" />
<object-type name="VIEW" enabled="true" />
<object-type name="MATERIALIZED VIEW" enabled="true" />
<object-type name="NESTED TABLE" enabled="false" />
<object-type name="COLUMN" enabled="false" />
<object-type name="INDEX" enabled="true" />
<object-type name="CONSTRAINT" enabled="true" />
<object-type name="DATASET TRIGGER" enabled="true" />
<object-type name="DATABASE TRIGGER" enabled="true" />
<object-type name="SYNONYM" enabled="false" />
<object-type name="SEQUENCE" enabled="true" />
<object-type name="PROCEDURE" enabled="true" />
<object-type name="FUNCTION" enabled="true" />
<object-type name="PACKAGE" enabled="true" />
<object-type name="TYPE" enabled="true" />
<object-type name="TYPE ATTRIBUTE" enabled="false" />
<object-type name="ARGUMENT" enabled="false" />
<object-type name="DIMENSION" enabled="false" />
<object-type name="CLUSTER" enabled="false" />
<object-type name="DBLINK" enabled="true" />
</lookup-objects>
<force-database-load value="false" />
<prompt-connection-selection value="true" />
<prompt-schema-selection value="true" />
</lookup-filters>
</navigation-settings>
<dataset-grid-settings>
<general>
<enable-zooming value="true" />
<enable-column-tooltip value="true" />
</general>
<sorting>
<nulls-first value="true" />
<max-sorting-columns value="4" />
</sorting>
<tracking-columns>
<columnNames value="" />
<visible value="true" />
<editable value="false" />
</tracking-columns>
</dataset-grid-settings>
<dataset-editor-settings>
<text-editor-popup>
<active value="false" />
<active-if-empty value="false" />
<data-length-threshold value="100" />
<popup-delay value="1000" />
</text-editor-popup>
<values-actions-popup>
<show-popup-button value="true" />
<element-count-threshold value="1000" />
<data-length-threshold value="250" />
</values-actions-popup>
<general>
<fetch-block-size value="100" />
<fetch-timeout value="30" />
<trim-whitespaces value="true" />
<convert-empty-strings-to-null value="true" />
<select-content-on-cell-edit value="true" />
<large-value-preview-active value="true" />
</general>
<filters>
<prompt-filter-dialog value="true" />
<default-filter-type value="BASIC" />
</filters>
<qualified-text-editor text-length-threshold="300">
<content-types>
<content-type name="Text" enabled="true" />
<content-type name="XML" enabled="true" />
<content-type name="DTD" enabled="true" />
<content-type name="HTML" enabled="true" />
<content-type name="XHTML" enabled="true" />
<content-type name="CSS" enabled="true" />
<content-type name="SQL" enabled="true" />
<content-type name="PL/SQL" enabled="true" />
<content-type name="JavaScript" enabled="true" />
<content-type name="JSON" enabled="true" />
<content-type name="JSON5" enabled="true" />
<content-type name="JSP" enabled="true" />
<content-type name="JSPx" enabled="true" />
<content-type name="ASP" enabled="true" />
<content-type name="YAML" enabled="true" />
</content-types>
</qualified-text-editor>
<record-navigation>
<navigation-target value="VIEWER" />
</record-navigation>
</dataset-editor-settings>
<code-editor-settings>
<general>
<show-object-navigation-gutter value="false" />
<show-spec-declaration-navigation-gutter value="true" />
<enable-spellchecking value="true" />
<enable-reference-spellchecking value="false" />
</general>
<confirmations>
<save-changes value="false" />
<revert-changes value="true" />
</confirmations>
</code-editor-settings>
<code-completion-settings>
<filters>
<basic-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="false" />
<filter-element type="OBJECT" id="view" selected="false" />
<filter-element type="OBJECT" id="materialized view" selected="false" />
<filter-element type="OBJECT" id="index" selected="false" />
<filter-element type="OBJECT" id="constraint" selected="false" />
<filter-element type="OBJECT" id="trigger" selected="false" />
<filter-element type="OBJECT" id="synonym" selected="false" />
<filter-element type="OBJECT" id="sequence" selected="false" />
<filter-element type="OBJECT" id="procedure" selected="false" />
<filter-element type="OBJECT" id="function" selected="false" />
<filter-element type="OBJECT" id="package" selected="false" />
<filter-element type="OBJECT" id="type" selected="false" />
<filter-element type="OBJECT" id="dimension" selected="false" />
<filter-element type="OBJECT" id="cluster" selected="false" />
<filter-element type="OBJECT" id="dblink" selected="false" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</basic-filter>
<extended-filter>
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
<filter-element type="RESERVED_WORD" id="function" selected="true" />
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
<filter-element type="OBJECT" id="schema" selected="true" />
<filter-element type="OBJECT" id="user" selected="true" />
<filter-element type="OBJECT" id="role" selected="true" />
<filter-element type="OBJECT" id="privilege" selected="true" />
<user-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</user-schema>
<public-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</public-schema>
<any-schema>
<filter-element type="OBJECT" id="table" selected="true" />
<filter-element type="OBJECT" id="view" selected="true" />
<filter-element type="OBJECT" id="materialized view" selected="true" />
<filter-element type="OBJECT" id="index" selected="true" />
<filter-element type="OBJECT" id="constraint" selected="true" />
<filter-element type="OBJECT" id="trigger" selected="true" />
<filter-element type="OBJECT" id="synonym" selected="true" />
<filter-element type="OBJECT" id="sequence" selected="true" />
<filter-element type="OBJECT" id="procedure" selected="true" />
<filter-element type="OBJECT" id="function" selected="true" />
<filter-element type="OBJECT" id="package" selected="true" />
<filter-element type="OBJECT" id="type" selected="true" />
<filter-element type="OBJECT" id="dimension" selected="true" />
<filter-element type="OBJECT" id="cluster" selected="true" />
<filter-element type="OBJECT" id="dblink" selected="true" />
</any-schema>
</extended-filter>
</filters>
<sorting enabled="true">
<sorting-element type="RESERVED_WORD" id="keyword" />
<sorting-element type="RESERVED_WORD" id="datatype" />
<sorting-element type="OBJECT" id="column" />
<sorting-element type="OBJECT" id="table" />
<sorting-element type="OBJECT" id="view" />
<sorting-element type="OBJECT" id="materialized view" />
<sorting-element type="OBJECT" id="index" />
<sorting-element type="OBJECT" id="constraint" />
<sorting-element type="OBJECT" id="trigger" />
<sorting-element type="OBJECT" id="synonym" />
<sorting-element type="OBJECT" id="sequence" />
<sorting-element type="OBJECT" id="procedure" />
<sorting-element type="OBJECT" id="function" />
<sorting-element type="OBJECT" id="package" />
<sorting-element type="OBJECT" id="type" />
<sorting-element type="OBJECT" id="dimension" />
<sorting-element type="OBJECT" id="cluster" />
<sorting-element type="OBJECT" id="dblink" />
<sorting-element type="OBJECT" id="schema" />
<sorting-element type="OBJECT" id="role" />
<sorting-element type="OBJECT" id="user" />
<sorting-element type="RESERVED_WORD" id="function" />
<sorting-element type="RESERVED_WORD" id="parameter" />
</sorting>
<format>
<enforce-code-style-case value="true" />
</format>
</code-completion-settings>
<execution-engine-settings>
<statement-execution>
<fetch-block-size value="100" />
<execution-timeout value="20" />
<debug-execution-timeout value="600" />
<focus-result value="false" />
<prompt-execution value="false" />
</statement-execution>
<script-execution>
<command-line-interfaces />
<execution-timeout value="300" />
</script-execution>
<method-execution>
<execution-timeout value="30" />
<debug-execution-timeout value="600" />
<parameter-history-size value="10" />
</method-execution>
</execution-engine-settings>
<operation-settings>
<transactions>
<uncommitted-changes>
<on-project-close value="ASK" />
<on-disconnect value="ASK" />
<on-autocommit-toggle value="ASK" />
</uncommitted-changes>
<multiple-uncommitted-changes>
<on-commit value="ASK" />
<on-rollback value="ASK" />
</multiple-uncommitted-changes>
</transactions>
<session-browser>
<disconnect-session value="ASK" />
<kill-session value="ASK" />
<reload-on-filter-change value="false" />
</session-browser>
<compiler>
<compile-type value="KEEP" />
<compile-dependencies value="ASK" />
<always-show-controls value="false" />
</compiler>
<debugger>
<debugger-type value="JDBC" />
<use-generic-runners value="true" />
</debugger>
</operation-settings>
<ddl-file-settings>
<extensions>
<mapping file-type-id="VIEW" extensions="vw" />
<mapping file-type-id="TRIGGER" extensions="trg" />
<mapping file-type-id="PROCEDURE" extensions="prc" />
<mapping file-type-id="FUNCTION" extensions="fnc" />
<mapping file-type-id="PACKAGE" extensions="pkg" />
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
<mapping file-type-id="TYPE" extensions="tpe" />
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
</extensions>
<general>
<lookup-ddl-files value="true" />
<create-ddl-files value="false" />
<synchronize-ddl-files value="true" />
<use-qualified-names value="false" />
<make-scripts-rerunnable value="true" />
</general>
</ddl-file-settings>
<general-settings>
<regional-settings>
<date-format value="MEDIUM" />
<number-format value="UNGROUPED" />
<locale value="SYSTEM_DEFAULT" />
<use-custom-formats value="false" />
</regional-settings>
<environment>
<environment-types>
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
</environment-types>
<visibility-settings>
<connection-tabs value="true" />
<dialog-headers value="true" />
<object-editor-tabs value="true" />
<script-editor-tabs value="false" />
<execution-result-tabs value="true" />
</visibility-settings>
</environment>
</general-settings>
</component>
<component name="DBNavigator.Project.StatementExecutionManager">
<execution-variables />
</component>
</project>

View File

@ -79,6 +79,8 @@
responses: responses:
200: 200:
$ref: 'api.yaml#/components/responses/Ok' $ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401: 401:
$ref: 'api.yaml#/components/responses/401' $ref: 'api.yaml#/components/responses/401'
403: 403:

View File

@ -41,8 +41,8 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
put: put:
summary: TODO change sample summary: change sample
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, maintain, dev, admin, only maintain and admin are allowed to edit samples created by another user'
tags: tags:
- /sample - /sample
security: security:
@ -59,7 +59,7 @@
content: content:
application/json: application/json:
schema: schema:
$ref: 'api.yaml#/components/schemas/SampleDetail' $ref: 'api.yaml#/components/schemas/SampleRefs'
400: 400:
$ref: 'api.yaml#/components/responses/400' $ref: 'api.yaml#/components/responses/400'
401: 401:
@ -71,8 +71,8 @@
500: 500:
$ref: 'api.yaml#/components/responses/500' $ref: 'api.yaml#/components/responses/500'
delete: delete:
summary: TODO delete sample summary: delete sample
description: 'Auth: basic, levels: write, maintain, dev, admin' description: 'Auth: basic, levels: write, maintain, dev, admin, only maintain and admin are allowed to edit samples created by another user'
tags: tags:
- /sample - /sample
security: security:
@ -123,7 +123,7 @@
/sample/notes/fields: /sample/notes/fields:
get: get:
summary: TODO list all existing field names for custom notes fields summary: list all existing field names for custom notes fields
description: 'Auth: all, levels: read, write, maintain, dev, admin' description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags: tags:
- /sample - /sample

View File

@ -7,7 +7,8 @@
"tsc": "tsc", "tsc": "tsc",
"test": "mocha dist/**/**.spec.js", "test": "mocha dist/**/**.spec.js",
"start": "tsc && node dist/index.js || exit 1", "start": "tsc && node dist/index.js || exit 1",
"dev": "nodemon -e ts,yaml --exec \"npm run start\"" "dev": "nodemon -e ts,yaml --exec \"npm run start\"",
"loadDev": "node dist/test/loadDev.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",

View File

@ -42,19 +42,19 @@ export default class db {
}); });
mongoose.connection.on('error', console.error.bind(console, 'connection error:')); mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
mongoose.connection.on('disconnected', () => { // reset state on disconnect mongoose.connection.on('disconnected', () => { // reset state on disconnect
console.log('Database disconnected'); console.info('Database disconnected');
this.state.db = 0; this.state.db = 0;
done(); done();
}); });
process.on('SIGINT', () => { // close connection when app is terminated process.on('SIGINT', () => { // close connection when app is terminated
mongoose.connection.close(() => { mongoose.connection.close(() => {
console.log('Mongoose default connection disconnected through app termination'); console.info('Mongoose default connection disconnected through app termination');
process.exit(0); process.exit(0);
}); });
}); });
mongoose.connection.once('open', () => { mongoose.connection.once('open', () => {
mongoose.set('useFindAndModify', false); mongoose.set('useFindAndModify', false);
console.log(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`); console.info(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`);
this.state.db = mongoose.connection; this.state.db = mongoose.connection;
done(); done();
}); });
@ -90,13 +90,7 @@ export default class db {
let loadCounter = 0; // count number of loaded collections to know when to return done() let loadCounter = 0; // count number of loaded collections to know when to return done()
Object.keys(json.collections).forEach(collectionName => { // create each collection Object.keys(json.collections).forEach(collectionName => { // create each collection
for(let i in json.collections[collectionName]) { // convert $oid fields to actual ObjectIds json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
Object.keys(json.collections[collectionName][i]).forEach(key => {
if (json.collections[collectionName][i][key] !== null && json.collections[collectionName][i][key].hasOwnProperty('$oid')) {
json.collections[collectionName][i][key] = mongoose.Types.ObjectId(json.collections[collectionName][i][key].$oid);
}
})
}
this.state.db.createCollection(collectionName, (err, collection) => { this.state.db.createCollection(collectionName, (err, collection) => {
collection.insertMany(json.collections[collectionName], () => { // insert JSON data collection.insertMany(json.collections[collectionName], () => { // insert JSON data
if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded if (++ loadCounter >= Object.keys(json.collections).length) { // all collections loaded
@ -106,4 +100,16 @@ export default class db {
}); });
}); });
} }
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')) {
object[key] = mongoose.Types.ObjectId(object[key].$oid);
}
else if (typeof object[key] === 'object' && object[key] !== null) {
object[key] = this.oidResolve(object[key]);
}
});
return object;
}
}; };

View File

@ -30,7 +30,7 @@ export default (mailAddress, subject, content, f) => { // callback, executed em
}); });
} }
else if (process.env.NODE_ENV === 'test') { else if (process.env.NODE_ENV === 'test') {
console.log('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content); console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content);
f(); f();
} }
else { // dev else { // dev

View File

@ -8,7 +8,7 @@ import db from './db';
// tell if server is running in debug or production environment // tell if server is running in debug or production environment
console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT ====='); console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
// mongodb connection // mongodb connection
@ -75,7 +75,7 @@ app.use((err, req, res, ignore) => { // internal server error handling
// hook up server to port // hook up server to port
const server = app.listen(port, () => { const server = app.listen(port, () => {
console.log(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`); console.info(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
}); });
module.exports = server; module.exports = server;

View File

@ -1,6 +1,6 @@
import should from 'should/as-function'; import should from 'should/as-function';
import MaterialModel from '../models/material'; import MaterialModel from '../models/material';
import TestHelper from "../helpers/test"; import TestHelper from "../test/helper";
describe('/material', () => { describe('/material', () => {
@ -171,14 +171,54 @@ describe('/material', () => {
res: {status: 'Material name already taken'} res: {status: 'Material name already taken'}
}); });
}); });
it('rejects wrong material properties', done => { it('rejects a wrong mineral property', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/material/100000000000000000000001', url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]}, req: {mineral: 'x'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"mineral" must be a number'}
});
});
it('rejects a wrong glass_fiber property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {glass_fiber: 'x'},
res: {status: 'Invalid body format', details: '"glass_fiber" must be a number'}
});
});
it('rejects a wrong carbon_fiber property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {carbon_fiber: 'x'},
res: {status: 'Invalid body format', details: '"carbon_fiber" must be a number'}
});
});
it('rejects a wrong color name property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {numbers: [{colorxx: 'black', number: 55}]},
res: {status: 'Invalid body format', details: '"numbers[0].color" is required'}
});
});
it('rejects a wrong color number property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/material/100000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {numbers: [{color: 'black', number: 'xxx'}]},
res: {status: 'Invalid body format', details: '"numbers[0].number" must be a number'}
}); });
}); });
it('rejects an invalid id', done => { it('rejects an invalid id', done => {
@ -231,20 +271,28 @@ describe('/material', () => {
it('deletes the material', done => { it('deletes the material', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
url: '/material/100000000000000000000001', url: '/material/100000000000000000000002',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 200 httpStatus: 200
}).end((err, res) => { }).end((err, res) => {
if (err) return done(err); if (err) return done(err);
should(res.body).be.eql({status: 'OK'}); should(res.body).be.eql({status: 'OK'});
MaterialModel.findById('100000000000000000000001').lean().exec((err, data) => { MaterialModel.findById('100000000000000000000002').lean().exec((err, data) => {
if (err) return done(err); if (err) return done(err);
should(data).be.null(); should(data).be.null();
done(); done();
}); });
}); });
}); });
it('rejects deleting a material referenced by samples'); it('rejects deleting a material referenced by samples', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/material/100000000000000000000004',
auth: {basic: 'janedoe'},
httpStatus: 400,
res: {status: 'Material still in use'}
})
});
it('rejects an invalid id', done => { it('rejects an invalid id', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'delete', method: 'delete',
@ -347,24 +395,94 @@ describe('/material', () => {
res: {status: 'Material name already taken'} res: {status: 'Material name already taken'}
}); });
}); });
it('rejects wrong material properties', done => { it('rejects a missing name', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]}, req: {supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"name" is required'}
}); });
}); });
it('rejects incomplete material properties', done => { it('rejects a missing supplier', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'post', method: 'post',
url: '/material/new', url: '/material/new',
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {name: 'Crastin CE 2510'}, req: {name: 'Crastin CE 2510', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"supplier" is required'}
});
});
it('rejects a missing group', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
res: {status: 'Invalid body format', details: '"group" is required'}
});
});
it('rejects a missing mineral property', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
res: {status: 'Invalid body format', details: '"mineral" is required'}
});
});
it('rejects a missing glass_fiber property', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
res: {status: 'Invalid body format', details: '"glass_fiber" is required'}
});
});
it('rejects a missing carbon_fiber property', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, numbers: [{color: 'black', number: 5515798402}]},
res: {status: 'Invalid body format', details: '"carbon_fiber" is required'}
});
});
it('rejects a missing numbers array', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0},
res: {status: 'Invalid body format', details: '"numbers" is required'}
});
});
it('rejects a missing color name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{number: 5515798402}]},
res: {status: 'Invalid body format', details: '"numbers[0].color" is required'}
});
});
it('rejects a missing color number', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/material/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {name: 'Crastin CE 2510', supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black'}]},
res: {status: 'Invalid body format', details: '"numbers[0].number" is required'}
}); });
}); });
it('rejects an API key', done => { it('rejects an API key', done => {

View File

@ -2,7 +2,10 @@ import express from 'express';
import MaterialValidate from './validate/material'; import MaterialValidate from './validate/material';
import MaterialModel from '../models/material' import MaterialModel from '../models/material'
import SampleModel from '../models/sample';
import IdValidate from './validate/id'; import IdValidate from './validate/id';
import res400 from './validate/res400';
import mongoose from 'mongoose';
const router = express.Router(); const router = express.Router();
@ -34,10 +37,7 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: material} = MaterialValidate.input(req.body, 'change'); const {error, value: material} = MaterialValidate.input(req.body, 'change');
if (error) { if (error) return res400(error, res);
res.status(400).json({status: 'Invalid body format'});
return;
}
if (material.hasOwnProperty('name')) { if (material.hasOwnProperty('name')) {
MaterialModel.find({name: material.name}).lean().exec((err, data) => { MaterialModel.find({name: material.name}).lean().exec((err, data) => {
@ -71,6 +71,12 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
router.delete('/material/' + IdValidate.parameter(), (req, res, next) => { router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
// check if there are still samples referencing this material
SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => {
if (err) return next(err);
if (data.length) {
return res.status(400).json({status: 'Material still in use'});
}
MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => { MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
if (data) { if (data) {
@ -80,6 +86,7 @@ router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
res.status(404).json({status: 'Not found'}); res.status(404).json({status: 'Not found'});
} }
}); });
});
}); });
router.post('/material/new', (req, res, next) => { router.post('/material/new', (req, res, next) => {
@ -87,10 +94,7 @@ router.post('/material/new', (req, res, next) => {
// validate input // validate input
const {error, value: material} = MaterialValidate.input(req.body, 'new'); const {error, value: material} = MaterialValidate.input(req.body, 'new');
if (error) { if (error) return res400(error, res);
res.status(400).json({status: 'Invalid body format'});
return;
}
MaterialModel.find({name: material.name}).lean().exec((err, data) => { MaterialModel.find({name: material.name}).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);

View File

@ -1,4 +1,4 @@
import TestHelper from "../helpers/test"; import TestHelper from "../test/helper";
describe('/', () => { describe('/', () => {

View File

@ -2,7 +2,7 @@ import should from 'should/as-function';
import SampleModel from '../models/sample'; import SampleModel from '../models/sample';
import NoteModel from '../models/note'; import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field'; import NoteFieldModel from '../models/note_field';
import TestHelper from "../helpers/test"; import TestHelper from "../test/helper";
describe('/sample', () => { describe('/sample', () => {
@ -69,6 +69,387 @@ describe('/sample', () => {
}); });
}); });
describe('PUT /sample/{id}', () => {
it('returns the right sample', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {},
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
});
});
it('keeps unchanged properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', notes: {}},
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
});
});
it('changes the given properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
}).end(err => {
if (err) return done (err);
SampleModel.findById('400000000000000000000001').lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.only.keys('_id', 'number', 'color', 'type', 'batch', 'validated', 'material_id', 'note_id', 'user_id', '__v');
should(data).have.property('_id');
should(data).have.property('number', '10');
should(data).have.property('color', 'signalviolet');
should(data).have.property('type', 'part');
should(data).have.property('batch', '114531');
should(data).have.property('validated').be.type('boolean');
should(data.material_id.toString()).be.eql('100000000000000000000002');
should(data.user_id.toString()).be.eql('000000000000000000000002');
should(data).have.property('note_id');
NoteModel.findById(data.note_id).lean().exec((err, data: any) => {
if (err) return done (err);
should(data).have.property('_id');
should(data).have.property('comment', 'Testcomment');
should(data).have.property('sample_references');
should(data.sample_references).have.lengthOf(1);
should(data.sample_references[0].id.toString()).be.eql('400000000000000000000003');
should(data.sample_references[0]).have.property('relation', 'part to this sample');
done();
});
})
});
});
it('adjusts the note_fields correctly', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000003',
auth: {basic: 'admin'},
httpStatus: 200,
req: {notes: {comment: 'Testcomment', sample_references: [], custom_fields: {field1: 'value 1'}}}
}).end(err => {
if (err) return done(err);
NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
console.log(data);
if (err) return done(err);
should(data).have.property('qty', 1);
NoteFieldModel.findOne({name: 'field1'}).lean().exec((err, data) => {
if (err) return done(err);
console.log(data);
should(data).have.property('qty', 1);
done();
});
});
});
});
it('deletes old note_fields', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200,
req: {notes: {comment: 'Testcomment', sample_references: []}}
}).end(err => {
if (err) return done (err);
NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => {
if (err) return done (err);
should(data).be.null();
done();
});
});
});
it('keeps untouched notes', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {number: '111'}
}).end((err, res) => {
if (err) return done (err);
NoteModel.findById(res.body.note_id).lean().exec((err, data) => {
if (err) return done (err);
console.log(data);
should(data).not.be.null();
should(data).have.property('comment', 'Stoff gesperrt');
should(data).have.property('sample_references').have.lengthOf(0);
done();
});
});
});
it('deletes old notes', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200,
req: {notes: {comment: 'Testcomment', sample_references: []}}
}).end(err => {
if (err) return done (err);
NoteModel.findById('500000000000000000000003').lean().exec((err, data) => {
if (err) return done (err);
should(data).be.null();
done();
});
});
});
it('rejects a color not defined for the material', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{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',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '000000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Material not available'}
});
});
it('rejects a sample number in use', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '21', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{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: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '000000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Sample reference not available'}
});
});
it('rejects an invalid material id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/10000000000h000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 401,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
});
});
it('rejects changes for samples from another user for a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000003',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('accepts changes for samples from another user for a maintain/admin user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
res: {_id: '400000000000000000000001', number: '1', type: 'granulate', color: 'black', batch: '', material_id: '100000000000000000000004', note_id: null, user_id: '000000000000000000000002'}
});
});
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
auth: {basic: 'user'},
httpStatus: 403,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
});
});
it('returns 404 for an unknown sample', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/000000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}
});
})
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/sample/400000000000000000000001',
httpStatus: 401,
req: {number: '10', type: 'part', color: 'signalviolet', batch: '114531', material_id: '100000000000000000000002', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
});
});
});
describe('DELETE /sample/{id}', () => {
it('deletes the sample', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
SampleModel.findById('400000000000000000000001').lean().exec((err, data) => {
if (err) return done(err);
should(data).be.null();
done();
});
});
});
it('deletes the notes of the sample', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
NoteModel.findById('500000000000000000000001').lean().exec((err, data) => {
if (err) return done(err);
should(data).be.null();
done();
});
});
});
it('adjusts the note_fields correctly', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
NoteFieldModel.findOne({name: 'not allowed for new applications'}).lean().exec((err, data) => {
if (err) return done(err);
should(data).have.property('qty', 1);
NoteFieldModel.findOne({name: 'another_field'}).lean().exec((err, data) => {
if (err) return done(err);
should(data).be.null();
done();
});
});
});
});
it('resets references to this sample', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000003',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
setTimeout(() => { // background action takes some time before we can check
NoteModel.findById('500000000000000000000003').lean().exec((err, data) => {
if (err) return done(err);
console.log(data);
should(data).have.property('sample_references').with.lengthOf(0);
done();
});
}, 100);
});
});
it('lets admin/maintain users delete samples of other users', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
SampleModel.findById('400000000000000000000001').lean().exec((err, data) => {
if (err) return done(err);
should(data).be.null();
done();
});
});
});
it('rejects deleting samples of other users for write users', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000004',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000h00000000004',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000004',
auth: {basic: 'user'},
httpStatus: 403
});
});
it('returns 404 for an unknown id', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/000000000000000000000004',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/sample/400000000000000000000001',
httpStatus: 401
});
});
});
describe('POST /sample/new', () => { describe('POST /sample/new', () => {
it('returns the right sample', done => { it('returns the right sample', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
@ -209,7 +590,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {number: 'Rng172', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"color" is required'}
}); });
}); });
it('rejects a missing sample number', done => { it('rejects a missing sample number', done => {
@ -219,7 +600,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {color: 'black', type: 'granulate', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"number" is required'}
}); });
}); });
it('rejects a missing type', done => { it('rejects a missing type', done => {
@ -229,7 +610,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {number: 'Rng172', color: 'black', batch: '1560237365', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"type" is required'}
}); });
}); });
it('rejects a missing batch', done => { it('rejects a missing batch', done => {
@ -239,7 +620,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {number: 'Rng172', color: 'black', type: 'granulate', material_id: '100000000000000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"batch" is required'}
}); });
}); });
it('rejects a missing material id', done => { it('rejects a missing material id', done => {
@ -249,7 +630,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"material_id" is required'}
}); });
}); });
it('rejects an invalid material id', done => { it('rejects an invalid material id', done => {
@ -259,7 +640,7 @@ describe('/sample', () => {
auth: {basic: 'janedoe'}, auth: {basic: 'janedoe'},
httpStatus: 400, httpStatus: 400,
req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}}, req: {number: 'Rng172', color: 'black', type: 'granulate', batch: '1560237365', material_id: '10000000000h000000000001', notes: {comment: 'Testcomment', sample_references: [{id: '400000000000000000000003', relation: 'part to this sample'}]}},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"material_id" with value "10000000000h000000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
}); });
}); });
it('rejects an API key', done => { it('rejects an API key', done => {

View File

@ -2,11 +2,12 @@ import express from 'express';
import SampleValidate from './validate/sample'; import SampleValidate from './validate/sample';
import NoteFieldValidate from './validate/note_field'; import NoteFieldValidate from './validate/note_field';
import res400 from './validate/res400';
import SampleModel from '../models/sample' import SampleModel from '../models/sample'
import MaterialModel from '../models/material'; import MaterialModel from '../models/material';
import NoteModel from '../models/note'; import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field'; import NoteFieldModel from '../models/note_field';
import IdValidate from './validate/id';
const router = express.Router(); const router = express.Router();
@ -20,66 +21,119 @@ router.get('/samples', (req, res, next) => {
}) })
}); });
router.put('/sample/' + IdValidate.parameter(), (req, res, next) => {
router.post('/sample/new', (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return; if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: sample} = SampleValidate.input(req.body, 'new'); const {error, value: sample} = SampleValidate.input(req.body, 'change');
if (error) { if (error) return res400(error, res);
return res.status(400).json({status: 'Invalid body format'});
SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists
if (err) return next(err);
if (!sampleData) {
return res.status(404).json({status: 'Not found'});
}
// only maintain and admin are allowed to edit other user's data
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
if (sample.hasOwnProperty('number') && sample.number !== sampleData.number) {
if (!await numberCheck(sample, res, next)) return;
}
if (sample.hasOwnProperty('material_id')) {
if (!await materialCheck(sample, res, next)) return;
}
else if (sample.hasOwnProperty('color')) {
if (!await materialCheck(sample, res, next, sampleData.material_id)) return;
} }
MaterialModel.findById(sample.material_id).lean().exec((err, data: any) => { // validate material_id if (sample.hasOwnProperty('notes') && sampleData.note_id !== null) { // deal with old notes data
NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => {
if (err) return console.error(err);
if (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1);
}
NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec(err => { // delete old notes
if (err) return console.error(err);
})
});
}
if (sample.hasOwnProperty('notes') && Object.keys(sample.notes).length > 0) { // 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);
}
let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes
delete sample.notes;
sample.note_id = data._id;
}
SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).lean().exec((err, data) => {
if (err) return next(err); if (err) return next(err);
if (!data) { // could not find material_id res.json(SampleValidate.output(data));
return res.status(400).json({status: 'Material not available'}); });
}
if (!data.numbers.find(e => e.color === sample.color)) { // color for material not specified
return res.status(400).json({status: 'Color not available for material'});
}
SampleModel.findOne({number: sample.number}).lean().exec((err, data) => { // validate sample number
if (err) return next(err);
if (data) { // found entry with sample number
return res.status(400).json({status: 'Sample number already taken'});
}
if (sample.notes.sample_references.length > 0) { // validate sample_references });
let referencesCount = sample.notes.sample_references.length; });
sample.notes.sample_references.forEach(reference => {
SampleModel.findById(reference.id).lean().exec((err, data) => { router.delete('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => { // check if id exists
if (err) return next(err); if (err) return next(err);
if (!data) { if (!sampleData) {
return res.status(400).json({status: 'Sample reference not available'}); return res.status(404).json({status: 'Not found'});
} }
referencesCount --; // only maintain and admin are allowed to edit other user's data
if (referencesCount <= 0) { if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
f();
SampleModel.findByIdAndDelete(req.params.id).lean().exec(err => { // delete sample
if (err) return next(err);
if (sampleData.note_id !== null) {
NoteModel.findByIdAndDelete(sampleData.note_id).lean().exec((err, data: any) => { // delete notes
if (err) return next(err);
console.log(data);
if (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1);
} }
res.json({status: 'OK'});
NoteModel.updateMany({'sample_references.id': req.params.id}, {$unset: {'sample_references.$': null}}).lean().exec(err => { // remove sample_references
if (err) console.error(err);
NoteModel.collection.updateMany({sample_references: null}, {$pull: {sample_references: null}}, err => { // only works with native MongoDB driver somehow
if (err) console.error(err);
});
}); });
}); });
} }
else { else {
f(); res.json({status: 'OK'});
}
});
});
});
router.post('/sample/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: sample} = SampleValidate.input(req.body, 'new');
if (error) return res400(error, res);
if (!await numberCheck(sample, res, next)) return;
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);
} }
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) {
customFieldsAdd(Object.keys(sample.notes.custom_fields));
}
function f() { // to resolve async
new NoteModel(sample.notes).save((err, data) => { new NoteModel(sample.notes).save((err, data) => {
if (err) return next(err); if (err) return next(err);
delete sample.notes; delete sample.notes;
sample.note_id = data._id; sample.note_id = data._id;
sample.user_id = req.authDetails.id; sample.user_id = req.authDetails.id;
console.log(sample);
new SampleModel(sample).save((err, data) => { new SampleModel(sample).save((err, data) => {
if (err) return next(err); if (err) return next(err);
res.json(SampleValidate.output(data.toObject())); res.json(SampleValidate.output(data.toObject()));
}); });
}); });
}
});
})
}); });
router.get('/sample/notes/fields', (req, res, next) => { router.get('/sample/notes/fields', (req, res, next) => {
@ -95,15 +149,69 @@ router.get('/sample/notes/fields', (req, res, next) => {
module.exports = router; module.exports = router;
function customFieldsAdd (fields) { async function numberCheck (sample, res, next) { // validate number, returns false if invalid
const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => { return next(err)});
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;
}
if (!materialData) { // could not find material_id
res.status(400).json({status: 'Material not available'});
return false;
}
if (sample.hasOwnProperty('color') && !materialData.numbers.find(e => e.color === sample.color)) { // color for material not specified
res.status(400).json({status: 'Color not available for material'});
return false;
}
return true;
}
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
let referencesCount = sample.notes.sample_references.length;
sample.notes.sample_references.forEach(reference => {
SampleModel.findById(reference.id).lean().exec((err, data) => {
if (err) {next(err); resolve(false)}
if (!data) {
res.status(400).json({status: 'Sample reference not available'});
return resolve(false);
}
referencesCount --;
if (referencesCount <= 0) {
resolve(true);
}
});
});
}
else {
resolve(true);
}
});
}
function customFieldsChange (fields, amount) {
fields.forEach(field => { fields.forEach(field => {
NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: 1}}).lean().exec((err, data) => { // check if field exists NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}}, {new: true}).lean().exec((err, data: any) => { // check if field exists
if (err) return console.error(err); if (err) return console.error(err);
if (!data) { // new field if (!data) { // new field
new NoteFieldModel({name: field, qty: 1}).save(err => { new NoteFieldModel({name: field, qty: 1}).save(err => {
if (err) return console.error(err); if (err) return console.error(err);
}) })
} }
else if (data.qty <= 0) {
NoteFieldModel.findOneAndDelete({name: field}).lean().exec(err => {
if (err) return console.error(err);
});
}
}); });
}); });
} }

View File

@ -1,7 +1,7 @@
import should from 'should/as-function'; import should from 'should/as-function';
import TemplateTreatmentModel from '../models/treatment_template'; import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template'; import TemplateMeasurementModel from '../models/measurement_template';
import TestHelper from "../helpers/test"; import TestHelper from "../test/helper";
describe('/template', () => { describe('/template', () => {
@ -182,14 +182,54 @@ describe('/template', () => {
}); });
}); });
}); });
it('rejects an incomplete template for a new name', done => { it('rejects a missing name for a new name', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/template/treatment/heat%20aging', url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {parameters: [{name: 'time'}]}, req: {parameters: [{name: 'time', range: {min: 1}}]},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"name" is required'}
});
});
it('rejects missing parameters for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging'},
res: {status: 'Invalid body format', details: '"parameters" is required'}
});
});
it('rejects a missing parameter name for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', parameters: [{range: {min: 1}}]},
res: {status: 'Invalid body format', details: '"parameters[0].name" is required'}
});
});
it('rejects a missing parameter range for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', parameters: [{name: 'time'}]},
res: {status: 'Invalid body format', details: '"parameters[0].range" is required'}
});
});
it('rejects a an invalid parameter range property for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/treatment/heat%20aging',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', parameters: [{name: 'time', range: {xx: 1}}]},
res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'}
}); });
}); });
it('rejects already existing names', done => { it('rejects already existing names', done => {
@ -209,7 +249,7 @@ describe('/template', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {parameters: [{name: 'time'}], xx: 33}, req: {parameters: [{name: 'time'}], xx: 33},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"name" is required'}
}); });
}); });
it('rejects an API key', done => { it('rejects an API key', done => {
@ -466,14 +506,54 @@ describe('/template', () => {
}); });
}); });
}); });
it('rejects an incomplete template for a new name', done => { it('rejects a missing name for a new name', done => {
TestHelper.request(server, done, { TestHelper.request(server, done, {
method: 'put', method: 'put',
url: '/template/measurement/vz', url: '/template/measurement/spectrum2',
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {parameters: [{name: 'vz'}]}, req: {parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"name" is required'}
});
});
it('rejects missing parameters for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum2',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'IR spectrum'},
res: {status: 'Invalid body format', details: '"parameters" is required'}
});
});
it('rejects a missing parameter name for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum2',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'IR spectrum', parameters: [{range: {min: 0, max: 1000}}]},
res: {status: 'Invalid body format', details: '"parameters[0].name" is required'}
});
});
it('rejects a missing parameter range for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum2',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'IR spectrum', parameters: [{name: 'data point table'}]},
res: {status: 'Invalid body format', details: '"parameters[0].range" is required'}
});
});
it('rejects a an invalid parameter range property for a new name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/spectrum2',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {xx: 0}}]},
res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'}
}); });
}); });
it('rejects already existing names', done => { it('rejects already existing names', done => {
@ -493,7 +573,7 @@ describe('/template', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {parameters: [{name: 'dpt'}], xx: 33}, req: {parameters: [{name: 'dpt'}], xx: 33},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"parameters[0].range" is required'}
}); });
}); });
it('rejects an API key', done => { it('rejects an API key', done => {

View File

@ -3,6 +3,7 @@ import express from 'express';
import TemplateValidate from './validate/template'; import TemplateValidate from './validate/template';
import TemplateTreatmentModel from '../models/treatment_template'; import TemplateTreatmentModel from '../models/treatment_template';
import TemplateMeasurementModel from '../models/measurement_template'; import TemplateMeasurementModel from '../models/measurement_template';
import res400 from './validate/res400';
const router = express.Router(); const router = express.Router();
@ -41,10 +42,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next
if (err) next (err); if (err) next (err);
const templateState = data? 'change': 'new'; const templateState = data? 'change': 'new';
const {error, value: template} = TemplateValidate.input(req.body, templateState); const {error, value: template} = TemplateValidate.input(req.body, templateState);
if (error) { if (error) return res400(error, res);
res.status(400).json({status: 'Invalid body format'});
return;
}
if (template.hasOwnProperty('name') && template.name !== req.params.name) { if (template.hasOwnProperty('name') && template.name !== req.params.name) {
collectionModel.find({name: template.name}).lean().exec((err, data) => { collectionModel.find({name: template.name}).lean().exec((err, data) => {

View File

@ -1,6 +1,6 @@
import should from 'should/as-function'; import should from 'should/as-function';
import UserModel from '../models/user'; import UserModel from '../models/user';
import TestHelper from "../helpers/test"; import TestHelper from "../test/helper";
describe('/user', () => { describe('/user', () => {
@ -224,7 +224,7 @@ describe('/user', () => {
req: {level: 'read'} req: {level: 'read'}
}).end((err, res) => { }).end((err, res) => {
if (err) return done (err); if (err) return done (err);
should(res.body).be.eql({status: 'Invalid body format'}); should(res.body).be.eql({status: 'Invalid body format', details: '"level" is not allowed'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => { UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err); if (err) return done(err);
should(data).have.lengthOf(1); should(data).have.lengthOf(1);
@ -267,7 +267,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'}, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', location: 44, device_name: 'Alpha II'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"location" must be a string'}
}); });
}); });
it('rejects an invalid email address', done => { it('rejects an invalid email address', done => {
@ -277,7 +277,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe'}, req: {email: 'john.doe'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"email" must be a valid email'}
}); });
}); });
it('rejects an invalid password', done => { it('rejects an invalid password', done => {
@ -287,7 +287,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {pass: 'password'}, req: {pass: 'password'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$/'}
}); });
}); });
it('rejects requests from non-admins for another user', done => { it('rejects requests from non-admins for another user', done => {
@ -515,7 +515,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'}, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 44, device_name: 'Alpha II'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"location" must be a string'}
}); });
}); });
it('rejects an invalid user level', done => { it('rejects an invalid user level', done => {
@ -525,7 +525,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'}, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'xxx', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"level" must be one of [read, write, maintain, dev, admin]'}
}); });
}); });
it('rejects an invalid email address', done => { it('rejects an invalid email address', done => {
@ -535,7 +535,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}, req: {email: 'john.doe', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"email" must be a valid email'}
}); });
}); });
it('rejects an invalid password', done => { it('rejects an invalid password', done => {
@ -545,7 +545,7 @@ describe('/user', () => {
auth: {basic: 'admin'}, auth: {basic: 'admin'},
httpStatus: 400, httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'}, req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Invalid body format'} res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$/'}
}); });
}); });
it('rejects requests from non-admins', done => { it('rejects requests from non-admins', done => {

View File

@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
import UserValidate from './validate/user'; import UserValidate from './validate/user';
import UserModel from '../models/user'; import UserModel from '../models/user';
import mail from '../helpers/mail'; import mail from '../helpers/mail';
import res400 from './validate/res400';
const router = express.Router(); const router = express.Router();
@ -46,10 +47,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // thi
username = req.params.username; username = req.params.username;
} }
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : '')); const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
if (error) { if (error) return res400(error, res);
res.status(400).json({status: 'Invalid body format'});
return;
}
if (user.hasOwnProperty('pass')) { if (user.hasOwnProperty('pass')) {
user.pass = bcrypt.hashSync(user.pass, 10); user.pass = bcrypt.hashSync(user.pass, 10);
@ -122,10 +120,7 @@ router.post('/user/new', (req, res, next) => {
// validate input // validate input
const {error, value: user} = UserValidate.input(req.body, 'new'); const {error, value: user} = UserValidate.input(req.body, 'new');
if (error) { if (error) return res400(error, res);
res.status(400).json({status: 'Invalid body format'});
return;
}
// check that user does not already exist // check that user does not already exist
UserModel.find({name: user.name}).lean().exec( (err, data:any) => { UserModel.find({name: user.name}).lean().exec( (err, data:any) => {

View File

@ -1,7 +1,7 @@
import joi from '@hapi/joi'; import Joi from '@hapi/joi';
export default class IdValidate { export default class IdValidate {
private static id = joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24); private static id = Joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24);
static get () { static get () {
return this.id; return this.id;

View File

@ -31,9 +31,11 @@ export default class MaterialValidate { // validate input for material
numbers: joi.array() numbers: joi.array()
.items(joi.object({ .items(joi.object({
color: joi.string() color: joi.string()
.max(128), .max(128)
.required(),
number: joi.number() number: joi.number()
.min(0) .min(0)
.required()
})) }))
}; };
@ -46,7 +48,7 @@ export default class MaterialValidate { // validate input for material
mineral: this.material.mineral.required(), mineral: this.material.mineral.required(),
glass_fiber: this.material.glass_fiber.required(), glass_fiber: this.material.glass_fiber.required(),
carbon_fiber: this.material.carbon_fiber.required(), carbon_fiber: this.material.carbon_fiber.required(),
numbers: this.material.numbers numbers: this.material.numbers.required()
}).validate(data); }).validate(data);
} }
else if (param === 'change') { else if (param === 'change') {

View File

@ -1,15 +1,15 @@
import joi from '@hapi/joi'; import Joi from '@hapi/joi';
export default class NoteFieldValidate { export default class NoteFieldValidate {
private static note_field = { private static note_field = {
name: joi.string() name: Joi.string()
.max(128), .max(128),
qty: joi.number() qty: Joi.number()
}; };
static output (data) { static output (data) {
const {value, error} = joi.object({ const {value, error} = Joi.object({
name: this.note_field.name, name: this.note_field.name,
qty: this.note_field.qty qty: this.note_field.qty
}).validate(data, {stripUnknown: true}); }).validate(data, {stripUnknown: true});

View File

@ -0,0 +1,3 @@
export default function res400 (error, res) {
res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
}

View File

@ -1,41 +1,41 @@
import joi from '@hapi/joi'; import Joi from '@hapi/joi';
import IdValidate from './id'; import IdValidate from './id';
export default class SampleValidate { export default class SampleValidate {
private static sample = { private static sample = {
number: joi.string() number: Joi.string()
.max(128), .max(128),
color: joi.string() color: Joi.string()
.max(128), .max(128),
type: joi.string() type: Joi.string()
.max(128), .max(128),
batch: joi.string() batch: Joi.string()
.max(128) .max(128)
.allow(''), .allow(''),
notes: joi.object({ notes: Joi.object({
comment: joi.string() comment: Joi.string()
.max(512), .max(512),
sample_references: joi.array() sample_references: Joi.array()
.items(joi.object({ .items(Joi.object({
id: IdValidate.get(), id: IdValidate.get(),
relation: joi.string() relation: Joi.string()
.max(128) .max(128)
})), })),
custom_fields: joi.object() custom_fields: Joi.object()
.pattern(/.*/, joi.alternatives() .pattern(/.*/, Joi.alternatives()
.try( .try(
joi.string().max(128), Joi.string().max(128),
joi.number(), Joi.number(),
joi.boolean(), Joi.boolean(),
joi.date() Joi.date()
) )
) )
}) })
@ -43,7 +43,7 @@ export default class SampleValidate {
static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated) static input (data, param) { // validate data, param: new(everything required)/change(available attributes are validated)
if (param === 'new') { if (param === 'new') {
return joi.object({ return Joi.object({
number: this.sample.number.required(), number: this.sample.number.required(),
color: this.sample.color.required(), color: this.sample.color.required(),
type: this.sample.type.required(), type: this.sample.type.required(),
@ -53,7 +53,14 @@ export default class SampleValidate {
}).validate(data); }).validate(data);
} }
else if (param === 'change') { else if (param === 'change') {
return{error: 'Not implemented!', value: {}}; return Joi.object({
number: this.sample.number,
color: this.sample.color,
type: this.sample.type,
batch: this.sample.batch,
material_id: IdValidate.get(),
notes: this.sample.notes,
}).validate(data);
} }
else { else {
return{error: 'No parameter specified!', value: {}}; return{error: 'No parameter specified!', value: {}};
@ -62,7 +69,7 @@ export default class SampleValidate {
static output (data) { static output (data) {
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),
number: this.sample.number, number: this.sample.number,
color: this.sample.color, color: this.sample.color,

View File

@ -1,32 +1,32 @@
import joi from '@hapi/joi'; import Joi from '@hapi/joi';
import globals from '../../globals'; import globals from '../../globals';
import IdValidate from './id'; import IdValidate from './id';
export default class UserValidate { // validate input for user export default class UserValidate { // validate input for user
private static user = { private static user = {
name: joi.string() name: Joi.string()
.alphanum() .alphanum()
.lowercase() .lowercase()
.max(128), .max(128),
email: joi.string() email: Joi.string()
.email({minDomainSegments: 2}) .email({minDomainSegments: 2})
.lowercase() .lowercase()
.max(128), .max(128),
pass: joi.string() pass: Joi.string()
.pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$')) .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$'))
.max(128), .max(128),
level: joi.string() level: Joi.string()
.valid(...globals.levels), .valid(...globals.levels),
location: joi.string() location: Joi.string()
.alphanum() .alphanum()
.max(128), .max(128),
device_name: joi.string() device_name: Joi.string()
.allow('') .allow('')
.max(128), .max(128),
}; };
@ -35,7 +35,7 @@ export default class UserValidate { // validate input for user
static input (data, param) { static input (data, param) {
if (param === 'new') { if (param === 'new') {
return joi.object({ return Joi.object({
name: this.user.name.required(), name: this.user.name.required(),
email: this.user.email.required(), email: this.user.email.required(),
pass: this.user.pass.required(), pass: this.user.pass.required(),
@ -45,7 +45,7 @@ export default class UserValidate { // validate input for user
}).validate(data); }).validate(data);
} }
else if (param === 'change') { else if (param === 'change') {
return joi.object({ return Joi.object({
name: this.user.name, name: this.user.name,
email: this.user.email, email: this.user.email,
pass: this.user.pass, pass: this.user.pass,
@ -54,7 +54,7 @@ export default class UserValidate { // validate input for user
}).validate(data); }).validate(data);
} }
else if (param === 'changeadmin') { else if (param === 'changeadmin') {
return joi.object({ return Joi.object({
name: this.user.name, name: this.user.name,
email: this.user.email, email: this.user.email,
pass: this.user.pass, pass: this.user.pass,
@ -70,7 +70,7 @@ export default class UserValidate { // validate input for user
static output (data) { // validate output from database for needed properties, strip everything else static output (data) { // validate output from database for needed properties, strip everything else
data = IdValidate.stringify(data); data = IdValidate.stringify(data);
const {value, error} = joi.object({ const {value, error} = Joi.object({
_id: IdValidate.get(), _id: IdValidate.get(),
name: this.user.name, name: this.user.name,
email: this.user.email, email: this.user.email,

View File

@ -61,7 +61,7 @@
"_id": {"$oid":"500000000000000000000002"}, "_id": {"$oid":"500000000000000000000002"},
"comment": "", "comment": "",
"sample_references": [{ "sample_references": [{
"id": "400000000000000000000004", "id": {"$oid":"400000000000000000000004"},
"relation": "granulate to sample" "relation": "granulate to sample"
}], }],
"custom_fields": { "custom_fields": {
@ -73,11 +73,12 @@
"_id": {"$oid":"500000000000000000000003"}, "_id": {"$oid":"500000000000000000000003"},
"comment": "", "comment": "",
"sample_references": [{ "sample_references": [{
"id": "400000000000000000000003", "id": {"$oid":"400000000000000000000003"},
"relation": "part to sample" "relation": "part to sample"
}], }],
"custom_fields": { "custom_fields": {
"not allowed for new applications": true "not allowed for new applications": true,
"another_field": "is there"
}, },
"__v": 0 "__v": 0
} }
@ -88,6 +89,12 @@
"name": "not allowed for new applications", "name": "not allowed for new applications",
"qty": 2, "qty": 2,
"__v": 0 "__v": 0
},
{
"_id": {"$oid":"600000000000000000000002"},
"name": "another_field",
"qty": 1,
"__v": 0
} }
], ],
"materials": [ "materials": [

View File

@ -28,7 +28,7 @@ export default class TestHelper {
server = require('../index'); server = require('../index');
db.drop(err => { // reset database db.drop(err => { // reset database
if (err) return done(err); if (err) return done(err);
db.loadJson(require('../test/db.json'), done); db.loadJson(require('./db.json'), done);
}); });
return server return server
} }

12
src/test/loadDev.ts Normal file
View File

@ -0,0 +1,12 @@
import db from '../db';
db.connect('dev', () => {
console.info('dropping data...');
db.drop(() => { // reset database
console.info('loading data...');
db.loadJson(require('./db.json'), () => {
console.info('done');
process.exit(0);
});
});
});