mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-07 14:02:48 +00:00
Resolve "Transition to ESLint"
This commit is contained in:
committed by
Rainer Killinger
parent
ca1d2444e0
commit
418ba67d15
2
.eslintignore
Normal file
2
.eslintignore
Normal file
@@ -0,0 +1,2 @@
|
||||
resources
|
||||
openapi
|
||||
3
.eslintrc.json
Normal file
3
.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "@openstapps"
|
||||
}
|
||||
126
ROUTES.md
126
ROUTES.md
@@ -18,12 +18,12 @@ This checks if a book is available in a library.<br>
|
||||
|
||||
### Definition
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCBookAvailabilityRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityrequest) |
|
||||
| response | [SCBookAvailabilityResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityresponse) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCBookAvailabilityRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityrequest) |
|
||||
| response | [SCBookAvailabilityResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbookavailabilityresponse) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCBulkAddRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbulkaddrequest) |
|
||||
| response | [SCBulkAddResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkaddresponse.html) |
|
||||
| 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) |
|
||||
| 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> |
|
||||
| parameter | value |
|
||||
|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCBulkAddRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scbulkaddrequest) |
|
||||
| response | [SCBulkAddResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkaddresponse.html) |
|
||||
| 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) |
|
||||
| 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)
|
||||
|
||||
@@ -50,13 +50,13 @@ This checks if a book is available in a library.<br>
|
||||
|
||||
### Definition
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCBulkDoneRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdonerequest.html) |
|
||||
| response | [SCBulkDoneResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdoneresponse.html) |
|
||||
| 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) |
|
||||
| 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> |
|
||||
| parameter | value |
|
||||
|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCBulkDoneRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdonerequest.html) |
|
||||
| response | [SCBulkDoneResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkdoneresponse.html) |
|
||||
| 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) |
|
||||
| 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)
|
||||
|
||||
@@ -66,12 +66,12 @@ This checks if a book is available in a library.<br>
|
||||
|
||||
### Definition
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCBulkRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkrequest.html) |
|
||||
| response | [SCBulkResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkresponse.html) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCBulkRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkrequest.html) |
|
||||
| response | [SCBulkResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scbulkresponse.html) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCFeedbackRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackrequest.html) |
|
||||
| response | [SCFeedbackResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackresponse.html) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCFeedbackRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackrequest.html) |
|
||||
| response | [SCFeedbackResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scfeedbackresponse.html) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCIndexRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexrequest.html) |
|
||||
| response | [SCIndexResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexresponse.html) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCIndexRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexrequest.html) |
|
||||
| response | [SCIndexResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scindexresponse.html) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCMultiSearchRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchrequest) |
|
||||
| response | [SCMultiSearchResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchresponse) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCMultiSearchRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchrequest) |
|
||||
| response | [SCMultiSearchResponse](https://openstapps.gitlab.io/core/modules/_index.d_.html#scmultisearchresponse) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCPluginRegisterRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scpluginregisterrequest) |
|
||||
| response | [SCPluginRegisterResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scpluginregisterresponse.html) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCPluginRegisterRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scpluginregisterrequest) |
|
||||
| response | [SCPluginRegisterResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scpluginregisterresponse.html) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCSearchRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchrequest.html) |
|
||||
| response | [SCSearchResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchresponse.html) |
|
||||
| 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) |
|
||||
| parameter | value |
|
||||
|--------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCSearchRequest](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchrequest.html) |
|
||||
| response | [SCSearchResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scsearchresponse.html) |
|
||||
| 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) |
|
||||
|
||||
|
||||
## `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
|
||||
|
||||
| parameter | value |
|
||||
| --- | --- |
|
||||
| request | [SCThingUpdateRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scthingupdaterequest) |
|
||||
| response | [SCThingUpdateResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scthingupdateresponse.html) |
|
||||
| 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) |
|
||||
| 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> |
|
||||
| parameter | value |
|
||||
|-----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| request | [SCThingUpdateRequest](https://openstapps.gitlab.io/core/modules/_index.d_.html#scthingupdaterequest) |
|
||||
| response | [SCThingUpdateResponse](https://openstapps.gitlab.io/core/interfaces/_index.d_.scthingupdateresponse.html) |
|
||||
| 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) |
|
||||
| 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
1311
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
39
package.json
39
package.json
@@ -12,10 +12,10 @@
|
||||
"Michel Jonathan Schmitz",
|
||||
"Rainer Killinger <mail-openstapps@killinger.co>",
|
||||
"Sebastian Lange",
|
||||
"Wieland Schöbl"
|
||||
"Thea Schöbl <dev@theaninova.de>"
|
||||
],
|
||||
"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'",
|
||||
"check-configuration": "openstapps-configuration",
|
||||
"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-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",
|
||||
"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": {
|
||||
"@elastic/elasticsearch": "5.6.22",
|
||||
"@openstapps/core": "0.66.1",
|
||||
"@openstapps/core-tools": "0.30.1",
|
||||
"@openstapps/core": "0.68.0",
|
||||
"@openstapps/core-tools": "0.31.0",
|
||||
"@openstapps/logger": "0.8.1",
|
||||
"@types/express-prometheus-middleware": "1.2.1",
|
||||
"@types/node": "14.18.18",
|
||||
"@types/node": "14.18.21",
|
||||
"config": "3.3.7",
|
||||
"cors": "2.8.5",
|
||||
"express": "4.18.1",
|
||||
"express-prometheus-middleware": "1.2.0",
|
||||
"express-promise-router": "4.1.1",
|
||||
"got": "11.8.3",
|
||||
"got": "11.8.5",
|
||||
"moment": "2.29.3",
|
||||
"morgan": "1.10.0",
|
||||
"nock": "13.2.4",
|
||||
"nock": "13.2.7",
|
||||
"node-cache": "5.1.2",
|
||||
"node-cron": "3.0.0",
|
||||
"node-cron": "3.0.1",
|
||||
"nodemailer": "6.7.5",
|
||||
"prom-client": "14.0.1",
|
||||
"promise-queue": "2.2.5",
|
||||
"ts-node": "10.8.0",
|
||||
"ts-node": "10.8.1",
|
||||
"uuid": "8.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openstapps/configuration": "0.29.1",
|
||||
"@openstapps/es-mapping-generator": "0.1.0",
|
||||
"@openstapps/configuration": "0.32.0",
|
||||
"@openstapps/es-mapping-generator": "0.2.0",
|
||||
"@openstapps/eslint-config": "1.1.0",
|
||||
"@testdeck/mocha": "0.2.0",
|
||||
"@types/chai": "4.3.1",
|
||||
"@types/chai-as-promised": "7.1.5",
|
||||
@@ -74,21 +76,28 @@
|
||||
"@types/sinon-express-mock": "1.3.9",
|
||||
"@types/supertest": "2.0.12",
|
||||
"@types/uuid": "8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
||||
"@typescript-eslint/parser": "5.29.0",
|
||||
"chai": "4.3.6",
|
||||
"chai-as-promised": "7.1.1",
|
||||
"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",
|
||||
"mocha": "10.0.0",
|
||||
"mocked-env": "1.3.5",
|
||||
"nyc": "15.1.0",
|
||||
"prepend-file-cli": "1.0.6",
|
||||
"redoc-cli": "0.13.14",
|
||||
"prettier": "2.7.1",
|
||||
"redoc-cli": "0.13.16",
|
||||
"rimraf": "3.0.2",
|
||||
"sinon": "14.0.0",
|
||||
"sinon-express-mock": "2.2.1",
|
||||
"supertest": "6.2.3",
|
||||
"tslint": "6.1.3",
|
||||
"typedoc": "0.22.15",
|
||||
"typedoc": "0.22.17",
|
||||
"typescript": "4.4.4"
|
||||
},
|
||||
"nyc": {
|
||||
|
||||
110
src/app.ts
110
src/app.ts
@@ -24,7 +24,7 @@ import config from 'config';
|
||||
import cors from 'cors';
|
||||
import {Express} from 'express';
|
||||
import morgan from 'morgan';
|
||||
import {join} from 'path';
|
||||
import path from 'path';
|
||||
import {configFile, DEFAULT_TIMEOUT, isTestEnvironment, mailer, plugins, validator} from './common';
|
||||
import {getPrometheusMiddleware} from './middleware/prometheus';
|
||||
import {MailQueue} from './notification/mail-queue';
|
||||
@@ -43,26 +43,26 @@ import {DatabaseConstructor} from './storage/database';
|
||||
/**
|
||||
* 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;
|
||||
// request loggers have to be the first middleware to be set in express
|
||||
app.use(morgan('dev', {
|
||||
skip: (_req, res) => {
|
||||
if (process.env.NODE_ENV === 'integration-test') {
|
||||
clearTimeout(integrationTestTimeout);
|
||||
integrationTestTimeout = setTimeout(() => {
|
||||
process.exit(1);
|
||||
},
|
||||
DEFAULT_TIMEOUT);
|
||||
app.use(
|
||||
morgan('dev', {
|
||||
skip: (_request, response) => {
|
||||
if (process.env.NODE_ENV === 'integration-test') {
|
||||
clearTimeout(integrationTestTimeout);
|
||||
integrationTestTimeout = setTimeout(() => {
|
||||
process.exit(1);
|
||||
}, DEFAULT_TIMEOUT);
|
||||
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// tslint:disable-next-line: no-magic-numbers
|
||||
return res.statusCode < 400;
|
||||
|
||||
}, stream: process.stdout,
|
||||
}));
|
||||
return response.statusCode < 400;
|
||||
},
|
||||
stream: process.stdout,
|
||||
}),
|
||||
);
|
||||
|
||||
if (process.env.PROMETHEUS_MIDDLEWARE === 'true') {
|
||||
app.use(getPrometheusMiddleware());
|
||||
@@ -80,7 +80,7 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
|
||||
'X-StApps-Version',
|
||||
],
|
||||
credentials: true,
|
||||
maxAge: 1728000,
|
||||
maxAge: 1_728_000,
|
||||
methods: ['GET', 'POST', 'PUT', 'OPTIONS'],
|
||||
optionsSuccessStatus: 204,
|
||||
};
|
||||
@@ -93,13 +93,13 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
|
||||
app.options('*', [cors(corsOptions)]);
|
||||
|
||||
// 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
|
||||
if (req.is('application/json') !== 'application/json') {
|
||||
if (request.is('application/json') !== 'application/json') {
|
||||
// return an error in the response
|
||||
const err = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment);
|
||||
res.status(err.statusCode);
|
||||
res.json(err);
|
||||
const error = new SCUnsupportedMediaTypeErrorResponse(isTestEnvironment);
|
||||
response.status(error.statusCode);
|
||||
response.json(error);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -111,12 +111,12 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
|
||||
bodySize += chunk.byteLength;
|
||||
// when adding each chunk size to the total size, check how large it now is.
|
||||
if (bodySize > configFile.backend.maxRequestBodySize) {
|
||||
req.off('data', chunkGatherer);
|
||||
req.off('end', endCallback);
|
||||
request.off('data', chunkGatherer);
|
||||
request.off('end', endCallback);
|
||||
// return an error in the response
|
||||
const err = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment);
|
||||
res.status(err.statusCode);
|
||||
res.json(err);
|
||||
const error = new SCRequestBodyTooLargeErrorResponse(isTestEnvironment);
|
||||
response.status(error.statusCode);
|
||||
response.json(error);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -125,26 +125,24 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
|
||||
};
|
||||
|
||||
const endCallback = () => {
|
||||
req.body = Buffer.concat(bodyBuffer)
|
||||
.toString();
|
||||
request.body = Buffer.concat(bodyBuffer).toString();
|
||||
|
||||
try {
|
||||
req.body = JSON.parse(req.body);
|
||||
request.body = JSON.parse(request.body);
|
||||
next();
|
||||
} catch (catchErr) {
|
||||
const err = new SCSyntaxErrorResponse(catchErr.message, isTestEnvironment);
|
||||
res.status(err.statusCode);
|
||||
res.json(err);
|
||||
} catch (error) {
|
||||
const error_ = new SCSyntaxErrorResponse(error.message, isTestEnvironment);
|
||||
response.status(error_.statusCode);
|
||||
response.json(error_);
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
req.on('data', chunkGatherer)
|
||||
.on('end', endCallback);
|
||||
request.on('data', chunkGatherer).on('end', endCallback);
|
||||
});
|
||||
|
||||
// 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
|
||||
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');
|
||||
}
|
||||
|
||||
const database =
|
||||
new databases[config.get<string>('internal.database.name')](
|
||||
configFile,
|
||||
// mailQueue
|
||||
typeof mailer !== 'undefined' && config.has('internal.monitoring') ? new MailQueue(mailer) : undefined,
|
||||
);
|
||||
const database = new databases[config.get<string>('internal.database.name')](
|
||||
configFile,
|
||||
// mailQueue
|
||||
typeof mailer !== 'undefined' && config.has('internal.monitoring') ? new MailQueue(mailer) : undefined,
|
||||
);
|
||||
|
||||
await database.init();
|
||||
|
||||
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');
|
||||
@@ -181,10 +178,7 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat
|
||||
app.enable('strict routing');
|
||||
|
||||
// make the bulk storage available to all http middlewares/routes
|
||||
app.set(
|
||||
'bulk',
|
||||
new BulkStorage(database),
|
||||
);
|
||||
app.set('bulk', new BulkStorage(database));
|
||||
|
||||
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)
|
||||
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 (plugins.has(req.originalUrl)) {
|
||||
if (plugins.has(request.originalUrl)) {
|
||||
try {
|
||||
res.json(await virtualPluginRoute(req, plugins.get(req.originalUrl)!));
|
||||
} catch (e) {
|
||||
response.json(await virtualPluginRoute(request, plugins.get(request.originalUrl)!));
|
||||
} catch (error) {
|
||||
// in case of error send an error response
|
||||
res.status(e.statusCode);
|
||||
res.json(e);
|
||||
response.status(error.statusCode);
|
||||
response.json(error);
|
||||
}
|
||||
} else {
|
||||
// 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)
|
||||
app.use((_req, res) => {
|
||||
app.use((_request, response) => {
|
||||
const errorResponse = new SCNotFoundErrorResponse(isTestEnvironment);
|
||||
res.status(errorResponse.statusCode);
|
||||
res.json(errorResponse);
|
||||
response.status(errorResponse.statusCode);
|
||||
response.json(errorResponse);
|
||||
});
|
||||
}
|
||||
|
||||
23
src/cli.ts
23
src/cli.ts
@@ -24,7 +24,6 @@ const app = express();
|
||||
/**
|
||||
* Get port from environment and store in Express.
|
||||
*/
|
||||
// tslint:disable-next-line: strict-boolean-expressions
|
||||
const port = normalizePort(process.env.PORT || '3000');
|
||||
|
||||
/**
|
||||
@@ -42,9 +41,9 @@ server.on('listening', onListening);
|
||||
* Normalize a port into a number, string, or false.
|
||||
*/
|
||||
function normalizePort(value: string) {
|
||||
const portNumber = parseInt(value, 10);
|
||||
const portNumber = Number.parseInt(value, 10);
|
||||
|
||||
if (isNaN(portNumber)) {
|
||||
if (Number.isNaN(portNumber)) {
|
||||
// named pipe
|
||||
return value;
|
||||
}
|
||||
@@ -60,15 +59,12 @@ function normalizePort(value: string) {
|
||||
/**
|
||||
* 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') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const bind = typeof port === 'string'
|
||||
? `Pipe ${port}`
|
||||
: `Port ${port}`;
|
||||
const bind = typeof port === 'string' ? `Pipe ${port}` : `Port ${port}`;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
@@ -92,13 +88,10 @@ function onListening() {
|
||||
const addr = server.address();
|
||||
|
||||
if (addr !== null) {
|
||||
const bind = typeof addr === 'string'
|
||||
? `pipe ${addr}`
|
||||
: `port ${addr.port}`;
|
||||
const bind = typeof addr === 'string' ? `pipe ${addr}` : `port ${addr.port}`;
|
||||
Logger.ok(`Listening on ${bind}`);
|
||||
} else {
|
||||
// tslint:disable-next-line: no-floating-promises
|
||||
Logger.error(`Failed to start binding`);
|
||||
void 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
|
||||
server.listen(port);
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err;
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
@@ -51,4 +51,4 @@ export const coreVersion: string = configFile.backend.SCVersion;
|
||||
/**
|
||||
* The default timeout in milliseconds
|
||||
*/
|
||||
export const DEFAULT_TIMEOUT = 20000;
|
||||
export const DEFAULT_TIMEOUT = 20_000;
|
||||
|
||||
@@ -24,9 +24,9 @@ type UserOptions = Parameters<typeof expressPrometheusMiddleware>[0];
|
||||
* Create and configure a new Express Prometheus Middleware instance
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
*
|
||||
* @returns express.Express
|
||||
*/
|
||||
export function getPrometheusMiddleware(): express.Express {
|
||||
@@ -34,9 +34,9 @@ export function getPrometheusMiddleware(): express.Express {
|
||||
let options: UserOptions = {};
|
||||
|
||||
try {
|
||||
options = JSON.parse(fs.readFileSync(configFileName, 'utf-8'));
|
||||
} catch(err) {
|
||||
Logger.warn('Could not get options for Prometheus Middleware.', err);
|
||||
options = JSON.parse(fs.readFileSync(configFileName, 'utf8'));
|
||||
} catch (error) {
|
||||
Logger.warn('Could not get options for Prometheus Middleware.', error);
|
||||
}
|
||||
|
||||
return expressPrometheusMiddleware(options);
|
||||
|
||||
@@ -103,8 +103,8 @@ export class BackendTransport {
|
||||
if (successful) {
|
||||
Logger.log('SMTP verification successful.');
|
||||
}
|
||||
} catch (err) {
|
||||
throw err;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
} finally {
|
||||
this.waitingForVerification = false;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import Queue from 'promise-queue';
|
||||
* A queue that can send mails in serial
|
||||
*/
|
||||
export class MailQueue {
|
||||
|
||||
/**
|
||||
* Number of allowed verification attempts after which the initialization of transport fails
|
||||
*/
|
||||
@@ -52,10 +51,10 @@ export class MailQueue {
|
||||
|
||||
/**
|
||||
* Creates a mail queue
|
||||
*
|
||||
* @param transport Transport which is used for sending mails
|
||||
*/
|
||||
constructor(private readonly transport: SMTP) {
|
||||
|
||||
this.queue = new Queue(1);
|
||||
|
||||
// this queue saves all request when the transport is not ready yet
|
||||
@@ -80,7 +79,6 @@ export class MailQueue {
|
||||
* Verify the given transport
|
||||
*/
|
||||
private checkForVerification() {
|
||||
|
||||
if (this.verificationCounter >= MailQueue.MAX_VERIFICATION_ATTEMPTS) {
|
||||
throw new Error('Failed to initialize the SMTP transport for the mail queue');
|
||||
}
|
||||
@@ -94,9 +92,9 @@ export class MailQueue {
|
||||
} else {
|
||||
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
|
||||
this.dryQueue.forEach(async (mail) => {
|
||||
await this.addToQueue(mail);
|
||||
});
|
||||
for (const mail of this.dryQueue) {
|
||||
void this.addToQueue(mail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,7 +104,8 @@ export class MailQueue {
|
||||
* @param mail Information required for sending a mail
|
||||
*/
|
||||
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
|
||||
this.dryQueue.push(mail);
|
||||
} else {
|
||||
|
||||
@@ -29,13 +29,12 @@ const bulkRouteModel = new SCBulkAddRoute();
|
||||
*/
|
||||
export const bulkAddRouter = createRoute<SCBulkAddRequest, SCBulkAddResponse>(
|
||||
bulkRouteModel,
|
||||
async (request, app, params) => {
|
||||
|
||||
async (request, app, parameters) => {
|
||||
const bulkMemory: BulkStorage = app.get('bulk');
|
||||
const bulk = bulkMemory.read(params.UID);
|
||||
const bulk = bulkMemory.read(parameters.UID);
|
||||
|
||||
if (typeof bulk === 'undefined') {
|
||||
Logger.warn(`Bulk with ${params.UID} not found.`);
|
||||
Logger.warn(`Bulk with ${parameters.UID} not found.`);
|
||||
throw new SCNotFoundErrorResponse(isTestEnvironment);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
* 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/>.
|
||||
*/
|
||||
import {SCBulkDoneRequest, SCBulkDoneResponse, SCBulkDoneRoute, SCNotFoundErrorResponse} from '@openstapps/core';
|
||||
import {
|
||||
SCBulkDoneRequest,
|
||||
SCBulkDoneResponse,
|
||||
SCBulkDoneRoute,
|
||||
SCNotFoundErrorResponse,
|
||||
} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {isTestEnvironment} from '../common';
|
||||
import {BulkStorage} from '../storage/bulk-storage';
|
||||
@@ -29,13 +34,12 @@ const bulkDoneRouteModel = new SCBulkDoneRoute();
|
||||
*/
|
||||
export const bulkDoneRouter = createRoute<SCBulkDoneRequest, SCBulkDoneResponse>(
|
||||
bulkDoneRouteModel,
|
||||
async (_request, app, params) => {
|
||||
|
||||
async (_request, app, parameters) => {
|
||||
const bulkMemory: BulkStorage = app.get('bulk');
|
||||
const bulk = bulkMemory.read(params.UID);
|
||||
const bulk = bulkMemory.read(parameters.UID);
|
||||
|
||||
if (typeof bulk === 'undefined') {
|
||||
Logger.warn(`Bulk with ${params.UID} not found.`);
|
||||
Logger.warn(`Bulk with ${parameters.UID} not found.`);
|
||||
throw new SCNotFoundErrorResponse(isTestEnvironment);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,11 +25,8 @@ const bulkRouteModel = new SCBulkRoute();
|
||||
/**
|
||||
* Implementation of the bulk request route (SCBulkRoute)
|
||||
*/
|
||||
export const bulkRouter = createRoute<SCBulkRequest, SCBulkResponse>(
|
||||
bulkRouteModel,
|
||||
async (request, app) => {
|
||||
const bulkMemory: BulkStorage = app.get('bulk');
|
||||
export const bulkRouter = createRoute<SCBulkRequest, SCBulkResponse>(bulkRouteModel, async (request, app) => {
|
||||
const bulkMemory: BulkStorage = app.get('bulk');
|
||||
|
||||
return bulkMemory.create(request);
|
||||
},
|
||||
);
|
||||
return bulkMemory.create(request);
|
||||
});
|
||||
|
||||
@@ -14,10 +14,31 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
// the list provides option to easily implement "isHttpMethod" guard
|
||||
const httpVerbs = ['get', 'post', 'put', 'delete', 'patch', 'options',
|
||||
'head', 'checkout', 'copy', 'lock', 'merge', 'mkactivity', 'mkcol',
|
||||
'move', 'm-search', 'notify', 'purge', 'report', 'search', 'subscribe',
|
||||
'trace', 'unlock','unsubscribe'] as const;
|
||||
const httpVerbs = [
|
||||
'get',
|
||||
'post',
|
||||
'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.
|
||||
*/
|
||||
@@ -29,5 +50,5 @@ export type HTTPVerb = typeof httpVerbs[number];
|
||||
* @param method A text (representing a method) to check
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -31,30 +31,28 @@ const multiSearchRouteModel = new SCMultiSearchRoute();
|
||||
/**
|
||||
* Implementation of the multi search route (SCMultiSearchRoute)
|
||||
*/
|
||||
export const multiSearchRouter = createRoute
|
||||
<SCMultiSearchRequest, SCMultiSearchResponse | SCTooManyRequestsErrorResponse>(
|
||||
multiSearchRouteModel,
|
||||
async (request, app) => {
|
||||
export const multiSearchRouter = createRoute<
|
||||
SCMultiSearchRequest,
|
||||
SCMultiSearchResponse | SCTooManyRequestsErrorResponse
|
||||
>(multiSearchRouteModel, async (request, app) => {
|
||||
const bulkMemory: BulkStorage = app.get('bulk');
|
||||
const queryNames = Object.keys(request);
|
||||
|
||||
const bulkMemory: BulkStorage = app.get('bulk');
|
||||
const queryNames = Object.keys(request);
|
||||
if (queryNames.length > configFile.backend.maxMultiSearchRouteQueries) {
|
||||
throw new SCTooManyRequestsErrorResponse(isTestEnvironment);
|
||||
}
|
||||
|
||||
if (queryNames.length > configFile.backend.maxMultiSearchRouteQueries) {
|
||||
throw new SCTooManyRequestsErrorResponse(isTestEnvironment);
|
||||
}
|
||||
// get a map of promises for each query
|
||||
const searchRequests = queryNames.map(async queryName => {
|
||||
return bulkMemory.database.search(request[queryName]);
|
||||
});
|
||||
|
||||
// get a map of promises for each query
|
||||
const searchRequests = queryNames.map(async (queryName) => {
|
||||
return bulkMemory.database.search(request[queryName]);
|
||||
});
|
||||
const listOfSearchResponses = await Promise.all(searchRequests);
|
||||
|
||||
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; } = {};
|
||||
queryNames.forEach((queryName, index) => {
|
||||
response[queryName] = listOfSearchResponses[index];
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
);
|
||||
return response;
|
||||
});
|
||||
|
||||
@@ -34,8 +34,7 @@ const pluginRegisterRouteModel = new SCPluginRegisterRoute();
|
||||
/**
|
||||
* Implementation of the plugin registration route (SCPluginRegisterRoute)
|
||||
*/
|
||||
export const pluginRegisterRouter = createRoute(
|
||||
pluginRegisterRouteModel, pluginRegisterHandler);
|
||||
export const pluginRegisterRouter = createRoute(pluginRegisterRouteModel, pluginRegisterHandler);
|
||||
|
||||
/**
|
||||
* 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 _app Express application
|
||||
*/
|
||||
export async function pluginRegisterHandler(request: SCPluginRegisterRequest, _app: Express.Application):
|
||||
Promise<SCPluginRegisterResponse> {
|
||||
export async function pluginRegisterHandler(
|
||||
request: SCPluginRegisterRequest,
|
||||
_app: Express.Application,
|
||||
): Promise<SCPluginRegisterResponse> {
|
||||
switch (request.action) {
|
||||
case 'add':
|
||||
return addPlugin(request.plugin);
|
||||
@@ -66,7 +67,7 @@ function addPlugin(plugin: SCPluginMetaData): SCPluginRegisterResponse {
|
||||
deepStrictEqual(previouslyRegistered, plugin);
|
||||
|
||||
return {success: true};
|
||||
} catch (error) {
|
||||
} catch {
|
||||
throw new SCPluginAlreadyRegisteredErrorResponse(
|
||||
'Plugin already registered',
|
||||
plugins.get(plugin.route)!,
|
||||
@@ -80,8 +81,10 @@ function addPlugin(plugin: SCPluginMetaData): SCPluginRegisterResponse {
|
||||
if (typeof configFile.app.features.plugins === 'undefined') {
|
||||
configFile.app.features.plugins = {};
|
||||
}
|
||||
configFile.app.features.plugins[plugin.name] = {urlPath : plugin.route};
|
||||
Logger.log(`Registered plugin (name: ${plugin.name}, address: ${plugin.address}) on the route "${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}".`,
|
||||
);
|
||||
|
||||
return {success: true};
|
||||
}
|
||||
@@ -93,9 +96,7 @@ function addPlugin(plugin: SCPluginMetaData): SCPluginRegisterResponse {
|
||||
*/
|
||||
function removePlugin(route: string): SCPluginRegisterResponse {
|
||||
if (!plugins.has(route)) {
|
||||
throw new SCNotFoundErrorResponse(
|
||||
isTestEnvironment,
|
||||
);
|
||||
throw new SCNotFoundErrorResponse(isTestEnvironment);
|
||||
}
|
||||
if (plugins.has(route)) {
|
||||
const plugin = plugins.get(route)!;
|
||||
|
||||
@@ -39,7 +39,8 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
|
||||
routeClass: SCRoute,
|
||||
handler: (
|
||||
validatedBody: REQUESTTYPE,
|
||||
app: Application, params: { [parameterName: string]: string; },
|
||||
app: Application,
|
||||
parameters: {[parameterName: string]: string},
|
||||
) => Promise<RETURNTYPE>,
|
||||
): Router {
|
||||
// create router
|
||||
@@ -54,81 +55,73 @@ export function createRoute<REQUESTTYPE, RETURNTYPE>(
|
||||
// check if route has a valid http verb
|
||||
if (isHttpMethod(verb)) {
|
||||
// create a route handler for the given HTTP method
|
||||
route[verb](async (req, res) => {
|
||||
|
||||
route[verb](async (request, response) => {
|
||||
try {
|
||||
// validate request
|
||||
const requestValidation = validator.validate(req.body, routeClass.requestBodyName);
|
||||
const requestValidation = validator.validate(request.body, routeClass.requestBodyName);
|
||||
|
||||
if (requestValidation.errors.length > 0) {
|
||||
const error = new SCValidationErrorResponse(
|
||||
requestValidation.errors,
|
||||
isTestEnvironment,
|
||||
);
|
||||
res.status(error.statusCode);
|
||||
res.json(error);
|
||||
const error = new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
|
||||
response.status(error.statusCode);
|
||||
response.json(error);
|
||||
await Logger.error(error);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
const responseErrors: ValidationError[] = validator.validate(response, routeClass.responseBodyName).errors;
|
||||
const responseErrors: ValidationError[] = validator.validate(
|
||||
handlerResponse,
|
||||
routeClass.responseBodyName,
|
||||
).errors;
|
||||
|
||||
if (responseErrors.length > 0) {
|
||||
const validationError = new SCValidationErrorResponse(
|
||||
responseErrors,
|
||||
isTestEnvironment,
|
||||
);
|
||||
const validationError = new SCValidationErrorResponse(responseErrors, isTestEnvironment);
|
||||
// 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.
|
||||
const internalServerError = new SCInternalServerErrorResponse(
|
||||
validationError,
|
||||
isTestEnvironment,
|
||||
);
|
||||
res.status(internalServerError.statusCode);
|
||||
res.json(internalServerError);
|
||||
const internalServerError = new SCInternalServerErrorResponse(validationError, isTestEnvironment);
|
||||
response.status(internalServerError.statusCode);
|
||||
response.json(internalServerError);
|
||||
await Logger.error(internalServerError);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// set status code
|
||||
res.status(routeClass.statusCodeSuccess);
|
||||
response.status(routeClass.statusCodeSuccess);
|
||||
|
||||
// respond
|
||||
res.json(response);
|
||||
response.json(handlerResponse);
|
||||
} catch (error) {
|
||||
// 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
|
||||
res.status(error.statusCode);
|
||||
res.json(error);
|
||||
response.status(error.statusCode);
|
||||
response.json(error);
|
||||
await Logger.error(error);
|
||||
} else {
|
||||
// the error is not allowed so something went wrong
|
||||
const internalServerError = new SCInternalServerErrorResponse(
|
||||
error,
|
||||
isTestEnvironment,
|
||||
);
|
||||
res.status(internalServerError.statusCode);
|
||||
res.json(internalServerError);
|
||||
const internalServerError = new SCInternalServerErrorResponse(error, isTestEnvironment);
|
||||
response.status(internalServerError.statusCode);
|
||||
response.json(internalServerError);
|
||||
await Logger.error(error);
|
||||
}
|
||||
}
|
||||
});
|
||||
} 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
|
||||
route.all((_req, res) => {
|
||||
route.all((_request, response) => {
|
||||
const error = new SCMethodNotAllowedErrorResponse(isTestEnvironment);
|
||||
res.status(error.statusCode);
|
||||
res.json(error);
|
||||
response.status(error.statusCode);
|
||||
response.json(error);
|
||||
Logger.warn(error);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
SCInternalServerErrorResponse,
|
||||
SCPluginMetaData,
|
||||
SCValidationErrorResponse,
|
||||
} from '@openstapps/core';
|
||||
import {SCInternalServerErrorResponse, SCPluginMetaData, SCValidationErrorResponse} from '@openstapps/core';
|
||||
import {Request} from 'express';
|
||||
import got from 'got';
|
||||
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
|
||||
*
|
||||
* @param req The request for a plugin resource
|
||||
* @param request The request for a plugin resource
|
||||
* @param plugin Meta data of the plugin
|
||||
* @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;
|
||||
try {
|
||||
const requestValidation = validator.validate(req.body, plugin.requestSchema);
|
||||
const requestValidation = validator.validate(request.body, plugin.requestSchema);
|
||||
if (requestValidation.errors.length > 0) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new SCValidationErrorResponse(requestValidation.errors, isTestEnvironment);
|
||||
}
|
||||
// send the request to the plugin (forward the body) and save the response
|
||||
const pluginResponse = await got.post(
|
||||
plugin.route.replace(/^\//gi, ''),
|
||||
{
|
||||
prefixUrl: plugin.address,
|
||||
json: req.body,
|
||||
timeout: configFile.backend.externalRequestTimeout,
|
||||
responseType: 'json',
|
||||
},
|
||||
);
|
||||
const pluginResponse = await got.post(plugin.route.replace(/^\//gi, ''), {
|
||||
prefixUrl: plugin.address,
|
||||
json: request.body,
|
||||
timeout: configFile.backend.externalRequestTimeout,
|
||||
responseType: 'json',
|
||||
});
|
||||
responseBody = pluginResponse.body as object;
|
||||
const responseValidation = validator.validate(responseBody, plugin.responseSchema);
|
||||
if (responseValidation.errors.length > 0) {
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new SCValidationErrorResponse(responseValidation.errors, isTestEnvironment);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
// wrap exact error inside of the internal server error response
|
||||
throw new SCInternalServerErrorResponse(e, isTestEnvironment);
|
||||
throw new SCInternalServerErrorResponse(error, isTestEnvironment);
|
||||
}
|
||||
|
||||
return responseBody;
|
||||
|
||||
@@ -29,7 +29,6 @@ export type BulkOperation = 'create' | 'expired' | 'update';
|
||||
* Describes an indexing process
|
||||
*/
|
||||
export class Bulk implements SCBulkRequest {
|
||||
|
||||
/**
|
||||
* Expiration of the bulk
|
||||
*
|
||||
@@ -70,19 +69,15 @@ export class Bulk implements SCBulkRequest {
|
||||
|
||||
/**
|
||||
* Creates a new bulk process
|
||||
*
|
||||
* @param request Data needed for requesting a bulk
|
||||
*/
|
||||
constructor(request: SCBulkRequest) {
|
||||
this.uid = v4();
|
||||
this.state = 'in progress';
|
||||
|
||||
if (typeof request.expiration === 'string') {
|
||||
this.expiration = request.expiration;
|
||||
} else {
|
||||
this.expiration = moment()
|
||||
.add(1, 'hour')
|
||||
.toISOString();
|
||||
}
|
||||
this.expiration =
|
||||
typeof request.expiration === 'string' ? request.expiration : moment().add(1, 'hour').toISOString();
|
||||
// when should this process be finished
|
||||
// where does the process come from
|
||||
this.source = request.source;
|
||||
@@ -102,10 +97,10 @@ export class BulkStorage {
|
||||
|
||||
/**
|
||||
* Creates a new BulkStorage
|
||||
*
|
||||
* @param database the database that is controlled by this bulk storage
|
||||
*/
|
||||
constructor(public database: Database) {
|
||||
|
||||
// a bulk lives 60 minutes if no expiration is given
|
||||
// the cache is checked every 60 seconds
|
||||
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)
|
||||
*
|
||||
* @param bulk the bulk process to save
|
||||
* @returns the bulk process that was saved
|
||||
*/
|
||||
private save(bulk: Bulk): Bulk {
|
||||
const expirationInSeconds = moment(bulk.expiration)
|
||||
// tslint:disable-next-line: no-magic-numbers
|
||||
.diff(moment.now()) / 1000;
|
||||
const expirationInSeconds = moment(bulk.expiration).diff(moment.now()) / 1000;
|
||||
Logger.info('Bulk expires in ', expirationInSeconds, 'seconds');
|
||||
|
||||
// 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
|
||||
*
|
||||
* @param bulkRequest a request for a new bulk process
|
||||
* @returns a promise that contains the new bulk process
|
||||
*/
|
||||
@@ -156,6 +151,7 @@ export class BulkStorage {
|
||||
|
||||
/**
|
||||
* Delete a bulk process
|
||||
*
|
||||
* @param uid uid of the bulk process
|
||||
* @returns a promise that contains the deleted bulk process
|
||||
*/
|
||||
@@ -163,7 +159,7 @@ export class BulkStorage {
|
||||
const bulk = this.read(uid);
|
||||
|
||||
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
|
||||
@@ -177,6 +173,7 @@ export class BulkStorage {
|
||||
|
||||
/**
|
||||
* Update an old bulk process (replace it with the new one)
|
||||
*
|
||||
* @param bulk new bulk process
|
||||
* @returns an empty promise
|
||||
*/
|
||||
@@ -192,11 +189,11 @@ export class BulkStorage {
|
||||
|
||||
/**
|
||||
* Read an existing bulk process
|
||||
*
|
||||
* @param uid uid of the bulk process
|
||||
* @returns a promise that contains a bulk
|
||||
*/
|
||||
public read(uid: string): Bulk | undefined {
|
||||
return this.cache.get(uid);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ export type DatabaseConstructor = new (config: SCConfigFile, mailQueue?: MailQue
|
||||
* Defines what one database class needs to have defined
|
||||
*/
|
||||
export interface Database {
|
||||
|
||||
/**
|
||||
* Gets called if a bulk was created
|
||||
*
|
||||
* The database should
|
||||
*
|
||||
* @param bulk A bulk to be created
|
||||
*/
|
||||
bulkCreated(bulk: Bulk): Promise<void>;
|
||||
@@ -39,6 +39,7 @@ export interface Database {
|
||||
* Gets called if a bulk expires
|
||||
*
|
||||
* The database should delete all data that is associtated with this bulk
|
||||
*
|
||||
* @param bulk A bulk which data needs to be removed
|
||||
*/
|
||||
bulkExpired(bulk: Bulk): Promise<void>;
|
||||
@@ -55,6 +56,7 @@ export interface Database {
|
||||
|
||||
/**
|
||||
* Get a single document
|
||||
*
|
||||
* @param uid Unique identifier of the document
|
||||
*/
|
||||
get(uid: SCUuid): Promise<SCThings>;
|
||||
@@ -66,6 +68,7 @@ export interface Database {
|
||||
|
||||
/**
|
||||
* Add a thing to an existing bulk
|
||||
*
|
||||
* @param thing A StAppsCore thing to be added
|
||||
* @param bulk A bulk to which the thing should be added
|
||||
*/
|
||||
@@ -82,7 +85,8 @@ export interface Database {
|
||||
|
||||
/**
|
||||
* Search for things
|
||||
*
|
||||
* @param params Parameters which form a search query to search the backend data
|
||||
*/
|
||||
search(params: SCSearchQuery): Promise<SCSearchResponse>;
|
||||
search(parameters: SCSearchQuery): Promise<SCSearchResponse>;
|
||||
}
|
||||
|
||||
@@ -26,10 +26,10 @@ import {
|
||||
|
||||
/**
|
||||
* Parses elasticsearch aggregations (response from es) to facets for the app
|
||||
*
|
||||
* @param aggregationResponse - aggregations response from elasticsearch
|
||||
*/
|
||||
export function parseAggregations(aggregationResponse: AggregationResponse): SCFacet[] {
|
||||
|
||||
const facets: SCFacet[] = [];
|
||||
|
||||
// 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...
|
||||
if (isESTermsFilter(field) && isBucketAggregation(realField) && realField.buckets.length > 0) {
|
||||
const facet: SCFacet = {
|
||||
buckets: realField.buckets.map((bucket) => {
|
||||
buckets: realField.buckets.map(bucket => {
|
||||
return {
|
||||
count: bucket.doc_count,
|
||||
key: bucket.key,
|
||||
@@ -71,7 +71,7 @@ export function parseAggregations(aggregationResponse: AggregationResponse): SCF
|
||||
// the last part here means that it is a bucket aggregation
|
||||
} else if (isESTermsFilter(type) && !isNestedAggregation(realType) && realType.buckets.length > 0) {
|
||||
facets.push({
|
||||
buckets: realType.buckets.map((bucket) => {
|
||||
buckets: realType.buckets.map(bucket => {
|
||||
return {
|
||||
count: bucket.doc_count,
|
||||
key: bucket.key,
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import {Logger} from '@openstapps/logger';
|
||||
// we only have the @types package because some things type definitions are still missing from the official
|
||||
// @elastic/elasticsearch package
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import {IndicesUpdateAliasesParamsAction, SearchResponse} from 'elasticsearch';
|
||||
import moment from 'moment';
|
||||
import {MailQueue} from '../../notification/mail-queue';
|
||||
@@ -39,7 +38,8 @@ import {buildQuery, buildSort} from './query';
|
||||
import {aggregations, putTemplate} from './templating';
|
||||
import {
|
||||
AggregationResponse,
|
||||
ElasticsearchConfig, ElasticsearchObject,
|
||||
ElasticsearchConfig,
|
||||
ElasticsearchObject,
|
||||
ElasticsearchQueryDisMaxConfig,
|
||||
ElasticsearchQueryQueryStringConfig,
|
||||
} from './types/elasticsearch';
|
||||
@@ -53,7 +53,6 @@ const indexRegex = /^stapps_([A-z0-9_]+)_([a-z0-9-_]+)_([-a-z0-9^_]+)$/;
|
||||
* A database interface for elasticsearch
|
||||
*/
|
||||
export class Elasticsearch implements Database {
|
||||
|
||||
/**
|
||||
* Length of the index UID used for generation of its name
|
||||
*/
|
||||
@@ -90,7 +89,7 @@ export class Elasticsearch implements Database {
|
||||
*/
|
||||
static getElasticsearchUrl(): string {
|
||||
// check if we have a docker link
|
||||
if (process.env.ES_ADDR !== undefined ) {
|
||||
if (process.env.ES_ADDR !== undefined) {
|
||||
return process.env.ES_ADDR;
|
||||
}
|
||||
|
||||
@@ -100,6 +99,7 @@ export class Elasticsearch implements Database {
|
||||
|
||||
/**
|
||||
* Gets the index name in elasticsearch for one SCThingType
|
||||
*
|
||||
* @param type SCThingType of data in the index
|
||||
* @param source source of data in the 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
|
||||
*
|
||||
* @param uid Bulk UID
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param alias The alias name
|
||||
* @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
|
||||
if (formattedAlias.includes(' ')) {
|
||||
formattedAlias = formattedAlias.trim();
|
||||
formattedAlias = formattedAlias.split(' ')
|
||||
.join('_');
|
||||
formattedAlias = formattedAlias.split(' ').join('_');
|
||||
}
|
||||
// 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)) {
|
||||
formattedAlias = formattedAlias.replace(value, '');
|
||||
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.
|
||||
New alias name is "${formattedAlias}."`);
|
||||
}
|
||||
});
|
||||
['-', '_', '+'].forEach((value) => {
|
||||
}
|
||||
for (const value of ['-', '_', '+']) {
|
||||
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
|
||||
alias despite having different types, as invalid characters are removed automatically.
|
||||
New alias name is "${formattedAlias}."`);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (formattedAlias === '.' || formattedAlias === '..') {
|
||||
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.`);
|
||||
@@ -176,22 +177,24 @@ export class Elasticsearch implements Database {
|
||||
|
||||
/**
|
||||
* Create a new interface for elasticsearch
|
||||
*
|
||||
* @param config an assembled config file
|
||||
* @param mailQueue a mailqueue for monitoring
|
||||
*/
|
||||
constructor(private readonly config: SCConfigFile, mailQueue?: MailQueue) {
|
||||
|
||||
if (typeof config.internal.database === 'undefined'
|
||||
|| typeof config.internal.database.version !== 'string') {
|
||||
throw new Error('Database version is undefined. Check your config file');
|
||||
if (
|
||||
typeof config.internal.database === 'undefined' ||
|
||||
typeof config.internal.database.version !== 'string'
|
||||
) {
|
||||
throw new TypeError('Database version is undefined. Check your config file');
|
||||
}
|
||||
|
||||
this.client = new Client({
|
||||
node: Elasticsearch.getElasticsearchUrl(),
|
||||
});
|
||||
this.client.on(events.REQUEST, async (err: Error | null, result: ApiResponse<unknown>) => {
|
||||
if (err !== null) {
|
||||
await Logger.error(err);
|
||||
this.client.on(events.REQUEST, async (error: Error | null, result: ApiResponse<unknown>) => {
|
||||
if (error !== null) {
|
||||
await Logger.error(error);
|
||||
}
|
||||
if (process.env.ES_DEBUG === 'true') {
|
||||
Logger.log(result);
|
||||
@@ -215,18 +218,20 @@ export class Elasticsearch implements Database {
|
||||
// create a list of old indices that are not in use
|
||||
const oldIndicesToDelete: string[] = [];
|
||||
|
||||
let aliases: {
|
||||
[index: string]: {
|
||||
/**
|
||||
* Aliases of an index
|
||||
*/
|
||||
aliases: {
|
||||
[K in SCThingType]: unknown
|
||||
};
|
||||
};
|
||||
} | undefined;
|
||||
let aliases:
|
||||
| {
|
||||
[index: string]: {
|
||||
/**
|
||||
* Aliases of an index
|
||||
*/
|
||||
aliases: {
|
||||
[K in SCThingType]: unknown;
|
||||
};
|
||||
};
|
||||
}
|
||||
| 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') {
|
||||
break;
|
||||
}
|
||||
@@ -241,16 +246,14 @@ export class Elasticsearch implements Database {
|
||||
}
|
||||
|
||||
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) {
|
||||
if (aliases.hasOwnProperty(index)) {
|
||||
|
||||
const matches = indexRegex.exec(index);
|
||||
if (matches !== null) {
|
||||
const type = matches[1];
|
||||
// tslint:disable-next-line: no-magic-numbers
|
||||
const source = matches[2];
|
||||
|
||||
// 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`);
|
||||
}
|
||||
|
||||
// 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)}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an elasticsearch object using containing thing's UID
|
||||
*
|
||||
* @param uid an UID to use for the search
|
||||
* @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
|
||||
*
|
||||
* @param bulk the bulk process that was created
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param bulk the bulk process that is expired
|
||||
*/
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
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
|
||||
// eslint-disable-next-line unicorn/no-await-expression-member
|
||||
if (!(await this.client.indices.exists({index})).body) {
|
||||
// re-apply the index template before each new bulk operation
|
||||
await putTemplate(this.client, bulk.type);
|
||||
@@ -411,6 +419,7 @@ export class Elasticsearch implements Database {
|
||||
];
|
||||
|
||||
// remove our old index if it exists
|
||||
// noinspection SuspiciousTypeOfGuard
|
||||
if (typeof oldIndex === 'string') {
|
||||
actions.push({
|
||||
remove: {index: oldIndex, alias: alias},
|
||||
@@ -431,6 +440,7 @@ export class Elasticsearch implements Database {
|
||||
|
||||
// swap the index in our aliasMap
|
||||
this.aliasMap[alias][bulk.source] = index;
|
||||
// noinspection SuspiciousTypeOfGuard
|
||||
if (typeof oldIndex === 'string') {
|
||||
// delete the old index
|
||||
await this.client.indices.delete({index: oldIndex});
|
||||
@@ -441,13 +451,14 @@ export class Elasticsearch implements Database {
|
||||
|
||||
/**
|
||||
* Gets an SCThing from all indexed data
|
||||
*
|
||||
* @param uid uid of an SCThing
|
||||
*/
|
||||
public async get(uid: SCUuid): Promise<SCThings> {
|
||||
const object = await this.getObject(uid);
|
||||
|
||||
if (typeof object === 'undefined') {
|
||||
throw new Error('Item not found.');
|
||||
throw new TypeError('Item not found.');
|
||||
}
|
||||
|
||||
return object._source;
|
||||
@@ -461,7 +472,9 @@ export class Elasticsearch implements Database {
|
||||
|
||||
if (typeof monitoringConfiguration !== '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
|
||||
await Monitoring.setUp(monitoringConfiguration, this.client, this.mailQueue);
|
||||
@@ -472,56 +485,55 @@ export class Elasticsearch implements Database {
|
||||
|
||||
/**
|
||||
* Add an item to an index
|
||||
*
|
||||
* @param object the SCThing to add to the index
|
||||
* @param bulk the bulk process which item belongs to
|
||||
*/
|
||||
public async post(object: SCThings, bulk: Bulk): Promise<void> {
|
||||
|
||||
// tslint:disable-next-line: completed-docs
|
||||
const obj: SCThings & { creation_date: string; } = {
|
||||
const object_: SCThings & {creation_date: string} = {
|
||||
...object,
|
||||
creation_date: moment()
|
||||
.format(),
|
||||
creation_date: moment().format(),
|
||||
};
|
||||
|
||||
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)
|
||||
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;
|
||||
|
||||
// new item doesn't replace the old one
|
||||
// tslint:disable-next-line:no-magic-numbers
|
||||
if (oldIndex.substring(0, oldIndex.lastIndexOf('_'))
|
||||
!== indexOfNew.substring(0, indexOfNew.lastIndexOf('_'))) {
|
||||
if (
|
||||
oldIndex.slice(0, Math.max(0, oldIndex.lastIndexOf('_'))) !==
|
||||
indexOfNew.slice(0, Math.max(0, indexOfNew.lastIndexOf('_')))
|
||||
) {
|
||||
throw new Error(
|
||||
// tslint:disable-next-line: no-magic-numbers
|
||||
`Object "${obj.uid}" already exists. Object was: ${JSON.stringify(obj, null, 2)}`,
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
`Object "${object_.uid}" already exists. Object was: ${JSON.stringify(object_, null, 2)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// regular bulk update (item gets replaced when bulk is updated)
|
||||
const searchResponse = await this.client.create({
|
||||
body: obj,
|
||||
id: obj.uid,
|
||||
index: Elasticsearch.getIndex(obj.type, bulk.source, bulk),
|
||||
body: object_,
|
||||
id: object_.uid,
|
||||
index: Elasticsearch.getIndex(object_.type, bulk.source, bulk),
|
||||
timeout: '90s',
|
||||
type: obj.type,
|
||||
type: object_.type,
|
||||
});
|
||||
|
||||
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
|
||||
*
|
||||
* @param object SCThing to put
|
||||
*/
|
||||
public async put(object: SCThings): Promise<void> {
|
||||
|
||||
const item = await this.getObject(object.uid);
|
||||
|
||||
if (typeof item !== 'undefined') {
|
||||
@@ -542,12 +554,12 @@ export class Elasticsearch implements Database {
|
||||
|
||||
/**
|
||||
* 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') {
|
||||
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
|
||||
@@ -557,23 +569,23 @@ export class Elasticsearch implements Database {
|
||||
};
|
||||
|
||||
if (typeof this.config.internal.database.query !== 'undefined') {
|
||||
esConfig.query =
|
||||
this.config.internal.database
|
||||
.query as ElasticsearchQueryDisMaxConfig | ElasticsearchQueryQueryStringConfig;
|
||||
esConfig.query = this.config.internal.database.query as
|
||||
| ElasticsearchQueryDisMaxConfig
|
||||
| ElasticsearchQueryQueryStringConfig;
|
||||
}
|
||||
|
||||
const searchRequest: RequestParams.Search = {
|
||||
body: {
|
||||
aggs: aggregations,
|
||||
query: buildQuery(params, this.config, esConfig),
|
||||
query: buildQuery(parameters, this.config, esConfig),
|
||||
},
|
||||
from: params.from,
|
||||
from: parameters.from,
|
||||
index: Elasticsearch.getListOfAllIndices(),
|
||||
size: params.size,
|
||||
size: parameters.size,
|
||||
};
|
||||
|
||||
if (typeof params.sort !== 'undefined') {
|
||||
searchRequest.body.sort = buildSort(params.sort);
|
||||
if (typeof parameters.sort !== 'undefined') {
|
||||
searchRequest.body.sort = buildSort(parameters.sort);
|
||||
}
|
||||
|
||||
// perform the search against elasticsearch
|
||||
@@ -582,7 +594,7 @@ export class Elasticsearch implements Database {
|
||||
// gather pagination information
|
||||
const pagination = {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -593,7 +605,7 @@ export class Elasticsearch implements Database {
|
||||
|
||||
// we only directly return the _source documents
|
||||
// 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
|
||||
});
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ import {
|
||||
import {Logger} from '@openstapps/logger';
|
||||
// we only have the @types package because some things type definitions are still missing from the official
|
||||
// @elastic/elasticsearch package
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import {SearchResponse} from 'elasticsearch';
|
||||
import cron from 'node-cron';
|
||||
import {MailQueue} from '../../notification/mail-queue';
|
||||
|
||||
/**
|
||||
* Check if the given condition fails on the given number of results and the condition
|
||||
*
|
||||
* @param condition condition
|
||||
* @param total number of results
|
||||
*/
|
||||
@@ -48,6 +48,7 @@ function conditionFails(
|
||||
|
||||
/**
|
||||
* Check if the min condition fails
|
||||
*
|
||||
* @param minimumLength Minimal length allowed
|
||||
* @param total Number of results
|
||||
*/
|
||||
@@ -57,6 +58,7 @@ function minConditionFails(minimumLength: number, total: number) {
|
||||
|
||||
/**
|
||||
* Check if the max condition fails
|
||||
*
|
||||
* @param maximumLength Maximal length allowed
|
||||
* @param total Number of results
|
||||
*/
|
||||
@@ -66,6 +68,7 @@ function maxConditionFails(maximumLength: number, total: number) {
|
||||
|
||||
/**
|
||||
* Run all the given actions
|
||||
*
|
||||
* @param actions actions to perform
|
||||
* @param watcherName name of watcher that wants to perform them
|
||||
* @param triggerName name of trigger that triggered the watcher
|
||||
@@ -79,38 +82,39 @@ function runActions(
|
||||
total: number,
|
||||
mailQueue: MailQueue,
|
||||
) {
|
||||
|
||||
actions.forEach(async (action) => {
|
||||
if (action.type === 'log') {
|
||||
await Logger.error(
|
||||
action.prefix,
|
||||
`Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'`, `Found ${total} hits instead`,
|
||||
action.message,
|
||||
);
|
||||
} else {
|
||||
await mailQueue.push({
|
||||
subject: action.subject,
|
||||
text: `Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'
|
||||
for (const action of actions) {
|
||||
void (action.type === 'log'
|
||||
? Logger.error(
|
||||
action.prefix,
|
||||
`Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'`,
|
||||
`Found ${total} hits instead`,
|
||||
action.message,
|
||||
)
|
||||
: mailQueue.push({
|
||||
subject: action.subject,
|
||||
text: `Watcher '${watcherName}' failed. Watcher was triggered by '${triggerName}'
|
||||
${action.message} Found ${total} hits instead`,
|
||||
to: action.recipients,
|
||||
});
|
||||
}
|
||||
});
|
||||
to: action.recipients,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up the triggers for the configured watchers
|
||||
*
|
||||
* @param monitoringConfig configuration of the monitoring
|
||||
* @param esClient elasticsearch client
|
||||
* @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
|
||||
monitoringConfig.watchers.forEach((watcher) => {
|
||||
|
||||
for (const watcher of monitoringConfig.watchers) {
|
||||
// make a schedule for each trigger
|
||||
watcher.triggers.forEach((trigger) => {
|
||||
for (const trigger of watcher.triggers) {
|
||||
switch (trigger.executionTime) {
|
||||
case 'hourly':
|
||||
trigger.executionTime = '5 * * * *';
|
||||
@@ -127,21 +131,21 @@ export async function setUp(monitoringConfig: SCMonitoringConfiguration, esClien
|
||||
|
||||
cron.schedule(trigger.executionTime, async () => {
|
||||
// execute watch (search->condition->action)
|
||||
const result: ApiResponse<SearchResponse<SCThings>> =
|
||||
await esClient.search(watcher.query as RequestParams.Search);
|
||||
const result: ApiResponse<SearchResponse<SCThings>> = await esClient.search(
|
||||
watcher.query as RequestParams.Search,
|
||||
);
|
||||
|
||||
// check conditions
|
||||
const total = result.body.hits.total;
|
||||
|
||||
watcher.conditions.forEach((condition) => {
|
||||
for (const condition of watcher.conditions) {
|
||||
if (conditionFails(condition, total)) {
|
||||
runActions(watcher.actions, watcher.name, trigger.name, total, mailQueue);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Logger.log(`Scheduled ${monitoringConfig.watchers.length} watches`);
|
||||
}
|
||||
|
||||
@@ -34,7 +34,8 @@ import {
|
||||
ESFunctionScoreQuery,
|
||||
ESFunctionScoreQueryFunction,
|
||||
ESGenericRange,
|
||||
ESGenericSort, ESGeoBoundingBoxFilter,
|
||||
ESGenericSort,
|
||||
ESGeoBoundingBoxFilter,
|
||||
ESGeoDistanceFilter,
|
||||
ESGeoDistanceFilterArguments,
|
||||
ESGeoDistanceSort,
|
||||
@@ -55,19 +56,19 @@ import {
|
||||
* 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
|
||||
*
|
||||
* @param str the string to escape the characters from
|
||||
* @param string_ the string to escape the characters from
|
||||
*/
|
||||
function escapeESReservedCharacters(str: string): string {
|
||||
return str.replace(/[+\-=!(){}\[\]^"~*?:\\/]|(&&)|(\|\|)/g, '\\$&');
|
||||
function escapeESReservedCharacters(string_: string): string {
|
||||
return string_.replace(/[+\-=!(){}\[\]^"~*?:\\/]|(&&)|(\|\|)/g, '\\$&');
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a boolean filter. Returns an elasticsearch boolean filter
|
||||
*
|
||||
* @param booleanFilter a search boolean filter for the retrieval of the data
|
||||
* @returns elasticsearch boolean arguments object
|
||||
*/
|
||||
export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBooleanFilterArguments<unknown> {
|
||||
|
||||
const result: ESBooleanFilterArguments<unknown> = {
|
||||
minimum_should_match: 0,
|
||||
must: [],
|
||||
@@ -76,16 +77,16 @@ export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBool
|
||||
};
|
||||
|
||||
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') {
|
||||
result.should = booleanFilter.arguments.filters.map((filter) => buildFilter(filter));
|
||||
result.should = booleanFilter.arguments.filters.map(filter => buildFilter(filter));
|
||||
result.minimum_should_match = 1;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -93,22 +94,31 @@ export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBool
|
||||
|
||||
/**
|
||||
* Converts Array of Filters to elasticsearch query-syntax
|
||||
*
|
||||
* @param filter A search filter for the retrieval of the data
|
||||
*/
|
||||
export function buildFilter(filter: SCSearchFilter):
|
||||
ESTermFilter | ESGeoDistanceFilter | ESBooleanFilter<ESGeoShapeFilter | ESGeoBoundingBoxFilter> | ESGeoShapeFilter | ESBooleanFilter<unknown> | ESRangeFilter {
|
||||
|
||||
export function buildFilter(
|
||||
filter: SCSearchFilter,
|
||||
):
|
||||
| ESTermFilter
|
||||
| ESGeoDistanceFilter
|
||||
| ESBooleanFilter<ESGeoShapeFilter | ESGeoBoundingBoxFilter>
|
||||
| ESGeoShapeFilter
|
||||
| ESBooleanFilter<unknown>
|
||||
| ESRangeFilter {
|
||||
switch (filter.type) {
|
||||
case 'value':
|
||||
return Array.isArray(filter.arguments.value) ? {
|
||||
terms: {
|
||||
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
||||
},
|
||||
} : {
|
||||
term: {
|
||||
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
||||
},
|
||||
};
|
||||
return Array.isArray(filter.arguments.value)
|
||||
? {
|
||||
terms: {
|
||||
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
||||
},
|
||||
}
|
||||
: {
|
||||
term: {
|
||||
[`${filter.arguments.field}.raw`]: filter.arguments.value,
|
||||
},
|
||||
};
|
||||
case 'availability':
|
||||
const scope = filter.arguments.scope?.charAt(0) ?? 's';
|
||||
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
|
||||
*/
|
||||
// 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.
|
||||
// @ts-expect-error unfortunately, typescript is stupid and won't allow me to map this to an actual type.
|
||||
ignore_unmapped: true,
|
||||
[`${filter.arguments.field}.polygon`]: {
|
||||
shape: filter.arguments.shape,
|
||||
@@ -195,8 +204,10 @@ export function buildFilter(filter: SCSearchFilter):
|
||||
},
|
||||
};
|
||||
|
||||
if ((typeof filter.arguments.spatialRelation === 'undefined' || filter.arguments.spatialRelation === 'intersects')
|
||||
&& filter.arguments.shape.type === 'envelope'
|
||||
if (
|
||||
(typeof filter.arguments.spatialRelation === 'undefined' ||
|
||||
filter.arguments.spatialRelation === 'intersects') &&
|
||||
filter.arguments.shape.type === 'envelope'
|
||||
) {
|
||||
return {
|
||||
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
|
||||
*/
|
||||
// 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,
|
||||
[`${filter.arguments.field}.point.coordinates`]: {
|
||||
top_left: filter.arguments.shape.coordinates[0],
|
||||
@@ -228,6 +237,7 @@ export function buildFilter(filter: SCSearchFilter):
|
||||
|
||||
/**
|
||||
* Builds scoring functions from boosting config
|
||||
*
|
||||
* @param boostings Backend boosting configuration for contexts and types
|
||||
* @param context The context of the app from where the search was initiated
|
||||
*/
|
||||
@@ -235,14 +245,14 @@ function buildFunctions(
|
||||
boostings: SCBackendConfigurationSearchBoostingContext,
|
||||
context: SCSearchContext | undefined,
|
||||
): ESFunctionScoreQueryFunction[] {
|
||||
|
||||
// default context
|
||||
let functions: ESFunctionScoreQueryFunction[] =
|
||||
buildFunctionsForBoostingTypes(boostings['default' as SCSearchContext]);
|
||||
let functions: ESFunctionScoreQueryFunction[] = buildFunctionsForBoostingTypes(
|
||||
boostings['default' as SCSearchContext],
|
||||
);
|
||||
|
||||
if (typeof context !== 'undefined' && context !== 'default') {
|
||||
// specific context provided, extend default context with additional boosts
|
||||
functions = functions.concat(buildFunctionsForBoostingTypes(boostings[context]));
|
||||
functions = [...functions, ...buildFunctionsForBoostingTypes(boostings[context])];
|
||||
}
|
||||
|
||||
return functions;
|
||||
@@ -258,7 +268,7 @@ function buildFunctionsForBoostingTypes(
|
||||
): ESFunctionScoreQueryFunction[] {
|
||||
const functions: ESFunctionScoreQueryFunction[] = [];
|
||||
|
||||
boostingTypes.forEach((boostingForOneSCType) => {
|
||||
for (const boostingForOneSCType of boostingTypes) {
|
||||
const typeFilter: ESTypeFilter = {
|
||||
type: {
|
||||
value: boostingForOneSCType.type,
|
||||
@@ -271,7 +281,6 @@ function buildFunctionsForBoostingTypes(
|
||||
});
|
||||
|
||||
if (typeof boostingForOneSCType.fields !== 'undefined') {
|
||||
|
||||
const fields = boostingForOneSCType.fields;
|
||||
|
||||
for (const fieldName in boostingForOneSCType.fields) {
|
||||
@@ -291,10 +300,7 @@ function buildFunctionsForBoostingTypes(
|
||||
functions.push({
|
||||
filter: {
|
||||
bool: {
|
||||
must: [
|
||||
typeFilter,
|
||||
termFilter,
|
||||
],
|
||||
must: [typeFilter, termFilter],
|
||||
should: [],
|
||||
},
|
||||
},
|
||||
@@ -305,24 +311,24 @@ function buildFunctionsForBoostingTypes(
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return functions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 elasticsearchConfig Elasticsearch configuration
|
||||
* @returns ElasticsearchQuery (body of a search-request)
|
||||
*/
|
||||
export function buildQuery(
|
||||
params: SCSearchQuery,
|
||||
parameters: SCSearchQuery,
|
||||
defaultConfig: SCConfigFile,
|
||||
elasticsearchConfig: ElasticsearchConfig,
|
||||
): ESFunctionScoreQuery {
|
||||
|
||||
// if config provides an minMatch parameter we use query_string instead of match query
|
||||
let query;
|
||||
if (typeof elasticsearchConfig.query === 'undefined') {
|
||||
@@ -331,7 +337,7 @@ export function buildQuery(
|
||||
analyzer: 'search_german',
|
||||
default_field: 'name',
|
||||
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') {
|
||||
@@ -340,11 +346,11 @@ export function buildQuery(
|
||||
analyzer: 'search_german',
|
||||
default_field: 'name',
|
||||
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') {
|
||||
if (params.query !== '*') {
|
||||
if (parameters.query !== '*') {
|
||||
query = {
|
||||
dis_max: {
|
||||
boost: 1.2,
|
||||
@@ -355,7 +361,7 @@ export function buildQuery(
|
||||
boost: elasticsearchConfig.query.matchBoosting,
|
||||
cutoff_frequency: elasticsearchConfig.query.cutoffFrequency,
|
||||
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',
|
||||
default_field: 'name',
|
||||
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,
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
} 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 = {
|
||||
function_score: {
|
||||
functions: buildFunctions(defaultConfig.internal.boostings, params.context),
|
||||
functions: buildFunctions(defaultConfig.internal.boostings, parameters.context),
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 0, // if we have no should, nothing can match
|
||||
@@ -398,8 +406,8 @@ export function buildQuery(
|
||||
mustMatch.push(query);
|
||||
}
|
||||
|
||||
if (typeof params.filter !== 'undefined') {
|
||||
mustMatch.push(buildFilter(params.filter));
|
||||
if (typeof parameters.filter !== 'undefined') {
|
||||
mustMatch.push(buildFilter(parameters.filter));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,13 +416,12 @@ export function buildQuery(
|
||||
|
||||
/**
|
||||
* converts query to
|
||||
*
|
||||
* @param sorts Sorting rules to apply to the data that is being queried
|
||||
* @returns an array of sort queries
|
||||
*/
|
||||
export function buildSort(
|
||||
sorts: SCSearchSort[],
|
||||
): Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> {
|
||||
return sorts.map((sort) => {
|
||||
export function buildSort(sorts: SCSearchSort[]): Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> {
|
||||
return sorts.map(sort => {
|
||||
switch (sort.type) {
|
||||
case 'generic':
|
||||
const esGenericSort: ESGenericSort = {};
|
||||
@@ -427,26 +434,26 @@ export function buildSort(
|
||||
|
||||
return esDucetSort;
|
||||
case 'distance':
|
||||
const args: ESGeoDistanceSortArguments = {
|
||||
const arguments_: ESGeoDistanceSortArguments = {
|
||||
mode: 'avg',
|
||||
order: sort.order,
|
||||
unit: 'm',
|
||||
};
|
||||
|
||||
args[`${sort.arguments.field}.point.coordinates`] = {
|
||||
arguments_[`${sort.arguments.field}.point.coordinates`] = {
|
||||
lat: sort.arguments.position[1],
|
||||
lon: sort.arguments.position[0],
|
||||
};
|
||||
|
||||
return {
|
||||
_geo_distance: args,
|
||||
_geo_distance: arguments_,
|
||||
};
|
||||
case 'price':
|
||||
return {
|
||||
_script: {
|
||||
order: sort.order,
|
||||
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 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 `
|
||||
// initialize the sort value with the maximum
|
||||
double price = Double.MAX_VALUE;
|
||||
|
||||
@@ -15,18 +15,19 @@
|
||||
*/
|
||||
import {Client} from '@elastic/elasticsearch';
|
||||
import {SCThingType} from '@openstapps/core';
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
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 {readFileSync} from 'fs';
|
||||
import {resolve} from 'path';
|
||||
import path from 'path';
|
||||
|
||||
const mappingsPath = 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;
|
||||
const mappingsPath = path.resolve('node_modules', '@openstapps', 'core', 'lib', 'mappings');
|
||||
|
||||
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
|
||||
@@ -44,8 +45,8 @@ export async function refreshAllTemplates(client: Client) {
|
||||
*
|
||||
* 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 type the SCThingType of which the template should be set
|
||||
*/
|
||||
export async function putTemplate(client: Client, type: SCThingType) {
|
||||
const sanitizedType = `template_${type.replace(/\s/g, '_')}`;
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
*/
|
||||
import {SCThing, SCThingType} from '@openstapps/core';
|
||||
// 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';
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import {Polygon, Position} from 'geojson';
|
||||
|
||||
/**
|
||||
@@ -75,7 +73,6 @@ export interface NestedAggregation {
|
||||
[name: string]: BucketAggregation | number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#fuzziness
|
||||
*/
|
||||
fuzziness: number | string;
|
||||
|
||||
/**
|
||||
* Increase the importance (relevance score) of a field
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-boost.html
|
||||
*/
|
||||
matchBoosting: number;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
minMatch: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
queryType: 'dis_max';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
tieBreaker: number;
|
||||
@@ -128,12 +130,14 @@ export interface ElasticsearchQueryDisMaxConfig {
|
||||
export interface ElasticsearchQueryQueryStringConfig {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
minMatch: string;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
queryType: 'query_string';
|
||||
@@ -141,6 +145,7 @@ export interface ElasticsearchQueryQueryStringConfig {
|
||||
|
||||
/**
|
||||
* A hit in an elasticsearch search result
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-fields.html
|
||||
*/
|
||||
export interface ElasticsearchObject<T extends SCThing> {
|
||||
@@ -166,6 +171,7 @@ export interface ElasticsearchObject<T extends SCThing> {
|
||||
|
||||
/**
|
||||
* The document's mapping type
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-type-field.html
|
||||
*/
|
||||
_type: string;
|
||||
@@ -177,22 +183,25 @@ export interface ElasticsearchObject<T extends SCThing> {
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fields?: NameList;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// tslint:disable-next-line: no-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
highlight?: any;
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
// tslint:disable-next-line: no-any
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
inner_hits?: any;
|
||||
|
||||
/**
|
||||
@@ -246,21 +255,23 @@ export interface ElasticsearchConfig {
|
||||
/**
|
||||
* An elasticsearch term filter
|
||||
*/
|
||||
export type ESTermFilter = {
|
||||
/**
|
||||
* Definition of a term to match
|
||||
*/
|
||||
term: {
|
||||
[fieldName: string]: string;
|
||||
};
|
||||
} | {
|
||||
/**
|
||||
* Definition of terms to match (or)
|
||||
*/
|
||||
terms: {
|
||||
[fieldName: string]: string[];
|
||||
};
|
||||
};
|
||||
export type ESTermFilter =
|
||||
| {
|
||||
/**
|
||||
* Definition of a term to match
|
||||
*/
|
||||
term: {
|
||||
[fieldName: string]: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Definition of terms to match (or)
|
||||
*/
|
||||
terms: {
|
||||
[fieldName: string]: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export interface ESGenericRange<T> {
|
||||
/**
|
||||
@@ -318,7 +329,6 @@ export type ESNumericRangeFilter = ESGenericRangeFilter<number, ESGenericRange<n
|
||||
export type ESDateRangeFilter = ESGenericRangeFilter<string, ESDateRange>;
|
||||
export type ESRangeFilter = ESNumericRangeFilter | ESDateRangeFilter;
|
||||
|
||||
|
||||
/**
|
||||
* An elasticsearch type filter
|
||||
*/
|
||||
@@ -343,17 +353,19 @@ export interface ESGeoDistanceFilterArguments {
|
||||
*/
|
||||
distance: string;
|
||||
|
||||
[fieldName: string]: {
|
||||
/**
|
||||
* Latitude
|
||||
*/
|
||||
lat: number;
|
||||
[fieldName: string]:
|
||||
| {
|
||||
/**
|
||||
* Latitude
|
||||
*/
|
||||
lat: number;
|
||||
|
||||
/**
|
||||
* Longitude
|
||||
*/
|
||||
lon: number;
|
||||
} | string;
|
||||
/**
|
||||
* Longitude
|
||||
*/
|
||||
lon: number;
|
||||
}
|
||||
| string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,11 +398,13 @@ export interface ESEnvelope {
|
||||
|
||||
/**
|
||||
* An Elasticsearch geo bounding box filter
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html
|
||||
*/
|
||||
export interface ESGeoBoundingBoxFilter {
|
||||
/**
|
||||
* An Elasticsearch geo bounding box filter
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html
|
||||
*/
|
||||
geo_bounding_box: {
|
||||
@@ -410,6 +424,7 @@ export interface ESGeoBoundingBoxFilter {
|
||||
|
||||
/**
|
||||
* An Elasticsearch geo shape filter
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html
|
||||
*/
|
||||
export interface ESGeoShapeFilter {
|
||||
@@ -432,11 +447,13 @@ export interface ESGeoShapeFilter {
|
||||
|
||||
/**
|
||||
* Filter arguments for an elasticsearch boolean filter
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-bool-query.html
|
||||
*/
|
||||
export interface ESBooleanFilterArguments<T> {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
minimum_should_match?: number;
|
||||
@@ -469,6 +486,7 @@ export interface ESBooleanFilter<T> {
|
||||
|
||||
/**
|
||||
* An elasticsearch function score query
|
||||
*
|
||||
* @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-function-score-query.html
|
||||
*/
|
||||
export interface ESFunctionScoreQuery {
|
||||
@@ -478,6 +496,7 @@ export interface ESFunctionScoreQuery {
|
||||
function_score: {
|
||||
/**
|
||||
* Functions that compute score for query results (documents)
|
||||
*
|
||||
* @see ESFunctionScoreQueryFunction
|
||||
*/
|
||||
functions: ESFunctionScoreQueryFunction[];
|
||||
@@ -535,17 +554,19 @@ export interface ESGeoDistanceSortArguments {
|
||||
*/
|
||||
unit: 'm';
|
||||
|
||||
[field: string]: {
|
||||
/**
|
||||
* Latitude
|
||||
*/
|
||||
lat: number;
|
||||
[field: string]:
|
||||
| {
|
||||
/**
|
||||
* Latitude
|
||||
*/
|
||||
lat: number;
|
||||
|
||||
/**
|
||||
* Longitude
|
||||
*/
|
||||
lon: number;
|
||||
} | string;
|
||||
/**
|
||||
* Longitude
|
||||
*/
|
||||
lon: number;
|
||||
}
|
||||
| string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,12 +18,12 @@ import {
|
||||
ESAggTypeFilter,
|
||||
ESNestedAggregation,
|
||||
ESTermsFilter,
|
||||
// tslint:disable-next-line:no-implicit-dependencies we're just using the types here
|
||||
} from '@openstapps/es-mapping-generator/src/types/aggregation';
|
||||
import {BucketAggregation, NestedAggregation} from './elasticsearch';
|
||||
|
||||
/**
|
||||
* Checks if the type is a BucketAggregation
|
||||
*
|
||||
* @param agg the type to check
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param agg the type to check
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param agg the value to check
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @param agg the value to check
|
||||
*/
|
||||
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
|
||||
*/
|
||||
export function isESAggMatchAllFilter(filter: ESAggTypeFilter | ESAggMatchAllFilter): filter is ESAggMatchAllFilter {
|
||||
export function isESAggMatchAllFilter(
|
||||
filter: ESAggTypeFilter | ESAggMatchAllFilter,
|
||||
): filter is ESAggMatchAllFilter {
|
||||
return filter.hasOwnProperty('match_all');
|
||||
}
|
||||
|
||||
@@ -35,15 +35,14 @@ describe('App', async function () {
|
||||
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();
|
||||
let processExitStub = sandbox.stub(process, 'exit');
|
||||
const processExitStub = sandbox.stub(process, 'exit');
|
||||
// fake NODE_ENV as integration test
|
||||
const restore = mockedEnv({
|
||||
NODE_ENV: 'integration-test',
|
||||
});
|
||||
await testApp
|
||||
.post('/');
|
||||
await testApp.post('/');
|
||||
// fake timeout of default timeout
|
||||
clock.tick(DEFAULT_TIMEOUT);
|
||||
|
||||
@@ -67,21 +66,18 @@ describe('App', async function () {
|
||||
});
|
||||
|
||||
it('should implement CORS', async function () {
|
||||
// @ts-ignore
|
||||
const {headers} = await testApp
|
||||
.options('/');
|
||||
// @ts-expect error
|
||||
const {headers} = await testApp.options('/');
|
||||
|
||||
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 () {
|
||||
const responseNoType = await testApp
|
||||
.post('/non-existing-route')
|
||||
.send();
|
||||
const responseNoType = await testApp.post('/non-existing-route').send();
|
||||
|
||||
const responseOtherType = await testApp
|
||||
.post('/non-existing-route')
|
||||
.set('Content-Type','application/foo')
|
||||
.set('Content-Type', 'application/foo')
|
||||
.send();
|
||||
|
||||
expect(responseNoType.status).to.equal(new SCUnsupportedMediaTypeErrorResponse().statusCode);
|
||||
|
||||
@@ -19,14 +19,14 @@ import {expect} from 'chai';
|
||||
describe('Common', function () {
|
||||
describe('inRangeInclusive', function () {
|
||||
it('should provide true if the given number is in the range', function () {
|
||||
expect(inRangeInclusive(1, [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, 3])).to.be.true;
|
||||
expect(inRangeInclusive(2, [1, 3])).to.be.true;
|
||||
expect(inRangeInclusive(1.1, [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 () {
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,25 +42,25 @@ export async function startApp(): Promise<Express> {
|
||||
const port = await getPort();
|
||||
server.listen(port);
|
||||
|
||||
server.on('error', err => {
|
||||
throw err;
|
||||
server.on('error', error => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
return new Promise(resolve => server.on('listening', () => {
|
||||
app.set(
|
||||
'bulk',
|
||||
bulkStorageMock,
|
||||
);
|
||||
resolve(app);
|
||||
}));
|
||||
return new Promise(resolve =>
|
||||
server.on('listening', () => {
|
||||
app.set('bulk', bulkStorageMock);
|
||||
resolve(app);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* An elasticsearch mock
|
||||
*/
|
||||
export class ElasticsearchMock implements Database {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error never read
|
||||
private bulk: Bulk | undefined;
|
||||
|
||||
private storageMock = new Map<string, SCThings>();
|
||||
|
||||
constructor(_configFile: SCConfigFile, _mailQueue?: MailQueue) {
|
||||
@@ -81,7 +81,7 @@ export class ElasticsearchMock implements Database {
|
||||
}
|
||||
|
||||
get(uid: SCUuid): Promise<SCThings> {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error incompatible types
|
||||
return Promise.resolve(this.storageMock.get(uid));
|
||||
}
|
||||
|
||||
@@ -98,49 +98,54 @@ export class ElasticsearchMock implements Database {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
search(_params: SCSearchQuery): Promise<SCSearchResponse> {
|
||||
return Promise.resolve({data: [], facets: [], pagination: {count: 0, offset: 0, total: 0}, stats: {time: 0}});
|
||||
search(_parameters: SCSearchQuery): Promise<SCSearchResponse> {
|
||||
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 bulk: Bulk = {
|
||||
expiration: moment().add(3600, 'seconds')
|
||||
.format(),
|
||||
source: 'some_source',
|
||||
state: 'in progress',
|
||||
type: SCThingType.Book,
|
||||
uid: ''
|
||||
};
|
||||
expiration: moment().add(3600, 'seconds').format(),
|
||||
source: 'some_source',
|
||||
state: 'in progress',
|
||||
type: SCThingType.Book,
|
||||
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 getTransport = (verified: boolean) => {
|
||||
return {
|
||||
cc: undefined,
|
||||
from: undefined,
|
||||
recipients: undefined,
|
||||
transportAgent: undefined,
|
||||
verified: undefined,
|
||||
isVerified(): boolean {
|
||||
return verified;
|
||||
},
|
||||
send(_subject: string, _message: string): Promise<string> {
|
||||
return Promise.resolve('');
|
||||
},
|
||||
sendMail(_mail: any): Promise<string> {
|
||||
return Promise.resolve(TRANSPORT_SEND_RESPONSE);
|
||||
},
|
||||
verify(): Promise<boolean> {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
cc: undefined,
|
||||
from: undefined,
|
||||
recipients: undefined,
|
||||
transportAgent: undefined,
|
||||
verified: undefined,
|
||||
isVerified(): boolean {
|
||||
return verified;
|
||||
},
|
||||
send(_subject: string, _message: string): Promise<string> {
|
||||
return Promise.resolve('');
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
sendMail(_mail: any): Promise<string> {
|
||||
return Promise.resolve(TRANSPORT_SEND_RESPONSE);
|
||||
},
|
||||
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())}`;
|
||||
|
||||
@@ -22,7 +22,6 @@ import sinon from 'sinon';
|
||||
|
||||
describe('Backend transport', function () {
|
||||
describe('isTransportWithVerification', function () {
|
||||
|
||||
it('should return false if transport is not verifiable', function () {
|
||||
expect(isTransportWithVerification({} 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 () {
|
||||
// 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;
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ describe('Backend transport', function () {
|
||||
});
|
||||
|
||||
it('should provide only one instance of the transport', function () {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(SMTP, 'getInstance').callsFake(() => {
|
||||
return {};
|
||||
});
|
||||
@@ -77,8 +77,10 @@ describe('Backend transport', function () {
|
||||
});
|
||||
|
||||
it('should provide information that the transport if waiting for its verification', function () {
|
||||
// @ts-ignore
|
||||
sandbox.stub(SMTP, 'getInstance').callsFake(() => {return {verify: () => Promise.resolve(true)}});
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(SMTP, 'getInstance').callsFake(() => {
|
||||
return {verify: () => Promise.resolve(true)};
|
||||
});
|
||||
|
||||
expect(BackendTransport.getInstance().isWaitingForVerification()).to.be.true;
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* 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 () {
|
||||
const loggerStub = sandbox.spy(Logger, 'warn');
|
||||
const test = () => {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
new MailQueue(getTransport(false));
|
||||
// fake that VERIFICATION_TIMEOUT was reached more times (one more) than MAX_VERIFICATION_ATTEMPTS
|
||||
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 () {
|
||||
const queueAddStub = sandbox.stub(Queue.prototype, 'add');
|
||||
const numberOfMails = 3;
|
||||
let transport = getTransport(false);
|
||||
// @ts-ignore
|
||||
const transport = getTransport(false);
|
||||
// @ts-expect-error not assignable
|
||||
const mailQueue = new MailQueue(transport);
|
||||
const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'};
|
||||
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 () {
|
||||
const queueAddStub = sandbox.stub(Queue.prototype, 'add');
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
const mailQueue = new MailQueue(getTransport(false));
|
||||
const mail: MailOptions = {};
|
||||
mailQueue.push(mail);
|
||||
@@ -77,8 +78,8 @@ describe('MailQueue', async function () {
|
||||
|
||||
it('should add SMTP sending tasks to queue when transport is verified', function () {
|
||||
const queueAddStub = sandbox.stub(Queue.prototype, 'add');
|
||||
let transport = getTransport(false);
|
||||
// @ts-ignore
|
||||
const transport = getTransport(false);
|
||||
// @ts-expect-error not assignable
|
||||
const mailQueue = new MailQueue(transport);
|
||||
const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'};
|
||||
// fake that transport is verified
|
||||
@@ -90,11 +91,11 @@ describe('MailQueue', async function () {
|
||||
|
||||
it('should send SMTP mails when transport is verified', async function () {
|
||||
let caught: any;
|
||||
sandbox.stub(Queue.prototype, 'add').callsFake(async (promiseGenerator) => {
|
||||
sandbox.stub(Queue.prototype, 'add').callsFake(async promiseGenerator => {
|
||||
caught = await promiseGenerator();
|
||||
});
|
||||
let transport = getTransport(false);
|
||||
// @ts-ignore
|
||||
const transport = getTransport(false);
|
||||
// @ts-expect-error not assignable
|
||||
const mailQueue = new MailQueue(transport);
|
||||
const mail: MailOptions = {from: 'Foo', subject: 'Foo Subject'};
|
||||
// fake that transport is verified
|
||||
|
||||
@@ -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 () {
|
||||
await testApp
|
||||
.post(bulkRoute.urlPath)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(request);
|
||||
const bulkAddRouteurlPath = bulkAddRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid');
|
||||
await testApp.post(bulkRoute.urlPath).set('Content-Type', 'application/json').send(request);
|
||||
const bulkAddRouteUrlPath = bulkAddRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid');
|
||||
|
||||
const {status} = await testApp
|
||||
.post(bulkAddRouteurlPath)
|
||||
.post(bulkAddRouteUrlPath)
|
||||
.set('Content-Type', 'application/json')
|
||||
.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 () {
|
||||
await testApp
|
||||
.post(bulkRoute.urlPath)
|
||||
.set('Content-Type', 'application/json')
|
||||
.send(request);
|
||||
await testApp.post(bulkRoute.urlPath).set('Content-Type', 'application/json').send(request);
|
||||
const bulkDoneRouteurlPath = bulkDoneRoute.urlPath.toLocaleLowerCase().replace(':uid', 'a-wrong-uid');
|
||||
|
||||
const {status} = await testApp
|
||||
@@ -100,7 +94,7 @@ describe('Bulk routes', async function () {
|
||||
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
|
||||
.post(bulkRoute.urlPath)
|
||||
.set('Content-Type', 'application/json')
|
||||
|
||||
@@ -16,8 +16,11 @@
|
||||
import {
|
||||
SCNotFoundErrorResponse,
|
||||
SCPluginAdd,
|
||||
SCPluginAlreadyRegisteredErrorResponse, SCPluginRegisterResponse, SCPluginRegisterRoute,
|
||||
SCPluginRemove, SCValidationErrorResponse,
|
||||
SCPluginAlreadyRegisteredErrorResponse,
|
||||
SCPluginRegisterResponse,
|
||||
SCPluginRegisterRoute,
|
||||
SCPluginRemove,
|
||||
SCValidationErrorResponse,
|
||||
} from '@openstapps/core';
|
||||
import nock from 'nock';
|
||||
import {configFile, plugins} from '../../src/common';
|
||||
@@ -36,7 +39,7 @@ export const registerAddRequest: SCPluginAdd = registerRequest as SCPluginAdd;
|
||||
|
||||
export const registerRemoveRequest: SCPluginRemove = {
|
||||
action: 'remove',
|
||||
route: registerAddRequest.plugin.route
|
||||
route: registerAddRequest.plugin.route,
|
||||
};
|
||||
|
||||
describe('Plugin registration', async function () {
|
||||
@@ -52,9 +55,9 @@ describe('Plugin registration', async function () {
|
||||
// register one plugin
|
||||
const response = await pluginRegisterHandler(registerAddRequest, {});
|
||||
|
||||
expect(response).to.deep.equal(bodySuccess)
|
||||
&& expect(plugins.size).to.equal(1)
|
||||
&& expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty;
|
||||
expect(response).to.deep.equal(bodySuccess) &&
|
||||
expect(plugins.size).to.equal(1) &&
|
||||
expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty;
|
||||
});
|
||||
|
||||
it('should allow re-registering the same plugin', async function () {
|
||||
@@ -64,9 +67,11 @@ describe('Plugin registration', async function () {
|
||||
// register the same plugin again
|
||||
const response = await pluginRegisterHandler(registerAddRequest, {});
|
||||
|
||||
return expect(response).to.deep.equal(bodySuccess)
|
||||
&& expect(plugins.size).to.equal(1)
|
||||
&& expect(configFile.app.features.plugins!['Foo Plugin']).to.not.be.empty;
|
||||
return (
|
||||
expect(response).to.deep.equal(bodySuccess) &&
|
||||
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 () {
|
||||
@@ -74,9 +79,9 @@ describe('Plugin registration', async function () {
|
||||
await pluginRegisterHandler(registerAddRequest, {});
|
||||
|
||||
// create new request for adding a plugin with only name that changed
|
||||
let registerAddRequestAltered: SCPluginAdd = {
|
||||
const registerAddRequestAltered: SCPluginAdd = {
|
||||
...registerAddRequest,
|
||||
plugin: {...registerAddRequest.plugin, name: registerAddRequest.plugin.name + 'foo'}
|
||||
plugin: {...registerAddRequest.plugin, name: registerAddRequest.plugin.name + 'foo'},
|
||||
};
|
||||
|
||||
// register the same plugin again
|
||||
@@ -95,9 +100,9 @@ describe('Plugin registration', async function () {
|
||||
|
||||
const response = await pluginRegisterHandler(registerRemoveRequest, {});
|
||||
|
||||
expect(response).to.deep.equal(bodySuccess)
|
||||
&& expect(plugins.size).to.equal(0)
|
||||
&& expect(configFile.app.features.plugins).to.be.empty;
|
||||
expect(response).to.deep.equal(bodySuccess) &&
|
||||
expect(plugins.size).to.equal(0) &&
|
||||
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 () {
|
||||
@@ -116,10 +121,10 @@ describe('Plugin registration', async function () {
|
||||
const pluginRegisterRoute = new SCPluginRegisterRoute();
|
||||
const validationError = new SCValidationErrorResponse([]);
|
||||
const notFoundError = new SCNotFoundErrorResponse();
|
||||
//@ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
const alreadyRegisteredError = new SCPluginAlreadyRegisteredErrorResponse('Foo Message', {});
|
||||
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
// remove routes
|
||||
plugins.clear();
|
||||
// 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
|
||||
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
|
||||
// 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
|
||||
.post(pluginRegisterRoute.urlPath)
|
||||
@@ -169,7 +177,7 @@ describe('Plugin registration', async function () {
|
||||
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
|
||||
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
|
||||
|
||||
@@ -183,7 +191,7 @@ describe('Plugin registration', async function () {
|
||||
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
|
||||
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -17,7 +18,8 @@ import {
|
||||
SCInternalServerErrorResponse,
|
||||
SCMethodNotAllowedErrorResponse,
|
||||
SCRoute,
|
||||
SCRouteHttpVerbs, SCValidationErrorResponse,
|
||||
SCRouteHttpVerbs,
|
||||
SCValidationErrorResponse,
|
||||
} from '@openstapps/core';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import sinon from 'sinon';
|
||||
@@ -31,15 +33,16 @@ import {Logger} from '@openstapps/logger';
|
||||
import {DEFAULT_TEST_TIMEOUT} from '../common';
|
||||
|
||||
interface ReturnType {
|
||||
foo: boolean;
|
||||
foo: boolean;
|
||||
}
|
||||
|
||||
describe('Create route', async function () {
|
||||
let routeClass: SCRoute;
|
||||
let handler: (
|
||||
validatedBody: any,
|
||||
app: Application, params?: { [parameterName: string]: string; },
|
||||
) => Promise<ReturnType>;
|
||||
validatedBody: any,
|
||||
app: Application,
|
||||
parameters?: {[parameterName: string]: string},
|
||||
) => Promise<ReturnType>;
|
||||
let app: Express;
|
||||
const statusCodeSuccess = 222;
|
||||
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 () {
|
||||
const methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
|
||||
// @ts-ignore
|
||||
sandbox.stub(validator, 'validate').returns({errors: []})
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(validator, 'validate').returns({errors: []});
|
||||
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);
|
||||
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 () {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(validator, 'validate').returns({errors: []});
|
||||
const router = createRoute<any, any>(routeClass, handler);
|
||||
app.use(router);
|
||||
|
||||
const response = await supertest(app)
|
||||
.post(routeClass.urlPath)
|
||||
.send();
|
||||
const response = await supertest(app).post(routeClass.urlPath).send();
|
||||
|
||||
expect(response.status).to.be.equal(statusCodeSuccess);
|
||||
expect(response.body).to.be.deep.equal(bodySuccess);
|
||||
@@ -112,7 +115,7 @@ describe('Create route', async function () {
|
||||
app.use(router);
|
||||
const startApp = supertest(app);
|
||||
const validatorStub = sandbox.stub(validator, 'validate');
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
validatorStub.withArgs(body, routeClass.requestBodyName).returns({errors: [new Error('Foo Error')]});
|
||||
|
||||
const response = await startApp
|
||||
@@ -128,14 +131,14 @@ describe('Create route', async function () {
|
||||
const router = createRoute<any, any>(routeClass, handler);
|
||||
await app.use(router);
|
||||
const startApp = supertest(app);
|
||||
// @ts-ignore
|
||||
const validatorStub = sandbox.stub(validator, 'validate').returns({errors:[]});
|
||||
// @ts-ignore
|
||||
validatorStub.withArgs(bodySuccess, routeClass.responseBodyName).returns({errors: [new Error('Foo Error')]});
|
||||
// @ts-expect-error not assignable
|
||||
const validatorStub = sandbox.stub(validator, 'validate').returns({errors: []});
|
||||
validatorStub
|
||||
.withArgs(bodySuccess, routeClass.responseBodyName)
|
||||
// @ts-expect-error not assignable
|
||||
.returns({errors: [new Error('Foo Error')]});
|
||||
|
||||
const response = await startApp
|
||||
.post(routeClass.urlPath)
|
||||
.send();
|
||||
const response = await startApp.post(routeClass.urlPath).send();
|
||||
|
||||
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 () {
|
||||
class FooErrorResponse {
|
||||
statusCode: number;
|
||||
|
||||
name: string;
|
||||
|
||||
message: string;
|
||||
|
||||
constructor(statusCode: number, name: string, message: string) {
|
||||
this.statusCode = statusCode;
|
||||
this.name = name;
|
||||
@@ -153,6 +159,7 @@ describe('Create route', async function () {
|
||||
}
|
||||
class BarErrorResponse {
|
||||
statusCode: number;
|
||||
|
||||
constructor(statusCode: number) {
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
@@ -170,12 +177,10 @@ describe('Create route', async function () {
|
||||
await app.use(router);
|
||||
const startApp = supertest(app);
|
||||
|
||||
// @ts-ignore
|
||||
sandbox.stub(validator, 'validate').returns({errors:[]});
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(validator, 'validate').returns({errors: []});
|
||||
|
||||
const response = await startApp
|
||||
.post(routeClass.urlPath)
|
||||
.send();
|
||||
const response = await startApp.post(routeClass.urlPath).send();
|
||||
|
||||
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 () {
|
||||
class FooErrorResponse {
|
||||
statusCode: number;
|
||||
|
||||
name: string;
|
||||
|
||||
message: string;
|
||||
|
||||
constructor(statusCode: number, name: string, message: string) {
|
||||
this.statusCode = statusCode;
|
||||
this.name = name;
|
||||
@@ -205,12 +213,10 @@ describe('Create route', async function () {
|
||||
await app.use(router);
|
||||
const startApp = supertest(app);
|
||||
|
||||
// @ts-ignore
|
||||
sandbox.stub(validator, 'validate').returns({errors:[]});
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(validator, 'validate').returns({errors: []});
|
||||
|
||||
const response = await startApp
|
||||
.post(routeClass.urlPath)
|
||||
.send();
|
||||
const response = await startApp.post(routeClass.urlPath).send();
|
||||
|
||||
expect(response.status).to.be.equal(fooErrorResponse.statusCode);
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
SCMultiSearchRoute,
|
||||
SCSearchRoute,
|
||||
SCSyntaxErrorResponse,
|
||||
SCTooManyRequestsErrorResponse
|
||||
SCTooManyRequestsErrorResponse,
|
||||
} from '@openstapps/core';
|
||||
import {expect} from 'chai';
|
||||
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 () {
|
||||
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
|
||||
const {status} = await testApp
|
||||
.get('/search')
|
||||
.set('Accept', 'application/json')
|
||||
.send({
|
||||
query: 'Some search terms'
|
||||
});
|
||||
const {status} = await testApp.get('/search').set('Accept', 'application/json').send({
|
||||
query: 'Some search terms',
|
||||
});
|
||||
|
||||
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 () {
|
||||
const {status} = await testApp
|
||||
.post(searchRoute.urlPath)
|
||||
.set('Accept', 'application/json')
|
||||
.send({});
|
||||
const {status} = await testApp.post(searchRoute.urlPath).set('Accept', 'application/json').send({});
|
||||
|
||||
expect(status).to.equal(searchRoute.statusCodeSuccess);
|
||||
});
|
||||
|
||||
it('should accept valid search request', async function () {
|
||||
const {status} = await testApp
|
||||
.post(searchRoute.urlPath)
|
||||
.set('Accept', 'application/json')
|
||||
.send({
|
||||
query: 'Some search terms'
|
||||
});
|
||||
const {status} = await testApp.post(searchRoute.urlPath).set('Accept', 'application/json').send({
|
||||
query: 'Some search terms',
|
||||
});
|
||||
|
||||
expect(status).to.equal(searchRoute.statusCodeSuccess);
|
||||
});
|
||||
@@ -74,14 +65,14 @@ describe('Search route', async function () {
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
.send({
|
||||
some: {invalid: 'search'}
|
||||
some: {invalid: 'search'},
|
||||
});
|
||||
|
||||
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 () {
|
||||
const {status} = await testApp
|
||||
.post(multiSearchRoute.urlPath)
|
||||
@@ -96,8 +87,8 @@ describe('Search route', async function () {
|
||||
.post(multiSearchRoute.urlPath)
|
||||
.set('Accept', 'application/json')
|
||||
.send({
|
||||
one: { query: 'Some search terms for one search'},
|
||||
two: { query: 'Some search terms for another search'}
|
||||
one: {query: 'Some search terms for one search'},
|
||||
two: {query: 'Some search terms for another search'},
|
||||
});
|
||||
|
||||
expect(status).to.equal(multiSearchRoute.statusCodeSuccess);
|
||||
|
||||
@@ -28,7 +28,8 @@ describe('Thing update route', async function () {
|
||||
const thingUpdateRoute = new SCThingUpdateRoute();
|
||||
|
||||
it('should update a thing', async function () {
|
||||
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath.toLocaleLowerCase()
|
||||
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath
|
||||
.toLocaleLowerCase()
|
||||
.replace(':type', book.type)
|
||||
.replace(':uid', book.uid);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable unicorn/consistent-function-scoping,@typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* Copyright (C) 2019 StApps
|
||||
* 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
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import {
|
||||
SCInternalServerErrorResponse,
|
||||
SCPluginMetaData,
|
||||
SCValidationErrorResponse
|
||||
} from '@openstapps/core';
|
||||
import {SCInternalServerErrorResponse, SCPluginMetaData, SCValidationErrorResponse} from '@openstapps/core';
|
||||
import {expect, use} from 'chai';
|
||||
import chaiAsPromised from 'chai-as-promised';
|
||||
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
|
||||
*
|
||||
* @param req Express request
|
||||
* @param request Express request
|
||||
* @param plugin Plugin information (metadata)
|
||||
* @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();
|
||||
try {
|
||||
await virtualPluginRoute(req, plugin);
|
||||
} catch (e) {
|
||||
thrownError = e;
|
||||
await virtualPluginRoute(request, plugin);
|
||||
} catch (error) {
|
||||
thrownError = error;
|
||||
}
|
||||
// return virtualPluginRoute(req, plugin).should.be.rejectedWith(SCInternalServerErrorResponse); was not working for some reason
|
||||
expect(thrownError).to.be.instanceOf(SCInternalServerErrorResponse);
|
||||
@@ -71,17 +69,17 @@ describe('Virtual plugin routes', async function () {
|
||||
},
|
||||
};
|
||||
// spy the post method of got
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
const gotStub = sandbox.stub(got, 'post').returns({body: {}});
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
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).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 () {
|
||||
@@ -91,18 +89,13 @@ describe('Virtual plugin routes', async function () {
|
||||
},
|
||||
};
|
||||
const response = {
|
||||
result: [
|
||||
{foo: 'bar'},
|
||||
{bar: 'foo'},
|
||||
]
|
||||
}
|
||||
result: [{foo: 'bar'}, {bar: 'foo'}],
|
||||
};
|
||||
// mock response of the plugin's address
|
||||
nock('http://foo.com:1234')
|
||||
.post('/foo')
|
||||
.reply(200, response);
|
||||
const req = mockReq(request);
|
||||
nock('http://foo.com:1234').post('/foo').reply(200, response);
|
||||
const request_ = 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 () {
|
||||
@@ -111,9 +104,9 @@ describe('Virtual plugin routes', async function () {
|
||||
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 () {
|
||||
@@ -126,9 +119,9 @@ describe('Virtual plugin routes', async function () {
|
||||
nock('http://foo.com:1234')
|
||||
.post('/foo')
|
||||
.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 () {
|
||||
@@ -139,13 +132,12 @@ describe('Virtual plugin routes', async function () {
|
||||
};
|
||||
|
||||
// fake that post method of got throws an error
|
||||
sandbox.stub(got, 'post')
|
||||
.callsFake(() => {
|
||||
throw new FooError();
|
||||
});
|
||||
const req = mockReq(request);
|
||||
sandbox.stub(got, 'post').callsFake(() => {
|
||||
throw new FooError();
|
||||
});
|
||||
const request_ = 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 internalServerError = new SCInternalServerErrorResponse();
|
||||
|
||||
afterEach(async function() {
|
||||
afterEach(async function () {
|
||||
// remove routes
|
||||
plugins.clear();
|
||||
// // 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'}]});
|
||||
});
|
||||
|
||||
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
|
||||
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
|
||||
|
||||
class FooError extends Error {}
|
||||
// fake that got's post method throws an error
|
||||
sandbox.stub(got, 'post')
|
||||
.callsFake(() => {
|
||||
throw new FooError();
|
||||
});
|
||||
sandbox.stub(got, 'post').callsFake(() => {
|
||||
throw new FooError();
|
||||
});
|
||||
|
||||
const {status} = await testApp
|
||||
.post('/foo')
|
||||
@@ -214,7 +205,7 @@ describe('Virtual plugin routes', async function () {
|
||||
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
|
||||
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
|
||||
|
||||
@@ -223,13 +214,13 @@ describe('Virtual plugin routes', async function () {
|
||||
.set('Content-Type', 'application/json')
|
||||
.set('Accept', 'application/json')
|
||||
// using number for query instead of (in request schema) required text
|
||||
.send({query: 123})
|
||||
.send({query: 123});
|
||||
|
||||
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
|
||||
plugins.set(registerAddRequest.plugin.route, registerAddRequest.plugin);
|
||||
// mock response of the plugin address
|
||||
@@ -244,7 +235,7 @@ describe('Virtual plugin routes', async function () {
|
||||
.send({query: 'foo'});
|
||||
|
||||
expect(status).to.be.equal(internalServerError.statusCode);
|
||||
expect(body.additionalData).to.haveOwnProperty('name','ValidationError');
|
||||
expect(body.additionalData).to.haveOwnProperty('name', 'ValidationError');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
import {SCBulkRequest, SCThingType} from '@openstapps/core';
|
||||
import moment from 'moment';
|
||||
// eslint-disable-next-line unicorn/import-style
|
||||
import util from 'util';
|
||||
import {configFile} from '../../src/common';
|
||||
import {Bulk, BulkStorage} from '../../src/storage/bulk-storage';
|
||||
@@ -72,7 +73,7 @@ describe('Bulk Storage', function () {
|
||||
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';
|
||||
sandbox.stub(NodeCache.prototype, 'on').withArgs('expired', sinon.match.any).yields(123, bulk);
|
||||
new BulkStorage(database);
|
||||
@@ -88,11 +89,17 @@ describe('Bulk Storage', 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;
|
||||
sandbox.stub(NodeCache.prototype, 'del').callsFake(() => caught = 123);
|
||||
sandbox.stub(NodeCache.prototype, 'del').callsFake(() => (caught = 123));
|
||||
// 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);
|
||||
|
||||
await bulkStorage.delete(bulk.uid);
|
||||
@@ -103,15 +110,22 @@ describe('Bulk Storage', function () {
|
||||
});
|
||||
|
||||
it('should read an existing bulk', async function () {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let caught: any;
|
||||
sandbox.stub(NodeCache.prototype, 'get').callsFake(() => caught = 123);
|
||||
sandbox.stub(NodeCache.prototype, 'get').callsFake(() => (caught = 123));
|
||||
// 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);
|
||||
|
||||
await bulkStorage.read(bulk.uid);
|
||||
|
||||
expect(caught).to.be.equal(123);
|
||||
});``
|
||||
});
|
||||
``;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,23 +20,23 @@ import {AggregationResponse} from '../../../src/storage/elasticsearch/types/elas
|
||||
|
||||
describe('Aggregations', function () {
|
||||
const aggregations: AggregationResponse = {
|
||||
catalog: {
|
||||
doc_count: 4,
|
||||
'catalog': {
|
||||
'doc_count': 4,
|
||||
'superCatalogs.categories': {
|
||||
buckets: []
|
||||
buckets: [],
|
||||
},
|
||||
'academicTerm.acronym': {
|
||||
buckets: [
|
||||
{
|
||||
key: 'SoSe 2020',
|
||||
doc_count: 2
|
||||
}
|
||||
]
|
||||
doc_count: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
'superCatalog.categories': {
|
||||
buckets: []
|
||||
buckets: [],
|
||||
},
|
||||
categories: {
|
||||
'categories': {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foo',
|
||||
@@ -46,21 +46,21 @@ describe('Aggregations', function () {
|
||||
key: 'bar',
|
||||
doc_count: 3,
|
||||
},
|
||||
]
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
person: {
|
||||
doc_count: 13,
|
||||
'person': {
|
||||
'doc_count': 13,
|
||||
'homeLocations.categories': {
|
||||
buckets: []
|
||||
}
|
||||
buckets: [],
|
||||
},
|
||||
},
|
||||
'academic event': {
|
||||
doc_count: 0,
|
||||
'doc_count': 0,
|
||||
'academicTerms.acronym': {
|
||||
buckets: []
|
||||
buckets: [],
|
||||
},
|
||||
categories: {
|
||||
'categories': {
|
||||
buckets: [
|
||||
{
|
||||
key: 'foobar',
|
||||
@@ -70,18 +70,18 @@ describe('Aggregations', function () {
|
||||
key: 'bar',
|
||||
doc_count: 2,
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
'creativeWorks.keywords': {
|
||||
buckets: []
|
||||
}
|
||||
buckets: [],
|
||||
},
|
||||
},
|
||||
fooType: {
|
||||
'fooType': {
|
||||
buckets: [
|
||||
{
|
||||
doc_count: 321,
|
||||
key: 'foo'
|
||||
}
|
||||
key: 'foo',
|
||||
},
|
||||
],
|
||||
},
|
||||
'@all': {
|
||||
@@ -90,71 +90,71 @@ describe('Aggregations', function () {
|
||||
buckets: [
|
||||
{
|
||||
key: 'person',
|
||||
doc_count: 13
|
||||
doc_count: 13,
|
||||
},
|
||||
{
|
||||
key: 'catalog',
|
||||
doc_count: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
doc_count: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const expectedFacets: SCFacet[] = [
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 13,
|
||||
'key': 'person'
|
||||
},
|
||||
{
|
||||
count: 4,
|
||||
key: 'catalog'
|
||||
}
|
||||
],
|
||||
field: 'type',
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 8,
|
||||
key: 'foobar'
|
||||
},
|
||||
{
|
||||
count: 2,
|
||||
key: 'bar'
|
||||
}
|
||||
],
|
||||
field: 'categories',
|
||||
onlyOnType: SCThingType.AcademicEvent,
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 2,
|
||||
key: 'SoSe 2020'
|
||||
}
|
||||
],
|
||||
field: 'academicTerm.acronym',
|
||||
onlyOnType: SCThingType.Catalog
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 1,
|
||||
key: 'foo'
|
||||
},
|
||||
{
|
||||
count: 3,
|
||||
key: 'bar'
|
||||
}
|
||||
],
|
||||
field: 'categories',
|
||||
onlyOnType: SCThingType.Catalog,
|
||||
},
|
||||
// no fooType as it doesn't appear in the aggregation schema
|
||||
];
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 13,
|
||||
key: 'person',
|
||||
},
|
||||
{
|
||||
count: 4,
|
||||
key: 'catalog',
|
||||
},
|
||||
],
|
||||
field: 'type',
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 8,
|
||||
key: 'foobar',
|
||||
},
|
||||
{
|
||||
count: 2,
|
||||
key: 'bar',
|
||||
},
|
||||
],
|
||||
field: 'categories',
|
||||
onlyOnType: SCThingType.AcademicEvent,
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 2,
|
||||
key: 'SoSe 2020',
|
||||
},
|
||||
],
|
||||
field: 'academicTerm.acronym',
|
||||
onlyOnType: SCThingType.Catalog,
|
||||
},
|
||||
{
|
||||
buckets: [
|
||||
{
|
||||
count: 1,
|
||||
key: 'foo',
|
||||
},
|
||||
{
|
||||
count: 3,
|
||||
key: 'bar',
|
||||
},
|
||||
],
|
||||
field: 'categories',
|
||||
onlyOnType: SCThingType.Catalog,
|
||||
},
|
||||
// no fooType as it doesn't appear in the aggregation schema
|
||||
];
|
||||
|
||||
it('should parse the aggregations providing the appropriate facets', function () {
|
||||
const facets = parseAggregations(aggregations);
|
||||
|
||||
@@ -17,15 +17,15 @@ import {
|
||||
ESAggMatchAllFilter,
|
||||
ESAggTypeFilter,
|
||||
ESNestedAggregation,
|
||||
ESTermsFilter
|
||||
ESTermsFilter,
|
||||
} from '@openstapps/es-mapping-generator/src/types/aggregation';
|
||||
import {expect} from "chai";
|
||||
import {expect} from 'chai';
|
||||
import {
|
||||
isNestedAggregation,
|
||||
isBucketAggregation,
|
||||
isESTermsFilter,
|
||||
isESAggMatchAllFilter,
|
||||
isESNestedAggregation
|
||||
isESNestedAggregation,
|
||||
} from '../../../lib/storage/elasticsearch/types/guards';
|
||||
import {BucketAggregation, NestedAggregation} from '../../../src/storage/elasticsearch/types/elasticsearch';
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any,unicorn/no-null */
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* 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/>.
|
||||
*/
|
||||
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 message} from '@openstapps/core/test/resources/indexable/Message.1.json';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
@@ -43,6 +52,7 @@ describe('Elasticsearch', function () {
|
||||
const sandbox = sinon.createSandbox();
|
||||
|
||||
before(function () {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log('before');
|
||||
sandbox.stub(fs, 'readFileSync').returns('{}');
|
||||
});
|
||||
@@ -54,7 +64,7 @@ describe('Elasticsearch', function () {
|
||||
it('should provide custom elasticsearch URL if defined', function () {
|
||||
const customAddress = 'http://foo-address:9200';
|
||||
const restore = mockedEnv({
|
||||
ES_ADDR: customAddress
|
||||
ES_ADDR: customAddress,
|
||||
});
|
||||
|
||||
expect(Elasticsearch.getElasticsearchUrl()).to.be.equal(customAddress);
|
||||
@@ -64,7 +74,7 @@ describe('Elasticsearch', function () {
|
||||
|
||||
it('should provide local URL as fallback', function () {
|
||||
const restore = mockedEnv({
|
||||
ES_ADDR: undefined
|
||||
ES_ADDR: undefined,
|
||||
});
|
||||
|
||||
expect(Elasticsearch.getElasticsearchUrl()).to.match(/(https?:\/\/)?localhost(:\d+)?/);
|
||||
@@ -81,7 +91,7 @@ describe('Elasticsearch', function () {
|
||||
source: '',
|
||||
state: 'in progress',
|
||||
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 () {
|
||||
@@ -94,8 +104,9 @@ describe('Elasticsearch', function () {
|
||||
});
|
||||
|
||||
it('should provide index name from the provided data', function () {
|
||||
expect(Elasticsearch.getIndex(type as SCThingType, source, bulk))
|
||||
.to.be.equal(`stapps_${type.split(' ').join('_')}_${source}_${Elasticsearch.getIndexUID(bulk.uid)}`);
|
||||
expect(Elasticsearch.getIndex(type as SCThingType, source, bulk)).to.be.equal(
|
||||
`stapps_${type.split(' ').join('_')}_${source}_${Elasticsearch.getIndexUID(bulk.uid)}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -109,7 +120,7 @@ describe('Elasticsearch', 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 () {
|
||||
@@ -124,10 +135,12 @@ describe('Elasticsearch', function () {
|
||||
});
|
||||
|
||||
it('should work with common cases', function () {
|
||||
expect(Elasticsearch.removeAliasChars('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890', 'bulk-uid'))
|
||||
.to.be.equal('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890');
|
||||
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');
|
||||
expect(
|
||||
Elasticsearch.removeAliasChars('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890', 'bulk-uid'),
|
||||
).to.be.equal('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890');
|
||||
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 () {
|
||||
@@ -158,16 +171,16 @@ describe('Elasticsearch', function () {
|
||||
...configFile.internal,
|
||||
database: {
|
||||
name: 'foo',
|
||||
version: 123
|
||||
}
|
||||
}
|
||||
version: 123,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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 () {
|
||||
const error = Error('Foo Error');
|
||||
const error = new Error('Foo Error');
|
||||
const loggerErrorStub = sandbox.stub(Logger, 'error').resolves('foo');
|
||||
sandbox.stub(Client.prototype, 'on').yields(error);
|
||||
|
||||
@@ -185,7 +198,7 @@ describe('Elasticsearch', function () {
|
||||
expect(loggerLogStub.calledWith(fakeResponse)).to.be.false;
|
||||
|
||||
const restore = mockedEnv({
|
||||
'ES_DEBUG': 'true',
|
||||
ES_DEBUG: 'true',
|
||||
});
|
||||
new Elasticsearch(configFile);
|
||||
|
||||
@@ -208,8 +221,8 @@ describe('Elasticsearch', function () {
|
||||
monitoring: {
|
||||
actions: [],
|
||||
watchers: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const es = new Elasticsearch(config);
|
||||
@@ -225,8 +238,8 @@ describe('Elasticsearch', function () {
|
||||
monitoring: {
|
||||
actions: [],
|
||||
watchers: [],
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
const monitoringSetUpStub = sandbox.stub(Monitoring, 'setUp');
|
||||
|
||||
@@ -245,22 +258,21 @@ describe('Elasticsearch', function () {
|
||||
|
||||
beforeEach(function () {
|
||||
es = new Elasticsearch(configFile);
|
||||
// @ts-ignore
|
||||
es.client.indices = {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
getAlias: () => Promise.resolve({body: [{[oldIndex]: {aliases: {[SCThingType.Book]: {}}}}]}),
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
putTemplate: () => Promise.resolve({}),
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
create: () => Promise.resolve({}),
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
delete: () => Promise.resolve({}),
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
exists: () => Promise.resolve({}),
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
refresh: () => Promise.resolve({}),
|
||||
// @ts-ignore
|
||||
updateAliases: () => Promise.resolve({})
|
||||
// @ts-expect-error not assignable
|
||||
updateAliases: () => Promise.resolve({}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -340,13 +352,13 @@ describe('Elasticsearch', function () {
|
||||
},
|
||||
{
|
||||
remove: {index: oldIndex, alias: SCThingType.Book},
|
||||
}
|
||||
},
|
||||
];
|
||||
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
|
||||
sandbox.stub(es, 'aliasMap').value({
|
||||
[SCThingType.Book]: {
|
||||
[bulk.source]: oldIndex,
|
||||
}
|
||||
},
|
||||
});
|
||||
const refreshStub = sandbox.stub(es.client.indices, 'refresh');
|
||||
const updateAliasesStub = sandbox.stub(es.client.indices, 'updateAliases');
|
||||
@@ -357,11 +369,12 @@ describe('Elasticsearch', function () {
|
||||
await es.bulkUpdated(bulk);
|
||||
|
||||
expect(refreshStub.calledWith({index})).to.be.true;
|
||||
expect(updateAliasesStub.calledWith({
|
||||
expect(
|
||||
updateAliasesStub.calledWith({
|
||||
body: {
|
||||
actions: expectedRefreshActions
|
||||
}
|
||||
})
|
||||
actions: expectedRefreshActions,
|
||||
},
|
||||
}),
|
||||
).to.be.true;
|
||||
expect(deleteStub.called).to.be.true;
|
||||
});
|
||||
@@ -392,7 +405,7 @@ describe('Elasticsearch', function () {
|
||||
_index: '',
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: message as SCMessage
|
||||
_source: message as SCMessage,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [foundObject]}}});
|
||||
|
||||
@@ -420,7 +433,7 @@ describe('Elasticsearch', function () {
|
||||
_index: oldIndex,
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: message as SCMessage
|
||||
_source: message as SCMessage,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
|
||||
sandbox.stub(Elasticsearch, 'getIndex').returns(index);
|
||||
@@ -434,7 +447,7 @@ describe('Elasticsearch', function () {
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: message as SCMessage
|
||||
_source: message as SCMessage,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
|
||||
// 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 () {
|
||||
let caughtParam: any;
|
||||
let caughtParameter: any;
|
||||
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
|
||||
// @ts-ignore
|
||||
let createStub = sandbox.stub(es.client, 'create').callsFake((param) => {
|
||||
caughtParam = param;
|
||||
// @ts-expect-error call
|
||||
const createStub = sandbox.stub(es.client, 'create').callsFake(parameter => {
|
||||
caughtParameter = parameter;
|
||||
return Promise.resolve({body: {created: true}});
|
||||
});
|
||||
|
||||
await es.post(message as SCMessage, bulk);
|
||||
|
||||
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(),
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: message as SCMessage
|
||||
_source: message as SCMessage,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}});
|
||||
|
||||
return expect(es.put(object._source)).to.rejectedWith('exist');
|
||||
});
|
||||
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
it('should update the object if it already exists', async function () {
|
||||
let caughtParam: any;
|
||||
let caughtParameter: any;
|
||||
const object: ElasticsearchObject<SCMessage> = {
|
||||
_id: '',
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: message as SCMessage
|
||||
_source: message as SCMessage,
|
||||
};
|
||||
sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}});
|
||||
// @ts-ignore
|
||||
const stubUpdate = sandbox.stub(es.client, 'update').callsFake((params) => {
|
||||
caughtParam = params;
|
||||
// @ts-expect-error unused
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const stubUpdate = sandbox.stub(es.client, 'update').callsFake(parameters => {
|
||||
caughtParameter = parameters;
|
||||
return Promise.resolve({body: {created: true}});
|
||||
});
|
||||
|
||||
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(),
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: message as SCMessage
|
||||
_source: message as SCMessage,
|
||||
};
|
||||
const objectBook: ElasticsearchObject<SCBook> = {
|
||||
_id: '321',
|
||||
_index: getIndex(),
|
||||
_score: 0,
|
||||
_type: '',
|
||||
_source: book as SCBook
|
||||
_source: book as SCBook,
|
||||
};
|
||||
const fakeEsAggregations = {
|
||||
'@all': {
|
||||
@@ -537,40 +555,36 @@ describe('Elasticsearch', function () {
|
||||
buckets: [
|
||||
{
|
||||
key: 'person',
|
||||
doc_count: 13
|
||||
doc_count: 13,
|
||||
},
|
||||
{
|
||||
key: 'catalog',
|
||||
doc_count: 4
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
doc_count: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = {
|
||||
// @ts-ignore
|
||||
body: {
|
||||
took: 12,
|
||||
timed_out: false,
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
_shards: {},
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
hits: {
|
||||
hits: [
|
||||
objectMessage,
|
||||
objectBook,
|
||||
],
|
||||
total: 123
|
||||
hits: [objectMessage, objectBook],
|
||||
total: 123,
|
||||
},
|
||||
aggregations: fakeEsAggregations
|
||||
aggregations: fakeEsAggregations,
|
||||
},
|
||||
headers: {},
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
meta: {},
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
statusCode: {},
|
||||
// @ts-ignore
|
||||
warnings: {}
|
||||
// @ts-expect-error not assignable
|
||||
warnings: {},
|
||||
};
|
||||
let searchStub: sinon.SinonStub;
|
||||
before(function () {
|
||||
@@ -593,11 +607,11 @@ describe('Elasticsearch', function () {
|
||||
},
|
||||
{
|
||||
count: 4,
|
||||
key: 'catalog'
|
||||
}
|
||||
key: 'catalog',
|
||||
},
|
||||
],
|
||||
field: 'type',
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const {data, facets} = await es.search({});
|
||||
@@ -613,7 +627,7 @@ describe('Elasticsearch', function () {
|
||||
expect(pagination).to.be.eql({
|
||||
count: fakeSearchResponse.body!.hits.hits.length,
|
||||
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 () {
|
||||
const params: SCSearchQuery = {
|
||||
const parameters: SCSearchQuery = {
|
||||
query: 'mathematics',
|
||||
from: 30,
|
||||
size: 5,
|
||||
@@ -632,42 +646,37 @@ describe('Elasticsearch', function () {
|
||||
{
|
||||
type: 'ducet',
|
||||
order: 'desc',
|
||||
arguments:
|
||||
{
|
||||
field: 'name'
|
||||
}
|
||||
}
|
||||
arguments: {
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
],
|
||||
filter: {
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.AcademicEvent
|
||||
}
|
||||
}
|
||||
value: SCThingType.AcademicEvent,
|
||||
},
|
||||
},
|
||||
};
|
||||
const fakeResponse = {foo: 'bar'};
|
||||
const fakeBuildSortResponse = [fakeResponse];
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
sandbox.stub(query, 'buildQuery').returns(fakeResponse);
|
||||
// @ts-ignore
|
||||
sandbox.stub(query, 'buildSort').returns(fakeBuildSortResponse);
|
||||
|
||||
await es.search(params);
|
||||
await es.search(parameters);
|
||||
|
||||
sandbox.assert
|
||||
.calledWithMatch(searchStub,
|
||||
{
|
||||
body: {
|
||||
aggs: aggregations,
|
||||
query: fakeResponse,
|
||||
sort: fakeBuildSortResponse
|
||||
},
|
||||
from: params.from,
|
||||
index: Elasticsearch.getListOfAllIndices(),
|
||||
size: params.size,
|
||||
}
|
||||
);
|
||||
sandbox.assert.calledWithMatch(searchStub, {
|
||||
body: {
|
||||
aggs: aggregations,
|
||||
query: fakeResponse,
|
||||
sort: fakeBuildSortResponse,
|
||||
},
|
||||
from: parameters.from,
|
||||
index: Elasticsearch.getListOfAllIndices(),
|
||||
size: parameters.size,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -18,7 +19,8 @@ import {
|
||||
SCMonitoringConfiguration,
|
||||
SCMonitoringLogAction,
|
||||
SCMonitoringMailAction,
|
||||
SCMonitoringWatcher, SCThings
|
||||
SCMonitoringWatcher,
|
||||
SCThings,
|
||||
} from '@openstapps/core';
|
||||
import {Logger} from '@openstapps/logger';
|
||||
import {SearchResponse} from 'elasticsearch';
|
||||
@@ -26,7 +28,7 @@ import {MailQueue} from '../../../src/notification/mail-queue';
|
||||
import {setUp} from '../../../src/storage/elasticsearch/monitoring';
|
||||
|
||||
import {getTransport} from '../../common';
|
||||
import { expect } from 'chai';
|
||||
import {expect} from 'chai';
|
||||
import sinon from 'sinon';
|
||||
import cron from 'node-cron';
|
||||
|
||||
@@ -35,16 +37,15 @@ describe('Monitoring', async function () {
|
||||
const logAction: SCMonitoringLogAction = {
|
||||
message: 'Foo monitoring message',
|
||||
prefix: 'Backend Monitoring',
|
||||
type: 'log'
|
||||
type: 'log',
|
||||
};
|
||||
const mailAction: SCMonitoringMailAction = {
|
||||
message: 'Bar monitoring message',
|
||||
recipients: ['xyz@xyz.com'],
|
||||
subject: 'Backend Monitoring',
|
||||
type: 'mail'
|
||||
type: 'mail',
|
||||
};
|
||||
let transport: any;
|
||||
// @ts-ignore
|
||||
let mailQueue: any;
|
||||
beforeEach(async function () {
|
||||
transport = getTransport(true);
|
||||
@@ -55,52 +56,52 @@ describe('Monitoring', async function () {
|
||||
sandbox.restore();
|
||||
});
|
||||
// const sandbox = sinon.createSandbox();
|
||||
let cronScheduleStub: sinon.SinonStub
|
||||
let cronScheduleStub: sinon.SinonStub;
|
||||
const minLengthWatcher: SCMonitoringWatcher = {
|
||||
actions: [logAction, mailAction],
|
||||
conditions: [
|
||||
{
|
||||
length: 10,
|
||||
type: 'MinimumLength'
|
||||
}
|
||||
type: 'MinimumLength',
|
||||
},
|
||||
],
|
||||
name: 'foo watcher',
|
||||
query: {foo: 'bar'},
|
||||
triggers: [
|
||||
{
|
||||
executionTime: 'monthly',
|
||||
name: 'beginning of month'
|
||||
name: 'beginning of month',
|
||||
},
|
||||
{
|
||||
executionTime: 'daily',
|
||||
name: 'every night'
|
||||
}
|
||||
]
|
||||
name: 'every night',
|
||||
},
|
||||
],
|
||||
};
|
||||
const maxLengthWatcher: SCMonitoringWatcher = {
|
||||
actions: [logAction, mailAction],
|
||||
conditions: [
|
||||
{
|
||||
length: 30,
|
||||
type: 'MaximumLength'
|
||||
}
|
||||
type: 'MaximumLength',
|
||||
},
|
||||
],
|
||||
name: 'foo watcher',
|
||||
query: {bar: 'foo'},
|
||||
triggers: [
|
||||
{
|
||||
executionTime: 'hourly',
|
||||
name: 'every hour'
|
||||
name: 'every hour',
|
||||
},
|
||||
{
|
||||
executionTime: 'weekly',
|
||||
name: 'every week'
|
||||
name: 'every week',
|
||||
},
|
||||
]
|
||||
],
|
||||
};
|
||||
const monitoringConfig: SCMonitoringConfiguration = {
|
||||
actions: [logAction, mailAction],
|
||||
watchers: [minLengthWatcher, maxLengthWatcher]
|
||||
watchers: [minLengthWatcher, maxLengthWatcher],
|
||||
};
|
||||
|
||||
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 () {
|
||||
const fakeSearchResponse: Partial<ApiResponse<SearchResponse<SCThings>>> = {
|
||||
// @ts-ignore
|
||||
body: {
|
||||
took: 12,
|
||||
timed_out: false,
|
||||
// @ts-ignore
|
||||
_shards: {},
|
||||
// @ts-ignore
|
||||
hits: {
|
||||
total: 123
|
||||
},
|
||||
took: 12,
|
||||
timed_out: false,
|
||||
// @ts-expect-error not assignable
|
||||
_shards: {},
|
||||
// @ts-expect-error not assignable
|
||||
hits: {
|
||||
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 mailQueueSpy = sinon.spy(mailQueue, 'push');
|
||||
cronScheduleStub.yields();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any,unicorn/no-null */
|
||||
/*
|
||||
* Copyright (C) 2020 StApps
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@@ -15,11 +16,13 @@
|
||||
*/
|
||||
import {
|
||||
SCConfigFile,
|
||||
SCSearchBooleanFilter, SCSearchDateRangeFilter,
|
||||
SCSearchFilter, SCSearchNumericRangeFilter,
|
||||
SCSearchBooleanFilter,
|
||||
SCSearchDateRangeFilter,
|
||||
SCSearchFilter,
|
||||
SCSearchNumericRangeFilter,
|
||||
SCSearchQuery,
|
||||
SCSearchSort,
|
||||
SCThingType
|
||||
SCThingType,
|
||||
} from '@openstapps/core';
|
||||
import {expect} from 'chai';
|
||||
import {
|
||||
@@ -35,7 +38,12 @@ import {
|
||||
ScriptSort,
|
||||
} from '../../../src/storage/elasticsearch/types/elasticsearch';
|
||||
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('buildBooleanFilter', function () {
|
||||
@@ -47,21 +55,21 @@ describe('Query', function () {
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Catalog
|
||||
}
|
||||
value: SCThingType.Catalog,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Building
|
||||
}
|
||||
}
|
||||
]
|
||||
value: SCThingType.Building,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
type: 'boolean'
|
||||
type: 'boolean',
|
||||
};
|
||||
const booleanFilters: { [key: string]: SCSearchBooleanFilter } = {
|
||||
const booleanFilters: {[key: string]: SCSearchBooleanFilter} = {
|
||||
and: booleanFilter,
|
||||
or: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'or'}},
|
||||
not: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'not'}},
|
||||
@@ -69,14 +77,14 @@ describe('Query', function () {
|
||||
const expectedEsFilters: Array<ESTermFilter> = [
|
||||
{
|
||||
term: {
|
||||
'type.raw': 'catalog'
|
||||
}
|
||||
'type.raw': 'catalog',
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'type.raw': 'building'
|
||||
}
|
||||
}
|
||||
'type.raw': 'building',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it('should create appropriate elasticsearch "and" filter argument', function () {
|
||||
@@ -100,7 +108,7 @@ describe('Query', function () {
|
||||
});
|
||||
|
||||
describe('buildQuery', function () {
|
||||
const params: SCSearchQuery = {
|
||||
const parameters: SCSearchQuery = {
|
||||
query: 'mathematics',
|
||||
from: 30,
|
||||
size: 5,
|
||||
@@ -108,27 +116,25 @@ describe('Query', function () {
|
||||
{
|
||||
type: 'ducet',
|
||||
order: 'desc',
|
||||
arguments:
|
||||
{
|
||||
field: 'name'
|
||||
}
|
||||
arguments: {
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'ducet',
|
||||
order: 'desc',
|
||||
arguments:
|
||||
{
|
||||
field: 'categories'
|
||||
}
|
||||
arguments: {
|
||||
field: 'categories',
|
||||
},
|
||||
},
|
||||
],
|
||||
filter: {
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.AcademicEvent
|
||||
}
|
||||
}
|
||||
value: SCThingType.AcademicEvent,
|
||||
},
|
||||
},
|
||||
};
|
||||
let esConfig: ElasticsearchConfig = {
|
||||
name: 'elasticsearch',
|
||||
@@ -138,7 +144,7 @@ describe('Query', function () {
|
||||
queryType: 'dis_max',
|
||||
matchBoosting: 1.3,
|
||||
fuzziness: 'AUTO',
|
||||
cutoffFrequency: 0.0,
|
||||
cutoffFrequency: 0,
|
||||
tieBreaker: 0,
|
||||
},
|
||||
};
|
||||
@@ -147,59 +153,59 @@ describe('Query', function () {
|
||||
queryType: 'dis_max',
|
||||
matchBoosting: 1.3,
|
||||
fuzziness: 'AUTO',
|
||||
cutoffFrequency: 0.0,
|
||||
cutoffFrequency: 0,
|
||||
tieBreaker: 0,
|
||||
};
|
||||
const config: SCConfigFile = {
|
||||
...configFile
|
||||
...configFile,
|
||||
};
|
||||
beforeEach(function () {
|
||||
esConfig = {
|
||||
name: 'elasticsearch',
|
||||
version: '123'
|
||||
version: '123',
|
||||
};
|
||||
});
|
||||
|
||||
// TODO: check parts of received elasticsearch query for each test case
|
||||
|
||||
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 () {
|
||||
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 () {
|
||||
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 () {
|
||||
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 () {
|
||||
// @ts-ignore
|
||||
// @ts-expect-error not assignable
|
||||
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 () {
|
||||
const searchFilters: { [key: string]: SCSearchFilter } = {
|
||||
const searchFilters: {[key: string]: SCSearchFilter} = {
|
||||
value: {
|
||||
type: 'value',
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Dish
|
||||
}
|
||||
value: SCThingType.Dish,
|
||||
},
|
||||
},
|
||||
distance: {
|
||||
type: 'distance',
|
||||
@@ -207,7 +213,7 @@ describe('Query', function () {
|
||||
distance: 1000,
|
||||
field: 'geo',
|
||||
position: [50.123, 8.123],
|
||||
}
|
||||
},
|
||||
},
|
||||
geoPoint: {
|
||||
type: 'geo',
|
||||
@@ -218,9 +224,9 @@ describe('Query', function () {
|
||||
coordinates: [
|
||||
[50.123, 8.123],
|
||||
[50.123, 8.123],
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
geoShape: {
|
||||
type: 'geo',
|
||||
@@ -232,9 +238,9 @@ describe('Query', function () {
|
||||
coordinates: [
|
||||
[50.123, 8.123],
|
||||
[50.123, 8.123],
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
boolean: {
|
||||
type: 'boolean',
|
||||
@@ -246,16 +252,16 @@ describe('Query', function () {
|
||||
arguments: {
|
||||
field: 'type',
|
||||
value: SCThingType.Dish,
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'availability',
|
||||
arguments: {
|
||||
field: 'offers.availabilityRange'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
field: 'offers.availabilityRange',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -263,8 +269,8 @@ describe('Query', function () {
|
||||
const filter = buildFilter(searchFilters.value);
|
||||
const expectedFilter: ESTermFilter = {
|
||||
term: {
|
||||
'type.raw': SCThingType.Dish
|
||||
}
|
||||
'type.raw': SCThingType.Dish,
|
||||
},
|
||||
};
|
||||
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -277,28 +283,30 @@ describe('Query', function () {
|
||||
range: {
|
||||
price: {
|
||||
relation: undefined,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const rawFilter: SCSearchNumericRangeFilter = {
|
||||
type: 'numeric range',
|
||||
arguments: {
|
||||
bounds: {},
|
||||
field: 'price'
|
||||
}
|
||||
field: 'price',
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
const setBound = (location: 'upperBound' | 'lowerBound', bound: string | null) => {
|
||||
let out: number | null = null;
|
||||
if (bound != null) {
|
||||
if (bound != undefined) {
|
||||
out = Math.random();
|
||||
rawFilter.arguments.bounds[location] = {
|
||||
mode: bound as 'inclusive' | 'exclusive',
|
||||
limit: out,
|
||||
};
|
||||
// @ts-ignore implicit any
|
||||
expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out;
|
||||
expectedFilter.range.price[
|
||||
`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`
|
||||
] = out;
|
||||
}
|
||||
};
|
||||
setBound('upperBound', upperMode);
|
||||
@@ -307,9 +315,9 @@ describe('Query', function () {
|
||||
const filter = buildFilter(rawFilter) as ESNumericRangeFilter;
|
||||
expect(filter).to.deep.equal(expectedFilter);
|
||||
for (const bound of ['g', 'l']) {
|
||||
// @ts-ignore implicit any
|
||||
// @ts-expect-error implicit any
|
||||
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';
|
||||
|
||||
// only one should exist at the same time
|
||||
@@ -328,8 +336,8 @@ describe('Query', function () {
|
||||
format: 'thisIsADummyFormat',
|
||||
time_zone: 'thisIsADummyTimeZone',
|
||||
relation: 'testRelation' as any,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const rawFilter: SCSearchDateRangeFilter = {
|
||||
@@ -340,19 +348,20 @@ describe('Query', function () {
|
||||
relation: 'testRelation' as any,
|
||||
format: 'thisIsADummyFormat',
|
||||
timeZone: 'thisIsADummyTimeZone',
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const setBound = (location: 'upperBound' | 'lowerBound', bound: string | null) => {
|
||||
let out: string | null = null;
|
||||
if (bound != null) {
|
||||
if (bound != undefined) {
|
||||
out = `${location} ${bound} ${upperMode} ${lowerMode}`;
|
||||
rawFilter.arguments.bounds[location] = {
|
||||
mode: bound as 'inclusive' | 'exclusive',
|
||||
limit: out,
|
||||
};
|
||||
// @ts-ignore implicit any
|
||||
expectedFilter.range.price[`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`] = out;
|
||||
expectedFilter.range.price[
|
||||
`${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}`
|
||||
] = out;
|
||||
}
|
||||
};
|
||||
setBound('upperBound', upperMode);
|
||||
@@ -361,9 +370,9 @@ describe('Query', function () {
|
||||
const filter = buildFilter(rawFilter) as ESNumericRangeFilter;
|
||||
expect(filter).to.deep.equal(expectedFilter);
|
||||
for (const bound of ['g', 'l']) {
|
||||
// @ts-ignore implicit any
|
||||
// @ts-expect-error implicit any
|
||||
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';
|
||||
|
||||
// only one should exist at the same time
|
||||
@@ -390,7 +399,7 @@ describe('Query', function () {
|
||||
'offers.availabilityRange': {
|
||||
gte: `test||/${scope}`,
|
||||
lt: `test||+1${scope}/${scope}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -411,7 +420,7 @@ describe('Query', function () {
|
||||
'offers.availabilityRange': {
|
||||
gte: 'test||/s',
|
||||
lt: 'test||+1s/s',
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -432,7 +441,7 @@ describe('Query', function () {
|
||||
'offers.availabilityRange': {
|
||||
gte: `test||/d`,
|
||||
lt: `test||+1d/d`,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -452,7 +461,7 @@ describe('Query', function () {
|
||||
'offers.availabilityRange': {
|
||||
gte: `now/d`,
|
||||
lt: `now+1d/d`,
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -463,12 +472,12 @@ describe('Query', function () {
|
||||
const filter = buildFilter(searchFilters.distance);
|
||||
const expectedFilter: ESGeoDistanceFilter = {
|
||||
geo_distance: {
|
||||
distance: '1000m',
|
||||
'distance': '1000m',
|
||||
'geo.point.coordinates': {
|
||||
lat: 8.123,
|
||||
lon: 50.123
|
||||
}
|
||||
}
|
||||
lon: 50.123,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -488,24 +497,24 @@ describe('Query', function () {
|
||||
type: 'envelope',
|
||||
coordinates: [
|
||||
[50.123, 8.123],
|
||||
[50.123, 8.123]
|
||||
]
|
||||
[50.123, 8.123],
|
||||
],
|
||||
},
|
||||
},
|
||||
ignore_unmapped: true,
|
||||
}
|
||||
'ignore_unmapped': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
geo_bounding_box: {
|
||||
'geo.point.coordinates': {
|
||||
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);
|
||||
@@ -521,12 +530,12 @@ describe('Query', function () {
|
||||
type: 'envelope',
|
||||
coordinates: [
|
||||
[50.123, 8.123],
|
||||
[50.123, 8.123]
|
||||
]
|
||||
[50.123, 8.123],
|
||||
],
|
||||
},
|
||||
},
|
||||
ignore_unmapped: true,
|
||||
}
|
||||
'ignore_unmapped': true,
|
||||
},
|
||||
};
|
||||
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -540,8 +549,8 @@ describe('Query', function () {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'type.raw': 'dish'
|
||||
}
|
||||
'type.raw': 'dish',
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
@@ -549,13 +558,13 @@ describe('Query', function () {
|
||||
gte: 'now/s',
|
||||
lt: 'now+1s/s',
|
||||
relation: 'intersects',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [],
|
||||
should: []
|
||||
}
|
||||
should: [],
|
||||
},
|
||||
};
|
||||
|
||||
expect(filter).to.be.eql(expectedFilter);
|
||||
@@ -568,7 +577,7 @@ describe('Query', function () {
|
||||
type: 'ducet',
|
||||
order: 'desc',
|
||||
arguments: {
|
||||
field: 'name'
|
||||
field: 'name',
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -576,14 +585,14 @@ describe('Query', function () {
|
||||
order: 'desc',
|
||||
arguments: {
|
||||
field: 'name',
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'distance',
|
||||
order: 'desc',
|
||||
arguments: {
|
||||
field: 'geo',
|
||||
position: [8.123, 50.123]
|
||||
position: [8.123, 50.123],
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -592,35 +601,35 @@ describe('Query', function () {
|
||||
arguments: {
|
||||
universityRole: 'student',
|
||||
field: 'offers.prices',
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
||||
let sorts: Array<ESGenericSort | ESGeoDistanceSort | ScriptSort> = [];
|
||||
const expectedSorts: { [key: string]: ESGenericSort | ESGeoDistanceSort | ScriptSort } = {
|
||||
const expectedSorts: {[key: string]: ESGenericSort | ESGeoDistanceSort | ScriptSort} = {
|
||||
ducet: {
|
||||
'name.sort': 'desc'
|
||||
'name.sort': 'desc',
|
||||
},
|
||||
generic: {
|
||||
'name': 'desc'
|
||||
name: 'desc',
|
||||
},
|
||||
distance: {
|
||||
_geo_distance: {
|
||||
mode: 'avg',
|
||||
order: 'desc',
|
||||
unit: 'm',
|
||||
'mode': 'avg',
|
||||
'order': 'desc',
|
||||
'unit': 'm',
|
||||
'geo.point.coordinates': {
|
||||
lat: 50.123,
|
||||
lon: 8.123
|
||||
}
|
||||
}
|
||||
lon: 8.123,
|
||||
},
|
||||
},
|
||||
},
|
||||
price: {
|
||||
_script: {
|
||||
order: 'asc',
|
||||
script: '\n // foo price sort script',
|
||||
type: 'number'
|
||||
}
|
||||
}
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
};
|
||||
before(function () {
|
||||
sorts = buildSort(searchSCSearchSort);
|
||||
@@ -641,7 +650,10 @@ describe('Query', function () {
|
||||
it('should build price sort', function () {
|
||||
const priceSortNoScript = {
|
||||
...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);
|
||||
});
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
},
|
||||
"exclude": [
|
||||
"./config/",
|
||||
"./test/"
|
||||
"./test"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "./node_modules/@openstapps/configuration/tslint.json"
|
||||
}
|
||||
Reference in New Issue
Block a user