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:
commit
ca5b315a01
1
.gitignore
vendored
1
.gitignore
vendored
@ -112,3 +112,4 @@ dist
|
|||||||
**/.idea/tasks.xml
|
**/.idea/tasks.xml
|
||||||
**/.idea/shelf
|
**/.idea/shelf
|
||||||
**/.idea/*.iml
|
**/.idea/*.iml
|
||||||
|
/tmp/
|
||||||
|
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||||
|
</state>
|
||||||
|
</component>
|
15
.idea/dataSources.local.xml
Normal file
15
.idea/dataSources.local.xml
Normal 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
11
.idea/dataSources.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="@localhost" uuid="46f112fc-d60d-4217-873f-f5ffea06180c">
|
||||||
|
<driver-ref>mongo</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>com.dbschema.MongoJdbcDriver</jdbc-driver>
|
||||||
|
<jdbc-url>mongodb://localhost:27017</jdbc-url>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
584
.idea/dataSources/46f112fc-d60d-4217-873f-f5ffea06180c.xml
Normal file
584
.idea/dataSources/46f112fc-d60d-4217-873f-f5ffea06180c.xml
Normal 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
458
.idea/dbnavigator.xml
Normal file
@ -0,0 +1,458 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DBNavigator.Project.DataEditorManager">
|
||||||
|
<record-view-column-sorting-type value="BY_INDEX" />
|
||||||
|
<value-preview-text-wrapping value="true" />
|
||||||
|
<value-preview-pinned value="false" />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.DataExportManager">
|
||||||
|
<export-instructions>
|
||||||
|
<create-header value="true" />
|
||||||
|
<quote-values-containing-separator value="true" />
|
||||||
|
<quote-all-values value="false" />
|
||||||
|
<value-separator value="" />
|
||||||
|
<file-name value="" />
|
||||||
|
<file-location value="" />
|
||||||
|
<scope value="GLOBAL" />
|
||||||
|
<destination value="FILE" />
|
||||||
|
<format value="EXCEL" />
|
||||||
|
<charset value="windows-1252" />
|
||||||
|
</export-instructions>
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.DatabaseBrowserManager">
|
||||||
|
<autoscroll-to-editor value="false" />
|
||||||
|
<autoscroll-from-editor value="true" />
|
||||||
|
<show-object-properties value="true" />
|
||||||
|
<loaded-nodes />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.DatabaseFileManager">
|
||||||
|
<open-files />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.EditorStateManager">
|
||||||
|
<last-used-providers />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.MethodExecutionManager">
|
||||||
|
<method-browser />
|
||||||
|
<execution-history>
|
||||||
|
<group-entries value="true" />
|
||||||
|
<execution-inputs />
|
||||||
|
</execution-history>
|
||||||
|
<argument-values-cache />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.ObjectDependencyManager">
|
||||||
|
<last-used-dependency-type value="INCOMING" />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.ObjectQuickFilterManager">
|
||||||
|
<last-used-operator value="EQUAL" />
|
||||||
|
<filters />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.ScriptExecutionManager" clear-outputs="true">
|
||||||
|
<recently-used-interfaces />
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.Settings">
|
||||||
|
<connections />
|
||||||
|
<browser-settings>
|
||||||
|
<general>
|
||||||
|
<display-mode value="TABBED" />
|
||||||
|
<navigation-history-size value="100" />
|
||||||
|
<show-object-details value="false" />
|
||||||
|
</general>
|
||||||
|
<filters>
|
||||||
|
<object-type-filter>
|
||||||
|
<object-type name="SCHEMA" enabled="true" />
|
||||||
|
<object-type name="USER" enabled="true" />
|
||||||
|
<object-type name="ROLE" enabled="true" />
|
||||||
|
<object-type name="PRIVILEGE" enabled="true" />
|
||||||
|
<object-type name="CHARSET" enabled="true" />
|
||||||
|
<object-type name="TABLE" enabled="true" />
|
||||||
|
<object-type name="VIEW" enabled="true" />
|
||||||
|
<object-type name="MATERIALIZED_VIEW" enabled="true" />
|
||||||
|
<object-type name="NESTED_TABLE" enabled="true" />
|
||||||
|
<object-type name="COLUMN" enabled="true" />
|
||||||
|
<object-type name="INDEX" enabled="true" />
|
||||||
|
<object-type name="CONSTRAINT" enabled="true" />
|
||||||
|
<object-type name="DATASET_TRIGGER" enabled="true" />
|
||||||
|
<object-type name="DATABASE_TRIGGER" enabled="true" />
|
||||||
|
<object-type name="SYNONYM" enabled="true" />
|
||||||
|
<object-type name="SEQUENCE" enabled="true" />
|
||||||
|
<object-type name="PROCEDURE" enabled="true" />
|
||||||
|
<object-type name="FUNCTION" enabled="true" />
|
||||||
|
<object-type name="PACKAGE" enabled="true" />
|
||||||
|
<object-type name="TYPE" enabled="true" />
|
||||||
|
<object-type name="TYPE_ATTRIBUTE" enabled="true" />
|
||||||
|
<object-type name="ARGUMENT" enabled="true" />
|
||||||
|
<object-type name="DIMENSION" enabled="true" />
|
||||||
|
<object-type name="CLUSTER" enabled="true" />
|
||||||
|
<object-type name="DBLINK" enabled="true" />
|
||||||
|
</object-type-filter>
|
||||||
|
</filters>
|
||||||
|
<sorting>
|
||||||
|
<object-type name="COLUMN" sorting-type="NAME" />
|
||||||
|
<object-type name="FUNCTION" sorting-type="NAME" />
|
||||||
|
<object-type name="PROCEDURE" sorting-type="NAME" />
|
||||||
|
<object-type name="ARGUMENT" sorting-type="POSITION" />
|
||||||
|
</sorting>
|
||||||
|
<default-editors>
|
||||||
|
<object-type name="VIEW" editor-type="SELECTION" />
|
||||||
|
<object-type name="PACKAGE" editor-type="SELECTION" />
|
||||||
|
<object-type name="TYPE" editor-type="SELECTION" />
|
||||||
|
</default-editors>
|
||||||
|
</browser-settings>
|
||||||
|
<navigation-settings>
|
||||||
|
<lookup-filters>
|
||||||
|
<lookup-objects>
|
||||||
|
<object-type name="SCHEMA" enabled="true" />
|
||||||
|
<object-type name="USER" enabled="false" />
|
||||||
|
<object-type name="ROLE" enabled="false" />
|
||||||
|
<object-type name="PRIVILEGE" enabled="false" />
|
||||||
|
<object-type name="CHARSET" enabled="false" />
|
||||||
|
<object-type name="TABLE" enabled="true" />
|
||||||
|
<object-type name="VIEW" enabled="true" />
|
||||||
|
<object-type name="MATERIALIZED VIEW" enabled="true" />
|
||||||
|
<object-type name="NESTED TABLE" enabled="false" />
|
||||||
|
<object-type name="COLUMN" enabled="false" />
|
||||||
|
<object-type name="INDEX" enabled="true" />
|
||||||
|
<object-type name="CONSTRAINT" enabled="true" />
|
||||||
|
<object-type name="DATASET TRIGGER" enabled="true" />
|
||||||
|
<object-type name="DATABASE TRIGGER" enabled="true" />
|
||||||
|
<object-type name="SYNONYM" enabled="false" />
|
||||||
|
<object-type name="SEQUENCE" enabled="true" />
|
||||||
|
<object-type name="PROCEDURE" enabled="true" />
|
||||||
|
<object-type name="FUNCTION" enabled="true" />
|
||||||
|
<object-type name="PACKAGE" enabled="true" />
|
||||||
|
<object-type name="TYPE" enabled="true" />
|
||||||
|
<object-type name="TYPE ATTRIBUTE" enabled="false" />
|
||||||
|
<object-type name="ARGUMENT" enabled="false" />
|
||||||
|
<object-type name="DIMENSION" enabled="false" />
|
||||||
|
<object-type name="CLUSTER" enabled="false" />
|
||||||
|
<object-type name="DBLINK" enabled="true" />
|
||||||
|
</lookup-objects>
|
||||||
|
<force-database-load value="false" />
|
||||||
|
<prompt-connection-selection value="true" />
|
||||||
|
<prompt-schema-selection value="true" />
|
||||||
|
</lookup-filters>
|
||||||
|
</navigation-settings>
|
||||||
|
<dataset-grid-settings>
|
||||||
|
<general>
|
||||||
|
<enable-zooming value="true" />
|
||||||
|
<enable-column-tooltip value="true" />
|
||||||
|
</general>
|
||||||
|
<sorting>
|
||||||
|
<nulls-first value="true" />
|
||||||
|
<max-sorting-columns value="4" />
|
||||||
|
</sorting>
|
||||||
|
<tracking-columns>
|
||||||
|
<columnNames value="" />
|
||||||
|
<visible value="true" />
|
||||||
|
<editable value="false" />
|
||||||
|
</tracking-columns>
|
||||||
|
</dataset-grid-settings>
|
||||||
|
<dataset-editor-settings>
|
||||||
|
<text-editor-popup>
|
||||||
|
<active value="false" />
|
||||||
|
<active-if-empty value="false" />
|
||||||
|
<data-length-threshold value="100" />
|
||||||
|
<popup-delay value="1000" />
|
||||||
|
</text-editor-popup>
|
||||||
|
<values-actions-popup>
|
||||||
|
<show-popup-button value="true" />
|
||||||
|
<element-count-threshold value="1000" />
|
||||||
|
<data-length-threshold value="250" />
|
||||||
|
</values-actions-popup>
|
||||||
|
<general>
|
||||||
|
<fetch-block-size value="100" />
|
||||||
|
<fetch-timeout value="30" />
|
||||||
|
<trim-whitespaces value="true" />
|
||||||
|
<convert-empty-strings-to-null value="true" />
|
||||||
|
<select-content-on-cell-edit value="true" />
|
||||||
|
<large-value-preview-active value="true" />
|
||||||
|
</general>
|
||||||
|
<filters>
|
||||||
|
<prompt-filter-dialog value="true" />
|
||||||
|
<default-filter-type value="BASIC" />
|
||||||
|
</filters>
|
||||||
|
<qualified-text-editor text-length-threshold="300">
|
||||||
|
<content-types>
|
||||||
|
<content-type name="Text" enabled="true" />
|
||||||
|
<content-type name="XML" enabled="true" />
|
||||||
|
<content-type name="DTD" enabled="true" />
|
||||||
|
<content-type name="HTML" enabled="true" />
|
||||||
|
<content-type name="XHTML" enabled="true" />
|
||||||
|
<content-type name="CSS" enabled="true" />
|
||||||
|
<content-type name="SQL" enabled="true" />
|
||||||
|
<content-type name="PL/SQL" enabled="true" />
|
||||||
|
<content-type name="JavaScript" enabled="true" />
|
||||||
|
<content-type name="JSON" enabled="true" />
|
||||||
|
<content-type name="JSON5" enabled="true" />
|
||||||
|
<content-type name="JSP" enabled="true" />
|
||||||
|
<content-type name="JSPx" enabled="true" />
|
||||||
|
<content-type name="ASP" enabled="true" />
|
||||||
|
<content-type name="YAML" enabled="true" />
|
||||||
|
</content-types>
|
||||||
|
</qualified-text-editor>
|
||||||
|
<record-navigation>
|
||||||
|
<navigation-target value="VIEWER" />
|
||||||
|
</record-navigation>
|
||||||
|
</dataset-editor-settings>
|
||||||
|
<code-editor-settings>
|
||||||
|
<general>
|
||||||
|
<show-object-navigation-gutter value="false" />
|
||||||
|
<show-spec-declaration-navigation-gutter value="true" />
|
||||||
|
<enable-spellchecking value="true" />
|
||||||
|
<enable-reference-spellchecking value="false" />
|
||||||
|
</general>
|
||||||
|
<confirmations>
|
||||||
|
<save-changes value="false" />
|
||||||
|
<revert-changes value="true" />
|
||||||
|
</confirmations>
|
||||||
|
</code-editor-settings>
|
||||||
|
<code-completion-settings>
|
||||||
|
<filters>
|
||||||
|
<basic-filter>
|
||||||
|
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="function" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="schema" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="role" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="user" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="privilege" selected="true" />
|
||||||
|
<user-schema>
|
||||||
|
<filter-element type="OBJECT" id="table" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="index" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="synonym" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="function" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="package" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="type" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||||
|
</user-schema>
|
||||||
|
<public-schema>
|
||||||
|
<filter-element type="OBJECT" id="table" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="view" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="materialized view" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="index" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="constraint" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="trigger" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="synonym" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="sequence" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="procedure" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="function" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="package" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="type" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="dimension" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="cluster" selected="false" />
|
||||||
|
<filter-element type="OBJECT" id="dblink" selected="false" />
|
||||||
|
</public-schema>
|
||||||
|
<any-schema>
|
||||||
|
<filter-element type="OBJECT" id="table" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="index" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="function" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="package" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="type" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||||
|
</any-schema>
|
||||||
|
</basic-filter>
|
||||||
|
<extended-filter>
|
||||||
|
<filter-element type="RESERVED_WORD" id="keyword" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="function" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="parameter" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="datatype" selected="true" />
|
||||||
|
<filter-element type="RESERVED_WORD" id="exception" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="schema" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="user" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="role" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="privilege" selected="true" />
|
||||||
|
<user-schema>
|
||||||
|
<filter-element type="OBJECT" id="table" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="index" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="function" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="package" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="type" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||||
|
</user-schema>
|
||||||
|
<public-schema>
|
||||||
|
<filter-element type="OBJECT" id="table" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="index" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="function" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="package" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="type" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||||
|
</public-schema>
|
||||||
|
<any-schema>
|
||||||
|
<filter-element type="OBJECT" id="table" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="materialized view" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="index" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="constraint" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="trigger" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="synonym" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="sequence" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="procedure" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="function" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="package" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="type" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dimension" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="cluster" selected="true" />
|
||||||
|
<filter-element type="OBJECT" id="dblink" selected="true" />
|
||||||
|
</any-schema>
|
||||||
|
</extended-filter>
|
||||||
|
</filters>
|
||||||
|
<sorting enabled="true">
|
||||||
|
<sorting-element type="RESERVED_WORD" id="keyword" />
|
||||||
|
<sorting-element type="RESERVED_WORD" id="datatype" />
|
||||||
|
<sorting-element type="OBJECT" id="column" />
|
||||||
|
<sorting-element type="OBJECT" id="table" />
|
||||||
|
<sorting-element type="OBJECT" id="view" />
|
||||||
|
<sorting-element type="OBJECT" id="materialized view" />
|
||||||
|
<sorting-element type="OBJECT" id="index" />
|
||||||
|
<sorting-element type="OBJECT" id="constraint" />
|
||||||
|
<sorting-element type="OBJECT" id="trigger" />
|
||||||
|
<sorting-element type="OBJECT" id="synonym" />
|
||||||
|
<sorting-element type="OBJECT" id="sequence" />
|
||||||
|
<sorting-element type="OBJECT" id="procedure" />
|
||||||
|
<sorting-element type="OBJECT" id="function" />
|
||||||
|
<sorting-element type="OBJECT" id="package" />
|
||||||
|
<sorting-element type="OBJECT" id="type" />
|
||||||
|
<sorting-element type="OBJECT" id="dimension" />
|
||||||
|
<sorting-element type="OBJECT" id="cluster" />
|
||||||
|
<sorting-element type="OBJECT" id="dblink" />
|
||||||
|
<sorting-element type="OBJECT" id="schema" />
|
||||||
|
<sorting-element type="OBJECT" id="role" />
|
||||||
|
<sorting-element type="OBJECT" id="user" />
|
||||||
|
<sorting-element type="RESERVED_WORD" id="function" />
|
||||||
|
<sorting-element type="RESERVED_WORD" id="parameter" />
|
||||||
|
</sorting>
|
||||||
|
<format>
|
||||||
|
<enforce-code-style-case value="true" />
|
||||||
|
</format>
|
||||||
|
</code-completion-settings>
|
||||||
|
<execution-engine-settings>
|
||||||
|
<statement-execution>
|
||||||
|
<fetch-block-size value="100" />
|
||||||
|
<execution-timeout value="20" />
|
||||||
|
<debug-execution-timeout value="600" />
|
||||||
|
<focus-result value="false" />
|
||||||
|
<prompt-execution value="false" />
|
||||||
|
</statement-execution>
|
||||||
|
<script-execution>
|
||||||
|
<command-line-interfaces />
|
||||||
|
<execution-timeout value="300" />
|
||||||
|
</script-execution>
|
||||||
|
<method-execution>
|
||||||
|
<execution-timeout value="30" />
|
||||||
|
<debug-execution-timeout value="600" />
|
||||||
|
<parameter-history-size value="10" />
|
||||||
|
</method-execution>
|
||||||
|
</execution-engine-settings>
|
||||||
|
<operation-settings>
|
||||||
|
<transactions>
|
||||||
|
<uncommitted-changes>
|
||||||
|
<on-project-close value="ASK" />
|
||||||
|
<on-disconnect value="ASK" />
|
||||||
|
<on-autocommit-toggle value="ASK" />
|
||||||
|
</uncommitted-changes>
|
||||||
|
<multiple-uncommitted-changes>
|
||||||
|
<on-commit value="ASK" />
|
||||||
|
<on-rollback value="ASK" />
|
||||||
|
</multiple-uncommitted-changes>
|
||||||
|
</transactions>
|
||||||
|
<session-browser>
|
||||||
|
<disconnect-session value="ASK" />
|
||||||
|
<kill-session value="ASK" />
|
||||||
|
<reload-on-filter-change value="false" />
|
||||||
|
</session-browser>
|
||||||
|
<compiler>
|
||||||
|
<compile-type value="KEEP" />
|
||||||
|
<compile-dependencies value="ASK" />
|
||||||
|
<always-show-controls value="false" />
|
||||||
|
</compiler>
|
||||||
|
<debugger>
|
||||||
|
<debugger-type value="JDBC" />
|
||||||
|
<use-generic-runners value="true" />
|
||||||
|
</debugger>
|
||||||
|
</operation-settings>
|
||||||
|
<ddl-file-settings>
|
||||||
|
<extensions>
|
||||||
|
<mapping file-type-id="VIEW" extensions="vw" />
|
||||||
|
<mapping file-type-id="TRIGGER" extensions="trg" />
|
||||||
|
<mapping file-type-id="PROCEDURE" extensions="prc" />
|
||||||
|
<mapping file-type-id="FUNCTION" extensions="fnc" />
|
||||||
|
<mapping file-type-id="PACKAGE" extensions="pkg" />
|
||||||
|
<mapping file-type-id="PACKAGE_SPEC" extensions="pks" />
|
||||||
|
<mapping file-type-id="PACKAGE_BODY" extensions="pkb" />
|
||||||
|
<mapping file-type-id="TYPE" extensions="tpe" />
|
||||||
|
<mapping file-type-id="TYPE_SPEC" extensions="tps" />
|
||||||
|
<mapping file-type-id="TYPE_BODY" extensions="tpb" />
|
||||||
|
</extensions>
|
||||||
|
<general>
|
||||||
|
<lookup-ddl-files value="true" />
|
||||||
|
<create-ddl-files value="false" />
|
||||||
|
<synchronize-ddl-files value="true" />
|
||||||
|
<use-qualified-names value="false" />
|
||||||
|
<make-scripts-rerunnable value="true" />
|
||||||
|
</general>
|
||||||
|
</ddl-file-settings>
|
||||||
|
<general-settings>
|
||||||
|
<regional-settings>
|
||||||
|
<date-format value="MEDIUM" />
|
||||||
|
<number-format value="UNGROUPED" />
|
||||||
|
<locale value="SYSTEM_DEFAULT" />
|
||||||
|
<use-custom-formats value="false" />
|
||||||
|
</regional-settings>
|
||||||
|
<environment>
|
||||||
|
<environment-types>
|
||||||
|
<environment-type id="development" name="Development" description="Development environment" color="-2430209/-12296320" readonly-code="false" readonly-data="false" />
|
||||||
|
<environment-type id="integration" name="Integration" description="Integration environment" color="-2621494/-12163514" readonly-code="true" readonly-data="false" />
|
||||||
|
<environment-type id="production" name="Production" description="Productive environment" color="-11574/-10271420" readonly-code="true" readonly-data="true" />
|
||||||
|
<environment-type id="other" name="Other" description="" color="-1576/-10724543" readonly-code="false" readonly-data="false" />
|
||||||
|
</environment-types>
|
||||||
|
<visibility-settings>
|
||||||
|
<connection-tabs value="true" />
|
||||||
|
<dialog-headers value="true" />
|
||||||
|
<object-editor-tabs value="true" />
|
||||||
|
<script-editor-tabs value="false" />
|
||||||
|
<execution-result-tabs value="true" />
|
||||||
|
</visibility-settings>
|
||||||
|
</environment>
|
||||||
|
</general-settings>
|
||||||
|
</component>
|
||||||
|
<component name="DBNavigator.Project.StatementExecutionManager">
|
||||||
|
<execution-variables />
|
||||||
|
</component>
|
||||||
|
</project>
|
12
.idea/dictionaries/VLE2FE.xml
Normal file
12
.idea/dictionaries/VLE2FE.xml
Normal 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>
|
@ -2,5 +2,6 @@
|
|||||||
<profile version="1.0">
|
<profile version="1.0">
|
||||||
<option name="myName" value="Project Default" />
|
<option name="myName" value="Project Default" />
|
||||||
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
|
<inspection_tool class="JSUnfilteredForInLoop" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
|
<inspection_tool class="ReservedWordUsedAsNameJS" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||||
</profile>
|
</profile>
|
||||||
</component>
|
</component>
|
@ -6,7 +6,10 @@ info:
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: |
|
description: |
|
||||||
This API gives access to the project database.<br>
|
This API gives access to the project database.<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>
|
There are a number of different user levels: <br>
|
||||||
<ul>
|
<ul>
|
||||||
<li>read: read access to the samples database</li>
|
<li>read: read access to the samples database</li>
|
||||||
@ -15,14 +18,31 @@ info:
|
|||||||
<li>dev: handling machine learning models</li>
|
<li>dev: handling machine learning models</li>
|
||||||
<li>admin: user administration</li>
|
<li>admin: user administration</li>
|
||||||
</ul>
|
</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:
|
servers:
|
||||||
|
- url: https://definma-api.apps.de1.bosch-iot-cloud.com
|
||||||
|
description: server on the BIC
|
||||||
- url: http://localhost:3000
|
- url: http://localhost:3000
|
||||||
description: local server
|
description: local server
|
||||||
- url: https://digital-fingerprint-of-plastics-api.apps.de1.bosch-iot-cloud.com/
|
|
||||||
description: server on the BIC
|
|
||||||
|
|
||||||
|
|
||||||
security:
|
security:
|
||||||
@ -34,19 +54,17 @@ tags:
|
|||||||
- name: /
|
- name: /
|
||||||
- name: /sample
|
- name: /sample
|
||||||
- name: /material
|
- name: /material
|
||||||
- name: /condition
|
|
||||||
- name: /measurement
|
- name: /measurement
|
||||||
- name: /templates
|
- name: /template
|
||||||
- name: /model
|
- name: /model
|
||||||
- name: /user
|
- name: /user
|
||||||
|
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: 'others.yaml'
|
- $ref: 'root.yaml'
|
||||||
- $ref: 'sample.yaml'
|
- $ref: 'sample.yaml'
|
||||||
- $ref: 'material.yaml'
|
- $ref: 'material.yaml'
|
||||||
- $ref: 'condition.yaml'
|
|
||||||
- $ref: 'measurement.yaml'
|
- $ref: 'measurement.yaml'
|
||||||
- $ref: 'template.yaml'
|
- $ref: 'template.yaml'
|
||||||
- $ref: 'model.yaml'
|
- $ref: 'model.yaml'
|
248
api/material.yaml
Normal file
248
api/material.yaml
Normal 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
155
api/measurement.yaml
Normal 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
70
api/model.yaml
Normal 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
24
api/parameters.yaml
Normal 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
102
api/root.yaml
Normal 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
312
api/sample.yaml
Normal 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
202
api/schemas.yaml
Normal 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
213
api/template.yaml
Normal 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
255
api/user.yaml
Normal 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
4
build.bat
Normal 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
579
data_import/import.js
Normal 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, '');
|
||||||
|
}
|
@ -1,8 +1,9 @@
|
|||||||
---
|
---
|
||||||
applications:
|
applications:
|
||||||
- name: digital-fingerprint-of-plastics-api
|
- name: definma-api
|
||||||
|
path: dist/
|
||||||
instances: 1
|
instances: 1
|
||||||
memory: 256M
|
memory: 1024M
|
||||||
stack: cflinuxfs3
|
stack: cflinuxfs3
|
||||||
buildpacks:
|
buildpacks:
|
||||||
- nodejs_buildpack
|
- nodejs_buildpack
|
||||||
@ -10,4 +11,4 @@ applications:
|
|||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
OPTIMIZE_MEMORY: true
|
OPTIMIZE_MEMORY: true
|
||||||
services:
|
services:
|
||||||
- dfopdb
|
- definmadb
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -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'
|
|
@ -1,12 +0,0 @@
|
|||||||
Id:
|
|
||||||
name: id
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
||||||
Name:
|
|
||||||
name: name
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
schema:
|
|
||||||
type: string
|
|
108
oas/sample.yaml
108
oas/sample.yaml
@ -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'
|
|
164
oas/schemas.yaml
164
oas/schemas.yaml
@ -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
|
|
@ -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'
|
|
170
oas/user.yaml
170
oas/user.yaml
@ -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
2024
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
52
package.json
52
package.json
@ -4,29 +4,61 @@
|
|||||||
"description": "API for the digital fingerprint of plastics mongodb",
|
"description": "API for the digital fingerprint of plastics mongodb",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"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",
|
"test": "mocha dist/**/**.spec.js",
|
||||||
"start": "tsc && node dist/index.js",
|
"start": "node index.js",
|
||||||
"dev": "nodemon -e ts,yaml --exec \"npm run start\""
|
"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": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@apidevtools/json-schema-ref-parser": "^8.0.0",
|
"@apidevtools/json-schema-ref-parser": "^8.0.0",
|
||||||
"@types/mocha": "^5.2.7",
|
"@apidevtools/swagger-parser": "^9.0.1",
|
||||||
"@types/node": "^13.1.6",
|
"@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",
|
"cfenv": "^1.2.2",
|
||||||
|
"compression": "^1.7.4",
|
||||||
|
"content-filter": "^1.1.2",
|
||||||
|
"cors": "^2.8.5",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"helmet": "^3.22.0",
|
||||||
"json-schema": "^0.2.5",
|
"json-schema": "^0.2.5",
|
||||||
|
"json2csv": "^5.0.1",
|
||||||
|
"lodash": "^4.17.15",
|
||||||
|
"mongo-sanitize": "^1.1.0",
|
||||||
"mongoose": "^5.8.7",
|
"mongoose": "^5.8.7",
|
||||||
"nodemon": "^2.0.3",
|
"swagger-ui-express": "4.1.2"
|
||||||
"swagger-ui-express": "^4.1.2",
|
|
||||||
"tslint": "^5.20.1",
|
|
||||||
"typescript": "^3.7.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"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",
|
"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
48
src/api.ts
Normal 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
116
src/customTypes/express.ts
Normal 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 child’s 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
158
src/db.ts
Normal 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
17
src/globals.ts
Normal 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
105
src/helpers/authorize.ts
Normal 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
34
src/helpers/csv.ts
Normal 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
64
src/helpers/mail.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
106
src/index.ts
106
src/index.ts
@ -1,37 +1,21 @@
|
|||||||
import cfenv from 'cfenv';
|
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import mongoose from 'mongoose';
|
import bodyParser from 'body-parser';
|
||||||
import swagger from 'swagger-ui-express';
|
import compression from 'compression';
|
||||||
import jsonRefParser, {JSONSchema} from '@apidevtools/json-schema-ref-parser';
|
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
|
// tell if server is running in debug or production environment
|
||||||
console.log(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : '===== DEVELOPMENT =====');
|
console.info(process.env.NODE_ENV === 'production' ? '===== PRODUCTION =====' : process.env.NODE_ENV === 'test' ? '' :'===== 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}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
// mongodb connection
|
||||||
|
db.connect();
|
||||||
|
|
||||||
// create Express app
|
// create Express app
|
||||||
const app = express();
|
const app = express();
|
||||||
@ -40,20 +24,68 @@ app.disable('x-powered-by');
|
|||||||
// get port from environment, defaults to 3000
|
// get port from environment, defaults to 3000
|
||||||
const port = process.env.PORT || 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
|
// require routes
|
||||||
app.use('/', require('./routes/root'));
|
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
|
// Swagger UI
|
||||||
let oasDoc: JSONSchema = {};
|
app.use('/api-doc', api.serve(), api.setup());
|
||||||
jsonRefParser.bundle('oas/oas.yaml', (err, doc) => {
|
|
||||||
if(err) throw err;
|
app.use((req, res) => { // 404 error handling
|
||||||
oasDoc = doc;
|
res.status(404).json({status: 'Not found'});
|
||||||
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', 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
|
// hook up server to port
|
||||||
app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
console.log(`Listening on http;//localhost:${port}`);
|
console.info(process.env.NODE_ENV === 'test' ? '' : `Listening on http://localhost:${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
module.exports = server;
|
11
src/models/changelog.ts
Normal file
11
src/models/changelog.ts
Normal 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);
|
20
src/models/condition_template.ts
Normal file
20
src/models/condition_template.ts
Normal 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
28
src/models/material.ts
Normal 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);
|
14
src/models/material_groups.ts
Normal file
14
src/models/material_groups.ts
Normal 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);
|
14
src/models/material_suppliers.ts
Normal file
14
src/models/material_suppliers.ts
Normal 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
23
src/models/measurement.ts
Normal 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);
|
20
src/models/measurement_template.ts
Normal file
20
src/models/measurement_template.ts
Normal 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
19
src/models/note.ts
Normal 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
15
src/models/note_field.ts
Normal 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
29
src/models/sample.ts
Normal 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
20
src/models/user.ts
Normal 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
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
223
src/routes/material.ts
Normal 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'});
|
||||||
|
});
|
||||||
|
}
|
790
src/routes/measurement.spec.ts
Normal file
790
src/routes/measurement.spec.ts
Normal 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
169
src/routes/measurement.ts
Normal 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'});
|
||||||
|
});
|
||||||
|
}
|
@ -1,19 +1,256 @@
|
|||||||
import supertest from 'supertest';
|
import TestHelper from "../test/helper";
|
||||||
import should from 'should/as-function';
|
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 /', () => {
|
describe('GET /', () => {
|
||||||
it('returns the message object', done => {
|
it('returns the root message', done => {
|
||||||
server
|
TestHelper.request(server, done, {
|
||||||
.get('/')
|
method: 'get',
|
||||||
.expect('Content-type', /json/)
|
url: '/',
|
||||||
.expect(200)
|
httpStatus: 200,
|
||||||
.end(function(err, res) {
|
res: {status: 'API server up and running!'}
|
||||||
should(res.statusCode).equal(200);
|
});
|
||||||
should(res.body).be.eql({message: '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();
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,9 +1,35 @@
|
|||||||
import express from 'express';
|
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();
|
const router = express.Router();
|
||||||
|
|
||||||
router.get('/', (req, res) => {
|
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;
|
module.exports = router;
|
||||||
|
1930
src/routes/sample.spec.ts
Normal file
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
780
src/routes/sample.ts
Normal 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
898
src/routes/template.spec.ts
Normal 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
86
src/routes/template.ts
Normal 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
677
src/routes/user.spec.ts
Normal 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
163
src/routes/user.ts
Normal 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
29
src/routes/validate/id.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
116
src/routes/validate/material.ts
Normal file
116
src/routes/validate/material.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
56
src/routes/validate/measurement.ts
Normal file
56
src/routes/validate/measurement.ts
Normal 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
18
src/routes/validate/note_field.ts
Normal file
18
src/routes/validate/note_field.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
48
src/routes/validate/parameters.ts
Normal file
48
src/routes/validate/parameters.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
5
src/routes/validate/res400.ts
Normal file
5
src/routes/validate/res400.ts
Normal 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});
|
||||||
|
}
|
50
src/routes/validate/root.ts
Normal file
50
src/routes/validate/root.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
223
src/routes/validate/sample.ts
Normal file
223
src/routes/validate/sample.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
70
src/routes/validate/template.ts
Normal file
70
src/routes/validate/template.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
91
src/routes/validate/user.ts
Normal file
91
src/routes/validate/user.ts
Normal 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
673
src/test/db.json
Normal 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
135
src/test/helper.ts
Normal 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
14
src/test/loadDev.ts
Normal 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
201
static/img/bosch-logo.svg
Normal 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
323
static/styles/swagger.css
Normal file
File diff suppressed because one or more lines are too long
@ -4,13 +4,21 @@
|
|||||||
"target": "es5",
|
"target": "es5",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"esModuleInterop": true
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"incremental": true,
|
||||||
|
"diagnostics": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"src/customTypings",
|
||||||
|
"node_modules/@types"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"./node_modules/@types/node/index.d.ts"
|
"./node_modules/@types/node/index.d.ts"
|
||||||
],
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts"
|
"src/**/*.ts",
|
||||||
|
"src/**/*.json"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
"node_modules"
|
"node_modules"
|
||||||
|
Reference in New Issue
Block a user