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:
		
							
								
								
									
										11
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal 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
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								.idea/dbnavigator.xml
									
									
									
										generated
									
									
									
										Normal 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>
 | 
			
		||||
@@ -79,6 +79,8 @@
 | 
			
		||||
    responses:
 | 
			
		||||
      200:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/Ok'
 | 
			
		||||
      400:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/400'
 | 
			
		||||
      401:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/401'
 | 
			
		||||
      403:
 | 
			
		||||
 
 | 
			
		||||
@@ -41,8 +41,8 @@
 | 
			
		||||
      500:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/500'
 | 
			
		||||
  put:
 | 
			
		||||
    summary: TODO change sample
 | 
			
		||||
    description: 'Auth: basic, levels: write, maintain, dev, admin'
 | 
			
		||||
    summary: change sample
 | 
			
		||||
    description: 'Auth: basic, levels: write, maintain, dev, admin, only maintain and admin are allowed to edit samples created by another user'
 | 
			
		||||
    tags:
 | 
			
		||||
      - /sample
 | 
			
		||||
    security:
 | 
			
		||||
@@ -59,7 +59,7 @@
 | 
			
		||||
        content:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: 'api.yaml#/components/schemas/SampleDetail'
 | 
			
		||||
              $ref: 'api.yaml#/components/schemas/SampleRefs'
 | 
			
		||||
      400:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/400'
 | 
			
		||||
      401:
 | 
			
		||||
@@ -71,8 +71,8 @@
 | 
			
		||||
      500:
 | 
			
		||||
        $ref: 'api.yaml#/components/responses/500'
 | 
			
		||||
  delete:
 | 
			
		||||
    summary: TODO delete sample
 | 
			
		||||
    description: 'Auth: basic, levels: write, maintain, dev, admin'
 | 
			
		||||
    summary: delete sample
 | 
			
		||||
    description: 'Auth: basic, levels: write, maintain, dev, admin, only maintain and admin are allowed to edit samples created by another user'
 | 
			
		||||
    tags:
 | 
			
		||||
      - /sample
 | 
			
		||||
    security:
 | 
			
		||||
@@ -123,7 +123,7 @@
 | 
			
		||||
 | 
			
		||||
/sample/notes/fields:
 | 
			
		||||
  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'
 | 
			
		||||
    tags:
 | 
			
		||||
      - /sample
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,8 @@
 | 
			
		||||
    "tsc": "tsc",
 | 
			
		||||
    "test": "mocha dist/**/**.spec.js",
 | 
			
		||||
    "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": [],
 | 
			
		||||
  "author": "",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								src/db.ts
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								src/db.ts
									
									
									
									
									
								
							@@ -42,19 +42,19 @@ export default class db {
 | 
			
		||||
    });
 | 
			
		||||
    mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
 | 
			
		||||
    mongoose.connection.on('disconnected', () => {  // reset state on disconnect
 | 
			
		||||
      console.log('Database disconnected');
 | 
			
		||||
      console.info('Database disconnected');
 | 
			
		||||
      this.state.db = 0;
 | 
			
		||||
      done();
 | 
			
		||||
    });
 | 
			
		||||
    process.on('SIGINT', () => {  // close connection when app is terminated
 | 
			
		||||
      mongoose.connection.close(() => {
 | 
			
		||||
        console.log('Mongoose default connection disconnected through app termination');
 | 
			
		||||
        console.info('Mongoose default connection disconnected through app termination');
 | 
			
		||||
        process.exit(0);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    mongoose.connection.once('open', () => {
 | 
			
		||||
      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;
 | 
			
		||||
      done();
 | 
			
		||||
    });
 | 
			
		||||
@@ -90,13 +90,7 @@ export default class db {
 | 
			
		||||
 | 
			
		||||
    let loadCounter = 0;  // count number of loaded collections to know when to return done()
 | 
			
		||||
    Object.keys(json.collections).forEach(collectionName => {  // create each collection
 | 
			
		||||
      for(let i in json.collections[collectionName]) {  // convert $oid fields to actual ObjectIds
 | 
			
		||||
        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);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
      json.collections[collectionName] = this.oidResolve(json.collections[collectionName]);
 | 
			
		||||
      this.state.db.createCollection(collectionName, (err, collection) => {
 | 
			
		||||
        collection.insertMany(json.collections[collectionName], () => {  // insert JSON data
 | 
			
		||||
          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;
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -30,7 +30,7 @@ export default (mailAddress, subject, content, f) => {  // callback, executed em
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
  else if (process.env.NODE_ENV === 'test') {
 | 
			
		||||
    console.log('Sending mail to ' + mailAddress + ':  -- ' + subject + ' -- ' + content);
 | 
			
		||||
    console.info('Sending mail to ' + mailAddress + ':  -- ' + subject + ' -- ' + content);
 | 
			
		||||
    f();
 | 
			
		||||
  }
 | 
			
		||||
  else {  // dev
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import db from './db';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
@@ -75,7 +75,7 @@ app.use((err, req, res, ignore) => {  // internal server error handling
 | 
			
		||||
 | 
			
		||||
// hook up server to 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;
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import should from 'should/as-function';
 | 
			
		||||
import MaterialModel from '../models/material';
 | 
			
		||||
import TestHelper from "../helpers/test";
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('/material', () => {
 | 
			
		||||
@@ -171,14 +171,54 @@ describe('/material', () => {
 | 
			
		||||
        res: {status: 'Material name already taken'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects wrong material properties', done => {
 | 
			
		||||
    it('rejects a wrong mineral property', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'put',
 | 
			
		||||
        url: '/material/100000000000000000000001',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        req: {mineral: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]},
 | 
			
		||||
        res: {status: 'Invalid body format'}
 | 
			
		||||
        req: {mineral: 'x'},
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -231,20 +271,28 @@ describe('/material', () => {
 | 
			
		||||
    it('deletes the material', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
        url: '/material/100000000000000000000001',
 | 
			
		||||
        url: '/material/100000000000000000000002',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 200
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
        if (err) return done(err);
 | 
			
		||||
        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);
 | 
			
		||||
          should(data).be.null();
 | 
			
		||||
          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 => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
        method: 'delete',
 | 
			
		||||
@@ -347,24 +395,94 @@ describe('/material', () => {
 | 
			
		||||
        res: {status: 'Material name already taken'}
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    it('rejects wrong material properties', done => {
 | 
			
		||||
    it('rejects a missing 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: 'x', glass_fiber: 'x', carbon_fiber: 'x', numbers: [{colorxx: 'black', number: 'xxx'}]},
 | 
			
		||||
        res: {status: 'Invalid body format'}
 | 
			
		||||
        req: {supplier: 'Du Pont', group: 'PBT', mineral: 0, glass_fiber: 30, carbon_fiber: 0, numbers: [{color: 'black', number: 5515798402}]},
 | 
			
		||||
        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, {
 | 
			
		||||
        method: 'post',
 | 
			
		||||
        url: '/material/new',
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        req: {name: 'Crastin CE 2510'},
 | 
			
		||||
        res: {status: 'Invalid body format'}
 | 
			
		||||
        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', 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 => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,10 @@ import express from 'express';
 | 
			
		||||
 | 
			
		||||
import MaterialValidate from './validate/material';
 | 
			
		||||
import MaterialModel from '../models/material'
 | 
			
		||||
import SampleModel from '../models/sample';
 | 
			
		||||
import IdValidate from './validate/id';
 | 
			
		||||
import res400 from './validate/res400';
 | 
			
		||||
import mongoose from 'mongoose';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
  const {error, value: material} = MaterialValidate.input(req.body, 'change');
 | 
			
		||||
  if (error) {
 | 
			
		||||
    res.status(400).json({status: 'Invalid body format'});
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  if (material.hasOwnProperty('name')) {
 | 
			
		||||
    MaterialModel.find({name: material.name}).lean().exec((err, data) => {
 | 
			
		||||
@@ -71,14 +71,21 @@ router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
			
		||||
router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
 | 
			
		||||
  if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
 | 
			
		||||
 | 
			
		||||
  MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => {
 | 
			
		||||
  // 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) {
 | 
			
		||||
      res.json({status: 'OK'})
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
      res.status(404).json({status: 'Not found'});
 | 
			
		||||
    if (data.length) {
 | 
			
		||||
      return res.status(400).json({status: 'Material still in use'});
 | 
			
		||||
    }
 | 
			
		||||
    MaterialModel.findByIdAndDelete(req.params.id).lean().exec((err, data) => {
 | 
			
		||||
      if (err) return next(err);
 | 
			
		||||
      if (data) {
 | 
			
		||||
        res.json({status: 'OK'})
 | 
			
		||||
      }
 | 
			
		||||
      else {
 | 
			
		||||
        res.status(404).json({status: 'Not found'});
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@@ -87,10 +94,7 @@ router.post('/material/new', (req, res, next) => {
 | 
			
		||||
 | 
			
		||||
  // validate input
 | 
			
		||||
  const {error, value: material} = MaterialValidate.input(req.body, 'new');
 | 
			
		||||
  if (error) {
 | 
			
		||||
    res.status(400).json({status: 'Invalid body format'});
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  MaterialModel.find({name: material.name}).lean().exec((err, data) => {
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import TestHelper from "../helpers/test";
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('/', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import should from 'should/as-function';
 | 
			
		||||
import SampleModel from '../models/sample';
 | 
			
		||||
import NoteModel from '../models/note';
 | 
			
		||||
import NoteFieldModel from '../models/note_field';
 | 
			
		||||
import TestHelper from "../helpers/test";
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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', () => {
 | 
			
		||||
    it('returns the right sample', done => {
 | 
			
		||||
      TestHelper.request(server, done, {
 | 
			
		||||
@@ -209,7 +590,7 @@ describe('/sample', () => {
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -219,7 +600,7 @@ describe('/sample', () => {
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -229,7 +610,7 @@ describe('/sample', () => {
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -239,7 +620,7 @@ describe('/sample', () => {
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -249,7 +630,7 @@ describe('/sample', () => {
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -259,7 +640,7 @@ describe('/sample', () => {
 | 
			
		||||
        auth: {basic: 'janedoe'},
 | 
			
		||||
        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'}]}},
 | 
			
		||||
        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 => {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,12 @@ import express from 'express';
 | 
			
		||||
 | 
			
		||||
import SampleValidate from './validate/sample';
 | 
			
		||||
import NoteFieldValidate from './validate/note_field';
 | 
			
		||||
import res400 from './validate/res400';
 | 
			
		||||
import SampleModel from '../models/sample'
 | 
			
		||||
import MaterialModel from '../models/material';
 | 
			
		||||
import NoteModel from '../models/note';
 | 
			
		||||
import NoteFieldModel from '../models/note_field';
 | 
			
		||||
 | 
			
		||||
import IdValidate from './validate/id';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
@@ -20,66 +21,119 @@ router.get('/samples', (req, res, next) => {
 | 
			
		||||
  })
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
router.post('/sample/new', (req, res, next) => {
 | 
			
		||||
router.put('/sample/' + IdValidate.parameter(), (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 res.status(400).json({status: 'Invalid body format'});
 | 
			
		||||
  }
 | 
			
		||||
  const {error, value: sample} = SampleValidate.input(req.body, 'change');
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  MaterialModel.findById(sample.material_id).lean().exec((err, data: any) => {  // validate material_id
 | 
			
		||||
  SampleModel.findById(req.params.id).lean().exec(async (err, sampleData: any) => {  // check if id exists
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
    if (!data) {  // could not find material_id
 | 
			
		||||
      return res.status(400).json({status: 'Material not available'});
 | 
			
		||||
    if (!sampleData) {
 | 
			
		||||
      return res.status(404).json({status: 'Not found'});
 | 
			
		||||
    }
 | 
			
		||||
    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'});
 | 
			
		||||
      }
 | 
			
		||||
    // 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.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) => {
 | 
			
		||||
            if (err) return next(err);
 | 
			
		||||
            if (!data) {
 | 
			
		||||
              return res.status(400).json({status: 'Sample reference not available'});
 | 
			
		||||
            }
 | 
			
		||||
            referencesCount --;
 | 
			
		||||
            if (referencesCount <= 0) {
 | 
			
		||||
              f();
 | 
			
		||||
            }
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
      res.json(SampleValidate.output(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 (!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;
 | 
			
		||||
 | 
			
		||||
    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 {
 | 
			
		||||
        f();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      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) => {
 | 
			
		||||
          if (err) return next(err);
 | 
			
		||||
          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);
 | 
			
		||||
            res.json(SampleValidate.output(data.toObject()));
 | 
			
		||||
          });
 | 
			
		||||
        });
 | 
			
		||||
        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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  new NoteModel(sample.notes).save((err, data) => {
 | 
			
		||||
    if (err) return next(err);
 | 
			
		||||
    delete sample.notes;
 | 
			
		||||
    sample.note_id = data._id;
 | 
			
		||||
    sample.user_id = req.authDetails.id;
 | 
			
		||||
    console.log(sample);
 | 
			
		||||
    new SampleModel(sample).save((err, data) => {
 | 
			
		||||
      if (err) return next(err);
 | 
			
		||||
      res.json(SampleValidate.output(data.toObject()));
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
router.get('/sample/notes/fields', (req, res, next) => {
 | 
			
		||||
@@ -95,15 +149,69 @@ router.get('/sample/notes/fields', (req, res, next) => {
 | 
			
		||||
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 => {
 | 
			
		||||
    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 (!data) {  // new field
 | 
			
		||||
        new NoteFieldModel({name: field, qty: 1}).save(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);
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import should from 'should/as-function';
 | 
			
		||||
import TemplateTreatmentModel from '../models/treatment_template';
 | 
			
		||||
import TemplateMeasurementModel from '../models/measurement_template';
 | 
			
		||||
import TestHelper from "../helpers/test";
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/treatment/heat%20aging',
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 400,
 | 
			
		||||
          req: {parameters: [{name: 'time'}]},
 | 
			
		||||
          res: {status: 'Invalid body format'}
 | 
			
		||||
          req: {parameters: [{name: 'time', range: {min: 1}}]},
 | 
			
		||||
          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 => {
 | 
			
		||||
@@ -209,7 +249,7 @@ describe('/template', () => {
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 400,
 | 
			
		||||
          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 => {
 | 
			
		||||
@@ -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, {
 | 
			
		||||
          method: 'put',
 | 
			
		||||
          url: '/template/measurement/vz',
 | 
			
		||||
          url: '/template/measurement/spectrum2',
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 400,
 | 
			
		||||
          req: {parameters: [{name: 'vz'}]},
 | 
			
		||||
          res: {status: 'Invalid body format'}
 | 
			
		||||
          req: {parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
 | 
			
		||||
          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 => {
 | 
			
		||||
@@ -493,7 +573,7 @@ describe('/template', () => {
 | 
			
		||||
          auth: {basic: 'admin'},
 | 
			
		||||
          httpStatus: 400,
 | 
			
		||||
          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 => {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import express from 'express';
 | 
			
		||||
import TemplateValidate from './validate/template';
 | 
			
		||||
import TemplateTreatmentModel from '../models/treatment_template';
 | 
			
		||||
import TemplateMeasurementModel from '../models/measurement_template';
 | 
			
		||||
import res400 from './validate/res400';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
@@ -41,10 +42,7 @@ router.put('/template/:collection(measurement|treatment)/:name', (req, res, next
 | 
			
		||||
    if (err) next (err);
 | 
			
		||||
    const templateState = data? 'change': 'new';
 | 
			
		||||
    const {error, value: template} = TemplateValidate.input(req.body, templateState);
 | 
			
		||||
    if (error) {
 | 
			
		||||
      res.status(400).json({status: 'Invalid body format'});
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
    if (template.hasOwnProperty('name') && template.name !== req.params.name) {
 | 
			
		||||
      collectionModel.find({name: template.name}).lean().exec((err, data) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import should from 'should/as-function';
 | 
			
		||||
import UserModel from '../models/user';
 | 
			
		||||
import TestHelper from "../helpers/test";
 | 
			
		||||
import TestHelper from "../test/helper";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
describe('/user', () => {
 | 
			
		||||
@@ -224,7 +224,7 @@ describe('/user', () => {
 | 
			
		||||
        req: {level: 'read'}
 | 
			
		||||
      }).end((err, res) => {
 | 
			
		||||
          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) => {
 | 
			
		||||
            if (err) return done(err);
 | 
			
		||||
            should(data).have.lengthOf(1);
 | 
			
		||||
@@ -267,7 +267,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -277,7 +277,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -287,7 +287,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -515,7 +515,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -525,7 +525,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -535,7 +535,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        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 => {
 | 
			
		||||
@@ -545,7 +545,7 @@ describe('/user', () => {
 | 
			
		||||
        auth: {basic: 'admin'},
 | 
			
		||||
        httpStatus: 400,
 | 
			
		||||
        req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'},
 | 
			
		||||
        res: {status: 'Invalid body format'}
 | 
			
		||||
        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 => {
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import bcrypt from 'bcryptjs';
 | 
			
		||||
import UserValidate from './validate/user';
 | 
			
		||||
import UserModel from '../models/user';
 | 
			
		||||
import mail from '../helpers/mail';
 | 
			
		||||
import res400 from './validate/res400';
 | 
			
		||||
 | 
			
		||||
const router = express.Router();
 | 
			
		||||
 | 
			
		||||
@@ -46,10 +47,7 @@ router.put('/user:username([/](?!key|new).?*|/?)', (req, res, next) => {  // thi
 | 
			
		||||
    username = req.params.username;
 | 
			
		||||
  }
 | 
			
		||||
  const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
 | 
			
		||||
  if (error) {
 | 
			
		||||
    res.status(400).json({status: 'Invalid body format'});
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  if (user.hasOwnProperty('pass')) {
 | 
			
		||||
    user.pass = bcrypt.hashSync(user.pass, 10);
 | 
			
		||||
@@ -122,10 +120,7 @@ router.post('/user/new', (req, res, next) => {
 | 
			
		||||
 | 
			
		||||
  // validate input
 | 
			
		||||
  const {error, value: user} = UserValidate.input(req.body, 'new');
 | 
			
		||||
  if (error) {
 | 
			
		||||
    res.status(400).json({status: 'Invalid body format'});
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (error) return res400(error, res);
 | 
			
		||||
 | 
			
		||||
  // check that user does not already exist
 | 
			
		||||
  UserModel.find({name: user.name}).lean().exec(  (err, data:any) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import joi from '@hapi/joi';
 | 
			
		||||
import Joi from '@hapi/joi';
 | 
			
		||||
 | 
			
		||||
export default class IdValidate {
 | 
			
		||||
  private static id = joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24);
 | 
			
		||||
  private static id = Joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24);
 | 
			
		||||
 | 
			
		||||
  static get () {
 | 
			
		||||
    return this.id;
 | 
			
		||||
 
 | 
			
		||||
@@ -31,9 +31,11 @@ export default class MaterialValidate {  // validate input for material
 | 
			
		||||
    numbers: joi.array()
 | 
			
		||||
      .items(joi.object({
 | 
			
		||||
        color: joi.string()
 | 
			
		||||
          .max(128),
 | 
			
		||||
          .max(128)
 | 
			
		||||
          .required(),
 | 
			
		||||
        number: joi.number()
 | 
			
		||||
          .min(0)
 | 
			
		||||
          .required()
 | 
			
		||||
      }))
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +48,7 @@ export default class MaterialValidate {  // validate input for material
 | 
			
		||||
        mineral: this.material.mineral.required(),
 | 
			
		||||
        glass_fiber: this.material.glass_fiber.required(),
 | 
			
		||||
        carbon_fiber: this.material.carbon_fiber.required(),
 | 
			
		||||
        numbers: this.material.numbers
 | 
			
		||||
        numbers: this.material.numbers.required()
 | 
			
		||||
      }).validate(data);
 | 
			
		||||
    }
 | 
			
		||||
    else if (param === 'change') {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import joi from '@hapi/joi';
 | 
			
		||||
import Joi from '@hapi/joi';
 | 
			
		||||
 | 
			
		||||
export default class NoteFieldValidate {
 | 
			
		||||
  private static note_field = {
 | 
			
		||||
    name: joi.string()
 | 
			
		||||
    name: Joi.string()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    qty: joi.number()
 | 
			
		||||
    qty: Joi.number()
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static output (data) {
 | 
			
		||||
    const {value, error} = joi.object({
 | 
			
		||||
    const {value, error} = Joi.object({
 | 
			
		||||
      name: this.note_field.name,
 | 
			
		||||
      qty: this.note_field.qty
 | 
			
		||||
    }).validate(data, {stripUnknown: true});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/routes/validate/res400.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/routes/validate/res400.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
export default function res400 (error, res) {
 | 
			
		||||
  res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
 | 
			
		||||
}
 | 
			
		||||
@@ -1,41 +1,41 @@
 | 
			
		||||
import joi from '@hapi/joi';
 | 
			
		||||
import Joi from '@hapi/joi';
 | 
			
		||||
 | 
			
		||||
import IdValidate from './id';
 | 
			
		||||
 | 
			
		||||
export default class SampleValidate {
 | 
			
		||||
  private static sample = {
 | 
			
		||||
    number: joi.string()
 | 
			
		||||
    number: Joi.string()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    color: joi.string()
 | 
			
		||||
    color: Joi.string()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    type: joi.string()
 | 
			
		||||
    type: Joi.string()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    batch: joi.string()
 | 
			
		||||
    batch: Joi.string()
 | 
			
		||||
      .max(128)
 | 
			
		||||
      .allow(''),
 | 
			
		||||
 | 
			
		||||
    notes: joi.object({
 | 
			
		||||
      comment: joi.string()
 | 
			
		||||
    notes: Joi.object({
 | 
			
		||||
      comment: Joi.string()
 | 
			
		||||
        .max(512),
 | 
			
		||||
 | 
			
		||||
      sample_references: joi.array()
 | 
			
		||||
        .items(joi.object({
 | 
			
		||||
      sample_references: Joi.array()
 | 
			
		||||
        .items(Joi.object({
 | 
			
		||||
          id: IdValidate.get(),
 | 
			
		||||
 | 
			
		||||
          relation: joi.string()
 | 
			
		||||
          relation: Joi.string()
 | 
			
		||||
            .max(128)
 | 
			
		||||
        })),
 | 
			
		||||
 | 
			
		||||
      custom_fields: joi.object()
 | 
			
		||||
        .pattern(/.*/, joi.alternatives()
 | 
			
		||||
      custom_fields: Joi.object()
 | 
			
		||||
        .pattern(/.*/, Joi.alternatives()
 | 
			
		||||
          .try(
 | 
			
		||||
            joi.string().max(128),
 | 
			
		||||
            joi.number(),
 | 
			
		||||
            joi.boolean(),
 | 
			
		||||
            joi.date()
 | 
			
		||||
            Joi.string().max(128),
 | 
			
		||||
            Joi.number(),
 | 
			
		||||
            Joi.boolean(),
 | 
			
		||||
            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)
 | 
			
		||||
    if (param === 'new') {
 | 
			
		||||
      return joi.object({
 | 
			
		||||
      return Joi.object({
 | 
			
		||||
        number: this.sample.number.required(),
 | 
			
		||||
        color: this.sample.color.required(),
 | 
			
		||||
        type: this.sample.type.required(),
 | 
			
		||||
@@ -53,7 +53,14 @@ export default class SampleValidate {
 | 
			
		||||
      }).validate(data);
 | 
			
		||||
    }
 | 
			
		||||
    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 {
 | 
			
		||||
      return{error: 'No parameter specified!', value: {}};
 | 
			
		||||
@@ -62,7 +69,7 @@ export default class SampleValidate {
 | 
			
		||||
 | 
			
		||||
  static output (data) {
 | 
			
		||||
    data = IdValidate.stringify(data);
 | 
			
		||||
    const {value, error} = joi.object({
 | 
			
		||||
    const {value, error} = Joi.object({
 | 
			
		||||
      _id: IdValidate.get(),
 | 
			
		||||
      number: this.sample.number,
 | 
			
		||||
      color: this.sample.color,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,32 +1,32 @@
 | 
			
		||||
import joi from '@hapi/joi';
 | 
			
		||||
import Joi from '@hapi/joi';
 | 
			
		||||
import globals from '../../globals';
 | 
			
		||||
 | 
			
		||||
import IdValidate from './id';
 | 
			
		||||
 | 
			
		||||
export default class UserValidate {  // validate input for user
 | 
			
		||||
  private static user = {
 | 
			
		||||
    name: joi.string()
 | 
			
		||||
    name: Joi.string()
 | 
			
		||||
      .alphanum()
 | 
			
		||||
      .lowercase()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    email: joi.string()
 | 
			
		||||
    email: Joi.string()
 | 
			
		||||
      .email({minDomainSegments: 2})
 | 
			
		||||
      .lowercase()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    pass: joi.string()
 | 
			
		||||
    pass: Joi.string()
 | 
			
		||||
      .pattern(new RegExp('^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$).{8,}$'))
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    level: joi.string()
 | 
			
		||||
    level: Joi.string()
 | 
			
		||||
      .valid(...globals.levels),
 | 
			
		||||
 | 
			
		||||
    location: joi.string()
 | 
			
		||||
    location: Joi.string()
 | 
			
		||||
      .alphanum()
 | 
			
		||||
      .max(128),
 | 
			
		||||
 | 
			
		||||
    device_name: joi.string()
 | 
			
		||||
    device_name: Joi.string()
 | 
			
		||||
      .allow('')
 | 
			
		||||
      .max(128),
 | 
			
		||||
  };
 | 
			
		||||
@@ -35,7 +35,7 @@ export default class UserValidate {  // validate input for user
 | 
			
		||||
 | 
			
		||||
  static input (data, param) {
 | 
			
		||||
    if (param === 'new') {
 | 
			
		||||
      return joi.object({
 | 
			
		||||
      return Joi.object({
 | 
			
		||||
        name: this.user.name.required(),
 | 
			
		||||
        email: this.user.email.required(),
 | 
			
		||||
        pass: this.user.pass.required(),
 | 
			
		||||
@@ -45,7 +45,7 @@ export default class UserValidate {  // validate input for user
 | 
			
		||||
      }).validate(data);
 | 
			
		||||
    }
 | 
			
		||||
    else if (param === 'change') {
 | 
			
		||||
      return joi.object({
 | 
			
		||||
      return Joi.object({
 | 
			
		||||
        name: this.user.name,
 | 
			
		||||
        email: this.user.email,
 | 
			
		||||
        pass: this.user.pass,
 | 
			
		||||
@@ -54,7 +54,7 @@ export default class UserValidate {  // validate input for user
 | 
			
		||||
      }).validate(data);
 | 
			
		||||
    }
 | 
			
		||||
    else if (param === 'changeadmin') {
 | 
			
		||||
      return joi.object({
 | 
			
		||||
      return Joi.object({
 | 
			
		||||
        name: this.user.name,
 | 
			
		||||
        email: this.user.email,
 | 
			
		||||
        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
 | 
			
		||||
    data = IdValidate.stringify(data);
 | 
			
		||||
    const {value, error} = joi.object({
 | 
			
		||||
    const {value, error} = Joi.object({
 | 
			
		||||
      _id: IdValidate.get(),
 | 
			
		||||
      name: this.user.name,
 | 
			
		||||
      email: this.user.email,
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
        "_id": {"$oid":"500000000000000000000002"},
 | 
			
		||||
        "comment": "",
 | 
			
		||||
        "sample_references": [{
 | 
			
		||||
          "id": "400000000000000000000004",
 | 
			
		||||
          "id": {"$oid":"400000000000000000000004"},
 | 
			
		||||
          "relation": "granulate to sample"
 | 
			
		||||
        }],
 | 
			
		||||
        "custom_fields": {
 | 
			
		||||
@@ -73,11 +73,12 @@
 | 
			
		||||
        "_id": {"$oid":"500000000000000000000003"},
 | 
			
		||||
        "comment": "",
 | 
			
		||||
        "sample_references": [{
 | 
			
		||||
          "id": "400000000000000000000003",
 | 
			
		||||
          "id": {"$oid":"400000000000000000000003"},
 | 
			
		||||
          "relation": "part to sample"
 | 
			
		||||
        }],
 | 
			
		||||
        "custom_fields": {
 | 
			
		||||
          "not allowed for new applications": true
 | 
			
		||||
          "not allowed for new applications": true,
 | 
			
		||||
          "another_field": "is there"
 | 
			
		||||
        },
 | 
			
		||||
        "__v": 0
 | 
			
		||||
      }
 | 
			
		||||
@@ -88,6 +89,12 @@
 | 
			
		||||
        "name": "not allowed for new applications",
 | 
			
		||||
        "qty": 2,
 | 
			
		||||
        "__v": 0
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "_id": {"$oid":"600000000000000000000002"},
 | 
			
		||||
        "name": "another_field",
 | 
			
		||||
        "qty": 1,
 | 
			
		||||
        "__v": 0
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "materials": [
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ export default class TestHelper {
 | 
			
		||||
    server = require('../index');
 | 
			
		||||
    db.drop(err => {  // reset database
 | 
			
		||||
      if (err) return done(err);
 | 
			
		||||
      db.loadJson(require('../test/db.json'), done);
 | 
			
		||||
      db.loadJson(require('./db.json'), done);
 | 
			
		||||
    });
 | 
			
		||||
    return server
 | 
			
		||||
  }
 | 
			
		||||
							
								
								
									
										12
									
								
								src/test/loadDev.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/test/loadDev.ts
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
		Reference in New Issue
	
	Block a user