Resolve "Transition to ESLint"

This commit is contained in:
Thea Schöbl
2022-06-27 14:40:09 +00:00
committed by Rainer Killinger
parent ca1d2444e0
commit 418ba67d15
47 changed files with 1854 additions and 1634 deletions

2
.eslintignore Normal file
View File

@@ -0,0 +1,2 @@
resources
openapi

3
.eslintrc.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "@openstapps"
}

126
ROUTES.md
View File

@@ -18,12 +18,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCBookAvailabilityRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityrequest) | | request | [SCBookAvailabilityRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityrequest) |
| response | [SCBookAvailabilityResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityresponse) | | response | [SCBookAvailabilityResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityresponse) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
## `POST /bulk/:UID` [Bulk add route](https://openstapps.gitlab.io/core/classes/_index.d_.scbulkaddroute.html) ## `POST /bulk/:UID` [Bulk add route](https://openstapps.gitlab.io/core/classes/_index.d_.scbulkaddroute.html)
@@ -34,13 +34,13 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCBulkAddRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbulkaddrequest) | | request | [SCBulkAddRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbulkaddrequest) |
| response | [SCBulkAddResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkaddresponse.html) | | response | [SCBulkAddResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkaddresponse.html) |
| success code | 201 | | success code | 201 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
| obligatory parameters | <table><tr><th>parameter</th><th>type</th></tr><tr><td>UID</td><td>[SCUuid](https://openstapps.gitlab.io/core/modules/_index.d_.html#scuuid)</td></tr></table> | | obligatory parameters | <table><tr><th>parameter</th><th>type</th></tr><tr><td>UID</td><td>[SCUuid](https://openstapps.gitlab.io/core/modules/_index.d_.html#scuuid)</td></tr></table> |
## `POST /bulk/:UID/done` [Bulk done route](https://openstapps.gitlab.io/core/classes/_index.d_.scbulkdoneroute.html) ## `POST /bulk/:UID/done` [Bulk done route](https://openstapps.gitlab.io/core/classes/_index.d_.scbulkdoneroute.html)
@@ -50,13 +50,13 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCBulkDoneRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdonerequest.html) | | request | [SCBulkDoneRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdonerequest.html) |
| response | [SCBulkDoneResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdoneresponse.html) | | response | [SCBulkDoneResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdoneresponse.html) |
| success code | 204 | | success code | 204 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
| obligatory parameters | <table><tr><th>parameter</th><th>type</th></tr><tr><td>UID</td><td>[SCUuid](https://openstapps.gitlab.io/core/modules/_index.d_.html#scuuid)</td></tr></table> | | obligatory parameters | <table><tr><th>parameter</th><th>type</th></tr><tr><td>UID</td><td>[SCUuid](https://openstapps.gitlab.io/core/modules/_index.d_.html#scuuid)</td></tr></table> |
## `POST /bulk` [Bulk route](https://openstapps.gitlab.io/core/classes/_index.d_.scbulkroute.html) ## `POST /bulk` [Bulk route](https://openstapps.gitlab.io/core/classes/_index.d_.scbulkroute.html)
@@ -66,12 +66,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCBulkRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkrequest.html) | | request | [SCBulkRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkrequest.html) |
| response | [SCBulkResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkresponse.html) | | response | [SCBulkResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkresponse.html) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
## `POST /feedback` [Feedback route](https://openstapps.gitlab.io/core/classes/_index.d_.scfeedbackroute.html) ## `POST /feedback` [Feedback route](https://openstapps.gitlab.io/core/classes/_index.d_.scfeedbackroute.html)
@@ -82,12 +82,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCFeedbackRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackrequest.html) | | request | [SCFeedbackRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackrequest.html) |
| response | [SCFeedbackResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackresponse.html) | | response | [SCFeedbackResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackresponse.html) |
| success code | 204 | | success code | 204 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
## `POST /` [Index route](https://openstapps.gitlab.io/core/classes/_index.d_.scindexroute.html) ## `POST /` [Index route](https://openstapps.gitlab.io/core/classes/_index.d_.scindexroute.html)
@@ -98,12 +98,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCIndexRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexrequest.html) | | request | [SCIndexRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexrequest.html) |
| response | [SCIndexResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexresponse.html) | | response | [SCIndexResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexresponse.html) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
## `POST /search/multi` [Multi search route](https://openstapps.gitlab.io/core/classes/_index.d_.scmultisearchroute.html) ## `POST /search/multi` [Multi search route](https://openstapps.gitlab.io/core/classes/_index.d_.scmultisearchroute.html)
@@ -114,12 +114,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCMultiSearchRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchrequest) | | request | [SCMultiSearchRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchrequest) |
| response | [SCMultiSearchResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchresponse) | | response | [SCMultiSearchResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchresponse) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCTooManyRequestsErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.sctoomanyrequestserrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCTooManyRequestsErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.sctoomanyrequestserrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
## `POST /plugin/register` [Plugin register route](https://openstapps.gitlab.io/core/classes/_index.d_.scpluginregisterroute.html) ## `POST /plugin/register` [Plugin register route](https://openstapps.gitlab.io/core/classes/_index.d_.scpluginregisterroute.html)
@@ -130,12 +130,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCPluginRegisterRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scpluginregisterrequest) | | request | [SCPluginRegisterRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scpluginregisterrequest) |
| response | [SCPluginRegisterResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scpluginregisterresponse.html) | | response | [SCPluginRegisterResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scpluginregisterresponse.html) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCParametersNotAcceptable](https://openstapps.gitlab.io/core/classes/_index.d_.scparametersnotacceptable.html)<br>[SCPluginAlreadyRegisteredErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scpluginalreadyregisterederrorresponse.html)<br>[SCPluginRegisteringFailedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scpluginregisteringfailederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCParametersNotAcceptable](https://openstapps.gitlab.io/core/classes/_index.d_.scparametersnotacceptable.html)<br>[SCPluginAlreadyRegisteredErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scpluginalreadyregisterederrorresponse.html)<br>[SCPluginRegisteringFailedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scpluginregisteringfailederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html) |
## `POST /search` [Search route](https://openstapps.gitlab.io/core/classes/_index.d_.scsearchroute.html) ## `POST /search` [Search route](https://openstapps.gitlab.io/core/classes/_index.d_.scsearchroute.html)
@@ -146,12 +146,12 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCSearchRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchrequest.html) | | request | [SCSearchRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchrequest.html) |
| response | [SCSearchResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchresponse.html) | | response | [SCSearchResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchresponse.html) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
## `PUT /:TYPE/:UID` [Thing update route](https://openstapps.gitlab.io/core/classes/_index.d_.scthingupdateroute.html) ## `PUT /:TYPE/:UID` [Thing update route](https://openstapps.gitlab.io/core/classes/_index.d_.scthingupdateroute.html)
@@ -162,11 +162,11 @@ This checks if a book is available in a library.<br>
### Definition ### Definition
| parameter | value | | parameter | value |
| --- | --- | |-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| request | [SCThingUpdateRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scthingupdaterequest) | | request | [SCThingUpdateRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scthingupdaterequest) |
| response | [SCThingUpdateResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scthingupdateresponse.html) | | response | [SCThingUpdateResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scthingupdateresponse.html) |
| success code | 200 | | success code | 200 |
| errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) | | errors | [SCInternalServerErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scinternalservererrorresponse.html)<br>[SCMethodNotAllowedErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scmethodnotallowederrorresponse.html)<br>[SCNotFoundErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scnotfounderrorresponse.html)<br>[SCRequestBodyTooLargeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.screquestbodytoolargeerrorresponse.html)<br>[SCSyntaxErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scsyntaxerrorresponse.html)<br>[SCUnsupportedMediaTypeErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scunsupportedmediatypeerrorresponse.html)<br>[SCValidationErrorResponse](https://openstapps.gitlab.io/core/classes/_index.d_.scvalidationerrorresponse.html) |
| obligatory parameters | <table><tr><th>parameter</th><th>type</th></tr><tr><td>TYPE</td><td>SCThingTypes</td></tr><tr><td>UID</td><td>[SCUuid](https://openstapps.gitlab.io/core/modules/_index.d_.html#scuuid)</td></tr></table> | | obligatory parameters | <table><tr><th>parameter</th><th>type</th></tr><tr><td>TYPE</td><td>SCThingTypes</td></tr><tr><td>UID</td><td>[SCUuid](https://openstapps.gitlab.io/core/modules/_index.d_.html#scuuid)</td></tr></table> |

1311
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,10 +12,10 @@
"Michel Jonathan Schmitz", "Michel Jonathan Schmitz",
"Rainer Killinger <mail-openstapps@killinger.co>", "Rainer Killinger <mail-openstapps@killinger.co>",
"Sebastian Lange", "Sebastian Lange",
"Wieland Schöbl" "Thea Schöbl <dev@theaninova.de>"
], ],
"scripts": { "scripts": {
"build": "npm run tslint && npm run compile", "build": "npm run lint && npm run compile",
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0 && git add CHANGELOG.md && git commit -m 'docs: update changelog'",
"check-configuration": "openstapps-configuration", "check-configuration": "openstapps-configuration",
"compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'", "compile": "rimraf lib && tsc && prepend lib/cli.js '#!/usr/bin/env node\n'",
@@ -29,35 +29,37 @@
"test": "npm run test-unit && npm run test-integration", "test": "npm run test-unit && npm run test-integration",
"test-unit": "env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 nyc mocha --require ts-node/register --exit 'test/**/*.spec.ts'", "test-unit": "env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 nyc mocha --require ts-node/register --exit 'test/**/*.spec.ts'",
"test-integration": "sudo docker-compose -f integration-test.yml pull && sudo docker-compose -f integration-test.yml up --build --abort-on-container-exit --exit-code-from apicli", "test-integration": "sudo docker-compose -f integration-test.yml pull && sudo docker-compose -f integration-test.yml up --build --abort-on-container-exit --exit-code-from apicli",
"tslint": "tslint -p tsconfig.json -c tslint.json 'src/**/*.ts'" "lint": "eslint -c .eslintrc.json --ignore-path .eslintignore --ext .ts src/ test/",
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts src/ test/"
}, },
"dependencies": { "dependencies": {
"@elastic/elasticsearch": "5.6.22", "@elastic/elasticsearch": "5.6.22",
"@openstapps/core": "0.66.1", "@openstapps/core": "0.68.0",
"@openstapps/core-tools": "0.30.1", "@openstapps/core-tools": "0.31.0",
"@openstapps/logger": "0.8.1", "@openstapps/logger": "0.8.1",
"@types/express-prometheus-middleware": "1.2.1", "@types/express-prometheus-middleware": "1.2.1",
"@types/node": "14.18.18", "@types/node": "14.18.21",
"config": "3.3.7", "config": "3.3.7",
"cors": "2.8.5", "cors": "2.8.5",
"express": "4.18.1", "express": "4.18.1",
"express-prometheus-middleware": "1.2.0", "express-prometheus-middleware": "1.2.0",
"express-promise-router": "4.1.1", "express-promise-router": "4.1.1",
"got": "11.8.3", "got": "11.8.5",
"moment": "2.29.3", "moment": "2.29.3",
"morgan": "1.10.0", "morgan": "1.10.0",
"nock": "13.2.4", "nock": "13.2.7",
"node-cache": "5.1.2", "node-cache": "5.1.2",
"node-cron": "3.0.0", "node-cron": "3.0.1",
"nodemailer": "6.7.5", "nodemailer": "6.7.5",
"prom-client": "14.0.1", "prom-client": "14.0.1",
"promise-queue": "2.2.5", "promise-queue": "2.2.5",
"ts-node": "10.8.0", "ts-node": "10.8.1",
"uuid": "8.3.2" "uuid": "8.3.2"
}, },
"devDependencies": { "devDependencies": {
"@openstapps/configuration": "0.29.1", "@openstapps/configuration": "0.32.0",
"@openstapps/es-mapping-generator": "0.1.0", "@openstapps/es-mapping-generator": "0.2.0",
"@openstapps/eslint-config": "1.1.0",
"@testdeck/mocha": "0.2.0", "@testdeck/mocha": "0.2.0",
"@types/chai": "4.3.1", "@types/chai": "4.3.1",
"@types/chai-as-promised": "7.1.5", "@types/chai-as-promised": "7.1.5",
@@ -74,21 +76,28 @@
"@types/sinon-express-mock": "1.3.9", "@types/sinon-express-mock": "1.3.9",
"@types/supertest": "2.0.12", "@types/supertest": "2.0.12",
"@types/uuid": "8.3.4", "@types/uuid": "8.3.4",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"chai": "4.3.6", "chai": "4.3.6",
"chai-as-promised": "7.1.1", "chai-as-promised": "7.1.1",
"conventional-changelog-cli": "2.2.2", "conventional-changelog-cli": "2.2.2",
"eslint": "8.18.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-jsdoc": "39.3.3",
"eslint-plugin-prettier": "4.1.0",
"eslint-plugin-unicorn": "42.0.0",
"get-port": "5.1.1", "get-port": "5.1.1",
"mocha": "10.0.0", "mocha": "10.0.0",
"mocked-env": "1.3.5", "mocked-env": "1.3.5",
"nyc": "15.1.0", "nyc": "15.1.0",
"prepend-file-cli": "1.0.6", "prepend-file-cli": "1.0.6",
"redoc-cli": "0.13.14", "prettier": "2.7.1",
"redoc-cli": "0.13.16",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"sinon": "14.0.0", "sinon": "14.0.0",
"sinon-express-mock": "2.2.1", "sinon-express-mock": "2.2.1",
"supertest": "6.2.3", "supertest": "6.2.3",
"tslint": "6.1.3", "typedoc": "0.22.17",
"typedoc": "0.22.15",
"typescript": "4.4.4" "typescript": "4.4.4"
}, },
"nyc": { "nyc": {

View File

@@ -24,7 +24,7 @@ import config from 'config';
import cors from 'cors'; import cors from 'cors';
import {Express} from 'express'; import {Express} from 'express';
import morgan from 'morgan'; import morgan from 'morgan';
import {join} from 'path'; import path from 'path';
import {configFile, DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common'; import {configFile, DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common';
import {getPrometheusMiddleware} from './middleware/prometheus'; import {getPrometheusMiddleware} from './middleware/prometheus';
import {MailQueue} from './notification/mail-queue'; import {MailQueue} from './notification/mail-queue';
@@ -43,26 +43,26 @@ import {DatabaseConstructor} from './storage/database';
/** /**
* Configure the backend * Configure the backend
*/ */
export async function configureApp(app: Express, databases: {[name: string]: DatabaseConstructor; }) { export async function configureApp(app: Express, databases: {[name: string]: DatabaseConstructor}) {
let integrationTestTimeout: NodeJS.Timeout; let integrationTestTimeout: NodeJS.Timeout;
// request loggers have to be the first middleware to be set in express // request loggers have to be the first middleware to be set in express
app.use(morgan('dev', { app.use(
skip: (_req, res) => { morgan('dev', {
if (process.env.NODE_ENV === 'integration-test') { skip: (_request, response) => {
clearTimeout(integrationTestTimeout); if (process.env.NODE_ENV === 'integration-test') {
integrationTestTimeout = setTimeout(() => { clearTimeout(integrationTestTimeout);
process.exit(1); integrationTestTimeout = setTimeout(() => {
}, process.exit(1);
DEFAULT_TIMEOUT); }, DEFAULT_TIMEOUT);
return false; return false;
} }
// tslint:disable-next-line: no-magic-numbers return response.statusCode < 400;
return res.statusCode < 400; },
stream: process.stdout,
}, stream: process.stdout, }),
})); );
if (process.env.PROMETHEUS_MIDDLEWARE === 'true') { if (process.env.PROMETHEUS_MIDDLEWARE === 'true') {
app.use(getPrometheusMiddleware()); app.use(getPrometheusMiddleware());
@@ -80,7 +80,7 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
'X-StApps-Version', 'X-StApps-Version',
], ],
credentials: true, credentials: true,
maxAge: 1728000, maxAge: 1_728_000,
methods: ['GET', 'POST', 'PUT', 'OPTIONS'], methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
optionsSuccessStatus: 204, optionsSuccessStatus: 204,
}; };
@@ -93,13 +93,13 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
app.options('*', [cors(corsOptions)]); app.options('*', [cors(corsOptions)]);
// only accept json as content type for all requests // only accept json as content type for all requests
app.use((req, res, next) => { app.use((request, response, next) => {
// Only accept json as content type // Only accept json as content type
if (req.is('application/json') !== 'application/json') { if (request.is('application/json') !== 'application/json') {
// return an error in the response // return an error in the response
const err = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment); const error = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment);
res.status(err.statusCode); response.status(error.statusCode);
res.json(err); response.json(error);
return; return;
} }
@@ -111,12 +111,12 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
bodySize += chunk.byteLength; bodySize += chunk.byteLength;
// when adding each chunk size to the total size, check how large it now is. // when adding each chunk size to the total size, check how large it now is.
if (bodySize > configFile.backend.maxRequestBodySize) { if (bodySize > configFile.backend.maxRequestBodySize) {
req.off('data', chunkGatherer); request.off('data', chunkGatherer);
req.off('end', endCallback); request.off('end', endCallback);
// return an error in the response // return an error in the response
const err = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment); const error = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment);
res.status(err.statusCode); response.status(error.statusCode);
res.json(err); response.json(error);
return; return;
} }
@@ -125,26 +125,24 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
}; };
const endCallback = () => { const endCallback = () => {
req.body = Buffer.concat(bodyBuffer) request.body = Buffer.concat(bodyBuffer).toString();
.toString();
try { try {
req.body = JSON.parse(req.body); request.body = JSON.parse(request.body);
next(); next();
} catch (catchErr) { } catch (error) {
const err = new SCSyntaxErrorResponse(catchErr.message, isTestEnvironment); const error_ = new SCSyntaxErrorResponse(error.message, isTestEnvironment);
res.status(err.statusCode); response.status(error_.statusCode);
res.json(err); response.json(error_);
return; return;
} }
}; };
req.on('data', chunkGatherer) request.on('data', chunkGatherer).on('end', endCallback);
.on('end', endCallback);
}); });
// validate config file // validate config file
await validator.addSchemas(join('node_modules', '@openstapps', 'core', 'lib', 'schema')); await validator.addSchemas(path.join('node_modules', '@openstapps', 'core', 'lib', 'schema'));
// validate the config file // validate the config file
const configValidation = validator.validate(configFile, 'SCConfigFile'); const configValidation = validator.validate(configFile, 'SCConfigFile');
@@ -161,17 +159,16 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
throw new Error('You have to configure a database'); throw new Error('You have to configure a database');
} }
const database = const database = new databases[config.get<string>('internal.database.name')](
new databases[config.get<string>('internal.database.name')]( configFile,
configFile, // mailQueue
// mailQueue typeof mailer !== 'undefined' && config.has('internal.monitoring') ? new MailQueue(mailer) : undefined,
typeof mailer !== 'undefined' && config.has('internal.monitoring') ? new MailQueue(mailer) : undefined, );
);
await database.init(); await database.init();
if (typeof database === 'undefined') { if (typeof database === 'undefined') {
throw new Error('No implementation for configured database found. Please check your configuration.'); throw new TypeError('No implementation for configured database found. Please check your configuration.');
} }
Logger.ok('Validated config file successfully'); Logger.ok('Validated config file successfully');
@@ -181,10 +178,7 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
app.enable('strict routing'); app.enable('strict routing');
// make the bulk storage available to all http middlewares/routes // make the bulk storage available to all http middlewares/routes
app.set( app.set('bulk', new BulkStorage(database));
'bulk',
new BulkStorage(database),
);
app.set('env', process.env.NODE_ENV); app.set('env', process.env.NODE_ENV);
@@ -202,15 +196,15 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
); );
// for plugins, as Express doesn't really want you to unregister routes (and doesn't offer any method to do so at all) // for plugins, as Express doesn't really want you to unregister routes (and doesn't offer any method to do so at all)
app.all('*', async (req, res, next) => { app.all('*', async (request, response, next) => {
// if the route exists then call virtual route on the plugin that registered that route // if the route exists then call virtual route on the plugin that registered that route
if (plugins.has(req.originalUrl)) { if (plugins.has(request.originalUrl)) {
try { try {
res.json(await virtualPluginRoute(req, plugins.get(req.originalUrl)!)); response.json(await virtualPluginRoute(request, plugins.get(request.originalUrl)!));
} catch (e) { } catch (error) {
// in case of error send an error response // in case of error send an error response
res.status(e.statusCode); response.status(error.statusCode);
res.json(e); response.json(error);
} }
} else { } else {
// pass to the next matching route (which is 404) // pass to the next matching route (which is 404)
@@ -219,9 +213,9 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
}); });
// add a route for a missing resource (404) // add a route for a missing resource (404)
app.use((_req, res) => { app.use((_request, response) => {
const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment); const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment);
res.status(errorResponse.statusCode); response.status(errorResponse.statusCode);
res.json(errorResponse); response.json(errorResponse);
}); });
} }

View File

@@ -24,7 +24,6 @@ const app = express();
/** /**
* Get port from environment and store in Express. * Get port from environment and store in Express.
*/ */
// tslint:disable-next-line: strict-boolean-expressions
const port = normalizePort(process.env.PORT || '3000'); const port = normalizePort(process.env.PORT || '3000');
/** /**
@@ -42,9 +41,9 @@ server.on('listening', onListening);
* Normalize a port into a number, string, or false. * Normalize a port into a number, string, or false.
*/ */
function normalizePort(value: string) { function normalizePort(value: string) {
const portNumber = parseInt(value, 10); const portNumber = Number.parseInt(value, 10);
if (isNaN(portNumber)) { if (Number.isNaN(portNumber)) {
// named pipe // named pipe
return value; return value;
} }
@@ -60,15 +59,12 @@ function normalizePort(value: string) {
/** /**
* Event listener for HTTP server "error" event. * Event listener for HTTP server "error" event.
*/ */
// tslint:disable-next-line: completed-docs async function onError(error: {code: string; syscall: string}) {
async function onError(error: { code: string; syscall: string; }) {
if (error.syscall !== 'listen') { if (error.syscall !== 'listen') {
throw error; throw error;
} }
const bind = typeof port === 'string' const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
? `Pipe ${port}`
: `Port ${port}`;
// handle specific listen errors with friendly messages // handle specific listen errors with friendly messages
switch (error.code) { switch (error.code) {
@@ -92,13 +88,10 @@ function onListening() {
const addr = server.address(); const addr = server.address();
if (addr !== null) { if (addr !== null) {
const bind = typeof addr === 'string' const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
? `pipe ${addr}`
: `port ${addr.port}`;
Logger.ok(`Listening on ${bind}`); Logger.ok(`Listening on ${bind}`);
} else { } else {
// tslint:disable-next-line: no-floating-promises void Logger.error(`Failed to start binding`);
Logger.error(`Failed to start binding`);
} }
} }
@@ -108,6 +101,6 @@ configureApp(app, {elasticsearch: Elasticsearch})
// After app setup listen on provided port, on all network interfaces // After app setup listen on provided port, on all network interfaces
server.listen(port); server.listen(port);
}) })
.catch((err) => { .catch(error => {
throw err; throw error;
}); });

View File

@@ -51,4 +51,4 @@ export const coreVersion: string = configFile.backend.SCVersion;
/** /**
* The default timeout in milliseconds * The default timeout in milliseconds
*/ */
export const DEFAULT_TIMEOUT = 20000; export const DEFAULT_TIMEOUT = 20_000;

View File

@@ -24,9 +24,9 @@ type UserOptions = Parameters<typeof expressPrometheusMiddleware>[0];
* Create and configure a new Express Prometheus Middleware instance * Create and configure a new Express Prometheus Middleware instance
* *
* This function tries to configure the new instance with JSON read from * This function tries to configure the new instance with JSON read from
* `./conf/prometheus.json`. When this fails an instance configured with * `./conf/prometheus.json`. When this fails an instance configured with
* default options is returned. * default options is returned.
* *
* @returns express.Express * @returns express.Express
*/ */
export function getPrometheusMiddleware(): express.Express { export function getPrometheusMiddleware(): express.Express {
@@ -34,9 +34,9 @@ export function getPrometheusMiddleware(): express.Express {
let options: UserOptions = {}; let options: UserOptions = {};
try { try {
options = JSON.parse(fs.readFileSync(configFileName, 'utf-8')); options = JSON.parse(fs.readFileSync(configFileName, 'utf8'));
} catch(err) { } catch (error) {
Logger.warn('Could not get options for Prometheus Middleware.', err); Logger.warn('Could not get options for Prometheus Middleware.', error);
} }
return expressPrometheusMiddleware(options); return expressPrometheusMiddleware(options);

View File

@@ -103,8 +103,8 @@ export class BackendTransport {
if (successful) { if (successful) {
Logger.log('SMTP verification successful.'); Logger.log('SMTP verification successful.');
} }
} catch (err) { } catch (error) {
throw err; throw error;
} finally { } finally {
this.waitingForVerification = false; this.waitingForVerification = false;
} }

View File

@@ -22,7 +22,6 @@ import Queue from 'promise-queue';
* A queue that can send mails in serial * A queue that can send mails in serial
*/ */
export class MailQueue { export class MailQueue {
/** /**
* Number of allowed verification attempts after which the initialization of transport fails * Number of allowed verification attempts after which the initialization of transport fails
*/ */
@@ -52,10 +51,10 @@ export class MailQueue {
/** /**
* Creates a mail queue * Creates a mail queue
*
* @param transport Transport which is used for sending mails * @param transport Transport which is used for sending mails
*/ */
constructor(private readonly transport: SMTP) { constructor(private readonly transport: SMTP) {
this.queue = new Queue(1); this.queue = new Queue(1);
// this queue saves all request when the transport is not ready yet // this queue saves all request when the transport is not ready yet
@@ -80,7 +79,6 @@ export class MailQueue {
* Verify the given transport * Verify the given transport
*/ */
private checkForVerification() { private checkForVerification() {
if (this.verificationCounter >= MailQueue.MAX_VERIFICATION_ATTEMPTS) { if (this.verificationCounter >= MailQueue.MAX_VERIFICATION_ATTEMPTS) {
throw new Error('Failed to initialize the SMTP transport for the mail queue'); throw new Error('Failed to initialize the SMTP transport for the mail queue');
} }
@@ -94,9 +92,9 @@ export class MailQueue {
} else { } else {
Logger.ok('Transport for mail queue was verified. We can send mails now'); Logger.ok('Transport for mail queue was verified. We can send mails now');
// if the transport finally was verified send all our mails from the dry queue // if the transport finally was verified send all our mails from the dry queue
this.dryQueue.forEach(async (mail) => { for (const mail of this.dryQueue) {
await this.addToQueue(mail); void this.addToQueue(mail);
}); }
} }
} }
@@ -106,7 +104,8 @@ export class MailQueue {
* @param mail Information required for sending a mail * @param mail Information required for sending a mail
*/ */
public async push(mail: MailOptions) { public async push(mail: MailOptions) {
if (!this.transport.isVerified()) { // the transport has verification, but is not verified yet if (!this.transport.isVerified()) {
// the transport has verification, but is not verified yet
// push to a dry queue which gets pushed to the real queue when the transport is verified // push to a dry queue which gets pushed to the real queue when the transport is verified
this.dryQueue.push(mail); this.dryQueue.push(mail);
} else { } else {

View File

@@ -29,13 +29,12 @@ const bulkRouteModel = new SCBulkAddRoute();
*/ */
export const bulkAddRouter = createRoute<SCBulkAddRequest, SCBulkAddResponse>( export const bulkAddRouter = createRoute<SCBulkAddRequest, SCBulkAddResponse>(
bulkRouteModel, bulkRouteModel,
async (request, app, params) => { async (request, app, parameters) => {
const bulkMemory: BulkStorage = app.get('bulk'); const bulkMemory: BulkStorage = app.get('bulk');
const bulk = bulkMemory.read(params.UID); const bulk = bulkMemory.read(parameters.UID);
if (typeof bulk === 'undefined') { if (typeof bulk === 'undefined') {
Logger.warn(`Bulk with ${params.UID} not found.`); Logger.warn(`Bulk with ${parameters.UID} not found.`);
throw new SCNotFoundErrorResponse(isTestEnvironment); throw new SCNotFoundErrorResponse(isTestEnvironment);
} }

View File

@@ -13,7 +13,12 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {SCBulkDoneRequest, SCBulkDoneResponse, SCBulkDoneRoute, SCNotFoundErrorResponse} from '@openstapps/core'; import {
SCBulkDoneRequest,
SCBulkDoneResponse,
SCBulkDoneRoute,
SCNotFoundErrorResponse,
} from '@openstapps/core';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {isTestEnvironment} from '../common'; import {isTestEnvironment} from '../common';
import {BulkStorage} from '../storage/bulk-storage'; import {BulkStorage} from '../storage/bulk-storage';
@@ -29,13 +34,12 @@ const bulkDoneRouteModel = new SCBulkDoneRoute();
*/ */
export const bulkDoneRouter = createRoute<SCBulkDoneRequest, SCBulkDoneResponse>( export const bulkDoneRouter = createRoute<SCBulkDoneRequest, SCBulkDoneResponse>(
bulkDoneRouteModel, bulkDoneRouteModel,
async (_request, app, params) => { async (_request, app, parameters) => {
const bulkMemory: BulkStorage = app.get('bulk'); const bulkMemory: BulkStorage = app.get('bulk');
const bulk = bulkMemory.read(params.UID); const bulk = bulkMemory.read(parameters.UID);
if (typeof bulk === 'undefined') { if (typeof bulk === 'undefined') {
Logger.warn(`Bulk with ${params.UID} not found.`); Logger.warn(`Bulk with ${parameters.UID} not found.`);
throw new SCNotFoundErrorResponse(isTestEnvironment); throw new SCNotFoundErrorResponse(isTestEnvironment);
} }

View File

@@ -25,11 +25,8 @@ const bulkRouteModel = new SCBulkRoute();
/** /**
* Implementation of the bulk request route (SCBulkRoute) * Implementation of the bulk request route (SCBulkRoute)
*/ */
export const bulkRouter = createRoute<SCBulkRequest, SCBulkResponse>( export const bulkRouter = createRoute<SCBulkRequest, SCBulkResponse>(bulkRouteModel, async (request, app) => {
bulkRouteModel, const bulkMemory: BulkStorage = app.get('bulk');
async (request, app) => {
const bulkMemory: BulkStorage = app.get('bulk');
return bulkMemory.create(request); return bulkMemory.create(request);
}, });
);

View File

@@ -14,10 +14,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
// the list provides option to easily implement "isHttpMethod" guard // the list provides option to easily implement "isHttpMethod" guard
const httpVerbs = ['get', 'post', 'put', 'delete', 'patch', 'options', const httpVerbs = [
'head', 'checkout', 'copy', 'lock', 'merge', 'mkactivity', 'mkcol', 'get',
'move', 'm-search', 'notify', 'purge', 'report', 'search', 'subscribe', 'post',
'trace', 'unlock','unsubscribe'] as const; 'put',
'delete',
'patch',
'options',
'head',
'checkout',
'copy',
'lock',
'merge',
'mkactivity',
'mkcol',
'move',
'm-search',
'notify',
'purge',
'report',
'search',
'subscribe',
'trace',
'unlock',
'unsubscribe',
] as const;
/** /**
* Strings that can be used as HTTP verbs (e.g. in requests): 'get' | 'post' | 'put' | 'delete' etc. * Strings that can be used as HTTP verbs (e.g. in requests): 'get' | 'post' | 'put' | 'delete' etc.
*/ */
@@ -29,5 +50,5 @@ export type HTTPVerb = typeof httpVerbs[number];
* @param method A text (representing a method) to check * @param method A text (representing a method) to check
*/ */
export function isHttpMethod(method: string): method is HTTPVerb { export function isHttpMethod(method: string): method is HTTPVerb {
return (httpVerbs as unknown as string[]).indexOf(method) > -1; return (httpVerbs as unknown as string[]).includes(method);
} }

View File

@@ -31,30 +31,28 @@ const multiSearchRouteModel = new SCMultiSearchRoute();
/** /**
* Implementation of the multi search route (SCMultiSearchRoute) * Implementation of the multi search route (SCMultiSearchRoute)
*/ */
export const multiSearchRouter = createRoute export const multiSearchRouter = createRoute<
<SCMultiSearchRequest, SCMultiSearchResponse | SCTooManyRequestsErrorResponse>( SCMultiSearchRequest,
multiSearchRouteModel, SCMultiSearchResponse | SCTooManyRequestsErrorResponse
async (request, app) => { >(multiSearchRouteModel, async (request, app) => {
const bulkMemory: BulkStorage = app.get('bulk');
const queryNames = Object.keys(request);
const bulkMemory: BulkStorage = app.get('bulk'); if (queryNames.length > configFile.backend.maxMultiSearchRouteQueries) {
const queryNames = Object.keys(request); throw new SCTooManyRequestsErrorResponse(isTestEnvironment);
}
if (queryNames.length > configFile.backend.maxMultiSearchRouteQueries) { // get a map of promises for each query
throw new SCTooManyRequestsErrorResponse(isTestEnvironment); const searchRequests = queryNames.map(async queryName => {
} return bulkMemory.database.search(request[queryName]);
});
// get a map of promises for each query const listOfSearchResponses = await Promise.all(searchRequests);
const searchRequests = queryNames.map(async (queryName) => {
return bulkMemory.database.search(request[queryName]);
});
const listOfSearchResponses = await Promise.all(searchRequests); const response: {[queryName: string]: SCSearchResponse} = {};
for (const [index, queryName] of queryNames.entries()) {
response[queryName] = listOfSearchResponses[index];
}
const response: { [queryName: string]: SCSearchResponse; } = {}; return response;
queryNames.forEach((queryName, index) => { });
response[queryName] = listOfSearchResponses[index];
});
return response;
},
);

View File

@@ -34,8 +34,7 @@ const pluginRegisterRouteModel = new SCPluginRegisterRoute();
/** /**
* Implementation of the plugin registration route (SCPluginRegisterRoute) * Implementation of the plugin registration route (SCPluginRegisterRoute)
*/ */
export const pluginRegisterRouter = createRoute( export const pluginRegisterRouter = createRoute(pluginRegisterRouteModel, pluginRegisterHandler);
pluginRegisterRouteModel, pluginRegisterHandler);
/** /**
* Handles requests on route for registering plugins * Handles requests on route for registering plugins
@@ -43,8 +42,10 @@ export const pluginRegisterRouter = createRoute(
* @param request Request received for registering or unregistering a plugin * @param request Request received for registering or unregistering a plugin
* @param _app Express application * @param _app Express application
*/ */
export async function pluginRegisterHandler(request: SCPluginRegisterRequest, _app: Express.Application): export async function pluginRegisterHandler(
Promise<SCPluginRegisterResponse> { request: SCPluginRegisterRequest,
_app: Express.Application,
): Promise<SCPluginRegisterResponse> {
switch (request.action) { switch (request.action) {
case 'add': case 'add':
return addPlugin(request.plugin); return addPlugin(request.plugin);
@@ -66,7 +67,7 @@ function addPlugin(plugin: SCPluginMetaData): SCPluginRegisterResponse {
deepStrictEqual(previouslyRegistered, plugin); deepStrictEqual(previouslyRegistered, plugin);
return {success: true}; return {success: true};
} catch (error) { } catch {
throw new SCPluginAlreadyRegisteredErrorResponse( throw new SCPluginAlreadyRegisteredErrorResponse(
'Plugin already registered', 'Plugin already registered',
plugins.get(plugin.route)!, plugins.get(plugin.route)!,
@@ -80,8 +81,10 @@ function addPlugin(plugin: SCPluginMetaData): SCPluginRegisterResponse {
if (typeof configFile.app.features.plugins === 'undefined') { if (typeof configFile.app.features.plugins === 'undefined') {
configFile.app.features.plugins = {}; configFile.app.features.plugins = {};
} }
configFile.app.features.plugins[plugin.name] = {urlPath : plugin.route}; configFile.app.features.plugins[plugin.name] = {urlPath: plugin.route};
Logger.log(`Registered plugin (name: ${plugin.name}, address: ${plugin.address}) on the route "${plugin.route}".`); Logger.log(
`Registered plugin (name: ${plugin.name}, address: ${plugin.address}) on the route "${plugin.route}".`,
);
return {success: true}; return {success: true};
} }
@@ -93,9 +96,7 @@ function addPlugin(plugin: SCPluginMetaData): SCPluginRegisterResponse {
*/ */
function removePlugin(route: string): SCPluginRegisterResponse { function removePlugin(route: string): SCPluginRegisterResponse {
if (!plugins.has(route)) { if (!plugins.has(route)) {
throw new SCNotFoundErrorResponse( throw new SCNotFoundErrorResponse(isTestEnvironment);
isTestEnvironment,
);
} }
if (plugins.has(route)) { if (plugins.has(route)) {
const plugin = plugins.get(route)!; const plugin = plugins.get(route)!;

View File

@@ -39,7 +39,8 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
routeClass: SCRoute, routeClass: SCRoute,
handler: ( handler: (
validatedBody: REQUESTTYPE, validatedBody: REQUESTTYPE,
app: Application, params: { [parameterName: string]: string; }, app: Application,
parameters: {[parameterName: string]: string},
) => Promise<RETURNTYPE>, ) => Promise<RETURNTYPE>,
): Router { ): Router {
// create router // create router
@@ -54,81 +55,73 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
// check if route has a valid http verb // check if route has a valid http verb
if (isHttpMethod(verb)) { if (isHttpMethod(verb)) {
// create a route handler for the given HTTP method // create a route handler for the given HTTP method
route[verb](async (req, res) => { route[verb](async (request, response) => {
try { try {
// validate request // validate request
const requestValidation = validator.validate(req.body, routeClass.requestBodyName); const requestValidation = validator.validate(request.body, routeClass.requestBodyName);
if (requestValidation.errors.length > 0) { if (requestValidation.errors.length > 0) {
const error = new SCValidationErrorResponse( const error = new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
requestValidation.errors, response.status(error.statusCode);
isTestEnvironment, response.json(error);
);
res.status(error.statusCode);
res.json(error);
await Logger.error(error); await Logger.error(error);
return; return;
} }
// hand over request to handler with path parameters // hand over request to handler with path parameters
const response = await handler(req.body, req.app, req.params); const handlerResponse = await handler(request.body, request.app, request.params);
// validate response generated by handler // validate response generated by handler
const responseErrors: ValidationError[] = validator.validate(response, routeClass.responseBodyName).errors; const responseErrors: ValidationError[] = validator.validate(
handlerResponse,
routeClass.responseBodyName,
).errors;
if (responseErrors.length > 0) { if (responseErrors.length > 0) {
const validationError = new SCValidationErrorResponse( const validationError = new SCValidationErrorResponse(responseErrors, isTestEnvironment);
responseErrors,
isTestEnvironment,
);
// The validation error is not caused by faulty user input, but through an error that originates somewhere in // The validation error is not caused by faulty user input, but through an error that originates somewhere in
// the backend, therefore we use this "stacked" error. // the backend, therefore we use this "stacked" error.
const internalServerError = new SCInternalServerErrorResponse( const internalServerError = new SCInternalServerErrorResponse(validationError, isTestEnvironment);
validationError, response.status(internalServerError.statusCode);
isTestEnvironment, response.json(internalServerError);
);
res.status(internalServerError.statusCode);
res.json(internalServerError);
await Logger.error(internalServerError); await Logger.error(internalServerError);
return; return;
} }
// set status code // set status code
res.status(routeClass.statusCodeSuccess); response.status(routeClass.statusCodeSuccess);
// respond // respond
res.json(response); response.json(handlerResponse);
} catch (error) { } catch (error) {
// if the error response is allowed on the route // if the error response is allowed on the route
if (routeClass.errorNames.some((constructorType) => error instanceof constructorType)) { if (routeClass.errorNames.some(constructorType => error instanceof constructorType)) {
// respond with the error from the handler // respond with the error from the handler
res.status(error.statusCode); response.status(error.statusCode);
res.json(error); response.json(error);
await Logger.error(error); await Logger.error(error);
} else { } else {
// the error is not allowed so something went wrong // the error is not allowed so something went wrong
const internalServerError = new SCInternalServerErrorResponse( const internalServerError = new SCInternalServerErrorResponse(error, isTestEnvironment);
error, response.status(internalServerError.statusCode);
isTestEnvironment, response.json(internalServerError);
);
res.status(internalServerError.statusCode);
res.json(internalServerError);
await Logger.error(error); await Logger.error(error);
} }
} }
}); });
} else { } else {
throw new Error('Invalid HTTP verb in route definition. Please check route definitions in `@openstapps/core`'); throw new Error(
'Invalid HTTP verb in route definition. Please check route definitions in `@openstapps/core`',
);
} }
// return a SCMethodNotAllowedErrorResponse on all other HTTP methods // return a SCMethodNotAllowedErrorResponse on all other HTTP methods
route.all((_req, res) => { route.all((_request, response) => {
const error = new SCMethodNotAllowedErrorResponse(isTestEnvironment); const error = new SCMethodNotAllowedErrorResponse(isTestEnvironment);
res.status(error.statusCode); response.status(error.statusCode);
res.json(error); response.json(error);
Logger.warn(error); Logger.warn(error);
}); });

View File

@@ -14,11 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { import {SCInternalServerErrorResponse, SCPluginMetaData, SCValidationErrorResponse} from '@openstapps/core';
SCInternalServerErrorResponse,
SCPluginMetaData,
SCValidationErrorResponse,
} from '@openstapps/core';
import {Request} from 'express'; import {Request} from 'express';
import got from 'got'; import got from 'got';
import {configFile, isTestEnvironment, validator} from '../common'; import {configFile, isTestEnvironment, validator} from '../common';
@@ -26,35 +22,34 @@ import {configFile, isTestEnvironment, validator} from '../common';
/** /**
* Generic route function used to proxy actual requests to plugins * Generic route function used to proxy actual requests to plugins
* *
* @param req The request for a plugin resource * @param request The request for a plugin resource
* @param plugin Meta data of the plugin * @param plugin Meta data of the plugin
* @throws {SCInternalServerErrorResponse} On request/response validation or response from the plugin errors * @throws {SCInternalServerErrorResponse} On request/response validation or response from the plugin errors
*/ */
export async function virtualPluginRoute(req: Request, plugin: SCPluginMetaData): Promise<object> { export async function virtualPluginRoute(request: Request, plugin: SCPluginMetaData): Promise<object> {
let responseBody: object; let responseBody: object;
try { try {
const requestValidation = validator.validate(req.body, plugin.requestSchema); const requestValidation = validator.validate(request.body, plugin.requestSchema);
if (requestValidation.errors.length > 0) { if (requestValidation.errors.length > 0) {
// noinspection ExceptionCaughtLocallyJS
throw new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment); throw new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
} }
// send the request to the plugin (forward the body) and save the response // send the request to the plugin (forward the body) and save the response
const pluginResponse = await got.post( const pluginResponse = await got.post(plugin.route.replace(/^\//gi, ''), {
plugin.route.replace(/^\//gi, ''), prefixUrl: plugin.address,
{ json: request.body,
prefixUrl: plugin.address, timeout: configFile.backend.externalRequestTimeout,
json: req.body, responseType: 'json',
timeout: configFile.backend.externalRequestTimeout, });
responseType: 'json',
},
);
responseBody = pluginResponse.body as object; responseBody = pluginResponse.body as object;
const responseValidation = validator.validate(responseBody, plugin.responseSchema); const responseValidation = validator.validate(responseBody, plugin.responseSchema);
if (responseValidation.errors.length > 0) { if (responseValidation.errors.length > 0) {
// noinspection ExceptionCaughtLocallyJS
throw new SCValidationErrorResponse(responseValidation.errors, isTestEnvironment); throw new SCValidationErrorResponse(responseValidation.errors, isTestEnvironment);
} }
} catch (e) { } catch (error) {
// wrap exact error inside of the internal server error response // wrap exact error inside of the internal server error response
throw new SCInternalServerErrorResponse(e, isTestEnvironment); throw new SCInternalServerErrorResponse(error, isTestEnvironment);
} }
return responseBody; return responseBody;

View File

@@ -29,7 +29,6 @@ export type BulkOperation = 'create' | 'expired' | 'update';
* Describes an indexing process * Describes an indexing process
*/ */
export class Bulk implements SCBulkRequest { export class Bulk implements SCBulkRequest {
/** /**
* Expiration of the bulk * Expiration of the bulk
* *
@@ -70,19 +69,15 @@ export class Bulk implements SCBulkRequest {
/** /**
* Creates a new bulk process * Creates a new bulk process
*
* @param request Data needed for requesting a bulk * @param request Data needed for requesting a bulk
*/ */
constructor(request: SCBulkRequest) { constructor(request: SCBulkRequest) {
this.uid = v4(); this.uid = v4();
this.state = 'in progress'; this.state = 'in progress';
if (typeof request.expiration === 'string') { this.expiration =
this.expiration = request.expiration; typeof request.expiration === 'string' ? request.expiration : moment().add(1, 'hour').toISOString();
} else {
this.expiration = moment()
.add(1, 'hour')
.toISOString();
}
// when should this process be finished // when should this process be finished
// where does the process come from // where does the process come from
this.source = request.source; this.source = request.source;
@@ -102,10 +97,10 @@ export class BulkStorage {
/** /**
* Creates a new BulkStorage * Creates a new BulkStorage
*
* @param database the database that is controlled by this bulk storage * @param database the database that is controlled by this bulk storage
*/ */
constructor(public database: Database) { constructor(public database: Database) {
// a bulk lives 60 minutes if no expiration is given // a bulk lives 60 minutes if no expiration is given
// the cache is checked every 60 seconds // the cache is checked every 60 seconds
this.cache = new NodeCache({stdTTL: 3600, checkperiod: 60}); this.cache = new NodeCache({stdTTL: 3600, checkperiod: 60});
@@ -121,13 +116,12 @@ export class BulkStorage {
/** /**
* Saves a bulk process and assigns to it a user-defined ttl (time-to-live) * Saves a bulk process and assigns to it a user-defined ttl (time-to-live)
*
* @param bulk the bulk process to save * @param bulk the bulk process to save
* @returns the bulk process that was saved * @returns the bulk process that was saved
*/ */
private save(bulk: Bulk): Bulk { private save(bulk: Bulk): Bulk {
const expirationInSeconds = moment(bulk.expiration) const expirationInSeconds = moment(bulk.expiration).diff(moment.now()) / 1000;
// tslint:disable-next-line: no-magic-numbers
.diff(moment.now()) / 1000;
Logger.info('Bulk expires in ', expirationInSeconds, 'seconds'); Logger.info('Bulk expires in ', expirationInSeconds, 'seconds');
// save the item in the cache with it's expected expiration // save the item in the cache with it's expected expiration
@@ -138,6 +132,7 @@ export class BulkStorage {
/** /**
* Create and save a new bulk process * Create and save a new bulk process
*
* @param bulkRequest a request for a new bulk process * @param bulkRequest a request for a new bulk process
* @returns a promise that contains the new bulk process * @returns a promise that contains the new bulk process
*/ */
@@ -156,6 +151,7 @@ export class BulkStorage {
/** /**
* Delete a bulk process * Delete a bulk process
*
* @param uid uid of the bulk process * @param uid uid of the bulk process
* @returns a promise that contains the deleted bulk process * @returns a promise that contains the deleted bulk process
*/ */
@@ -163,7 +159,7 @@ export class BulkStorage {
const bulk = this.read(uid); const bulk = this.read(uid);
if (typeof bulk === 'undefined') { if (typeof bulk === 'undefined') {
throw new Error(`Bulk that should be deleted was not found. UID was "${uid}"`); throw new TypeError(`Bulk that should be deleted was not found. UID was "${uid}"`);
} }
// delete the bulk process from the cache // delete the bulk process from the cache
@@ -177,6 +173,7 @@ export class BulkStorage {
/** /**
* Update an old bulk process (replace it with the new one) * Update an old bulk process (replace it with the new one)
*
* @param bulk new bulk process * @param bulk new bulk process
* @returns an empty promise * @returns an empty promise
*/ */
@@ -192,11 +189,11 @@ export class BulkStorage {
/** /**
* Read an existing bulk process * Read an existing bulk process
*
* @param uid uid of the bulk process * @param uid uid of the bulk process
* @returns a promise that contains a bulk * @returns a promise that contains a bulk
*/ */
public read(uid: string): Bulk | undefined { public read(uid: string): Bulk | undefined {
return this.cache.get(uid); return this.cache.get(uid);
} }
} }

View File

@@ -26,11 +26,11 @@ export type DatabaseConstructor = new (config: SCConfigFile, mailQueue?: MailQue
* Defines what one database class needs to have defined * Defines what one database class needs to have defined
*/ */
export interface Database { export interface Database {
/** /**
* Gets called if a bulk was created * Gets called if a bulk was created
* *
* The database should * The database should
*
* @param bulk A bulk to be created * @param bulk A bulk to be created
*/ */
bulkCreated(bulk: Bulk): Promise<void>; bulkCreated(bulk: Bulk): Promise<void>;
@@ -39,6 +39,7 @@ export interface Database {
* Gets called if a bulk expires * Gets called if a bulk expires
* *
* The database should delete all data that is associtated with this bulk * The database should delete all data that is associtated with this bulk
*
* @param bulk A bulk which data needs to be removed * @param bulk A bulk which data needs to be removed
*/ */
bulkExpired(bulk: Bulk): Promise<void>; bulkExpired(bulk: Bulk): Promise<void>;
@@ -55,6 +56,7 @@ export interface Database {
/** /**
* Get a single document * Get a single document
*
* @param uid Unique identifier of the document * @param uid Unique identifier of the document
*/ */
get(uid: SCUuid): Promise<SCThings>; get(uid: SCUuid): Promise<SCThings>;
@@ -66,6 +68,7 @@ export interface Database {
/** /**
* Add a thing to an existing bulk * Add a thing to an existing bulk
*
* @param thing A StAppsCore thing to be added * @param thing A StAppsCore thing to be added
* @param bulk A bulk to which the thing should be added * @param bulk A bulk to which the thing should be added
*/ */
@@ -82,7 +85,8 @@ export interface Database {
/** /**
* Search for things * Search for things
*
* @param params Parameters which form a search query to search the backend data * @param params Parameters which form a search query to search the backend data
*/ */
search(params: SCSearchQuery): Promise<SCSearchResponse>; search(parameters: SCSearchQuery): Promise<SCSearchResponse>;
} }

View File

@@ -26,10 +26,10 @@ import {
/** /**
* Parses elasticsearch aggregations (response from es) to facets for the app * Parses elasticsearch aggregations (response from es) to facets for the app
*
* @param aggregationResponse - aggregations response from elasticsearch * @param aggregationResponse - aggregations response from elasticsearch
*/ */
export function parseAggregations(aggregationResponse: AggregationResponse): SCFacet[] { export function parseAggregations(aggregationResponse: AggregationResponse): SCFacet[] {
const facets: SCFacet[] = []; const facets: SCFacet[] = [];
// get all names of the types an aggregation is on // get all names of the types an aggregation is on
@@ -52,7 +52,7 @@ export function parseAggregations(aggregationResponse: AggregationResponse): SCF
// this should always be true in theory... // this should always be true in theory...
if (isESTermsFilter(field) && isBucketAggregation(realField) && realField.buckets.length > 0) { if (isESTermsFilter(field) && isBucketAggregation(realField) && realField.buckets.length > 0) {
const facet: SCFacet = { const facet: SCFacet = {
buckets: realField.buckets.map((bucket) => { buckets: realField.buckets.map(bucket => {
return { return {
count: bucket.doc_count, count: bucket.doc_count,
key: bucket.key, key: bucket.key,
@@ -71,7 +71,7 @@ export function parseAggregations(aggregationResponse: AggregationResponse): SCF
// the last part here means that it is a bucket aggregation // the last part here means that it is a bucket aggregation
} else if (isESTermsFilter(type) && !isNestedAggregation(realType) && realType.buckets.length > 0) { } else if (isESTermsFilter(type) && !isNestedAggregation(realType) && realType.buckets.length > 0) {
facets.push({ facets.push({
buckets: realType.buckets.map((bucket) => { buckets: realType.buckets.map(bucket => {
return { return {
count: bucket.doc_count, count: bucket.doc_count,
key: bucket.key, key: bucket.key,

View File

@@ -27,7 +27,6 @@ import {
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
// we only have the @types package because some things type definitions are still missing from the official // we only have the @types package because some things type definitions are still missing from the official
// @elastic/elasticsearch package // @elastic/elasticsearch package
// tslint:disable-next-line:no-implicit-dependencies
import {IndicesUpdateAliasesParamsAction, SearchResponse} from 'elasticsearch'; import {IndicesUpdateAliasesParamsAction, SearchResponse} from 'elasticsearch';
import moment from 'moment'; import moment from 'moment';
import {MailQueue} from '../../notification/mail-queue'; import {MailQueue} from '../../notification/mail-queue';
@@ -39,7 +38,8 @@ import {buildQuery, buildSort} from './query';
import {aggregations, putTemplate} from './templating'; import {aggregations, putTemplate} from './templating';
import { import {
AggregationResponse, AggregationResponse,
ElasticsearchConfig, ElasticsearchObject, ElasticsearchConfig,
ElasticsearchObject,
ElasticsearchQueryDisMaxConfig, ElasticsearchQueryDisMaxConfig,
ElasticsearchQueryQueryStringConfig, ElasticsearchQueryQueryStringConfig,
} from './types/elasticsearch'; } from './types/elasticsearch';
@@ -53,7 +53,6 @@ const indexRegex = /^stapps_([A-z0-9_]+)_([a-z0-9-_]+)_([-a-z0-9^_]+)$/;
* A database interface for elasticsearch * A database interface for elasticsearch
*/ */
export class Elasticsearch implements Database { export class Elasticsearch implements Database {
/** /**
* Length of the index UID used for generation of its name * Length of the index UID used for generation of its name
*/ */
@@ -90,7 +89,7 @@ export class Elasticsearch implements Database {
*/ */
static getElasticsearchUrl(): string { static getElasticsearchUrl(): string {
// check if we have a docker link // check if we have a docker link
if (process.env.ES_ADDR !== undefined ) { if (process.env.ES_ADDR !== undefined) {
return process.env.ES_ADDR; return process.env.ES_ADDR;
} }
@@ -100,6 +99,7 @@ export class Elasticsearch implements Database {
/** /**
* Gets the index name in elasticsearch for one SCThingType * Gets the index name in elasticsearch for one SCThingType
*
* @param type SCThingType of data in the index * @param type SCThingType of data in the index
* @param source source of data in the index * @param source source of data in the index
* @param bulk bulk process which created this index * @param bulk bulk process which created this index
@@ -115,10 +115,11 @@ export class Elasticsearch implements Database {
/** /**
* Provides the index UID (for its name) from the bulk UID * Provides the index UID (for its name) from the bulk UID
*
* @param uid Bulk UID * @param uid Bulk UID
*/ */
static getIndexUID(uid: SCUuid) { static getIndexUID(uid: SCUuid) {
return uid.substring(0, Elasticsearch.INDEX_UID_LENGTH); return uid.slice(0, Math.max(0, Elasticsearch.INDEX_UID_LENGTH));
} }
/** /**
@@ -131,6 +132,7 @@ export class Elasticsearch implements Database {
/** /**
* Checks for invalid character in alias names and removes them * Checks for invalid character in alias names and removes them
*
* @param alias The alias name * @param alias The alias name
* @param uid The UID of the current bulk (for debugging purposes) * @param uid The UID of the current bulk (for debugging purposes)
*/ */
@@ -140,26 +142,25 @@ export class Elasticsearch implements Database {
// spaces are included in some types, replace them with underscores // spaces are included in some types, replace them with underscores
if (formattedAlias.includes(' ')) { if (formattedAlias.includes(' ')) {
formattedAlias = formattedAlias.trim(); formattedAlias = formattedAlias.trim();
formattedAlias = formattedAlias.split(' ') formattedAlias = formattedAlias.split(' ').join('_');
.join('_');
} }
// List of invalid characters: https://www.elastic.co/guide/en/elasticsearch/reference/6.6/indices-create-index.html // List of invalid characters: https://www.elastic.co/guide/en/elasticsearch/reference/6.6/indices-create-index.html
['\\', '/', '*', '?', '"', '<', '>', '|', ',', '#'].forEach((value) => { for (const value of ['\\', '/', '*', '?', '"', '<', '>', '|', ',', '#']) {
if (formattedAlias.includes(value)) { if (formattedAlias.includes(value)) {
formattedAlias = formattedAlias.replace(value, ''); formattedAlias = formattedAlias.replace(value, '');
Logger.warn(`Type of the bulk ${uid} contains an invalid character '${value}'. This can lead to two bulks Logger.warn(`Type of the bulk ${uid} contains an invalid character '${value}'. This can lead to two bulks
having the same alias despite having different types, as invalid characters are removed automatically. having the same alias despite having different types, as invalid characters are removed automatically.
New alias name is "${formattedAlias}."`); New alias name is "${formattedAlias}."`);
} }
}); }
['-', '_', '+'].forEach((value) => { for (const value of ['-', '_', '+']) {
if (formattedAlias.charAt(0) === value) { if (formattedAlias.charAt(0) === value) {
formattedAlias = formattedAlias.substring(1); formattedAlias = formattedAlias.slice(1);
Logger.warn(`Type of the bulk ${uid} begins with '${value}'. This can lead to two bulks having the same Logger.warn(`Type of the bulk ${uid} begins with '${value}'. This can lead to two bulks having the same
alias despite having different types, as invalid characters are removed automatically. alias despite having different types, as invalid characters are removed automatically.
New alias name is "${formattedAlias}."`); New alias name is "${formattedAlias}."`);
} }
}); }
if (formattedAlias === '.' || formattedAlias === '..') { if (formattedAlias === '.' || formattedAlias === '..') {
Logger.warn(`Type of the bulk ${uid} is ${formattedAlias}. This is an invalid name, please consider using Logger.warn(`Type of the bulk ${uid} is ${formattedAlias}. This is an invalid name, please consider using
another one, as it will be replaced with 'alias_placeholder', which can lead to strange errors.`); another one, as it will be replaced with 'alias_placeholder', which can lead to strange errors.`);
@@ -176,22 +177,24 @@ export class Elasticsearch implements Database {
/** /**
* Create a new interface for elasticsearch * Create a new interface for elasticsearch
*
* @param config an assembled config file * @param config an assembled config file
* @param mailQueue a mailqueue for monitoring * @param mailQueue a mailqueue for monitoring
*/ */
constructor(private readonly config: SCConfigFile, mailQueue?: MailQueue) { constructor(private readonly config: SCConfigFile, mailQueue?: MailQueue) {
if (
if (typeof config.internal.database === 'undefined' typeof config.internal.database === 'undefined' ||
|| typeof config.internal.database.version !== 'string') { typeof config.internal.database.version !== 'string'
throw new Error('Database version is undefined. Check your config file'); ) {
throw new TypeError('Database version is undefined. Check your config file');
} }
this.client = new Client({ this.client = new Client({
node: Elasticsearch.getElasticsearchUrl(), node: Elasticsearch.getElasticsearchUrl(),
}); });
this.client.on(events.REQUEST, async (err: Error | null, result: ApiResponse<unknown>) => { this.client.on(events.REQUEST, async (error: Error | null, result: ApiResponse<unknown>) => {
if (err !== null) { if (error !== null) {
await Logger.error(err); await Logger.error(error);
} }
if (process.env.ES_DEBUG === 'true') { if (process.env.ES_DEBUG === 'true') {
Logger.log(result); Logger.log(result);
@@ -215,18 +218,20 @@ export class Elasticsearch implements Database {
// create a list of old indices that are not in use // create a list of old indices that are not in use
const oldIndicesToDelete: string[] = []; const oldIndicesToDelete: string[] = [];
let aliases: { let aliases:
[index: string]: { | {
/** [index: string]: {
* Aliases of an index /**
*/ * Aliases of an index
aliases: { */
[K in SCThingType]: unknown aliases: {
}; [K in SCThingType]: unknown;
}; };
} | undefined; };
}
| undefined;
for(const retry of [...Array(RETRY_COUNT)].map((_, i) => i+1)) { for (const retry of [...Array.from({length: RETRY_COUNT})].map((_, i) => i + 1)) {
if (typeof aliases !== 'undefined') { if (typeof aliases !== 'undefined') {
break; break;
} }
@@ -241,16 +246,14 @@ export class Elasticsearch implements Database {
} }
if (typeof aliases === 'undefined') { if (typeof aliases === 'undefined') {
throw Error(`Failed to retrieve alias map after ${RETRY_COUNT} attempts!`); throw new TypeError(`Failed to retrieve alias map after ${RETRY_COUNT} attempts!`);
} }
for (const index in aliases) { for (const index in aliases) {
if (aliases.hasOwnProperty(index)) { if (aliases.hasOwnProperty(index)) {
const matches = indexRegex.exec(index); const matches = indexRegex.exec(index);
if (matches !== null) { if (matches !== null) {
const type = matches[1]; const type = matches[1];
// tslint:disable-next-line: no-magic-numbers
const source = matches[2]; const source = matches[2];
// check if there is an alias for the current index // check if there is an alias for the current index
@@ -278,12 +281,13 @@ export class Elasticsearch implements Database {
Logger.warn(`Deleted old indices: oldIndicesToDelete`); Logger.warn(`Deleted old indices: oldIndicesToDelete`);
} }
// tslint:disable-next-line: no-magic-numbers // eslint-disable-next-line unicorn/no-null
Logger.ok(`Read alias map from elasticsearch: ${JSON.stringify(this.aliasMap, null, 2)}`); Logger.ok(`Read alias map from elasticsearch: ${JSON.stringify(this.aliasMap, null, 2)}`);
} }
/** /**
* Provides an elasticsearch object using containing thing's UID * Provides an elasticsearch object using containing thing's UID
*
* @param uid an UID to use for the search * @param uid an UID to use for the search
* @returns an elasticsearch object containing the thing * @returns an elasticsearch object containing the thing
*/ */
@@ -309,6 +313,7 @@ export class Elasticsearch implements Database {
/** /**
* Should be called, when a new bulk was created. Creates a new index and applies a the mapping to the index * Should be called, when a new bulk was created. Creates a new index and applies a the mapping to the index
*
* @param bulk the bulk process that was created * @param bulk the bulk process that was created
*/ */
public async bulkCreated(bulk: Bulk): Promise<void> { public async bulkCreated(bulk: Bulk): Promise<void> {
@@ -346,6 +351,7 @@ export class Elasticsearch implements Database {
/** /**
* Should be called when a bulk process is expired. The index that was created with this bulk gets deleted * Should be called when a bulk process is expired. The index that was created with this bulk gets deleted
*
* @param bulk the bulk process that is expired * @param bulk the bulk process that is expired
*/ */
public async bulkExpired(bulk: Bulk): Promise<void> { public async bulkExpired(bulk: Bulk): Promise<void> {
@@ -365,6 +371,7 @@ export class Elasticsearch implements Database {
/** /**
* Should be called when a bulk process is updated (replaced by a newer bulk). This will replace the old * Should be called when a bulk process is updated (replaced by a newer bulk). This will replace the old
* index and publish all data, that was index in the new instead * index and publish all data, that was index in the new instead
*
* @param bulk the new bulk process that should replace the old one with same type and source * @param bulk the new bulk process that should replace the old one with same type and source
*/ */
public async bulkUpdated(bulk: Bulk): Promise<void> { public async bulkUpdated(bulk: Bulk): Promise<void> {
@@ -391,6 +398,7 @@ export class Elasticsearch implements Database {
} }
// create the new index if it does not exists // create the new index if it does not exists
// eslint-disable-next-line unicorn/no-await-expression-member
if (!(await this.client.indices.exists({index})).body) { if (!(await this.client.indices.exists({index})).body) {
// re-apply the index template before each new bulk operation // re-apply the index template before each new bulk operation
await putTemplate(this.client, bulk.type); await putTemplate(this.client, bulk.type);
@@ -411,6 +419,7 @@ export class Elasticsearch implements Database {
]; ];
// remove our old index if it exists // remove our old index if it exists
// noinspection SuspiciousTypeOfGuard
if (typeof oldIndex === 'string') { if (typeof oldIndex === 'string') {
actions.push({ actions.push({
remove: {index: oldIndex, alias: alias}, remove: {index: oldIndex, alias: alias},
@@ -431,6 +440,7 @@ export class Elasticsearch implements Database {
// swap the index in our aliasMap // swap the index in our aliasMap
this.aliasMap[alias][bulk.source] = index; this.aliasMap[alias][bulk.source] = index;
// noinspection SuspiciousTypeOfGuard
if (typeof oldIndex === 'string') { if (typeof oldIndex === 'string') {
// delete the old index // delete the old index
await this.client.indices.delete({index: oldIndex}); await this.client.indices.delete({index: oldIndex});
@@ -441,13 +451,14 @@ export class Elasticsearch implements Database {
/** /**
* Gets an SCThing from all indexed data * Gets an SCThing from all indexed data
*
* @param uid uid of an SCThing * @param uid uid of an SCThing
*/ */
public async get(uid: SCUuid): Promise<SCThings> { public async get(uid: SCUuid): Promise<SCThings> {
const object = await this.getObject(uid); const object = await this.getObject(uid);
if (typeof object === 'undefined') { if (typeof object === 'undefined') {
throw new Error('Item not found.'); throw new TypeError('Item not found.');
} }
return object._source; return object._source;
@@ -461,7 +472,9 @@ export class Elasticsearch implements Database {
if (typeof monitoringConfiguration !== 'undefined') { if (typeof monitoringConfiguration !== 'undefined') {
if (typeof this.mailQueue === 'undefined') { if (typeof this.mailQueue === 'undefined') {
throw new Error('Monitoring is defined, but MailQueue is undefined. A MailQueue is obligatory for monitoring.'); throw new TypeError(
'Monitoring is defined, but MailQueue is undefined. A MailQueue is obligatory for monitoring.',
);
} }
// read all watches and schedule searches on the client // read all watches and schedule searches on the client
await Monitoring.setUp(monitoringConfiguration, this.client, this.mailQueue); await Monitoring.setUp(monitoringConfiguration, this.client, this.mailQueue);
@@ -472,56 +485,55 @@ export class Elasticsearch implements Database {
/** /**
* Add an item to an index * Add an item to an index
*
* @param object the SCThing to add to the index * @param object the SCThing to add to the index
* @param bulk the bulk process which item belongs to * @param bulk the bulk process which item belongs to
*/ */
public async post(object: SCThings, bulk: Bulk): Promise<void> { public async post(object: SCThings, bulk: Bulk): Promise<void> {
const object_: SCThings & {creation_date: string} = {
// tslint:disable-next-line: completed-docs
const obj: SCThings & { creation_date: string; } = {
...object, ...object,
creation_date: moment() creation_date: moment().format(),
.format(),
}; };
const item = await this.getObject(object.uid); const item = await this.getObject(object.uid);
// check that the item will get replaced if the index is rolled over (index with the same name excluding ending uid) // check that the item will get replaced if the index is rolled over (index with the same name excluding ending uid)
if (typeof item !== 'undefined') { if (typeof item !== 'undefined') {
const indexOfNew = Elasticsearch.getIndex(obj.type, bulk.source, bulk); const indexOfNew = Elasticsearch.getIndex(object_.type, bulk.source, bulk);
const oldIndex = item._index; const oldIndex = item._index;
// new item doesn't replace the old one // new item doesn't replace the old one
// tslint:disable-next-line:no-magic-numbers if (
if (oldIndex.substring(0, oldIndex.lastIndexOf('_')) oldIndex.slice(0, Math.max(0, oldIndex.lastIndexOf('_'))) !==
!== indexOfNew.substring(0, indexOfNew.lastIndexOf('_'))) { indexOfNew.slice(0, Math.max(0, indexOfNew.lastIndexOf('_')))
) {
throw new Error( throw new Error(
// tslint:disable-next-line: no-magic-numbers // eslint-disable-next-line unicorn/no-null
`Object "${obj.uid}" already exists. Object was: ${JSON.stringify(obj, null, 2)}`, `Object "${object_.uid}" already exists. Object was: ${JSON.stringify(object_, null, 2)}`,
); );
} }
} }
// regular bulk update (item gets replaced when bulk is updated) // regular bulk update (item gets replaced when bulk is updated)
const searchResponse = await this.client.create({ const searchResponse = await this.client.create({
body: obj, body: object_,
id: obj.uid, id: object_.uid,
index: Elasticsearch.getIndex(obj.type, bulk.source, bulk), index: Elasticsearch.getIndex(object_.type, bulk.source, bulk),
timeout: '90s', timeout: '90s',
type: obj.type, type: object_.type,
}); });
if (!searchResponse.body.created) { if (!searchResponse.body.created) {
throw new Error(`Object creation Error: Instance was: ${JSON.stringify(obj)}`); throw new Error(`Object creation Error: Instance was: ${JSON.stringify(object_)}`);
} }
} }
/** /**
* Put (update) an existing item * Put (update) an existing item
*
* @param object SCThing to put * @param object SCThing to put
*/ */
public async put(object: SCThings): Promise<void> { public async put(object: SCThings): Promise<void> {
const item = await this.getObject(object.uid); const item = await this.getObject(object.uid);
if (typeof item !== 'undefined') { if (typeof item !== 'undefined') {
@@ -542,12 +554,12 @@ export class Elasticsearch implements Database {
/** /**
* Search all indexed data * Search all indexed data
* @param params search query *
* @param parameters search query
*/ */
public async search(params: SCSearchQuery): Promise<SCSearchResponse> { public async search(parameters: SCSearchQuery): Promise<SCSearchResponse> {
if (typeof this.config.internal.database === 'undefined') { if (typeof this.config.internal.database === 'undefined') {
throw new Error('Database is undefined. You have to configure the query build'); throw new TypeError('Database is undefined. You have to configure the query build');
} }
// create elasticsearch configuration out of data from database configuration // create elasticsearch configuration out of data from database configuration
@@ -557,23 +569,23 @@ export class Elasticsearch implements Database {
}; };
if (typeof this.config.internal.database.query !== 'undefined') { if (typeof this.config.internal.database.query !== 'undefined') {
esConfig.query = esConfig.query = this.config.internal.database.query as
this.config.internal.database | ElasticsearchQueryDisMaxConfig
.query as ElasticsearchQueryDisMaxConfig | ElasticsearchQueryQueryStringConfig; | ElasticsearchQueryQueryStringConfig;
} }
const searchRequest: RequestParams.Search = { const searchRequest: RequestParams.Search = {
body: { body: {
aggs: aggregations, aggs: aggregations,
query: buildQuery(params, this.config, esConfig), query: buildQuery(parameters, this.config, esConfig),
}, },
from: params.from, from: parameters.from,
index: Elasticsearch.getListOfAllIndices(), index: Elasticsearch.getListOfAllIndices(),
size: params.size, size: parameters.size,
}; };
if (typeof params.sort !== 'undefined') { if (typeof parameters.sort !== 'undefined') {
searchRequest.body.sort = buildSort(params.sort); searchRequest.body.sort = buildSort(parameters.sort);
} }
// perform the search against elasticsearch // perform the search against elasticsearch
@@ -582,7 +594,7 @@ export class Elasticsearch implements Database {
// gather pagination information // gather pagination information
const pagination = { const pagination = {
count: response.body.hits.hits.length, count: response.body.hits.hits.length,
offset: (typeof params.from === 'number') ? params.from : 0, offset: typeof parameters.from === 'number' ? parameters.from : 0,
total: response.body.hits.total, total: response.body.hits.total,
}; };
@@ -593,7 +605,7 @@ export class Elasticsearch implements Database {
// we only directly return the _source documents // we only directly return the _source documents
// elasticsearch provides much more information, the user shouldn't see // elasticsearch provides much more information, the user shouldn't see
const data = response.body.hits.hits.map((hit) => { const data = response.body.hits.hits.map(hit => {
return hit._source; // SCThing return hit._source; // SCThing
}); });

View File

@@ -25,13 +25,13 @@ import {
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
// we only have the @types package because some things type definitions are still missing from the official // we only have the @types package because some things type definitions are still missing from the official
// @elastic/elasticsearch package // @elastic/elasticsearch package
// tslint:disable-next-line:no-implicit-dependencies
import {SearchResponse} from 'elasticsearch'; import {SearchResponse} from 'elasticsearch';
import cron from 'node-cron'; import cron from 'node-cron';
import {MailQueue} from '../../notification/mail-queue'; import {MailQueue} from '../../notification/mail-queue';
/** /**
* Check if the given condition fails on the given number of results and the condition * Check if the given condition fails on the given number of results and the condition
*
* @param condition condition * @param condition condition
* @param total number of results * @param total number of results
*/ */
@@ -48,6 +48,7 @@ function conditionFails(
/** /**
* Check if the min condition fails * Check if the min condition fails
*
* @param minimumLength Minimal length allowed * @param minimumLength Minimal length allowed
* @param total Number of results * @param total Number of results
*/ */
@@ -57,6 +58,7 @@ function minConditionFails(minimumLength: number, total: number) {
/** /**
* Check if the max condition fails * Check if the max condition fails
*
* @param maximumLength Maximal length allowed * @param maximumLength Maximal length allowed
* @param total Number of results * @param total Number of results
*/ */
@@ -66,6 +68,7 @@ function maxConditionFails(maximumLength: number, total: number) {
/** /**
* Run all the given actions * Run all the given actions
*
* @param actions actions to perform * @param actions actions to perform
* @param watcherName name of watcher that wants to perform them * @param watcherName name of watcher that wants to perform them
* @param triggerName name of trigger that triggered the watcher * @param triggerName name of trigger that triggered the watcher
@@ -79,38 +82,39 @@ function runActions(
total: number, total: number,
mailQueue: MailQueue, mailQueue: MailQueue,
) { ) {
for (const action of actions) {
actions.forEach(async (action) => { void (action.type === 'log'
if (action.type === 'log') { ? Logger.error(
await Logger.error( action.prefix,
action.prefix, `Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'`,
`Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'`, `Found ${total} hits instead`, `Found ${total} hits instead`,
action.message, action.message,
); )
} else { : mailQueue.push({
await mailQueue.push({ subject: action.subject,
subject: action.subject, text: `Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'
text: `Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'
${action.message} Found ${total} hits instead`, ${action.message} Found ${total} hits instead`,
to: action.recipients, to: action.recipients,
}); }));
} }
});
} }
/** /**
* Set up the triggers for the configured watchers * Set up the triggers for the configured watchers
*
* @param monitoringConfig configuration of the monitoring * @param monitoringConfig configuration of the monitoring
* @param esClient elasticsearch client * @param esClient elasticsearch client
* @param mailQueue mailQueue for mail actions * @param mailQueue mailQueue for mail actions
*/ */
export async function setUp(monitoringConfig: SCMonitoringConfiguration, esClient: Client, mailQueue: MailQueue) { export async function setUp(
monitoringConfig: SCMonitoringConfiguration,
esClient: Client,
mailQueue: MailQueue,
) {
// set up Watches // set up Watches
monitoringConfig.watchers.forEach((watcher) => { for (const watcher of monitoringConfig.watchers) {
// make a schedule for each trigger // make a schedule for each trigger
watcher.triggers.forEach((trigger) => { for (const trigger of watcher.triggers) {
switch (trigger.executionTime) { switch (trigger.executionTime) {
case 'hourly': case 'hourly':
trigger.executionTime = '5 * * * *'; trigger.executionTime = '5 * * * *';
@@ -127,21 +131,21 @@ export async function setUp(monitoringConfig: SCMonitoringConfiguration, esClien
cron.schedule(trigger.executionTime, async () => { cron.schedule(trigger.executionTime, async () => {
// execute watch (search->condition->action) // execute watch (search->condition->action)
const result: ApiResponse<SearchResponse<SCThings>> = const result: ApiResponse<SearchResponse<SCThings>> = await esClient.search(
await esClient.search(watcher.query as RequestParams.Search); watcher.query as RequestParams.Search,
);
// check conditions // check conditions
const total = result.body.hits.total; const total = result.body.hits.total;
watcher.conditions.forEach((condition) => { for (const condition of watcher.conditions) {
if (conditionFails(condition, total)) { if (conditionFails(condition, total)) {
runActions(watcher.actions, watcher.name, trigger.name, total, mailQueue); runActions(watcher.actions, watcher.name, trigger.name, total, mailQueue);
} }
}); }
}); });
}); }
}
});
Logger.log(`Scheduled ${monitoringConfig.watchers.length} watches`); Logger.log(`Scheduled ${monitoringConfig.watchers.length} watches`);
} }

View File

@@ -34,7 +34,8 @@ import {
ESFunctionScoreQuery, ESFunctionScoreQuery,
ESFunctionScoreQueryFunction, ESFunctionScoreQueryFunction,
ESGenericRange, ESGenericRange,
ESGenericSort, ESGeoBoundingBoxFilter, ESGenericSort,
ESGeoBoundingBoxFilter,
ESGeoDistanceFilter, ESGeoDistanceFilter,
ESGeoDistanceFilterArguments, ESGeoDistanceFilterArguments,
ESGeoDistanceSort, ESGeoDistanceSort,
@@ -55,19 +56,19 @@ import {
* It is possible to use all, with the exception of < and >, of them by escaping them with a \ * It is possible to use all, with the exception of < and >, of them by escaping them with a \
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html * https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html
* *
* @param str the string to escape the characters from * @param string_ the string to escape the characters from
*/ */
function escapeESReservedCharacters(str: string): string { function escapeESReservedCharacters(string_: string): string {
return str.replace(/[+\-=!(){}\[\]^"~*?:\\/]|(&&)|(\|\|)/g, '\\$&'); return string_.replace(/[+\-=!(){}\[\]^"~*?:\\/]|(&&)|(\|\|)/g, '\\$&');
} }
/** /**
* Builds a boolean filter. Returns an elasticsearch boolean filter * Builds a boolean filter. Returns an elasticsearch boolean filter
*
* @param booleanFilter a search boolean filter for the retrieval of the data * @param booleanFilter a search boolean filter for the retrieval of the data
* @returns elasticsearch boolean arguments object * @returns elasticsearch boolean arguments object
*/ */
export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBooleanFilterArguments<unknown> { export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBooleanFilterArguments<unknown> {
const result: ESBooleanFilterArguments<unknown> = { const result: ESBooleanFilterArguments<unknown> = {
minimum_should_match: 0, minimum_should_match: 0,
must: [], must: [],
@@ -76,16 +77,16 @@ export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBool
}; };
if (booleanFilter.arguments.operation === 'and') { if (booleanFilter.arguments.operation === 'and') {
result.must = booleanFilter.arguments.filters.map((filter) => buildFilter(filter)); result.must = booleanFilter.arguments.filters.map(filter => buildFilter(filter));
} }
if (booleanFilter.arguments.operation === 'or') { if (booleanFilter.arguments.operation === 'or') {
result.should = booleanFilter.arguments.filters.map((filter) => buildFilter(filter)); result.should = booleanFilter.arguments.filters.map(filter => buildFilter(filter));
result.minimum_should_match = 1; result.minimum_should_match = 1;
} }
if (booleanFilter.arguments.operation === 'not') { if (booleanFilter.arguments.operation === 'not') {
result.must_not = booleanFilter.arguments.filters.map((filter) => buildFilter(filter)); result.must_not = booleanFilter.arguments.filters.map(filter => buildFilter(filter));
} }
return result; return result;
@@ -93,22 +94,31 @@ export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBool
/** /**
* Converts Array of Filters to elasticsearch query-syntax * Converts Array of Filters to elasticsearch query-syntax
*
* @param filter A search filter for the retrieval of the data * @param filter A search filter for the retrieval of the data
*/ */
export function buildFilter(filter: SCSearchFilter): export function buildFilter(
ESTermFilter | ESGeoDistanceFilter | ESBooleanFilter<ESGeoShapeFilter | ESGeoBoundingBoxFilter> | ESGeoShapeFilter | ESBooleanFilter<unknown> | ESRangeFilter { filter: SCSearchFilter,
):
| ESTermFilter
| ESGeoDistanceFilter
| ESBooleanFilter<ESGeoShapeFilter | ESGeoBoundingBoxFilter>
| ESGeoShapeFilter
| ESBooleanFilter<unknown>
| ESRangeFilter {
switch (filter.type) { switch (filter.type) {
case 'value': case 'value':
return Array.isArray(filter.arguments.value) ? { return Array.isArray(filter.arguments.value)
terms: { ? {
[`${filter.arguments.field}.raw`]: filter.arguments.value, terms: {
}, [`${filter.arguments.field}.raw`]: filter.arguments.value,
} : { },
term: { }
[`${filter.arguments.field}.raw`]: filter.arguments.value, : {
}, term: {
}; [`${filter.arguments.field}.raw`]: filter.arguments.value,
},
};
case 'availability': case 'availability':
const scope = filter.arguments.scope?.charAt(0) ?? 's'; const scope = filter.arguments.scope?.charAt(0) ?? 's';
const time = typeof filter.arguments.time === 'undefined' ? 'now' : `${filter.arguments.time}||`; const time = typeof filter.arguments.time === 'undefined' ? 'now' : `${filter.arguments.time}||`;
@@ -185,8 +195,7 @@ export function buildFilter(filter: SCSearchFilter):
/** /**
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3 * https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3
*/ */
// tslint:disable-next-line:ban-ts-ignore // @ts-expect-error unfortunately, typescript is stupid and won't allow me to map this to an actual type.
// @ts-ignore unfortunately, typescript is stupid and won't allow me to map this to an actual type.
ignore_unmapped: true, ignore_unmapped: true,
[`${filter.arguments.field}.polygon`]: { [`${filter.arguments.field}.polygon`]: {
shape: filter.arguments.shape, shape: filter.arguments.shape,
@@ -195,8 +204,10 @@ export function buildFilter(filter: SCSearchFilter):
}, },
}; };
if ((typeof filter.arguments.spatialRelation === 'undefined' || filter.arguments.spatialRelation === 'intersects') if (
&& filter.arguments.shape.type === 'envelope' (typeof filter.arguments.spatialRelation === 'undefined' ||
filter.arguments.spatialRelation === 'intersects') &&
filter.arguments.shape.type === 'envelope'
) { ) {
return { return {
bool: { bool: {
@@ -208,8 +219,6 @@ export function buildFilter(filter: SCSearchFilter):
/** /**
* https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3 * https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3
*/ */
// tslint:disable-next-line:ban-ts-ignore
// @ts-ignore unfortunately, typescript is stupid and won't allow me to map this to an actual type.
ignore_unmapped: true, ignore_unmapped: true,
[`${filter.arguments.field}.point.coordinates`]: { [`${filter.arguments.field}.point.coordinates`]: {
top_left: filter.arguments.shape.coordinates[0], top_left: filter.arguments.shape.coordinates[0],
@@ -228,6 +237,7 @@ export function buildFilter(filter: SCSearchFilter):
/** /**
* Builds scoring functions from boosting config * Builds scoring functions from boosting config
*
* @param boostings Backend boosting configuration for contexts and types * @param boostings Backend boosting configuration for contexts and types
* @param context The context of the app from where the search was initiated * @param context The context of the app from where the search was initiated
*/ */
@@ -235,14 +245,14 @@ function buildFunctions(
boostings: SCBackendConfigurationSearchBoostingContext, boostings: SCBackendConfigurationSearchBoostingContext,
context: SCSearchContext | undefined, context: SCSearchContext | undefined,
): ESFunctionScoreQueryFunction[] { ): ESFunctionScoreQueryFunction[] {
// default context // default context
let functions: ESFunctionScoreQueryFunction[] = let functions: ESFunctionScoreQueryFunction[] = buildFunctionsForBoostingTypes(
buildFunctionsForBoostingTypes(boostings['default' as SCSearchContext]); boostings['default' as SCSearchContext],
);
if (typeof context !== 'undefined' && context !== 'default') { if (typeof context !== 'undefined' && context !== 'default') {
// specific context provided, extend default context with additional boosts // specific context provided, extend default context with additional boosts
functions = functions.concat(buildFunctionsForBoostingTypes(boostings[context])); functions = [...functions, ...buildFunctionsForBoostingTypes(boostings[context])];
} }
return functions; return functions;
@@ -258,7 +268,7 @@ function buildFunctionsForBoostingTypes(
): ESFunctionScoreQueryFunction[] { ): ESFunctionScoreQueryFunction[] {
const functions: ESFunctionScoreQueryFunction[] = []; const functions: ESFunctionScoreQueryFunction[] = [];
boostingTypes.forEach((boostingForOneSCType) => { for (const boostingForOneSCType of boostingTypes) {
const typeFilter: ESTypeFilter = { const typeFilter: ESTypeFilter = {
type: { type: {
value: boostingForOneSCType.type, value: boostingForOneSCType.type,
@@ -271,7 +281,6 @@ function buildFunctionsForBoostingTypes(
}); });
if (typeof boostingForOneSCType.fields !== 'undefined') { if (typeof boostingForOneSCType.fields !== 'undefined') {
const fields = boostingForOneSCType.fields; const fields = boostingForOneSCType.fields;
for (const fieldName in boostingForOneSCType.fields) { for (const fieldName in boostingForOneSCType.fields) {
@@ -291,10 +300,7 @@ function buildFunctionsForBoostingTypes(
functions.push({ functions.push({
filter: { filter: {
bool: { bool: {
must: [ must: [typeFilter, termFilter],
typeFilter,
termFilter,
],
should: [], should: [],
}, },
}, },
@@ -305,24 +311,24 @@ function buildFunctionsForBoostingTypes(
} }
} }
} }
}); }
return functions; return functions;
} }
/** /**
* Builds body for Elasticsearch requests * Builds body for Elasticsearch requests
* @param params Parameters for querying the backend *
* @param parameters Parameters for querying the backend
* @param defaultConfig Default configuration of the backend * @param defaultConfig Default configuration of the backend
* @param elasticsearchConfig Elasticsearch configuration * @param elasticsearchConfig Elasticsearch configuration
* @returns ElasticsearchQuery (body of a search-request) * @returns ElasticsearchQuery (body of a search-request)
*/ */
export function buildQuery( export function buildQuery(
params: SCSearchQuery, parameters: SCSearchQuery,
defaultConfig: SCConfigFile, defaultConfig: SCConfigFile,
elasticsearchConfig: ElasticsearchConfig, elasticsearchConfig: ElasticsearchConfig,
): ESFunctionScoreQuery { ): ESFunctionScoreQuery {
// if config provides an minMatch parameter we use query_string instead of match query // if config provides an minMatch parameter we use query_string instead of match query
let query; let query;
if (typeof elasticsearchConfig.query === 'undefined') { if (typeof elasticsearchConfig.query === 'undefined') {
@@ -331,7 +337,7 @@ export function buildQuery(
analyzer: 'search_german', analyzer: 'search_german',
default_field: 'name', default_field: 'name',
minimum_should_match: '90%', minimum_should_match: '90%',
query: (typeof params.query !== 'string') ? '*' : escapeESReservedCharacters(params.query), query: typeof parameters.query !== 'string' ? '*' : escapeESReservedCharacters(parameters.query),
}, },
}; };
} else if (elasticsearchConfig.query.queryType === 'query_string') { } else if (elasticsearchConfig.query.queryType === 'query_string') {
@@ -340,11 +346,11 @@ export function buildQuery(
analyzer: 'search_german', analyzer: 'search_german',
default_field: 'name', default_field: 'name',
minimum_should_match: elasticsearchConfig.query.minMatch, minimum_should_match: elasticsearchConfig.query.minMatch,
query: (typeof params.query !== 'string') ? '*' : escapeESReservedCharacters(params.query), query: typeof parameters.query !== 'string' ? '*' : escapeESReservedCharacters(parameters.query),
}, },
}; };
} else if (elasticsearchConfig.query.queryType === 'dis_max') { } else if (elasticsearchConfig.query.queryType === 'dis_max') {
if (params.query !== '*') { if (parameters.query !== '*') {
query = { query = {
dis_max: { dis_max: {
boost: 1.2, boost: 1.2,
@@ -355,7 +361,7 @@ export function buildQuery(
boost: elasticsearchConfig.query.matchBoosting, boost: elasticsearchConfig.query.matchBoosting,
cutoff_frequency: elasticsearchConfig.query.cutoffFrequency, cutoff_frequency: elasticsearchConfig.query.cutoffFrequency,
fuzziness: elasticsearchConfig.query.fuzziness, fuzziness: elasticsearchConfig.query.fuzziness,
query: (typeof params.query !== 'string') ? '*' : params.query, query: typeof parameters.query !== 'string' ? '*' : parameters.query,
}, },
}, },
}, },
@@ -364,22 +370,24 @@ export function buildQuery(
analyzer: 'search_german', analyzer: 'search_german',
default_field: 'name', default_field: 'name',
minimum_should_match: elasticsearchConfig.query.minMatch, minimum_should_match: elasticsearchConfig.query.minMatch,
query: (typeof params.query !== 'string') ? '*' : escapeESReservedCharacters(params.query), query:
typeof parameters.query !== 'string' ? '*' : escapeESReservedCharacters(parameters.query),
}, },
}, },
], ],
tie_breaker: elasticsearchConfig.query.tieBreaker, tie_breaker: elasticsearchConfig.query.tieBreaker,
}, },
}; };
} }
} else { } else {
throw new Error('Unsupported query type. Check your config file and reconfigure your elasticsearch query'); throw new Error(
'Unsupported query type. Check your config file and reconfigure your elasticsearch query',
);
} }
const functionScoreQuery: ESFunctionScoreQuery = { const functionScoreQuery: ESFunctionScoreQuery = {
function_score: { function_score: {
functions: buildFunctions(defaultConfig.internal.boostings, params.context), functions: buildFunctions(defaultConfig.internal.boostings, parameters.context),
query: { query: {
bool: { bool: {
minimum_should_match: 0, // if we have no should, nothing can match minimum_should_match: 0, // if we have no should, nothing can match
@@ -398,8 +406,8 @@ export function buildQuery(
mustMatch.push(query); mustMatch.push(query);
} }
if (typeof params.filter !== 'undefined') { if (typeof parameters.filter !== 'undefined') {
mustMatch.push(buildFilter(params.filter)); mustMatch.push(buildFilter(parameters.filter));
} }
} }
@@ -408,13 +416,12 @@ export function buildQuery(
/** /**
* converts query to * converts query to
*
* @param sorts Sorting rules to apply to the data that is being queried * @param sorts Sorting rules to apply to the data that is being queried
* @returns an array of sort queries * @returns an array of sort queries
*/ */
export function buildSort( export function buildSort(sorts: SCSearchSort[]): Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> {
sorts: SCSearchSort[], return sorts.map(sort => {
): Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> {
return sorts.map((sort) => {
switch (sort.type) { switch (sort.type) {
case 'generic': case 'generic':
const esGenericSort: ESGenericSort = {}; const esGenericSort: ESGenericSort = {};
@@ -427,26 +434,26 @@ export function buildSort(
return esDucetSort; return esDucetSort;
case 'distance': case 'distance':
const args: ESGeoDistanceSortArguments = { const arguments_: ESGeoDistanceSortArguments = {
mode: 'avg', mode: 'avg',
order: sort.order, order: sort.order,
unit: 'm', unit: 'm',
}; };
args[`${sort.arguments.field}.point.coordinates`] = { arguments_[`${sort.arguments.field}.point.coordinates`] = {
lat: sort.arguments.position[1], lat: sort.arguments.position[1],
lon: sort.arguments.position[0], lon: sort.arguments.position[0],
}; };
return { return {
_geo_distance: args, _geo_distance: arguments_,
}; };
case 'price': case 'price':
return { return {
_script: { _script: {
order: sort.order, order: sort.order,
script: buildPriceSortScript(sort.arguments.universityRole, sort.arguments.field), script: buildPriceSortScript(sort.arguments.universityRole, sort.arguments.field),
type: 'number' as 'number', type: 'number' as const,
}, },
}; };
} }
@@ -459,7 +466,10 @@ export function buildSort(
* @param universityRole User group which consumes university services * @param universityRole User group which consumes university services
* @param field Field in which wanted offers with prices are located * @param field Field in which wanted offers with prices are located
*/ */
export function buildPriceSortScript(universityRole: keyof SCSportCoursePriceGroup, field: SCThingsField): string { export function buildPriceSortScript(
universityRole: keyof SCSportCoursePriceGroup,
field: SCThingsField,
): string {
return ` return `
// initialize the sort value with the maximum // initialize the sort value with the maximum
double price = Double.MAX_VALUE; double price = Double.MAX_VALUE;

View File

@@ -15,18 +15,19 @@
*/ */
import {Client} from '@elastic/elasticsearch'; import {Client} from '@elastic/elasticsearch';
import {SCThingType} from '@openstapps/core'; import {SCThingType} from '@openstapps/core';
// tslint:disable-next-line:no-implicit-dependencies
import {AggregationSchema} from '@openstapps/es-mapping-generator/src/types/aggregation'; import {AggregationSchema} from '@openstapps/es-mapping-generator/src/types/aggregation';
// tslint:disable-next-line:no-implicit-dependencies
import {ElasticsearchTemplateCollection} from '@openstapps/es-mapping-generator/src/types/mapping'; import {ElasticsearchTemplateCollection} from '@openstapps/es-mapping-generator/src/types/mapping';
import {readFileSync} from 'fs'; import {readFileSync} from 'fs';
import {resolve} from 'path'; import path from 'path';
const mappingsPath = resolve('node_modules', '@openstapps', 'core', 'lib','mappings'); const mappingsPath = path.resolve('node_modules', '@openstapps', 'core', 'lib', 'mappings');
export const mappings = JSON.parse(readFileSync(resolve(mappingsPath, 'mappings.json'), 'utf-8')) as ElasticsearchTemplateCollection;
export const aggregations = JSON.parse(readFileSync(resolve(mappingsPath, 'aggregations.json'), 'utf-8')) as AggregationSchema;
export const mappings = JSON.parse(
readFileSync(path.resolve(mappingsPath, 'mappings.json'), 'utf8'),
) as ElasticsearchTemplateCollection;
export const aggregations = JSON.parse(
readFileSync(path.resolve(mappingsPath, 'aggregations.json'), 'utf8'),
) as AggregationSchema;
/** /**
* Re-applies all interfaces for every type * Re-applies all interfaces for every type
@@ -44,8 +45,8 @@ export async function refreshAllTemplates(client: Client) {
* *
* This includes applying the mapping, settings * This includes applying the mapping, settings
* *
* @param type the SCThingType of which the template should be set
* @param client An elasticsearch client to use * @param client An elasticsearch client to use
* @param type the SCThingType of which the template should be set
*/ */
export async function putTemplate(client: Client, type: SCThingType) { export async function putTemplate(client: Client, type: SCThingType) {
const sanitizedType = `template_${type.replace(/\s/g, '_')}`; const sanitizedType = `template_${type.replace(/\s/g, '_')}`;

View File

@@ -15,9 +15,7 @@
*/ */
import {SCThing, SCThingType} from '@openstapps/core'; import {SCThing, SCThingType} from '@openstapps/core';
// we only have the @types package because some things type definitions are still missing from the official // we only have the @types package because some things type definitions are still missing from the official
// tslint:disable-next-line:no-implicit-dependencies
import {NameList} from 'elasticsearch'; import {NameList} from 'elasticsearch';
// tslint:disable-next-line:no-implicit-dependencies
import {Polygon, Position} from 'geojson'; import {Polygon, Position} from 'geojson';
/** /**
@@ -75,7 +73,6 @@ export interface NestedAggregation {
[name: string]: BucketAggregation | number; [name: string]: BucketAggregation | number;
} }
/** /**
* A configuration for using the Dis Max Query * A configuration for using the Dis Max Query
* *
@@ -90,30 +87,35 @@ export interface ElasticsearchQueryDisMaxConfig {
/** /**
* The maximum allowed Levenshtein Edit Distance (or number of edits) * The maximum allowed Levenshtein Edit Distance (or number of edits)
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#fuzziness * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#fuzziness
*/ */
fuzziness: number | string; fuzziness: number | string;
/** /**
* Increase the importance (relevance score) of a field * Increase the importance (relevance score) of a field
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-boost.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-boost.html
*/ */
matchBoosting: number; matchBoosting: number;
/** /**
* Minimal number (or percentage) of words that should match in a query * Minimal number (or percentage) of words that should match in a query
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html
*/ */
minMatch: string; minMatch: string;
/** /**
* Type of the query - in this case 'dis_max' which is a union of its subqueries * Type of the query - in this case 'dis_max' which is a union of its subqueries
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-dis-max-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-dis-max-query.html
*/ */
queryType: 'dis_max'; queryType: 'dis_max';
/** /**
* Changes behavior of default calculation of the score when multiple results match * Changes behavior of default calculation of the score when multiple results match
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-multi-match-query.html#tie-breaker * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-multi-match-query.html#tie-breaker
*/ */
tieBreaker: number; tieBreaker: number;
@@ -128,12 +130,14 @@ export interface ElasticsearchQueryDisMaxConfig {
export interface ElasticsearchQueryQueryStringConfig { export interface ElasticsearchQueryQueryStringConfig {
/** /**
* Minimal number (or percentage) of words that should match in a query * Minimal number (or percentage) of words that should match in a query
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html
*/ */
minMatch: string; minMatch: string;
/** /**
* Type of the query - in this case 'query_string' which uses a query parser in order to parse content * Type of the query - in this case 'query_string' which uses a query parser in order to parse content
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html
*/ */
queryType: 'query_string'; queryType: 'query_string';
@@ -141,6 +145,7 @@ export interface ElasticsearchQueryQueryStringConfig {
/** /**
* A hit in an elasticsearch search result * A hit in an elasticsearch search result
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-fields.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-fields.html
*/ */
export interface ElasticsearchObject<T extends SCThing> { export interface ElasticsearchObject<T extends SCThing> {
@@ -166,6 +171,7 @@ export interface ElasticsearchObject<T extends SCThing> {
/** /**
* The document's mapping type * The document's mapping type
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-type-field.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-type-field.html
*/ */
_type: string; _type: string;
@@ -177,22 +183,25 @@ export interface ElasticsearchObject<T extends SCThing> {
/** /**
* Used to index the same field in different ways for different purposes * Used to index the same field in different ways for different purposes
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/multi-fields.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/multi-fields.html
*/ */
fields?: NameList; fields?: NameList;
/** /**
* Used to highlight search results on one or more fields * Used to highlight search results on one or more fields
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-highlighting.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-highlighting.html
*/ */
// tslint:disable-next-line: no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
highlight?: any; highlight?: any;
/** /**
* Used in when nested/children documents match the query * Used in when nested/children documents match the query
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-inner-hits.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-inner-hits.html
*/ */
// tslint:disable-next-line: no-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
inner_hits?: any; inner_hits?: any;
/** /**
@@ -246,21 +255,23 @@ export interface ElasticsearchConfig {
/** /**
* An elasticsearch term filter * An elasticsearch term filter
*/ */
export type ESTermFilter = { export type ESTermFilter =
/** | {
* Definition of a term to match /**
*/ * Definition of a term to match
term: { */
[fieldName: string]: string; term: {
}; [fieldName: string]: string;
} | { };
/** }
* Definition of terms to match (or) | {
*/ /**
terms: { * Definition of terms to match (or)
[fieldName: string]: string[]; */
}; terms: {
}; [fieldName: string]: string[];
};
};
export interface ESGenericRange<T> { export interface ESGenericRange<T> {
/** /**
@@ -318,7 +329,6 @@ export type ESNumericRangeFilter = ESGenericRangeFilter<number, ESGenericRange<n
export type ESDateRangeFilter = ESGenericRangeFilter<string, ESDateRange>; export type ESDateRangeFilter = ESGenericRangeFilter<string, ESDateRange>;
export type ESRangeFilter = ESNumericRangeFilter | ESDateRangeFilter; export type ESRangeFilter = ESNumericRangeFilter | ESDateRangeFilter;
/** /**
* An elasticsearch type filter * An elasticsearch type filter
*/ */
@@ -343,17 +353,19 @@ export interface ESGeoDistanceFilterArguments {
*/ */
distance: string; distance: string;
[fieldName: string]: { [fieldName: string]:
/** | {
* Latitude /**
*/ * Latitude
lat: number; */
lat: number;
/** /**
* Longitude * Longitude
*/ */
lon: number; lon: number;
} | string; }
| string;
} }
/** /**
@@ -386,11 +398,13 @@ export interface ESEnvelope {
/** /**
* An Elasticsearch geo bounding box filter * An Elasticsearch geo bounding box filter
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html
*/ */
export interface ESGeoBoundingBoxFilter { export interface ESGeoBoundingBoxFilter {
/** /**
* An Elasticsearch geo bounding box filter * An Elasticsearch geo bounding box filter
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html
*/ */
geo_bounding_box: { geo_bounding_box: {
@@ -410,6 +424,7 @@ export interface ESGeoBoundingBoxFilter {
/** /**
* An Elasticsearch geo shape filter * An Elasticsearch geo shape filter
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html
*/ */
export interface ESGeoShapeFilter { export interface ESGeoShapeFilter {
@@ -432,11 +447,13 @@ export interface ESGeoShapeFilter {
/** /**
* Filter arguments for an elasticsearch boolean filter * Filter arguments for an elasticsearch boolean filter
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-bool-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-bool-query.html
*/ */
export interface ESBooleanFilterArguments<T> { export interface ESBooleanFilterArguments<T> {
/** /**
* Minimal number (or percentage) of words that should match in a query * Minimal number (or percentage) of words that should match in a query
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html
*/ */
minimum_should_match?: number; minimum_should_match?: number;
@@ -469,6 +486,7 @@ export interface ESBooleanFilter<T> {
/** /**
* An elasticsearch function score query * An elasticsearch function score query
*
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-function-score-query.html * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-function-score-query.html
*/ */
export interface ESFunctionScoreQuery { export interface ESFunctionScoreQuery {
@@ -478,6 +496,7 @@ export interface ESFunctionScoreQuery {
function_score: { function_score: {
/** /**
* Functions that compute score for query results (documents) * Functions that compute score for query results (documents)
*
* @see ESFunctionScoreQueryFunction * @see ESFunctionScoreQueryFunction
*/ */
functions: ESFunctionScoreQueryFunction[]; functions: ESFunctionScoreQueryFunction[];
@@ -535,17 +554,19 @@ export interface ESGeoDistanceSortArguments {
*/ */
unit: 'm'; unit: 'm';
[field: string]: { [field: string]:
/** | {
* Latitude /**
*/ * Latitude
lat: number; */
lat: number;
/** /**
* Longitude * Longitude
*/ */
lon: number; lon: number;
} | string; }
| string;
} }
/** /**

View File

@@ -18,12 +18,12 @@ import {
ESAggTypeFilter, ESAggTypeFilter,
ESNestedAggregation, ESNestedAggregation,
ESTermsFilter, ESTermsFilter,
// tslint:disable-next-line:no-implicit-dependencies we're just using the types here
} from '@openstapps/es-mapping-generator/src/types/aggregation'; } from '@openstapps/es-mapping-generator/src/types/aggregation';
import {BucketAggregation, NestedAggregation} from './elasticsearch'; import {BucketAggregation, NestedAggregation} from './elasticsearch';
/** /**
* Checks if the type is a BucketAggregation * Checks if the type is a BucketAggregation
*
* @param agg the type to check * @param agg the type to check
*/ */
export function isBucketAggregation(agg: BucketAggregation | number): agg is BucketAggregation { export function isBucketAggregation(agg: BucketAggregation | number): agg is BucketAggregation {
@@ -32,6 +32,7 @@ export function isBucketAggregation(agg: BucketAggregation | number): agg is Buc
/** /**
* Checks if the type is a NestedAggregation * Checks if the type is a NestedAggregation
*
* @param agg the type to check * @param agg the type to check
*/ */
export function isNestedAggregation(agg: BucketAggregation | NestedAggregation): agg is NestedAggregation { export function isNestedAggregation(agg: BucketAggregation | NestedAggregation): agg is NestedAggregation {
@@ -40,6 +41,7 @@ export function isNestedAggregation(agg: BucketAggregation | NestedAggregation):
/** /**
* Checks if the parameter is of type ESTermsFilter * Checks if the parameter is of type ESTermsFilter
*
* @param agg the value to check * @param agg the value to check
*/ */
export function isESTermsFilter(agg: ESTermsFilter | ESNestedAggregation): agg is ESTermsFilter { export function isESTermsFilter(agg: ESTermsFilter | ESNestedAggregation): agg is ESTermsFilter {
@@ -48,6 +50,7 @@ export function isESTermsFilter(agg: ESTermsFilter | ESNestedAggregation): agg i
/** /**
* Checks if the parameter is of type ESTermsFilter * Checks if the parameter is of type ESTermsFilter
*
* @param agg the value to check * @param agg the value to check
*/ */
export function isESNestedAggregation(agg: ESTermsFilter | ESNestedAggregation): agg is ESNestedAggregation { export function isESNestedAggregation(agg: ESTermsFilter | ESNestedAggregation): agg is ESNestedAggregation {
@@ -59,6 +62,8 @@ export function isESNestedAggregation(agg: ESTermsFilter | ESNestedAggregation):
* *
* @param filter the filter to narrow the type of * @param filter the filter to narrow the type of
*/ */
export function isESAggMatchAllFilter(filter: ESAggTypeFilter | ESAggMatchAllFilter): filter is ESAggMatchAllFilter { export function isESAggMatchAllFilter(
filter: ESAggTypeFilter | ESAggMatchAllFilter,
): filter is ESAggMatchAllFilter {
return filter.hasOwnProperty('match_all'); return filter.hasOwnProperty('match_all');
} }

View File

@@ -35,15 +35,14 @@ describe('App', async function () {
sandbox.restore(); sandbox.restore();
}); });
it('should exit process if there is 20 seconds of pause after a request during the integration test', async function() { it('should exit process if there is 20 seconds of pause after a request during the integration test', async function () {
const clock = sandbox.useFakeTimers(); const clock = sandbox.useFakeTimers();
let processExitStub = sandbox.stub(process, 'exit'); const processExitStub = sandbox.stub(process, 'exit');
// fake NODE_ENV as integration test // fake NODE_ENV as integration test
const restore = mockedEnv({ const restore = mockedEnv({
NODE_ENV: 'integration-test', NODE_ENV: 'integration-test',
}); });
await testApp await testApp.post('/');
.post('/');
// fake timeout of default timeout // fake timeout of default timeout
clock.tick(DEFAULT_TIMEOUT); clock.tick(DEFAULT_TIMEOUT);
@@ -67,21 +66,18 @@ describe('App', async function () {
}); });
it('should implement CORS', async function () { it('should implement CORS', async function () {
// @ts-ignore // @ts-expect error
const {headers} = await testApp const {headers} = await testApp.options('/');
.options('/');
expect(headers['access-control-allow-origin']).to.be.equal('*'); expect(headers['access-control-allow-origin']).to.be.equal('*');
}); });
it('should provide unsupported media type error in case of a non-json body', async function () { it('should provide unsupported media type error in case of a non-json body', async function () {
const responseNoType = await testApp const responseNoType = await testApp.post('/non-existing-route').send();
.post('/non-existing-route')
.send();
const responseOtherType = await testApp const responseOtherType = await testApp
.post('/non-existing-route') .post('/non-existing-route')
.set('Content-Type','application/foo') .set('Content-Type', 'application/foo')
.send(); .send();
expect(responseNoType.status).to.equal(new SCUnsupportedMediaTypeErrorResponse().statusCode); expect(responseNoType.status).to.equal(new SCUnsupportedMediaTypeErrorResponse().statusCode);

View File

@@ -19,14 +19,14 @@ import {expect} from 'chai';
describe('Common', function () { describe('Common', function () {
describe('inRangeInclusive', function () { describe('inRangeInclusive', function () {
it('should provide true if the given number is in the range', function () { it('should provide true if the given number is in the range', function () {
expect(inRangeInclusive(1, [1,3])).to.be.true; expect(inRangeInclusive(1, [1, 3])).to.be.true;
expect(inRangeInclusive(2, [1,3])).to.be.true; expect(inRangeInclusive(2, [1, 3])).to.be.true;
expect(inRangeInclusive(1.1, [1,3])).to.be.true; expect(inRangeInclusive(1.1, [1, 3])).to.be.true;
expect(inRangeInclusive(3, [1, 3])).to.be.true; expect(inRangeInclusive(3, [1, 3])).to.be.true;
}); });
it('should provide false if the given number is not in the range', function () { it('should provide false if the given number is not in the range', function () {
expect(inRangeInclusive(3.1, [1,3])).to.be.false; expect(inRangeInclusive(3.1, [1, 3])).to.be.false;
expect(inRangeInclusive(0, [1, 3])).to.be.false; expect(inRangeInclusive(0, [1, 3])).to.be.false;
}); });
}); });

View File

@@ -42,25 +42,25 @@ export async function startApp(): Promise<Express> {
const port = await getPort(); const port = await getPort();
server.listen(port); server.listen(port);
server.on('error', err => { server.on('error', error => {
throw err; throw error;
}); });
return new Promise(resolve => server.on('listening', () => { return new Promise(resolve =>
app.set( server.on('listening', () => {
'bulk', app.set('bulk', bulkStorageMock);
bulkStorageMock, resolve(app);
); }),
resolve(app); );
}));
} }
/** /**
* An elasticsearch mock * An elasticsearch mock
*/ */
export class ElasticsearchMock implements Database { export class ElasticsearchMock implements Database {
// @ts-ignore // @ts-expect-error never read
private bulk: Bulk | undefined; private bulk: Bulk | undefined;
private storageMock = new Map<string, SCThings>(); private storageMock = new Map<string, SCThings>();
constructor(_configFile: SCConfigFile, _mailQueue?: MailQueue) { constructor(_configFile: SCConfigFile, _mailQueue?: MailQueue) {
@@ -81,7 +81,7 @@ export class ElasticsearchMock implements Database {
} }
get(uid: SCUuid): Promise<SCThings> { get(uid: SCUuid): Promise<SCThings> {
// @ts-ignore // @ts-expect-error incompatible types
return Promise.resolve(this.storageMock.get(uid)); return Promise.resolve(this.storageMock.get(uid));
} }
@@ -98,49 +98,54 @@ export class ElasticsearchMock implements Database {
return Promise.resolve(); return Promise.resolve();
} }
search(_params: SCSearchQuery): Promise<SCSearchResponse> { search(_parameters: SCSearchQuery): Promise<SCSearchResponse> {
return Promise.resolve({data: [], facets: [], pagination: {count: 0, offset: 0, total: 0}, stats: {time: 0}}); return Promise.resolve({
data: [],
facets: [],
pagination: {count: 0, offset: 0, total: 0},
stats: {time: 0},
});
} }
} }
export const bulkStorageMock = new BulkStorage(new ElasticsearchMock(configFile)); export const bulkStorageMock = new BulkStorage(new ElasticsearchMock(configFile));
export const bulk: Bulk = { export const bulk: Bulk = {
expiration: moment().add(3600, 'seconds') expiration: moment().add(3600, 'seconds').format(),
.format(), source: 'some_source',
source: 'some_source', state: 'in progress',
state: 'in progress', type: SCThingType.Book,
type: SCThingType.Book, uid: '',
uid: '' };
};
export class FooError extends Error { export class FooError extends Error {}
}
export const DEFAULT_TEST_TIMEOUT = 10000; export const DEFAULT_TEST_TIMEOUT = 10_000;
export const TRANSPORT_SEND_RESPONSE = 'Send Response'; export const TRANSPORT_SEND_RESPONSE = 'Send Response';
export const getTransport = (verified: boolean) => { export const getTransport = (verified: boolean) => {
return { return {
cc: undefined, cc: undefined,
from: undefined, from: undefined,
recipients: undefined, recipients: undefined,
transportAgent: undefined, transportAgent: undefined,
verified: undefined, verified: undefined,
isVerified(): boolean { isVerified(): boolean {
return verified; return verified;
}, },
send(_subject: string, _message: string): Promise<string> { send(_subject: string, _message: string): Promise<string> {
return Promise.resolve(''); return Promise.resolve('');
}, },
sendMail(_mail: any): Promise<string> { // eslint-disable-next-line @typescript-eslint/no-explicit-any
return Promise.resolve(TRANSPORT_SEND_RESPONSE); sendMail(_mail: any): Promise<string> {
}, return Promise.resolve(TRANSPORT_SEND_RESPONSE);
verify(): Promise<boolean> { },
return Promise.resolve(false); verify(): Promise<boolean> {
} return Promise.resolve(false);
} },
} };
};
export const getIndex = (uid?: string) => `stapps_footype_foosource_${uid ? uid : Elasticsearch.getIndexUID(v4())}`; export const getIndex = (uid?: string) =>
`stapps_footype_foosource_${uid ? uid : Elasticsearch.getIndexUID(v4())}`;

View File

@@ -22,7 +22,6 @@ import sinon from 'sinon';
describe('Backend transport', function () { describe('Backend transport', function () {
describe('isTransportWithVerification', function () { describe('isTransportWithVerification', function () {
it('should return false if transport is not verifiable', function () { it('should return false if transport is not verifiable', function () {
expect(isTransportWithVerification({} as Transport)).to.be.false; expect(isTransportWithVerification({} as Transport)).to.be.false;
expect(isTransportWithVerification({verify: 'foo'} as unknown as Transport)).to.be.false; expect(isTransportWithVerification({verify: 'foo'} as unknown as Transport)).to.be.false;
@@ -30,6 +29,7 @@ describe('Backend transport', function () {
it('should return true if transport is verifiable', function () { it('should return true if transport is verifiable', function () {
// a transport which has verify function should be verifiable // a transport which has verify function should be verifiable
// eslint-disable-next-line @typescript-eslint/no-empty-function
expect(isTransportWithVerification({verify: () => {}} as unknown as Transport)).to.be.true; expect(isTransportWithVerification({verify: () => {}} as unknown as Transport)).to.be.true;
}); });
}); });
@@ -43,7 +43,7 @@ describe('Backend transport', function () {
}); });
it('should provide only one instance of the transport', function () { it('should provide only one instance of the transport', function () {
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(SMTP, 'getInstance').callsFake(() => { sandbox.stub(SMTP, 'getInstance').callsFake(() => {
return {}; return {};
}); });
@@ -77,8 +77,10 @@ describe('Backend transport', function () {
}); });
it('should provide information that the transport if waiting for its verification', function () { it('should provide information that the transport if waiting for its verification', function () {
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(SMTP, 'getInstance').callsFake(() => {return {verify: () => Promise.resolve(true)}}); sandbox.stub(SMTP, 'getInstance').callsFake(() => {
return {verify: () => Promise.resolve(true)};
});
expect(BackendTransport.getInstance().isWaitingForVerification()).to.be.true; expect(BackendTransport.getInstance().isWaitingForVerification()).to.be.true;
}); });

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* /*
* Copyright (C) 2020 StApps * Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -37,7 +38,7 @@ describe('MailQueue', async function () {
it('should fail after maximal number of verification checks', function () { it('should fail after maximal number of verification checks', function () {
const loggerStub = sandbox.spy(Logger, 'warn'); const loggerStub = sandbox.spy(Logger, 'warn');
const test = () => { const test = () => {
// @ts-ignore // @ts-expect-error not assignable
new MailQueue(getTransport(false)); new MailQueue(getTransport(false));
// fake that VERIFICATION_TIMEOUT was reached more times (one more) than MAX_VERIFICATION_ATTEMPTS // fake that VERIFICATION_TIMEOUT was reached more times (one more) than MAX_VERIFICATION_ATTEMPTS
clock.tick(MailQueue.VERIFICATION_TIMEOUT * (MailQueue.MAX_VERIFICATION_ATTEMPTS + 1)); clock.tick(MailQueue.VERIFICATION_TIMEOUT * (MailQueue.MAX_VERIFICATION_ATTEMPTS + 1));
@@ -50,8 +51,8 @@ describe('MailQueue', async function () {
it('should add queued mails to the queue for sending when transport is verified', async function () { it('should add queued mails to the queue for sending when transport is verified', async function () {
const queueAddStub = sandbox.stub(Queue.prototype, 'add'); const queueAddStub = sandbox.stub(Queue.prototype, 'add');
const numberOfMails = 3; const numberOfMails = 3;
let transport = getTransport(false); const transport = getTransport(false);
// @ts-ignore // @ts-expect-error not assignable
const mailQueue = new MailQueue(transport); const mailQueue = new MailQueue(transport);
const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'}; const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'};
for (let i = 0; i < numberOfMails; i++) { for (let i = 0; i < numberOfMails; i++) {
@@ -67,7 +68,7 @@ describe('MailQueue', async function () {
it('should not add SMTP sending tasks to queue when transport is not verified', function () { it('should not add SMTP sending tasks to queue when transport is not verified', function () {
const queueAddStub = sandbox.stub(Queue.prototype, 'add'); const queueAddStub = sandbox.stub(Queue.prototype, 'add');
// @ts-ignore // @ts-expect-error not assignable
const mailQueue = new MailQueue(getTransport(false)); const mailQueue = new MailQueue(getTransport(false));
const mail: MailOptions = {}; const mail: MailOptions = {};
mailQueue.push(mail); mailQueue.push(mail);
@@ -77,8 +78,8 @@ describe('MailQueue', async function () {
it('should add SMTP sending tasks to queue when transport is verified', function () { it('should add SMTP sending tasks to queue when transport is verified', function () {
const queueAddStub = sandbox.stub(Queue.prototype, 'add'); const queueAddStub = sandbox.stub(Queue.prototype, 'add');
let transport = getTransport(false); const transport = getTransport(false);
// @ts-ignore // @ts-expect-error not assignable
const mailQueue = new MailQueue(transport); const mailQueue = new MailQueue(transport);
const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'}; const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'};
// fake that transport is verified // fake that transport is verified
@@ -90,11 +91,11 @@ describe('MailQueue', async function () {
it('should send SMTP mails when transport is verified', async function () { it('should send SMTP mails when transport is verified', async function () {
let caught: any; let caught: any;
sandbox.stub(Queue.prototype, 'add').callsFake(async (promiseGenerator) => { sandbox.stub(Queue.prototype, 'add').callsFake(async promiseGenerator => {
caught = await promiseGenerator(); caught = await promiseGenerator();
}); });
let transport = getTransport(false); const transport = getTransport(false);
// @ts-ignore // @ts-expect-error not assignable
const mailQueue = new MailQueue(transport); const mailQueue = new MailQueue(transport);
const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'}; const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'};
// fake that transport is verified // fake that transport is verified

View File

@@ -55,14 +55,11 @@ describe('Bulk routes', async function () {
}); });
it('should return (throw) error if a bulk with the provided UID cannot be found when adding to a bulk', async function () { it('should return (throw) error if a bulk with the provided UID cannot be found when adding to a bulk', async function () {
await testApp await testApp.post(bulkRoute.urlPath).set('Content-Type', 'application/json').send(request);
.post(bulkRoute.urlPath) const bulkAddRouteUrlPath = bulkAddRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid');
.set('Content-Type', 'application/json')
.send(request);
const bulkAddRouteurlPath = bulkAddRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid');
const {status} = await testApp const {status} = await testApp
.post(bulkAddRouteurlPath) .post(bulkAddRouteUrlPath)
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.send(book); .send(book);
@@ -86,10 +83,7 @@ describe('Bulk routes', async function () {
}); });
it('should return (throw) error if a bulk with the provided UID cannot be found when closing a bulk (done)', async function () { it('should return (throw) error if a bulk with the provided UID cannot be found when closing a bulk (done)', async function () {
await testApp await testApp.post(bulkRoute.urlPath).set('Content-Type', 'application/json').send(request);
.post(bulkRoute.urlPath)
.set('Content-Type', 'application/json')
.send(request);
const bulkDoneRouteurlPath = bulkDoneRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid'); const bulkDoneRouteurlPath = bulkDoneRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid');
const {status} = await testApp const {status} = await testApp
@@ -100,7 +94,7 @@ describe('Bulk routes', async function () {
expect(status).to.be.equal(new SCNotFoundErrorResponse().statusCode); expect(status).to.be.equal(new SCNotFoundErrorResponse().statusCode);
}); });
it ('should close the bulk (done)', async function () { it('should close the bulk (done)', async function () {
const response = await testApp const response = await testApp
.post(bulkRoute.urlPath) .post(bulkRoute.urlPath)
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')

View File

@@ -16,8 +16,11 @@
import { import {
SCNotFoundErrorResponse, SCNotFoundErrorResponse,
SCPluginAdd, SCPluginAdd,
SCPluginAlreadyRegisteredErrorResponse, SCPluginRegisterResponse, SCPluginRegisterRoute, SCPluginAlreadyRegisteredErrorResponse,
SCPluginRemove, SCValidationErrorResponse, SCPluginRegisterResponse,
SCPluginRegisterRoute,
SCPluginRemove,
SCValidationErrorResponse,
} from '@openstapps/core'; } from '@openstapps/core';
import nock from 'nock'; import nock from 'nock';
import {configFile, plugins} from '../../src/common'; import {configFile, plugins} from '../../src/common';
@@ -36,7 +39,7 @@ export const registerAddRequest: SCPluginAdd = registerRequest as SCPluginAdd;
export const registerRemoveRequest: SCPluginRemove = { export const registerRemoveRequest: SCPluginRemove = {
action: 'remove', action: 'remove',
route: registerAddRequest.plugin.route route: registerAddRequest.plugin.route,
}; };
describe('Plugin registration', async function () { describe('Plugin registration', async function () {
@@ -52,9 +55,9 @@ describe('Plugin registration', async function () {
// register one plugin // register one plugin
const response = await pluginRegisterHandler(registerAddRequest, {}); const response = await pluginRegisterHandler(registerAddRequest, {});
expect(response).to.deep.equal(bodySuccess) expect(response).to.deep.equal(bodySuccess) &&
&& expect(plugins.size).to.equal(1) expect(plugins.size).to.equal(1) &&
&& expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty; expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty;
}); });
it('should allow re-registering the same plugin', async function () { it('should allow re-registering the same plugin', async function () {
@@ -64,9 +67,11 @@ describe('Plugin registration', async function () {
// register the same plugin again // register the same plugin again
const response = await pluginRegisterHandler(registerAddRequest, {}); const response = await pluginRegisterHandler(registerAddRequest, {});
return expect(response).to.deep.equal(bodySuccess) return (
&& expect(plugins.size).to.equal(1) expect(response).to.deep.equal(bodySuccess) &&
&& expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty; expect(plugins.size).to.equal(1) &&
expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty
);
}); });
it('should show an error if a plugin has already been registered', async function () { it('should show an error if a plugin has already been registered', async function () {
@@ -74,9 +79,9 @@ describe('Plugin registration', async function () {
await pluginRegisterHandler(registerAddRequest, {}); await pluginRegisterHandler(registerAddRequest, {});
// create new request for adding a plugin with only name that changed // create new request for adding a plugin with only name that changed
let registerAddRequestAltered: SCPluginAdd = { const registerAddRequestAltered: SCPluginAdd = {
...registerAddRequest, ...registerAddRequest,
plugin: {...registerAddRequest.plugin, name: registerAddRequest.plugin.name + 'foo'} plugin: {...registerAddRequest.plugin, name: registerAddRequest.plugin.name + 'foo'},
}; };
// register the same plugin again // register the same plugin again
@@ -95,9 +100,9 @@ describe('Plugin registration', async function () {
const response = await pluginRegisterHandler(registerRemoveRequest, {}); const response = await pluginRegisterHandler(registerRemoveRequest, {});
expect(response).to.deep.equal(bodySuccess) expect(response).to.deep.equal(bodySuccess) &&
&& expect(plugins.size).to.equal(0) expect(plugins.size).to.equal(0) &&
&& expect(configFile.app.features.plugins).to.be.empty; expect(configFile.app.features.plugins).to.be.empty;
}); });
it('should throw a "not found" error when removing a plugin whose registered route does not exist', async function () { it('should throw a "not found" error when removing a plugin whose registered route does not exist', async function () {
@@ -116,10 +121,10 @@ describe('Plugin registration', async function () {
const pluginRegisterRoute = new SCPluginRegisterRoute(); const pluginRegisterRoute = new SCPluginRegisterRoute();
const validationError = new SCValidationErrorResponse([]); const validationError = new SCValidationErrorResponse([]);
const notFoundError = new SCNotFoundErrorResponse(); const notFoundError = new SCNotFoundErrorResponse();
//@ts-ignore // @ts-expect-error not assignable
const alreadyRegisteredError = new SCPluginAlreadyRegisteredErrorResponse('Foo Message', {}); const alreadyRegisteredError = new SCPluginAlreadyRegisteredErrorResponse('Foo Message', {});
afterEach(async function() { afterEach(async function () {
// remove routes // remove routes
plugins.clear(); plugins.clear();
// clean up request mocks (fixes issue with receiving response from mock from previous test case) // clean up request mocks (fixes issue with receiving response from mock from previous test case)
@@ -157,7 +162,10 @@ describe('Plugin registration', async function () {
// lets simulate that a plugin is already registered // lets simulate that a plugin is already registered
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin); plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
// registering a different plugin for the same route causes the expected error // registering a different plugin for the same route causes the expected error
const registerAddRequestAltered = {...registerAddRequest, plugin: {...registerAddRequest.plugin, name: 'FooBar Plugin'}}; const registerAddRequestAltered = {
...registerAddRequest,
plugin: {...registerAddRequest.plugin, name: 'FooBar Plugin'},
};
const {status, body} = await testApp const {status, body} = await testApp
.post(pluginRegisterRoute.urlPath) .post(pluginRegisterRoute.urlPath)
@@ -169,7 +177,7 @@ describe('Plugin registration', async function () {
expect(body).to.haveOwnProperty('name', 'SCPluginAlreadyRegisteredError'); expect(body).to.haveOwnProperty('name', 'SCPluginAlreadyRegisteredError');
}); });
it('should respond with success when de-registering already registered plugin', async function() { it('should respond with success when de-registering already registered plugin', async function () {
// lets simulate that a plugin is already registered // lets simulate that a plugin is already registered
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin); plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
@@ -183,7 +191,7 @@ describe('Plugin registration', async function () {
expect(body).to.be.deep.equal(bodySuccess); expect(body).to.be.deep.equal(bodySuccess);
}); });
it('should response with 404 when deleting a plugin which was not registered', async function() { it('should response with 404 when deleting a plugin which was not registered', async function () {
// lets simulate that the plugin is already registered // lets simulate that the plugin is already registered
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin); plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* /*
* Copyright (C) 2020 StApps * Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -17,7 +18,8 @@ import {
SCInternalServerErrorResponse, SCInternalServerErrorResponse,
SCMethodNotAllowedErrorResponse, SCMethodNotAllowedErrorResponse,
SCRoute, SCRoute,
SCRouteHttpVerbs, SCValidationErrorResponse, SCRouteHttpVerbs,
SCValidationErrorResponse,
} from '@openstapps/core'; } from '@openstapps/core';
import * as bodyParser from 'body-parser'; import * as bodyParser from 'body-parser';
import sinon from 'sinon'; import sinon from 'sinon';
@@ -31,15 +33,16 @@ import {Logger} from '@openstapps/logger';
import {DEFAULT_TEST_TIMEOUT} from '../common'; import {DEFAULT_TEST_TIMEOUT} from '../common';
interface ReturnType { interface ReturnType {
foo: boolean; foo: boolean;
} }
describe('Create route', async function () { describe('Create route', async function () {
let routeClass: SCRoute; let routeClass: SCRoute;
let handler: ( let handler: (
validatedBody: any, validatedBody: any,
app: Application, params?: { [parameterName: string]: string; }, app: Application,
) => Promise<ReturnType>; parameters?: {[parameterName: string]: string},
) => Promise<ReturnType>;
let app: Express; let app: Express;
const statusCodeSuccess = 222; const statusCodeSuccess = 222;
const bodySuccess = {foo: true}; const bodySuccess = {foo: true};
@@ -75,10 +78,12 @@ describe('Create route', async function () {
it('should complain (throw an error) if used method is other than defined in the route creation', async function () { it('should complain (throw an error) if used method is other than defined in the route creation', async function () {
const methodNotAllowedError = new SCMethodNotAllowedErrorResponse(); const methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(validator, 'validate').returns({errors: []}) sandbox.stub(validator, 'validate').returns({errors: []});
let error: any = {}; let error: any = {};
sandbox.stub(Logger, 'warn').callsFake((err) => { error = err }); sandbox.stub(Logger, 'warn').callsFake(error_ => {
error = error_;
});
const router = createRoute<any, any>(routeClass, handler); const router = createRoute<any, any>(routeClass, handler);
await app.use(router); await app.use(router);
@@ -92,14 +97,12 @@ describe('Create route', async function () {
}); });
it('should provide a route which returns handler response and success code', async function () { it('should provide a route which returns handler response and success code', async function () {
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(validator, 'validate').returns({errors: []}); sandbox.stub(validator, 'validate').returns({errors: []});
const router = createRoute<any, any>(routeClass, handler); const router = createRoute<any, any>(routeClass, handler);
app.use(router); app.use(router);
const response = await supertest(app) const response = await supertest(app).post(routeClass.urlPath).send();
.post(routeClass.urlPath)
.send();
expect(response.status).to.be.equal(statusCodeSuccess); expect(response.status).to.be.equal(statusCodeSuccess);
expect(response.body).to.be.deep.equal(bodySuccess); expect(response.body).to.be.deep.equal(bodySuccess);
@@ -112,7 +115,7 @@ describe('Create route', async function () {
app.use(router); app.use(router);
const startApp = supertest(app); const startApp = supertest(app);
const validatorStub = sandbox.stub(validator, 'validate'); const validatorStub = sandbox.stub(validator, 'validate');
// @ts-ignore // @ts-expect-error not assignable
validatorStub.withArgs(body, routeClass.requestBodyName).returns({errors: [new Error('Foo Error')]}); validatorStub.withArgs(body, routeClass.requestBodyName).returns({errors: [new Error('Foo Error')]});
const response = await startApp const response = await startApp
@@ -128,14 +131,14 @@ describe('Create route', async function () {
const router = createRoute<any, any>(routeClass, handler); const router = createRoute<any, any>(routeClass, handler);
await app.use(router); await app.use(router);
const startApp = supertest(app); const startApp = supertest(app);
// @ts-ignore // @ts-expect-error not assignable
const validatorStub = sandbox.stub(validator, 'validate').returns({errors:[]}); const validatorStub = sandbox.stub(validator, 'validate').returns({errors: []});
// @ts-ignore validatorStub
validatorStub.withArgs(bodySuccess, routeClass.responseBodyName).returns({errors: [new Error('Foo Error')]}); .withArgs(bodySuccess, routeClass.responseBodyName)
// @ts-expect-error not assignable
.returns({errors: [new Error('Foo Error')]});
const response = await startApp const response = await startApp.post(routeClass.urlPath).send();
.post(routeClass.urlPath)
.send();
expect(response.status).to.be.equal(internalServerError.statusCode); expect(response.status).to.be.equal(internalServerError.statusCode);
}); });
@@ -143,8 +146,11 @@ describe('Create route', async function () {
it('should return internal server error if error response not allowed', async function () { it('should return internal server error if error response not allowed', async function () {
class FooErrorResponse { class FooErrorResponse {
statusCode: number; statusCode: number;
name: string; name: string;
message: string; message: string;
constructor(statusCode: number, name: string, message: string) { constructor(statusCode: number, name: string, message: string) {
this.statusCode = statusCode; this.statusCode = statusCode;
this.name = name; this.name = name;
@@ -153,6 +159,7 @@ describe('Create route', async function () {
} }
class BarErrorResponse { class BarErrorResponse {
statusCode: number; statusCode: number;
constructor(statusCode: number) { constructor(statusCode: number) {
this.statusCode = statusCode; this.statusCode = statusCode;
} }
@@ -170,12 +177,10 @@ describe('Create route', async function () {
await app.use(router); await app.use(router);
const startApp = supertest(app); const startApp = supertest(app);
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(validator, 'validate').returns({errors:[]}); sandbox.stub(validator, 'validate').returns({errors: []});
const response = await startApp const response = await startApp.post(routeClass.urlPath).send();
.post(routeClass.urlPath)
.send();
expect(response.status).to.be.equal(internalServerError.statusCode); expect(response.status).to.be.equal(internalServerError.statusCode);
}); });
@@ -183,8 +188,11 @@ describe('Create route', async function () {
it('should return the exact error if error response is allowed', async function () { it('should return the exact error if error response is allowed', async function () {
class FooErrorResponse { class FooErrorResponse {
statusCode: number; statusCode: number;
name: string; name: string;
message: string; message: string;
constructor(statusCode: number, name: string, message: string) { constructor(statusCode: number, name: string, message: string) {
this.statusCode = statusCode; this.statusCode = statusCode;
this.name = name; this.name = name;
@@ -205,12 +213,10 @@ describe('Create route', async function () {
await app.use(router); await app.use(router);
const startApp = supertest(app); const startApp = supertest(app);
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(validator, 'validate').returns({errors:[]}); sandbox.stub(validator, 'validate').returns({errors: []});
const response = await startApp const response = await startApp.post(routeClass.urlPath).send();
.post(routeClass.urlPath)
.send();
expect(response.status).to.be.equal(fooErrorResponse.statusCode); expect(response.status).to.be.equal(fooErrorResponse.statusCode);
}); });

View File

@@ -18,7 +18,7 @@ import {
SCMultiSearchRoute, SCMultiSearchRoute,
SCSearchRoute, SCSearchRoute,
SCSyntaxErrorResponse, SCSyntaxErrorResponse,
SCTooManyRequestsErrorResponse SCTooManyRequestsErrorResponse,
} from '@openstapps/core'; } from '@openstapps/core';
import {expect} from 'chai'; import {expect} from 'chai';
import {configFile} from '../../src/common'; import {configFile} from '../../src/common';
@@ -37,33 +37,24 @@ describe('Search route', async function () {
it('should reject GET, PUT with a valid search query', async function () { it('should reject GET, PUT with a valid search query', async function () {
// const expectedParams = JSON.parse(JSON.stringify(defaultParams)); // const expectedParams = JSON.parse(JSON.stringify(defaultParams));
const {status} = await testApp const {status} = await testApp.get('/search').set('Accept', 'application/json').send({
.get('/search') query: 'Some search terms',
.set('Accept', 'application/json') });
.send({
query: 'Some search terms'
});
expect(status).to.equal(methodNotAllowedError.statusCode); expect(status).to.equal(methodNotAllowedError.statusCode);
}); });
describe('Basic POST /search', async function() { describe('Basic POST /search', async function () {
it('should accept empty JSON object', async function () { it('should accept empty JSON object', async function () {
const {status} = await testApp const {status} = await testApp.post(searchRoute.urlPath).set('Accept', 'application/json').send({});
.post(searchRoute.urlPath)
.set('Accept', 'application/json')
.send({});
expect(status).to.equal(searchRoute.statusCodeSuccess); expect(status).to.equal(searchRoute.statusCodeSuccess);
}); });
it('should accept valid search request', async function () { it('should accept valid search request', async function () {
const {status} = await testApp const {status} = await testApp.post(searchRoute.urlPath).set('Accept', 'application/json').send({
.post(searchRoute.urlPath) query: 'Some search terms',
.set('Accept', 'application/json') });
.send({
query: 'Some search terms'
});
expect(status).to.equal(searchRoute.statusCodeSuccess); expect(status).to.equal(searchRoute.statusCodeSuccess);
}); });
@@ -74,14 +65,14 @@ describe('Search route', async function () {
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.set('Accept', 'application/json') .set('Accept', 'application/json')
.send({ .send({
some: {invalid: 'search'} some: {invalid: 'search'},
}); });
expect(status).to.equal(syntaxError.statusCode); expect(status).to.equal(syntaxError.statusCode);
}); });
}); });
describe('Basic POST /multi/search', async function() { describe('Basic POST /multi/search', async function () {
it('should respond with bad request on invalid search request (empty JSON object as body)', async function () { it('should respond with bad request on invalid search request (empty JSON object as body)', async function () {
const {status} = await testApp const {status} = await testApp
.post(multiSearchRoute.urlPath) .post(multiSearchRoute.urlPath)
@@ -96,8 +87,8 @@ describe('Search route', async function () {
.post(multiSearchRoute.urlPath) .post(multiSearchRoute.urlPath)
.set('Accept', 'application/json') .set('Accept', 'application/json')
.send({ .send({
one: { query: 'Some search terms for one search'}, one: {query: 'Some search terms for one search'},
two: { query: 'Some search terms for another search'} two: {query: 'Some search terms for another search'},
}); });
expect(status).to.equal(multiSearchRoute.statusCodeSuccess); expect(status).to.equal(multiSearchRoute.statusCodeSuccess);

View File

@@ -28,7 +28,8 @@ describe('Thing update route', async function () {
const thingUpdateRoute = new SCThingUpdateRoute(); const thingUpdateRoute = new SCThingUpdateRoute();
it('should update a thing', async function () { it('should update a thing', async function () {
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath.toLocaleLowerCase() const thingUpdateRouteurlPath = thingUpdateRoute.urlPath
.toLocaleLowerCase()
.replace(':type', book.type) .replace(':type', book.type)
.replace(':uid', book.uid); .replace(':uid', book.uid);

View File

@@ -1,3 +1,4 @@
/* eslint-disable unicorn/consistent-function-scoping,@typescript-eslint/no-explicit-any */
/* /*
* Copyright (C) 2019 StApps * Copyright (C) 2019 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -13,11 +14,7 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { import {SCInternalServerErrorResponse, SCPluginMetaData, SCValidationErrorResponse} from '@openstapps/core';
SCInternalServerErrorResponse,
SCPluginMetaData,
SCValidationErrorResponse
} from '@openstapps/core';
import {expect, use} from 'chai'; import {expect, use} from 'chai';
import chaiAsPromised from 'chai-as-promised'; import chaiAsPromised from 'chai-as-promised';
import {Request} from 'express'; import {Request} from 'express';
@@ -41,16 +38,17 @@ describe('Virtual plugin routes', async function () {
/** /**
* Internal method which provides information about the specific error inside of an internal server error * Internal method which provides information about the specific error inside of an internal server error
* *
* @param req Express request * @param request Express request
* @param plugin Plugin information (metadata) * @param plugin Plugin information (metadata)
* @param specificError Class of a specific error * @param specificError Class of a specific error
*/ */
async function testRejection(req: Request, plugin: SCPluginMetaData, specificError: object) { async function testRejection(request: Request, plugin: SCPluginMetaData, specificError: object) {
// eslint-disable-next-line unicorn/error-message
let thrownError: Error = new Error(); let thrownError: Error = new Error();
try { try {
await virtualPluginRoute(req, plugin); await virtualPluginRoute(request, plugin);
} catch (e) { } catch (error) {
thrownError = e; thrownError = error;
} }
// return virtualPluginRoute(req, plugin).should.be.rejectedWith(SCInternalServerErrorResponse); was not working for some reason // return virtualPluginRoute(req, plugin).should.be.rejectedWith(SCInternalServerErrorResponse); was not working for some reason
expect(thrownError).to.be.instanceOf(SCInternalServerErrorResponse); expect(thrownError).to.be.instanceOf(SCInternalServerErrorResponse);
@@ -71,17 +69,17 @@ describe('Virtual plugin routes', async function () {
}, },
}; };
// spy the post method of got // spy the post method of got
// @ts-ignore // @ts-expect-error not assignable
const gotStub = sandbox.stub(got, 'post').returns({body: {}}); const gotStub = sandbox.stub(got, 'post').returns({body: {}});
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(validator, 'validate').returns({errors: []}); sandbox.stub(validator, 'validate').returns({errors: []});
const req = mockReq(request); const request_ = mockReq(request);
await virtualPluginRoute(req, plugin); await virtualPluginRoute(request_, plugin);
expect(gotStub.args[0][0]).to.equal(plugin.route.substr(1)); expect(gotStub.args[0][0]).to.equal(plugin.route.slice(1));
expect(((gotStub.args[0] as any)[1] as Options).prefixUrl).to.equal(plugin.address); expect(((gotStub.args[0] as any)[1] as Options).prefixUrl).to.equal(plugin.address);
expect(((gotStub.args[0] as any)[1] as Options).json).to.equal(req.body); expect(((gotStub.args[0] as any)[1] as Options).json).to.equal(request_.body);
}); });
it('should provide data from the plugin when its route is called', async function () { it('should provide data from the plugin when its route is called', async function () {
@@ -91,18 +89,13 @@ describe('Virtual plugin routes', async function () {
}, },
}; };
const response = { const response = {
result: [ result: [{foo: 'bar'}, {bar: 'foo'}],
{foo: 'bar'}, };
{bar: 'foo'},
]
}
// mock response of the plugin's address // mock response of the plugin's address
nock('http://foo.com:1234') nock('http://foo.com:1234').post('/foo').reply(200, response);
.post('/foo') const request_ = mockReq(request);
.reply(200, response);
const req = mockReq(request);
expect(await virtualPluginRoute(req, plugin)).to.eql(response); expect(await virtualPluginRoute(request_, plugin)).to.eql(response);
}); });
it('should throw the validation error if request is not valid', async function () { it('should throw the validation error if request is not valid', async function () {
@@ -111,9 +104,9 @@ describe('Virtual plugin routes', async function () {
invalid_query_field: 'foo', invalid_query_field: 'foo',
}, },
}; };
const req = mockReq(request); const request_ = mockReq(request);
await testRejection(req, plugin, SCValidationErrorResponse); await testRejection(request_, plugin, SCValidationErrorResponse);
}); });
it('should throw the validation error if response is not valid', async function () { it('should throw the validation error if response is not valid', async function () {
@@ -126,9 +119,9 @@ describe('Virtual plugin routes', async function () {
nock('http://foo.com:1234') nock('http://foo.com:1234')
.post('/foo') .post('/foo')
.reply(200, {invalid_result: ['foo bar']}); .reply(200, {invalid_result: ['foo bar']});
const req = mockReq(request); const request_ = mockReq(request);
await testRejection(req, plugin, SCValidationErrorResponse); await testRejection(request_, plugin, SCValidationErrorResponse);
}); });
it('should throw error if there is a problem with reaching the address of a plugin', async function () { it('should throw error if there is a problem with reaching the address of a plugin', async function () {
@@ -139,13 +132,12 @@ describe('Virtual plugin routes', async function () {
}; };
// fake that post method of got throws an error // fake that post method of got throws an error
sandbox.stub(got, 'post') sandbox.stub(got, 'post').callsFake(() => {
.callsFake(() => { throw new FooError();
throw new FooError(); });
}); const request_ = mockReq(request);
const req = mockReq(request);
await testRejection(req, plugin, FooError); await testRejection(request_, plugin, FooError);
}); });
}); });
@@ -157,7 +149,7 @@ describe('Virtual plugin routes', async function () {
const OK = 200; const OK = 200;
const internalServerError = new SCInternalServerErrorResponse(); const internalServerError = new SCInternalServerErrorResponse();
afterEach(async function() { afterEach(async function () {
// remove routes // remove routes
plugins.clear(); plugins.clear();
// // restore everything to default methods (remove stubs) // // restore everything to default methods (remove stubs)
@@ -194,16 +186,15 @@ describe('Virtual plugin routes', async function () {
expect(barResponse.body).to.be.deep.equal({result: [{foo: 'bar'}, {bar: 'bar'}]}); expect(barResponse.body).to.be.deep.equal({result: [{foo: 'bar'}, {bar: 'bar'}]});
}); });
it('should return error response if plugin address is not responding', async function() { it('should return error response if plugin address is not responding', async function () {
// lets simulate that the plugin is already registered // lets simulate that the plugin is already registered
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin); plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
class FooError extends Error {} class FooError extends Error {}
// fake that got's post method throws an error // fake that got's post method throws an error
sandbox.stub(got, 'post') sandbox.stub(got, 'post').callsFake(() => {
.callsFake(() => { throw new FooError();
throw new FooError(); });
});
const {status} = await testApp const {status} = await testApp
.post('/foo') .post('/foo')
@@ -214,7 +205,7 @@ describe('Virtual plugin routes', async function () {
expect(status).to.be.equal(internalServerError.statusCode); expect(status).to.be.equal(internalServerError.statusCode);
}); });
it('should return the validation error response if plugin request is not valid', async function() { it('should return the validation error response if plugin request is not valid', async function () {
// lets simulate that the plugin is already registered // lets simulate that the plugin is already registered
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin); plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
@@ -223,13 +214,13 @@ describe('Virtual plugin routes', async function () {
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.set('Accept', 'application/json') .set('Accept', 'application/json')
// using number for query instead of (in request schema) required text // using number for query instead of (in request schema) required text
.send({query: 123}) .send({query: 123});
expect(status).to.be.equal(502); expect(status).to.be.equal(502);
expect(body.additionalData).to.haveOwnProperty('name','ValidationError'); expect(body.additionalData).to.haveOwnProperty('name', 'ValidationError');
}); });
it('should return the validation error response if plugin response is not valid', async function() { it('should return the validation error response if plugin response is not valid', async function () {
// lets simulate that the plugin is already registered // lets simulate that the plugin is already registered
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin); plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
// mock response of the plugin address // mock response of the plugin address
@@ -244,7 +235,7 @@ describe('Virtual plugin routes', async function () {
.send({query: 'foo'}); .send({query: 'foo'});
expect(status).to.be.equal(internalServerError.statusCode); expect(status).to.be.equal(internalServerError.statusCode);
expect(body.additionalData).to.haveOwnProperty('name','ValidationError'); expect(body.additionalData).to.haveOwnProperty('name', 'ValidationError');
}); });
}); });
}); });

View File

@@ -15,6 +15,7 @@
*/ */
import {SCBulkRequest, SCThingType} from '@openstapps/core'; import {SCBulkRequest, SCThingType} from '@openstapps/core';
import moment from 'moment'; import moment from 'moment';
// eslint-disable-next-line unicorn/import-style
import util from 'util'; import util from 'util';
import {configFile} from '../../src/common'; import {configFile} from '../../src/common';
import {Bulk, BulkStorage} from '../../src/storage/bulk-storage'; import {Bulk, BulkStorage} from '../../src/storage/bulk-storage';
@@ -72,7 +73,7 @@ describe('Bulk Storage', function () {
expect(esMock.calledWith(bulk)).to.be.true; expect(esMock.calledWith(bulk)).to.be.true;
}); });
it('should not call appropriate database clean-up method on expire if bulk\'s state is done', async function () { it("should not call appropriate database clean-up method on expire if bulk's state is done", async function () {
bulk.state = 'done'; bulk.state = 'done';
sandbox.stub(NodeCache.prototype, 'on').withArgs('expired', sinon.match.any).yields(123, bulk); sandbox.stub(NodeCache.prototype, 'on').withArgs('expired', sinon.match.any).yields(123, bulk);
new BulkStorage(database); new BulkStorage(database);
@@ -88,11 +89,17 @@ describe('Bulk Storage', function () {
}); });
it('should delete a bulk', async function () { it('should delete a bulk', async function () {
const readStub = sandbox.stub(BulkStorage.prototype, 'read').callsFake(() => bulk); const readStub = sandbox.stub(BulkStorage.prototype, 'read').callsFake(() => bulk);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let caught: any; let caught: any;
sandbox.stub(NodeCache.prototype, 'del').callsFake(() => caught = 123); sandbox.stub(NodeCache.prototype, 'del').callsFake(() => (caught = 123));
// force call // force call
sandbox.stub(util, 'promisify').callsFake(() => () => {}).yields(null); sandbox
.stub(util, 'promisify')
// eslint-disable-next-line @typescript-eslint/no-empty-function,unicorn/consistent-function-scoping
.callsFake(() => () => {})
// eslint-disable-next-line unicorn/no-null
.yields(null);
const bulkStorage = new BulkStorage(database); const bulkStorage = new BulkStorage(database);
await bulkStorage.delete(bulk.uid); await bulkStorage.delete(bulk.uid);
@@ -103,15 +110,22 @@ describe('Bulk Storage', function () {
}); });
it('should read an existing bulk', async function () { it('should read an existing bulk', async function () {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let caught: any; let caught: any;
sandbox.stub(NodeCache.prototype, 'get').callsFake(() => caught = 123); sandbox.stub(NodeCache.prototype, 'get').callsFake(() => (caught = 123));
// force call // force call
sandbox.stub(util, 'promisify').callsFake(() => () => {}).yields(null); sandbox
.stub(util, 'promisify')
// eslint-disable-next-line unicorn/consistent-function-scoping,@typescript-eslint/no-empty-function
.callsFake(() => () => {})
// eslint-disable-next-line unicorn/no-null
.yields(null);
const bulkStorage = new BulkStorage(database); const bulkStorage = new BulkStorage(database);
await bulkStorage.read(bulk.uid); await bulkStorage.read(bulk.uid);
expect(caught).to.be.equal(123); expect(caught).to.be.equal(123);
});`` });
``;
}); });
}); });

View File

@@ -20,23 +20,23 @@ import {AggregationResponse} from '../../../src/storage/elasticsearch/types/elas
describe('Aggregations', function () { describe('Aggregations', function () {
const aggregations: AggregationResponse = { const aggregations: AggregationResponse = {
catalog: { 'catalog': {
doc_count: 4, 'doc_count': 4,
'superCatalogs.categories': { 'superCatalogs.categories': {
buckets: [] buckets: [],
}, },
'academicTerm.acronym': { 'academicTerm.acronym': {
buckets: [ buckets: [
{ {
key: 'SoSe 2020', key: 'SoSe 2020',
doc_count: 2 doc_count: 2,
} },
] ],
}, },
'superCatalog.categories': { 'superCatalog.categories': {
buckets: [] buckets: [],
}, },
categories: { 'categories': {
buckets: [ buckets: [
{ {
key: 'foo', key: 'foo',
@@ -46,21 +46,21 @@ describe('Aggregations', function () {
key: 'bar', key: 'bar',
doc_count: 3, doc_count: 3,
}, },
] ],
} },
}, },
person: { 'person': {
doc_count: 13, 'doc_count': 13,
'homeLocations.categories': { 'homeLocations.categories': {
buckets: [] buckets: [],
} },
}, },
'academic event': { 'academic event': {
doc_count: 0, 'doc_count': 0,
'academicTerms.acronym': { 'academicTerms.acronym': {
buckets: [] buckets: [],
}, },
categories: { 'categories': {
buckets: [ buckets: [
{ {
key: 'foobar', key: 'foobar',
@@ -70,18 +70,18 @@ describe('Aggregations', function () {
key: 'bar', key: 'bar',
doc_count: 2, doc_count: 2,
}, },
] ],
}, },
'creativeWorks.keywords': { 'creativeWorks.keywords': {
buckets: [] buckets: [],
} },
}, },
fooType: { 'fooType': {
buckets: [ buckets: [
{ {
doc_count: 321, doc_count: 321,
key: 'foo' key: 'foo',
} },
], ],
}, },
'@all': { '@all': {
@@ -90,71 +90,71 @@ describe('Aggregations', function () {
buckets: [ buckets: [
{ {
key: 'person', key: 'person',
doc_count: 13 doc_count: 13,
}, },
{ {
key: 'catalog', key: 'catalog',
doc_count: 4 doc_count: 4,
} },
] ],
} },
} },
}; };
const expectedFacets: SCFacet[] = [ const expectedFacets: SCFacet[] = [
{ {
buckets: [ buckets: [
{ {
count: 13, count: 13,
'key': 'person' key: 'person',
}, },
{ {
count: 4, count: 4,
key: 'catalog' key: 'catalog',
} },
], ],
field: 'type', field: 'type',
}, },
{ {
buckets: [ buckets: [
{ {
count: 8, count: 8,
key: 'foobar' key: 'foobar',
}, },
{ {
count: 2, count: 2,
key: 'bar' key: 'bar',
} },
], ],
field: 'categories', field: 'categories',
onlyOnType: SCThingType.AcademicEvent, onlyOnType: SCThingType.AcademicEvent,
}, },
{ {
buckets: [ buckets: [
{ {
count: 2, count: 2,
key: 'SoSe 2020' key: 'SoSe 2020',
} },
], ],
field: 'academicTerm.acronym', field: 'academicTerm.acronym',
onlyOnType: SCThingType.Catalog onlyOnType: SCThingType.Catalog,
}, },
{ {
buckets: [ buckets: [
{ {
count: 1, count: 1,
key: 'foo' key: 'foo',
}, },
{ {
count: 3, count: 3,
key: 'bar' key: 'bar',
} },
], ],
field: 'categories', field: 'categories',
onlyOnType: SCThingType.Catalog, onlyOnType: SCThingType.Catalog,
}, },
// no fooType as it doesn't appear in the aggregation schema // no fooType as it doesn't appear in the aggregation schema
]; ];
it('should parse the aggregations providing the appropriate facets', function () { it('should parse the aggregations providing the appropriate facets', function () {
const facets = parseAggregations(aggregations); const facets = parseAggregations(aggregations);

View File

@@ -17,15 +17,15 @@ import {
ESAggMatchAllFilter, ESAggMatchAllFilter,
ESAggTypeFilter, ESAggTypeFilter,
ESNestedAggregation, ESNestedAggregation,
ESTermsFilter ESTermsFilter,
} from '@openstapps/es-mapping-generator/src/types/aggregation'; } from '@openstapps/es-mapping-generator/src/types/aggregation';
import {expect} from "chai"; import {expect} from 'chai';
import { import {
isNestedAggregation, isNestedAggregation,
isBucketAggregation, isBucketAggregation,
isESTermsFilter, isESTermsFilter,
isESAggMatchAllFilter, isESAggMatchAllFilter,
isESNestedAggregation isESNestedAggregation,
} from '../../../lib/storage/elasticsearch/types/guards'; } from '../../../lib/storage/elasticsearch/types/guards';
import {BucketAggregation, NestedAggregation} from '../../../src/storage/elasticsearch/types/elasticsearch'; import {BucketAggregation, NestedAggregation} from '../../../src/storage/elasticsearch/types/elasticsearch';

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any,unicorn/no-null */
/* /*
* Copyright (C) 2020 StApps * Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -14,7 +15,15 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import {ApiResponse, Client} from '@elastic/elasticsearch'; import {ApiResponse, Client} from '@elastic/elasticsearch';
import {SCBook, SCBulkResponse, SCConfigFile, SCMessage, SCSearchQuery, SCThings, SCThingType} from '@openstapps/core'; import {
SCBook,
SCBulkResponse,
SCConfigFile,
SCMessage,
SCSearchQuery,
SCThings,
SCThingType,
} from '@openstapps/core';
import {instance as book} from '@openstapps/core/test/resources/indexable/Book.1.json'; import {instance as book} from '@openstapps/core/test/resources/indexable/Book.1.json';
import {instance as message} from '@openstapps/core/test/resources/indexable/Message.1.json'; import {instance as message} from '@openstapps/core/test/resources/indexable/Message.1.json';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
@@ -43,6 +52,7 @@ describe('Elasticsearch', function () {
const sandbox = sinon.createSandbox(); const sandbox = sinon.createSandbox();
before(function () { before(function () {
// eslint-disable-next-line no-console
console.log('before'); console.log('before');
sandbox.stub(fs, 'readFileSync').returns('{}'); sandbox.stub(fs, 'readFileSync').returns('{}');
}); });
@@ -54,7 +64,7 @@ describe('Elasticsearch', function () {
it('should provide custom elasticsearch URL if defined', function () { it('should provide custom elasticsearch URL if defined', function () {
const customAddress = 'http://foo-address:9200'; const customAddress = 'http://foo-address:9200';
const restore = mockedEnv({ const restore = mockedEnv({
ES_ADDR: customAddress ES_ADDR: customAddress,
}); });
expect(Elasticsearch.getElasticsearchUrl()).to.be.equal(customAddress); expect(Elasticsearch.getElasticsearchUrl()).to.be.equal(customAddress);
@@ -64,7 +74,7 @@ describe('Elasticsearch', function () {
it('should provide local URL as fallback', function () { it('should provide local URL as fallback', function () {
const restore = mockedEnv({ const restore = mockedEnv({
ES_ADDR: undefined ES_ADDR: undefined,
}); });
expect(Elasticsearch.getElasticsearchUrl()).to.match(/(https?:\/\/)?localhost(:\d+)?/); expect(Elasticsearch.getElasticsearchUrl()).to.match(/(https?:\/\/)?localhost(:\d+)?/);
@@ -81,7 +91,7 @@ describe('Elasticsearch', function () {
source: '', source: '',
state: 'in progress', state: 'in progress',
type: SCThingType.Semester, type: SCThingType.Semester,
uid: 'bulk-uid-123-123-123' uid: 'bulk-uid-123-123-123',
}; };
it('should provide index UID from the provided UID', function () { it('should provide index UID from the provided UID', function () {
@@ -94,8 +104,9 @@ describe('Elasticsearch', function () {
}); });
it('should provide index name from the provided data', function () { it('should provide index name from the provided data', function () {
expect(Elasticsearch.getIndex(type as SCThingType, source, bulk)) expect(Elasticsearch.getIndex(type as SCThingType, source, bulk)).to.be.equal(
.to.be.equal(`stapps_${type.split(' ').join('_')}_${source}_${Elasticsearch.getIndexUID(bulk.uid)}`); `stapps_${type.split(' ').join('_')}_${source}_${Elasticsearch.getIndexUID(bulk.uid)}`,
);
}); });
}); });
@@ -109,7 +120,7 @@ describe('Elasticsearch', function () {
}); });
it('should remove invalid characters', function () { it('should remove invalid characters', function () {
expect(Elasticsearch.removeAliasChars('f,o#o\\b|ar/<?al\ias>* ', 'bulk-uid')).to.be.equal('foobaralias'); expect(Elasticsearch.removeAliasChars('f,o#o\\b|ar/<?alias>* ', 'bulk-uid')).to.be.equal('foobaralias');
}); });
it('should remove invalid starting characters', function () { it('should remove invalid starting characters', function () {
@@ -124,10 +135,12 @@ describe('Elasticsearch', function () {
}); });
it('should work with common cases', function () { it('should work with common cases', function () {
expect(Elasticsearch.removeAliasChars('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890', 'bulk-uid')) expect(
.to.be.equal('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890'); Elasticsearch.removeAliasChars('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890', 'bulk-uid'),
expect(Elasticsearch.removeAliasChars('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG', 'bulk-uid')) ).to.be.equal('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890');
.to.be.equal('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG'); expect(
Elasticsearch.removeAliasChars('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG', 'bulk-uid'),
).to.be.equal('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG');
}); });
it('should warn in case of characters that are invalid in future elasticsearch versions', function () { it('should warn in case of characters that are invalid in future elasticsearch versions', function () {
@@ -158,16 +171,16 @@ describe('Elasticsearch', function () {
...configFile.internal, ...configFile.internal,
database: { database: {
name: 'foo', name: 'foo',
version: 123 version: 123,
} },
} },
}; };
expect(() => new Elasticsearch(config)).to.throw(Error); expect(() => new Elasticsearch(config)).to.throw(Error);
}); });
it('should log an error in case of there is one when getting response from the elasticsearch client', async function () { it('should log an error in case of there is one when getting response from the elasticsearch client', async function () {
const error = Error('Foo Error'); const error = new Error('Foo Error');
const loggerErrorStub = sandbox.stub(Logger, 'error').resolves('foo'); const loggerErrorStub = sandbox.stub(Logger, 'error').resolves('foo');
sandbox.stub(Client.prototype, 'on').yields(error); sandbox.stub(Client.prototype, 'on').yields(error);
@@ -185,7 +198,7 @@ describe('Elasticsearch', function () {
expect(loggerLogStub.calledWith(fakeResponse)).to.be.false; expect(loggerLogStub.calledWith(fakeResponse)).to.be.false;
const restore = mockedEnv({ const restore = mockedEnv({
'ES_DEBUG': 'true', ES_DEBUG: 'true',
}); });
new Elasticsearch(configFile); new Elasticsearch(configFile);
@@ -208,8 +221,8 @@ describe('Elasticsearch', function () {
monitoring: { monitoring: {
actions: [], actions: [],
watchers: [], watchers: [],
} },
} },
}; };
const es = new Elasticsearch(config); const es = new Elasticsearch(config);
@@ -225,8 +238,8 @@ describe('Elasticsearch', function () {
monitoring: { monitoring: {
actions: [], actions: [],
watchers: [], watchers: [],
} },
} },
}; };
const monitoringSetUpStub = sandbox.stub(Monitoring, 'setUp'); const monitoringSetUpStub = sandbox.stub(Monitoring, 'setUp');
@@ -245,22 +258,21 @@ describe('Elasticsearch', function () {
beforeEach(function () { beforeEach(function () {
es = new Elasticsearch(configFile); es = new Elasticsearch(configFile);
// @ts-ignore
es.client.indices = { es.client.indices = {
// @ts-ignore // @ts-expect-error not assignable
getAlias: () => Promise.resolve({body: [{[oldIndex]: {aliases: {[SCThingType.Book]: {}}}}]}), getAlias: () => Promise.resolve({body: [{[oldIndex]: {aliases: {[SCThingType.Book]: {}}}}]}),
// @ts-ignore // @ts-expect-error not assignable
putTemplate: () => Promise.resolve({}), putTemplate: () => Promise.resolve({}),
// @ts-ignore // @ts-expect-error not assignable
create: () => Promise.resolve({}), create: () => Promise.resolve({}),
// @ts-ignore // @ts-expect-error not assignable
delete: () => Promise.resolve({}), delete: () => Promise.resolve({}),
// @ts-ignore // @ts-expect-error not assignable
exists: () => Promise.resolve({}), exists: () => Promise.resolve({}),
// @ts-ignore // @ts-expect-error not assignable
refresh: () => Promise.resolve({}), refresh: () => Promise.resolve({}),
// @ts-ignore // @ts-expect-error not assignable
updateAliases: () => Promise.resolve({}) updateAliases: () => Promise.resolve({}),
}; };
}); });
@@ -340,13 +352,13 @@ describe('Elasticsearch', function () {
}, },
{ {
remove: {index: oldIndex, alias: SCThingType.Book}, remove: {index: oldIndex, alias: SCThingType.Book},
} },
]; ];
sandbox.stub(Elasticsearch, 'getIndex').returns(index); sandbox.stub(Elasticsearch, 'getIndex').returns(index);
sandbox.stub(es, 'aliasMap').value({ sandbox.stub(es, 'aliasMap').value({
[SCThingType.Book]: { [SCThingType.Book]: {
[bulk.source]: oldIndex, [bulk.source]: oldIndex,
} },
}); });
const refreshStub = sandbox.stub(es.client.indices, 'refresh'); const refreshStub = sandbox.stub(es.client.indices, 'refresh');
const updateAliasesStub = sandbox.stub(es.client.indices, 'updateAliases'); const updateAliasesStub = sandbox.stub(es.client.indices, 'updateAliases');
@@ -357,11 +369,12 @@ describe('Elasticsearch', function () {
await es.bulkUpdated(bulk); await es.bulkUpdated(bulk);
expect(refreshStub.calledWith({index})).to.be.true; expect(refreshStub.calledWith({index})).to.be.true;
expect(updateAliasesStub.calledWith({ expect(
updateAliasesStub.calledWith({
body: { body: {
actions: expectedRefreshActions actions: expectedRefreshActions,
} },
}) }),
).to.be.true; ).to.be.true;
expect(deleteStub.called).to.be.true; expect(deleteStub.called).to.be.true;
}); });
@@ -392,7 +405,7 @@ describe('Elasticsearch', function () {
_index: '', _index: '',
_score: 0, _score: 0,
_type: '', _type: '',
_source: message as SCMessage _source: message as SCMessage,
}; };
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [foundObject]}}}); sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [foundObject]}}});
@@ -420,7 +433,7 @@ describe('Elasticsearch', function () {
_index: oldIndex, _index: oldIndex,
_score: 0, _score: 0,
_type: '', _type: '',
_source: message as SCMessage _source: message as SCMessage,
}; };
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}}); sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
sandbox.stub(Elasticsearch, 'getIndex').returns(index); sandbox.stub(Elasticsearch, 'getIndex').returns(index);
@@ -434,7 +447,7 @@ describe('Elasticsearch', function () {
_index: getIndex(), _index: getIndex(),
_score: 0, _score: 0,
_type: '', _type: '',
_source: message as SCMessage _source: message as SCMessage,
}; };
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}}); sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
// return index name with different generated UID (see getIndex method) // return index name with different generated UID (see getIndex method)
@@ -451,18 +464,21 @@ describe('Elasticsearch', function () {
}); });
it('should create a new object', async function () { it('should create a new object', async function () {
let caughtParam: any; let caughtParameter: any;
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}}); sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
// @ts-ignore // @ts-expect-error call
let createStub = sandbox.stub(es.client, 'create').callsFake((param) => { const createStub = sandbox.stub(es.client, 'create').callsFake(parameter => {
caughtParam = param; caughtParameter = parameter;
return Promise.resolve({body: {created: true}}); return Promise.resolve({body: {created: true}});
}); });
await es.post(message as SCMessage, bulk); await es.post(message as SCMessage, bulk);
expect(createStub.called).to.be.true; expect(createStub.called).to.be.true;
expect(caughtParam.body).to.be.eql({...message, creation_date: caughtParam.body.creation_date}); expect(caughtParameter.body).to.be.eql({
...message,
creation_date: caughtParameter.body.creation_date,
});
}); });
}); });
@@ -482,32 +498,34 @@ describe('Elasticsearch', function () {
_index: getIndex(), _index: getIndex(),
_score: 0, _score: 0,
_type: '', _type: '',
_source: message as SCMessage _source: message as SCMessage,
}; };
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}}); sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
return expect(es.put(object._source)).to.rejectedWith('exist'); return expect(es.put(object._source)).to.rejectedWith('exist');
}); });
// noinspection JSUnusedLocalSymbols
it('should update the object if it already exists', async function () { it('should update the object if it already exists', async function () {
let caughtParam: any; let caughtParameter: any;
const object: ElasticsearchObject<SCMessage> = { const object: ElasticsearchObject<SCMessage> = {
_id: '', _id: '',
_index: getIndex(), _index: getIndex(),
_score: 0, _score: 0,
_type: '', _type: '',
_source: message as SCMessage _source: message as SCMessage,
}; };
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}}); sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
// @ts-ignore // @ts-expect-error unused
const stubUpdate = sandbox.stub(es.client, 'update').callsFake((params) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars
caughtParam = params; const stubUpdate = sandbox.stub(es.client, 'update').callsFake(parameters => {
caughtParameter = parameters;
return Promise.resolve({body: {created: true}}); return Promise.resolve({body: {created: true}});
}); });
await es.put(object._source); await es.put(object._source);
expect(caughtParam.body.doc).to.be.eql(object._source); expect(caughtParameter.body.doc).to.be.eql(object._source);
}); });
}); });
@@ -519,14 +537,14 @@ describe('Elasticsearch', function () {
_index: getIndex(), _index: getIndex(),
_score: 0, _score: 0,
_type: '', _type: '',
_source: message as SCMessage _source: message as SCMessage,
}; };
const objectBook: ElasticsearchObject<SCBook> = { const objectBook: ElasticsearchObject<SCBook> = {
_id: '321', _id: '321',
_index: getIndex(), _index: getIndex(),
_score: 0, _score: 0,
_type: '', _type: '',
_source: book as SCBook _source: book as SCBook,
}; };
const fakeEsAggregations = { const fakeEsAggregations = {
'@all': { '@all': {
@@ -537,40 +555,36 @@ describe('Elasticsearch', function () {
buckets: [ buckets: [
{ {
key: 'person', key: 'person',
doc_count: 13 doc_count: 13,
}, },
{ {
key: 'catalog', key: 'catalog',
doc_count: 4 doc_count: 4,
} },
] ],
} },
} },
}; };
const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = { const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = {
// @ts-ignore
body: { body: {
took: 12, took: 12,
timed_out: false, timed_out: false,
// @ts-ignore // @ts-expect-error not assignable
_shards: {}, _shards: {},
// @ts-ignore // @ts-expect-error not assignable
hits: { hits: {
hits: [ hits: [objectMessage, objectBook],
objectMessage, total: 123,
objectBook,
],
total: 123
}, },
aggregations: fakeEsAggregations aggregations: fakeEsAggregations,
}, },
headers: {}, headers: {},
// @ts-ignore // @ts-expect-error not assignable
meta: {}, meta: {},
// @ts-ignore // @ts-expect-error not assignable
statusCode: {}, statusCode: {},
// @ts-ignore // @ts-expect-error not assignable
warnings: {} warnings: {},
}; };
let searchStub: sinon.SinonStub; let searchStub: sinon.SinonStub;
before(function () { before(function () {
@@ -593,11 +607,11 @@ describe('Elasticsearch', function () {
}, },
{ {
count: 4, count: 4,
key: 'catalog' key: 'catalog',
} },
], ],
field: 'type', field: 'type',
} },
]; ];
const {data, facets} = await es.search({}); const {data, facets} = await es.search({});
@@ -613,7 +627,7 @@ describe('Elasticsearch', function () {
expect(pagination).to.be.eql({ expect(pagination).to.be.eql({
count: fakeSearchResponse.body!.hits.hits.length, count: fakeSearchResponse.body!.hits.hits.length,
offset: from, offset: from,
total: fakeSearchResponse.body!.hits.total total: fakeSearchResponse.body!.hits.total,
}); });
}); });
@@ -624,7 +638,7 @@ describe('Elasticsearch', function () {
}); });
it('should build the search request properly', async function () { it('should build the search request properly', async function () {
const params: SCSearchQuery = { const parameters: SCSearchQuery = {
query: 'mathematics', query: 'mathematics',
from: 30, from: 30,
size: 5, size: 5,
@@ -632,42 +646,37 @@ describe('Elasticsearch', function () {
{ {
type: 'ducet', type: 'ducet',
order: 'desc', order: 'desc',
arguments: arguments: {
{ field: 'name',
field: 'name' },
} },
}
], ],
filter: { filter: {
type: 'value', type: 'value',
arguments: { arguments: {
field: 'type', field: 'type',
value: SCThingType.AcademicEvent value: SCThingType.AcademicEvent,
} },
} },
}; };
const fakeResponse = {foo: 'bar'}; const fakeResponse = {foo: 'bar'};
const fakeBuildSortResponse = [fakeResponse]; const fakeBuildSortResponse = [fakeResponse];
// @ts-ignore // @ts-expect-error not assignable
sandbox.stub(query, 'buildQuery').returns(fakeResponse); sandbox.stub(query, 'buildQuery').returns(fakeResponse);
// @ts-ignore
sandbox.stub(query, 'buildSort').returns(fakeBuildSortResponse); sandbox.stub(query, 'buildSort').returns(fakeBuildSortResponse);
await es.search(params); await es.search(parameters);
sandbox.assert sandbox.assert.calledWithMatch(searchStub, {
.calledWithMatch(searchStub, body: {
{ aggs: aggregations,
body: { query: fakeResponse,
aggs: aggregations, sort: fakeBuildSortResponse,
query: fakeResponse, },
sort: fakeBuildSortResponse from: parameters.from,
}, index: Elasticsearch.getListOfAllIndices(),
from: params.from, size: parameters.size,
index: Elasticsearch.getListOfAllIndices(), });
size: params.size,
}
);
}); });
}); });
}); });

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* /*
* Copyright (C) 2020 StApps * Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -18,7 +19,8 @@ import {
SCMonitoringConfiguration, SCMonitoringConfiguration,
SCMonitoringLogAction, SCMonitoringLogAction,
SCMonitoringMailAction, SCMonitoringMailAction,
SCMonitoringWatcher, SCThings SCMonitoringWatcher,
SCThings,
} from '@openstapps/core'; } from '@openstapps/core';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {SearchResponse} from 'elasticsearch'; import {SearchResponse} from 'elasticsearch';
@@ -26,7 +28,7 @@ import {MailQueue} from '../../../src/notification/mail-queue';
import {setUp} from '../../../src/storage/elasticsearch/monitoring'; import {setUp} from '../../../src/storage/elasticsearch/monitoring';
import {getTransport} from '../../common'; import {getTransport} from '../../common';
import { expect } from 'chai'; import {expect} from 'chai';
import sinon from 'sinon'; import sinon from 'sinon';
import cron from 'node-cron'; import cron from 'node-cron';
@@ -35,16 +37,15 @@ describe('Monitoring', async function () {
const logAction: SCMonitoringLogAction = { const logAction: SCMonitoringLogAction = {
message: 'Foo monitoring message', message: 'Foo monitoring message',
prefix: 'Backend Monitoring', prefix: 'Backend Monitoring',
type: 'log' type: 'log',
}; };
const mailAction: SCMonitoringMailAction = { const mailAction: SCMonitoringMailAction = {
message: 'Bar monitoring message', message: 'Bar monitoring message',
recipients: ['xyz@xyz.com'], recipients: ['xyz@xyz.com'],
subject: 'Backend Monitoring', subject: 'Backend Monitoring',
type: 'mail' type: 'mail',
}; };
let transport: any; let transport: any;
// @ts-ignore
let mailQueue: any; let mailQueue: any;
beforeEach(async function () { beforeEach(async function () {
transport = getTransport(true); transport = getTransport(true);
@@ -55,52 +56,52 @@ describe('Monitoring', async function () {
sandbox.restore(); sandbox.restore();
}); });
// const sandbox = sinon.createSandbox(); // const sandbox = sinon.createSandbox();
let cronScheduleStub: sinon.SinonStub let cronScheduleStub: sinon.SinonStub;
const minLengthWatcher: SCMonitoringWatcher = { const minLengthWatcher: SCMonitoringWatcher = {
actions: [logAction, mailAction], actions: [logAction, mailAction],
conditions: [ conditions: [
{ {
length: 10, length: 10,
type: 'MinimumLength' type: 'MinimumLength',
} },
], ],
name: 'foo watcher', name: 'foo watcher',
query: {foo: 'bar'}, query: {foo: 'bar'},
triggers: [ triggers: [
{ {
executionTime: 'monthly', executionTime: 'monthly',
name: 'beginning of month' name: 'beginning of month',
}, },
{ {
executionTime: 'daily', executionTime: 'daily',
name: 'every night' name: 'every night',
} },
] ],
}; };
const maxLengthWatcher: SCMonitoringWatcher = { const maxLengthWatcher: SCMonitoringWatcher = {
actions: [logAction, mailAction], actions: [logAction, mailAction],
conditions: [ conditions: [
{ {
length: 30, length: 30,
type: 'MaximumLength' type: 'MaximumLength',
} },
], ],
name: 'foo watcher', name: 'foo watcher',
query: {bar: 'foo'}, query: {bar: 'foo'},
triggers: [ triggers: [
{ {
executionTime: 'hourly', executionTime: 'hourly',
name: 'every hour' name: 'every hour',
}, },
{ {
executionTime: 'weekly', executionTime: 'weekly',
name: 'every week' name: 'every week',
}, },
] ],
}; };
const monitoringConfig: SCMonitoringConfiguration = { const monitoringConfig: SCMonitoringConfiguration = {
actions: [logAction, mailAction], actions: [logAction, mailAction],
watchers: [minLengthWatcher, maxLengthWatcher] watchers: [minLengthWatcher, maxLengthWatcher],
}; };
it('should create a schedule for each trigger', async function () { it('should create a schedule for each trigger', async function () {
@@ -111,19 +112,18 @@ describe('Monitoring', async function () {
it('should log errors where conditions failed', async function () { it('should log errors where conditions failed', async function () {
const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = { const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = {
// @ts-ignore
body: { body: {
took: 12, took: 12,
timed_out: false, timed_out: false,
// @ts-ignore // @ts-expect-error not assignable
_shards: {}, _shards: {},
// @ts-ignore // @ts-expect-error not assignable
hits: { hits: {
total: 123 total: 123,
}, },
}, },
}; };
let fakeClient = new Client({node: 'http://foohost:9200'}); const fakeClient = new Client({node: 'http://foohost:9200'});
const loggerErrorStub = sandbox.stub(Logger, 'error'); const loggerErrorStub = sandbox.stub(Logger, 'error');
const mailQueueSpy = sinon.spy(mailQueue, 'push'); const mailQueueSpy = sinon.spy(mailQueue, 'push');
cronScheduleStub.yields(); cronScheduleStub.yields();

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any,unicorn/no-null */
/* /*
* Copyright (C) 2020 StApps * Copyright (C) 2020 StApps
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@@ -15,11 +16,13 @@
*/ */
import { import {
SCConfigFile, SCConfigFile,
SCSearchBooleanFilter, SCSearchDateRangeFilter, SCSearchBooleanFilter,
SCSearchFilter, SCSearchNumericRangeFilter, SCSearchDateRangeFilter,
SCSearchFilter,
SCSearchNumericRangeFilter,
SCSearchQuery, SCSearchQuery,
SCSearchSort, SCSearchSort,
SCThingType SCThingType,
} from '@openstapps/core'; } from '@openstapps/core';
import {expect} from 'chai'; import {expect} from 'chai';
import { import {
@@ -35,7 +38,12 @@ import {
ScriptSort, ScriptSort,
} from '../../../src/storage/elasticsearch/types/elasticsearch'; } from '../../../src/storage/elasticsearch/types/elasticsearch';
import {configFile} from '../../../src/common'; import {configFile} from '../../../src/common';
import {buildBooleanFilter, buildFilter, buildQuery, buildSort} from '../../../src/storage/elasticsearch/query'; import {
buildBooleanFilter,
buildFilter,
buildQuery,
buildSort,
} from '../../../src/storage/elasticsearch/query';
describe('Query', function () { describe('Query', function () {
describe('buildBooleanFilter', function () { describe('buildBooleanFilter', function () {
@@ -47,21 +55,21 @@ describe('Query', function () {
type: 'value', type: 'value',
arguments: { arguments: {
field: 'type', field: 'type',
value: SCThingType.Catalog value: SCThingType.Catalog,
} },
}, },
{ {
type: 'value', type: 'value',
arguments: { arguments: {
field: 'type', field: 'type',
value: SCThingType.Building value: SCThingType.Building,
} },
} },
] ],
}, },
type: 'boolean' type: 'boolean',
}; };
const booleanFilters: { [key: string]: SCSearchBooleanFilter } = { const booleanFilters: {[key: string]: SCSearchBooleanFilter} = {
and: booleanFilter, and: booleanFilter,
or: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'or'}}, or: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'or'}},
not: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'not'}}, not: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'not'}},
@@ -69,14 +77,14 @@ describe('Query', function () {
const expectedEsFilters: Array<ESTermFilter> = [ const expectedEsFilters: Array<ESTermFilter> = [
{ {
term: { term: {
'type.raw': 'catalog' 'type.raw': 'catalog',
} },
}, },
{ {
term: { term: {
'type.raw': 'building' 'type.raw': 'building',
} },
} },
]; ];
it('should create appropriate elasticsearch "and" filter argument', function () { it('should create appropriate elasticsearch "and" filter argument', function () {
@@ -100,7 +108,7 @@ describe('Query', function () {
}); });
describe('buildQuery', function () { describe('buildQuery', function () {
const params: SCSearchQuery = { const parameters: SCSearchQuery = {
query: 'mathematics', query: 'mathematics',
from: 30, from: 30,
size: 5, size: 5,
@@ -108,27 +116,25 @@ describe('Query', function () {
{ {
type: 'ducet', type: 'ducet',
order: 'desc', order: 'desc',
arguments: arguments: {
{ field: 'name',
field: 'name' },
}
}, },
{ {
type: 'ducet', type: 'ducet',
order: 'desc', order: 'desc',
arguments: arguments: {
{ field: 'categories',
field: 'categories' },
}
}, },
], ],
filter: { filter: {
type: 'value', type: 'value',
arguments: { arguments: {
field: 'type', field: 'type',
value: SCThingType.AcademicEvent value: SCThingType.AcademicEvent,
} },
} },
}; };
let esConfig: ElasticsearchConfig = { let esConfig: ElasticsearchConfig = {
name: 'elasticsearch', name: 'elasticsearch',
@@ -138,7 +144,7 @@ describe('Query', function () {
queryType: 'dis_max', queryType: 'dis_max',
matchBoosting: 1.3, matchBoosting: 1.3,
fuzziness: 'AUTO', fuzziness: 'AUTO',
cutoffFrequency: 0.0, cutoffFrequency: 0,
tieBreaker: 0, tieBreaker: 0,
}, },
}; };
@@ -147,59 +153,59 @@ describe('Query', function () {
queryType: 'dis_max', queryType: 'dis_max',
matchBoosting: 1.3, matchBoosting: 1.3,
fuzziness: 'AUTO', fuzziness: 'AUTO',
cutoffFrequency: 0.0, cutoffFrequency: 0,
tieBreaker: 0, tieBreaker: 0,
}; };
const config: SCConfigFile = { const config: SCConfigFile = {
...configFile ...configFile,
}; };
beforeEach(function () { beforeEach(function () {
esConfig = { esConfig = {
name: 'elasticsearch', name: 'elasticsearch',
version: '123' version: '123',
}; };
}); });
// TODO: check parts of received elasticsearch query for each test case // TODO: check parts of received elasticsearch query for each test case
it('should build query that includes sorting when query is undefined', function () { it('should build query that includes sorting when query is undefined', function () {
expect(buildQuery(params, config, esConfig)).to.be.an('object'); expect(buildQuery(parameters, config, esConfig)).to.be.an('object');
}); });
it('should build query that includes sorting when query type is query_string', function () { it('should build query that includes sorting when query type is query_string', function () {
esConfig.query = {...query, queryType: 'query_string'}; esConfig.query = {...query, queryType: 'query_string'};
expect(buildQuery(params, config, esConfig)).to.be.an('object'); expect(buildQuery(parameters, config, esConfig)).to.be.an('object');
}); });
it('should build query that includes sorting when query type is dis_max', function () { it('should build query that includes sorting when query type is dis_max', function () {
esConfig.query = {...query, queryType: 'dis_max'}; esConfig.query = {...query, queryType: 'dis_max'};
expect(buildQuery(params, config, esConfig)).to.be.an('object'); expect(buildQuery(parameters, config, esConfig)).to.be.an('object');
}); });
it('should build query that includes sorting when query type is dis_max', function () { it('should build query that includes sorting when query type is dis_max', function () {
esConfig.query = {...query, queryType: 'dis_max'}; esConfig.query = {...query, queryType: 'dis_max'};
expect(buildQuery(params, config, esConfig)).to.be.an('object'); expect(buildQuery(parameters, config, esConfig)).to.be.an('object');
}); });
it('should reject (throw an error) if provided query type is not supported', function () { it('should reject (throw an error) if provided query type is not supported', function () {
// @ts-ignore // @ts-expect-error not assignable
esConfig.query = {...query, queryType: 'invalid_query_type'}; esConfig.query = {...query, queryType: 'invalid_query_type'};
expect(() => buildQuery(params, config, esConfig)).to.throw('query type'); expect(() => buildQuery(parameters, config, esConfig)).to.throw('query type');
}); });
}); });
describe('buildFilter', function () { describe('buildFilter', function () {
const searchFilters: { [key: string]: SCSearchFilter } = { const searchFilters: {[key: string]: SCSearchFilter} = {
value: { value: {
type: 'value', type: 'value',
arguments: { arguments: {
field: 'type', field: 'type',
value: SCThingType.Dish value: SCThingType.Dish,
} },
}, },
distance: { distance: {
type: 'distance', type: 'distance',
@@ -207,7 +213,7 @@ describe('Query', function () {
distance: 1000, distance: 1000,
field: 'geo', field: 'geo',
position: [50.123, 8.123], position: [50.123, 8.123],
} },
}, },
geoPoint: { geoPoint: {
type: 'geo', type: 'geo',
@@ -218,9 +224,9 @@ describe('Query', function () {
coordinates: [ coordinates: [
[50.123, 8.123], [50.123, 8.123],
[50.123, 8.123], [50.123, 8.123],
] ],
} },
} },
}, },
geoShape: { geoShape: {
type: 'geo', type: 'geo',
@@ -232,9 +238,9 @@ describe('Query', function () {
coordinates: [ coordinates: [
[50.123, 8.123], [50.123, 8.123],
[50.123, 8.123], [50.123, 8.123],
] ],
} },
} },
}, },
boolean: { boolean: {
type: 'boolean', type: 'boolean',
@@ -246,16 +252,16 @@ describe('Query', function () {
arguments: { arguments: {
field: 'type', field: 'type',
value: SCThingType.Dish, value: SCThingType.Dish,
} },
}, },
{ {
type: 'availability', type: 'availability',
arguments: { arguments: {
field: 'offers.availabilityRange' field: 'offers.availabilityRange',
} },
} },
] ],
} },
}, },
}; };
@@ -263,8 +269,8 @@ describe('Query', function () {
const filter = buildFilter(searchFilters.value); const filter = buildFilter(searchFilters.value);
const expectedFilter: ESTermFilter = { const expectedFilter: ESTermFilter = {
term: { term: {
'type.raw': SCThingType.Dish 'type.raw': SCThingType.Dish,
} },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -277,28 +283,30 @@ describe('Query', function () {
range: { range: {
price: { price: {
relation: undefined, relation: undefined,
} },
} },
}; };
const rawFilter: SCSearchNumericRangeFilter = { const rawFilter: SCSearchNumericRangeFilter = {
type: 'numeric range', type: 'numeric range',
arguments: { arguments: {
bounds: {}, bounds: {},
field: 'price' field: 'price',
} },
}; };
// eslint-disable-next-line unicorn/consistent-function-scoping
const setBound = (location: 'upperBound' | 'lowerBound', bound: string | null) => { const setBound = (location: 'upperBound' | 'lowerBound', bound: string | null) => {
let out: number | null = null; let out: number | null = null;
if (bound != null) { if (bound != undefined) {
out = Math.random(); out = Math.random();
rawFilter.arguments.bounds[location] = { rawFilter.arguments.bounds[location] = {
mode: bound as 'inclusive' | 'exclusive', mode: bound as 'inclusive' | 'exclusive',
limit: out, limit: out,
}; };
// @ts-ignore implicit any expectedFilter.range.price[
expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out; `${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`
] = out;
} }
}; };
setBound('upperBound', upperMode); setBound('upperBound', upperMode);
@@ -307,9 +315,9 @@ describe('Query', function () {
const filter = buildFilter(rawFilter) as ESNumericRangeFilter; const filter = buildFilter(rawFilter) as ESNumericRangeFilter;
expect(filter).to.deep.equal(expectedFilter); expect(filter).to.deep.equal(expectedFilter);
for (const bound of ['g', 'l']) { for (const bound of ['g', 'l']) {
// @ts-ignore implicit any // @ts-expect-error implicit any
const inclusiveExists = typeof filter.range.price[`${bound}t`] !== 'undefined'; const inclusiveExists = typeof filter.range.price[`${bound}t`] !== 'undefined';
// @ts-ignore implicit any // @ts-expect-error implicit any
const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined'; const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined';
// only one should exist at the same time // only one should exist at the same time
@@ -328,8 +336,8 @@ describe('Query', function () {
format: 'thisIsADummyFormat', format: 'thisIsADummyFormat',
time_zone: 'thisIsADummyTimeZone', time_zone: 'thisIsADummyTimeZone',
relation: 'testRelation' as any, relation: 'testRelation' as any,
} },
} },
}; };
const rawFilter: SCSearchDateRangeFilter = { const rawFilter: SCSearchDateRangeFilter = {
@@ -340,19 +348,20 @@ describe('Query', function () {
relation: 'testRelation' as any, relation: 'testRelation' as any,
format: 'thisIsADummyFormat', format: 'thisIsADummyFormat',
timeZone: 'thisIsADummyTimeZone', timeZone: 'thisIsADummyTimeZone',
} },
}; };
const setBound = (location: 'upperBound' | 'lowerBound', bound: string | null) => { const setBound = (location: 'upperBound' | 'lowerBound', bound: string | null) => {
let out: string | null = null; let out: string | null = null;
if (bound != null) { if (bound != undefined) {
out = `${location} ${bound} ${upperMode} ${lowerMode}`; out = `${location} ${bound} ${upperMode} ${lowerMode}`;
rawFilter.arguments.bounds[location] = { rawFilter.arguments.bounds[location] = {
mode: bound as 'inclusive' | 'exclusive', mode: bound as 'inclusive' | 'exclusive',
limit: out, limit: out,
}; };
// @ts-ignore implicit any expectedFilter.range.price[
expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out; `${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`
] = out;
} }
}; };
setBound('upperBound', upperMode); setBound('upperBound', upperMode);
@@ -361,9 +370,9 @@ describe('Query', function () {
const filter = buildFilter(rawFilter) as ESNumericRangeFilter; const filter = buildFilter(rawFilter) as ESNumericRangeFilter;
expect(filter).to.deep.equal(expectedFilter); expect(filter).to.deep.equal(expectedFilter);
for (const bound of ['g', 'l']) { for (const bound of ['g', 'l']) {
// @ts-ignore implicit any // @ts-expect-error implicit any
const inclusiveExists = typeof filter.range.price[`${bound}t`] !== 'undefined'; const inclusiveExists = typeof filter.range.price[`${bound}t`] !== 'undefined';
// @ts-ignore implicit any // @ts-expect-error implicit any
const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined'; const exclusiveExists = typeof filter.range.price[`${bound}te`] !== 'undefined';
// only one should exist at the same time // only one should exist at the same time
@@ -390,7 +399,7 @@ describe('Query', function () {
'offers.availabilityRange': { 'offers.availabilityRange': {
gte: `test||/${scope}`, gte: `test||/${scope}`,
lt: `test||+1${scope}/${scope}`, lt: `test||+1${scope}/${scope}`,
} },
}, },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -411,7 +420,7 @@ describe('Query', function () {
'offers.availabilityRange': { 'offers.availabilityRange': {
gte: 'test||/s', gte: 'test||/s',
lt: 'test||+1s/s', lt: 'test||+1s/s',
} },
}, },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -432,7 +441,7 @@ describe('Query', function () {
'offers.availabilityRange': { 'offers.availabilityRange': {
gte: `test||/d`, gte: `test||/d`,
lt: `test||+1d/d`, lt: `test||+1d/d`,
} },
}, },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -452,7 +461,7 @@ describe('Query', function () {
'offers.availabilityRange': { 'offers.availabilityRange': {
gte: `now/d`, gte: `now/d`,
lt: `now+1d/d`, lt: `now+1d/d`,
} },
}, },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -463,12 +472,12 @@ describe('Query', function () {
const filter = buildFilter(searchFilters.distance); const filter = buildFilter(searchFilters.distance);
const expectedFilter: ESGeoDistanceFilter = { const expectedFilter: ESGeoDistanceFilter = {
geo_distance: { geo_distance: {
distance: '1000m', 'distance': '1000m',
'geo.point.coordinates': { 'geo.point.coordinates': {
lat: 8.123, lat: 8.123,
lon: 50.123 lon: 50.123,
} },
} },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -488,24 +497,24 @@ describe('Query', function () {
type: 'envelope', type: 'envelope',
coordinates: [ coordinates: [
[50.123, 8.123], [50.123, 8.123],
[50.123, 8.123] [50.123, 8.123],
] ],
}, },
}, },
ignore_unmapped: true, 'ignore_unmapped': true,
} },
}, },
{ {
geo_bounding_box: { geo_bounding_box: {
'geo.point.coordinates': { 'geo.point.coordinates': {
bottom_right: [50.123, 8.123], bottom_right: [50.123, 8.123],
top_left: [50.123, 8.123] top_left: [50.123, 8.123],
}, },
ignore_unmapped: true, 'ignore_unmapped': true,
}, },
}, },
] ],
} },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -521,12 +530,12 @@ describe('Query', function () {
type: 'envelope', type: 'envelope',
coordinates: [ coordinates: [
[50.123, 8.123], [50.123, 8.123],
[50.123, 8.123] [50.123, 8.123],
] ],
}, },
}, },
ignore_unmapped: true, 'ignore_unmapped': true,
} },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -540,8 +549,8 @@ describe('Query', function () {
must: [ must: [
{ {
term: { term: {
'type.raw': 'dish' 'type.raw': 'dish',
} },
}, },
{ {
range: { range: {
@@ -549,13 +558,13 @@ describe('Query', function () {
gte: 'now/s', gte: 'now/s',
lt: 'now+1s/s', lt: 'now+1s/s',
relation: 'intersects', relation: 'intersects',
} },
} },
} },
], ],
must_not: [], must_not: [],
should: [] should: [],
} },
}; };
expect(filter).to.be.eql(expectedFilter); expect(filter).to.be.eql(expectedFilter);
@@ -568,7 +577,7 @@ describe('Query', function () {
type: 'ducet', type: 'ducet',
order: 'desc', order: 'desc',
arguments: { arguments: {
field: 'name' field: 'name',
}, },
}, },
{ {
@@ -576,14 +585,14 @@ describe('Query', function () {
order: 'desc', order: 'desc',
arguments: { arguments: {
field: 'name', field: 'name',
} },
}, },
{ {
type: 'distance', type: 'distance',
order: 'desc', order: 'desc',
arguments: { arguments: {
field: 'geo', field: 'geo',
position: [8.123, 50.123] position: [8.123, 50.123],
}, },
}, },
{ {
@@ -592,35 +601,35 @@ describe('Query', function () {
arguments: { arguments: {
universityRole: 'student', universityRole: 'student',
field: 'offers.prices', field: 'offers.prices',
} },
}, },
]; ];
let sorts: Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> = []; let sorts: Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> = [];
const expectedSorts: { [key: string]: ESGenericSort | ESGeoDistanceSort | ScriptSort } = { const expectedSorts: {[key: string]: ESGenericSort | ESGeoDistanceSort | ScriptSort} = {
ducet: { ducet: {
'name.sort': 'desc' 'name.sort': 'desc',
}, },
generic: { generic: {
'name': 'desc' name: 'desc',
}, },
distance: { distance: {
_geo_distance: { _geo_distance: {
mode: 'avg', 'mode': 'avg',
order: 'desc', 'order': 'desc',
unit: 'm', 'unit': 'm',
'geo.point.coordinates': { 'geo.point.coordinates': {
lat: 50.123, lat: 50.123,
lon: 8.123 lon: 8.123,
} },
} },
}, },
price: { price: {
_script: { _script: {
order: 'asc', order: 'asc',
script: '\n // foo price sort script', script: '\n // foo price sort script',
type: 'number' type: 'number',
} },
} },
}; };
before(function () { before(function () {
sorts = buildSort(searchSCSearchSort); sorts = buildSort(searchSCSearchSort);
@@ -641,7 +650,10 @@ describe('Query', function () {
it('should build price sort', function () { it('should build price sort', function () {
const priceSortNoScript = { const priceSortNoScript = {
...sorts[3], ...sorts[3],
_script: {...(sorts[3] as ScriptSort)._script, script: (expectedSorts.price as ScriptSort)._script.script} _script: {
...(sorts[3] as ScriptSort)._script,
script: (expectedSorts.price as ScriptSort)._script.script,
},
}; };
expect(priceSortNoScript).to.be.eql(expectedSorts.price); expect(priceSortNoScript).to.be.eql(expectedSorts.price);
}); });

View File

@@ -6,6 +6,6 @@
}, },
"exclude": [ "exclude": [
"./config/", "./config/",
"./test/" "./test"
] ]
} }

View File

@@ -1,3 +0,0 @@
{
"extends": "./node_modules/@openstapps/configuration/tslint.json"
}