Archived
2

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

* commit 'e976d45dedb978d8e5cf0c5d8d09c77a7154f757': (83 commits)
  minor fixes
  implemented added filters
  added workaround for 'added' field compatible to MongoDB 3.6
  implemented x-total-items header
  spectrum field working again
  reworked filters
  added filters
  restructured aggregation
  implementation of measurement fields
  first implementation of fields
  base for csv export
  switched to aggregation, included material sort keys
  sorting for direct sample properties added
  added /samples/count
  changed last-id behaviour to from-id
  implemented paging
  fixed validation to return measurements in /sample/{id}
  added status filter for materials
  added status filter
  cleaned TODOS
  ...
This commit is contained in:
Veit Lukas (PEA4-Fe) 2020-07-14 12:10:30 +02:00
commit ca5b315a01
81 changed files with 15138 additions and 1192 deletions

1
.gitignore vendored
View File

@ -112,3 +112,4 @@ dist
**/.idea/tasks.xml
**/.idea/shelf
**/.idea/*.iml
/tmp/

View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal">
<data-source name="@localhost" uuid="46f112fc-d60d-4217-873f-f5ffea06180c">
<database-info product="Mongo DB" version="4.2.5" jdbc-version="4.2" driver-name="MongoDB JDBC Driver" driver-version="1.7.1" dbms="MONGO" exact-version="4.2.5" exact-driver-version="1.7" />
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<secret-storage>master_key</secret-storage>
<schema-mapping>
<introspection-scope>
<node kind="schema" negative="1" />
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>

11
.idea/dataSources.xml Normal file
View File

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

View File

@ -0,0 +1,584 @@
<?xml version="1.0" encoding="UTF-8"?>
<dataSource name="@localhost">
<database-model serializer="dbm" dbms="MONGO" family-id="MONGO" format-version="4.18">
<root id="1">
<ServerVersion>4.2.5</ServerVersion>
</root>
<schema id="2" parent="1" name="admin"/>
<schema id="3" parent="1" name="config"/>
<schema id="4" parent="1" name="dfopdb"/>
<schema id="5" parent="1" name="dfopdb_test"/>
<schema id="6" parent="1" name="local"/>
<schema id="7" parent="1" name="test">
<Current>1</Current>
</schema>
<table id="8" parent="3" name="system.sessions"/>
<table id="9" parent="4" name="materials"/>
<table id="10" parent="4" name="measurement_templates"/>
<table id="11" parent="4" name="note_fields"/>
<table id="12" parent="4" name="notes"/>
<table id="13" parent="4" name="samples"/>
<table id="14" parent="4" name="treatment_templates"/>
<table id="15" parent="4" name="users"/>
<table id="16" parent="5" name="materials"/>
<table id="17" parent="5" name="measurement_templates"/>
<table id="18" parent="5" name="note_fields"/>
<table id="19" parent="5" name="notes"/>
<table id="20" parent="5" name="samples"/>
<table id="21" parent="5" name="treatment_templates"/>
<table id="22" parent="5" name="users"/>
<table id="23" parent="6" name="startup_log"/>
<table id="24" parent="7" name="a"/>
<table id="25" parent="7" name="b"/>
<column id="26" parent="9" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="27" parent="9" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="28" parent="9" name="carbon_fiber">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="29" parent="9" name="glass_fiber">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="30" parent="9" name="group">
<Position>4</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="31" parent="9" name="mineral">
<Position>5</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="32" parent="9" name="name">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="33" parent="9" name="numbers">
<Position>7</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="34" parent="9" name="numbers._id">
<Position>8</Position>
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="35" parent="9" name="numbers.color">
<Position>9</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="36" parent="9" name="numbers.number">
<Position>10</Position>
<DataType>Double(0)|8s</DataType>
</column>
<column id="37" parent="9" name="supplier">
<Position>11</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="38" parent="12" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="39" parent="12" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="40" parent="12" name="comment">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="41" parent="12" name="sample_references">
<Position>3</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="42" parent="13" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="43" parent="13" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="44" parent="13" name="batch">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="45" parent="13" name="color">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="46" parent="13" name="material_id">
<Position>4</Position>
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="47" parent="13" name="note_id">
<Position>5</Position>
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="48" parent="13" name="number">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="49" parent="13" name="type">
<Position>7</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="50" parent="13" name="user_id">
<Position>8</Position>
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="51" parent="15" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="52" parent="15" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="53" parent="15" name="device_name">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="54" parent="15" name="email">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="55" parent="15" name="key">
<Position>4</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="56" parent="15" name="level">
<Position>5</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="57" parent="15" name="location">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="58" parent="15" name="name">
<Position>7</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="59" parent="15" name="pass">
<Position>8</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="60" parent="16" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="61" parent="16" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="62" parent="16" name="carbon_fiber">
<Position>2</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="63" parent="16" name="glass_fiber">
<Position>3</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="64" parent="16" name="group">
<Position>4</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="65" parent="16" name="mineral">
<Position>5</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="66" parent="16" name="name">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="67" parent="16" name="numbers">
<Position>7</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="68" parent="16" name="numbers.color">
<Position>8</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="69" parent="16" name="numbers.number">
<Position>9</Position>
<DataType>Double(0)|8s</DataType>
</column>
<column id="70" parent="16" name="supplier">
<Position>10</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="71" parent="17" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="72" parent="17" name="name">
<Position>1</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="73" parent="17" name="parameters">
<Position>2</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="74" parent="17" name="parameters.name">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="75" parent="17" name="parameters.range">
<Position>4</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="76" parent="17" name="parameters.range.max">
<Position>5</Position>
<DataType>Double(0)|8s</DataType>
</column>
<column id="77" parent="17" name="parameters.range.min">
<Position>6</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="78" parent="18" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="79" parent="18" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="80" parent="18" name="name">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="81" parent="18" name="qty">
<Position>3</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="82" parent="19" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="83" parent="19" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="84" parent="19" name="comment">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="85" parent="19" name="custom_fields">
<Position>3</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="86" parent="19" name="custom_fields.another_field">
<Position>4</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="87" parent="19" name="custom_fields.not allowed for new applications">
<Position>5</Position>
<DataType>Boolean|12s</DataType>
</column>
<column id="88" parent="19" name="sample_references">
<Position>6</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="89" parent="19" name="sample_references.id">
<Position>7</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="90" parent="19" name="sample_references.relation">
<Position>8</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="91" parent="20" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="92" parent="20" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="93" parent="20" name="batch">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="94" parent="20" name="color">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="95" parent="20" name="material_id">
<Position>4</Position>
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="96" parent="20" name="note_id">
<Position>5</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="97" parent="20" name="number">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="98" parent="20" name="type">
<Position>7</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="99" parent="20" name="user_id">
<Position>8</Position>
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="100" parent="20" name="validated">
<Position>9</Position>
<DataType>Boolean|12s</DataType>
</column>
<column id="101" parent="21" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="102" parent="21" name="name">
<Position>1</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="103" parent="21" name="parameters">
<Position>2</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="104" parent="21" name="parameters.name">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="105" parent="21" name="parameters.range">
<Position>4</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="106" parent="21" name="parameters.range.max">
<Position>5</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="107" parent="21" name="parameters.range.min">
<Position>6</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="108" parent="21" name="parameters.range.values">
<Position>7</Position>
<DataType>array(0)|2003s</DataType>
</column>
<column id="109" parent="22" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="110" parent="22" name="__v">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="111" parent="22" name="device_name">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="112" parent="22" name="email">
<Position>3</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="113" parent="22" name="key">
<Position>4</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="114" parent="22" name="level">
<Position>5</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="115" parent="22" name="location">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="116" parent="22" name="name">
<Position>7</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="117" parent="22" name="pass">
<Position>8</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="118" parent="23" name="_id">
<DataType>String(0)|12s</DataType>
</column>
<column id="119" parent="23" name="buildinfo">
<Position>1</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="120" parent="23" name="buildinfo.allocator">
<Position>2</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="121" parent="23" name="buildinfo.bits">
<Position>3</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="122" parent="23" name="buildinfo.buildEnvironment">
<Position>4</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="123" parent="23" name="buildinfo.buildEnvironment.cc">
<Position>5</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="124" parent="23" name="buildinfo.buildEnvironment.ccflags">
<Position>6</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="125" parent="23" name="buildinfo.buildEnvironment.cxx">
<Position>7</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="126" parent="23" name="buildinfo.buildEnvironment.cxxflags">
<Position>8</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="127" parent="23" name="buildinfo.buildEnvironment.distarch">
<Position>9</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="128" parent="23" name="buildinfo.buildEnvironment.distmod">
<Position>10</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="129" parent="23" name="buildinfo.buildEnvironment.linkflags">
<Position>11</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="130" parent="23" name="buildinfo.buildEnvironment.target_arch">
<Position>12</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="131" parent="23" name="buildinfo.buildEnvironment.target_os">
<Position>13</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="132" parent="23" name="buildinfo.debug">
<Position>14</Position>
<DataType>Boolean|12s</DataType>
</column>
<column id="133" parent="23" name="buildinfo.gitVersion">
<Position>15</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="134" parent="23" name="buildinfo.javascriptEngine">
<Position>16</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="135" parent="23" name="buildinfo.maxBsonObjectSize">
<Position>17</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="136" parent="23" name="buildinfo.modules">
<Position>18</Position>
<DataType>list(0)|4999545s</DataType>
</column>
<column id="137" parent="23" name="buildinfo.openssl">
<Position>19</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="138" parent="23" name="buildinfo.openssl.running">
<Position>20</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="139" parent="23" name="buildinfo.storageEngines">
<Position>21</Position>
<DataType>array(0)|2003s</DataType>
</column>
<column id="140" parent="23" name="buildinfo.sysInfo">
<Position>22</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="141" parent="23" name="buildinfo.targetMinOS">
<Position>23</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="142" parent="23" name="buildinfo.version">
<Position>24</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="143" parent="23" name="buildinfo.versionArray">
<Position>25</Position>
<DataType>array(0)|2003s</DataType>
</column>
<column id="144" parent="23" name="cmdLine">
<Position>26</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="145" parent="23" name="cmdLine.config">
<Position>27</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="146" parent="23" name="cmdLine.net">
<Position>28</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="147" parent="23" name="cmdLine.net.bindIp">
<Position>29</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="148" parent="23" name="cmdLine.net.port">
<Position>30</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="149" parent="23" name="cmdLine.service">
<Position>31</Position>
<DataType>Boolean|12s</DataType>
</column>
<column id="150" parent="23" name="cmdLine.storage">
<Position>32</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="151" parent="23" name="cmdLine.storage.dbPath">
<Position>33</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="152" parent="23" name="cmdLine.storage.journal">
<Position>34</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="153" parent="23" name="cmdLine.storage.journal.enabled">
<Position>35</Position>
<DataType>Boolean|12s</DataType>
</column>
<column id="154" parent="23" name="cmdLine.systemLog">
<Position>36</Position>
<DataType>map(0)|4999544s</DataType>
</column>
<column id="155" parent="23" name="cmdLine.systemLog.destination">
<Position>37</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="156" parent="23" name="cmdLine.systemLog.logAppend">
<Position>38</Position>
<DataType>Boolean|12s</DataType>
</column>
<column id="157" parent="23" name="cmdLine.systemLog.path">
<Position>39</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="158" parent="23" name="hostname">
<Position>40</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="159" parent="23" name="pid">
<Position>41</Position>
<DataType>Long(0)|12s</DataType>
</column>
<column id="160" parent="23" name="startTime">
<Position>42</Position>
<DataType>Date(0)|91s</DataType>
</column>
<column id="161" parent="23" name="startTimeLocal">
<Position>43</Position>
<DataType>String(0)|12s</DataType>
</column>
<column id="162" parent="24" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="163" parent="24" name="x">
<Position>1</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="164" parent="24" name="y">
<Position>2</Position>
<DataType>Integer|4s</DataType>
</column>
<column id="165" parent="25" name="_id">
<DataType>ObjectId(0)|12s</DataType>
</column>
<column id="166" parent="25" name="s">
<Position>1</Position>
<DataType>String(0)|12s</DataType>
</column>
</database-model>
</dataSource>

458
.idea/dbnavigator.xml Normal file
View File

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

View File

@ -0,0 +1,12 @@
<component name="ProjectDictionaryState">
<dictionary name="VLE2FE">
<words>
<w>bcrypt</w>
<w>cfenv</w>
<w>dfopdb</w>
<w>janedoe</w>
<w>pagesize</w>
<w>testcomment</w>
</words>
</dictionary>
</component>

View File

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

View File

@ -6,7 +6,10 @@ info:
version: 1.0.0
description: |
This API gives access to the project database.<br>
Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password. Data access methods can also be accessed using an API key at the URL ending like ?key=xxx<br>
Access is restricted. Authentication can be obtained with HTTP Basic Auth using username and password.
Data access methods can also be accessed using an API key at the URL ending like ?key=xxx<br>
The description lists available authentication methods, also the locks of each method close correspondingly
if the entered authentication is allowed.<br><br>
There are a number of different user levels: <br>
<ul>
<li>read: read access to the samples database</li>
@ -15,14 +18,31 @@ info:
<li>dev: handling machine learning models</li>
<li>admin: user administration</li>
</ul>
Password policy:
<ul>
<li>at least one digit</li>
<li>at least one lower case letter</li>
<li>at least one upper case letter</li>
<li>at least one of the following special characters: !"#%&'()*+,-./:;<=>?@[\]^_`{|}~</li>
<li>no whitespace</li>
<li>at least 8 characters</li>
</ul>
x-doc: |
status:
<ul>
<li>-10: deleted</li>
<li>0: newly added/changed</li>
<li>10: validated</li>
</ul>
<a href="https://sourcecode.socialcoding.bosch.com/users/vle2fe/repos/dfop-api/">Bitbucket repository</a>
# TODO: Link to new documentation page
servers:
- url: https://definma-api.apps.de1.bosch-iot-cloud.com
description: server on the BIC
- url: http://localhost:3000
description: local server
- url: https://digital-fingerprint-of-plastics-api.apps.de1.bosch-iot-cloud.com/
description: server on the BIC
security:
@ -34,19 +54,17 @@ tags:
- name: /
- name: /sample
- name: /material
- name: /condition
- name: /measurement
- name: /templates
- name: /template
- name: /model
- name: /user
paths:
allOf:
- $ref: 'others.yaml'
- $ref: 'root.yaml'
- $ref: 'sample.yaml'
- $ref: 'material.yaml'
- $ref: 'condition.yaml'
- $ref: 'measurement.yaml'
- $ref: 'template.yaml'
- $ref: 'model.yaml'

248
api/material.yaml Normal file
View File

@ -0,0 +1,248 @@
/materials:
get:
summary: lists all materials
description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: returns only materials with status 10
tags:
- /material
parameters:
- name: status
description: 'values: validated|new|all, defaults to validated'
in: query
schema:
type: string
example: all
responses:
200:
description: all material details
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/Material'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/materials/{state}:
parameters:
- $ref: 'api.yaml#/components/parameters/State'
get:
summary: lists all new/deleted materials
description: 'Auth: basic, levels: maintain, admin'
x-doc: returns materials with status 0/-1
tags:
- /material
responses:
200:
description: all material details
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/Material'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/material/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
get:
summary: get material details
description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: deleted samples are available only for maintain/admin
tags:
- /material
responses:
200:
description: material details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Material'
401:
$ref: 'api.yaml#/components/responses/401'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change material
description: 'Auth: basic, levels: write, maintain, dev, admin'
x-doc: status is reset to 0 on any changes, deleted samples cannot be changed
tags:
- /material
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Material'
responses:
200:
description: material details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Material'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
summary: delete material
description: 'Auth: basic, levels: write, maintain, dev, admin'
x-doc: sets status to -1
tags:
- /material
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/material/restore/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
put:
summary: restore material
description: 'Auth: basic, levels: maintain, admin'
x-doc: status is set to 0
tags:
- /material
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/material/validate/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
put:
summary: restore material
description: 'Auth: basic, levels: maintain, admin'
x-doc: status is set to 10
tags:
- /material
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/material/new:
post:
summary: add material
description: 'Auth: basic, levels: write, maintain, dev, admin'
x-doc: 'Adds status: 0 automatically'
tags:
- /material
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Material'
responses:
200:
description: material details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Material'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
/material/groups:
get:
summary: list all existing material groups
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /material
responses:
200:
description: all material groups
content:
application/json:
schema:
type: array
items:
type: string
example: PA66
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
/material/suppliers:
get:
summary: list all existing material suppliers
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /material
responses:
200:
description: all material suppliers
content:
application/json:
schema:
type: array
items:
type: string
example: BASF
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'

155
api/measurement.yaml Normal file
View File

@ -0,0 +1,155 @@
/measurement/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
get:
summary: measurement values by id
description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: deleted samples are available only for maintain/admin
tags:
- /measurement
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Measurement'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change measurement
description: 'Auth: basic, levels: write, maintain, dev, admin'
x-doc: status is reset to 0 on any changes, deleted measurements cannot be edited
tags:
- /measurement
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
properties:
values:
type: object
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Measurement'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
summary: delete measurement
description: 'Auth: basic, levels: write, maintain, dev, admin'
x-doc: sets status to -1
tags:
- /measurement
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/measurement/restore/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
put:
summary: restore measurement
description: 'Auth: basic, levels: maintain, admin'
x-doc: status is set to 0
tags:
- /measurement
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/measurement/validate/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
put:
summary: set measurement status to validated
description: 'Auth: basic, levels: maintain, admin'
x-doc: status is set to 10
tags:
- /measurement
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/measurement/new:
post:
summary: add measurement
description: 'Auth: basic, levels: write, maintain, dev, admin'
x-doc: 'Adds status: 0 automatically'
tags:
- /measurement
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Measurement'
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Measurement'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'

70
api/model.yaml Normal file
View File

@ -0,0 +1,70 @@
/model/{name}:
parameters:
- $ref: 'api.yaml#/components/parameters/Name'
get:
summary: TODO get model data by name
description: 'Auth: all, levels: dev, admin'
tags:
- /model
responses:
200:
description: binary model data
content:
application/octet-stream:
schema:
type: string
format: binary
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: TODO add/replace model data by name
description: 'Auth: all, levels: dev, admin'
tags:
- /model
requestBody:
required: true
description: binary model data
content:
application/json:
schema:
type: string
format: binary
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
summary: TODO delete model data
description: 'Auth: basic, levels: dev, admin'
tags:
- /model
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'

24
api/parameters.yaml Normal file
View File

@ -0,0 +1,24 @@
Id:
name: id
in: path
required: true
schema:
type: string
example: 5ea0450ed851c30a90e70894
Name:
name: name
description: has to be URL encoded
in: path
required: true
schema:
type: string
State:
name: group
description: 'possible values: new, deleted'
in: path
required: true
schema:
type: string
example: deleted

102
api/root.yaml Normal file
View File

@ -0,0 +1,102 @@
/:
get:
summary: Root method
description: 'Auth: none'
tags:
- /
security: []
responses:
200:
description: Server is working
content:
application/json:
schema:
properties:
status:
type: string
example: 'API server up and running!'
500:
$ref: 'api.yaml#/components/responses/500'
/authorized:
get:
summary: Checks authorization
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /
responses:
200:
description: Authorized
content:
application/json:
schema:
properties:
status:
type: string
example: 'Authorization successful'
method:
type: string
example: 'basic'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/changelog/{timestamp}/{page}/{pagesize}:
parameters:
- name: timestamp
in: path
required: true
schema:
type: string
example: 1970-01-01T00:00:00.000Z
- name: page
in: path
required: true
schema:
type: number
example: 3
- name: pagesize
in: path
required: true
schema:
type: number
example: 30
get:
summary: get changelog
description: 'Auth: basic, levels: maintain, admin<br>Displays all logs older than timestamp, sorted by date descending, page defaults to 0, pagesize defaults to 25<br>Avoid using high page numbers for older logs, better use an older timestamp'
tags:
- /
responses:
200:
description: Changelog
content:
application/json:
schema:
properties:
date:
type: string
example: 1970-01-01T00:00:00.000Z
action:
type: string
example: PUT /sample/400000000000000000000001
collection:
type: string
example: samples
conditions:
type: object
example:
_id: '400000000000000000000001'
data:
type: object
example:
type: part
status: 0
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'

312
api/sample.yaml Normal file
View File

@ -0,0 +1,312 @@
/samples:
get:
summary: all samples in overview
description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: 'Limitations: paging and csv output does not work when including the spectrum measurement fields as well as the returned number of total samples'
tags:
- /sample
parameters:
- name: status
description: 'values: validated|new|all, defaults to validated'
in: query
schema:
type: string
example: all
- name: from-id
description: first id of the requested page, if not given the results are displayed from start
in: query
schema:
type: string
example: 5ea0450ed851c30a90e70894
- name: to-page
description: relative change of pages, use negative values to get back, defaults to 0, works only together with page-size
in: query
schema:
type: string
example: 1
- name: page-size
description: number of items per page
in: query
schema:
type: string
example: 30
- name: sort
description: sorting of results, in format 'key-asc/desc'
in: query
schema:
type: string
example: color-asc
- name: csv
description: output as csv
in: query
schema:
type: boolean
example: false
- name: fields[]
description: the fields to include in the output as array, defaults to ['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']
in: query
schema:
type: array
items:
type: string
example: ['number', 'batch']
- name: filters[]
description: "the filters to apply as an array of URIComponent encoded objects in the form {mode: 'eq/ne/lt/lte/gt/gte/in/nin', field: 'material.m', values: ['15']} using encodeURIComponent(JSON.stringify({}))"
in: query
schema:
type: array
items:
type: string
example: ["%7B%22mode%22%3A%22eq%22%2C%22field%22%3A%22material.m%22%2C%22values%22%3A%5B%2215%22%5D%7D", "%7B%22mode%22%3A%22isin%22%2C%22field%22%3A%22material.supplier%22%2C%22values%22%3A%5B%22BASF%22%2C%22DSM%22%5D%7D"]
responses:
200:
description: samples overview (if the csv parameter is set, this is in CSV instead of JSON format)
headers:
x-total-items:
description: Total number of available items when from-id is not specified and spectrum field is not included
schema:
type: integer
example: 243
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/SampleRefs'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/samples/{state}:
parameters:
- $ref: 'api.yaml#/components/parameters/State'
get:
summary: all new/deleted samples in overview
description: 'Auth: basic, levels: maintain, admin'
x-doc: returns only samples with status 0/-1
tags:
- /sample
responses:
200:
description: samples overview
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/SampleRefs'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/samples/count:
get:
summary: total number of samples
description: 'Auth: all, levels: read, write, maintain, dev, admin'
tags:
- /sample
responses:
200:
description: sample count
content:
application/json:
schema:
properties:
count:
type: number
example: 864
500:
$ref: 'api.yaml#/components/responses/500'
/sample/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
get:
summary: sample details
description: 'Auth: all, levels: read, write, maintain, dev, admin<br>Returns validated as well as new measurements'
x-doc: deleted samples are available only for maintain/admin
tags:
- /sample
responses:
200:
description: samples details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/SampleDetail'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change sample
description: 'Auth: basic, levels: write, maintain, dev, admin <br>Only maintain and admin are allowed to edit samples created by another user'
x-doc: status is reset to 0 on any changes, deleted samples cannot be changed
tags:
- /sample
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Sample'
responses:
200:
description: samples details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/SampleRefs'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
summary: delete sample
description: 'Auth: basic, levels: write, maintain, dev, admin <br>Only maintain and admin are allowed to edit samples created by another user'
x-doc: sets status to -1, notes and references to this sample are also kept, only note_fields are updated accordingly
tags:
- /sample
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/restore/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
put:
summary: restore sample
description: 'Auth: basic, levels: maintain, admin'
x-doc: status is set to 0
tags:
- /sample
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/validate/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
put:
summary: set sample status to validated
description: 'Auth: basic, levels: maintain, admin'
x-doc: status is set to 10
tags:
- /sample
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/new:
post:
summary: add sample
description: 'Auth: basic, levels: write, maintain, dev, admin. Number property is only for admin when adding existing samples'
x-doc: 'Adds status: 0 automatically'
tags:
- /sample
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: 'api.yaml#/components/schemas/Sample'
properties:
number:
type: string
readOnly: false
responses:
200:
description: samples details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/SampleRefs'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
/sample/notes/fields:
get:
summary: list all existing field names for custom notes fields
description: 'Auth: all, levels: read, write, maintain, dev, admin'
x-doc: integrity has to be ensured
tags:
- /sample
responses:
200:
description: field names and quantity of usage
content:
application/json:
schema:
type: array
items:
properties:
name:
type: string
qty:
type: number
example: 20
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'

202
api/schemas.yaml Normal file
View File

@ -0,0 +1,202 @@
Id:
type: string
example: 5ea0450ed851c30a90e70894
_Id:
properties:
_id:
allOf:
- $ref: 'api.yaml#/components/schemas/Id'
readOnly: true
Color:
properties:
color:
type: string
example: black
SampleProperties:
properties:
number:
type: string
readOnly: true
example: Rng172
type:
type: string
example: granulate
batch:
type: string
example: 1560237365
condition:
type: object
properties:
condition_template:
$ref: 'api.yaml#/components/schemas/Id'
example:
condition_template: 5ea0450ed851c30a90e70894
material: hot air
weeks: 5
SampleRefs:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
- $ref: 'api.yaml#/components/schemas/Color'
- $ref: 'api.yaml#/components/schemas/SampleProperties'
properties:
material_id:
$ref: 'api.yaml#/components/schemas/Id'
note_id:
$ref: 'api.yaml#/components/schemas/Id'
user_id:
$ref: 'api.yaml#/components/schemas/Id'
added:
type: string
example: 1970-01-01T00:00:00.000Z
Sample:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
- $ref: 'api.yaml#/components/schemas/Color'
- $ref: 'api.yaml#/components/schemas/SampleProperties'
properties:
material_id:
allOf:
- $ref: 'api.yaml#/components/schemas/Id'
notes:
type: object
properties:
comment:
type: string
sample_references:
type: array
items:
properties:
sample_id:
$ref: 'api.yaml#/components/schemas/Id'
relation:
type: string
example: part to this sample
custom_fields:
type: object
SampleDetail:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
- $ref: 'api.yaml#/components/schemas/Color'
- $ref: 'api.yaml#/components/schemas/SampleProperties'
properties:
material:
allOf:
- $ref: 'api.yaml#/components/schemas/Material'
notes:
type: object
properties:
comment:
type: string
sample_references:
type: array
items:
$ref: 'api.yaml#/components/schemas/Id'
measurements:
type: array
items:
allOf:
- $ref: 'api.yaml#/components/schemas/Measurement'
user:
type: string
example: admin
Material:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
properties:
name:
type: string
example: Stanyl TW 200 F8
supplier:
type: string
example: DSM
group:
type: string
example: PA46
mineral:
type: number
example: 0
glass_fiber:
type: number
example: 40
carbon_fiber:
type: number
example: 0
numbers:
type: array
items:
type: object
allOf:
- $ref: 'api.yaml#/components/schemas/Color'
properties:
number:
type: string
example: 5514263423
Measurement:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
properties:
sample_id:
$ref: 'api.yaml#/components/schemas/Id'
values:
type: object
measurement_template:
$ref: 'api.yaml#/components/schemas/Id'
Template:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
properties:
name:
type: string
example: humidity
version:
type: number
readOnly: true
example: 1
parameters:
type: array
items:
type: object
properties:
name:
type: string
example: kf
range:
type: object
example:
min: 0
max: 2
Email:
properties:
email:
type: string
example: john.doe@bosch.com
UserName:
properties:
name:
type: string
example: johndoe
User:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
- $ref: 'api.yaml#/components/schemas/UserName'
- $ref: 'api.yaml#/components/schemas/Email'
properties:
pass:
type: string
writeOnly: true
example: Abc123!#
level:
type: string
example: read
location:
type: string
example: Rng
device_name:
type: string
example: Alpha II

213
api/template.yaml Normal file
View File

@ -0,0 +1,213 @@
/template/conditions:
get:
summary: all available condition methods
description: 'Auth: basic, levels: read, write, maintain, dev, admin'
tags:
- /template
security:
- BasicAuth: []
responses:
200:
description: list of conditions
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/Template'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/template/condition/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
get:
summary: condition method details
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /template
security:
- BasicAuth: []
responses:
200:
description: condition details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
401:
$ref: 'api.yaml#/components/responses/401'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change condition method
description: 'Auth: basic, levels: maintain, admin'
x-doc: With a change a new version is set, resulting in a new template with a new id
tags:
- /template
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
responses:
200:
description: condition details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/template/condition/new:
post:
summary: add condition method
description: 'Auth: basic, levels: maintain, admin'
tags:
- /template
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
responses:
200:
description: condition details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
/template/measurements:
get:
summary: all available measurement methods
description: 'Auth: basic, levels: read, write, maintain, dev, admin'
tags:
- /template
security:
- BasicAuth: []
responses:
200:
description: list of measurement methods
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/Template'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/template/measurement/{id}:
parameters:
- $ref: 'api.yaml#/components/parameters/Id'
get:
summary: measurement method details
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /template
security:
- BasicAuth: []
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change measurement method
description: 'Auth: basic, levels: maintain, admin'
tags:
- /template
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/template/measurement/new:
post:
summary: add measurement method
description: 'Auth: basic, levels: maintain, admin'
tags:
- /template
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/Template'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'

255
api/user.yaml Normal file
View File

@ -0,0 +1,255 @@
/users:
get:
summary: lists all users
description: 'Auth: basic, levels: admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user API key
content:
application/json:
schema:
type: array
items:
$ref: 'api.yaml#/components/schemas/User'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
/user:
get:
summary: list own user details
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/User'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change user details
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /user
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: 'api.yaml#/components/schemas/_Id'
- $ref: 'api.yaml#/components/schemas/UserName'
- $ref: 'api.yaml#/components/schemas/Email'
properties:
pass:
type: string
writeOnly: true
example: Abc123!#
location:
type: string
example: Rng
device_name:
type: string
example: Alpha II
responses:
200:
description: user details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/User'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
summary: delete user
description: 'Auth: basic, levels: read, write, maintain, admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/user/{name}:
parameters:
- $ref: 'api.yaml#/components/parameters/Name'
get:
summary: list user details
description: 'Auth: basic, levels: admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/User'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
put:
summary: change user details
description: 'Auth: basic, levels: admin'
tags:
- /user
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/User'
responses:
200:
description: user details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/User'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
delete:
summary: delete user
description: 'Auth: basic, levels: admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'
/user/key:
get:
summary: get API key for the user
description: 'Auth: basic, levels: read, write, maintain, dev, admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user details
content:
application/json:
schema:
properties:
key:
type: string
example: 5ea0450ed851c30a90e70899
401:
$ref: 'api.yaml#/components/responses/401'
500:
$ref: 'api.yaml#/components/responses/500'
/user/new:
post:
summary: add new user
description: 'Auth: basic, levels: admin'
tags:
- /user
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
required:
- email
- name
- pass
- level
- location
- device_name
allOf:
- $ref: 'api.yaml#/components/schemas/User'
responses:
200:
description: user details
content:
application/json:
schema:
$ref: 'api.yaml#/components/schemas/User'
400:
$ref: 'api.yaml#/components/responses/400'
401:
$ref: 'api.yaml#/components/responses/401'
403:
$ref: 'api.yaml#/components/responses/403'
500:
$ref: 'api.yaml#/components/responses/500'
/user/passreset:
post:
summary: reset password and send mail to restore
description: 'Auth: none'
tags:
- /user
security: []
requestBody:
required: true
description: mail saved in user profile to provide authentication
content:
application/json:
schema:
allOf:
- $ref: 'api.yaml#/components/schemas/UserName'
- $ref: 'api.yaml#/components/schemas/Email'
responses:
200:
$ref: 'api.yaml#/components/responses/Ok'
404:
$ref: 'api.yaml#/components/responses/404'
500:
$ref: 'api.yaml#/components/responses/500'

4
build.bat Normal file
View File

@ -0,0 +1,4 @@
call npm run tsc-full
copy package.json dist\package.json
Xcopy /E /I api dist\api
Xcopy /E /I static dist\static

579
data_import/import.js Normal file
View File

@ -0,0 +1,579 @@
const csv = require('csv-parser');
const fs = require('fs');
const axios = require('axios');
const {Builder} = require('selenium-webdriver'); // selenium and the chrome driver must be installed and configured separately
const chrome = require('selenium-webdriver/chrome');
const pdfReader = require('pdfreader');
const iconv = require('iconv-lite');
const metaDoc = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\metadata.csv'; // metadata files
const kfDoc = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\kf.csv';
const vzDoc = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\vz.csv';
const nmDocs = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\nmDocs'; // NormMaster Documents
const dptFiles = 'C:\\Users\\vle2fe\\Documents\\Data\\Rng_200707\\DPT'; // Spectrum files
const host = 'http://localhost:3000';
// const host = 'https://definma-api.apps.de1.bosch-iot-cloud.com';
let data = []; // metadata contents
let materials = {};
let samples = [];
let normMaster = {};
let sampleDevices = {};
// TODO: BASF twice, BASF as color
// TODO: duplicate kf values
// TODO: conditions
// TODO: comment and reference handling
// TODO: check last color errors (filter out already taken) use location and device for user, upload to BIC
main();
async function main() {
if (0) { // materials
await getNormMaster();
await importCsv(metaDoc);
await allMaterials();
await saveMaterials();
await importCsv(kfDoc);
await allMaterials();
await saveMaterials();
await importCsv(vzDoc);
await allMaterials();
await saveMaterials();
}
if (0) { // samples
sampleDeviceMap();
if (1) {
console.log('-------- META ----------');
await importCsv(metaDoc);
await allSamples();
await saveSamples();
}
if (1) {
console.log('-------- KF ----------');
await importCsv(kfDoc);
await allSamples();
await saveSamples();
await allKfVz();
}
if (1) {
console.log('-------- VZ ----------');
await importCsv(vzDoc);
await allSamples();
await saveSamples();
await allKfVz();
}
}
if (1) { // DPT
await allDpts();
}
if (0) { // pdf test
console.log(await readPdf('N28_BN05-OX013_2016-03-11.pdf'));
}
}
async function importCsv(doc) {
data = [];
await new Promise(resolve => {
fs.createReadStream(doc)
.pipe(iconv.decodeStream('win1252'))
.pipe(csv())
.on('data', (row) => {
data.push(row);
})
.on('end', () => {
console.info('CSV file successfully processed');
if (data[0]['Farbe']) { // fix German column names
data.map(e => {e['Color'] = e['Farbe']; return e; });
}
resolve();
});
});
}
async function allDpts() {
let res = await axios({
method: 'get',
url: host + '/template/measurements',
auth: {
username: 'admin',
password: 'Abc123!#'
}
});
const measurement_template = res.data.find(e => e.name === 'spectrum')._id;
res = await axios({
method: 'get',
url: host + '/samples?status=all',
auth: {
username: 'admin',
password: 'Abc123!#'
}
});
const sampleIds = {};
res.data.forEach(sample => {
sampleIds[sample.number] = sample._id;
});
const dptRegex = /.*?_(.*?)_(\d+|\d+_\d+).DPT/;
const dpts = fs.readdirSync(dptFiles);
for (let i in dpts) {
const regexRes = dptRegex.exec(dpts[i])
if (regexRes && sampleIds[regexRes[1]]) { // found matching sample
console.log(dpts[i]);
const f = fs.readFileSync(dptFiles + '\\' + dpts[i], 'utf-8');
const data = {
sample_id: sampleIds[regexRes[1]],
values: {},
measurement_template
};
data.values.dpt = f.split('\r\n').map(e => e.split(','));
let rescale = false;
for (let i in data.values.dpt) {
if (data.values.dpt[i][1] > 2) {
rescale = true;
break;
}
}
if (rescale) {
data.values.dpt = data.values.dpt.map(e => [e[0], e[1] / 100]);
}
await axios({
method: 'post',
url: host + '/measurement/new',
auth: {
username: 'admin',
password: 'Abc123!#'
},
data
}).catch(err => {
console.log(dpts[i]);
console.error(err.response.data);
});
}
else {
console.log(`Could not find sample for ${dpts[i]} !!!!!!`);
}
}
}
async function allKfVz() {
let res = await axios({
method: 'get',
url: host + '/template/measurements',
auth: {
username: 'admin',
password: 'Abc123!#'
}
});
const kf_template = res.data.find(e => e.name === 'kf')._id;
const vz_template = res.data.find(e => e.name === 'vz')._id;
res = await axios({
method: 'get',
url: host + '/samples?status=all',
auth: {
username: 'admin',
password: 'Abc123!#'
}
});
const sampleIds = {};
res.data.forEach(sample => {
sampleIds[sample.number] = sample._id;
});
for (let index in data) {
console.info(`${index}/${data.length}`);
let sample = data[index];
if (sample['Sample number'] !== '') {
let credentials = ['admin', 'Abc123!#'];
if (sampleDevices[sample['Sample number']]) {
credentials = [sampleDevices[sample['Sample number']], '2020DeFinMachen!']
}
if (sample['KF in Gew%']) {
await axios({
method: 'post',
url: host + '/measurement/new',
auth: {
username: credentials[0],
password: credentials[1]
},
data: {
sample_id: sampleIds[sample['Sample number']],
measurement_template: kf_template,
values: {
'weight %': sample['KF in Gew%'],
'standard deviation': sample['Stabwn']
}
}
}).catch(err => {
console.log(sample['Sample number']);
console.error(err.response.data);
});
}
if (sample['VZ (ml/g)']) {
await axios({
method: 'post',
url: host + '/measurement/new',
auth: {
username: credentials[0],
password: credentials[1]
},
data: {
sample_id: sampleIds[sample['Sample number']],
measurement_template: vz_template,
values: {
vz: sample['VZ (ml/g)']
}
}
}).catch(err => {
console.log(sample['Sample number']);
console.error(err.response.data);
});
}
}
}
}
async function allSamples() {
samples = [];
let res = await axios({
method: 'get',
url: host + '/materials?status=all',
auth: {
username: 'admin',
password: 'Abc123!#'
}
});
const dbMaterials = {}
res.data.forEach(m => {
dbMaterials[m.name] = m;
})
res = await axios({
method: 'get',
url: host + '/samples?status=all',
auth: {
username: 'admin',
password: 'Abc123!#'
}
});
const sampleColors = {};
res.data.forEach(sample => {
sampleColors[sample.number] = sample.color;
});
for (let index in data) {
console.info(`${index}/${data.length}`);
let sample = data[index];
if (sample['Sample number'] !== '') { // TODO: what about samples without color
if (sample['Supplier'] === '') { // empty supplier fields
sample['Supplier'] = 'unknown';
}
if (sample['Granulate/Part'] === '') { // empty supplier fields
sample['Granulate/Part'] = 'unknown';
}
const material = dbMaterials[trim(sample['Material name'])];
if (!material) { // could not find material, skipping sample
continue;
}
console.log(sample['Material name']);
console.log(material._id);
samples.push({
number: sample['Sample number'],
type: sample['Granulate/Part'],
batch: sample['Charge/batch granulate/part'] || '',
material_id: material._id,
notes: {
comment: sample['Comments']
}
});
const si = samples.length - 1;
if (sample['Material number'] !== '' && material.numbers.find(e => e.number === sample['Material number'])) { // TODO: fix because of false material/material number
samples[si].color = material.numbers.find(e => e.number === sample['Material number']).color;
}
else if (sample['Color'] && sample['Color'] !== '') {
let number = material.numbers.find(e => e.color.indexOf(trim(sample['Color'])) >= 0);
if (!number && /black/.test(sample['Color'])) { // special case bk for black
number = material.numbers.find(e => e.color.toLowerCase().indexOf('bk') >= 0);
if (!number) { // try German word
number = material.numbers.find(e => e.color.toLowerCase().indexOf('schwarz') >= 0);
}
}
samples[si].color = number.color;
}
else if (sampleColors[sample['Sample number'].split('_')[0]]) { // derive color from main sample for kf/vz
samples[si].color = sampleColors[sample['Sample number'].split('_')[0]];
}
else {
samples[si].color = '';
}
}
}
}
async function saveSamples() {
for (let i in samples) {
console.info(`${i}/${samples.length}`);
let credentials = ['admin', 'Abc123!#'];
if (sampleDevices[samples[i].number]) {
credentials = [sampleDevices[samples[i].number], '2020DeFinMachen!']
}
await axios({
method: 'post',
url: host + '/sample/new',
auth: {
username: credentials[0],
password: credentials[1]
},
data: samples[i]
}).catch(err => {
if (err.response.data.status && err.response.data.status !== 'Sample number already taken') {
console.log(samples[i]);
console.error(err.response.data);
}
});
}
console.info('saved all samples');
}
async function allMaterials() {
materials = {};
for (let index in data) {
let sample = data[index];
if (sample['Sample number'] && sample['Sample number'] !== '') {
if (sample['Supplier'] === '') { // empty supplier fields
sample['Supplier'] = 'unknown';
}
if (sample['Material name'] === '') { // empty name fields
sample['Material name'] = sample['Material'];
}
if (!sample['Material']) { // column Material is named Plastic in VZ metadata
sample['Material'] = sample['Plastic'];
}
sample['Material name'] = trim(sample['Material name']);
if (materials.hasOwnProperty(sample['Material name'])) { // material already found at least once
if (sample['Material number'] && sample['Material number'] !== '') {
if (materials[sample['Material name']].numbers.length === 0 || !materials[sample['Material name']].numbers.find(e => e.number === stripSpaces(sample['Material number']))) { // new material number
if (materials[sample['Material name']].numbers.find(e => e.color === sample['Color'] && e.number === '')) { // color already in list, only number missing
materials[sample['Material name']].numbers.find(e => e.color === sample['Color'] && e.number === '').number = stripSpaces(sample['Material number']);
}
else {
materials[sample['Material name']].numbers.push({color: trim(sample['Color']), number: stripSpaces(sample['Material number'])});
}
}
}
else if (sample['Color'] && sample['Color'] !== '') {
if (!materials[sample['Material name']].numbers.find(e => e.color === stripSpaces(sample['Color']))) { // new material color
materials[sample['Material name']].numbers.push({color: trim(sample['Color']), number: ''});
}
}
}
else { // new material
console.info(`${index}/${data.length} ${sample['Material name']}`);
materials[sample['Material name']] = {
name: sample['Material name'],
supplier: trim(sample['Supplier']),
group: trim(sample['Material'])
};
let tmp = /M(\d+)/.exec(sample['Reinforcing material']);
materials[sample['Material name']].mineral = tmp ? tmp[1] : 0;
tmp = /GF(\d+)/.exec(sample['Reinforcing material']);
materials[sample['Material name']].glass_fiber = tmp ? tmp[1] : 0;
tmp = /CF(\d+)/.exec(sample['Reinforcing material']);
materials[sample['Material name']].carbon_fiber = tmp ? tmp[1] : 0;
materials[sample['Material name']].numbers = await numbersFetch(sample);
console.log(materials[sample['Material name']]);
}
}
}
}
async function saveMaterials() {
const mKeys = Object.keys(materials)
for (let i in mKeys) {
console.info(`${i}/${mKeys.length}`);
await axios({
method: 'post',
url: host + '/material/new',
auth: {
username: 'admin',
password: 'Abc123!#'
},
data: materials[mKeys[i]]
}).catch(err => {
if (err.response.data.status && err.response.data.status !== 'Material name already taken') {
console.info(materials[mKeys[i]]);
console.error(err.response.data);
}
});
}
console.info('saved all materials');
}
async function numbersFetch(sample) {
let nm = [];
let res = [];
if (sample['Material number']) { // sample has a material number
nm = normMaster[stripSpaces(sample['Material number'])]? [normMaster[stripSpaces(sample['Material number'])]] : [];
}
else { // try finding via material name
nm = Object.keys(normMaster).filter(e => normMaster[e].nameSpaceless === stripSpaces(sample['Material name'])).map(e => normMaster[e]);
}
if (nm.length > 0) {
for (let i in nm) {
// if (!fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document not loaded
// await getNormMasterDoc(nm[i].url.replace(/ /g, '%20'));
// }
// if (!fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document not loaded
// console.info('Retrying download...');
// await getNormMasterDoc(nm[i].url.replace(/ /g, '%20'), 2.2);
// }
// if (!fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document not loaded
// console.info('Retrying download again...');
// await getNormMasterDoc(nm[i].url.replace(/ /g, '%20'), 5);
// }
if (fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0)) { // document loaded
res = await readPdf(fs.readdirSync(nmDocs).find(e => e.indexOf(nm[i].doc.replace(/ /g, '_')) >= 0));
}
if (res.length > 0) { // no results
break;
}
else if (i + 1 >= nm.length) {
console.error('Download failed!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!');
}
}
}
if (res.length === 0) { // no results
if ((sample['Color'] && sample['Color'] !== '') || (sample['Material number'] &&sample['Material number'] !== '')) {
return [{color: trim(sample['Color']), number: sample['Material number']}];
}
else {
return [];
}
}
else {
if (sample['Material number'] && !res.find(e => e.number === sample['Material number'])) { // sometimes norm master does not include sample number even if listed
res.push({color: trim(sample['Color']), number: sample['Material number']});
}
return res;
}
}
async function getNormMaster(fetchAgain = false) {
if (fetchAgain) {
console.info('fetching norm master...');
const res = await axios({
method: 'get',
url: 'http://rb-normen.bosch.com/cgi-bin/searchRBNorm4TradeName'
});
console.info('finding documents...');
let match;
// const regex = /<tr>.*?<td>.*?<\/span>(.*?)<\/td><td>(\d+)<\/td>.*?<a href="(.*?)"/gm;
const regex = /<tr>.*?<td>.*?<\/span>(.*?)<\/td><td>(\d+)<\/td><td>40.*?<a href="(.*?)".*?<\/a>(.*?)<\/td>/gm; // only valid materials
do {
match = regex.exec(res.data);
if (match) {
normMaster[match[2]] = {name: match[1], nameSpaceless: stripSpaces(match[1]), number: match[2], url: match[3], doc: match[4]};
}
} while (match);
fs.writeFileSync('./data_import/normMaster.json', JSON.stringify(normMaster));
}
else {
normMaster = JSON.parse(fs.readFileSync('./data_import/normMaster.json'), 'utf-8');
}
}
function getNormMasterDoc(url, timing = 1) {
console.info(url);
return new Promise(async resolve => {
const options = new chrome.Options();
options.setUserPreferences({
"download.default_directory": nmDocs,
"download.prompt_for_download": false,
"download.directory_upgrade": true,
"plugins.always_open_pdf_externally": true
});
let driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
let timeout = 7000 * timing;
try {
await driver.get(url);
if (await driver.getCurrentUrl() !== 'https://rb-wam-saml.bosch.com/tfim/sps/normmaster/saml20/login') { // got document selection page
timeout = 11000 * timing;
await driver.executeScript('Array.prototype.slice.call(document.querySelectorAll(\'.functionlink\')).filter(e => e.innerText === \'English\')[0].click()').catch(() => {timeout = 0; });
}
}
finally {
setTimeout(async () => { // wait until download is finished
await driver.quit();
resolve();
}, timeout);
}
});
}
function readPdf(file) {
return new Promise(async resolve => {
const countdown = 100; // value for text timeout
let table = 0; // > 0 when in correct table area
let rows = []; // found table rows
let lastY = 0; // y of last row
let lastX = 0; // right x of last item
let lastText = ''; // text of last item
let lastLastText = ''; // text of last last item
await new pdfReader.PdfReader().parseFileItems(nmDocs + '\\' + file, (err, item) => {
if (item && item.text) {
if ((stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignationsupplier') >= 0) || (stripSpaces(lastLastText + lastText + item.text).toLowerCase().indexOf('colordesignatiomsupplier') >= 0)) { // table area starts
table = countdown;
}
if (table > 0) {
// console.log(item);
// console.log(item.y - lastY);
// console.log(item.text);
if (item.y - lastY > 0.8 && Math.abs(item.x - lastX) > 5) { // new row
lastY = item.y;
rows.push(item.text);
}
else { // still the same row row
rows[rows.length - 1] += (item.x - lastX > 1.09 ? '$' : '') + item.text; // push to row, detect if still same cell
}
lastX = (item.w * 0.055) + item.x;
if (/\d \d\d\d \d\d\d \d\d\d/.test(item.text)) {
table = countdown;
}
table --;
if (table <= 0 || item.text.toLowerCase().indexOf('release document') >= 0 || item.text.toLowerCase().indexOf('normative references') >= 0) { // table area ended
table = -1;
// console.log(rows);
rows = rows.filter(e => /^\d{10}/m.test(stripSpaces(e))); // filter non-table rows
resolve(rows.map(e => {return {color: trim(e.split('$')[3]), number: stripSpaces(e.split('$')[0])}; }));
}
}
lastLastText = lastText;
lastText = item.text;
}
if (!item && table !== -1) { // document ended
rows = rows.filter(e => /^\d{10}/m.test(stripSpaces(e))); // filter non-table rows
resolve(rows.map(e => {return {color: trim(e.split('$')[3]), number: stripSpaces(e.split('$')[0])}; }));
}
});
});
}
function sampleDeviceMap() {
const dpts = fs.readdirSync(dptFiles);
const regex = /(.*?)_(.*?)_(\d+|[^_]+_\d+).DPT/;
for (let i in dpts) {
const regexRes = regex.exec(dpts[i])
if (regexRes) { // found matching sample
sampleDevices[regexRes[2]] = regexRes[1] === 'plastics' ? 'rng01' : regexRes[1].toLowerCase();
}
}
}
function stripSpaces(s) {
return s ? s.replace(/ /g,'') : '';
}
function trim(s) {
return s.replace(/(^\s+|\s+$)/gm, '');
}

View File

@ -1,8 +1,9 @@
---
applications:
- name: digital-fingerprint-of-plastics-api
- name: definma-api
path: dist/
instances: 1
memory: 256M
memory: 1024M
stack: cflinuxfs3
buildpacks:
- nodejs_buildpack
@ -10,4 +11,4 @@ applications:
NODE_ENV: production
OPTIMIZE_MEMORY: true
services:
- dfopdb
- definmadb

View File

@ -1,69 +0,0 @@
/condition/{id}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Id'
get:
summary: TODO condition by id
description: 'levels: read, write, maintain, dev, admin'
tags:
- /condition
responses:
200:
description: condition details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Condition'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/change condition
description: 'levels: write, maintain, dev, admin'
tags:
- /condition
requestBody:
required: true
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Condition'
responses:
200:
description: condition details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Condition'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete condition
description: 'levels: write, maintain, dev, admin'
tags:
- /condition
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,63 +0,0 @@
/material/{id}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Id'
get:
summary: TODO get material details
description: 'levels: read, write, maintain, dev, admin'
tags:
- /material
responses:
200:
description: created material
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Material'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/change material
description: 'levels: write, maintain, dev, admin'
tags:
- /material
requestBody:
required: true
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Material'
responses:
200:
description: material details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Material'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete material
description: 'levels: write, maintain, dev, admin'
tags:
- /material
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,69 +0,0 @@
/measurement/{id}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Id'
get:
summary: TODO measurement values by id
description: 'levels: read, write, maintain, dev, admin'
tags:
- /measurement
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Measurement'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/change measurement
description: 'levels: write, maintain, dev, admin'
tags:
- /measurement
requestBody:
required: true
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Measurement'
responses:
200:
description: measurement details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Measurement'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete measurement
description: 'levels: write, maintain, dev, admin'
tags:
- /measurement
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,68 +0,0 @@
/model/{name}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Name'
get:
summary: TODO get model data by name
description: 'levels: dev, admin'
tags:
- /model
responses:
200:
description: binary model data
content:
application/octet-stream:
schema:
type: string
format: binary
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/replace model data by name
description: 'levels: dev, admin'
tags:
- /model
requestBody:
required: true
description: binary model data
content:
application/json:
schema:
type: string
format: binary
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete model data
description: 'levels: dev, admin'
tags:
- /model
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,18 +0,0 @@
/:
get:
summary: Root method
tags:
- /
security: []
responses:
200:
description: Server is working
content:
application/json:
schema:
properties:
message:
type: string
example: 'API server up and running!'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,12 +0,0 @@
Id:
name: id
in: path
required: true
schema:
type: string
Name:
name: name
in: path
required: true
schema:
type: string

View File

@ -1,108 +0,0 @@
/samples:
get:
summary: TODO all samples in overview
description: 'levels: read, write, maintain, dev, admin'
tags:
- /sample
responses:
200:
description: samples overview
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Samples'
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'
/sample/{id}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Id'
get:
summary: TODO sample details
description: 'levels: read, write, maintain, dev, admin'
tags:
- /sample
responses:
200:
description: samples details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/SampleDetail'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/change sample
description: 'levels: write, maintain, dev, admin'
tags:
- /sample
requestBody:
required: true
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Sample'
responses:
200:
description: samples details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/SampleDetail'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete sample
description: 'levels: write, maintain, dev, admin'
tags:
- /sample
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
/sample/notes/fields:
get:
summary: TODO list all existing field names for custom notes fields
description: 'levels: write, maintain, dev, admin'
tags:
- /sample
responses:
200:
description: field names and quantity of usage
content:
application/json:
schema:
properties:
name:
type: string
qty:
type: number
example: 20
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,164 +0,0 @@
Id:
type: string
_Id:
properties:
_id:
allOf:
- $ref: 'oas.yaml#/components/schemas/Id'
readOnly: true
Color:
properties:
color:
type: string
SampleProperties:
properties:
sample_number:
type: string
type:
type: string
batch:
type: string
validated:
type: boolean
Samples:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
- $ref: 'oas.yaml#/components/schemas/Color'
- $ref: 'oas.yaml#/components/schemas/SampleProperties'
properties:
material_id:
$ref: 'oas.yaml#/components/schemas/Id'
note_id:
$ref: 'oas.yaml#/components/schemas/Id'
user_id:
$ref: 'oas.yaml#/components/schemas/Id'
Sample:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
- $ref: 'oas.yaml#/components/schemas/Color'
- $ref: 'oas.yaml#/components/schemas/SampleProperties'
properties:
material:
$ref: 'oas.yaml#/components/schemas/Material'
notes:
type: object
properties:
comments:
type: string
sample_references:
type: array
items:
$ref: 'oas.yaml#/components/schemas/Id'
SampleDetail:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
- $ref: 'oas.yaml#/components/schemas/Color'
- $ref: 'oas.yaml#/components/schemas/SampleProperties'
properties:
material:
$ref: 'oas.yaml#/components/schemas/Material'
notes:
type: object
properties:
comments:
type: string
sample_references:
type: array
items:
$ref: 'oas.yaml#/components/schemas/Id'
conditions:
type: array
items:
$ref: 'oas.yaml#/components/schemas/Condition'
Material:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
properties:
material_numbers:
type: array
items:
type: object
allOf:
- $ref: 'oas.yaml#/components/schemas/Color'
properties:
number:
type: number
material_group:
type: string
supplier:
type: string
material_name:
type: string
mineral:
type: number
glass_fiber:
type: number
carbon_fiber:
type: number
Condition:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
properties:
sample_id:
$ref: 'oas.yaml#/components/schemas/Id'
parameters:
type: object
treatment_template:
$ref: 'oas.yaml#/components/schemas/Id'
Measurement:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
properties:
condition_id:
$ref: 'oas.yaml#/components/schemas/Id'
values:
type: object
measurement_template:
$ref: 'oas.yaml#/components/schemas/Id'
Template:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
properties:
name:
type: string
parameters:
type: array
items:
type: object
properties:
name:
type: string
range:
type: object
Email:
required:
- email
properties:
email:
type: string
example: john.doe@bosch.com
User:
allOf:
- $ref: 'oas.yaml#/components/schemas/_Id'
- $ref: 'oas.yaml#/components/schemas/Email'
properties:
name:
type: string
example: johndoe
levels:
type: array
items:
type: string
example: read
location:
type: string
example: Rng
device_name:
type: string
example: Alpha II

View File

@ -1,242 +0,0 @@
/template/treatments:
get:
summary: TODO all available treatment methods
description: 'levels: read, write, maintain, dev, admin'
tags:
- /templates
security:
- BasicAuth: []
responses:
200:
description: list of treatments
content:
application/json:
schema:
type: array
items:
$ref: 'oas.yaml#/components/schemas/Template'
example:
name: heat aging
parameters:
- name: method
range:
- copper
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'
/templates/treatment/{name}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Name'
get:
summary: TODO treatment method details
description: 'levels: read, write, maintain, admin'
tags:
- /templates
security:
- BasicAuth: []
responses:
200:
description: treatment details
content:
application/json:
schema:
allOf:
- $ref: 'oas.yaml#/components/schemas/Template'
example:
name: heat aging
parameters:
- name: method
range:
- copper
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/change treatment method
description: 'levels: maintain, admin'
tags:
- /templates
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: 'oas.yaml#/components/schemas/Template'
example:
name: heat aging
parameters:
- name: method
range:
- copper
responses:
200:
description: treatment details
content:
application/json:
schema:
allOf:
- $ref: 'oas.yaml#/components/schemas/Template'
example:
name: heat aging
parameters:
- name: method
range:
- copper
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete treatment method
description: 'levels: maintain, admin'
tags:
- /templates
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
/template/measurements:
get:
summary: TODO all available measurement methods
description: 'levels: read, write, maintain, dev, admin'
tags:
- /templates
security:
- BasicAuth: []
responses:
200:
description: list of measurement methods
content:
application/json:
schema:
type: array
items:
$ref: 'oas.yaml#/components/schemas/Template'
example:
name: humidity
parameters:
- name: kf
range:
min: 0
max: 2
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'
/templates/measurement/{name}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Name'
get:
summary: TODO measurement method details
description: 'levels: read, write, maintain, admin'
tags:
- /templates
security:
- BasicAuth: []
responses:
200:
description: measurement details
content:
application/json:
schema:
allOf:
- $ref: 'oas.yaml#/components/schemas/Template'
example:
name: humidity
parameters:
- name: kf
range:
min: 0
max: 2
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO add/change measurement method
description: 'levels: maintain, admin'
tags:
- /templates
requestBody:
required: true
content:
application/json:
schema:
allOf:
- $ref: 'oas.yaml#/components/schemas/Template'
example:
name: humidity
parameters:
- name: kf
range:
min: 0
max: 2
responses:
200:
description: measurement details
content:
application/json:
schema:
allOf:
- $ref: 'oas.yaml#/components/schemas/Template'
example:
name: humidity
parameters:
- name: kf
range:
min: 0
max: 2
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete measurement method
description: 'levels: maintain, admin'
tags:
- /templates
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'

View File

@ -1,170 +0,0 @@
/users:
get:
summary: TODO lists all users
description: 'levels: admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user API key
content:
application/json:
schema:
type: array
items:
$ref: 'oas.yaml#/components/schemas/User'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
500:
$ref: 'oas.yaml#/components/responses/500'
/user/{name}:
parameters:
- $ref: 'oas.yaml#/components/parameters/Name'
get:
summary: TODO list user details
description: 'levels: read, write, maintain, dev get their own information without a name property specified, level: admin can get any user using the name parameter'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user details
content:
application/json:
schema:
type: array
items:
$ref: 'oas.yaml#/components/schemas/User'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
put:
summary: TODO change user details
description: 'levels: read, write, maintain, dev can change their own information (except level) without a name property specified, level: admin can change any user using the name parameter'
tags:
- /user
requestBody:
required: true
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/User'
responses:
200:
description: user details
content:
application/json:
schema:
type: array
items:
$ref: 'oas.yaml#/components/schemas/User'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
delete:
summary: TODO delete user
description: 'levels: read, write, maintain, dev can delete their own account, level: admin can delete any user using the name parameter'
tags:
- /user
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
404:
$ref: 'oas.yaml#/components/responses/404'
500:
$ref: 'oas.yaml#/components/responses/500'
/user/key:
get:
summary: TODO get API key for the user
description: 'levels: read, write, maintain, dev, admin'
tags:
- /user
security:
- BasicAuth: []
responses:
200:
description: user details
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/User'
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'
/user/new:
post:
summary: TODO add new user
description: 'levels: admin'
tags:
- /user
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/User'
responses:
200:
description: user details
content:
application/json:
schema:
type: array
items:
$ref: 'oas.yaml#/components/schemas/User'
400:
$ref: 'oas.yaml#/components/responses/400'
401:
$ref: 'oas.yaml#/components/responses/401'
403:
$ref: 'oas.yaml#/components/responses/403'
500:
$ref: 'oas.yaml#/components/responses/500'
/user/passreset:
post:
summary: TODO reset password and send mail to restore
tags:
- /user
security: []
requestBody:
required: true
description: mail saved in user profile to provide authentication
content:
application/json:
schema:
$ref: 'oas.yaml#/components/schemas/Email'
responses:
200:
$ref: 'oas.yaml#/components/responses/Ok'
401:
$ref: 'oas.yaml#/components/responses/401'
500:
$ref: 'oas.yaml#/components/responses/500'

2024
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -4,29 +4,61 @@
"description": "API for the digital fingerprint of plastics mongodb",
"main": "index.js",
"scripts": {
"tsc": "tsc",
"tsc-full": "del /q dist\\* & (for /d %x in (dist\\*) do @rd /s /q \"%x\") & tsc",
"build": "build.bat",
"build-push": "build.bat && cf push",
"test": "mocha dist/**/**.spec.js",
"start": "tsc && node dist/index.js",
"dev": "nodemon -e ts,yaml --exec \"npm run start\""
"start": "node index.js",
"dev": "nodemon -e ts,yaml --exec \"tsc && node dist/index.js || exit 1\"",
"loadDev": "node dist/test/loadDev.js",
"coverage": "tsc && nyc --reporter=html --reporter=text mocha dist/**/**.spec.js --timeout 5000",
"import": "node data_import/import.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^8.0.0",
"@types/mocha": "^5.2.7",
"@types/node": "^13.1.6",
"@apidevtools/swagger-parser": "^9.0.1",
"@hapi/joi": "^17.1.1",
"axios": "^0.19.2",
"basic-auth": "^2.0.1",
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"cfenv": "^1.2.2",
"compression": "^1.7.4",
"content-filter": "^1.1.2",
"cors": "^2.8.5",
"express": "^4.17.1",
"helmet": "^3.22.0",
"json-schema": "^0.2.5",
"json2csv": "^5.0.1",
"lodash": "^4.17.15",
"mongo-sanitize": "^1.1.0",
"mongoose": "^5.8.7",
"nodemon": "^2.0.3",
"swagger-ui-express": "^4.1.2",
"tslint": "^5.20.1",
"typescript": "^3.7.4"
"swagger-ui-express": "4.1.2"
},
"devDependencies": {
"mocha": "^7.0.0",
"@types/bcrypt": "^3.0.0",
"@types/body-parser": "^1.19.0",
"@types/express-serve-static-core": "^4.17.5",
"@types/lodash": "^4.14.150",
"@types/mocha": "^5.2.7",
"@types/mongoose": "^5.7.12",
"@types/node": "^13.1.6",
"@types/qs": "^6.9.1",
"@types/serve-static": "^1.13.3",
"csv-parser": "^2.3.3",
"iconv-lite": "^0.6.0",
"mocha": "^7.1.2",
"nodemon": "^2.0.3",
"nyc": "^15.0.1",
"pdfreader": "^1.0.7",
"selenium-webdriver": "^4.0.0-alpha.7",
"should": "^13.2.3",
"supertest": "^4.0.2"
"supertest": "^4.0.2",
"tslint": "^5.20.1",
"typescript": "^3.7.4"
}
}

48
src/api.ts Normal file
View File

@ -0,0 +1,48 @@
import swagger from 'swagger-ui-express';
import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser';
import oasParser from '@apidevtools/swagger-parser';
// modifies the normal swagger-ui-express package
// usage: app.use('/api-doc', api.serve(), api.setup());
// the paths property can be split using allOf
// further route documentation can be included in the x-doc property
export default class api {
static serve () {
return swagger.serve;
}
static setup () {
let apiDoc: JSONSchema = {};
jsonRefParser.bundle('api/api.yaml', (err, doc) => { // parse yaml
if (err) throw err;
apiDoc = doc;
apiDoc.servers.splice(process.env.NODE_ENV === 'production', 1);
apiDoc.paths = apiDoc.paths.allOf.reduce((s, e) => Object.assign(s, e)); // bundle routes
apiDoc = this.resolveXDoc(apiDoc);
oasParser.validate(apiDoc, (err, api) => { // validate oas schema
if (err) {
console.error(err);
}
else {
console.info(process.env.NODE_ENV === 'test' ? '' : 'API ok, version ' + api.info.version);
swagger.setup(apiDoc);
}
});
});
return swagger.setup(apiDoc, {customCssUrl: '/static/styles/swagger.css'})
}
private static resolveXDoc (doc) { // resolve x-doc properties recursively
Object.keys(doc).forEach(key => {
if (doc[key] !== null && doc[key].hasOwnProperty('x-doc')) { // add x-doc to description, is styled via css
doc[key].description += '<details class="docs"><summary>docs</summary>' + doc[key]['x-doc'] + '</details>';
}
else if (typeof doc[key] === 'object' && doc[key] !== null) { // go deeper into recursion
doc[key] = this.resolveXDoc(doc[key]);
}
});
return doc;
}
}

116
src/customTypes/express.ts Normal file
View File

@ -0,0 +1,116 @@
// Type definitions for Express 4.17
// Project: http://expressjs.com
// Definitions by: Boris Yankov <https://github.com/borisyankov>
// China Medical University Hospital <https://github.com/CMUH>
// Puneet Arora <https://github.com/puneetar>
// Dylan Frankland <https://github.com/dfrankland>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
// TypeScript Version: 2.3
/* =================== USAGE ===================
import * as express from "express";
var app = express();
=============================================== */
/// <reference types="express-serve-static-core" />
/// <reference types="serve-static" />
import * as bodyParser from "body-parser";
import serveStatic = require("serve-static");
import * as core from "express-serve-static-core";
import * as qs from "qs";
/**
* Creates an Express application. The express() function is a top-level function exported by the express module.
*/
declare function e(): core.Express;
declare namespace e {
/**
* This is a built-in middleware function in Express. It parses incoming requests with JSON payloads and is based on body-parser.
* @since 4.16.0
*/
var json: typeof bodyParser.json;
/**
* This is a built-in middleware function in Express. It parses incoming requests with Buffer payloads and is based on body-parser.
* @since 4.17.0
*/
var raw: typeof bodyParser.raw;
/**
* This is a built-in middleware function in Express. It parses incoming requests with text payloads and is based on body-parser.
* @since 4.17.0
*/
var text: typeof bodyParser.text;
/**
* These are the exposed prototypes.
*/
var application: Application;
var request: Request;
var response: Response;
/**
* This is a built-in middleware function in Express. It serves static files and is based on serve-static.
*/
var static: typeof serveStatic;
/**
* This is a built-in middleware function in Express. It parses incoming requests with urlencoded payloads and is based on body-parser.
* @since 4.16.0
*/
var urlencoded: typeof bodyParser.urlencoded;
/**
* This is a built-in middleware function in Express. It parses incoming request query parameters.
*/
export function query(options: qs.IParseOptions | typeof qs.parse): Handler;
export function Router(options?: RouterOptions): core.Router;
interface RouterOptions {
/**
* Enable case sensitivity.
*/
caseSensitive?: boolean;
/**
* Preserve the req.params values from the parent router.
* If the parent and the child have conflicting param names, the childs value take precedence.
*
* @default false
* @since 4.5.0
*/
mergeParams?: boolean;
/**
* Enable strict routing.
*/
strict?: boolean;
}
interface Application extends core.Application { }
interface CookieOptions extends core.CookieOptions { }
interface Errback extends core.Errback { }
interface ErrorRequestHandler<P extends core.Params = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query>
extends core.ErrorRequestHandler<P, ResBody, ReqBody, ReqQuery> { }
interface Express extends core.Express { }
interface Handler extends core.Handler { }
interface IRoute extends core.IRoute { }
interface IRouter extends core.IRouter { }
interface IRouterHandler<T> extends core.IRouterHandler<T> { }
interface IRouterMatcher<T> extends core.IRouterMatcher<T> { }
interface MediaType extends core.MediaType { }
interface NextFunction extends core.NextFunction { }
interface Request<P extends core.Params = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query> extends core.Request<P, ResBody, ReqBody, ReqQuery> { }
interface RequestHandler<P extends core.Params = core.ParamsDictionary, ResBody = any, ReqBody = any, ReqQuery = core.Query> extends core.RequestHandler<P, ResBody, ReqBody, ReqQuery> { }
interface RequestParamHandler extends core.RequestParamHandler { }
export interface Response<ResBody = any> extends core.Response<ResBody> { }
interface Router extends core.Router { }
interface Send extends core.Send { }
}
export = e;

158
src/db.ts Normal file
View File

@ -0,0 +1,158 @@
import mongoose from 'mongoose';
import cfenv from 'cfenv';
import _ from 'lodash';
import ChangelogModel from './models/changelog';
// database urls, prod db url is retrieved automatically
const TESTING_URL = 'mongodb://localhost/dfopdb_test';
const DEV_URL = 'mongodb://localhost/dfopdb';
const debugging = true;
if (process.env.NODE_ENV !== 'production' && debugging) {
mongoose.set('debug', true); // enable mongoose debug
}
export default class db {
private static state = { // db object and current mode (test, dev, prod)
db: null,
mode: null,
};
static connect (mode = '', done: Function = () => {}) { // set mode to test for unit/integration tests, otherwise skip parameters. done is also only needed for testing
if (this.state.db) return done(); // db is already connected
// find right connection url
let connectionString: string = "";
if (mode === 'test') { // testing
connectionString = TESTING_URL;
this.state.mode = 'test';
}
else if(process.env.NODE_ENV === 'production') {
let services = cfenv.getAppEnv().getServices();
for (let service in services) {
if(services[service].tags.indexOf("mongodb") >= 0) {
connectionString = services[service]["credentials"].uri;
}
}
this.state.mode = 'prod';
}
else {
connectionString = DEV_URL;
this.state.mode = 'dev';
}
// connect to db
mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true, useCreateIndex: true, connectTimeoutMS: 10000}, err => {
if (err) done(err);
});
mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
mongoose.connection.on('connected', () => { // evaluation connection behaviour on prod
if (process.env.NODE_ENV !== 'test') { // Do not interfere with testing
console.info('Database connected');
}
});
mongoose.connection.on('disconnected', () => { // reset state on disconnect
if (process.env.NODE_ENV !== 'test') { // Do not interfere with testing
console.info('Database disconnected');
// this.state.db = 0; // prod database connects and disconnects automatically
}
});
process.on('SIGINT', () => { // close connection when app is terminated
if (!this.state.db) { // database still connected
mongoose.connection.close(() => {
console.info('Mongoose default connection disconnected through app termination');
process.exit(0);
});
}
});
mongoose.connection.once('open', () => {
mongoose.set('useFindAndModify', false);
console.info(process.env.NODE_ENV === 'test' ? '' : `Connected to ${connectionString}`);
this.state.db = mongoose.connection;
done();
});
}
static disconnect (done) {
mongoose.connection.close(() => {
console.info(process.env.NODE_ENV === 'test' ? '' : `Disconnected from database`);
this.state.db = 0;
done();
});
}
static getState () {
return this.state;
}
static drop (done: Function = () => {}) { // drop all collections of connected db (only dev and test for safety reasons ;)
if (!this.state.db || this.state.mode === 'prod') return done(); // no db connection or prod db
this.state.db.db.listCollections().toArray((err, collections) => { // get list of all collections
if (collections.length === 0) { // there are no collections to drop
return done();
}
else {
let dropCounter = 0; // count number of dropped collections to know when to return done()
collections.forEach(collection => { // drop each collection
this.state.db.dropCollection(collection.name, () => {
if (++ dropCounter >= collections.length) { // all collections dropped
done();
}
});
});
}
});
}
static loadJson (json, done: Function = () => {}) { // insert given JSON data into db, uses core mongodb methods
if (!this.state.db || !json.hasOwnProperty('collections') || json.collections.length === 0) { // no db connection or nothing to load
return done();
}
let loadCounter = 0; // count number of loaded collections to know when to return done()
Object.keys(json.collections).forEach(collectionName => { // create each collection
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
done();
}
});
});
});
}
// changelog entry
static log(req, thisOrCollection, conditions = null, data = null) { // expects (req, this (from query helper)) or (req, collection, conditions, data)
if (! (conditions || data)) { // (req, this)
data = thisOrCollection._update ? _.cloneDeep(thisOrCollection._update) : {}; // replace undefined with {}
Object.keys(data).forEach(key => {
if (key[0] === '$') {
data[key.substr(1)] = data[key];
delete data[key];
}
});
new ChangelogModel({action: req.method + ' ' + req.url, collectionName: thisOrCollection._collection.collectionName, conditions: thisOrCollection._conditions, data: data, user_id: req.authDetails.id ? req.authDetails.id : null}).save(err => {
if (err) console.error(err);
});
}
else { // (req, collection, conditions, data)
new ChangelogModel({action: req.method + ' ' + req.url, collectionName: thisOrCollection, conditions: conditions, data: data, user_id: req.authDetails.id ? req.authDetails.id : null}).save(err => {
if (err) console.error(err);
});
}
}
private static oidResolve (object: any) { // resolve $oid fields to actual ObjectIds recursively
Object.keys(object).forEach(key => {
if (object[key] !== null && object[key].hasOwnProperty('$oid')) { // found oid, replace
object[key] = mongoose.Types.ObjectId(object[key].$oid);
}
else if (typeof object[key] === 'object' && object[key] !== null) { // deeper into recursion
object[key] = this.oidResolve(object[key]);
}
});
return object;
}
};

17
src/globals.ts Normal file
View File

@ -0,0 +1,17 @@
const globals = {
levels: [ // access levels
'read',
'write',
'maintain',
'dev',
'admin'
],
status: { // document statuses
deleted: -1,
new: 0,
validated: 10,
}
};
export default globals;

105
src/helpers/authorize.ts Normal file
View File

@ -0,0 +1,105 @@
import basicAuth from 'basic-auth';
import bcrypt from 'bcryptjs';
import UserModel from '../models/user';
// appends req.auth(res, ['levels'], method = 'all')
// which returns sends error message and returns false if unauthorized, otherwise true
// req.authDetails returns eg. {methods: ['basic'], username: 'johndoe', level: 'write'}
module.exports = async (req, res, next) => {
let givenMethod = ''; // authorization method given by client, basic taken preferred
let user = {name: '', level: '', id: '', location: ''}; // user object
// test authentications
const userBasic = await basic(req, next);
if (userBasic) { // basic available
givenMethod = 'basic';
user = userBasic;
}
else { // if basic not available, test key
const userKey = await key(req, next);
if (userKey) {
givenMethod = 'key';
user = userKey;
}
}
req.auth = (res, levels, method = 'all') => {
if (givenMethod === method || (method === 'all' && givenMethod !== '')) { // method is available
if (levels.indexOf(user.level) > -1) { // level is available
return true;
}
else {
res.status(403).json({status: 'Forbidden'});
return false;
}
}
else {
res.status(401).json({status: 'Unauthorized'});
return false;
}
}
req.authDetails = {
method: givenMethod,
username: user.name,
level: user.level,
id: user.id,
location: user.location
};
next();
}
function basic (req, next): any { // checks basic auth and returns changed user object
return new Promise(resolve => {
const auth = basicAuth(req);
if (auth !== undefined) { // basic auth available
UserModel.find({name: auth.name}).lean().exec( (err, data: any) => { // find user
if (err) return next(err);
if (data.length === 1) { // one user found
bcrypt.compare(auth.pass, data[0].pass, (err, res) => { // check password
if (err) return next(err);
if (res === true) { // password correct
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location});
}
else {
resolve(null);
}
});
}
else {
resolve(null);
}
});
}
else {
resolve(null);
}
});
}
function key (req, next): any { // checks API key and returns changed user object
return new Promise(resolve => {
if (req.query.key !== undefined) { // key available
UserModel.find({key: req.query.key}).lean().exec( (err, data: any) => { // find user
if (err) return next(err);
if (data.length === 1) { // one user found
resolve({level: data[0].level, name: data[0].name, id: data[0]._id.toString(), location: data[0].location});
if (!/^\/api/m.test(req.url)){
delete req.query.key; // delete query parameter to avoid interference with later validation
}
}
else {
resolve(null);
}
});
}
else {
resolve(null);
}
});
}

34
src/helpers/csv.ts Normal file
View File

@ -0,0 +1,34 @@
import {parseAsync} from 'json2csv';
export default function csv(input: any[], f: (err, data) => void) {
parseAsync(input.map(e => flatten(e)), {includeEmptyRows: true})
.then(csv => f(null, csv))
.catch(err => f(err, null));
}
function flatten (data) { // flatten object: {a: {b: true}} -> {a.b: true}
const result = {};
function recurse (cur, prop) {
if (Object(cur) !== cur || Object.keys(cur).length === 0) {
result[prop] = cur;
}
else if (Array.isArray(cur)) {
let l = 0;
for(let i = 0, l = cur.length; i < l; i++)
recurse(cur[i], prop + "[" + i + "]");
if (l == 0)
result[prop] = [];
}
else {
let isEmpty = true;
for (let p in cur) {
isEmpty = false;
recurse(cur[p], prop ? prop+"."+p : p);
}
if (isEmpty && prop)
result[prop] = {};
}
}
recurse(data, '');
return result;
}

64
src/helpers/mail.ts Normal file
View File

@ -0,0 +1,64 @@
import axios from 'axios';
// sends an email using the BIC service
export default (mailAddress, subject, content, f) => { // callback, executed empty or with error
if (process.env.NODE_ENV === 'production') {
const mailService = JSON.parse(process.env.VCAP_SERVICES).Mail[0];
axios({
method: 'post',
url: mailService.credentials.uri + '/email',
auth: {username: mailService.credentials.username, password: mailService.credentials.password},
data: {
recipients: [{to: mailAddress}],
subject: {content: subject},
body: {
content: content,
contentType: "text/html"
},
from: {
eMail: "definma@bosch-iot.com",
password: "PlasticsOfFingerprintDigital"
}
}
})
.then(() => {
f();
})
.catch((err) => {
f(err);
});
}
else if (process.env.NODE_ENV === 'test') {
console.info('Sending mail to ' + mailAddress + ': -- ' + subject + ' -- ' + content);
f();
}
else { // dev
axios({
method: 'get',
url: 'https://digital-fingerprint-of-plastics-mail-test.apps.de1.bosch-iot-cloud.com/api',
data: {
method: 'post',
url: '/email',
data: {
recipients: [{to: mailAddress}],
subject: {content: subject},
body: {
content: content,
contentType: "text/html"
},
from: {
eMail: "dfop-test@bosch-iot.com",
password: "PlasticsOfFingerprintDigital"
}
}
}
})
.then(() => {
f();
})
.catch((err) => {
f(err);
});
}
}

View File

@ -1,37 +1,21 @@
import cfenv from 'cfenv';
import express from 'express';
import mongoose from 'mongoose';
import swagger from 'swagger-ui-express';
import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser';
import bodyParser from 'body-parser';
import compression from 'compression';
import contentFilter from 'content-filter';
import mongoSanitize from 'mongo-sanitize';
import helmet from 'helmet';
import cors from 'cors';
import api from './api';
import db from './db';
// TODO: working demo branch
// tell if server is running in debug or production environment
console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : '===== DEVELOPMENT =====');
// get mongodb address from server, otherwise set to localhost
let connectionString: string = "";
if(process.env.NODE_ENV === 'production') {
let services = cfenv.getAppEnv().getServices();
for (let service in services) {
if(services[service].tags.indexOf("mongodb") >= 0) {
connectionString = services[service]["credentials"].uri;
}
}
}
else {
connectionString = 'mongodb://localhost/dfopdb';
}
mongoose.connect(connectionString, {useNewUrlParser: true, useUnifiedTopology: true});
// connect to mongodb
let db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log(`Connected to ${connectionString}`);
});
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== DEVELOPMENT =====');
// mongodb connection
db.connect();
// create Express app
const app = express();
@ -40,20 +24,68 @@ app.disable('x-powered-by');
// get port from environment, defaults to 3000
const port = process.env.PORT || 3000;
//middleware
app.use(helmet());
app.use(contentFilter()); // filter URL query attacks
app.use(express.json({ limit: '5mb'}));
app.use(express.urlencoded({ extended: false, limit: '5mb' }));
app.use(compression()); // compress responses
app.use(bodyParser.json());
app.use((req, res, next) => { // filter body query attacks
req.body = mongoSanitize(req.body);
next();
});
app.use((err, req, res, ignore) => { // bodyParser error handling
res.status(400).send({status: 'Invalid JSON body'});
});
app.use((req, res, next) => { // no database connection error
if (db.getState().db) {
next();
}
else {
console.error('No database connection');
res.status(500).send({status: 'Internal server error'});
}
});
app.use(cors()); // CORS headers
app.use(require('./helpers/authorize')); // handle authentication
// redirect /api routes for Angular proxy in development
if (process.env.NODE_ENV !== 'production') {
app.use('/api/:url([^]+)', (req, res) => {
req.url = '/' + req.params.url;
app.handle(req, res);
});
}
// require routes
app.use('/', require('./routes/root'));
app.use('/', require('./routes/sample'));
app.use('/', require('./routes/material'));
app.use('/', require('./routes/template'));
app.use('/', require('./routes/user'));
app.use('/', require('./routes/measurement'));
// static files
app.use('/static', express.static('static'));
// Swagger UI
let oasDoc: JSONSchema = {};
jsonRefParser.bundle('oas/oas.yaml', (err, doc) => {
if(err) throw err;
oasDoc = doc;
oasDoc.paths = oasDoc.paths.allOf.reduce((s, e) => Object.assign(s, e));
swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'});
app.use('/api-doc', api.serve(), api.setup());
app.use((req, res) => { // 404 error handling
res.status(404).json({status: 'Not found'});
});
app.use('/api', swagger.serve, swagger.setup(oasDoc, {defaultModelsExpandDepth: -1, customCss: '.swagger-ui .topbar { display: none }'}));
app.use((err, req, res, ignore) => { // internal server error handling
console.error(err);
res.status(500).json({status: 'Internal server error'});
});
// hook up server to port
app.listen(port, () => {
console.log(`Listening on http;//localhost:${port}`);
const server = app.listen(port, () => {
console.info(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
});
module.exports = server;

11
src/models/changelog.ts Normal file
View File

@ -0,0 +1,11 @@
import mongoose from 'mongoose';
const ChangelogSchema = new mongoose.Schema({
action: String,
collectionName: String,
conditions: Object,
data: Object,
user_id: mongoose.Schema.Types.ObjectId
}, {minimize: false});
export default mongoose.model<any, mongoose.Model<any, any>>('changelog', ChangelogSchema);

View File

@ -0,0 +1,20 @@
import mongoose from 'mongoose';
import db from '../db';
const ConditionTemplateSchema = new mongoose.Schema({
first_id: mongoose.Schema.Types.ObjectId,
name: String,
version: Number,
parameters: [new mongoose.Schema({
name: String,
range: mongoose.Schema.Types.Mixed
} ,{ _id : false })]
}, {minimize: false}); // to allow empty objects
// changelog query helper
ConditionTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('condition_template', ConditionTemplateSchema);

28
src/models/material.ts Normal file
View File

@ -0,0 +1,28 @@
import mongoose from 'mongoose';
import MaterialSupplierModel from '../models/material_suppliers';
import MaterialGroupsModel from '../models/material_groups';
import db from '../db';
const MaterialSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
supplier_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialSupplierModel},
group_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialGroupsModel},
mineral: Number,
glass_fiber: Number,
carbon_fiber: Number,
numbers: [{
color: String,
number: String
}],
status: Number
}, {minimize: false});
// changelog query helper
MaterialSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
MaterialSchema.index({supplier_id: 1});
MaterialSchema.index({group_id: 1});
export default mongoose.model<any, mongoose.Model<any, any>>('material', MaterialSchema);

View File

@ -0,0 +1,14 @@
import mongoose from 'mongoose';
import db from '../db';
const MaterialGroupsSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}}
});
// changelog query helper
MaterialGroupsSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('material_groups', MaterialGroupsSchema);

View File

@ -0,0 +1,14 @@
import mongoose from 'mongoose';
import db from '../db';
const MaterialSuppliersSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}}
});
// changelog query helper
MaterialSuppliersSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('material_suppliers', MaterialSuppliersSchema);

23
src/models/measurement.ts Normal file
View File

@ -0,0 +1,23 @@
import mongoose from 'mongoose';
import SampleModel from './sample';
import MeasurementTemplateModel from './measurement_template';
import db from '../db';
const MeasurementSchema = new mongoose.Schema({
sample_id: {type: mongoose.Schema.Types.ObjectId, ref: SampleModel},
values: mongoose.Schema.Types.Mixed,
measurement_template: {type: mongoose.Schema.Types.ObjectId, ref: MeasurementTemplateModel},
status: Number
}, {minimize: false});
// changelog query helper
MeasurementSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
MeasurementSchema.index({sample_id: 1});
MeasurementSchema.index({measurement_template: 1});
export default mongoose.model<any, mongoose.Model<any, any>>('measurement', MeasurementSchema);

View File

@ -0,0 +1,20 @@
import mongoose from 'mongoose';
import db from '../db';
const MeasurementTemplateSchema = new mongoose.Schema({
first_id: mongoose.Schema.Types.ObjectId,
name: String,
version: Number,
parameters: [new mongoose.Schema({
name: String,
range: mongoose.Schema.Types.Mixed
} ,{ _id : false })]
}, {minimize: false}); // to allow empty objects
// changelog query helper
MeasurementTemplateSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('measurement_template', MeasurementTemplateSchema);

19
src/models/note.ts Normal file
View File

@ -0,0 +1,19 @@
import mongoose from 'mongoose';
import db from '../db';
const NoteSchema = new mongoose.Schema({
comment: String,
sample_references: [{
sample_id: mongoose.Schema.Types.ObjectId,
relation: String
}],
custom_fields: mongoose.Schema.Types.Mixed
});
// changelog query helper
NoteSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('note', NoteSchema);

15
src/models/note_field.ts Normal file
View File

@ -0,0 +1,15 @@
import mongoose from 'mongoose';
import db from '../db';
const NoteFieldSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
qty: Number
});
// changelog query helper
NoteFieldSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('note_field', NoteFieldSchema);

29
src/models/sample.ts Normal file
View File

@ -0,0 +1,29 @@
import mongoose from 'mongoose';
import MaterialModel from './material';
import NoteModel from './note';
import UserModel from './user';
import db from '../db';
const SampleSchema = new mongoose.Schema({
number: {type: String, index: {unique: true}},
type: String,
color: String,
batch: String,
condition: mongoose.Schema.Types.Mixed,
material_id: {type: mongoose.Schema.Types.ObjectId, ref: MaterialModel},
note_id: {type: mongoose.Schema.Types.ObjectId, ref: NoteModel},
user_id: {type: mongoose.Schema.Types.ObjectId, ref: UserModel},
status: Number
}, {minimize: false});
// changelog query helper
SampleSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
SampleSchema.index({material_id: 1});
SampleSchema.index({note_id: 1});
SampleSchema.index({user_id: 1});
export default mongoose.model<any, mongoose.Model<any, any>>('sample', SampleSchema);

20
src/models/user.ts Normal file
View File

@ -0,0 +1,20 @@
import mongoose from 'mongoose';
import db from '../db';
const UserSchema = new mongoose.Schema({
name: {type: String, index: {unique: true}},
email: String,
pass: String,
key: String,
level: String,
location: String,
device_name: String
});
// changelog query helper
UserSchema.query.log = function <Q extends mongoose.DocumentQuery<any, any>> (req) {
db.log(req, this);
return this;
}
export default mongoose.model<any, mongoose.Model<any, any>>('user', UserSchema);

1051
src/routes/material.spec.ts Normal file

File diff suppressed because it is too large Load Diff

223
src/routes/material.ts Normal file
View File

@ -0,0 +1,223 @@
import express from 'express';
import _ from 'lodash';
import MaterialValidate from './validate/material';
import MaterialModel from '../models/material'
import SampleModel from '../models/sample';
import MaterialGroupModel from '../models/material_groups';
import MaterialSupplierModel from '../models/material_suppliers';
import IdValidate from './validate/id';
import res400 from './validate/res400';
import mongoose from 'mongoose';
import globals from '../globals';
import db from '../db';
const router = express.Router();
router.get('/materials', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
const {error, value: filters} = MaterialValidate.query(req.query);
if (error) return res400(error, res);
let conditions;
if (filters.hasOwnProperty('status')) {
if(filters.status === 'all') {
conditions = {$or: [{status: globals.status.validated}, {status: globals.status.new}]}
}
else {
conditions = {status: globals.status[filters.status]};
}
}
else { // default
conditions = {status: globals.status.validated};
}
MaterialModel.find(conditions).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors
});
});
router.get('/materials/:state(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
MaterialModel.find({status: globals.status[req.params.state]}).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.output(e)))); // validate all and filter null values from validation errors
});
});
router.get('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
MaterialModel.findById(req.params.id).populate('group_id').populate('supplier_id').lean().exec((err, data: any) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
if (data.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted materials only available for maintain/admin
res.json(MaterialValidate.output(data));
});
});
router.put('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
let {error, value: material} = MaterialValidate.input(req.body, 'change');
if (error) return res400(error, res);
MaterialModel.findById(req.params.id).lean().exec(async (err, materialData: any) => {
if (!materialData) {
return res.status(404).json({status: 'Not found'});
}
if (materialData.status === globals.status.deleted) {
return res.status(403).json({status: 'Forbidden'});
}
if (material.hasOwnProperty('name') && material.name !== materialData.name) {
if (!await nameCheck(material, res, next)) return;
}
if (material.hasOwnProperty('group')) {
material = await groupResolve(material, req, next);
if (!material) return;
}
if (material.hasOwnProperty('supplier')) {
material = await supplierResolve(material, req, next);
if (!material) return;
}
// check for changes
if (!_.isEqual(_.pick(IdValidate.stringify(materialData), _.keys(material)), IdValidate.stringify(material))) {
material.status = globals.status.new; // set status to new
}
await MaterialModel.findByIdAndUpdate(req.params.id, material, {new: true}).log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err);
res.json(MaterialValidate.output(data));
});
});
});
router.delete('/material/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
// check if there are still samples referencing this material
SampleModel.find({'material_id': new mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => {
if (err) return next(err);
if (data.length) {
return res.status(400).json({status: 'Material still in use'});
}
MaterialModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).populate('group_id').populate('supplier_id').lean().exec((err, data) => {
if (err) return next(err);
if (data) {
res.json({status: 'OK'});
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
});
router.put('/material/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
setStatus(globals.status.new, req, res, next);
});
router.put('/material/validate/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
setStatus(globals.status.validated, req, res, next);
});
router.post('/material/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
let {error, value: material} = MaterialValidate.input(req.body, 'new');
if (error) return res400(error, res);
if (!await nameCheck(material, res, next)) return;
material = await groupResolve(material, req, next);
if (!material) return;
material = await supplierResolve(material, req, next);
if (!material) return;
material.status = globals.status.new; // set status to new
await new MaterialModel(material).save(async (err, data) => {
if (err) return next(err);
db.log(req, 'materials', {_id: data._id}, data.toObject());
await data.populate('group_id').populate('supplier_id').execPopulate().catch(err => next(err));
if (data instanceof Error) return;
res.json(MaterialValidate.output(data.toObject()));
});
});
router.get('/material/groups', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
MaterialGroupModel.find().lean().exec((err, data: any) => {
if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.outputGroups(e.name)))); // validate all and filter null values from validation errors
});
});
router.get('/material/suppliers', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
MaterialSupplierModel.find().lean().exec((err, data: any) => {
if (err) return next(err);
res.json(_.compact(data.map(e => MaterialValidate.outputSuppliers(e.name)))); // validate all and filter null values from validation errors
});
});
module.exports = router;
async function nameCheck (material, res, next) { // check if name was already taken
const materialData = await MaterialModel.findOne({name: material.name}).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 name already taken'});
return false;
}
return true;
}
async function groupResolve (material, req, next) {
const groupData = await MaterialGroupModel.findOneAndUpdate({name: material.group}, {name: material.group}, {upsert: true, new: true}).log(req).lean().exec().catch(err => next(err)) as any;
if (groupData instanceof Error) return false;
material.group_id = groupData._id;
delete material.group;
return material;
}
async function supplierResolve (material, req, next) {
const supplierData = await MaterialSupplierModel.findOneAndUpdate({name: material.supplier}, {name: material.supplier}, {upsert: true, new: true}).log(req).lean().exec().catch(err => next(err)) as any;
if (supplierData instanceof Error) return false;
material.supplier_id = supplierData._id;
delete material.supplier;
return material;
}
function setStatus (status, req, res, next) { // set measurement status
MaterialModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
res.json({status: 'OK'});
});
}

View File

@ -0,0 +1,790 @@
import should from 'should/as-function';
import MeasurementModel from '../models/measurement';
import TestHelper from "../test/helper";
import globals from '../globals';
describe('/measurement', () => {
let server;
before(done => TestHelper.before(done));
beforeEach(done => server = TestHelper.beforeEach(server, done));
afterEach(done => TestHelper.afterEach(server, done));
after(done => TestHelper.after(done));
describe('GET /measurement/{id}', () => {
it('returns the right measurement', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}
});
});
it('returns the measurement for an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/800000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 200,
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}
});
});
it('returns deleted measurements for a maintain/admin user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/800000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200,
res: {_id: '800000000000000000000004', sample_id: '400000000000000000000003', values: {val1: 1}, measurement_template: '300000000000000000000003'}
});
});
it('rejects requests for deleted measurements from a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/800000000000000000000004',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/8000000000h0000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/000000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/measurement/800000000000000000000001',
httpStatus: 401
});
});
});
describe('PUT /measurement/{id}', () => {
it('returns the right measurement', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {},
res: {_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'}
});
});
it('keeps unchanged values', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}}
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[3997.12558,98.00555],[3995.08519,98.03253],[3993.04480,98.02657]]}, measurement_template: '300000000000000000000001'});
MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
done();
});
});
});
it('keeps only one unchanged value', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {values: {'weight %': 0.5}}
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.5, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'});
MeasurementModel.findById('800000000000000000000002').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
done();
});
});
});
it('changes the given values', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {values: {dpt: [[1,2],[3,4],[5,6]]}}
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({_id: '800000000000000000000001', sample_id: '400000000000000000000001', values: {dpt: [[1,2],[3,4],[5,6]]}, measurement_template: '300000000000000000000001'});
MeasurementModel.findById('800000000000000000000001').lean().exec((err, data: any) => {
should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000001');
should(data.measurement_template.toString()).be.eql('300000000000000000000001');
should(data).have.property('status',globals.status.new);
should(data).have.property('values');
should(data.values).have.property('dpt', [[1,2],[3,4],[5,6]]);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {values: {dpt: [[1,2],[3,4],[5,6]]}},
log: {
collection: 'measurements',
dataAdd: {
measurement_template: '300000000000000000000001',
sample_id: '400000000000000000000001',
status: 0
}
}
});
});
it('allows changing only one value', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {values: {'weight %': 0.9}},
res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.2}, measurement_template: '300000000000000000000002'}
});
});
it('allows keeping empty values empty', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000005',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {values: {'weight %': 0.9}},
res: {_id: '800000000000000000000005', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': null}, measurement_template: '300000000000000000000002'}
});
});
it('rejects not specified values', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3, xx: 44}},
res: {status: 'Invalid body format', details: '"xx" is not allowed'}
});
});
it('rejects a value not in the value range', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000003',
auth: {basic: 'admin'},
httpStatus: 400,
req: {values: {val1: 4}},
res: {status: 'Invalid body format', details: '"val1" must be one of [1, 2, 3, null]'}
});
});
it('rejects a value below minimum range', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {values: {'weight %': -1, 'standard deviation': 0.3}},
res: {status: 'Invalid body format', details: '"weight %" must be larger than or equal to 0'}
});
});
it('rejects a value above maximum range', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {values: {'weight %': 0.9, 'standard deviation': 3}},
res: {status: 'Invalid body format', details: '"standard deviation" must be less than or equal to 0.5'}
});
});
it('rejects a new measurement template', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000001'},
res: {status: 'Invalid body format', details: '"measurement_template" is not allowed'}
});
});
it('rejects a new sample id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3}, sample_id: '400000000000000000000002'},
res: {status: 'Invalid body format', details: '"sample_id" is not allowed'}
});
});
it('rejects editing a measurement for a write user who did not create this measurement', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000003',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {values: {val1: 2}}
});
});
it('accepts editing a measurement of another user for a maintain/admin user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'admin'},
httpStatus: 200,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3}},
res: {_id: '800000000000000000000002', sample_id: '400000000000000000000002', values: {'weight %': 0.9, 'standard deviation': 0.3}, measurement_template: '300000000000000000000002'}
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000h00000000000002',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/000000000000000000000002',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects editing a deleted measurement', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 403,
req: {}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {key: 'janedoe'},
httpStatus: 401,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3}},
});
});
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
auth: {basic: 'user'},
httpStatus: 403,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3}},
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/800000000000000000000002',
httpStatus: 401,
req: {values: {'weight %': 0.9, 'standard deviation': 0.3}},
});
});
});
describe('DELETE /measurement/{id}', () => {
it('sets the status to deleted', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
MeasurementModel.findById('800000000000000000000001').lean().exec((err, data) => {
if (err) return done(err);
should(data).have.property('status',globals.status.deleted);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
log: {
collection: 'measurements',
dataAdd: {
status: -1
}
}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 401,
});
});
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000001',
auth: {basic: 'user'},
httpStatus: 403,
});
});
it('rejects deleting a measurement for a write user who did not create this measurement', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000003',
auth: {basic: 'janedoe'},
httpStatus: 403,
});
});
it('accepts deleting a measurement of another user for a maintain/admin user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
res: {status: 'OK'}
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000h00000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404,
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/000000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404,
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/measurement/800000000000000000000001',
httpStatus: 401,
});
});
});
describe('PUT /measurement/restore/{id}', () => {
it('sets the status', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/restore/800000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200,
req: {}
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
MeasurementModel.findById('800000000000000000000004').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.new);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/restore/800000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
log: {
collection: 'measurements',
dataAdd: {
status: 0
}
}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/restore/800000000000000000000004',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('rejects a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/restore/800000000000000000000004',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('returns 404 for an unknown sample', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/restore/000000000000000000000004',
auth: {basic: 'admin'},
httpStatus: 404,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/restore/800000000000000000000004',
httpStatus: 401,
req: {}
});
});
});
describe('PUT /measurement/validate/{id}', () => {
it('sets the status', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/validate/800000000000000000000003',
auth: {basic: 'admin'},
httpStatus: 200,
req: {}
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
MeasurementModel.findById('800000000000000000000003').lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.property('status',globals.status.validated);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/validate/800000000000000000000003',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
log: {
collection: 'measurements',
dataAdd: {
status: 10
}
}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/validate/800000000000000000000003',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('rejects a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/validate/800000000000000000000003',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('returns 404 for an unknown sample', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/validate/000000000000000000000003',
auth: {basic: 'admin'},
httpStatus: 404,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/measurement/validate/800000000000000000000003',
httpStatus: 401,
req: {}
});
});
});
describe('POST /measurement/new', () => {
it('returns the right measurement', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('sample_id', '400000000000000000000001');
should(res.body).have.property('measurement_template', '300000000000000000000002');
should(res.body).have.property('values');
should(res.body.values).have.property('weight %', 0.8);
should(res.body.values).have.property('standard deviation', 0.1);
done();
});
});
it('stores the measurement', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
}).end((err, res) => {
if (err) return done(err);
MeasurementModel.findById(res.body._id).lean().exec((err, data: any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'sample_id', 'values', 'measurement_template', 'status', '__v');
should(data.sample_id.toString()).be.eql('400000000000000000000001');
should(data.measurement_template.toString()).be.eql('300000000000000000000002');
should(data).have.property('status', 0);
should(data).have.property('values');
should(data.values).have.property('weight %', 0.8);
should(data.values).have.property('standard deviation', 0.1);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'},
log: {
collection: 'measurements',
dataAdd: {
status: 0
}
}
});
});
it('rejects an invalid sample id', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000h00000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'},
res: {status: 'Invalid body format', details: '"sample_id" with value "400000000000h00000000001" fails to match the required pattern: /[0-9a-f]{24}/'}
});
});
it('rejects a sample id not available', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '000000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'},
res: {status: 'Sample id not available'}
});
});
it('rejects an invalid measurement_template id', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '30000000000h000000000002'},
res: {status: 'Invalid body format', details: '"measurement_template" with value "30000000000h000000000002" fails to match the required pattern: /[0-9a-f]{24}/'}
});
});
it('rejects a measurement_template not available', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '000000000000000000000002'},
res: {status: 'Measurement template not available'}
});
});
it('rejects not specified values', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1, xx: 44}, measurement_template: '300000000000000000000002'},
res: {status: 'Invalid body format', details: '"xx" is not allowed'}
});
});
it('accepts missing values', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8}, measurement_template: '300000000000000000000002'}
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('sample_id', '400000000000000000000001');
should(res.body).have.property('measurement_template', '300000000000000000000002');
should(res.body).have.property('values');
should(res.body.values).have.property('weight %', 0.8);
should(res.body.values).have.property('standard deviation', null);
done();
});
});
it('rejects no values', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {}, measurement_template: '300000000000000000000002'},
res: {status: 'At least one value is required'}
});
});
it('rejects a value not in the value range', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {val2: 5}, measurement_template: '300000000000000000000004'},
res: {status: 'Invalid body format', details: '"val2" must be one of [1, 2, 3, 4, null]'}
});
});
it('rejects a value below minimum range', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {'weight %': -1, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'},
res: {status: 'Invalid body format', details: '"weight %" must be larger than or equal to 0'}
});
});
it('rejects a value above maximum range', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 2}, measurement_template: '300000000000000000000002'},
res: {status: 'Invalid body format', details: '"standard deviation" must be less than or equal to 0.5'}
});
});
it('rejects a missing sample id', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'},
res: {status: 'Invalid body format', details: '"sample_id" is required'}
});
});
it('rejects a missing measurement_template', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}},
res: {status: 'Invalid body format', details: '"measurement_template" is required'}
});
});
it('rejects adding a measurement to the sample of another user for a write user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {sample_id: '400000000000000000000003', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
});
});
it('accepts adding a measurement to the sample of another user for a maintain/admin user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.only.keys('_id', 'sample_id', 'values', 'measurement_template');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('sample_id', '400000000000000000000001');
should(res.body).have.property('measurement_template', '300000000000000000000002');
should(res.body).have.property('values');
should(res.body.values).have.property('weight %', 0.8);
should(res.body.values).have.property('standard deviation', 0.1);
done();
});
});
it('rejects an old version of a measurement template', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 400,
req: {sample_id: '400000000000000000000001', values: {val1: 2}, measurement_template: '300000000000000000000003'},
res: {status: 'Old template version not allowed'}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {key: 'janedoe'},
httpStatus: 401,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
});
});
it('rejects requests from a read user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
auth: {basic: 'user'},
httpStatus: 403,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/measurement/new',
httpStatus: 401,
req: {sample_id: '400000000000000000000001', values: {'weight %': 0.8, 'standard deviation': 0.1}, measurement_template: '300000000000000000000002'}
});
});
});
});

169
src/routes/measurement.ts Normal file
View File

@ -0,0 +1,169 @@
import express from 'express';
import _ from 'lodash';
import MeasurementModel from '../models/measurement';
import MeasurementTemplateModel from '../models/measurement_template';
import SampleModel from '../models/sample';
import MeasurementValidate from './validate/measurement';
import IdValidate from './validate/id';
import res400 from './validate/res400';
import ParametersValidate from './validate/parameters';
import globals from '../globals';
import db from '../db';
const router = express.Router();
router.get('/measurement/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
MeasurementModel.findById(req.params.id).lean().exec((err, data: any) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
if (data.status ===globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted measurements only available for maintain/admin
res.json(MeasurementValidate.output(data));
});
});
router.put('/measurement/' + IdValidate.parameter(), async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: measurement} = MeasurementValidate.input(req.body, 'change');
if (error) return res400(error, res);
const data = await MeasurementModel.findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
if (data instanceof Error) return;
if (!data) {
return res.status(404).json({status: 'Not found'});
}
if (data.status === globals.status.deleted) {
return res.status(403).json({status: 'Forbidden'});
}
// add properties needed for sampleIdCheck
measurement.measurement_template = data.measurement_template;
measurement.sample_id = data.sample_id;
if (!await sampleIdCheck(measurement, req, res, next)) return;
// check for changes
if (measurement.values) { // fill not changed values from database
measurement.values = _.assign({}, data.values, measurement.values);
if (!_.isEqual(measurement.values, data.values)) {
measurement.status = globals.status.new; // set status to new
}
}
if (!await templateCheck(measurement, 'change', res, next)) return;
await MeasurementModel.findByIdAndUpdate(req.params.id, measurement, {new: true}).log(req).lean().exec((err, data) => {
if (err) return next(err);
res.json(MeasurementValidate.output(data));
});
});
router.delete('/measurement/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
MeasurementModel.findById(req.params.id).lean().exec(async (err, data) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
if (!await sampleIdCheck(data, req, res, next)) return;
await MeasurementModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => {
if (err) return next(err);
return res.json({status: 'OK'});
});
});
});
router.put('/measurement/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
setStatus(globals.status.new, req, res, next);
});
router.put('/measurement/validate/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
setStatus(globals.status.validated, req, res, next);
});
router.post('/measurement/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
const {error, value: measurement} = MeasurementValidate.input(req.body, 'new');
if (error) return res400(error, res);
if (!await sampleIdCheck(measurement, req, res, next)) return;
measurement.values = await templateCheck(measurement, 'new', res, next);
if (!measurement.values) return;
measurement.status = 0;
await new MeasurementModel(measurement).save((err, data) => {
if (err) return next(err);
db.log(req, 'measurements', {_id: data._id}, data.toObject());
res.json(MeasurementValidate.output(data.toObject()));
});
});
module.exports = router;
async function sampleIdCheck (measurement, req, res, next) { // validate sample_id, returns false if invalid or user has no access for this sample
const sampleData = await SampleModel.findById(measurement.sample_id).lean().exec().catch(err => {next(err); return false;}) as any;
if (!sampleData) { // sample_id not found
res.status(400).json({status: 'Sample id not available'});
return false
}
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return false; // sample does not belong to user
return true;
}
async function templateCheck (measurement, param, res, next) { // validate measurement_template and values, returns values, true if values are {} or false if invalid, param for 'new'/'change'
const templateData = await MeasurementTemplateModel.findById(measurement.measurement_template).lean().exec().catch(err => {next(err); return false;}) as any;
if (!templateData) { // template not found
res.status(400).json({status: 'Measurement template not available'});
return false
}
// fill not given values for new measurements
if (param === 'new') {
// get all template versions and check if given is latest
const templateVersions = await MeasurementTemplateModel.find({first_id: templateData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any;
if (templateVersions instanceof Error) return false;
if (measurement.measurement_template !== templateVersions[0]._id.toString()) { // template not latest
res.status(400).json({status: 'Old template version not allowed'});
return false;
}
if (Object.keys(measurement.values).length === 0) {
res.status(400).json({status: 'At least one value is required'});
return false
}
const fillValues = {}; // initialize not given values with null
templateData.parameters.forEach(parameter => {
fillValues[parameter.name] = null;
});
measurement.values = _.assign({}, fillValues, measurement.values);
}
// validate values
const {error, value} = ParametersValidate.input(measurement.values, templateData.parameters, 'null');
if (error) {res400(error, res); return false;}
return value || true;
}
function setStatus (status, req, res, next) { // set measurement status
MeasurementModel.findByIdAndUpdate(req.params.id, {status: status}).log(req).lean().exec((err, data) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
res.json({status: 'OK'});
});
}

View File

@ -1,19 +1,256 @@
import supertest from 'supertest';
import TestHelper from "../test/helper";
import should from 'should/as-function';
import db from '../db';
let server = supertest.agent('http://localhost:3000');
describe('/', () => {
let server;
before(done => TestHelper.before(done));
beforeEach(done => server = TestHelper.beforeEach(server, done));
afterEach(done => TestHelper.afterEach(server, done));
after(done => TestHelper.after(done));
describe('Testing /', () => {
it('returns the message object', done => {
server
.get('/')
.expect('Content-type', /json/)
.expect(200)
.end(function(err, res) {
should(res.statusCode).equal(200);
should(res.body).be.eql({message: 'API server up and running!'});
describe('GET /', () => {
it('returns the root message', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/',
httpStatus: 200,
res: {status: 'API server up and running!'}
});
});
});
describe('GET /changelog/{timestamp}/{page}/{pagesize}', () => {
it('returns the first page', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/0/2',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.lengthOf(2);
should(res.body[0].date).be.eql('1979-07-28T06:04:51.000Z');
should(res.body[1].date).be.eql('1979-07-28T06:04:50.000Z');
should(res.body).matchEach(log => {
should(log).have.only.keys('date', 'action', 'collection', 'conditions', 'data');
should(log).have.property('action', 'PUT /sample/400000000000000000000001');
should(log).have.property('collection', 'samples');
should(log).have.property('conditions', {_id: '400000000000000000000001'});
should(log).have.property('data', {type: 'part', status: 0});
});
done();
});
});
it('returns another page', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/1/2',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.lengthOf(1);
should(res.body[0].date).be.eql('1979-07-28T06:04:49.000Z');
should(res.body).matchEach(log => {
should(log).have.only.keys('date', 'action', 'collection', 'conditions', 'data');
should(log).have.property('action', 'PUT /sample/400000000000000000000001');
should(log).have.property('collection', 'samples');
should(log).have.property('conditions', {_id: '400000000000000000000001'});
should(log).have.property('data', {type: 'part', status: 0});
done();
});
});
});
it('returns an empty array for a page with no results', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.lengthOf(0);
done();
});
});
it('rejects timestamps pre unix epoch', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1879-07-28T06:04:51.000Z/10/2',
auth: {basic: 'admin'},
httpStatus: 400,
res: {status: 'Invalid body format', details: '"timestamp" must be larger than or equal to "1970-01-01T00:00:00.000Z"'}
});
});
it('rejects invalid timestamps', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-14-28T06:04:51.000Z/10/2',
auth: {basic: 'admin'},
httpStatus: 400,
res: {status: 'Invalid body format', details: '"timestamp" must be in ISO 8601 date format'}
});
});
it('rejects negative page numbers', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/-10/2',
auth: {basic: 'admin'},
httpStatus: 400,
res: {status: 'Invalid body format', details: '"page" must be larger than or equal to 0'}
});
});
it('rejects negative pagesizes', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/10/-2',
auth: {basic: 'admin'},
httpStatus: 400,
res: {status: 'Invalid body format', details: '"pagesize" must be larger than or equal to 0'}
});
});
it('rejects request from a write user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects requests from an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/changelog/1979-07-28T06:04:51.000Z/10/2',
httpStatus: 401
});
});
});
describe('Unknown routes', () => {
it('return a 404 message', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/unknownroute',
httpStatus: 404
});
});
});
describe('An unauthorized request', () => {
it('returns a 401 message', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/authorized',
httpStatus: 401
});
});
it('does not work with correct username', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/authorized',
auth: {basic: {name: 'admin', pass: 'Abc123!!'}},
httpStatus: 401
});
});
it('does not work with incorrect username', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/authorized',
auth: {basic: {name: 'adminxx', pass: 'Abc123!!'}},
httpStatus: 401
});
});
});
describe('An authorized request', () => {
it('works with an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/authorized',
auth: {key: 'admin'},
httpStatus: 200,
res: {status: 'Authorization successful', method: 'key'}
});
});
it('works with basic auth', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/authorized',
auth: {basic: 'admin'},
httpStatus: 200,
res: {status: 'Authorization successful', method: 'basic'}
});
});
});
describe('An invalid JSON body', () => {
it('is rejected', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/',
httpStatus: 400,
reqType: 'json',
req: '{"xxx"}',
res: {status: 'Invalid JSON body'}
});
});
});
describe('A not connected database', () => { // RUN AS LAST OR RECONNECT DATABASE!!
it('resolves to an 500 error', done => {
db.disconnect(() => {
TestHelper.request(server, done, {
method: 'get',
url: '/',
httpStatus: 500
});
});
});
});
});
describe('The /api/{url} redirect', () => {
let server;
let counter = 0; // count number of current test method
before(done => {
process.env.port = '2999';
db.connect('test', done);
});
beforeEach(done => {
process.env.NODE_ENV = counter === 1 ? 'production' : 'test';
counter ++;
server = TestHelper.beforeEach(server, done);
});
afterEach(done => TestHelper.afterEach(server, done));
after(done => TestHelper.after(done));
it('returns the right method', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/api/authorized',
auth: {basic: 'admin'},
httpStatus: 200,
res: {status: 'Authorization successful', method: 'basic'}
});
});
it('is disabled in production', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/api/authorized',
auth: {basic: 'admin'},
httpStatus: 404
});
});
});

View File

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

1930
src/routes/sample.spec.ts Normal file

File diff suppressed because it is too large Load Diff

780
src/routes/sample.ts Normal file
View File

@ -0,0 +1,780 @@
import express from 'express';
import _ from 'lodash';
import SampleValidate from './validate/sample';
import NoteFieldValidate from './validate/note_field';
import res400 from './validate/res400';
import SampleModel from '../models/sample'
import MeasurementModel from '../models/measurement';
import MeasurementTemplateModel from '../models/measurement_template';
import MaterialModel from '../models/material';
import NoteModel from '../models/note';
import NoteFieldModel from '../models/note_field';
import IdValidate from './validate/id';
import mongoose from 'mongoose';
import ConditionTemplateModel from '../models/condition_template';
import ParametersValidate from './validate/parameters';
import globals from '../globals';
import db from '../db';
import csv from '../helpers/csv';
const router = express.Router();
// TODO: check added filter
// TODO: return total number of pages -> use facet
// TODO: use query pointer
// TODO: convert filter value to number according to table model
// TODO: validation for filter parameters
// TODO: location/device sort/filter
router.get('/samples', async (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
const {error, value: filters} = SampleValidate.query(req.query);
if (error) return res400(error, res);
// TODO: find a better place for these
const sampleKeys = ['_id', 'color', 'number', 'type', 'batch', 'added', 'condition', 'material_id', 'note_id', 'user_id'];
// evaluate sort parameter from 'color-asc' to ['color', 1]
filters.sort = filters.sort.split('-');
filters.sort[0] = filters.sort[0] === 'added' ? '_id' : filters.sort[0]; // route added sorting criteria to _id
filters.sort[1] = filters.sort[1] === 'desc' ? -1 : 1;
if (!filters['to-page']) { // set to-page default
filters['to-page'] = 0;
}
const addedFilter = filters.filters.find(e => e.field === 'added');
if (addedFilter) { // convert added filter to object id
filters.filters.splice(filters.filters.findIndex(e => e.field === 'added'), 1);
if (addedFilter.mode === 'in') {
const v = []; // query value
addedFilter.values.forEach(value => {
const date = [new Date(value).setHours(0,0,0,0), new Date(value).setHours(23,59,59,999)];
v.push({$and: [{ _id: { '$gte': dateToOId(date[0])}}, { _id: { '$lte': dateToOId(date[1])}}]});
});
filters.filters.push({mode: 'or', field: '_id', values: v});
}
else if (addedFilter.mode === 'nin') {
addedFilter.values = addedFilter.values.sort();
const v = []; // query value
for (let i = 0; i <= addedFilter.values.length; i ++) {
v[i] = {$and: []};
if (i > 0) {
const date = new Date(addedFilter.values[i - 1]).setHours(23,59,59,999);
v[i].$and.push({ _id: { '$gt': dateToOId(date)}}) ;
}
if (i < addedFilter.values.length) {
const date = new Date(addedFilter.values[i]).setHours(0,0,0,0);
v[i].$and.push({ _id: { '$lt': dateToOId(date)}}) ;
}
}
filters.filters.push({mode: 'or', field: '_id', values: v});
}
else {
// start and end of day
const date = [new Date(addedFilter.values[0]).setHours(0,0,0,0), new Date(addedFilter.values[0]).setHours(23,59,59,999)];
if (addedFilter.mode === 'lt') { // lt start
filters.filters.push({mode: 'lt', field: '_id', values: [dateToOId(date[0])]});
}
if (addedFilter.mode === 'eq' || addedFilter.mode === 'lte') { // lte end
filters.filters.push({mode: 'lte', field: '_id', values: [dateToOId(date[1])]});
}
if (addedFilter.mode === 'gt') { // gt end
filters.filters.push({mode: 'gt', field: '_id', values: [dateToOId(date[1])]});
}
if (addedFilter.mode === 'eq' || addedFilter.mode === 'gte') { // gte start
filters.filters.push({mode: 'gte', field: '_id', values: [dateToOId(date[0])]});
}
if (addedFilter.mode === 'ne') {
filters.filters.push({mode: 'or', field: '_id', values: [{ _id: { '$lt': dateToOId(date[0])}}, { _id: { '$gt': dateToOId(date[1])}}]});
}
}
}
const sortFilterKeys = filters.filters.map(e => e.field);
let collection;
const query = [];
let queryPtr = query;
queryPtr.push({$match: {$and: []}});
if (filters.sort[0].indexOf('measurements.') >= 0) { // sorting with measurements as starting collection
collection = MeasurementModel;
const [,measurementName, measurementParam] = filters.sort[0].split('.');
const measurementTemplate = await MeasurementTemplateModel.findOne({name: measurementName}).lean().exec().catch(err => {next(err);});
if (measurementTemplate instanceof Error) return;
if (!measurementTemplate) {
return res.status(400).json({status: 'Invalid body format', details: filters.sort[0] + ' not found'});
}
let sortStartValue = null;
if (filters['from-id']) { // from-id specified, fetch values for sorting
const fromSample = await MeasurementModel.findOne({sample_id: mongoose.Types.ObjectId(filters['from-id'])}).lean().exec().catch(err => {next(err);}); // TODO: what if more than one measurement for sample?
if (fromSample instanceof Error) return;
if (!fromSample) {
return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'});
}
sortStartValue = fromSample.values[measurementParam];
}
queryPtr[0].$match.$and.push({measurement_template: mongoose.Types.ObjectId(measurementTemplate._id)}); // find measurements to sort
if (filters.filters.find(e => e.field === filters.sort[0])) { // sorted measurement should also be filtered
queryPtr[0].$match.$and.push(...filterQueries(filters.filters.filter(e => e.field === filters.sort[0]).map(e => {e.field = 'values.' + e.field.split('.')[2]; return e; })));
}
queryPtr.push(
...sortQuery(filters, ['values.' + measurementParam, 'sample_id'], sortStartValue), // sort measurements
{$replaceRoot: {newRoot: {measurement: '$$ROOT'}}}, // fetch samples and restructure them to fit sample structure
{$lookup: {from: 'samples', localField: 'measurement.sample_id', foreignField: '_id', as: 'sample'}},
{$match: statusQuery(filters, 'sample.status')}, // filter out wrong status once samples were added
{$addFields: {['sample.' + measurementName]: '$measurement.values'}}, // more restructuring
{$replaceRoot: {newRoot: {$mergeObjects: [{$arrayElemAt: ['$sample', 0]}, {}]}}}
);
}
else { // sorting with samples as starting collection
collection = SampleModel;
queryPtr[0].$match.$and.push(statusQuery(filters, 'status'));
if (sampleKeys.indexOf(filters.sort[0]) >= 0) { // sorting for sample keys
let sortStartValue = null;
if (filters['from-id']) { // from-id specified
const fromSample = await SampleModel.findById(filters['from-id']).lean().exec().catch(err => {
next(err);
});
if (fromSample instanceof Error) return;
if (!fromSample) {
return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'});
}
sortStartValue = fromSample[filters.sort[0]];
}
queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue));
}
else { // add sort key to list to add field later
sortFilterKeys.push(filters.sort[0]);
}
}
addFilterQueries(queryPtr, filters.filters.filter(e => sampleKeys.indexOf(e.field) >= 0)); // sample filters
let materialQuery = []; // put material query together separate first to reuse for first-id
let materialAdded = false;
if (sortFilterKeys.find(e => /material\./.test(e))) { // add material fields
materialAdded = true;
materialQuery.push( // add material properties
{$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}}, // TODO: project out unnecessary fields
{$addFields: {material: {$arrayElemAt: ['$material', 0]}}}
);
const baseMFilters = sortFilterKeys.filter(e => /material\./.test(e)).filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) < 0);
addFilterQueries(materialQuery, filters.filters.filter(e => baseMFilters.indexOf(e.field) >= 0)); // base material filters
if (sortFilterKeys.find(e => e === 'material.supplier')) { // add supplier if needed
materialQuery.push(
{$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}},
{$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
);
}
if (sortFilterKeys.find(e => e === 'material.group')) { // add group if needed
materialQuery.push(
{$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }},
{$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}
);
}
if (sortFilterKeys.find(e => e === 'material.number')) { // add material number if needed
materialQuery.push(
{$addFields: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]}}}
);
}
const specialMFilters = sortFilterKeys.filter(e => /material\./.test(e)).filter(e => ['material.supplier', 'material.group', 'material.number'].indexOf(e) >= 0);
addFilterQueries(materialQuery, filters.filters.filter(e => specialMFilters.indexOf(e.field) >= 0)); // base material filters
queryPtr.push(...materialQuery);
if (/material\./.test(filters.sort[0])) { // sort by material key
let sortStartValue = null;
if (filters['from-id']) { // from-id specified
const fromSample = await SampleModel.aggregate([{$match: {_id: mongoose.Types.ObjectId(filters['from-id'])}}, ...materialQuery]).exec().catch(err => {next(err);});
if (fromSample instanceof Error) return;
if (!fromSample) {
return res.status(400).json({status: 'Invalid body format', details: 'from-id not found'});
}
sortStartValue = fromSample[filters.sort[0]];
}
queryPtr.push(...sortQuery(filters, [filters.sort[0], '_id'], sortStartValue));
}
}
const measurementFilterFields = _.uniq(sortFilterKeys.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters
if (sortFilterKeys.find(e => /measurements\./.test(e))) { // add measurement fields
const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFilterFields}}).lean().exec().catch(err => {next(err);});
if (measurementTemplates instanceof Error) return;
if (measurementTemplates.length < measurementFilterFields.length) {
return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'});
}
queryPtr.push({$lookup: {
from: 'measurements', let: {sId: '$_id'},
pipeline: [{$match: {$expr: {$and: [{$eq: ['$sample_id', '$$sId']}, {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}]}}}],
as: 'measurements'
}});
measurementTemplates.forEach(template => {
queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values
vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}},
in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']}
}}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}});
});
addFilterQueries(queryPtr, filters.filters
.filter(e => sortFilterKeys.filter(e => /measurements\./.test(e)).indexOf(e.field) >= 0)
.map(e => {e.field = e.field.replace('measurements.', ''); return e; })
); // measurement filters
}
if (!filters.fields.find(e => /spectrum\./.test(e)) && !filters['from-id']) { // count total number of items before $skip and $limit, only works when from-id is not specified and spectra are not included
queryPtr.push({$facet: {count: [{$count: 'count'}], samples: []}});
queryPtr = queryPtr[queryPtr.length - 1].$facet.samples; // add rest of aggregation pipeline into $facet
}
// paging
if (filters['to-page']) {
queryPtr.push({$skip: Math.abs(filters['to-page'] + Number(filters['to-page'] < 0)) * filters['page-size'] + Number(filters['to-page'] < 0)}) // number to skip, if going back pages, one page has to be skipped less but on sample more
}
if (filters['page-size']) {
queryPtr.push({$limit: filters['page-size']});
}
const fieldsToAdd = filters.fields.filter(e => // fields to add
sortFilterKeys.indexOf(e) < 0 // field was not in filter
&& e !== filters.sort[0] // field was not in sort
);
if (fieldsToAdd.find(e => /material\./.test(e)) && !materialAdded) { // add material, was not added already
queryPtr.push(
{$lookup: {from: 'materials', localField: 'material_id', foreignField: '_id', as: 'material'}},
{$addFields: {material: { $arrayElemAt: ['$material', 0]}}}
);
}
if (fieldsToAdd.indexOf('material.supplier') >= 0) { // add supplier if needed
queryPtr.push(
{$lookup: { from: 'material_suppliers', localField: 'material.supplier_id', foreignField: '_id', as: 'material.supplier'}},
{$addFields: {'material.supplier': {$arrayElemAt: ['$material.supplier.name', 0]}}}
);
}
if (fieldsToAdd.indexOf('material.group') >= 0) { // add group if needed
queryPtr.push(
{$lookup: { from: 'material_groups', localField: 'material.group_id', foreignField: '_id', as: 'material.group' }},
{$addFields: {'material.group': { $arrayElemAt: ['$material.group.name', 0]}}}
);
}
if (fieldsToAdd.indexOf('material.number') >= 0) { // add material number if needed
queryPtr.push(
{$addFields: {'material.number': { $arrayElemAt: ['$material.numbers.number', {$indexOfArray: ['$material.numbers.color', '$color']}]}}}
);
}
let measurementFieldsFields: string[] = _.uniq(fieldsToAdd.filter(e => /measurements\./.test(e)).map(e => e.split('.')[1])); // filter measurement names and remove duplicates from parameters
if (fieldsToAdd.find(e => /measurements\./.test(e))) { // add measurement fields
const measurementTemplates = await MeasurementTemplateModel.find({name: {$in: measurementFieldsFields}}).lean().exec().catch(err => {next(err);});
if (measurementTemplates instanceof Error) return;
if (measurementTemplates.length < measurementFieldsFields.length) {
return res.status(400).json({status: 'Invalid body format', details: 'Measurement key not found'});
}
if (fieldsToAdd.find(e => /spectrum\./.test(e))) { // use different lookup methods with and without spectrum for the best performance
queryPtr.push({$lookup: {from: 'measurements', localField: '_id', foreignField: 'sample_id', as: 'measurements'}});
}
else {
queryPtr.push({$lookup: {
from: 'measurements', let: {sId: '$_id'},
pipeline: [{$match: {$expr: {$and: [{$eq: ['$sample_id', '$$sId']}, {$in: ['$measurement_template', measurementTemplates.map(e => mongoose.Types.ObjectId(e._id))]}]}}}],
as: 'measurements'
}});
}
measurementTemplates.filter(e => e.name !== 'spectrum').forEach(template => { // TODO: hard coded dpt for special treatment, change later
queryPtr.push({$addFields: {[template.name]: {$let: { // add measurements as property [template.name], if one result, array is reduced to direct values
vars: {arr: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', mongoose.Types.ObjectId(template._id)]}}}},
in:{$cond: [{$lte: [{$size: '$$arr'}, 1]}, {$arrayElemAt: ['$$arr', 0]}, '$$arr']}
}}}}, {$addFields: {[template.name]: {$cond: ['$' + template.name + '.values', '$' + template.name + '.values', template.parameters.reduce((s, e) => {s[e.name] = null; return s;}, {})]}}});
});
if (measurementFieldsFields.find(e => e === 'spectrum')) { // TODO: remove hardcoded as well
queryPtr.push(
{$addFields: {spectrum: {$filter: {input: '$measurements', cond: {$eq: ['$$this.measurement_template', measurementTemplates.filter(e => e.name === 'spectrum')[0]._id]}}}}},
{$addFields: {spectrum: '$spectrum.values'}},
{$unwind: '$spectrum'}
);
}
// queryPtr.push({$unset: 'measurements'});
queryPtr.push({$project: {measurements: 0}});
}
const projection = filters.fields.map(e => e.replace('measurements.', '')).reduce((s, e) => {s[e] = true; return s; }, {});
if (filters.fields.indexOf('added') >= 0) { // add added date
// projection.added = {$toDate: '$_id'};
// projection.added = { $convert: { input: '$_id', to: "date" } } // TODO: upgrade MongoDB version or find alternative
}
if (filters.fields.indexOf('_id') < 0 && filters.fields.indexOf('added') < 0) { // disable _id explicitly
projection._id = false;
}
queryPtr.push({$project: projection});
if (!fieldsToAdd.find(e => /spectrum\./.test(e))) { // use streaming when including spectrum files
collection.aggregate(query).exec((err, data) => {
if (err) return next(err);
if (data[0].count) {
res.header('x-total-items', data[0].count.length > 0 ? data[0].count[0].count : 0);
res.header('Access-Control-Expose-Headers', 'x-total-items');
data = data[0].samples;
}
if (filters.fields.indexOf('added') >= 0) { // add added date
data.map(e => {
e.added = e._id.getTimestamp();
if (filters.fields.indexOf('_id') < 0) {
delete e._id;
}
return e
});
}
if (filters['to-page'] < 0) {
data.reverse();
}
const measurementFields = _.uniq([filters.sort[0].split('.')[1], ...measurementFilterFields, ...measurementFieldsFields]);
if (filters.csv) { // output as csv
csv(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields))), (err, data) => {
if (err) return next(err);
res.set('Content-Type', 'text/csv');
res.send(data);
});
}
else {
res.json(_.compact(data.map(e => SampleValidate.output(e, 'refs', measurementFields)))); // validate all and filter null values from validation errors
}
});
}
else {
res.writeHead(200, {'Content-Type': 'application/json; charset=utf-8'});
res.write('[');
let count = 0;
const stream = collection.aggregate(query).cursor().exec();
stream.on('data', data => {
if (filters.fields.indexOf('added') >= 0) { // add added date
data.added = data._id.getTimestamp();
if (filters.fields.indexOf('_id') < 0) {
delete data._id;
}
}
res.write((count === 0 ? '' : ',\n') + JSON.stringify(data)); count ++;
});
stream.on('close', () => {
res.write(']');
res.end();
});
}
});
router.get('/samples/:state(new|deleted)', (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
SampleModel.find({status: globals.status[req.params.state]}).lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => SampleValidate.output(e)))); // validate all and filter null values from validation errors
});
});
router.get('/samples/count', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
SampleModel.estimatedDocumentCount((err, data) => {
if (err) return next(err);
res.json({count: data});
});
});
router.get('/sample/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
SampleModel.findById(req.params.id).populate('material_id').populate('user_id', 'name').populate('note_id').exec(async (err, sampleData: any) => {
if (err) return next(err);
if (sampleData) {
await sampleData.populate('material_id.group_id').populate('material_id.supplier_id').execPopulate().catch(err => next(err));
if (sampleData instanceof Error) return;
sampleData = sampleData.toObject();
if (sampleData.status === globals.status.deleted && !req.auth(res, ['maintain', 'admin'], 'all')) return; // deleted samples only available for maintain/admin
sampleData.material = sampleData.material_id; // map data to right keys
sampleData.material.group = sampleData.material.group_id.name;
sampleData.material.supplier = sampleData.material.supplier_id.name;
sampleData.user = sampleData.user_id.name;
sampleData.notes = sampleData.note_id ? sampleData.note_id : {};
MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id), status: {$ne: globals.status.deleted}}).lean().exec((err, data) => {
sampleData.measurements = data;
res.json(SampleValidate.output(sampleData, 'details'));
});
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
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, 'change');
if (error) return res400(error, res);
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'});
}
if (sampleData.status === globals.status.deleted) {
return res.status(403).json({status: 'Forbidden'});
}
// only maintain and admin are allowed to edit other user's data
if (sampleData.user_id.toString() !== req.authDetails.id && !req.auth(res, ['maintain', 'admin'], 'basic')) return;
if (sample.hasOwnProperty('material_id')) {
if (!await materialCheck(sample, res, next)) return;
}
else if (sample.hasOwnProperty('color')) {
if (!await materialCheck(sample, res, next, sampleData.material_id)) return;
}
if (sample.hasOwnProperty('condition') && !(_.isEmpty(sample.condition) && _.isEmpty(sampleData.condition))) { // do not execute check if condition is and was empty
if (!await conditionCheck(sample.condition, 'change', res, next, sampleData.condition.condition_template.toString() !== sample.condition.condition_template)) return;
}
if (sample.hasOwnProperty('notes')) {
let newNotes = true;
if (sampleData.note_id !== null) { // old notes data exists
const data = await NoteModel.findById(sampleData.note_id).lean().exec().catch(err => {next(err);}) as any;
if (data instanceof Error) return;
newNotes = !_.isEqual(_.pick(IdValidate.stringify(data), _.keys(sample.notes)), sample.notes); // check if notes were changed
if (newNotes) {
if (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1, req);
}
await NoteModel.findByIdAndDelete(sampleData.note_id).log(req).lean().exec(err => { // delete old notes
if (err) return console.error(err);
});
}
}
if (_.keys(sample.notes).length > 0 && newNotes) { // save new notes
if (!await sampleRefCheck(sample, res, next)) return;
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields
customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
}
let data = await new NoteModel(sample.notes).save().catch(err => { return next(err)}); // save new notes
db.log(req, 'notes', {_id: data._id}, data.toObject());
delete sample.notes;
sample.note_id = data._id;
}
}
// check for changes
if (!_.isEqual(_.pick(IdValidate.stringify(sampleData), _.keys(sample)), _.omit(sample, ['notes']))) {
sample.status = globals.status.new;
}
await SampleModel.findByIdAndUpdate(req.params.id, sample, {new: true}).log(req).lean().exec((err, data: any) => {
if (err) return next(err);
res.json(SampleValidate.output(data));
});
});
});
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;
await SampleModel.findByIdAndUpdate(req.params.id, {status:globals.status.deleted}).log(req).lean().exec(err => { // set sample status
if (err) return next(err);
// set status of associated measurements also to deleted
MeasurementModel.updateMany({sample_id: mongoose.Types.ObjectId(req.params.id)}, {status: -1}).log(req).lean().exec(err => {
if (err) return next(err);
if (sampleData.note_id !== null) { // handle notes
NoteModel.findById(sampleData.note_id).lean().exec((err, data: any) => { // find notes to update note_fields
if (err) return next(err);
if (data.hasOwnProperty('custom_fields')) { // update note_fields
customFieldsChange(Object.keys(data.custom_fields), -1, req);
}
res.json({status: 'OK'});
});
}
else {
res.json({status: 'OK'});
}
});
});
});
});
router.put('/sample/restore/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.new}).log(req).lean().exec((err, data) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
res.json({status: 'OK'});
});
});
router.put('/sample/validate/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
SampleModel.findById(req.params.id).lean().exec((err, data: any) => {
if (err) return next(err);
if (!data) {
return res.status(404).json({status: 'Not found'});
}
if (Object.keys(data.condition).length === 0) {
return res.status(400).json({status: 'Sample without condition cannot be valid'});
}
MeasurementModel.find({sample_id: mongoose.Types.ObjectId(req.params.id)}).lean().exec((err, data) => {
if (err) return next(err);
if (data.length === 0) {
return res.status(400).json({status: 'Sample without measurements cannot be valid'});
}
SampleModel.findByIdAndUpdate(req.params.id, {status: globals.status.validated}).log(req).lean().exec(err => {
if (err) return next(err);
res.json({status: 'OK'});
});
});
});
});
router.post('/sample/new', async (req, res, next) => {
if (!req.auth(res, ['write', 'maintain', 'dev', 'admin'], 'basic')) return;
if (!req.body.hasOwnProperty('condition')) { // add empty condition if not specified
req.body.condition = {};
}
const {error, value: sample} = SampleValidate.input(req.body, 'new' + (req.authDetails.level === 'admin' ? '-admin' : ''));
if (error) return res400(error, res);
if (!await materialCheck(sample, res, next)) return;
if (!await sampleRefCheck(sample, res, next)) return;
if (sample.notes.hasOwnProperty('custom_fields') && Object.keys(sample.notes.custom_fields).length > 0) { // new custom_fields
customFieldsChange(Object.keys(sample.notes.custom_fields), 1, req);
}
if (!_.isEmpty(sample.condition)) { // do not execute check if condition is empty
if (!await conditionCheck(sample.condition, 'change', res, next)) return;
}
sample.status = globals.status.new; // set status to new
if (sample.hasOwnProperty('number')) {
if (!await numberCheck(sample, res, next)) return;
}
else {
sample.number = await numberGenerate(sample, req, res, next);
}
if (!sample.number) return;
await new NoteModel(sample.notes).save((err, data) => { // save notes
if (err) return next(err);
db.log(req, 'notes', {_id: data._id}, data.toObject());
delete sample.notes;
sample.note_id = data._id;
sample.user_id = req.authDetails.id;
new SampleModel(sample).save((err, data) => {
if (err) return next(err);
db.log(req, 'samples', {_id: data._id}, data.toObject());
res.json(SampleValidate.output(data.toObject()));
});
});
});
router.get('/sample/notes/fields', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'all')) return;
NoteFieldModel.find({}).lean().exec((err, data) => {
if (err) return next(err);
res.json(_.compact(data.map(e => NoteFieldValidate.output(e)))); // validate all and filter null values from validation errors
})
});
module.exports = router;
async function numberGenerate (sample, req, res, next) { // generate number in format Location32, returns false on error
const sampleData = await SampleModel
// .findOne({number: new RegExp('^' + req.authDetails.location + '[0-9]+$', 'm')})
// .sort({number: -1})
// .lean()
.aggregate([
{$match: {number: new RegExp('^' + 'Rng' + '[0-9]+$', 'm')}},
// {$addFields: {number2: {$toDecimal: {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}}}}, // not working with MongoDb 3.6
{$addFields: {sortNumber: {$let: {
vars: {tmp: {$concat: ['000000000000000000000000000000', {$arrayElemAt: [{$split: [{$arrayElemAt: [{$split: ['$number', 'Rng']}, 1]}, '_']}, 0]}]}},
in: {$substrCP: ['$$tmp', {$subtract: [{$strLenCP: '$$tmp'}, 30]}, {$strLenCP: '$$tmp'}]}
}}}},
{$sort: {sortNumber: -1}},
{$limit: 1}
])
.exec()
.catch(err => next(err));
if (sampleData instanceof Error) return false;
return req.authDetails.location + (sampleData[0] ? Number(sampleData[0].number.replace(/[^0-9]+/g, '')) + 1 : 1);
}
async function numberCheck(sample, res, next) {
const sampleData = await SampleModel.findOne({number: sample.number}).lean().exec().catch(err => {next(err); return false;});
if (sampleData) { // found entry with sample number
res.status(400).json({status: 'Sample number already taken'});
return false
}
return true;
}
async function materialCheck (sample, res, next, id = sample.material_id) { // validate material_id and color, returns false if invalid
const materialData = await MaterialModel.findById(id).lean().exec().catch(err => next(err)) as any;
if (materialData instanceof Error) return false;
if (!materialData) { // could not find material_id
res.status(400).json({status: 'Material not available'});
return false;
}
if (sample.hasOwnProperty('color') && sample.color !== '' && !materialData.numbers.find(e => e.color === sample.color)) { // color for material not specified
res.status(400).json({status: 'Color not available for material'});
return false;
}
return true;
}
async function conditionCheck (condition, param, res, next, checkVersion = true) { // validate treatment template, returns false if invalid, otherwise template data
if (!condition.condition_template || !IdValidate.valid(condition.condition_template)) { // template id not found
res.status(400).json({status: 'Condition template not available'});
return false;
}
const conditionData = await ConditionTemplateModel.findById(condition.condition_template).lean().exec().catch(err => next(err)) as any;
if (conditionData instanceof Error) return false;
if (!conditionData) { // template not found
res.status(400).json({status: 'Condition template not available'});
return false;
}
if (checkVersion) {
// get all template versions and check if given is latest
const conditionVersions = await ConditionTemplateModel.find({first_id: conditionData.first_id}).sort({version: -1}).lean().exec().catch(err => next(err)) as any;
if (conditionVersions instanceof Error) return false;
if (condition.condition_template !== conditionVersions[0]._id.toString()) { // template not latest
res.status(400).json({status: 'Old template version not allowed'});
return false;
}
}
// validate parameters
const {error, value: ignore} = ParametersValidate.input(_.omit(condition, 'condition_template'), conditionData.parameters, param);
if (error) {res400(error, res); return false;}
return conditionData;
}
function sampleRefCheck (sample, res, next) { // validate sample_references, resolves false for invalid reference
return new Promise(resolve => {
if (sample.notes.hasOwnProperty('sample_references') && sample.notes.sample_references.length > 0) { // there are sample_references
let referencesCount = sample.notes.sample_references.length; // count to keep track of running async operations
sample.notes.sample_references.forEach(reference => {
SampleModel.findById(reference.sample_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) { // all async requests done
resolve(true);
}
});
});
}
else {
resolve(true);
}
});
}
function customFieldsChange (fields, amount, req) { // update custom_fields and respective quantities
fields.forEach(field => {
NoteFieldModel.findOneAndUpdate({name: field}, {$inc: {qty: amount}} as any, {new: true}).log(req).lean().exec((err, data: any) => { // check if field exists
if (err) return console.error(err);
if (!data) { // new field
new NoteFieldModel({name: field, qty: 1}).save((err, data) => {
if (err) return console.error(err);
db.log(req, 'note_fields', {_id: data._id}, data.toObject());
})
}
else if (data.qty <= 0) { // delete document if field is not used anymore
NoteFieldModel.findOneAndDelete({name: field}).log(req).lean().exec(err => {
if (err) return console.error(err);
});
}
});
});
}
function sortQuery(filters, sortKeys, sortStartValue) { // sortKeys = ['primary key', 'secondary key']
if (filters['from-id']) { // from-id specified
if ((filters['to-page'] === 0 && filters.sort[1] === 1) || (filters.sort[1] * filters['to-page'] > 0)) { // asc
return [{$match: {$or: [{[sortKeys[0]]: {$gt: sortStartValue}}, {$and: [{[sortKeys[0]]: sortStartValue}, {[sortKeys[1]]: {$gte: new mongoose.Types.ObjectId(filters['from-id'])}}]}]}},
{$sort: {[sortKeys[0]]: 1, _id: 1}}];
} else {
return [{$match: {$or: [{[sortKeys[0]]: {$lt: sortStartValue}}, {$and: [{[sortKeys[0]]: sortStartValue}, {[sortKeys[1]]: {$lte: new mongoose.Types.ObjectId(filters['from-id'])}}]}]}},
{$sort: {[sortKeys[0]]: -1, _id: -1}}];
}
} else { // sort from beginning
return [{$sort: {[sortKeys[0]]: filters.sort[1], [sortKeys[1]]: filters.sort[1]}}]; // set _id as secondary sort
}
}
function statusQuery(filters, field) {
if (filters.hasOwnProperty('status')) {
if(filters.status === 'all') {
return {$or: [{[field]: globals.status.validated}, {[field]: globals.status.new}]};
}
else {
return {[field]: globals.status[filters.status]};
}
}
else { // default
return {[field]: globals.status.validated};
}
}
function addFilterQueries (queryPtr, filters) { // returns array of match queries from given filters
if (filters.length) {
queryPtr.push({$match: {$and: filterQueries(filters)}});
}
}
function filterQueries (filters) {
console.log(filters);
return filters.map(e => {
if (e.mode === 'or') { // allow or queries (needed for $ne added)
return {['$' + e.mode]: e.values};
}
else {
return {[e.field]: {['$' + e.mode]: (e.mode.indexOf('in') >= 0 ? e.values : e.values[0])}}; // add filter criteria as {field: {$mode: value}}, only use first value when mode is not in/nin
}
});
}
function dateToOId (date) { // convert date to ObjectId
return mongoose.Types.ObjectId(Math.floor(date / 1000).toString(16) + '0000000000000000');
}

898
src/routes/template.spec.ts Normal file
View File

@ -0,0 +1,898 @@
import should from 'should/as-function';
import _ from 'lodash';
import TemplateConditionModel from '../models/condition_template';
import TemplateMeasurementModel from '../models/measurement_template';
import TestHelper from "../test/helper";
describe('/template', () => {
let server;
before(done => TestHelper.before(done));
beforeEach(done => server = TestHelper.beforeEach(server, done));
afterEach(done => TestHelper.afterEach(server, done));
after(done => TestHelper.after(done));
describe('/template/condition', () => {
describe('GET /template/conditions', () => {
it('returns all condition templates', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/conditions',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.condition_templates.length);
should(res.body).matchEach(condition => {
should(condition).have.only.keys('_id', 'name', 'version', 'parameters');
should(condition).have.property('_id').be.type('string');
should(condition).have.property('name').be.type('string');
should(condition).have.property('version').be.type('number');
should(condition.parameters).matchEach(number => {
should(number).have.only.keys('name', 'range');
should(number).have.property('name').be.type('string');
should(number).have.property('range').be.type('object');
});
});
done();
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/conditions',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/conditions',
httpStatus: 401
});
});
});
describe('GET /template/condition/{id}', () => {
it('returns the right condition template', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/condition/200000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/condition/000000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/condition/200000000000000000000001',
httpStatus: 401
});
});
});
describe('PUT /template/condition/{name}', () => {
it('returns the right condition template', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('keeps unchanged properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat treatment', parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]},
res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('keeps only one unchanged property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat treatment'},
res: {_id: '200000000000000000000001', name: 'heat treatment', version: 1, parameters: [{name: 'material', range: {values: ['copper', 'hot air']}}, {name: 'weeks', range: {min: 1, max: 10}}]}
});
});
it('changes the given properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
}).end((err, res) => {
if (err) return done(err);
TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data.first_id.toString()).be.eql('200000000000000000000001');
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(1);
should(data.parameters[0]).have.property('name', 'time');
should(data.parameters[0]).have.property('range');
should(data.parameters[0].range).have.property('min', 1);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]},
log: {
collection: 'condition_templates',
dataAdd: {
first_id: '200000000000000000000001',
version: 2
}
}
});
});
it('allows changing only one property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging'}
}).end((err, res) => {
if (err) return done(err);
TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data.first_id.toString()).be.eql('200000000000000000000001');
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(2);
should(data.parameters[0]).have.property('name', 'material');
should(data.parameters[1]).have.property('name', 'weeks');
done();
});
});
});
it('supports values ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {values: [1, 2, 5]}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {values: [1, 2, 5]}}]});
done();
});
});
it('supports min max ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {min: 1, max: 11}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {min: 1, max: 11}}]});
done();
});
});
it('supports array type ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {type: 'array'}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {type: 'array'}}]});
done();
});
});
it('supports empty ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'time', range: {}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'heat treatment', version: 2, parameters: [{name: 'time', range: {}}]});
done();
});
});
it('rejects `condition_template` as parameter name', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'condition_template', range: {}}]},
res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'}
});
});
it('rejects not specified parameters', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat treatment', parameters: [{name: 'material', range: {xx: 5}}]},
res: {status: 'Invalid body format', details: '"parameters[0].range.xx" is not allowed'}
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/2000000000h0000000000001',
auth: {basic: 'admin'},
httpStatus: 404,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/000000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 404,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/condition/200000000000000000000001',
httpStatus: 401,
req: {}
});
});
});
describe('POST /template/condition/new', () => {
it('returns the right condition template', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat treatment3', parameters: [{name: 'material', range: {values: ['copper']}}]}
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.only.keys('_id', 'name', 'version', 'parameters');
should(res.body).have.property('name', 'heat treatment3');
should(res.body).have.property('version', 1);
should(res.body).have.property('parameters').have.lengthOf(1);
should(res.body.parameters[0]).have.property('name', 'material');
should(res.body.parameters[0]).have.property('range');
should(res.body.parameters[0].range).have.property('values');
should(res.body.parameters[0].range.values[0]).be.eql('copper');
done();
});
});
it('stores the template', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
}).end((err, res) => {
if (err) return done(err);
TemplateConditionModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data.first_id.toString()).be.eql(data._id.toString());
should(data).have.property('name', 'heat aging');
should(data).have.property('version', 1);
should(data).have.property('parameters').have.lengthOf(1);
should(data.parameters[0]).have.property('name', 'time');
should(data.parameters[0]).have.property('range');
should(data.parameters[0].range).have.property('min', 1);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]},
log: {
collection: 'condition_templates',
dataAdd: {version: 1},
dataIgn: ['first_id']
}
});
});
it('rejects a missing name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'time', range: {min: 1}}]},
res: {status: 'Invalid body format', details: '"name" is required'}
});
});
it('rejects `condition_template` as parameter name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', parameters: [{name: 'condition_template', range: {min: 1}}]},
res: {status: 'Invalid body format', details: '"parameters[0].name" contains an invalid value'}
});
});
it('rejects a number prefix', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', number_prefix: 'C', parameters: [{name: 'time', range: {min: 1}}]},
res: {status: 'Invalid body format', details: '"number_prefix" is not allowed'}
});
});
it('rejects missing parameters', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging'},
res: {status: 'Invalid body format', details: '"parameters" is required'}
});
});
it('rejects a missing parameter name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
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', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
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 an invalid parameter range property', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
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 wrong properties', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'heat aging', parameters: [{name: 'time', range: {}}], xx: 33},
res: {status: 'Invalid body format', details: '"xx" is not allowed'}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {key: 'admin'},
httpStatus: 401,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/condition/new',
httpStatus: 401,
req: {name: 'heat aging', parameters: [{name: 'time', range: {min: 1}}]}
});
});
});
});
describe('/template/measurement', () => {
describe('GET /template/measurements', () => {
it('returns all measurement templates', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurements',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.measurement_templates.length);
should(res.body).matchEach(measurement => {
should(measurement).have.only.keys('_id', 'name', 'version', 'parameters');
should(measurement).have.property('_id').be.type('string');
should(measurement).have.property('name').be.type('string');
should(measurement).have.property('version').be.type('number');
should(measurement.parameters).matchEach(number => {
should(number).have.only.keys('name', 'range');
should(number).have.property('name').be.type('string');
should(number).have.property('range').be.type('object');
});
});
done();
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurements',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurements',
httpStatus: 401
});
});
});
describe('GET /template/measurement/id', () => {
it('returns the right measurement template', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: { type: 'array'}}]}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/300000000000000000000001',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/000000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 404
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/template/measurement/300000000000000000000001',
httpStatus: 401
});
});
});
describe('PUT /template/measurement/{name}', () => {
it('returns the right measurement template', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {},
res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: { type: 'array'}}]}
});
});
it('keeps unchanged properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'spectrum', parameters: [{name: 'dpt', range: { type: 'array'}}]},
res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {type: 'array'}}]}
});
});
it('keeps only one unchanged property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'spectrum'},
res: {_id: '300000000000000000000001', name: 'spectrum', version: 1, parameters: [{name: 'dpt', range: {type: 'array'}}]}
});
});
it('changes the given properties', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]});
TemplateMeasurementModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data.first_id.toString()).be.eql('300000000000000000000001');
should(data).have.property('name', 'IR spectrum');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(1);
should(data.parameters[0]).have.property('name', 'data point table');
should(data.parameters[0]).have.property('range');
should(data.parameters[0].range).have.property('min', 0);
should(data.parameters[0].range).have.property('max', 1000);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
log: {
collection: 'measurement_templates',
dataAdd: {
first_id: '300000000000000000000001',
version: 2
}
}
});
});
it('allows changing only one property', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'IR spectrum'},
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'IR spectrum', version: 2, parameters: [{name: 'dpt', range: {type: 'array'}}]});
TemplateMeasurementModel.findById(res.body._id).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data.first_id.toString()).be.eql('300000000000000000000001');
should(data).have.property('name', 'IR spectrum');
should(data).have.property('version', 2);
should(data).have.property('parameters').have.lengthOf(1);
should(data.parameters[0]).have.property('name', 'dpt');
should(data.parameters[0]).have.property('range');
should(data.parameters[0].range).have.property('type', 'array');
done();
});
});
});
it('supports values ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {values: [1, 2, 5]}}]});
done();
});
});
it('supports min max ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, parameters: [{name: 'dpt', range: {min: 0, max: 1000}}]});
done();
});
});
it('supports array type ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'dpt2', range: {type: 'array'}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'spectrum', version: 2, parameters: [{name: 'dpt2', range: {type: 'array'}}]});
done();
});
});
it('supports empty ranges', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000002',
auth: {basic: 'admin'},
httpStatus: 200,
req: {parameters: [{name: 'weight %', range: {}}]}
}).end((err, res) => {
if (err) return done(err);
should(_.omit(res.body, '_id')).be.eql({name: 'kf', version: 2, parameters: [{name: 'weight %', range: {}}]});
done();
});
});
it('rejects not specified parameters', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'dpt'}], range: {xx: 33}},
res: {status: 'Invalid body format', details: '"parameters[0].range" is required'}
});
});
it('rejects an invalid id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/3000000000h0000000000001',
auth: {basic: 'admin'},
httpStatus: 404,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
});
});
it('rejects an unknown id', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/000000000000000000000001',
auth: {basic: 'admin'},
httpStatus: 404,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/template/measurement/300000000000000000000001',
httpStatus: 401,
req: {}
});
});
});
describe('POST /template/measurement/new', () => {
it('returns the right measurement template', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
}).end((err, res) => {
if (err) return done(err);
should(res.body).have.only.keys('_id', 'name', 'version', 'parameters');
should(res.body).have.property('name', 'vz');
should(res.body).have.property('version', 1);
should(res.body).have.property('parameters').have.lengthOf(1);
should(res.body.parameters[0]).have.property('name', 'vz');
should(res.body.parameters[0]).have.property('range');
should(res.body.parameters[0].range).have.property('min', 1);
done();
});
});
it('stores the template', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
}).end(err => {
if (err) return done(err);
TemplateMeasurementModel.find({name: 'vz'}).lean().exec((err, data:any) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'first_id', 'name', 'version', 'parameters', '__v');
should(data[0].first_id.toString()).be.eql(data[0]._id.toString());
should(data[0]).have.property('name', 'vz');
should(data[0]).have.property('version', 1);
should(data[0]).have.property('parameters').have.lengthOf(1);
should(data[0].parameters[0]).have.property('name', 'vz');
should(data[0].parameters[0]).have.property('range');
should(data[0].parameters[0].range).have.property('min', 1);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]},
log: {
collection: 'measurement_templates',
dataAdd: {version: 1},
dataIgn: ['first_id']
}
});
});
it('rejects a missing name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {parameters: [{name: 'data point table', range: {min: 0, max: 1000}}]},
res: {status: 'Invalid body format', details: '"name" is required'}
});
});
it('rejects missing parameters', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'IR spectrum'},
res: {status: 'Invalid body format', details: '"parameters" is required'}
});
});
it('rejects a missing parameter name', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
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', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
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', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
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 wrong properties', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {name: 'IR spectrum', parameters: [{name: 'data point table', range: {}}], xx: 35},
res: {status: 'Invalid body format', details: '"xx" is not allowed'}
});
});
it('rejects an API key', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {key: 'admin'},
httpStatus: 401,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
});
});
it('rejects requests from a write user', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/template/measurement/new',
httpStatus: 401,
req: {name: 'vz', parameters: [{name: 'vz', range: {min: 1}}]}
});
});
});
});
});

86
src/routes/template.ts Normal file
View File

@ -0,0 +1,86 @@
import express from 'express';
import _ from 'lodash';
import TemplateValidate from './validate/template';
import ConditionTemplateModel from '../models/condition_template';
import MeasurementTemplateModel from '../models/measurement_template';
import res400 from './validate/res400';
import IdValidate from './validate/id';
import mongoose from "mongoose";
import db from '../db';
const router = express.Router();
router.get('/template/:collection(measurements|conditions)', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
req.params.collection = req.params.collection.replace(/s$/g, ''); // remove trailing s
model(req).find({}).lean().exec((err, data) => {
if (err) next (err);
res.json(_.compact(data.map(e => TemplateValidate.output(e)))); // validate all and filter null values from validation errors
});
});
router.get('/template/:collection(measurement|condition)/' + IdValidate.parameter(), (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
model(req).findById(req.params.id).lean().exec((err, data) => {
if (err) next (err);
if (data) {
res.json(TemplateValidate.output(data));
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
router.put('/template/:collection(measurement|condition)/' + IdValidate.parameter(), async (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
const {error, value: template} = TemplateValidate.input(req.body, 'change');
if (error) return res400(error, res);
const templateData = await model(req).findById(req.params.id).lean().exec().catch(err => {next(err);}) as any;
if (templateData instanceof Error) return;
if (!templateData) {
return res.status(404).json({status: 'Not found'});
}
if (!_.isEqual(_.pick(templateData, _.keys(template)), template)) { // data was changed
template.version = templateData.version + 1; // increase version
await new (model(req))(_.assign({}, _.omit(templateData, ['_id', '__v']), template)).save((err, data) => { // save new template, fill with old properties
if (err) next (err);
db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
res.json(TemplateValidate.output(data.toObject()));
});
}
else {
res.json(TemplateValidate.output(templateData));
}
});
router.post('/template/:collection(measurement|condition)/new', async (req, res, next) => {
if (!req.auth(res, ['maintain', 'admin'], 'basic')) return;
const {error, value: template} = TemplateValidate.input(req.body, 'new');
if (error) return res400(error, res);
template._id = mongoose.Types.ObjectId(); // set reference to itself for first version of template
template.first_id = template._id;
template.version = 1; // set template version
await new (model(req))(template).save((err, data) => {
if (err) next (err);
db.log(req, req.params.collection + '_templates', {_id: data._id}, data.toObject());
res.json(TemplateValidate.output(data.toObject()));
});
});
module.exports = router;
function model (req) { // return right template model
return req.params.collection === 'condition' ? ConditionTemplateModel : MeasurementTemplateModel;
}

677
src/routes/user.spec.ts Normal file
View File

@ -0,0 +1,677 @@
import should from 'should/as-function';
import UserModel from '../models/user';
import TestHelper from "../test/helper";
describe('/user', () => {
let server;
before(done => TestHelper.before(done));
beforeEach(done => server = TestHelper.beforeEach(server, done));
afterEach(done => TestHelper.afterEach(server, done));
after(done => TestHelper.after(done));
describe('GET /users', () => {
it('returns all users', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/users',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done(err);
const json = require('../test/db.json');
should(res.body).have.lengthOf(json.collections.users.length);
should(res.body).matchEach(user => {
should(user).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name');
should(user).have.property('_id').be.type('string');
should(user).have.property('email').be.type('string');
should(user).have.property('name').be.type('string');
should(user).have.property('level').be.type('string');
should(user).have.property('location').be.type('string');
should(user).have.property('device_name').be.type('string');
});
done();
});
});
it('rejects requests from non-admins', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/users',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/users',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/users',
httpStatus: 401
});
});
});
describe('GET /user/{name}', () => {
it('returns own user details', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('email', 'jane.doe@bosch.com');
should(res.body).have.property('name', 'janedoe');
should(res.body).have.property('level', 'write');
should(res.body).have.property('location', 'Rng');
should(res.body).have.property('device_name', 'Alpha I');
done();
});
});
it('returns other user details for admin', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/janedoe',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('email', 'jane.doe@bosch.com');
should(res.body).have.property('name', 'janedoe');
should(res.body).have.property('level', 'write');
should(res.body).have.property('location', 'Rng');
should(res.body).have.property('device_name', 'Alpha I');
done();
});
});
it('rejects requests from non-admins for another user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/admin',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects requests from a user API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/janedoe',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('returns 404 for an unknown user', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/unknown',
auth: {basic: 'admin'},
httpStatus: 404
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/janedoe',
httpStatus: 401
});
});
});
describe('PUT /user/{name}', () => {
it('returns own user details', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'janedoe'},
httpStatus: 200,
req: {}
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('email', 'jane.doe@bosch.com');
should(res.body).have.property('name', 'janedoe');
should(res.body).have.property('level', 'write');
should(res.body).have.property('location', 'Rng');
should(res.body).have.property('device_name', 'Alpha I');
done();
});
});
it('returns other user details for admin', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user/janedoe',
auth: {basic: 'admin'},
httpStatus: 200,
req: {}
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('email', 'jane.doe@bosch.com');
should(res.body).have.property('name', 'janedoe');
should(res.body).have.property('level', 'write');
should(res.body).have.property('location', 'Rng');
should(res.body).have.property('device_name', 'Alpha I');
done();
});
});
it('changes user details as given', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'}
}).end(err => {
if (err) return done (err);
UserModel.find({name: 'adminnew'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v');
should(data[0]).have.property('_id');
should(data[0]).have.property('name', 'adminnew');
should(data[0]).have.property('email', 'adminnew@bosch.com');
should(data[0]).have.property('pass').not.eql('Abc123##');
should(data[0]).have.property('level', 'admin');
should(data[0]).have.property('location', 'Abt');
should(data[0]).have.property('device_name', 'test');
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'admin'},
httpStatus: 200,
req: {name: 'adminnew', email: 'adminnew@bosch.com', pass: 'Abc123##', location: 'Abt', device_name: 'test'},
log: {
collection: 'users',
dataIgn: ['pass']
}
});
});
it('lets the admin change a user level', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user/janedoe',
auth: {basic: 'admin'},
httpStatus: 200,
req: {level: 'read'}
}).end(err => {
if (err) return done (err);
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.property('level', 'read');
done();
});
});
});
it('does not change the level', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'janedoe'},
httpStatus: 400, default: false,
req: {level: 'read'}
}).end((err, res) => {
if (err) return done (err);
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);
should(data[0]).have.property('level', 'write');
done();
});
});
});
it('rejects a username already in use', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'admin'},
httpStatus: 400, default: false,
req: {name: 'janedoe'}
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'Username already taken'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
done();
});
});
});
it('rejects a username which is in the special names', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 400, default: false,
req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Username already taken'}
});
});
it('rejects invalid user details', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/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', details: '"location" must be a string'}
});
});
it('rejects an invalid email address', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe'},
res: {status: 'Invalid body format', details: '"email" must be a valid email'}
});
});
it('rejects an invalid password', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {basic: 'admin'},
httpStatus: 400,
req: {pass: 'password'},
res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$)[a-zA-Z0-9!"#%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{|}~]{8,}$/'}
});
});
it('rejects requests from non-admins for another user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user/admin',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {}
});
});
it('rejects requests from a user API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user',
auth: {key: 'janedoe'},
httpStatus: 401,
req: {}
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user/janedoe',
auth: {key: 'admin'},
httpStatus: 401,
req: {}
});
});
it('returns 404 for an unknown user', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user/unknown',
auth: {basic: 'admin'},
httpStatus: 404,
req: {}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'put',
url: '/user/janedoe',
httpStatus: 401,
req: {}
});
});
});
describe('DELETE /user/{name}', () => {
it('deletes own user details', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user',
auth: {basic: 'janedoe'},
httpStatus: 200
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(0);
done();
});
});
});
it('deletes other user details for admin', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/janedoe',
auth: {basic: 'admin'},
httpStatus: 200
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'OK'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(0);
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user',
auth: {basic: 'janedoe'},
httpStatus: 200,
log: {
collection: 'users'
}
});
});
it('rejects requests from non-admins for another user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/admin',
auth: {basic: 'janedoe'},
httpStatus: 403
});
});
it('rejects requests from a user API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/janedoe',
auth: {key: 'admin'},
httpStatus: 401
});
});
it('returns 404 for an unknown user', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/unknown',
auth: {basic: 'admin'},
httpStatus: 404
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'delete',
url: '/user/janedoe',
httpStatus: 401
});
});
});
describe('GET /user/key', () => {
it('returns the right API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/key',
auth: {basic: 'janedoe'},
httpStatus: 200,
res: {key: TestHelper.auth.janedoe.key}
});
});
it('rejects requests from an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/key',
auth: {key: 'janedoe'},
httpStatus: 401
});
});
it('rejects requests from an API key', done => {
TestHelper.request(server, done, {
method: 'get',
url: '/user/key',
httpStatus: 401
});
});
});
describe('POST /user/new', () => {
it('returns the added user data', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}
}).end((err, res) => {
if (err) return done (err);
should(res.body).have.only.keys('_id', 'email', 'name', 'level', 'location', 'device_name');
should(res.body).have.property('_id').be.type('string');
should(res.body).have.property('email', 'john.doe@bosch.com');
should(res.body).have.property('name', 'johndoe');
should(res.body).have.property('level', 'read');
should(res.body).have.property('location', 'Rng');
should(res.body).have.property('device_name', 'Alpha II');
done();
});
});
it('stores the data', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}
}).end(err => {
if (err) return done (err);
UserModel.find({name: 'johndoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
should(data[0]).have.only.keys('_id', 'name', 'pass', 'email', 'level', 'location', 'device_name', 'key', '__v');
should(data[0]).have.property('_id');
should(data[0]).have.property('name', 'johndoe');
should(data[0]).have.property('email', 'john.doe@bosch.com');
should(data[0]).have.property('pass').not.eql('Abc123!#');
should(data[0]).have.property('level', 'read');
should(data[0]).have.property('location', 'Rng');
should(data[0]).have.property('device_name', 'Alpha II');
done();
});
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 200,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
log: {
collection: 'users',
dataIgn: ['pass', 'key']
}
});
});
it('rejects a username already in use', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 400, default: false,
req: {email: 'j.doe@bosch.com', name: 'janedoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}
}).end((err, res) => {
if (err) return done (err);
should(res.body).be.eql({status: 'Username already taken'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data) => {
if (err) return done(err);
should(data).have.lengthOf(1);
done();
});
});
});
it('rejects a username which is in the special names', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 400, default: false,
req: {email: 'j.doe@bosch.com', name: 'passreset', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Username already taken'}
});
});
it('rejects invalid user details', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
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', details: '"location" must be a string'}
});
});
it('rejects an invalid user level', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
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', details: '"level" must be one of [read, write, maintain, dev, admin]'}
});
});
it('rejects an invalid email address', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
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', details: '"email" must be a valid email'}
});
});
it('rejects an invalid password', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'admin'},
httpStatus: 400,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'password', level: 'read', location: 'Rng', device_name: 'Alpha II'},
res: {status: 'Invalid body format', details: '"pass" with value "password" fails to match the required pattern: /^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&\'()*+,-.\\/:;<=>?@[\\]^_`{|}~])(?=\\S+$)[a-zA-Z0-9!"#%&\'()*+,\\-.\\/:;<=>?@[\\]^_`{|}~]{8,}$/'}
});
});
it('rejects requests from non-admins', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {basic: 'janedoe'},
httpStatus: 403,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}
});
});
it('rejects requests from an admin API key', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
auth: {key: 'admin'},
httpStatus: 401,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}
});
});
it('rejects unauthorized requests', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/new',
httpStatus: 401,
req: {email: 'john.doe@bosch.com', name: 'johndoe', pass: 'Abc123!#', level: 'read', location: 'Rng', device_name: 'Alpha II'}
});
});
});
describe('POST /user/passreset', () => {
it('returns the ok response', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/passreset',
httpStatus: 200,
req: {email: 'jane.doe@bosch.com', name: 'janedoe'},
res: {status: 'OK'}
});
});
it('creates a changelog', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/passreset',
httpStatus: 200,
req: {email: 'jane.doe@bosch.com', name: 'janedoe'},
log: {
collection: 'users',
dataIgn: ['email', 'name', 'pass']
}
});
});
it('returns 404 for wrong username/email combo', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/passreset',
httpStatus: 404,
req: {email: 'jane.doe@bosch.com', name: 'admin'}
});
});
it('returns 404 for unknown username', done => {
TestHelper.request(server, done, {
method: 'post',
url: '/user/passreset',
httpStatus: 404,
req: {email: 'jane.doe@bosch.com', name: 'username'}
});
});
it('changes the user password', done => {
UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => {
if (err) return done(err);
const oldpass = data[0].pass;
TestHelper.request(server, done, {
method: 'post',
url: '/user/passreset',
httpStatus: 200,
req: {email: 'jane.doe@bosch.com', name: 'janedoe'}
}).end((err, res) => {
if (err) return done(err);
should(res.body).be.eql({status: 'OK'});
UserModel.find({name: 'janedoe'}).lean().exec( (err, data: any) => {
if (err) return done(err);
should(data[0].pass).not.eql(oldpass);
done();
});
});
});
});
});
});

163
src/routes/user.ts Normal file
View File

@ -0,0 +1,163 @@
import express from 'express';
import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
import _ from 'lodash';
import UserValidate from './validate/user';
import UserModel from '../models/user';
import mail from '../helpers/mail';
import res400 from './validate/res400';
import db from '../db';
const router = express.Router();
router.get('/users', (req, res) => {
if (!req.auth(res, ['admin'], 'basic')) return;
UserModel.find({}).lean().exec( (err, data:any) => {
res.json(_.compact(data.map(e => UserValidate.output(e)))); // validate all and filter null values from validation errors
});
});
router.get('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
const username = getUsername(req, res);
if (!username) return;
UserModel.findOne({name: username}).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data)); // validate all and filter null values from validation errors
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
router.put('/user:username([/](?!key|new).?*|/?)', async (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
const username = getUsername(req, res);
if (!username) return;
const {error, value: user} = UserValidate.input(req.body, 'change' + (req.authDetails.level === 'admin'? 'admin' : ''));
if (error) return res400(error, res);
if (user.hasOwnProperty('pass')) {
user.pass = bcrypt.hashSync(user.pass, 10);
}
// check that user does not already exist if new name was specified
if (user.hasOwnProperty('name') && user.name !== username) {
if (!await usernameCheck(user.name, res, next)) return;
}
await UserModel.findOneAndUpdate({name: username}, user, {new: true}).log(req).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json(UserValidate.output(data));
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
router.delete('/user:username([/](?!key|new).?*|/?)', (req, res, next) => { // this path matches /user, /user/ and /user/xxx, but not /user/key or user/new. See https://forbeslindesay.github.io/express-route-tester/ for the generated regex
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
const username = getUsername(req, res);
if (!username) return;
UserModel.findOneAndDelete({name: username}).log(req).lean().exec( (err, data:any) => {
if (err) return next(err);
if (data) {
res.json({status: 'OK'})
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
router.get('/user/key', (req, res, next) => {
if (!req.auth(res, ['read', 'write', 'maintain', 'dev', 'admin'], 'basic')) return;
UserModel.findOne({name: req.authDetails.username}).lean().exec( (err, data:any) => {
if (err) return next(err);
res.json({key: data.key});
});
});
router.post('/user/new', async (req, res, next) => {
if (!req.auth(res, ['admin'], 'basic')) return;
// validate input
const {error, value: user} = UserValidate.input(req.body, 'new');
if (error) return res400(error, res);
// check that user does not already exist
if (!await usernameCheck(user.name, res, next)) return;
user.key = mongoose.Types.ObjectId(); // use object id as unique API key
bcrypt.hash(user.pass, 10, (err, hash) => { // password hashing
user.pass = hash;
new UserModel(user).save((err, data) => { // store user
if (err) return next(err);
db.log(req, 'users', {_id: data._id}, data.toObject());
res.json(UserValidate.output(data.toObject()));
});
});
});
router.post('/user/passreset', (req, res, next) => {
// check if user/email combo exists
UserModel.find({name: req.body.name, email: req.body.email}).lean().exec( (err, data: any) => {
if (err) return next(err);
if (data.length === 1) { // it exists
const newPass = Math.random().toString(36).substring(2); // generate temporary password
bcrypt.hash(newPass, 10, (err, hash) => { // password hashing
if (err) return next(err);
UserModel.findByIdAndUpdate(data[0]._id, {pass: hash}).log(req).exec(err => { // write new password
if (err) return next(err);
// send email
mail(data[0].email, 'Your new password for the DFOP database', 'Hi, <br><br> You requested to reset your password.<br>Your new password is:<br><br>' + newPass + '<br><br>If you did not request a password reset, talk to the sysadmin quickly!<br><br>Have a nice day.<br><br>The DFOP team', err => {
if (err) return next(err);
res.json({status: 'OK'});
});
});
});
}
else {
res.status(404).json({status: 'Not found'});
}
});
});
module.exports = router;
function getUsername (req, res) { // returns username or false if action is not allowed
req.params.username = req.params[0]; // because of path regex
if (req.params.username !== undefined) { // different username than request user
if (!req.auth(res, ['admin'], 'basic')) return false;
return req.params.username;
}
else {
return req.authDetails.username;
}
}
async function usernameCheck (name, res, next) { // check if username is already taken
const userData = await UserModel.findOne({name: name}).lean().exec().catch(err => next(err)) as any;
if (userData instanceof Error) return false;
if (userData || UserValidate.isSpecialName(name)) {
res.status(400).json({status: 'Username already taken'});
return false;
}
return true;
}

29
src/routes/validate/id.ts Normal file
View File

@ -0,0 +1,29 @@
import Joi from '@hapi/joi';
export default class IdValidate {
private static id = Joi.string().pattern(new RegExp('[0-9a-f]{24}')).length(24);
static get () { // return joi validation
return this.id;
}
static valid (id) { // validate id
return this.id.validate(id).error === undefined;
}
static parameter () { // :id url parameter
return ':id([0-9a-f]{24})';
}
static stringify (data) { // convert all ObjectID objects to plain strings
Object.keys(data).forEach(key => {
if (data[key] !== null && data[key].hasOwnProperty('_bsontype') && data[key]._bsontype === 'ObjectID') { // stringify id
data[key] = data[key].toString();
}
else if (typeof data[key] === 'object' && data[key] !== null) { // deeper into recursion
data[key] = this.stringify(data[key]);
}
});
return data;
}
}

View File

@ -0,0 +1,116 @@
import Joi from '@hapi/joi';
import IdValidate from './id';
export default class MaterialValidate { // validate input for material
private static material = {
name: Joi.string()
.max(128),
supplier: Joi.string()
.max(128),
group: Joi.string()
.max(128),
mineral: Joi.number()
.integer()
.min(0)
.max(100),
glass_fiber: Joi.number()
.integer()
.min(0)
.max(100),
carbon_fiber: Joi.number()
.integer()
.min(0)
.max(100),
numbers: Joi.array()
.items(Joi.object({
color: Joi.string()
.max(128)
.required(),
number: Joi.string()
.max(128)
.allow('')
.required()
}))
};
static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') {
return Joi.object({
name: this.material.name.required(),
supplier: this.material.supplier.required(),
group: this.material.group.required(),
mineral: this.material.mineral.required(),
glass_fiber: this.material.glass_fiber.required(),
carbon_fiber: this.material.carbon_fiber.required(),
numbers: this.material.numbers.required()
}).validate(data);
}
else if (param === 'change') {
return Joi.object({
name: this.material.name,
supplier: this.material.supplier,
group: this.material.group,
mineral: this.material.mineral,
glass_fiber: this.material.glass_fiber,
carbon_fiber: this.material.carbon_fiber,
numbers: this.material.numbers
}).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data);
data.group = data.group_id.name;
data.supplier = data.supplier_id.name;
const {value, error} = Joi.object({
_id: IdValidate.get(),
name: this.material.name,
supplier: this.material.supplier,
group: this.material.group,
mineral: this.material.mineral,
glass_fiber: this.material.glass_fiber,
carbon_fiber: this.material.carbon_fiber,
numbers: this.material.numbers
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
static outputGroups (data) {// validate groups output and strip unwanted properties, returns null if not valid
const {value, error} = this.material.group.validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
static outputSuppliers (data) {// validate suppliers output and strip unwanted properties, returns null if not valid
const {value, error} = this.material.supplier.validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
static outputV() { // return output validator
return Joi.object({
_id: IdValidate.get(),
name: this.material.name,
supplier: this.material.supplier,
group: this.material.group,
mineral: this.material.mineral,
glass_fiber: this.material.glass_fiber,
carbon_fiber: this.material.carbon_fiber,
numbers: this.material.numbers
});
}
static query (data) {
return Joi.object({
status: Joi.string().valid('validated', 'new', 'all')
}).validate(data);
}
}

View File

@ -0,0 +1,56 @@
import Joi from '@hapi/joi';
import IdValidate from './id';
export default class MeasurementValidate {
private static measurement = {
values: Joi.object()
.pattern(/.*/, Joi.alternatives()
.try(
Joi.string().max(128),
Joi.number(),
Joi.boolean(),
Joi.array()
)
.allow(null)
)
};
static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') {
return Joi.object({
sample_id: IdValidate.get().required(),
values: this.measurement.values.required(),
measurement_template: IdValidate.get().required()
}).validate(data);
}
else if (param === 'change') {
return Joi.object({
values: this.measurement.values
}).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data);
const {value, error} = Joi.object({
_id: IdValidate.get(),
sample_id: IdValidate.get(),
values: this.measurement.values,
measurement_template: IdValidate.get()
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
static outputV() { // return output validator
return Joi.object({
_id: IdValidate.get(),
sample_id: IdValidate.get(),
values: this.measurement.values,
measurement_template: IdValidate.get()
});
}
}

View File

@ -0,0 +1,18 @@
import Joi from '@hapi/joi';
export default class NoteFieldValidate {
private static note_field = {
name: Joi.string()
.max(128),
qty: Joi.number()
};
static output (data) { // validate output and strip unwanted properties, returns null if not valid
const {value, error} = Joi.object({
name: this.note_field.name,
qty: this.note_field.qty
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
}

View File

@ -0,0 +1,48 @@
import Joi from '@hapi/joi';
export default class ParametersValidate {
static input (data, parameters, param) { // data to validate, parameters from template, param: 'new', 'change', 'null'(null values are allowed)
let joiObject = {};
parameters.forEach(parameter => {
if (parameter.range.hasOwnProperty('values')) { // append right validation method according to parameter
joiObject[parameter.name] = Joi.alternatives()
.try(Joi.string().max(128), Joi.number(), Joi.boolean())
.valid(...parameter.range.values);
}
else if (parameter.range.hasOwnProperty('min') && parameter.range.hasOwnProperty('max')) {
joiObject[parameter.name] = Joi.number()
.min(parameter.range.min)
.max(parameter.range.max);
}
else if (parameter.range.hasOwnProperty('min')) {
joiObject[parameter.name] = Joi.number()
.min(parameter.range.min);
}
else if (parameter.range.hasOwnProperty('max')) {
joiObject[parameter.name] = Joi.number()
.max(parameter.range.max);
}
else if (parameter.range.hasOwnProperty('type')) {
switch (parameter.range.type) {
case 'array':
joiObject[parameter.name] = Joi.array();
break;
default:
joiObject[parameter.name] = Joi.string().max(128);
break;
}
}
else {
joiObject[parameter.name] = Joi.alternatives()
.try(Joi.string().max(128), Joi.number(), Joi.boolean());
}
if (param === 'new') {
joiObject[parameter.name] = joiObject[parameter.name].required()
}
else if (param === 'null') {
joiObject[parameter.name] = joiObject[parameter.name].allow(null)
}
});
return Joi.object(joiObject).validate(data);
}
}

View File

@ -0,0 +1,5 @@
// respond with 400 and include error details from the joi validation
export default function res400 (error, res) {
res.status(400).json({status: 'Invalid body format', details: error.details[0].message});
}

View File

@ -0,0 +1,50 @@
import Joi from '@hapi/joi';
import IdValidate from './id';
export default class RootValidate { // validate input for root methods
private static changelog = {
timestamp: Joi.date()
.iso()
.min('1970-01-01T00:00:00.000Z'),
page: Joi.number()
.integer()
.min(0)
.default(0),
pagesize: Joi.number()
.integer()
.min(0)
.default(25),
action: Joi.string(),
collection: Joi.string(),
conditions: Joi.object(),
data: Joi.object()
};
static changelogParams (data) {
return Joi.object({
timestamp: this.changelog.timestamp.required(),
page: this.changelog.page,
pagesize: this.changelog.pagesize
}).validate(data);
}
static changelogOutput (data) {
data.date = data._id.getTimestamp();
data.collection = data.collectionName;
data = IdValidate.stringify(data);
const {value, error} = Joi.object({
date: this.changelog.timestamp,
action: this.changelog.action,
collection: this.changelog.collection,
conditions: this.changelog.conditions,
data: this.changelog.data,
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
}

View File

@ -0,0 +1,223 @@
import Joi from '@hapi/joi';
import IdValidate from './id';
import UserValidate from './user';
import MaterialValidate from './material';
import MeasurementValidate from './measurement';
export default class SampleValidate {
private static sample = {
number: Joi.string()
.max(128),
color: Joi.string()
.max(128)
.allow(''),
type: Joi.string()
.max(128),
batch: Joi.string()
.max(128)
.allow(''),
condition: Joi.object(),
notes: Joi.object({
comment: Joi.string()
.max(512)
.allow(''),
sample_references: Joi.array()
.items(Joi.object({
sample_id: IdValidate.get(),
relation: Joi.string()
.max(128)
})),
custom_fields: Joi.object()
.pattern(/.*/, Joi.alternatives()
.try(
Joi.string().max(128),
Joi.number(),
Joi.boolean(),
Joi.date()
)
)
}),
added: Joi.date()
.iso()
.min('1970-01-01T00:00:00.000Z')
};
private static sortKeys = [
'_id',
'color',
'number',
'type',
'batch',
'added',
'material.name',
'material.supplier',
'material.group',
'material.mineral',
'material.glass_fiber',
'material.carbon_fiber',
'material.number',
'measurements.(?!spectrum)*'
];
private static fieldKeys = [
...SampleValidate.sortKeys,
'condition',
'material_id',
'material',
'note_id',
'user_id',
'material._id',
'material.numbers',
'measurements.spectrum.dpt'
];
static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') {
return Joi.object({
color: this.sample.color.required(),
type: this.sample.type.required(),
batch: this.sample.batch.required(),
condition: this.sample.condition.required(),
material_id: IdValidate.get().required(),
notes: this.sample.notes.required()
}).validate(data);
}
else if (param === 'change') {
return Joi.object({
color: this.sample.color,
type: this.sample.type,
batch: this.sample.batch,
condition: this.sample.condition,
material_id: IdValidate.get(),
notes: this.sample.notes,
}).validate(data);
}
else if (param === 'new-admin') {
return Joi.object({
number: this.sample.number,
color: this.sample.color.required(),
type: this.sample.type.required(),
batch: this.sample.batch.required(),
condition: this.sample.condition.required(),
material_id: IdValidate.get().required(),
notes: this.sample.notes.required()
}).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data, param = 'refs+added', additionalParams = []) { // validate output and strip unwanted properties, returns null if not valid
if (param === 'refs+added') {
param = 'refs';
data.added = data._id.getTimestamp();
}
data = IdValidate.stringify(data);
let joiObject;
if (param === 'refs') {
joiObject = {
_id: IdValidate.get(),
number: this.sample.number,
color: this.sample.color,
type: this.sample.type,
batch: this.sample.batch,
condition: this.sample.condition,
material_id: IdValidate.get(),
material: MaterialValidate.outputV().append({number: Joi.string().max(128).allow('')}),
note_id: IdValidate.get().allow(null),
user_id: IdValidate.get(),
added: this.sample.added
};
}
else if(param === 'details') {
joiObject = {
_id: IdValidate.get(),
number: this.sample.number,
color: this.sample.color,
type: this.sample.type,
batch: this.sample.batch,
condition: this.sample.condition,
material: MaterialValidate.outputV(),
measurements: Joi.array().items(MeasurementValidate.outputV()),
notes: this.sample.notes,
user: UserValidate.username()
}
}
else {
return null;
}
additionalParams.forEach(param => {
joiObject[param] = Joi.any();
});
const {value, error} = Joi.object(joiObject).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
static query (data) {
if (data.filters && data.filters.length) {
const filterValidation = Joi.array().items(Joi.string()).validate(data.filters);
if (filterValidation.error) return filterValidation;
try {
for (let i in data.filters) {
data.filters[i] = JSON.parse(data.filters[i]);
data.filters[i].values = data.filters[i].values.map(e => { // validate filter values
let validator;
let field = data.filters[i].field
if (/material\./.test(field)) { // select right validation model
validator = MaterialValidate.outputV().append({number: Joi.string().max(128).allow('')});
field = field.replace('material.', '');
}
else if (/measurements\./.test(field)) {
validator = Joi.object({
value: Joi.alternatives()
.try(
Joi.number(),
Joi.string().max(128),
Joi.boolean(),
Joi.array()
)
.allow(null)
});
field = 'value';
}
else {
validator = Joi.object(this.sample);
}
const {value, error} = validator.validate({[field]: e});
console.log(value);
if (error) throw error; // reject invalid values // TODO: return exact error description, handle in frontend filters
return value[field];
});
}
}
catch {
return {error: {details: [{message: 'Invalid JSON string for filter parameter'}]}, value: null}
}
}
return Joi.object({
status: Joi.string().valid('validated', 'new', 'all'),
'from-id': IdValidate.get(),
'to-page': Joi.number().integer(),
'page-size': Joi.number().integer().min(1),
sort: Joi.string().pattern(new RegExp('^(' + this.sortKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')-(asc|desc)$', 'm')).default('_id-asc'),
csv: Joi.boolean().default(false),
fields: Joi.array().items(Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm'))).default(['_id','number','type','batch','material_id','color','condition','note_id','user_id','added']),
filters: Joi.array().items(Joi.object({
mode: Joi.string().valid('eq', 'ne', 'lt', 'lte', 'gt', 'gte', 'in', 'nin'),
field: Joi.string().pattern(new RegExp('^(' + this.fieldKeys.join('|').replace(/\./g, '\\.').replace(/\*/g, '.+') + ')$', 'm')),
values: Joi.array().items(Joi.alternatives().try(Joi.string().max(128), Joi.number(), Joi.boolean(), Joi.date().iso())).min(1)
})).default([])
}).with('to-page', 'page-size').validate(data);
}
}

View File

@ -0,0 +1,70 @@
import Joi from '@hapi/joi';
import IdValidate from './id';
// TODO: do not allow a . in the name
export default class TemplateValidate {
private static template = {
name: Joi.string()
.max(128),
version: Joi.number()
.min(1),
parameters: Joi.array()
.items(
Joi.object({
name: Joi.string()
.max(128)
.invalid('condition_template')
.required(),
range: Joi.object({
values: Joi.array()
.min(1),
min: Joi.number(),
max: Joi.number(),
type: Joi.string()
.valid('array')
})
.oxor('values', 'min')
.oxor('values', 'max')
.oxor('type', 'values')
.oxor('type', 'min')
.oxor('type', 'max')
.required()
})
)
};
static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') {
return Joi.object({
name: this.template.name.required(),
parameters: this.template.parameters.required()
}).validate(data);
}
else if (param === 'change') {
return Joi.object({
name: this.template.name,
parameters: this.template.parameters
}).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data);
const {value, error} = Joi.object({
_id: IdValidate.get(),
name: this.template.name,
version: this.template.version,
parameters: this.template.parameters
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
}

View File

@ -0,0 +1,91 @@
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()
.lowercase()
.pattern(new RegExp('^[a-z0-9-_.]+$'))
.max(128),
email: Joi.string()
.email({minDomainSegments: 2})
.lowercase()
.max(128),
pass: Joi.string()
.pattern(/^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[!"#%&'()*+,-.\/:;<=>?@[\]^_`{|}~])(?=\S+$)[a-zA-Z0-9!"#%&'()*+,\-.\/:;<=>?@[\]^_`{|}~]{8,}$/)
.max(128),
level: Joi.string()
.valid(...globals.levels),
location: Joi.string()
.alphanum()
.max(128),
device_name: Joi.string()
.allow('')
.max(128),
};
private static specialUsernames = ['admin', 'user', 'key', 'new', 'passreset']; // names a user cannot take
static input (data, param) { // validate input, set param to 'new' to make all attributes required
if (param === 'new') {
return Joi.object({
name: this.user.name.required(),
email: this.user.email.required(),
pass: this.user.pass.required(),
level: this.user.level.required(),
location: this.user.location.required(),
device_name: this.user.device_name.required()
}).validate(data);
}
else if (param === 'change') {
return Joi.object({
name: this.user.name,
email: this.user.email,
pass: this.user.pass,
location: this.user.location,
device_name: this.user.device_name
}).validate(data);
}
else if (param === 'changeadmin') {
return Joi.object({
name: this.user.name,
email: this.user.email,
pass: this.user.pass,
level: this.user.level,
location: this.user.location,
device_name: this.user.device_name
}).validate(data);
}
else {
return{error: 'No parameter specified!', value: {}};
}
}
static output (data) { // validate output and strip unwanted properties, returns null if not valid
data = IdValidate.stringify(data);
const {value, error} = Joi.object({
_id: IdValidate.get(),
name: this.user.name,
email: this.user.email,
level: this.user.level,
location: this.user.location,
device_name: this.user.device_name
}).validate(data, {stripUnknown: true});
return error !== undefined? null : value;
}
static isSpecialName (name) { // true if name belongs to special names
return this.specialUsernames.indexOf(name) > -1;
}
static username() {
return this.user.name;
}
}

673
src/test/db.json Normal file
View File

@ -0,0 +1,673 @@
{
"collections": {
"samples": [
{
"_id": {"$oid":"400000000000000000000001"},
"number": "1",
"type": "granulate",
"color": "black",
"batch": "",
"condition": {
"material": "copper",
"weeks": 3,
"condition_template": {"$oid":"200000000000000000000001"}
},
"material_id": {"$oid":"100000000000000000000004"},
"note_id": null,
"user_id": {"$oid":"000000000000000000000002"},
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"400000000000000000000002"},
"number": "21",
"type": "granulate",
"color": "natural",
"batch": "1560237365",
"condition": {
"material": "copper",
"weeks": 3,
"condition_template": {"$oid":"200000000000000000000001"}
},
"material_id": {"$oid":"100000000000000000000001"},
"note_id": {"$oid":"500000000000000000000001"},
"user_id": {"$oid":"000000000000000000000002"},
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"400000000000000000000003"},
"number": "33",
"type": "part",
"color": "black",
"batch": "1704-005",
"condition": {
"material": "copper",
"weeks": 3,
"condition_template": {"$oid":"200000000000000000000001"}
},
"material_id": {"$oid":"100000000000000000000005"},
"note_id": {"$oid":"500000000000000000000002"},
"user_id": {"$oid":"000000000000000000000003"},
"status": 0,
"__v": 0
},
{
"_id": {"$oid":"400000000000000000000004"},
"number": "32",
"type": "granulate",
"color": "black",
"batch": "1653000308",
"condition": {
"p1": 44,
"condition_template": {"$oid":"200000000000000000000004"}
},
"material_id": {"$oid":"100000000000000000000005"},
"note_id": {"$oid":"500000000000000000000003"},
"user_id": {"$oid":"000000000000000000000003"},
"status": 0,
"__v": 0
},
{
"_id": {"$oid":"400000000000000000000005"},
"number": "Rng33",
"type": "granulate",
"color": "black",
"batch": "1653000308",
"condition": {
"condition_template": {"$oid":"200000000000000000000003"}
},
"material_id": {"$oid":"100000000000000000000005"},
"note_id": null,
"user_id": {"$oid":"000000000000000000000003"},
"status": -1,
"__v": 0
},
{
"_id": {"$oid":"400000000000000000000006"},
"number": "Rng36",
"type": "granulate",
"color": "black",
"batch": "",
"condition": {},
"material_id": {"$oid":"100000000000000000000004"},
"note_id": null,
"user_id": {"$oid":"000000000000000000000002"},
"status": 0,
"__v": 0
}
],
"notes": [
{
"_id": {"$oid":"500000000000000000000001"},
"comment": "Stoff gesperrt",
"sample_references": [],
"__v": 0
},
{
"_id": {"$oid":"500000000000000000000002"},
"comment": "",
"sample_references": [{
"sample_id": {"$oid":"400000000000000000000004"},
"relation": "granulate to sample"
}],
"custom_fields": {
"not allowed for new applications": true
},
"__v": 0
},
{
"_id": {"$oid":"500000000000000000000003"},
"comment": "",
"sample_references": [{
"sample_id": {"$oid":"400000000000000000000003"},
"relation": "part to sample"
}],
"custom_fields": {
"not allowed for new applications": true,
"another_field": "is there"
},
"__v": 0
}
],
"note_fields": [
{
"_id": {"$oid":"600000000000000000000001"},
"name": "not allowed for new applications",
"qty": 2,
"__v": 0
},
{
"_id": {"$oid":"600000000000000000000002"},
"name": "another_field",
"qty": 1,
"__v": 0
}
],
"materials": [
{
"_id": {"$oid":"100000000000000000000001"},
"name": "Stanyl TW 200 F8",
"supplier_id": {"$oid":"110000000000000000000001"},
"group_id": {"$oid":"900000000000000000000001"},
"mineral": 0,
"glass_fiber": 40,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": "5514263423"
},
{
"color": "natural",
"number": "5514263422"
}
],
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000002"},
"name": "Ultramid T KR 4355 G7",
"supplier_id": {"$oid":"110000000000000000000002"},
"group_id": {"$oid":"900000000000000000000002"},
"mineral": 0,
"glass_fiber": 35,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": "5514212901"
},
{
"color": "signalviolet",
"number": "5514612901"
}
],
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000003"},
"name": "PA GF 50 black (2706)",
"supplier_id": {"$oid":"110000000000000000000003"},
"group_id": {"$oid":"900000000000000000000003"},
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
"numbers": [
],
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000004"},
"name": "Schulamid 66 GF 25 H",
"supplier_id": {"$oid":"110000000000000000000004"},
"group_id": {"$oid":"900000000000000000000004"},
"mineral": 0,
"glass_fiber": 25,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": "5513933405"
}
],
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000005"},
"name": "Amodel A 1133 HS",
"supplier_id": {"$oid":"110000000000000000000005"},
"group_id": {"$oid":"900000000000000000000005"},
"mineral": 0,
"glass_fiber": 33,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": "5514262406"
}
],
"status": 10,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000006"},
"name": "PK-HM natural (4773)",
"supplier_id": {"$oid":"110000000000000000000003"},
"group_id": {"$oid":"900000000000000000000006"},
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
"numbers": [
{
"color": "natural",
"number": "10000000"
}
],
"status": -1,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000007"},
"name": "Ultramid A4H",
"supplier_id": {"$oid":"110000000000000000000002"},
"group_id": {"$oid":"900000000000000000000004"},
"mineral": 0,
"glass_fiber": 0,
"carbon_fiber": 0,
"numbers": [
{
"color": "black",
"number": ""
}
],
"status": 0,
"__v": 0
},
{
"_id": {"$oid":"100000000000000000000008"},
"name": "Latamid 66 H 2 G 30",
"supplier_id": {"$oid":"110000000000000000000006"},
"group_id": {"$oid":"900000000000000000000004"},
"mineral": 0,
"glass_fiber": 30,
"carbon_fiber": 0,
"numbers": [
{
"color": "blue",
"number": "5513943509"
}
],
"status": -1,
"__v": 0
}
],
"material_groups": [
{
"_id": {"$oid":"900000000000000000000001"},
"name": "PA46",
"__v": 0
},
{
"_id": {"$oid":"900000000000000000000002"},
"name": "PA6/6T",
"__v": 0
},
{
"_id": {"$oid":"900000000000000000000003"},
"name": "PA66+PA6I/6T",
"__v": 0
},
{
"_id": {"$oid":"900000000000000000000004"},
"name": "PA66",
"__v": 0
},
{
"_id": {"$oid":"900000000000000000000005"},
"name": "PPA",
"__v": 0
},
{
"_id": {"$oid":"900000000000000000000006"},
"name": "PK",
"__v": 0
}
],
"material_suppliers": [
{
"_id": {"$oid":"110000000000000000000001"},
"name": "DSM",
"__v": 0
},
{
"_id": {"$oid":"110000000000000000000002"},
"name": "BASF",
"__v": 0
},
{
"_id": {"$oid":"110000000000000000000003"},
"name": "Akro-Plastic",
"__v": 0
},
{
"_id": {"$oid":"110000000000000000000004"},
"name": "Schulmann",
"__v": 0
},
{
"_id": {"$oid":"110000000000000000000005"},
"name": "Solvay",
"__v": 0
},
{
"_id": {"$oid":"110000000000000000000006"},
"name": "LATI",
"__v": 0
}
],
"measurements": [
{
"_id": {"$oid":"800000000000000000000001"},
"sample_id": {"$oid":"400000000000000000000001"},
"values": {
"dpt": [
[3997.12558,98.00555],
[3995.08519,98.03253],
[3993.04480,98.02657]
]
},
"status": 10,
"measurement_template": {"$oid":"300000000000000000000001"},
"__v": 0
},
{
"_id": {"$oid":"800000000000000000000002"},
"sample_id": {"$oid":"400000000000000000000002"},
"values": {
"weight %": 0.5,
"standard deviation": 0.2
},
"status": 10,
"measurement_template": {"$oid":"300000000000000000000002"},
"__v": 0
},
{
"_id": {"$oid":"800000000000000000000003"},
"sample_id": {"$oid":"400000000000000000000003"},
"values": {
"val1": 1
},
"status": 0,
"measurement_template": {"$oid":"300000000000000000000003"},
"__v": 0
},
{
"_id": {"$oid":"800000000000000000000004"},
"sample_id": {"$oid":"400000000000000000000003"},
"values": {
"val1": 1
},
"status": -1,
"measurement_template": {"$oid":"300000000000000000000003"},
"__v": 0
},
{
"_id": {"$oid":"800000000000000000000005"},
"sample_id": {"$oid":"400000000000000000000002"},
"values": {
"weight %": 0.5,
"standard deviation":null
},
"status": 10,
"measurement_template": {"$oid":"300000000000000000000002"},
"__v": 0
},
{
"_id": {"$oid":"800000000000000000000006"},
"sample_id": {"$oid":"400000000000000000000006"},
"values": {
"weight %": 0.6,
"standard deviation":null
},
"status": 0,
"measurement_template": {"$oid":"300000000000000000000002"},
"__v": 0
},
{
"_id": {"$oid":"800000000000000000000007"},
"sample_id": {"$oid":"400000000000000000000001"},
"values": {
"dpt": [
[3996.12558,98.00555],
[3995.08519,98.03253],
[3993.04480,98.02657]
]
},
"status": 10,
"measurement_template": {"$oid":"300000000000000000000001"},
"__v": 0
}
],
"condition_templates": [
{
"_id": {"$oid":"200000000000000000000001"},
"first_id": {"$oid":"200000000000000000000001"},
"name": "heat treatment",
"version": 1,
"parameters": [
{
"name": "material",
"range": {
"values": [
"copper",
"hot air"
]
}
},
{
"name": "weeks",
"range": {
"min": 1,
"max": 10
}
}
],
"__v": 0
},
{
"_id": {"$oid":"200000000000000000000003"},
"first_id": {"$oid":"200000000000000000000003"},
"name": "raw material",
"version": 1,
"parameters": [
],
"__v": 0
},
{
"_id": {"$oid":"200000000000000000000004"},
"first_id": {"$oid":"200000000000000000000004"},
"name": "old condition",
"version": 1,
"parameters": [
{
"name": "p1",
"range": {}
}
],
"__v": 0
},
{
"_id": {"$oid":"200000000000000000000005"},
"first_id": {"$oid":"200000000000000000000004"},
"name": "new condition",
"version": 2,
"parameters": [
{
"name": "p11",
"range": {}
}
],
"__v": 0
}
],
"measurement_templates": [
{
"_id": {"$oid":"300000000000000000000001"},
"first_id": {"$oid":"300000000000000000000001"},
"name": "spectrum",
"version": 1,
"parameters": [
{
"name": "dpt",
"range": {
"type": "array"
}
}
],
"__v": 0
},
{
"_id": {"$oid":"300000000000000000000002"},
"first_id": {"$oid":"300000000000000000000002"},
"name": "kf",
"version": 1,
"parameters": [
{
"name": "weight %",
"range": {
"min": 0,
"max": 1.5
}
},
{
"name": "standard deviation",
"range": {
"min": 0,
"max": 0.5
}
}
],
"__v": 0
},
{
"_id": {"$oid":"300000000000000000000003"},
"first_id": {"$oid":"300000000000000000000003"},
"name": "mt 3",
"version": 1,
"parameters": [
{
"name": "val1",
"range": {
"values": [1,2,3]
}
}
],
"__v": 0
},
{
"_id": {"$oid":"300000000000000000000004"},
"first_id": {"$oid":"300000000000000000000003"},
"name": "mt 31",
"version": 2,
"parameters": [
{
"name": "val2",
"range": {
"values": [1,2,3,4]
}
}
],
"__v": 0
}
],
"users": [
{
"_id": {"$oid":"000000000000000000000001"},
"email": "user@bosch.com",
"name": "user",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "read",
"location": "Rng",
"device_name": "Alpha I",
"key": "000000000000000000001001",
"__v": 0
},
{
"_id": {"$oid":"000000000000000000000002"},
"email": "jane.doe@bosch.com",
"name": "janedoe",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "write",
"location": "Rng",
"device_name": "Alpha I",
"key": "000000000000000000001002",
"__v": 0
},
{
"_id": {"$oid":"000000000000000000000003"},
"email": "a.d.m.i.n@bosch.com",
"name": "admin",
"pass": "$2a$10$i872o3qR5V3JnbDArD8Z.eDo.BNPDBaR7dUX9KSEtl9pUjLyucy2K",
"level": "admin",
"location": "Rng",
"device_name": "",
"key": "000000000000000000001003",
"__v": "0"
},
{
"_id": {"$oid":"000000000000000000000004"},
"email": "johnny.doe@bosch.com",
"name": "johnnydoe",
"pass": "$2a$10$di26XKF63OG0V00PL1kSK.ceCcTxDExBMOg.jkHiCnXcY7cN7DlPi",
"level": "write",
"location": "Fe",
"device_name": "Alpha I",
"key": "000000000000000000001004",
"__v": 0
}
],
"changelogs": [
{
"_id" : {"$oid": "120000010000000000000000"},
"action" : "PUT /sample/400000000000000000000001",
"collectionName" : "samples",
"conditions" : {
"_id" : {"$oid": "400000000000000000000001"}
},
"data" : {
"type" : "part",
"status" : 0
},
"user_id" : {"$oid": "000000000000000000000003"},
"__v" : 0
},
{
"_id" : {"$oid": "120000020000000000000000"},
"action" : "PUT /sample/400000000000000000000001",
"collectionName" : "samples",
"conditions" : {
"_id" : {"$oid": "400000000000000000000001"}
},
"data" : {
"type" : "part",
"status" : 0
},
"user_id" : {"$oid": "000000000000000000000003"},
"__v" : 0
},
{
"_id" : {"$oid": "120000030000000000000000"},
"action" : "PUT /sample/400000000000000000000001",
"collectionName" : "samples",
"conditions" : {
"_id" : {"$oid": "400000000000000000000001"}
},
"data" : {
"type" : "part",
"status" : 0
},
"user_id" : {"$oid": "000000000000000000000003"},
"__v" : 0
},
{
"_id" : {"$oid": "120000040000000000000000"},
"action" : "PUT /sample/400000000000000000000001",
"collectionName" : "samples",
"conditions" : {
"_id" : {"$oid": "400000000000000000000001"}
},
"data" : {
"type" : "part",
"status" : 0
},
"user_id" : {"$oid": "000000000000000000000003"},
"__v" : 0
}
]
}
}

135
src/test/helper.ts Normal file
View File

@ -0,0 +1,135 @@
import supertest from 'supertest';
import should from 'should/as-function';
import _ from 'lodash';
import db from '../db';
import ChangelogModel from '../models/changelog';
import IdValidate from '../routes/validate/id';
export default class TestHelper {
public static auth = { // test user credentials
admin: {pass: 'Abc123!#', key: '000000000000000000001003', id: '000000000000000000000003'},
janedoe: {pass: 'Xyz890*)', key: '000000000000000000001002', id: '000000000000000000000002'},
user: {pass: 'Xyz890*)', key: '000000000000000000001001', id: '000000000000000000000001'},
johnnydoe: {pass: 'Xyz890*)', key: '000000000000000000001004', id: '000000000000000000000004'}
}
public static res = { // default responses
400: {status: 'Bad request'},
401: {status: 'Unauthorized'},
403: {status: 'Forbidden'},
404: {status: 'Not found'},
500: {status: 'Internal server error'}
}
static before (done) {
process.env.port = '2999';
process.env.NODE_ENV = 'test';
db.connect('test', done);
}
static beforeEach (server, done) {
delete require.cache[require.resolve('../index')]; // prevent loading from cache
server = require('../index');
db.drop(err => { // reset database
if (err) return done(err);
db.loadJson(require('./db.json'), done);
});
return server
}
static request (server, done, options) { // options in form: {method, url, contentType, auth: {key/basic: 'name' or 'key'/{name, pass}}, httpStatus, req, res, default (set to false if you want to dismiss default .end handling)}
let st = supertest(server);
if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('key')) { // resolve API key
options.url += '?key=' + (this.auth.hasOwnProperty(options.auth.key)? this.auth[options.auth.key].key : options.auth.key);
}
switch (options.method) { // http method
case 'get':
st = st.get(options.url)
break;
case 'post':
st = st.post(options.url)
break;
case 'put':
st = st.put(options.url)
break;
case 'delete':
st = st.delete(options.url)
break;
}
if (options.hasOwnProperty('reqType')) { // request body
st = st.type(options.reqType);
}
if (options.hasOwnProperty('req')) { // request body
st = st.send(options.req);
}
if (options.hasOwnProperty('auth') && options.auth.hasOwnProperty('basic')) { // resolve basic auth
if (this.auth.hasOwnProperty(options.auth.basic)) {
st = st.auth(options.auth.basic, this.auth[options.auth.basic].pass)
}
else {
st = st.auth(options.auth.basic.name, options.auth.basic.pass)
}
}
if (options.hasOwnProperty('contentType')) {
st = st.expect('Content-type', options.contentType).expect(options.httpStatus);
}
else {
st = st.expect('Content-type', /json/).expect(options.httpStatus);
}
if (options.hasOwnProperty('res')) { // evaluate result
return st.end((err, res) => {
if (err) return done (err);
should(res.body).be.eql(options.res);
done();
});
}
else if (this.res.hasOwnProperty(options.httpStatus) && options.default !== false) { // evaluate default results
return st.end((err, res) => {
if (err) return done (err);
should(res.body).be.eql(this.res[options.httpStatus]);
done();
});
}
else if (options.hasOwnProperty('log')) { // check changelog, takes log: {collection, skip, data/(dataAdd, dataIgn)}
return st.end(err => {
if (err) return done (err);
ChangelogModel.findOne({}).sort({_id: -1}).skip(options.log.skip? options.log.skip : 0).lean().exec((err, data) => { // latest entry
if (err) return done(err);
should(data).have.only.keys('_id', 'action', 'collectionName', 'conditions', 'data', 'user_id', '__v');
should(data).have.property('action', options.method.toUpperCase() + ' ' + options.url);
should(data).have.property('collectionName', options.log.collection);
if (options.log.hasOwnProperty('data')) {
should(data).have.property('data', options.log.data);
}
else {
const ignore = ['_id', '__v'];
if (options.log.hasOwnProperty('dataIgn')) {
ignore.push(...options.log.dataIgn);
}
let tmp = options.req ? options.req : {};
if (options.log.hasOwnProperty('dataAdd')) {
_.assign(tmp, options.log.dataAdd)
}
should(IdValidate.stringify(_.omit(data.data, ignore))).be.eql(_.omit(tmp, ignore));
}
if (data.user_id) {
should(data.user_id.toString()).be.eql(this.auth[options.auth.basic].id);
}
done();
});
});
}
else { // return object to do .end() manually
return st;
}
}
static afterEach (server, done) {
server.close(done);
}
static after(done) {
db.disconnect(done);
}
}

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

@ -0,0 +1,14 @@
import db from '../db';
// script to load test db into dev db for a clean start
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);
});
});
});

201
static/img/bosch-logo.svg Normal file
View File

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="bosch-lifeclip" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 435 155"
style="enable-background:new 0 0 435 155;" xml:space="preserve">
<style type="text/css">
.anker{fill:#606061;}
.bosch{fill-rule:evenodd;clip-rule:evenodd;fill:#EA0016;}
.claim{fill:#000000;}
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#942432;}
.st3{fill:#B22739;}
.st4{fill:#931915;}
.st5{fill:#AF1A19;}
.st6{fill:#D5151A;}
.st7{fill:url(#SVGID_2_);}
.st8{fill:url(#SVGID_3_);}
.st9{fill:url(#SVGID_4_);}
.st10{fill:url(#SVGID_5_);}
.st11{fill:url(#SVGID_6_);}
.st12{fill:#253783;}
.st13{fill:url(#SVGID_7_);}
.st14{fill:url(#SVGID_8_);}
.st15{fill:url(#SVGID_9_);}
.st16{fill:url(#SVGID_10_);}
.st17{fill:url(#SVGID_11_);}
.st18{fill:url(#SVGID_12_);}
.st19{fill:#159A39;}
.st20{fill:url(#SVGID_13_);}
.st21{fill:url(#SVGID_14_);}
.st22{fill:url(#SVGID_15_);}
.st23{fill:url(#SVGID_16_);}
.st24{fill:url(#SVGID_17_);}
.st25{fill:url(#SVGID_18_);}
.st26{fill:url(#SVGID_19_);}
.st27{fill:url(#SVGID_20_);}
.st28{fill:url(#SVGID_21_);}
.st29{fill:url(#SVGID_22_);}
.st30{fill:url(#SVGID_23_);}
.st31{fill:url(#SVGID_24_);}
.st32{fill:url(#SVGID_25_);}
.st33{fill:url(#SVGID_26_);}
.st34{fill:url(#SVGID_27_);}
.st35{fill:url(#SVGID_28_);}
.st36{fill:url(#SVGID_29_);}
.st37{fill:url(#SVGID_30_);}
.st38{fill:url(#SVGID_31_);}
.st39{fill:url(#SVGID_32_);}
.st40{fill:url(#SVGID_33_);}
.st41{fill:url(#SVGID_34_);}
.st42{fill:url(#SVGID_35_);}
.st43{fill:url(#SVGID_36_);}
.st44{fill:url(#SVGID_37_);}
.st45{fill:url(#SVGID_38_);}
.st46{fill:url(#SVGID_39_);}
.st47{fill:url(#SVGID_40_);}
.st48{fill:url(#SVGID_41_);}
.st49{fill:url(#SVGID_42_);}
.st50{fill:url(#SVGID_43_);}
.st51{fill:url(#SVGID_44_);}
.st52{fill:url(#SVGID_45_);}
.st53{fill:url(#SVGID_46_);}
.st54{fill:url(#SVGID_47_);}
.st55{fill:url(#SVGID_48_);}
.st56{fill:url(#SVGID_49_);}
.st57{fill:url(#SVGID_50_);}
.st58{fill:url(#SVGID_51_);}
.st59{fill:url(#SVGID_52_);}
.st60{fill:url(#SVGID_53_);}
.st61{fill:url(#SVGID_54_);}
.st62{fill:url(#SVGID_55_);}
.st63{fill:url(#SVGID_56_);}
</style>
<g id="box">
<g id="claim-english">
<path class="claim" d="M147.66699,107.6748v27.80957h-3.22852V107.6748H147.66699z"/>
<path class="claim" d="M157.896,115.14258v3.11133c1.24463-2.29492,3.30615-3.46191,6.18408-3.46191
c4.35645,0,6.729,2.83984,6.729,8.09082v12.60156h-3.22852v-12.83496c0-3.50098-1.32227-5.05664-4.31689-5.05664
c-3.26758,0-5.36768,2.13965-5.36768,5.44531v12.44629h-3.22803v-20.3418H157.896z"/>
<path class="claim" d="M178.39355,115.14258l5.44531,17.07422l5.32861-17.07422h3.34473l-6.84521,20.3418h-3.81201
l-6.96191-20.3418H178.39355z"/>
<path class="claim" d="M213.16455,132.25586c-1.8667,2.68359-4.47266,3.57812-7.73975,3.57812
c-6.45654,0-10.07373-4.23926-10.07373-10.46191c0-6.2627,3.77246-10.58008,9.25684-10.58008
c5.40625,0,8.7124,3.8125,8.7124,10.11328v1.0498H198.6958c0.07764,2.91699,0.73926,4.55078,2.13916,5.83398
c1.05029,0.93359,2.41162,1.36133,4.51172,1.36133c2.25586,0,3.88965-0.62207,5.44531-2.7998L213.16455,132.25586z
M210.01416,123.34961c-0.42773-4.16211-1.9834-5.87305-5.36719-5.87305c-3.22852,0-5.09521,1.90527-5.83447,5.87305H210.01416z"
/>
<path class="claim" d="M222.69336,115.14258v3.11133c1.24414-2.29492,3.30566-3.46191,6.18359-3.46191
c4.35645,0,6.72852,2.83984,6.72852,8.09082v12.60156h-3.22754v-12.83496c0-3.50098-1.32227-5.05664-4.31738-5.05664
c-3.26758,0-5.36719,2.13965-5.36719,5.44531v12.44629h-3.22852v-20.3418H222.69336z"/>
<path class="claim" d="M243.9668,115.14258v-5.29004l3.22754-0.77734v6.06738h5.13477v2.68359h-5.13477v12.40723
c0,1.9834,0.73926,2.91699,2.33398,2.91699c1.0498,0,1.82812-0.27148,2.76172-0.97168l1.24414,2.2168
c-1.36133,1.0498-2.52734,1.43848-4.2002,1.43848c-3.30566,0-5.36719-1.90527-5.36719-4.97852v-13.0293h-3.1123v-2.68359H243.9668
z"/>
<path class="claim" d="M274.61426,132.25586c-1.86719,2.68359-4.47266,3.57812-7.74023,3.57812
c-6.45605,0-10.07324-4.23926-10.07324-10.46191c0-6.2627,3.77246-10.58008,9.25684-10.58008
c5.40625,0,8.71191,3.8125,8.71191,10.11328v1.0498h-14.62402c0.07812,2.91699,0.73926,4.55078,2.13965,5.83398
c1.0498,0.93359,2.41113,1.36133,4.51172,1.36133c2.25586,0,3.88867-0.62207,5.44531-2.7998L274.61426,132.25586z
M271.46387,123.34961c-0.42773-4.16211-1.9834-5.87305-5.36719-5.87305c-3.22852,0-5.0957,1.90527-5.83398,5.87305H271.46387z"/>
<path class="claim" d="M294.64453,132.2168c-1.51758,2.37305-3.8125,3.61719-6.72949,3.61719
c-5.28906,0-8.71191-4.16113-8.71191-10.61816c0-6.30078,3.46191-10.42383,8.67383-10.42383
c2.87793,0,5.21094,1.28418,6.76758,3.69531v-12.29102h3.22754v29.28809h-3.22754V132.2168z M282.54785,125.41113
c0,5.13379,1.94434,7.73926,5.83398,7.73926c4.08398,0,6.37891-2.83887,6.37891-7.81738c0-4.8623-2.37207-7.85645-6.26172-7.85645
C284.6875,117.47656,282.54785,120.35449,282.54785,125.41113z"/>
<path class="claim" d="M318.79688,115.14258v-2.83984c0-4.16113,2.2168-6.53418,6.02832-6.53418
c1.36133,0,2.4502,0.27246,3.65625,0.93359l-0.81738,2.48926c-1.20508-0.58301-1.75-0.73926-2.68359-0.73926
c-1.86719,0-2.95605,1.32324-2.95605,3.50098v3.18945h4.66797v2.68359h-4.66797v17.6582h-3.22754v-17.6582h-2.91797v-2.68359
H318.79688z"/>
<path class="claim" d="M348.08398,125.29395c0,6.14551-4.00586,10.54004-9.64551,10.54004
c-5.52344,0-9.56836-4.43359-9.56836-10.50098c0-6.14551,4.00586-10.54102,9.68457-10.54102
C344.07812,114.79199,348.08398,119.22656,348.08398,125.29395z M332.21484,125.37207c0,4.78418,2.41113,7.77832,6.22363,7.77832
c3.85059,0,6.30078-3.0332,6.30078-7.81738c0-4.74512-2.4502-7.85645-6.18457-7.85645
C334.62598,117.47656,332.21484,120.43262,332.21484,125.37207z"/>
<path class="claim" d="M356.05664,115.14258v3.2666c1.32227-2.4502,3.07227-3.61719,5.44531-3.61719
c1.24414,0,2.2168,0.2334,3.30566,0.81738l-1.32227,2.72266c-0.89453-0.4668-1.43848-0.62207-2.37207-0.62207
c-3.15039,0-5.05664,2.52734-5.05664,6.61133v11.16309h-3.22852v-20.3418H356.05664z"/>
<path class="claim" d="M387.52148,134.8623c-0.97266,0.58301-1.98438,0.93359-3.5791,0.93359
c-2.95508,0-4.93945-1.32324-4.93945-5.36816v-24.23145h3.22852v24.50391c0,1.90625,1.01172,2.41113,2.2168,2.41113
c0.93359,0,1.51758-0.27148,2.02246-0.66113L387.52148,134.8623z"/>
<path class="claim" d="M395.96094,108.80273c0,1.16699-0.97266,2.13867-2.13965,2.13867s-2.13867-0.97168-2.13867-2.13867
s0.97168-2.13965,2.13867-2.13965S395.96094,107.63574,395.96094,108.80273z M395.49414,115.14258v20.3418h-3.22852v-20.3418
H395.49414z"/>
<path class="claim" d="M403.93359,115.14258v-2.83984c0-4.16113,2.2168-6.53418,6.02832-6.53418
c1.36133,0,2.4502,0.27246,3.65625,0.93359l-0.81738,2.48926c-1.20508-0.58301-1.75-0.73926-2.68359-0.73926
c-1.86719,0-2.95605,1.32324-2.95605,3.50098v3.18945h4.66797v2.68359h-4.66797v17.6582h-3.22754v-17.6582h-2.91797v-2.68359
H403.93359z"/>
<path class="claim" d="M431.89844,132.25586c-1.86719,2.68359-4.47266,3.57812-7.74023,3.57812
c-6.45605,0-10.07324-4.23926-10.07324-10.46191c0-6.2627,3.77246-10.58008,9.25684-10.58008
c5.40625,0,8.71191,3.8125,8.71191,10.11328v1.0498h-14.62402c0.07812,2.91699,0.73926,4.55078,2.13965,5.83398
c1.0498,0.93359,2.41113,1.36133,4.51172,1.36133c2.25586,0,3.88965-0.62207,5.44531-2.7998L431.89844,132.25586z
M428.74805,123.34961c-0.42773-4.16211-1.9834-5.87305-5.36719-5.87305c-3.22852,0-5.0957,1.90527-5.83398,5.87305H428.74805z"/>
</g>
<g id="bosch">
<g>
<path class="bosch" d="M185.19998,46.7c0,0,8.79999-3,8.79999-13c0-11.7-8.29999-17.5-19.70001-17.5h-29.89996v63.59999h32.5
c10,0,19.79999-7,19.79999-17.7C196.69998,49.39999,185.19998,46.8,185.19998,46.7z M160,29.39999h11.60001
c3.60001,0,6,2.39999,6,6c0,2.8-2.20001,5.8-6.29999,5.8h-11.39999L160,29.39999L160,29.39999z M171.69998,66.5h-11.60001V54
h11.30002c5.70001,0,8.39999,2.5,8.39999,6.2C179.79999,64.8,176.39999,66.5,171.69998,66.5z"/>
<path class="bosch" d="M231.10001,14.60001c-18.39999,0-29.20001,14.7-29.20001,33.3c0,18.7,10.79999,33.3,29.20001,33.3
c18.5,0,29.20001-14.60001,29.20001-33.3C260.29999,29.3,249.60001,14.60001,231.10001,14.60001z M231.10001,66
c-9,0-13.5-8.10001-13.5-18.10001s4.5-18,13.5-18s13.60001,8.10001,13.60001,18C244.69998,58,240.10001,66,231.10001,66z"/>
<path class="bosch" d="M294.19998,41.2l-2.20001-0.5c-5.39999-1.10001-9.70001-2.5-9.70001-6.39999
c0-4.2,4.10001-5.89999,7.70001-5.89999c5.29999,0,10,2.60001,13,5.89999l9.89999-9.8c-4.5-5.10001-11.79999-10-23.20001-10
c-13.39999,0-23.60001,7.5-23.60001,20c0,11.39999,8.20001,17,18.20001,19.10001l2.20001,0.5c8.29999,1.7,11.39999,3,11.39999,7
c0,3.8-3.39999,6.3-8.60001,6.3c-6.20001,0-11.79999-2.7-16.10001-8.2l-10.10001,10
c5.60001,6.7,12.70001,11.89999,26.39999,11.89999c11.89999,0,24.60001-6.8,24.60001-20.7
C314.29999,45.89999,303.29999,43.10001,294.19998,41.2z"/>
<path class="bosch" d="M349.69998,66c-7,0-14.29999-5.8-14.29999-18.5c0-11.3,6.79999-17.60001,13.89999-17.60001
c5.60001,0,8.89999,2.60001,11.5,7.10001l12.79999-8.5c-6.39999-9.7-14-13.8-24.5-13.8
c-19.20001,0-29.60001,14.89999-29.60001,32.90001c0,18.89999,11.5,33.7,29.39999,33.7
c12.60001,0,18.60001-4.39999,25.10001-13.8l-12.89999-8.7C358.5,63,355.69998,66,349.69998,66z"/>
<polygon class="bosch" points="416.30002,16.2 416.30002,39.60001 396.99997,39.60001 396.99997,16.2 380.29999,16.2
380.29999,79.8 396.99997,79.8 396.99997,54.7 416.30002,54.7 416.30002,79.8 432.99997,79.8 432.99997,16.2 "/>
</g>
<g>
<path class="bosch" d="M185.19998,46.7c0,0,8.79999-3,8.79999-13c0-11.7-8.29999-17.5-19.70001-17.5h-29.89996v63.59999h32.5
c10,0,19.79999-7,19.79999-17.7C196.69998,49.39999,185.19998,46.8,185.19998,46.7z M160,29.39999h11.60001
c3.60001,0,6,2.39999,6,6c0,2.8-2.20001,5.8-6.29999,5.8h-11.39999L160,29.39999L160,29.39999z M171.69998,66.5h-11.60001V54
h11.30002c5.70001,0,8.39999,2.5,8.39999,6.2C179.79999,64.8,176.39999,66.5,171.69998,66.5z"/>
<path class="bosch" d="M231.10001,14.60001c-18.39999,0-29.20001,14.7-29.20001,33.3c0,18.7,10.79999,33.3,29.20001,33.3
c18.5,0,29.20001-14.60001,29.20001-33.3C260.29999,29.3,249.60001,14.60001,231.10001,14.60001z M231.10001,66
c-9,0-13.5-8.10001-13.5-18.10001s4.5-18,13.5-18s13.60001,8.10001,13.60001,18C244.69998,58,240.10001,66,231.10001,66z"/>
<path class="bosch" d="M294.19998,41.2l-2.20001-0.5c-5.39999-1.10001-9.70001-2.5-9.70001-6.39999
c0-4.2,4.10001-5.89999,7.70001-5.89999c5.29999,0,10,2.60001,13,5.89999l9.89999-9.8c-4.5-5.10001-11.79999-10-23.20001-10
c-13.39999,0-23.60001,7.5-23.60001,20c0,11.39999,8.20001,17,18.20001,19.10001l2.20001,0.5c8.29999,1.7,11.39999,3,11.39999,7
c0,3.8-3.39999,6.3-8.60001,6.3c-6.20001,0-11.79999-2.7-16.10001-8.2l-10.10001,10
c5.60001,6.7,12.70001,11.89999,26.39999,11.89999c11.89999,0,24.60001-6.8,24.60001-20.7
C314.29999,45.89999,303.29999,43.10001,294.19998,41.2z"/>
<path class="bosch" d="M349.69998,66c-7,0-14.29999-5.8-14.29999-18.5c0-11.3,6.79999-17.60001,13.89999-17.60001
c5.60001,0,8.89999,2.60001,11.5,7.10001l12.79999-8.5c-6.39999-9.7-14-13.8-24.5-13.8
c-19.20001,0-29.60001,14.89999-29.60001,32.90001c0,18.89999,11.5,33.7,29.39999,33.7
c12.60001,0,18.60001-4.39999,25.10001-13.8l-12.89999-8.7C358.5,63,355.69998,66,349.69998,66z"/>
<polygon class="bosch" points="416.30002,16.2 416.30002,39.60001 396.99997,39.60001 396.99997,16.2 380.29999,16.2
380.29999,79.8 396.99997,79.8 396.99997,54.7 416.30002,54.7 416.30002,79.8 432.99997,79.8 432.99997,16.2 "/>
</g>
</g>
<g id="anker">
<g>
<path class="anker" d="M48.2,0C21.59999,0,0,21.60001,0,48.2s21.60001,48.2,48.2,48.2s48.2-21.60001,48.2-48.2S74.79999,0,48.2,0
z M48.2,91.89999c-24.10001,0-43.7-19.60001-43.7-43.7S24.10001,4.5,48.2,4.5s43.7,19.60001,43.7,43.7
S72.29999,91.89999,48.2,91.89999z"/>
<path class="anker" d="M68.09999,18.10001h-3.3v16.5H31.69998v-16.5h-3.39999c-9.7,6.5-16.2,17.5-16.2,30.10001
s6.5,23.60001,16.2,30.10001h3.39999v-16.5h33.10001v16.5h3.3c9.8-6.5,16.2-17.5,16.2-30.10001
S77.89999,24.60001,68.09999,18.10001z M27.09999,71.8c-6.7-5.89999-10.60001-14.39999-10.60001-23.60001
c0-9.2,3.89999-17.7,10.60001-23.60001V71.8z M64.79999,57.2H31.69998V39.09999h33.10001V57.2z M69.29999,71.7v-10l0,0V34.59999
l0,0v-10c6.60001,5.89999,10.5,14.39999,10.5,23.5C79.79999,57.3,75.89999,65.8,69.29999,71.7z"/>
</g>
<g>
<path class="anker" d="M48.2,0C21.59999,0,0,21.60001,0,48.2s21.60001,48.2,48.2,48.2s48.2-21.60001,48.2-48.2S74.79999,0,48.2,0
z M48.2,91.89999c-24.10001,0-43.7-19.60001-43.7-43.7S24.10001,4.5,48.2,4.5s43.7,19.60001,43.7,43.7
S72.29999,91.89999,48.2,91.89999z"/>
<path class="anker" d="M68.09999,18.10001h-3.3v16.5H31.69998v-16.5h-3.39999c-9.7,6.5-16.2,17.5-16.2,30.10001
s6.5,23.60001,16.2,30.10001h3.39999v-16.5h33.10001v16.5h3.3c9.8-6.5,16.2-17.5,16.2-30.10001
S77.89999,24.60001,68.09999,18.10001z M27.09999,71.8c-6.7-5.89999-10.60001-14.39999-10.60001-23.60001
c0-9.2,3.89999-17.7,10.60001-23.60001V71.8z M64.79999,57.2H31.69998V39.09999h33.10001V57.2z M69.29999,71.7v-10l0,0V34.59999
l0,0v-10c6.60001,5.89999,10.5,14.39999,10.5,23.5C79.79999,57.3,75.89999,65.8,69.29999,71.7z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

323
static/styles/swagger.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,21 @@
"target": "es5",
"outDir": "dist",
"sourceMap": true,
"esModuleInterop": true
"esModuleInterop": true,
"resolveJsonModule": true,
"incremental": true,
"diagnostics": true,
"typeRoots": [
"src/customTypings",
"node_modules/@types"
]
},
"files": [
"./node_modules/@types/node/index.d.ts"
],
"include": [
"src/**/*.ts"
"src/**/*.ts",
"src/**/*.json"
],
"exclude": [
"node_modules"